use pushkin_core::manifest::Manifest;
use pushkin_core::mapper::{resolve_reference, slices_for_write, ReferenceError};
const MANIFEST: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user"
source = "contracts/user.zod.ts"
emit = ["zod"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user"]
require = "boundary-validation"
[gates]
protected_paths = ["pushkin.toml"]
"#;
const MANIFEST_TWO: &str = r#"
version = 1
canonical = "json-schema-2020-12"
authoring = "zod"
[[contracts]]
name = "user-api"
source = "contracts/user-api.zod.ts"
emit = ["zod"]
[[contracts]]
name = "user-admin"
source = "contracts/user-admin.zod.ts"
emit = ["zod"]
[[mappings]]
glob = "app/api/**/*.ts"
contracts = ["user-api"]
[gates]
protected_paths = []
"#;
const CONTRACT_SOURCE: &str = "import { z } from \"zod\";\n\n\
export const UserCreateSchema = z.object({ name: z.string(), email: z.string() }).strict();\n\
export const UserAdminSchema = z.object({ role: z.string() }).strict();\n";
const HANDLER_TOUCHING_CREATE_ONLY: &str =
"import { UserCreateSchema } from \"../../../contracts/user.zod\";\n\n\
export async function POST(req: Request) {\n\
const body = UserCreateSchema.parse(await req.json());\n\
return Response.json(body);\n\
}\n";
#[test]
fn glob_maps_touched_file_to_contract_slice() {
let manifest = Manifest::parse(MANIFEST).unwrap();
let sources = [("user", CONTRACT_SOURCE)];
let slices = slices_for_write(
&manifest,
"app/api/users/route.ts",
HANDLER_TOUCHING_CREATE_ONLY,
&sources,
);
assert_eq!(slices.len(), 1, "one mapped contract, one slice");
assert_eq!(slices[0].contract.as_str(), "user");
let unmapped = slices_for_write(&manifest, "docs/notes.ts", "const x = 1;", &sources);
assert!(unmapped.is_empty(), "unmapped paths produce no slices");
}
#[test]
fn symbol_slice_covers_only_touched_symbols() {
let manifest = Manifest::parse(MANIFEST).unwrap();
let sources = [("user", CONTRACT_SOURCE)];
let slices = slices_for_write(
&manifest,
"app/api/users/route.ts",
HANDLER_TOUCHING_CREATE_ONLY,
&sources,
);
assert_eq!(
slices[0].symbols,
vec!["UserCreateSchema".to_owned()],
"handler touches UserCreateSchema only — UserAdminSchema must not be in the slice"
);
}
#[test]
fn shorthand_resolves_once_at_boundary_to_canonical_ref() {
let manifest = Manifest::parse(MANIFEST).unwrap();
assert_eq!(
resolve_reference(&manifest, "user").unwrap().as_str(),
"user"
);
assert_eq!(
resolve_reference(&manifest, "contracts/user.zod.ts")
.unwrap()
.as_str(),
"user"
);
}
#[test]
fn ambiguous_contract_reference_returns_candidates() {
let manifest = Manifest::parse(MANIFEST_TWO).unwrap();
let error = resolve_reference(&manifest, "user").unwrap_err();
let ReferenceError::Ambiguous { candidates, .. } = &error else {
panic!("expected Ambiguous, got: {error}");
};
assert!(candidates.contains("user-api"), "names both: {candidates}");
assert!(
candidates.contains("user-admin"),
"names both: {candidates}"
);
}
#[test]
fn unknown_reference_is_structured_error_not_silent_miss() {
let manifest = Manifest::parse(MANIFEST).unwrap();
let error = resolve_reference(&manifest, "uzer").unwrap_err();
let ReferenceError::Unknown { candidates, .. } = &error else {
panic!("expected Unknown, got: {error}");
};
assert!(
candidates.contains("user"),
"near-miss candidate named: {candidates}"
);
}