.claude/skills/write-tests/SKILL.md
Always use this skill before writing any test code in the Toasty repository
npx skillsauth add tokio-rs/toasty write-testsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Load this skill when writing or editing tests anywhere in this project.
Prefer integration tests over lib (inline #[cfg(test)]) tests. Choose the location that best matches what is being tested:
| What you're testing | Where to put it |
|---|---|
| Full-stack behavior requiring a real DB | crates/toasty-driver-integration-suite/src/tests/ |
| Full-stack behavior with no operational DB needed | tests/tests/ |
| Public API of toasty-core | crates/toasty-core/tests/ |
| SQL generation in toasty-sql | crates/toasty-sql/tests/ |
| Complex internal logic (e.g., query simplification) | Inline #[cfg(test)] in the relevant source file |
Never write tests for macro codegen internals directly. Test macro behavior at the user level (define a model, use the generated code) in the integration suite or tests/tests/.
Never write per-driver tests. Any test that exercises a real DB goes in toasty-driver-integration-suite so third-party driver authors can run it too. The suite is instantiated per driver in tests/tests/{sqlite,mysql,postgresql,dynamodb}.rs.
create! for data setup: Unless the test is explicitly testing the create builder itself, use the toasty::create! macro to insert data. It is more concise and reads like a struct literal. For example: let user = toasty::create!(User { name: "Alice" }).exec(&db).await?; instead of let user = User::create().name("Alice").exec(&db).await?;.Tests in crates/toasty-driver-integration-suite/src/tests/ run against every supported driver. New files are auto-discovered at compile time via generate_test_registry!.
use crate::prelude::*;
#[driver_test(id(ID))]
pub async fn my_test(t: &mut Test) -> Result<()> {
// 1. Define model(s) inline
#[derive(Debug, toasty::Model)]
struct Foo {
#[key]
#[auto]
id: ID, // placeholder: rewritten to u64 and uuid::Uuid by macro
name: String,
}
// 2. Setup database
let db = t.setup_db(models!(Foo)).await;
// 3. Exercise behavior
let foo = toasty::create!(Foo { name: "hi" }).exec(&db).await?;
// 4. Assert
assert_eq!(foo.name, "hi");
Ok(())
}
The id(ID) argument names a placeholder identifier — ID by convention — that the macro replaces with a concrete type everywhere it appears as a type in the function body. The function is emitted twice: once with every ID replaced by u64, once with every ID replaced by uuid::Uuid. Both variants are wrapped with #[tokio::test] and each gets an isolated table prefix that is cleaned up after the test runs.
This is why FK fields are also typed ID (e.g. user_id: ID): the substitution is textual across the whole function, so the FK gets the same concrete type as the primary key it references.
Attribute forms:
#[driver_test(id(ID))] — expands to two variants (u64 and uuid::Uuid); only use when it genuinely makes sense to test both ID types#[driver_test(id(ID), requires(sql))] — ID expansion with a capability gate#[driver_test(requires(native_decimal))] — no ID expansion, just a capability gate#[driver_test] — single variant, no capability gate, use a concrete ID type in the modelChoosing an ID type: Only use id(ID) expansion when running against both u64 and uuid::Uuid adds meaningful coverage. Otherwise pick the type that fits the test:
uuid::Uuid when the test must run on non-SQL drivers (DynamoDB does not support auto-increment)u64 when testing auto-increment behavior specificallyAlways use requires(...) to gate tests on capabilities. Never use runtime if !t.capability().foo { return Ok(()); } — that is what the macro is for.
crates/toasty-driver-integration-suite/src/prelude.rs re-exports everything test files need. Start new test files with use crate::prelude::*;.
// Build a Db::Builder registering a set of models
models!(Foo, Bar, Baz)
// Look up a TableId from the schema (for driver-op assertions)
table_id(&db, "foos") // -> TableId
column(&db, "foos", "name") // -> ColumnId
columns(&db, "foos", &["id", "name"]) // -> Vec<ColumnId>
t.setup_db(models!(Foo)).await // create DB, push schema, return Db
t.capability() // &'static Capability (sql, auto_increment, …)
t.log().clear() // clear driver operation log
t.log().pop() -> (Operation, Response) // inspect what the driver received
t.log().is_empty() // assert no extra driver calls
Capture and assert on the raw operation the engine sent to the driver:
t.log().clear();
foo.update().name("new").exec(&db).await?;
let (op, resp) = t.log().pop();
if t.capability().sql {
assert_struct!(op, Operation::QuerySql(_ {
stmt: Statement::Update(_ {
target: UpdateTarget::Table(== foo_table_id),
assignments: #{ 1: _ { expr: == "new", .. } },
..
}),
..
}));
} else {
assert_struct!(op, Operation::UpdateByKey(_ {
table: == foo_table_id,
keys.len(): 1,
assignments: #{ 1: _ { expr: == "new", .. } },
..
}));
}
assert_struct!(resp, _ { rows: Rows::Count(1), .. });
assert!(t.log().is_empty());
Use whichever form produces fewer characters and the same test coverage.
// assert_eq! wins — single field
assert_eq!(foo.name, "hello");
// assert_struct! wins — multiple fields in one call
assert_struct!(foo, _ { name: "hello", age: 30 });
// vs. assert_eq!(foo.name, "hello"); assert_eq!(foo.age, 30);
// assert_eq! wins — you have the whole value
assert_eq!(result, Foo::default());
Patterns compose freely. Use _ for wildcard struct (no import needed), .. for partial match:
assert_struct!(val, _ { field: "text", count: > 0, flag: true, .. });
assert_struct!(val, _ { opt: Some(42), res: Ok("ok"), .. });
assert_struct!(val, _ { items: [1, 2, ..], tags: #("a", "b", ..), .. });
assert_struct!(val, _ { nested.child.x: >= 0, .. }); // dot-path shorthand
assert_struct!(val, _ { items.len(): 3, .. }); // method call
Operator patterns at leaves avoid importing types:
// Instead of: field: SomeEnum::Variant(42)
// Write: field: == expected_var
Full pattern grammar: ~/.cargo/registry/src/index.crates.io-*/assert-struct-*/LLM.txt
rustfmt does not format assert_struct! bodies. You are responsible for formatting them manually, using the same rules rustfmt would apply to equivalent Rust code.
One-liner: If the entire call fits on one line within the column limit (~100 chars), keep it on one line.
assert_struct!(resp, _ { rows: Rows::Count(1), .. });
Multi-line: When breaking across lines, treat the macro body like a struct literal or match arm — indent the contents by 4 spaces relative to the assert_struct! call, and put the closing }); on its own line.
assert_struct!(op, Operation::QuerySql(_ {
stmt: Statement::Update(_ {
target: UpdateTarget::Table(== foo_table_id),
assignments: #{ 1: _ { expr: == "new", .. } },
..
}),
..
}));
When the macro syntax differs from Rust (e.g. #{ .. } for map patterns, == expr leaf operators, dot-path keys like items.len():), use your best judgment to apply equivalent rustfmt rules: align commas consistently, indent nested levels, and never leave trailing whitespace.
assert_err!(expr) // asserts Err, returns the error value
assert_ok!(expr) // asserts Ok, returns the inner value
assert_none!(expr) // asserts None
assert_unique!(slice) // asserts all elements are distinct
Common attributes used in inline test model definitions:
#[derive(Debug, toasty::Model)]
struct User {
#[key] #[auto] id: ID, // primary key, auto-generated
#[unique] email: String, // generates get_by_email / filter_by_email
#[index] name: String, // generates filter_by_name
#[default(0)] score: i64, // default value on create
#[has_many] todos: toasty::HasMany<Todo>,
}
#[derive(Debug, toasty::Model)]
struct Todo {
#[key] #[auto] id: ID,
#[index] user_id: ID,
#[belongs_to(key = user_id, references = id)]
user: toasty::BelongsTo<User>,
}
// Composite key
#[derive(Debug, toasty::Model)]
#[key(partition = team, local = name)]
struct Player { team: String, name: String }
// Get by PK (errors if missing)
let u = User::get_by_id(&db, &id).await?;
// Filter (returns cursor)
let users = User::filter_by_name("Alice").collect::<Vec<_>>(&db).await?;
// First (returns Option)
let opt = User::filter_by_email("[email protected]").first(&db).await?;
// Delete
u.delete(&db).await?;
User::filter_by_id(id).delete(&db).await?;
// Update (instance — mutates in memory too)
u.update().name("Bob").exec(&db).await?;
// Update (query-based)
User::filter_by_id(id).update().name("Bob").exec(&db).await?;
// Create with nested association
User::create().name("Alice").todo(Todo::create().title("T1")).exec(&db).await?;
documentation
Author or edit documentation for the Toasty project, following project writing conventions
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.