use jerrycan::platform::design::Design;
use jerrycan::platform::testgen;
const GOLDEN: &str = include_str!("../../../conformance/designs/todo-api.design.json");
fn golden(db: bool) -> Design {
let mut v: serde_json::Value = serde_json::from_str(GOLDEN).unwrap();
if db {
v["dependencies"] = serde_json::json!(["db"]);
}
serde_json::from_value(v).unwrap()
}
#[test]
fn memory_mode_probe_body_keeps_the_identity_fk() {
const IDENTITY_FK: &str = r#"{
"name": "notes-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "notes",
"entities": [
{ "name": "User", "fields": [{ "name": "email", "type": "string" }] },
{ "name": "Note",
"belongs_to": [{ "entity": "User", "on_delete": "cascade" }],
"fields": [{ "name": "body", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_note", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Note" },
"success": { "status": 201, "entity": "Note" } }
]
}]
}"#;
let mut v: serde_json::Value = serde_json::from_str(IDENTITY_FK).unwrap();
let mem: Design = serde_json::from_value(v.clone()).unwrap();
assert!(!mem.wants_db());
let mem_gen = testgen::acceptance_rs(&mem, &mem.modules[0]);
assert!(
mem_gen.contains("\"user_id\": 1"),
"memory-mode probe body must keep the identity fk (no DTO to drop it): {mem_gen}"
);
v["dependencies"] = serde_json::json!(["db", "auth"]);
let dbm: Design = serde_json::from_value(v).unwrap();
let db_gen = testgen::acceptance_rs(&dbm, &dbm.modules[0]);
assert!(
!db_gen.contains("\"user_id\":"),
"db-mode probe body omits the server-owned identity fk: {db_gen}"
);
}
#[test]
fn memory_mode_tests_cover_success_and_listed_errors() {
let design = golden(false);
let module = &design.modules[0]; let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("GENERATED by jerrycan gen-tests"),
"tool-owned banner"
);
assert!(
generated.contains("use route_todos::module;"),
"{generated}"
);
for expected in [
"async fn list_todos_returns_200",
"async fn create_todo_returns_201",
"async fn show_todo_returns_200",
"async fn delete_todo_returns_204",
"async fn list_comments_returns_200",
"async fn create_comment_returns_201",
] {
assert!(
generated.contains(expected),
"missing {expected}\n{generated}"
);
}
assert!(
generated.contains("async fn show_todo_missing_id_is_404"),
"{generated}"
);
assert!(generated.contains("/todos/999999"), "{generated}");
assert!(generated.contains("post_json(\"/todos/\""), "{generated}");
assert!(!generated.contains("jerrycan::db"), "{generated}");
assert_eq!(
testgen::test_count(&generated),
8,
"6 success + 2 listed 404s"
);
}
#[test]
fn db_mode_preamble_migrates_an_in_memory_database() {
let design = golden(true);
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("Db::connect(\"sqlite::memory:\")"),
"{generated}"
);
assert!(
generated.contains("include_str!(\"../migrations/sqlite/0001_create_tables.sql\")"),
"{generated}"
);
assert!(generated.contains(".extend(db)"), "{generated}");
}
#[test]
fn module_testapp_migrates_the_full_workspace_schema() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "shop-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [
{ "name": "products",
"entities": [{ "name": "Product", "fields": [
{ "name": "sku", "type": "string" } ]}],
"endpoints": [{ "operation_id": "list_products", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Product", "list": true } }] },
{ "name": "orders",
"entities": [{ "name": "Order", "fields": [
{ "name": "total", "type": "integer" } ]}],
"endpoints": [{ "operation_id": "list_orders", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Order", "list": true } }] }
]
}))
.unwrap();
let orders = design.modules.iter().find(|m| m.name == "orders").unwrap();
let generated = testgen::acceptance_rs(&design, orders);
assert!(
generated.contains("include_str!(\"../migrations/sqlite/0001_create_tables.sql\")"),
"orders TestApp migrates its own tables: {generated}"
);
assert!(
generated
.contains("include_str!(\"../../products/migrations/sqlite/0001_create_tables.sql\")"),
"orders TestApp must also migrate the products module's tables: {generated}"
);
}
#[test]
fn unsupported_error_cases_become_an_agent_todo_comment() {
let mut design = golden(false);
design.modules[0].endpoints[1]
.errors
.push(
serde_json::from_value(serde_json::json!({
"status": 409,
"code": "JC0409",
"when": "duplicate title",
}))
.unwrap(),
);
let generated = testgen::acceptance_rs(&design, &design.modules[0].clone());
assert!(
generated.contains("// AGENT TODO: design lists 409 (duplicate title)"),
"{generated}"
);
}
#[test]
fn public_endpoints_get_no_401_test_and_no_cookie() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "auth-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "accounts",
"entities": [{ "name": "User", "fields": [
{ "name": "email", "type": "string" },
{ "name": "password", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "register", "method": "POST", "path": "/register",
"public": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" } },
{ "operation_id": "create_account", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" } }
]
}]
}))
.unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("async fn register_returns_201"),
"public route still gets a success test: {generated}"
);
assert!(
!generated.contains("register_without_auth_is_401"),
"public route must NOT get a 401 test: {generated}"
);
assert!(
generated.contains("t.post_json(\"/accounts/register\""),
"public route request must carry no cookie: {generated}"
);
assert!(
generated.contains("create_account_without_auth_is_401"),
"a guarded route still gets its 401 test: {generated}"
);
assert!(
generated.contains("t.post_json_with(\"/accounts/\""),
"a guarded route still threads the credential: {generated}"
);
assert!(
generated.contains("(\"authorization\", &test_cookie())"),
"jwt guarded request threads the Authorization header, not a cookie: {generated}"
);
assert!(
generated.contains("jerrycan::auth::jwt::encode(&shared::SessionUser")
&& generated.contains("auth.jwt_key())")
&& generated.contains("format!(\"Bearer {token}\")"),
"jwt preamble mints a Bearer token via jwt::encode + jwt_key: {generated}"
);
assert!(
!generated.contains("jerrycan_session=") && !generated.contains("(\"cookie\","),
"jwt model must not mint or thread a session cookie: {generated}"
);
}
#[test]
fn tenancy_module_tests_seed_membership_and_provide_the_guard() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let leads = design
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let generated = testgen::acceptance_rs(&design, leads);
assert!(
generated.contains(".provide_dep(shared::tenant)"),
"{generated}"
);
assert!(
generated.contains(
"include_str!(\"../../workspaces/migrations/sqlite/0001_create_tables.sql\")"
),
"tenant migration cross-included: {generated}"
);
assert!(
generated.contains("INSERT INTO \\\"workspaces\\\"") && generated.contains("'trial'"),
"tenant row seeded with a valid enum value: {generated}"
);
assert!(
generated.contains(
"INSERT INTO \\\"workspace_members\\\" (user_id, workspace_id, role) VALUES (1, 1, 'owner')"
),
"membership row seeded for user 1: {generated}"
);
assert!(
generated.contains("use jerrycan::db::sea_orm::ConnectionTrait;"),
"{generated}"
);
let workspaces = design
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let ws_gen = testgen::acceptance_rs(&design, workspaces);
let app_fn = ws_gen
.split("async fn app()")
.nth(1)
.expect("app() present")
.split("#[tokio::test]")
.next()
.unwrap();
assert!(
!app_fn.contains(".provide_dep(shared::tenant)") && !app_fn.contains("workspace_members"),
"the tenant module's regular app() must neither seed membership nor provide the guard: {app_fn}"
);
let member_fn = ws_gen
.split("async fn member_app()")
.nth(1)
.expect("member_app() present (#107)")
.split("#[tokio::test]")
.next()
.unwrap();
assert!(
member_fn.contains(".provide_dep(shared::tenant)")
&& member_fn.contains("INSERT INTO \\\"workspace_members\\\""),
"member_app() seeds membership and provides the guard itself: {member_fn}"
);
}
#[test]
fn generated_request_body_uses_a_declared_enum_value_not_the_placeholder() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let users = design
.modules
.iter()
.find(|m| m.name == "users")
.expect("users module");
let generated = testgen::acceptance_rs(&design, users);
assert!(
generated.contains("\"role\": \"admin\""),
"enum field uses its first declared value: {generated}"
);
assert!(
!generated.contains("\"role\": \"test-value\""),
"enum field must NOT use the placeholder (trips the CHECK): {generated}"
);
assert!(
generated.contains("\"email\": \"test-value\""),
"non-enum string keeps the placeholder: {generated}"
);
}
#[test]
fn tenant_owned_fixtures_carry_the_foreign_key() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let leads = design
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let generated = testgen::acceptance_rs(&design, leads);
assert!(
generated.contains("\"workspace_id\": 1"),
"Lead fixture bodies must carry workspace_id: 1: {generated}"
);
let workspaces = design
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let ws_gen = testgen::acceptance_rs(&design, workspaces);
assert!(
!ws_gen.contains("\"workspace_id\""),
"Workspace (the tenant itself) must not carry a self fk: {ws_gen}"
);
}
#[test]
fn tenant_owned_modules_get_isolation_tests() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let leads = d
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let out = testgen::acceptance_rs(&d, leads);
assert!(
out.contains("async fn tenant_a_cannot_read_tenant_b_leads()"),
"{out}"
);
assert!(out.contains("404"), "cross-tenant get must 404: {out}");
assert!(
out.contains("fn seed_second_tenant(") && out.contains("test_cookie_for("),
"isolation test needs a second-tenant seed + per-user cookie helper: {out}"
);
assert!(
out.contains("test_cookie_for(1)"),
"test_cookie() must delegate to test_cookie_for(1): {out}"
);
assert!(
out.contains("'owner'"),
"second tenant membership seeds the owner role: {out}"
);
let workspaces = d
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let ws_gen = testgen::acceptance_rs(&d, workspaces);
assert!(
!ws_gen.contains("cannot_read_tenant_b"),
"non-tenant-owned module gets no isolation test: {ws_gen}"
);
assert!(
!ws_gen.contains("seeds_only_the_creators_membership"),
"a public tenant list can't be membership-scoped, so no I1 test: {ws_gen}"
);
}
#[test]
fn flat_tenant_module_gets_the_write_403_isolation_test() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let leads = d.modules.iter().find(|m| m.name == "leads").unwrap();
let out = testgen::acceptance_rs(&d, leads);
assert!(
out.contains("async fn leads_flat_write_into_foreign_tenant_is_403()"),
"the flat-write isolation test must be emitted for a flat tenant module: {out}"
);
let iso = out
.split("async fn leads_flat_write_into_foreign_tenant_is_403()")
.nth(1)
.expect("flat-write isolation fn present");
assert!(
iso.contains("\"workspace_id\": 2"),
"the create body must aim its tenant fk at the foreign tenant 2: {iso}"
);
assert!(
iso.contains("test_cookie_for(1)") && iso.contains("403"),
"user 1 POSTs and must get 403 (create_for_memberships WITH CHECK): {iso}"
);
let api_keys = d.modules.iter().find(|m| m.name == "api-keys").unwrap();
let ak = testgen::acceptance_rs(&d, api_keys);
assert!(
ak.contains("async fn api_keys_flat_write_into_foreign_tenant_is_403()"),
"the flat-write isolation test must be emitted for api-keys too: {ak}"
);
let workspaces = d.modules.iter().find(|m| m.name == "workspaces").unwrap();
let ws = testgen::acceptance_rs(&d, workspaces);
assert!(
!ws.contains("flat_write_into_foreign_tenant_is_403"),
"the tenant root gets no flat-write isolation test: {ws}"
);
const CLUBS: &str = r#"{
"name": "clubs-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Club", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "clubs",
"entities": [{ "name": "Club", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_club", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Club" }, "success": { "status": 201, "entity": "Club" } } ] },
{ "name": "books", "mount": "/clubs/{club_id}",
"entities": [{ "name": "Book", "belongs_to": [{ "entity": "Club" }],
"fields": [{ "name": "id", "type": "integer" }, { "name": "title", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_book", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Book" }, "success": { "status": 201, "entity": "Book" } },
{ "operation_id": "get_book", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Book" } } ] }
]
}"#;
let nested: Design = serde_json::from_str(CLUBS).unwrap();
let books = nested.modules.iter().find(|m| m.name == "books").unwrap();
let books_gen = testgen::acceptance_rs(&nested, books);
assert!(
!books_gen.contains("flat_write_into_foreign_tenant_is_403"),
"a path-scoped nested module gets no flat-write isolation test: {books_gen}"
);
}
#[test]
fn per_user_identity_owned_modules_get_isolation_tests() {
const FITNESS: &str = r#"{
"name": "fitness-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["user"] },
"dependencies": ["db", "auth"],
"modules": [
{ "name": "users",
"entities": [{ "name": "User", "fields": [{ "name": "email", "type": "string" }] }],
"endpoints": [] },
{ "name": "workouts",
"entities": [{ "name": "Workout",
"belongs_to": [{ "entity": "User", "on_delete": "cascade" }],
"fields": [{ "name": "id", "type": "integer" },
{ "name": "distance", "type": "float" }] }],
"endpoints": [
{ "operation_id": "create_workout", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Workout" },
"success": { "status": 201, "entity": "Workout" } },
{ "operation_id": "list_workouts", "method": "GET", "path": "/", "auth_required": true,
"success": { "status": 200, "entity": "Workout", "list": true } },
{ "operation_id": "get_workout", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Workout" } },
{ "operation_id": "delete_workout", "method": "DELETE", "path": "/{id}", "auth_required": true,
"success": { "status": 204 } } ] }
]
}"#;
let d: Design = serde_json::from_str(FITNESS).unwrap();
let workouts = d.modules.iter().find(|m| m.name == "workouts").unwrap();
let out = testgen::acceptance_rs(&d, workouts);
assert!(
out.contains("async fn user_a_cannot_read_user_b_workouts()"),
"per-user isolation test emitted: {out}"
);
assert!(
out.contains("test_cookie_for(1)") && out.contains("test_cookie_for(2)"),
"acts as two distinct users: {out}"
);
assert!(
!out.contains("seed_second_tenant"),
"per-user isolation needs no tenant seed: {out}"
);
assert!(
out.contains("cross-user get must 404") && out.contains("cross-user delete must 404"),
"get + delete legs present: {out}"
);
assert!(
out.contains("cross-user list must NOT contain user 1's row"),
"list leg present: {out}"
);
}
const FEED: &str = r#"{
"name": "feedapp", "contract_version": 1,
"auth": { "model": "session", "roles": ["user"] },
"dependencies": ["db", "auth"],
"modules": [
{ "name": "users",
"entities": [{ "name": "User", "fields": [{ "name": "email", "type": "string" }] }],
"endpoints": [] },
{ "name": "posts",
"entities": [{ "name": "Post", "public_read": true,
"belongs_to": [{ "entity": "User", "on_delete": "cascade" }],
"fields": [{ "name": "title", "type": "string" }] }],
"endpoints": [
{ "operation_id": "list_posts", "method": "GET", "path": "/", "auth_required": true,
"success": { "status": 200, "entity": "Post", "list": true } },
{ "operation_id": "get_post", "method": "GET", "path": "/{id}",
"success": { "status": 200, "entity": "Post" } },
{ "operation_id": "create_post", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Post" },
"success": { "status": 201, "entity": "Post" } },
{ "operation_id": "update_post", "method": "PUT", "path": "/{id}", "auth_required": true,
"request_body": { "entity": "Post" },
"success": { "status": 200, "entity": "Post" } },
{ "operation_id": "delete_post", "method": "DELETE", "path": "/{id}", "auth_required": true,
"success": { "status": 204 } } ] }
]
}"#;
#[test]
fn public_read_gets_lose_the_401_probe_and_probe_anonymously() {
let d: Design = serde_json::from_str(FEED).unwrap();
let posts = d.modules.iter().find(|m| m.name == "posts").unwrap();
let out = testgen::acceptance_rs(&d, posts);
assert!(
!out.contains("list_posts_without_auth_is_401")
&& !out.contains("get_post_without_auth_is_401"),
"a public_read GET must not get a 401 probe (the handler is unguarded — the test would be red-when-correct): {out}"
);
let list_probe = out
.split("async fn list_posts_returns_200")
.nth(1)
.expect("list success probe present")
.split("async fn")
.next()
.unwrap();
assert!(
list_probe.contains("t.get(\"/posts/\")") && !list_probe.contains("test_cookie"),
"the public list probes anonymously: {list_probe}"
);
for probe in [
"create_post_without_auth_is_401",
"update_post_without_auth_is_401",
"delete_post_without_auth_is_401",
] {
assert!(out.contains(probe), "{probe} must stay: {out}");
}
}
#[test]
fn public_read_modules_get_the_owner_write_isolation_test() {
let d: Design = serde_json::from_str(FEED).unwrap();
let posts = d.modules.iter().find(|m| m.name == "posts").unwrap();
let out = testgen::acceptance_rs(&d, posts);
assert!(
out.contains("async fn anon_reads_but_only_the_owner_writes_posts()"),
"public_read isolation test emitted: {out}"
);
assert!(
out.contains("SECURITY (#105)"),
"carries the #105 security doc-comment: {out}"
);
assert!(
!out.contains("async fn user_a_cannot_read_user_b_posts()"),
"the #79 cross-user test must NOT be emitted for a public_read entity: {out}"
);
let body = out
.split("async fn anon_reads_but_only_the_owner_writes_posts()")
.nth(1)
.unwrap()
.split("#[tokio::test]")
.next()
.unwrap();
for leg in [
"anonymous list must 200 (public_read)",
"must contain ANOTHER user's row",
"anonymous detail must 200 (public_read)",
"an anonymous create must 401",
"a non-owner update must 404",
"a non-owner delete must 404",
"must SURVIVE a non-owner write attempt",
"the OWNER's update must succeed",
] {
assert!(body.contains(leg), "leg `{leg}` present: {body}");
}
}
#[test]
fn role_gated_get_on_a_public_read_entity_keeps_the_401_probe() {
let mut d: Design = serde_json::from_str(FEED).unwrap();
let posts_idx = d.modules.iter().position(|m| m.name == "posts").unwrap();
d.modules[posts_idx]
.endpoints
.iter_mut()
.find(|ep| ep.operation_id == "list_posts")
.unwrap()
.required_roles = vec!["user".to_string()];
let posts = &d.modules[posts_idx];
let out = testgen::acceptance_rs(&d, posts);
assert!(
out.contains("list_posts_without_auth_is_401"),
"a role-gated GET keeps its guard and its 401 probe: {out}"
);
let list_probe = out
.split("async fn list_posts_returns_200")
.nth(1)
.expect("list success probe present")
.split("async fn")
.next()
.unwrap();
assert!(
list_probe.contains("test_cookie"),
"a role-gated GET probes WITH a credential: {list_probe}"
);
}
#[test]
fn entityless_authed_get_keeps_the_401_probe_beside_public_read() {
let mut v: serde_json::Value = serde_json::from_str(FEED).unwrap();
v["modules"][1]["endpoints"]
.as_array_mut()
.unwrap()
.push(serde_json::json!({
"operation_id": "get_stats", "method": "GET", "path": "/stats",
"auth_required": true, "success": { "status": 200 }
}));
let d: Design = serde_json::from_value(v).unwrap();
let posts = d.modules.iter().find(|m| m.name == "posts").unwrap();
let out = testgen::acceptance_rs(&d, posts);
assert!(
out.contains("get_stats_without_auth_is_401"),
"an entity-less auth_required GET keeps its 401 probe: {out}"
);
assert!(
!out.contains("list_posts_without_auth_is_401")
&& !out.contains("get_post_without_auth_is_401"),
"the explicit public_read GETs stay 401-probe-free: {out}"
);
}
#[test]
fn nested_path_scoped_module_gets_isolation_test_with_pinned_tenant() {
const CLUBS: &str = r#"{
"name": "clubs-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Club", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "clubs",
"entities": [{ "name": "Club", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_club", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Club" }, "success": { "status": 201, "entity": "Club" } },
{ "operation_id": "list_clubs", "method": "GET", "path": "/", "auth_required": true,
"success": { "status": 200, "entity": "Club", "list": true } } ] },
{ "name": "books", "mount": "/clubs/{club_id}",
"entities": [{ "name": "Book", "belongs_to": [{ "entity": "Club" }],
"fields": [{ "name": "id", "type": "integer" }, { "name": "title", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_book", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Book" }, "success": { "status": 201, "entity": "Book" } },
{ "operation_id": "get_book", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Book" } } ] }
]
}"#;
let d: Design = serde_json::from_str(CLUBS).unwrap();
let books = d.modules.iter().find(|m| m.name == "books").unwrap();
let out = testgen::acceptance_rs(&d, books);
assert!(
out.contains("async fn tenant_a_cannot_read_tenant_b_books()"),
"nested isolation test emitted: {out}"
);
let iso = out
.split("async fn tenant_a_cannot_read_tenant_b_books()")
.nth(1)
.expect("isolation fn present");
assert!(
iso.contains("\"/clubs/1/\"") && iso.contains("/clubs/1/{id}"),
"isolation probe URLs pin the tenant fk to 1: {iso}"
);
assert!(
!iso.contains("{club_id}"),
"the isolation test carries no unsubstituted tenant param: {iso}"
);
assert!(
out.contains("cross-tenant get must 404"),
"get leg 404s: {out}"
);
assert!(
!out.contains("user 2 lists their own"),
"nested route emits no list leg: {out}"
);
let clubs = d.modules.iter().find(|m| m.name == "clubs").unwrap();
let clubs_out = testgen::acceptance_rs(&d, clubs);
assert!(
clubs_out.contains("async fn creating_a_club_seeds_only_the_creators_membership()"),
"I1 tenant-collection-create test emitted: {clubs_out}"
);
assert!(
clubs_out.contains("the creator's list MUST contain the new Club")
&& clubs_out.contains("a non-creator's list must NOT contain the new Club"),
"I1 asserts creator-lists-own + second-user-empty: {clubs_out}"
);
}
#[test]
fn tenant_root_with_a_guarded_detail_route_gets_the_non_member_404_probe() {
const GUARDED: &str = r#"{
"name": "ws-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Workspace", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "workspaces",
"entities": [{ "name": "Workspace", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_workspace", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Workspace" }, "success": { "status": 201, "entity": "Workspace" } },
{ "operation_id": "list_workspaces", "method": "GET", "path": "/", "auth_required": true,
"success": { "status": 200, "entity": "Workspace", "list": true } },
{ "operation_id": "show_workspace", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Workspace" } } ] }
]
}"#;
let d: Design = serde_json::from_str(GUARDED).unwrap();
let ws = d.modules.iter().find(|m| m.name == "workspaces").unwrap();
let out = testgen::acceptance_rs(&d, ws);
assert!(
out.contains("async fn a_non_member_cannot_read_the_workspace_detail()"),
"guarded root detail route must get the non-member-404 probe: {out}"
);
let probe = out
.split("async fn a_non_member_cannot_read_the_workspace_detail()")
.nth(1)
.expect("root-detail probe fn present");
assert!(
probe.contains("&test_cookie_for(1))]).await;") && probe.contains("\"/workspaces/\""),
"user 1 seeds the tenant-1 root row via the create: {probe}"
);
assert!(
probe.contains("&format!(\"/workspaces/{id}\"), &[(\"cookie\", &test_cookie_for(2))])")
&& probe.contains("cross-tenant get on the tenant root must 404"),
"the probe asserts a non-member (test_cookie_for(2)) 404s on the root detail route: {probe}"
);
let public_detail = GUARDED.replace(
r#"{ "operation_id": "show_workspace", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Workspace" } }"#,
r#"{ "operation_id": "show_workspace", "method": "GET", "path": "/{id}", "public": true,
"success": { "status": 200, "entity": "Workspace" } }"#,
);
let dp: Design = serde_json::from_str(&public_detail).unwrap();
let wsp = dp.modules.iter().find(|m| m.name == "workspaces").unwrap();
let outp = testgen::acceptance_rs(&dp, wsp);
assert!(
!outp.contains("a_non_member_cannot_read_the_workspace_detail"),
"a PUBLIC root detail route must emit NO non-member-404 probe: {outp}"
);
}
const ORG_ACCOUNT_CONTACT_NESTED: &str = r#"{
"name": "crm-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "orgs",
"entities": [{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_org", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Org" }, "success": { "status": 201, "entity": "Org" } } ] },
{ "name": "accounts",
"entities": [{ "name": "Account", "belongs_to": [{ "entity": "Org" }],
"fields": [{ "name": "id", "type": "integer" }] }],
"endpoints": [
{ "operation_id": "create_account", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Account" }, "success": { "status": 201, "entity": "Account" } } ] },
{ "name": "contacts", "mount": "/accounts/{account_id}",
"entities": [{ "name": "Contact", "belongs_to": [{ "entity": "Account" }],
"fields": [{ "name": "id", "type": "integer" }, { "name": "title", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_contact", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Contact" }, "success": { "status": 201, "entity": "Contact" } },
{ "operation_id": "get_contact", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Contact" } },
{ "operation_id": "delete_contact", "method": "DELETE", "path": "/{id}", "auth_required": true,
"success": { "status": 204 } } ] }
]
}"#;
#[test]
fn grandchild_gets_a_transitive_isolation_test() {
let d: Design = serde_json::from_str(ORG_ACCOUNT_CONTACT_NESTED).unwrap();
let contacts = d.modules.iter().find(|m| m.name == "contacts").unwrap();
let out = testgen::acceptance_rs(&d, contacts);
assert!(
out.contains("async fn tenant_a_cannot_read_tenant_b_contacts()"),
"grandchild isolation test emitted: {out}"
);
assert!(
out.contains("INSERT INTO \\\"accounts\\\" (id, org_id) VALUES (1, 1)"),
"seeds the intermediate Account in tenant 1: {out}"
);
let iso = out
.split("async fn tenant_a_cannot_read_tenant_b_contacts()")
.nth(1)
.expect("isolation fn present");
assert!(
iso.contains("\"/accounts/1/\"") && iso.contains("/accounts/1/{id}"),
"probe URLs pin the parent fk to the seeded id 1: {iso}"
);
assert!(
!iso.contains("{account_id}"),
"no unsubstituted parent mount param in the isolation test: {iso}"
);
assert!(
iso.contains("cross-tenant get must 404"),
"get leg 404s for user 2: {iso}"
);
assert!(
iso.contains("cross-tenant delete must 404"),
"delete leg 404s for user 2: {iso}"
);
assert!(
!out.contains("user 2 lists their own"),
"nested grandchild route emits no list leg: {out}"
);
assert!(
out.contains("INSERT INTO \\\"orgs\\\" (id")
&& out.contains(
"INSERT INTO \\\"org_members\\\" (user_id, org_id, role) VALUES (1, 1, 'owner')"
),
"tenant 1 Org + user 1 membership seeded for the grandchild module: {out}"
);
assert!(
out.contains("fn seed_second_tenant(") && out.contains("test_cookie_for("),
"grandchild isolation needs a second tenant + per-user cookie helper: {out}"
);
}
const TICKET_COMMENT_MULTIPARENT: &str = r#"{
"name": "helpdesk-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "orgs",
"entities": [{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_org", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Org" }, "success": { "status": 201, "entity": "Org" } } ] },
{ "name": "tickets",
"entities": [{ "name": "Ticket", "belongs_to": [{ "entity": "Org" }],
"fields": [{ "name": "id", "type": "integer" }] }],
"endpoints": [
{ "operation_id": "create_ticket", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Ticket" }, "success": { "status": 201, "entity": "Ticket" } } ] },
{ "name": "comments",
"entities": [{ "name": "Comment",
"belongs_to": [{ "entity": "Ticket" }, { "entity": "User" }],
"fields": [{ "name": "id", "type": "integer" }, { "name": "body", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_comment", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Comment" }, "success": { "status": 201, "entity": "Comment" } },
{ "operation_id": "get_comment", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Comment" } } ] }
]
}"#;
#[test]
fn multi_parent_entity_reaching_tenant_is_tenant_owned_not_per_user() {
let d: Design = serde_json::from_str(TICKET_COMMENT_MULTIPARENT).unwrap();
let comments = d.modules.iter().find(|m| m.name == "comments").unwrap();
let out = testgen::acceptance_rs(&d, comments);
assert!(
out.contains("async fn tenant_a_cannot_read_tenant_b_comments()"),
"multi-parent entity reaching the tenant gets the tenant isolation test: {out}"
);
assert!(
!out.contains("async fn user_a_cannot_read_user_b_comments()"),
"a tenant-owned multi-parent entity must NOT also get a per-user test: {out}"
);
assert!(
out.contains("INSERT INTO \\\"tickets\\\" (id, org_id) VALUES (1, 1)"),
"seeds the intermediate Ticket in tenant 1: {out}"
);
}
#[test]
fn credential_gated_endpoints_get_an_agent_todo_not_a_success_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "gated-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "accounts",
"endpoints": [
{ "operation_id": "login", "method": "POST", "path": "/login",
"public": true,
"success": { "status": 200 },
"errors": [{ "status": 401, "when": "invalid email or password" }] },
{ "operation_id": "stripe_webhook", "method": "POST", "path": "/webhook",
"success": { "status": 200 },
"errors": [{ "status": 400, "when": "Stripe signature is missing or invalid" }] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn login_returns_"),
"credential-gated login must NOT get a success test: {generated}"
);
assert!(
!generated.contains("async fn stripe_webhook_returns_"),
"signature webhook must NOT get a success test: {generated}"
);
assert!(
generated.contains(
"// AGENT TODO: login (POST /accounts/login) authenticates via a credential/signature"
),
"login must get a credential AGENT TODO: {generated}"
);
assert!(
generated.contains(
"// AGENT TODO: stripe_webhook (POST /accounts/webhook) authenticates via a credential/signature"
),
"webhook must get a credential AGENT TODO: {generated}"
);
}
#[test]
fn probe_skip_hint_drops_the_ungreenable_success_probe() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "ingest-api",
"contract_version": 1,
"dependencies": [],
"modules": [{
"name": "ingest",
"endpoints": [
{ "operation_id": "receive_hook", "method": "POST", "path": "/hook",
"public": true, "probe": "skip",
"success": { "status": 202 } },
{ "operation_id": "health_ping", "method": "GET", "path": "/ping",
"success": { "status": 200 } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn receive_hook_returns_"),
"probe: skip must drop the un-greenable success probe: {generated}"
);
assert!(
generated
.contains("// AGENT TODO: receive_hook (POST /ingest/hook) is marked `probe: skip`"),
"probe: skip must emit an explanatory TODO: {generated}"
);
assert!(
generated.contains("async fn health_ping_returns_200()"),
"an ordinary (auto) endpoint still gets its success probe: {generated}"
);
}
#[test]
fn probe_skip_on_a_guarded_endpoint_keeps_the_401_guard_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "vault-api",
"contract_version": 1,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "vault",
"endpoints": [
{ "operation_id": "rotate_key", "method": "POST", "path": "/rotate",
"auth_required": true, "probe": "skip",
"success": { "status": 202 } },
{ "operation_id": "reveal_secret", "method": "GET", "path": "/{id}/reveal",
"auth_required": true, "probe": "skip",
"success": { "status": 200 } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn rotate_key_returns_"),
"probe: skip still drops the un-greenable success probe: {generated}"
);
assert!(
generated.contains("async fn rotate_key_without_auth_is_401"),
"a guarded probe:skip endpoint keeps its 401 guard test: {generated}"
);
assert!(
generated.contains("async fn reveal_secret_without_auth_is_401"),
"a guarded param-path probe:skip endpoint keeps its 401 guard test: {generated}"
);
assert!(
generated.contains("\"/vault/1/reveal\""),
"the 401 probe pins the path param to a literal id: {generated}"
);
assert!(
generated
.contains("// AGENT TODO: rotate_key (POST /vault/rotate) is marked `probe: skip`"),
"the probe:skip TODO is retained: {generated}"
);
assert!(
!generated.contains("and its 401/403 rejection test"),
"a guarded skip TODO must not ask for the generated rejection test: {generated}"
);
let tmp = std::env::temp_dir().join(format!("jc123b-{}", std::process::id()));
std::fs::create_dir_all(tmp.join("crates/routes/vault/tests")).unwrap();
let (_rel, expected_failing) = testgen::write_acceptance(&tmp, &design, "vault").unwrap();
assert_eq!(
expected_failing, 2,
"both 401 guard tests count toward expected_failing: {generated}"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn guarded_id_endpoint_without_creator_keeps_the_401_guard_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "audit-api",
"contract_version": 1,
"auth": { "model": "session" },
"dependencies": ["auth"],
"modules": [{
"name": "audits",
"endpoints": [
{ "operation_id": "show_audit", "method": "GET", "path": "/{id}",
"auth_required": true,
"success": { "status": 200 } },
{ "operation_id": "show_open_audit", "method": "GET", "path": "/open/{id}",
"success": { "status": 200 } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn show_audit_returns_"),
"no creator still drops the un-seedable success probe: {generated}"
);
assert!(
generated.contains("// AGENT TODO: show_audit (GET /audits/{id}) has no creator route"),
"the no-creator TODO is retained: {generated}"
);
assert!(
generated.contains("async fn show_audit_without_auth_is_401"),
"a guarded no-creator /{{id}} endpoint keeps its 401 guard test: {generated}"
);
assert!(
generated.contains("\"/audits/1\""),
"the 401 probe pins the id param to a literal id: {generated}"
);
assert!(
!generated.contains("show_open_audit_without_auth_is_401"),
"an unguarded no-creator endpoint must NOT get a 401 test: {generated}"
);
let tmp = std::env::temp_dir().join(format!("jc153a-{}", std::process::id()));
std::fs::create_dir_all(tmp.join("crates/routes/audits/tests")).unwrap();
let (_rel, expected_failing) = testgen::write_acceptance(&tmp, &design, "audits").unwrap();
assert_eq!(
expected_failing, 1,
"the 401 guard test counts toward expected_failing: {generated}"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn guarded_multi_param_endpoint_keeps_the_401_guard_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "grid-api",
"contract_version": 1,
"auth": { "model": "session" },
"dependencies": ["auth"],
"modules": [{
"name": "grids",
"endpoints": [
{ "operation_id": "get_cell", "method": "GET",
"path": "/{row}/cells/{col}",
"auth_required": true,
"success": { "status": 200 } },
{ "operation_id": "get_open_cell", "method": "GET",
"path": "/{row}/open/{col}",
"success": { "status": 200 } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn get_cell_returns_"),
"a multi-param endpoint still gets no auto-seeded success probe: {generated}"
);
assert!(
generated
.contains("// AGENT TODO: get_cell (GET /grids/{row}/cells/{col}) needs a creator"),
"the multi-param TODO is retained: {generated}"
);
assert!(
generated.contains("async fn get_cell_without_auth_is_401"),
"a guarded multi-param endpoint keeps its 401 guard test: {generated}"
);
assert!(
generated.contains("\"/grids/1/cells/1\""),
"the 401 probe pins EVERY param to a literal id: {generated}"
);
assert!(
!generated.contains("get_open_cell_without_auth_is_401"),
"an unguarded multi-param endpoint must NOT get a 401 test: {generated}"
);
}
#[test]
fn module_with_only_todos_emits_no_dead_app_helper() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "webhook-only-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "billing",
"endpoints": [
{ "operation_id": "stripe_webhook", "method": "POST", "path": "/webhook",
"success": { "status": 200 },
"errors": [{ "status": 400, "when": "Stripe signature is missing or invalid" }] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert_eq!(
testgen::test_count(&generated),
0,
"a signature-only billing module emits no tests: {generated}"
);
assert!(
generated.contains("// AGENT TODO: stripe_webhook"),
"the webhook TODO must be emitted: {generated}"
);
assert!(
!generated.contains("async fn app()"),
"a tests-less module must NOT emit a dead app() helper: {generated}"
);
assert!(
!generated.contains("use jerrycan::prelude::*;") && !generated.contains("::module;"),
"a tests-less module must NOT emit dead imports: {generated}"
);
assert!(
generated.contains("GENERATED by jerrycan gen-tests"),
"the tool banner must remain: {generated}"
);
}
#[test]
fn public_post_without_401_keeps_its_success_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "reg-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["admin"] },
"dependencies": ["auth"],
"modules": [{
"name": "accounts",
"entities": [{ "name": "User", "fields": [
{ "name": "email", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "register", "method": "POST", "path": "/register",
"public": true,
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" },
"errors": [
{ "status": 409, "when": "email already registered" },
{ "status": 422, "when": "request body fails validation" }
] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("async fn register_returns_201"),
"a public POST with no 401/403 keeps its success test: {generated}"
);
}
#[test]
fn post_only_id_action_404_probe_uses_post_not_get() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "tickets-api",
"contract_version": 1,
"modules": [{
"name": "tickets",
"endpoints": [
{ "operation_id": "close_ticket", "method": "POST", "path": "/{id}/close",
"success": { "status": 200 },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("async fn close_ticket_missing_id_is_404"),
"POST-only /{{id}} action with a 404 gets a 404 test: {generated}"
);
assert!(
generated.contains("t.post_json(\"/tickets/999999/close\""),
"404 probe must POST (the endpoint's method), not GET: {generated}"
);
assert!(
!generated.contains("t.get(\"/tickets/999999/close\")"),
"404 probe must NOT be a hardcoded GET: {generated}"
);
}
#[test]
fn two_tenant_seed_uses_distinct_values_for_a_unique_field() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "slug-api",
"contract_version": 1,
"auth": { "model": "jwt", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner", "member"] },
"modules": [
{
"name": "orgs",
"entities": [{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "slug", "type": "string", "unique": true }
]}],
"endpoints": []
},
{
"name": "projects",
"entities": [{ "name": "Project",
"belongs_to": [{ "entity": "Org" }],
"fields": [
{ "name": "id", "type": "integer" },
{ "name": "title", "type": "string" }
]}],
"endpoints": [
{ "operation_id": "list_projects", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "Project", "list": true } },
{ "operation_id": "create_project", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Project" },
"success": { "status": 201, "entity": "Project" } },
{ "operation_id": "show_project", "method": "GET", "path": "/{id}",
"auth_required": true,
"success": { "status": 200, "entity": "Project" },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}
]
}))
.unwrap();
let projects = design
.modules
.iter()
.find(|m| m.name == "projects")
.expect("projects module");
let generated = testgen::acceptance_rs(&design, projects);
assert!(
generated.contains("VALUES (1, 'seed-test-value')"),
"tenant 1 seeds a unique slug distinct from the probe fixture: {generated}"
);
assert!(
generated.contains("VALUES (2, 'test-value-2')"),
"tenant 2 seeds a DISTINCT slug so the unique column doesn't collide: {generated}"
);
let org_inserts: Vec<&str> = generated
.lines()
.filter(|l| l.contains("INSERT INTO \\\"orgs\\\""))
.collect();
assert_eq!(org_inserts.len(), 2, "two org rows seeded: {generated}");
assert_ne!(
org_inserts[0], org_inserts[1],
"the two tenant org INSERTs must differ on the unique slug: {generated}"
);
}
#[test]
fn guarded_identity_fk_is_omitted_from_probe_bodies() {
let s = r#"{
"name": "linkvault",
"contract_version": 1,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["db", "auth"],
"modules": [
{ "name": "users",
"entities": [{ "name": "User", "fields": [
{ "name": "email", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "list_users", "method": "GET", "path": "/",
"auth_required": true,
"success": { "status": 200, "entity": "User", "list": true } }
] },
{ "name": "collections",
"entities": [
{ "name": "Collection",
"belongs_to": [{ "entity": "User", "on_delete": "cascade" }],
"fields": [{ "name": "title", "type": "string" }] },
{ "name": "Bookmark",
"belongs_to": [
{ "entity": "User", "on_delete": "cascade" },
{ "entity": "Collection", "on_delete": "cascade" }
],
"fields": [{ "name": "url", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_collection", "method": "POST", "path": "/",
"auth_required": true,
"request_body": { "entity": "Collection" },
"success": { "status": 201, "entity": "Collection" } },
{ "operation_id": "create_bookmark", "method": "POST", "path": "/bookmarks",
"auth_required": true,
"request_body": { "entity": "Bookmark" },
"success": { "status": 201, "entity": "Bookmark" } },
{ "operation_id": "import_collection", "method": "POST", "path": "/import",
"request_body": { "entity": "Collection" },
"success": { "status": 201, "entity": "Collection" } }
] }
]
}"#;
let design: Design = serde_json::from_str(s).unwrap();
let collections = design
.modules
.iter()
.find(|m| m.name == "collections")
.expect("collections module");
let generated = testgen::acceptance_rs(&design, collections);
assert!(
generated.contains(
"t.post_json_with(\"/collections/\", &serde_json::json!({\"title\": \"test-value\"})"
),
"guarded create body must omit user_id: {generated}"
);
assert!(
generated.contains("serde_json::json!({\"collection_id\": 1, \"url\": \"test-value\"})"),
"non-identity fk stays in the body: {generated}"
);
assert!(
generated.contains(
"t.post_json(\"/collections/import\", &serde_json::json!({\"user_id\": 1, \"title\": \"test-value\"}))"
),
"unguarded body keeps user_id: {generated}"
);
}
#[test]
fn enum_request_body_gets_out_of_range_reject_test() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let leads = design
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let generated = testgen::acceptance_rs(&design, leads);
assert!(
generated.contains("async fn create_lead_rejects_out_of_range_status"),
"create reject test: {generated}"
);
assert!(
generated.contains("async fn update_lead_rejects_out_of_range_status"),
"update reject test: {generated}"
);
assert!(
generated.contains("\"status\": \"__invalid_enum_value__\""),
"reject body uses an out-of-range sentinel: {generated}"
);
assert!(
generated.contains("as_u16(), 422"),
"reject test asserts 422: {generated}"
);
assert!(
generated.contains("_rejects_out_of_range_status() {\n let t = app().await;\n let res = t.post_json_with"),
"guarded create reject threads the cookie: {generated}"
);
}
#[test]
fn enum_reject_tests_are_excluded_from_expected_failing() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let design: Design = serde_json::from_str(s).unwrap();
let leads = design
.modules
.iter()
.find(|m| m.name == "leads")
.expect("leads module");
let generated = testgen::acceptance_rs(&design, leads);
let total = testgen::test_count(&generated);
let rejects = generated.matches("_rejects_out_of_range_").count();
assert!(
rejects >= 2,
"at least create + update reject tests: {rejects}"
);
let tmp = tempfile::tempdir().unwrap();
let (_rel, expected_failing) =
testgen::write_acceptance(tmp.path(), &design, "leads").expect("write acceptance");
assert_eq!(
expected_failing,
total - rejects,
"expected_failing must exclude exactly the reject tests (total {total}, rejects {rejects})"
);
}
fn test_body<'a>(generated: &'a str, fn_name: &str) -> &'a str {
let start = generated
.find(&format!("async fn {fn_name}("))
.unwrap_or_else(|| panic!("no `async fn {fn_name}` in:\n{generated}"));
let rest = &generated[start..];
match rest[1..].find("#[tokio::test]") {
Some(i) => &rest[..=i],
None => rest,
}
}
fn j3_two_entity_module() -> Design {
serde_json::from_value(serde_json::json!({
"name": "j3-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "projects",
"entities": [
{ "name": "Task", "fields": [{ "name": "title", "type": "string" }] },
{ "name": "Project", "fields": [{ "name": "name", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "list_projects", "method": "GET", "path": "/",
"success": { "status": 200, "entity": "Project", "list": true } },
{ "operation_id": "create_project", "method": "POST", "path": "/",
"request_body": { "entity": "Project" },
"success": { "status": 201, "entity": "Project" } },
{ "operation_id": "list_tasks", "method": "GET", "path": "/tasks",
"success": { "status": 200, "entity": "Task", "list": true } },
{ "operation_id": "create_task", "method": "POST", "path": "/tasks",
"request_body": { "entity": "Task" },
"success": { "status": 201, "entity": "Task" } },
{ "operation_id": "update_task", "method": "PUT", "path": "/tasks/{id}",
"request_body": { "entity": "Task" },
"success": { "status": 200, "entity": "Task" },
"errors": [{ "status": 404, "when": "unknown id" }] },
{ "operation_id": "delete_task", "method": "DELETE", "path": "/tasks/{id}",
"success": { "status": 204 },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}]
}))
.unwrap()
}
#[test]
fn second_entity_id_probe_seeds_via_its_own_creator() {
let design = j3_two_entity_module();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
for probe in ["update_task_returns_200", "delete_task_returns_204"] {
let body = test_body(&generated, probe);
assert!(
body.contains("post_json(\"/projects/tasks\""),
"{probe} must seed a Task via its own creator POST /projects/tasks:\n{body}"
);
assert!(
body.contains("/projects/tasks/1"),
"{probe} must address the seeded Task:\n{body}"
);
assert!(
!body.contains("post_json(\"/projects/\""),
"{probe} must not seed a Project for a Task probe:\n{body}"
);
}
}
#[test]
fn id_probe_seeds_belongs_to_parents_before_the_entity() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "j3-fk-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "projects",
"entities": [
{ "name": "Task",
"belongs_to": [{ "entity": "Project" }],
"fields": [{ "name": "title", "type": "string" }] },
{ "name": "Project", "fields": [{ "name": "name", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_project", "method": "POST", "path": "/",
"request_body": { "entity": "Project" },
"success": { "status": 201, "entity": "Project" } },
{ "operation_id": "create_task", "method": "POST", "path": "/tasks",
"request_body": { "entity": "Task" },
"success": { "status": 201, "entity": "Task" } },
{ "operation_id": "update_task", "method": "PUT", "path": "/tasks/{id}",
"request_body": { "entity": "Task" },
"success": { "status": 200, "entity": "Task" } }
]
}]
}))
.unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
let body = test_body(&generated, "update_task_returns_200");
let parent = body
.find("post_json(\"/projects/\"")
.expect("parent Project must be seeded (POST /projects/)");
let child = body
.find("post_json(\"/projects/tasks\"")
.expect("Task must be seeded (POST /projects/tasks)");
assert!(
parent < child,
"the parent Project must be seeded before the Task:\n{body}"
);
assert!(
body.contains("\"project_id\": 1"),
"the Task fixture must carry the fk pointing at the seeded parent:\n{body}"
);
}
#[test]
fn id_probe_without_a_creator_becomes_agent_todo() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "j3-nocreate-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "projects",
"entities": [
{ "name": "Project", "fields": [{ "name": "name", "type": "string" }] },
{ "name": "Task", "fields": [{ "name": "title", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_project", "method": "POST", "path": "/",
"request_body": { "entity": "Project" },
"success": { "status": 201, "entity": "Project" } },
{ "operation_id": "update_task", "method": "PUT", "path": "/tasks/{id}",
"request_body": { "entity": "Task" },
"success": { "status": 200, "entity": "Task" } }
]
}]
}))
.unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
!generated.contains("async fn update_task_returns_200"),
"no un-greenable success probe when the entity can't be seeded:\n{generated}"
);
assert!(
generated.contains("// AGENT TODO: update_task")
&& generated.contains("no creator route to seed"),
"must emit an AGENT TODO naming the missing creator:\n{generated}"
);
}
#[test]
fn format_typed_fields_use_valid_fixtures_not_placeholders() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "fmt-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "events",
"entities": [{ "name": "Event", "fields": [
{ "name": "ref_id", "type": "uuid" },
{ "name": "at", "type": "datetime" }
]}],
"endpoints": [{ "operation_id": "create_event", "method": "POST", "path": "/",
"request_body": { "entity": "Event" },
"success": { "status": 201, "entity": "Event" } }]
}]
}))
.unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
const V4: &str = "f47ac10b-58cc-4372-a567-0e02b2c3d479";
assert!(
generated.contains(V4),
"uuid fixture must be a valid v4:\n{generated}"
);
assert!(
!generated.contains("00000000-0000-0000-0000-000000000000"),
"the nil uuid is not a valid v4 — un-greenable against a v4 validator:\n{generated}"
);
assert!(
generated.contains("2026-01-01T00:00:00Z"),
"datetime fixture stays valid RFC3339:\n{generated}"
);
let b = V4.as_bytes();
assert_eq!(b[14], b'4', "uuid v4 version nibble");
assert!(
matches!(b[19], b'8' | b'9' | b'a' | b'b'),
"uuid v4 variant nibble"
);
}
#[test]
fn defaulted_fields_are_omitted_from_the_probe_body() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "news",
"contract_version": 0,
"dependencies": ["db"],
"modules": [{ "name": "subscribers",
"entities": [{ "name": "Subscriber", "fields": [
{ "name": "email", "type": "string" },
{ "name": "confirmed", "type": "boolean", "default": false },
{ "name": "status", "type": "string", "values": ["active", "expired"], "default": "active" } ] }],
"endpoints": [{ "operation_id": "create_subscriber", "method": "POST", "path": "/",
"request_body": { "entity": "Subscriber" },
"success": { "status": 201, "entity": "Subscriber" } }] }]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("async fn create_subscriber_returns_201"),
"{generated}"
);
assert!(generated.contains("\"email\""), "{generated}");
assert!(
!generated.contains("\"confirmed\"") && !generated.contains("\"status\""),
"defaulted fields must not appear in any probe body: {generated}"
);
assert!(
!generated.contains("rejects_out_of_range_status"),
"a defaulted enum field gets no boundary-reject probe: {generated}"
);
}
#[test]
fn nested_parent_fk_is_omitted_from_the_probe_body_and_taken_from_the_path() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "habits",
"contract_version": 0,
"dependencies": ["db"],
"modules": [{ "name": "habits",
"entities": [
{ "name": "Habit", "fields": [{ "name": "name", "type": "string" }] },
{ "name": "Checkin", "belongs_to": [{ "entity": "Habit" }],
"fields": [{ "name": "note", "type": "string" }] } ],
"endpoints": [
{ "operation_id": "create_habit", "method": "POST", "path": "/",
"request_body": { "entity": "Habit" },
"success": { "status": 201, "entity": "Habit" } },
{ "operation_id": "create_checkin", "method": "POST", "path": "/{habit_id}/checkins",
"request_body": { "entity": "Checkin" },
"success": { "status": 201, "entity": "Checkin" } }] }]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("async fn create_checkin_returns_201"),
"{generated}"
);
assert!(
generated.contains("post_json(\"/habits/\""),
"seed habit: {generated}"
);
assert!(
generated.contains("post_json(\"/habits/1/checkins\""),
"checkin addressed under the path's habit: {generated}"
);
assert!(generated.contains("\"note\""), "{generated}");
assert!(
!generated.contains("\"habit_id\""),
"path-redundant fk must not appear in the body: {generated}"
);
}
#[test]
fn credential_role_is_drawn_from_the_designs_role_gate() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "helpdesk",
"contract_version": 1,
"dependencies": ["db", "auth"],
"auth": { "model": "jwt", "roles": ["agent", "customer"] },
"modules": [{
"name": "tickets",
"entities": [{ "name": "Ticket", "fields": [{ "name": "subject", "type": "string" }] }],
"endpoints": [
{ "operation_id": "resolve_ticket", "method": "POST", "path": "/resolve",
"auth_required": true, "required_roles": ["agent"],
"request_body": { "entity": "Ticket" },
"success": { "status": 200, "entity": "Ticket" } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains("role: \"agent\".into()"),
"the minted credential must carry the gate's role `agent`, not a hardcoded `admin`: {generated}"
);
assert!(
!generated.contains("role: \"admin\""),
"no hardcoded admin role when the design declares none: {generated}"
);
}
#[test]
fn credential_role_falls_back_to_first_declared_then_admin() {
let roled: Design = serde_json::from_value(serde_json::json!({
"name": "roled", "contract_version": 1, "dependencies": ["db", "auth"],
"auth": { "model": "session", "roles": ["owner", "member"] },
"modules": [{ "name": "notes",
"entities": [{ "name": "Note", "fields": [{ "name": "body", "type": "string" }] }],
"endpoints": [{ "operation_id": "create_note", "method": "POST", "path": "/",
"auth_required": true, "request_body": { "entity": "Note" },
"success": { "status": 201, "entity": "Note" } }] }]
}))
.unwrap();
let g = testgen::acceptance_rs(&roled, &roled.modules[0]);
assert!(
g.contains("role: \"owner\".into()"),
"first declared role: {g}"
);
let roleless: Design = serde_json::from_value(serde_json::json!({
"name": "roleless", "contract_version": 1, "dependencies": ["db", "auth"],
"auth": { "model": "session" },
"modules": [{ "name": "notes",
"entities": [{ "name": "Note", "fields": [{ "name": "body", "type": "string" }] }],
"endpoints": [{ "operation_id": "create_note", "method": "POST", "path": "/",
"auth_required": true, "request_body": { "entity": "Note" },
"success": { "status": 201, "entity": "Note" } }] }]
}))
.unwrap();
let g2 = testgen::acceptance_rs(&roleless, &roleless.modules[0]);
assert!(
g2.contains("role: \"admin\".into()"),
"no roles -> admin fallback: {g2}"
);
}
#[test]
fn testapp_wires_declared_realtime_and_jobs_extensions() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "live",
"contract_version": 1,
"dependencies": ["db", "auth"],
"auth": { "model": "jwt", "roles": ["admin"] },
"realtime": {
"broadcast": [{ "name": "feed", "scope": "auth" }],
"presence": [{ "name": "cursors", "scope": "auth" }]
},
"jobs": [{ "name": "sweep", "schedule": "0 * * * *", "queue": "default" }],
"modules": [{
"name": "posts",
"entities": [{ "name": "Post", "fields": [{ "name": "title", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_post", "method": "POST", "path": "/",
"auth_required": true, "request_body": { "entity": "Post" },
"success": { "status": 201, "entity": "Post" } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains(".extend(jerrycan::realtime::Realtime::new(db.clone())"),
"realtime design's TestApp must wire the realtime extension (else JC1001): {generated}"
);
assert!(
generated.contains(".broadcast(\"feed\", jerrycan::realtime::TopicScope::Auth)"),
"realtime TestApp must declare the app's broadcast topics (issue #84): {generated}"
);
assert!(
generated.contains(".presence(\"cursors\", jerrycan::realtime::TopicScope::Auth)"),
"realtime TestApp must declare the app's presence topics (issue #84): {generated}"
);
assert!(
generated.contains(".extend(jerrycan::jobs::Jobs::postgres(db.clone()))"),
"jobs design's TestApp must wire the jobs extension: {generated}"
);
let builder = generated
.lines()
.find(|l| l.contains("App::new()"))
.expect("app() builder line");
let realtime_at = builder.find(".extend(jerrycan::realtime").unwrap();
let db_at = builder.find(".extend(db)").unwrap();
assert!(
realtime_at < db_at,
"extensions must precede .extend(db): {builder}"
);
assert!(
generated.contains("issue #66")
&& generated.contains("observe")
&& generated.contains("validate"),
"the harness must document which extensions it excludes and why: {generated}"
);
}
#[test]
fn testapp_wires_declared_storage_extension() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "files",
"contract_version": 2,
"dependencies": ["db", "auth"],
"auth": { "model": "session", "roles": ["admin"] },
"storage": { "buckets": [{ "name": "avatars", "visibility": "public" }] },
"modules": [{
"name": "profiles",
"entities": [{ "name": "Profile", "fields": [{ "name": "handle", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_profile", "method": "POST", "path": "/",
"auth_required": true, "request_body": { "entity": "Profile" },
"success": { "status": 201, "entity": "Profile" } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
generated.contains(".extend(jerrycan::storage::Storage::memory().with_sign_secret("),
"storage design's TestApp must wire an in-memory Storage extension: {generated}"
);
}
#[test]
fn plain_db_design_harness_is_unchanged_by_extension_wiring() {
let design = golden(true); let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("issue #66"),
"no extension-wiring comment for a design that declares no extensions: {generated}"
);
assert!(
!generated.contains("jerrycan::jobs::")
&& !generated.contains("jerrycan::realtime::")
&& !generated.contains("jerrycan::storage::"),
"no extension extends for a plain db design: {generated}"
);
}
#[test]
fn skipped_creator_suppresses_sibling_id_probes() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "sitemonitor",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "monitors",
"entities": [{ "name": "Monitor",
"fields": [{ "name": "url", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_monitor", "method": "POST", "path": "/",
"probe": "skip", "request_body": { "entity": "Monitor" },
"success": { "status": 201, "entity": "Monitor" } },
{ "operation_id": "get_monitor", "method": "GET", "path": "/{id}",
"success": { "status": 200, "entity": "Monitor" },
"errors": [{ "status": 404, "when": "unknown id" }] },
{ "operation_id": "delete_monitor", "method": "DELETE", "path": "/{id}",
"success": { "status": 204 } }
]
}]
}))
.unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
!generated.contains("async fn get_monitor_returns_200"),
"a probe:skip creator must not seed a sibling GET /{{id}} success probe: {generated}"
);
assert!(
!generated.contains("async fn delete_monitor_returns_204"),
"a probe:skip creator must not seed a sibling DELETE /{{id}} success probe: {generated}"
);
assert!(
generated.contains("// AGENT TODO: get_monitor") && generated.contains("probe: skip"),
"must emit an AGENT TODO explaining the skipped seed creator: {generated}"
);
assert!(
generated.contains("async fn get_monitor_missing_id_is_404"),
"the missing-id 404 probe needs no seed and must remain greenable: {generated}"
);
let tmp = std::env::temp_dir().join(format!("jc68-{}", std::process::id()));
std::fs::create_dir_all(tmp.join("crates/routes/monitors/tests")).unwrap();
let (_rel, expected_failing) = testgen::write_acceptance(&tmp, &design, "monitors").unwrap();
assert_eq!(
expected_failing, 1,
"only the 404 missing-id probe is a counted RED-on-stubs test: {generated}"
);
std::fs::remove_dir_all(&tmp).ok();
}
#[test]
fn skipped_creator_guarded_sibling_keeps_the_401_guard_test() {
let design: Design = serde_json::from_value(serde_json::json!({
"name": "sitemonitor",
"contract_version": 1,
"auth": { "model": "session", "roles": ["admin"] },
"dependencies": ["db", "auth"],
"modules": [{
"name": "monitors",
"entities": [{ "name": "Monitor",
"fields": [{ "name": "url", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_monitor", "method": "POST", "path": "/",
"auth_required": true, "probe": "skip",
"request_body": { "entity": "Monitor" },
"success": { "status": 201, "entity": "Monitor" } },
{ "operation_id": "get_monitor", "method": "GET", "path": "/{id}",
"auth_required": true,
"success": { "status": 200, "entity": "Monitor" } }
]
}]
}))
.unwrap();
let generated = testgen::acceptance_rs(&design, &design.modules[0]);
assert!(
!generated.contains("async fn get_monitor_returns_200"),
"a probe:skip creator still suppresses the sibling success probe: {generated}"
);
assert!(
generated.contains("async fn get_monitor_without_auth_is_401"),
"a guarded sibling of a skipped creator keeps its 401 guard test: {generated}"
);
assert!(
generated.contains("\"/monitors/1\""),
"the sibling 401 probe pins {{id}} to a literal — no seed needed: {generated}"
);
assert!(
generated.contains("async fn create_monitor_without_auth_is_401"),
"the guarded skipped creator keeps its own 401 guard test: {generated}"
);
}
const WORKSPACE_CHANNELS_NESTED: &str = r#"{
"name": "chat-api", "contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Workspace", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "workspaces",
"entities": [{ "name": "Workspace", "fields": [
{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_workspace", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Workspace" }, "success": { "status": 201, "entity": "Workspace" } },
{ "operation_id": "list_workspaces", "method": "GET", "path": "/", "auth_required": true,
"success": { "status": 200, "entity": "Workspace", "list": true } } ] },
{ "name": "channels", "mount": "/workspaces/{workspace_id}/channels",
"entities": [{ "name": "Channel", "belongs_to": [{ "entity": "Workspace" }],
"fields": [{ "name": "id", "type": "integer" }, { "name": "name", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_channel", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Channel" }, "success": { "status": 201, "entity": "Channel" } },
{ "operation_id": "get_channel", "method": "GET", "path": "/{id}", "auth_required": true,
"success": { "status": 200, "entity": "Channel" },
"errors": [{ "status": 404, "when": "unknown id" }] } ] }
]
}"#;
#[test]
fn subroute_mount_param_is_substituted_in_per_endpoint_urls() {
let d: Design = serde_json::from_str(WORKSPACE_CHANNELS_NESTED).unwrap();
let channels = d.modules.iter().find(|m| m.name == "channels").unwrap();
let out = testgen::acceptance_rs(&d, channels);
for line in out
.lines()
.filter(|l| l.contains(" t.") || l.contains("= t."))
{
assert!(
!line.contains("{workspace_id}"),
"a request URL carries the literal mount param:\n{line}\n---\n{out}"
);
}
let create = test_body(&out, "create_channel_returns_201");
assert!(
create.contains("t.post_json_with(\"/workspaces/1/channels/\""),
"create URL pins the mount param to the seeded id 1:\n{create}"
);
let get = test_body(&out, "get_channel_returns_200");
assert!(
get.contains("post_json_with(\"/workspaces/1/channels/\"")
&& get.contains("/workspaces/1/channels/1"),
"get URL seeds + probes under the concrete mount:\n{get}"
);
let missing = test_body(&out, "get_channel_missing_id_is_404");
assert!(
missing.contains("/workspaces/1/channels/999999"),
"404 probe substitutes the mount param:\n{missing}"
);
}
const FLAT_CUSTOMERS: &str = r#"{
"name": "shop", "contract_version": 1, "dependencies": ["db"],
"modules": [{
"name": "customers",
"entities": [{ "name": "Customer", "fields": [
{ "name": "id", "type": "integer" }, { "name": "email", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_customer", "method": "POST", "path": "/",
"request_body": { "entity": "Customer" },
"success": { "status": 201, "entity": "Customer" } },
{ "operation_id": "show_customer", "method": "GET", "path": "/{id}",
"success": { "status": 200, "entity": "Customer" },
"errors": [{ "status": 404, "when": "unknown id" }] }
]
}]
}"#;
#[test]
fn flat_module_urls_are_unchanged_by_mount_substitution() {
let d: Design = serde_json::from_str(FLAT_CUSTOMERS).unwrap();
let customers = d.modules.iter().find(|m| m.name == "customers").unwrap();
let out = testgen::acceptance_rs(&d, customers);
assert!(
out.contains("t.post_json(\"/customers/\""),
"flat create URL is the plain collection path: {out}"
);
let show = test_body(&out, "show_customer_returns_200");
assert!(
show.contains("/customers/1"),
"flat by-id URL is the plain path:\n{show}"
);
assert!(
out.contains("/customers/999999"),
"flat 404 URL is the plain path: {out}"
);
assert!(
!out.contains("/customers/1/"),
"a flat module must not gain a nested mount segment: {out}"
);
}
#[test]
fn unique_tenant_field_seed_does_not_collide_with_the_create_probe() {
const UNIQUE_TENANT: &str = r#"{
"name": "orgs", "contract_version": 0,
"auth": { "model": "session" },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner"] },
"modules": [{ "name": "orgs",
"entities": [
{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "slug", "type": "string", "unique": true } ] },
{ "name": "Project", "belongs_to": [{ "entity": "Org" }],
"fields": [
{ "name": "id", "type": "integer" },
{ "name": "title", "type": "string" } ] } ],
"endpoints": [
{ "operation_id": "list_orgs", "method": "GET", "path": "/", "auth_required": true,
"success": { "status": 200, "entity": "Org", "list": true } },
{ "operation_id": "create_org", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Org" },
"success": { "status": 201, "entity": "Org" } } ] }]
}"#;
let design: Design = serde_json::from_str(UNIQUE_TENANT).unwrap();
let module = &design.modules[0];
let generated = testgen::acceptance_rs(&design, module);
assert!(
generated.contains("\"slug\": \"test-value\""),
"create probe posts the string fixture for slug:\n{generated}"
);
assert!(
!generated.contains("VALUES (1, 'test-value')"),
"tenant-1 seed must not reuse the probe's unique slug value (would 409):\n{generated}"
);
}
#[test]
fn tenant_module_gets_member_surface_tests() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let workspaces = d
.modules
.iter()
.find(|m| m.name == "workspaces")
.expect("workspaces module");
let out = testgen::acceptance_rs(&d, workspaces);
for expected in [
"async fn list_workspace_members_returns_200",
"async fn add_workspace_member_returns_201",
"async fn add_workspace_member_without_the_admin_role_is_403",
"async fn set_workspace_member_role_returns_204",
"async fn remove_workspace_member_returns_204",
"async fn set_workspace_member_role_last_admin_demotion_is_409",
"async fn remove_workspace_member_last_admin_is_409",
"async fn remove_workspace_member_self_leave_returns_204",
"async fn add_workspace_member_rejects_out_of_range_role",
] {
assert!(out.contains(expected), "missing {expected}\n{out}");
}
let member_fn = out
.split("async fn member_app()")
.nth(1)
.expect("member_app() helper")
.split("#[tokio::test]")
.next()
.unwrap();
assert!(
member_fn.contains(
"INSERT INTO \\\"workspace_members\\\" (user_id, workspace_id, role) VALUES (1, 1, 'owner')"
) && member_fn.contains(
"INSERT INTO \\\"workspace_members\\\" (user_id, workspace_id, role) VALUES (2, 1, 'member')"
),
"member_app seeds the admin (user 1) and a non-admin member (user 2) in tenant 1: {member_fn}"
);
assert!(
member_fn.contains("INSERT INTO \\\"workspaces\\\"") && member_fn.contains("'trial'"),
"member_app seeds the tenant row with a CHECK-valid enum value: {member_fn}"
);
assert!(
member_fn.contains(".provide_dep(shared::tenant)"),
"member_app registers the Tenant factory the member handlers resolve: {member_fn}"
);
assert!(
out.contains("t.get_with(\"/workspaces/1/members\"")
&& out.contains("/workspaces/1/members/2")
&& out.contains("(\"authorization\", &test_cookie_for(2))"),
"member probes hit the path-scoped member routes as distinct users: {out}"
);
let forbidden = test_body(&out, "add_workspace_member_without_the_admin_role_is_403");
assert!(
forbidden.contains("test_cookie_for(2)") && forbidden.contains("403"),
"the 403 probe must act as the non-admin member:\n{forbidden}"
);
let last_admin = test_body(&out, "remove_workspace_member_last_admin_is_409");
assert!(
last_admin.contains("/workspaces/1/members/1") && last_admin.contains("409"),
"the last-admin probe removes the sole admin (user 1) and expects 409:\n{last_admin}"
);
let leave = test_body(&out, "remove_workspace_member_self_leave_returns_204");
assert!(
leave.contains("/workspaces/1/members/2") && leave.contains("test_cookie_for(2)"),
"self-leave acts as user 2 on their own membership:\n{leave}"
);
let leads = d.modules.iter().find(|m| m.name == "leads").unwrap();
let leads_out = testgen::acceptance_rs(&d, leads);
assert!(
!leads_out.contains("member_app") && !leads_out.contains("_members_returns_"),
"non-tenant modules must not gain member tests: {leads_out}"
);
let plain = golden(true);
let plain_out = testgen::acceptance_rs(&plain, &plain.modules[0]);
assert!(
!plain_out.contains("member_app"),
"a non-tenancy design must not gain member tests: {plain_out}"
);
}
#[test]
fn member_surface_tests_are_excluded_from_expected_failing() {
let s = include_str!("../../../conformance/designs/reference-slice.design.json");
let d: Design = serde_json::from_str(s).unwrap();
let tmp = std::env::temp_dir().join(format!("jc107-{}", std::process::id()));
std::fs::create_dir_all(tmp.join("crates/routes/workspaces/tests")).unwrap();
let (_rel, expected_failing) = testgen::write_acceptance(&tmp, &d, "workspaces").unwrap();
let generated =
std::fs::read_to_string(tmp.join("crates/routes/workspaces/tests/acceptance.rs")).unwrap();
std::fs::remove_dir_all(&tmp).ok();
assert_eq!(
testgen::test_count(&generated),
15,
"5 endpoint probes + 1 enum reject + 9 member tests: {generated}"
);
assert_eq!(
expected_failing, 5,
"the enum reject probe and the 9 member tests must be excluded: {generated}"
);
}
#[test]
fn single_role_tenancy_keeps_only_the_roleless_member_tests() {
let d: Design = serde_json::from_value(serde_json::json!({
"name": "solo-api",
"contract_version": 1,
"auth": { "model": "session", "roles": ["owner"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Org", "member_roles": ["owner"] },
"modules": [{
"name": "orgs",
"entities": [{ "name": "Org", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "name", "type": "string" } ]}],
"endpoints": [
{ "operation_id": "create_org", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Org" },
"success": { "status": 201, "entity": "Org" } }
]
}]
}))
.unwrap();
let out = testgen::acceptance_rs(&d, &d.modules[0]);
for expected in [
"async fn list_org_members_returns_200",
"async fn add_org_member_returns_201",
"async fn remove_org_member_last_admin_is_409",
"async fn add_org_member_rejects_out_of_range_role",
] {
assert!(out.contains(expected), "missing {expected}\n{out}");
}
for absent in [
"without_the_admin_role_is_403",
"set_org_member_role_returns_204",
"async fn remove_org_member_returns_204",
"last_admin_demotion_is_409",
"self_leave_returns_204",
] {
assert!(
!out.contains(absent),
"a one-role design must not emit `{absent}` (needs a non-admin member):\n{out}"
);
}
assert!(
!out.contains("VALUES (2, 1,"),
"single-role member_app seeds no user-2 membership: {out}"
);
let add = test_body(&out, "add_org_member_returns_201");
assert!(
add.contains("\"role\": \"owner\""),
"single-role add uses the only declared role:\n{add}"
);
}
fn constrained_design(fields: serde_json::Value) -> Design {
serde_json::from_value(serde_json::json!({
"name": "shop-api",
"contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "items",
"entities": [{ "name": "Item", "fields": fields }],
"endpoints": [
{ "operation_id": "create_item", "method": "POST", "path": "/",
"request_body": { "entity": "Item" },
"success": { "status": 201, "entity": "Item" } },
{ "operation_id": "update_item", "method": "PUT", "path": "/{id}",
"request_body": { "entity": "Item" },
"success": { "status": 200, "entity": "Item" } }
]
}]
}))
.unwrap()
}
#[test]
fn constrained_fixtures_are_derived_in_range() {
let d = constrained_design(serde_json::json!([
{ "name": "quantity", "type": "integer", "min": 5, "max": 600 },
{ "name": "code", "type": "string", "max_len": 5 },
{ "name": "body", "type": "string", "min_len": 12 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"quantity\": 5"),
"int fixture must clamp into [5, 600]: {generated}"
);
assert!(
generated.contains("\"code\": \"test-\""),
"string fixture must truncate to max_len: {generated}"
);
assert!(
generated.contains("\"body\": \"aaaaaaaaaaaa\""),
"string fixture must satisfy min_len (12): {generated}"
);
}
#[test]
fn constrained_body_gets_out_of_range_reject_probe_on_create_and_update() {
let d = constrained_design(serde_json::json!([
{ "name": "quantity", "type": "integer", "min": 1, "max": 600 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("async fn create_item_rejects_out_of_range_quantity"),
"create reject probe: {generated}"
);
assert!(
generated.contains("async fn update_item_rejects_out_of_range_quantity"),
"update reject probe: {generated}"
);
assert!(
generated.contains("\"quantity\": 601"),
"reject body carries max + 1: {generated}"
);
let probe = test_body(&generated, "create_item_rejects_out_of_range_quantity");
assert!(probe.contains("as_u16(), 422"), "asserts 422:\n{probe}");
let happy = test_body(&generated, "create_item_returns_201");
assert!(
happy.contains("\"quantity\": 1"),
"in-range fixture:\n{happy}"
);
}
#[test]
fn out_of_i32_range_bounds_emit_i64_suffixed_literals() {
let d = constrained_design(serde_json::json!([
{ "name": "starts_at", "type": "integer", "min": 0, "max": 4102444800i64 },
{ "name": "seq", "type": "integer", "min": 3000000000i64 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"seq\": 3000000000i64"),
"out-of-i32-range fixture must be i64-suffixed: {generated}"
);
assert!(
generated.contains("\"starts_at\": 4102444801i64"),
"out-of-i32-range reject literal must be i64-suffixed: {generated}"
);
assert!(
generated.contains("\"starts_at\": 1,"),
"in-i32-range fixture stays unsuffixed: {generated}"
);
let d = constrained_design(serde_json::json!([
{ "name": "depth", "type": "integer", "max": -3000000000i64 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"depth\": -3000000000i64"),
"below-i32::MIN fixture must be i64-suffixed: {generated}"
);
}
#[test]
fn constraint_reject_probes_are_excluded_from_expected_failing() {
let d = constrained_design(serde_json::json!([
{ "name": "quantity", "type": "integer", "min": 1, "max": 600 },
{ "name": "code", "type": "string", "max_len": 5 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
let total = testgen::test_count(&generated);
let rejects = generated
.matches("async fn create_item_rejects_out_of_range_")
.count()
+ generated
.matches("async fn update_item_rejects_out_of_range_")
.count();
assert!(rejects >= 2, "create + update reject probes: {generated}");
let tmp = tempfile::tempdir().unwrap();
let (_rel, expected_failing) =
testgen::write_acceptance(tmp.path(), &d, "items").expect("write acceptance");
assert_eq!(
expected_failing,
total - rejects,
"expected_failing must exclude exactly the reject probes (total {total}, rejects {rejects})"
);
}
#[test]
fn string_reject_probe_uses_a_repeat_expression() {
let d = constrained_design(serde_json::json!([
{ "name": "code", "type": "string", "max_len": 5 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"code\": \"a\".repeat(6)"),
"over-max reject sends max_len + 1 code points: {generated}"
);
let d = constrained_design(serde_json::json!([
{ "name": "body", "type": "string", "min_len": 3 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"body\": \"a\".repeat(2)"),
"under-min reject sends min_len - 1 code points: {generated}"
);
}
#[test]
fn huge_max_len_never_materializes_an_over_max_reject_string() {
let d = constrained_design(serde_json::json!([
{ "name": "body", "type": "string", "max_len": 100000 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
!generated.contains("_rejects_out_of_range_"),
"an over-cap max_len with no min_len emits no probe: {generated}"
);
assert!(
generated.contains("\"body\": \"test-value\""),
"{generated}"
);
let d = constrained_design(serde_json::json!([
{ "name": "body", "type": "string", "min_len": 3, "max_len": 100000 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"body\": \"a\".repeat(2)"),
"over-cap max_len falls back to under-min: {generated}"
);
assert!(
!generated.contains("repeat(100001)"),
"never materialize an over-cap string: {generated}"
);
}
#[test]
fn i64_extreme_bounds_fall_back_or_skip_the_probe() {
let d = constrained_design(serde_json::json!([
{ "name": "views", "type": "integer", "min": 1, "max": i64::MAX }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
generated.contains("\"views\": 0"),
"max at i64::MAX falls back to min - 1: {generated}"
);
let d = constrained_design(serde_json::json!([
{ "name": "views", "type": "integer", "min": i64::MIN, "max": i64::MAX }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
!generated.contains("_rejects_out_of_range_"),
"both extremes gated: nothing rejectable, no probe: {generated}"
);
}
#[test]
fn defaulted_constrained_field_gets_no_reject_probe() {
let d = constrained_design(serde_json::json!([
{ "name": "quantity", "type": "integer", "min": 1, "max": 600, "default": 1 }
]));
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
!generated.contains("_rejects_out_of_range_"),
"a defaulted field cannot be rejected at the boundary: {generated}"
);
}
#[test]
fn tenant_seeds_for_constrained_unique_fields_stay_in_range_and_distinct() {
let d: Design = serde_json::from_value(serde_json::json!({
"name": "crm-api",
"contract_version": 1,
"auth": { "model": "session", "roles": ["owner", "member"] },
"dependencies": ["db", "auth"],
"tenancy": { "entity": "Workspace", "member_roles": ["owner", "member"] },
"modules": [
{ "name": "workspaces",
"entities": [{ "name": "Workspace", "fields": [
{ "name": "id", "type": "integer" },
{ "name": "slots", "type": "integer", "unique": true, "min": 5, "max": 600 },
{ "name": "slug", "type": "string", "unique": true, "max_len": 8 }
]}],
"endpoints": [] },
{ "name": "leads",
"entities": [{ "name": "Lead",
"belongs_to": [{ "entity": "Workspace", "on_delete": "cascade" }],
"fields": [{ "name": "name", "type": "string" }] }],
"endpoints": [
{ "operation_id": "create_lead", "method": "POST", "path": "/", "auth_required": true,
"request_body": { "entity": "Lead" },
"success": { "status": 201, "entity": "Lead" } }
] }
]
}))
.unwrap();
let leads = d.modules.iter().find(|m| m.name == "leads").unwrap();
let generated = testgen::acceptance_rs(&d, leads);
assert!(
generated.contains("VALUES (1, 6, 'seed-tes')"),
"tenant-1 seed must be in-range and distinct from the fixture: {generated}"
);
assert!(
generated.contains("VALUES (2, 7, '2-test-v')"),
"tenant-2 seed must be in-range and distinct from tenant 1: {generated}"
);
}
#[test]
fn composite_unique_emits_a_409_conflict_test() {
const LIKES: &str = r#"{
"name": "likes-api", "contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "engagement",
"entities": [
{ "name": "User", "fields": [{ "name": "email", "type": "string" }] },
{ "name": "Post", "fields": [{ "name": "title", "type": "string" }] },
{ "name": "Like",
"belongs_to": [{ "entity": "User" }, { "entity": "Post" }],
"unique": [["user_id", "post_id"]],
"fields": [{ "name": "reaction", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_user", "method": "POST", "path": "/users",
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" } },
{ "operation_id": "create_post", "method": "POST", "path": "/posts",
"request_body": { "entity": "Post" },
"success": { "status": 201, "entity": "Post" } },
{ "operation_id": "create_like", "method": "POST", "path": "/likes",
"request_body": { "entity": "Like" },
"success": { "status": 201, "entity": "Like" } }
]
}]
}"#;
let d: Design = serde_json::from_str(LIKES).unwrap();
let module = &d.modules[0];
let generated = testgen::acceptance_rs(&d, module);
assert!(
generated.contains("async fn like_composite_unique_0_is_409()"),
"the composite-unique 409 test must be emitted:\n{generated}"
);
assert!(
generated.contains("/engagement/users") && generated.contains("/engagement/posts"),
"the 409 test must seed the User and Post parents:\n{generated}"
);
let body = generated
.split("async fn like_composite_unique_0_is_409()")
.nth(1)
.unwrap();
let body = body.split("\n}\n").next().unwrap();
assert_eq!(
body.matches("/engagement/likes").count(),
2,
"two POSTs to the likes collection (first + duplicate):\n{body}"
);
assert!(
body.contains("assert_eq!(dup.status().as_u16(), 409"),
"the duplicate insert must assert 409:\n{body}"
);
let plain = d.clone();
let mut v: serde_json::Value = serde_json::to_value(&plain).unwrap();
v["modules"][0]["entities"][2]
.as_object_mut()
.unwrap()
.remove("unique");
let plain: Design = serde_json::from_value(v).unwrap();
let plain_gen = testgen::acceptance_rs(&plain, &plain.modules[0]);
assert!(
!plain_gen.contains("_composite_unique_"),
"an entity with no composite unique emits no 409 conflict test:\n{plain_gen}"
);
}
#[test]
fn composite_unique_dup_bumps_competing_unique_columns_and_pk() {
const ENROLL: &str = r#"{
"name": "enroll-api", "contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "enrollments",
"entities": [
{ "name": "User", "fields": [{ "name": "email", "type": "string" }] },
{ "name": "Course", "fields": [{ "name": "title", "type": "string" }] },
{ "name": "Enrollment",
"belongs_to": [{ "entity": "User" }, { "entity": "Course" }],
"unique": [["user_id", "course_id"]],
"fields": [
{ "name": "id", "type": "string" },
{ "name": "code", "type": "string", "unique": true }
] }
],
"endpoints": [
{ "operation_id": "create_user", "method": "POST", "path": "/users",
"request_body": { "entity": "User" },
"success": { "status": 201, "entity": "User" } },
{ "operation_id": "create_course", "method": "POST", "path": "/courses",
"request_body": { "entity": "Course" },
"success": { "status": 201, "entity": "Course" } },
{ "operation_id": "create_enrollment", "method": "POST", "path": "/enrollments",
"request_body": { "entity": "Enrollment" },
"success": { "status": 201, "entity": "Enrollment" } }
]
}]
}"#;
let d: Design = serde_json::from_str(ENROLL).unwrap();
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
let body = generated
.split("async fn enrollment_composite_unique_0_is_409()")
.nth(1)
.expect("the composite-unique 409 test must be emitted");
let body = body.split("\n}\n").next().unwrap();
let first = body.split("let dup =").next().unwrap();
let dup = &body[body.find("let dup =").unwrap()..];
assert!(
first.contains("\"user_id\": 1") && first.contains("\"course_id\": 1"),
"the first body must carry the seeded group fks:\n{first}"
);
assert!(
dup.contains("\"user_id\": 1") && dup.contains("\"course_id\": 1"),
"the dup must AGREE with the first on the group columns:\n{dup}"
);
assert!(
first.contains("\"code\": \"test-value\""),
"the first body uses the `code` fixture value:\n{first}"
);
assert!(
dup.contains("\"code\": \"test-value-2\"") && !dup.contains("\"code\": \"test-value\""),
"the dup must bump the competing `code` unique to a DISTINCT value:\n{dup}"
);
assert!(
first.contains("\"id\": \"test-value\""),
"the first body carries the constant fixture pk:\n{first}"
);
assert!(
dup.contains("\"id\": \"test-value-2\"") && !dup.contains("\"id\": \"test-value\""),
"the dup must bump the explicit pk to a DISTINCT value:\n{dup}"
);
assert!(
dup.contains("assert_eq!(dup.status().as_u16(), 409"),
"the duplicate insert must assert 409:\n{dup}"
);
}
#[test]
fn composite_unique_skips_with_agent_todo_when_a_competing_group_masks() {
const TWO_GROUPS: &str = r#"{
"name": "pairs-api", "contract_version": 1,
"dependencies": ["db"],
"modules": [{
"name": "pairs",
"entities": [
{ "name": "A", "fields": [{ "name": "label", "type": "string" }] },
{ "name": "B", "fields": [{ "name": "label", "type": "string" }] },
{ "name": "C", "fields": [{ "name": "label", "type": "string" }] },
{ "name": "Pair",
"belongs_to": [{ "entity": "A" }, { "entity": "B" }, { "entity": "C" }],
"unique": [["a_id", "b_id"], ["a_id", "c_id"]],
"fields": [{ "name": "note", "type": "string" }] }
],
"endpoints": [
{ "operation_id": "create_a", "method": "POST", "path": "/as",
"request_body": { "entity": "A" },
"success": { "status": 201, "entity": "A" } },
{ "operation_id": "create_b", "method": "POST", "path": "/bs",
"request_body": { "entity": "B" },
"success": { "status": 201, "entity": "B" } },
{ "operation_id": "create_c", "method": "POST", "path": "/cs",
"request_body": { "entity": "C" },
"success": { "status": 201, "entity": "C" } },
{ "operation_id": "create_pair", "method": "POST", "path": "/pairs",
"request_body": { "entity": "Pair" },
"success": { "status": 201, "entity": "Pair" } }
]
}]
}"#;
let d: Design = serde_json::from_str(TWO_GROUPS).unwrap();
let generated = testgen::acceptance_rs(&d, &d.modules[0]);
assert!(
!generated.contains("_composite_unique_0_is_409()")
&& !generated.contains("_composite_unique_1_is_409()"),
"a masked composite group must NOT emit a false-green probe:\n{generated}"
);
assert!(
generated.contains("AGENT TODO: pair composite UNIQUE(a_id, b_id) (group #0)")
&& generated.contains("AGENT TODO: pair composite UNIQUE(a_id, c_id) (group #1)"),
"each masked composite group must emit an AGENT TODO:\n{generated}"
);
}