diff --git a/crates/path-cli/src/cmd_kind.rs b/crates/path-cli/src/cmd_kind.rs index 955bcf07..a649bf8b 100644 --- a/crates/path-cli/src/cmd_kind.rs +++ b/crates/path-cli/src/cmd_kind.rs @@ -82,7 +82,7 @@ mod tests { #[test] fn newest_version_resolves_and_is_json() { let k = kinds::resolve("agent-coding-session").unwrap(); - assert_eq!(k.version, "v1.1.0"); + assert_eq!(k.uri, toolpath::v1::PATH_KIND_AGENT_CODING_SESSION); let _: serde_json::Value = serde_json::from_str(k.schema).expect("bundled schema is valid JSON"); } diff --git a/crates/path-cli/src/kinds.rs b/crates/path-cli/src/kinds.rs index 34131bfd..d2c97c69 100644 --- a/crates/path-cli/src/kinds.rs +++ b/crates/path-cli/src/kinds.rs @@ -3,7 +3,8 @@ //! The binary ships a copy of every kind spec it knows about so that //! `path kind` and `path query --kind` work offline. [`BUNDLED_KINDS`] is the //! single source of truth for which `(name, version)` specs are baked in; -//! [`crate::schema`] (kind-aware validation) and the query layer both read it. +//! the `schema` module (kind-aware validation) and the query layer both read +//! it. //! //! A `meta.kind` value is a semver-versioned URI of the form //! `…/kinds//v..`. A [`KindSelector`] matches a @@ -196,6 +197,54 @@ mod tests { } } + /// Registration glue and schema internals must move together: each + /// entry's URI, its schema's `$id` and `meta.kind` const, and the + /// `kinds///` directory the schema is included from must + /// all name the same version, and the newest bundled URI must be the one + /// production stamps on derived paths. + #[test] + fn bundled_kinds_are_internally_consistent() { + for k in BUNDLED_KINDS { + let ctx = format!("{}/{}", k.name, k.version); + let schema: serde_json::Value = + serde_json::from_str(k.schema).expect("bundled schema is valid JSON"); + assert_eq!( + schema["$id"], + serde_json::json!(format!("{}/schema.json", k.uri)), + "{ctx}: schema `$id` must be the entry URI plus `/schema.json`" + ); + assert_eq!( + schema["properties"]["meta"]["properties"]["kind"]["const"], + serde_json::json!(k.uri), + "{ctx}: schema's `meta.kind` const must equal the entry URI" + ); + let (name, ver) = parse_kind_uri(k.uri).expect("bundled URI parses"); + assert_eq!(name, k.name, "{ctx}: URI name segment"); + assert_eq!( + format!("v{}.{}.{}", ver.major, ver.minor, ver.patch), + k.version, + "{ctx}: URI version segment" + ); + // `schema` is a compile-time include; require it byte-identical + // to the file in the directory named by `version`, so the include + // path can't point at a different version than the entry claims. + let on_disk = std::fs::read_to_string(format!( + "{}/kinds/{}/{}/schema.json", + env!("CARGO_MANIFEST_DIR"), + k.name, + k.version + )) + .expect("schema file exists under kinds///"); + assert_eq!( + on_disk, k.schema, + "{ctx}: bundled schema must be included from its version directory" + ); + } + + let newest = BUNDLED_KINDS.last().expect("at least one bundled kind"); + assert_eq!(newest.uri, toolpath::v1::PATH_KIND_AGENT_CODING_SESSION); + } + #[test] fn parse_uri_extracts_name_and_version() { let (name, ver) = @@ -256,7 +305,7 @@ mod tests { #[test] fn resolve_picks_newest_for_bare_name() { let k = resolve("agent-coding-session").expect("bundled"); - assert_eq!(k.version, "v1.1.0"); + assert_eq!(k.uri, toolpath::v1::PATH_KIND_AGENT_CODING_SESSION); } #[test] diff --git a/crates/path-cli/src/lib.rs b/crates/path-cli/src/lib.rs index 14ed9bba..e6c93367 100644 --- a/crates/path-cli/src/lib.rs +++ b/crates/path-cli/src/lib.rs @@ -33,7 +33,7 @@ mod fuzzy; #[cfg(not(target_os = "emscripten"))] pub mod harness; mod io; -mod kinds; +pub mod kinds; mod query; mod schema; #[cfg(all(not(target_os = "emscripten"), feature = "embedded-picker"))] diff --git a/crates/path-cli/src/query/mod.rs b/crates/path-cli/src/query/mod.rs index 08e079db..e4fe5a89 100644 --- a/crates/path-cli/src/query/mod.rs +++ b/crates/path-cli/src/query/mod.rs @@ -405,7 +405,7 @@ mod tests { fn wrap_graph_filters_by_kind() { let graph = Graph::from_path(forked_path()); let mut out = Vec::new(); - let sel = kinds::parse_kind_selector("agent-coding-session/v1.1.0"); + let sel = kinds::parse_kind_selector(toolpath::v1::PATH_KIND_AGENT_CODING_SESSION); wrap_graph(&doc_src("g"), &graph, Some(&sel), None, &mut out); assert_eq!(out.len(), 4, "matching kind keeps all steps"); diff --git a/crates/path-cli/src/schema.rs b/crates/path-cli/src/schema.rs index d98d33e4..79d62fff 100644 --- a/crates/path-cli/src/schema.rs +++ b/crates/path-cli/src/schema.rs @@ -222,7 +222,7 @@ mod tests { validate(&doc).expect("base is optional on path identity"); } - const ACS_KIND: &str = "https://toolpath.net/kinds/agent-coding-session/v1.1.0"; + const ACS_KIND: &str = toolpath::v1::PATH_KIND_AGENT_CODING_SESSION; fn acs_graph(append: serde_json::Value) -> serde_json::Value { json!({ diff --git a/crates/path-cli/tests/query.rs b/crates/path-cli/tests/query.rs index 5ebbf029..e17ced51 100644 --- a/crates/path-cli/tests/query.rs +++ b/crates/path-cli/tests/query.rs @@ -473,23 +473,25 @@ fn raw_unescapes_string_content() { #[test] fn kind_lists_bundled_kinds() { - cmd() - .arg("kind") - .assert() - .success() - .stdout(predicate::str::contains("agent-coding-session")) - .stdout(predicate::str::contains("v1.1.0")); + let mut assert = cmd().arg("kind").assert().success(); + for k in path_cli::kinds::BUNDLED_KINDS { + assert = assert + .stdout(predicate::str::contains(k.name)) + .stdout(predicate::str::contains(k.version)); + } } #[test] fn kind_prints_newest_schema() { + let newest = path_cli::kinds::resolve("agent-coding-session").expect("bundled"); cmd() .args(["kind", "agent-coding-session"]) .assert() .success() - .stdout(predicate::str::contains( - "kinds/agent-coding-session/v1.1.0/schema.json", - )); + .stdout(predicate::str::contains(format!( + "kinds/{}/{}/schema.json", + newest.name, newest.version + ))); } #[test] diff --git a/crates/toolpath-convo/src/derive.rs b/crates/toolpath-convo/src/derive.rs index b0d13266..1463b74f 100644 --- a/crates/toolpath-convo/src/derive.rs +++ b/crates/toolpath-convo/src/derive.rs @@ -863,9 +863,7 @@ mod tests { ); // ...and survives a JSON round-trip. let json = serde_json::to_string(&path).unwrap(); - assert!( - json.contains(r#""kind":"https://toolpath.net/kinds/agent-coding-session/v1.1.0""#) - ); + assert!(json.contains(&format!(r#""kind":"{PATH_KIND_AGENT_CODING_SESSION}""#))); } #[test] @@ -1100,9 +1098,15 @@ mod tests { "derive_path must stamp the agent-coding-session kind" ); - let schema_src = std::fs::read_to_string(concat!( - env!("CARGO_MANIFEST_DIR"), - "/../path-cli/kinds/agent-coding-session/v1.1.0/schema.json" + // Validate against the schema for the version the constant names, so + // this test tracks the current kind without a spelled-out version. + let version = PATH_KIND_AGENT_CODING_SESSION + .rsplit('/') + .next() + .expect("kind URI has a version segment"); + let schema_src = std::fs::read_to_string(format!( + "{}/../path-cli/kinds/agent-coding-session/{version}/schema.json", + env!("CARGO_MANIFEST_DIR") )) .expect("read kind schema"); let schema: serde_json::Value = serde_json::from_str(&schema_src).unwrap(); diff --git a/crates/toolpath/src/jsonl.rs b/crates/toolpath/src/jsonl.rs index 17139da6..81e7f9f8 100644 --- a/crates/toolpath/src/jsonl.rs +++ b/crates/toolpath/src/jsonl.rs @@ -1249,9 +1249,10 @@ mod tests { }), }; let jsonl = p.to_jsonl_string().unwrap(); - assert!( - jsonl.contains(r#""kind":"https://toolpath.net/kinds/agent-coding-session/v1.1.0""#) - ); + assert!(jsonl.contains(&format!( + r#""kind":"{}""#, + crate::v1::PATH_KIND_AGENT_CODING_SESSION + ))); let back = Path::from_jsonl_str(&jsonl).unwrap(); assert_eq!(canonical_json(&p), canonical_json(&back)); } @@ -1259,14 +1260,14 @@ mod tests { #[test] fn path_meta_line_can_set_kind() { let patch = PathMetaPatch { - kind: Some("https://toolpath.net/kinds/agent-coding-session/v1.1.0".into()), + kind: Some(crate::v1::PATH_KIND_AGENT_CODING_SESSION.into()), ..Default::default() }; let mut meta = PathMeta::default(); apply_meta_patch(&mut meta, patch); assert_eq!( meta.kind.as_deref(), - Some("https://toolpath.net/kinds/agent-coding-session/v1.1.0") + Some(crate::v1::PATH_KIND_AGENT_CODING_SESSION) ); } diff --git a/crates/toolpath/src/types.rs b/crates/toolpath/src/types.rs index d1be6137..0d84d534 100644 --- a/crates/toolpath/src/types.rs +++ b/crates/toolpath/src/types.rs @@ -835,13 +835,19 @@ mod tests { ..Default::default() }; let json = serde_json::to_string(&meta).unwrap(); - assert!( - json.contains(r#""kind":"https://toolpath.net/kinds/agent-coding-session/v1.1.0""#) - ); + assert!(json.contains(&format!(r#""kind":"{PATH_KIND_AGENT_CODING_SESSION}""#))); let parsed: PathMeta = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.kind.as_deref(), Some(PATH_KIND_AGENT_CODING_SESSION)); + } + + /// The one test that spells the current kind URI out literally: every + /// other test goes through the constant, so a typo in the constant would + /// otherwise self-validate. + #[test] + fn test_path_kind_constant_spells_current_uri() { assert_eq!( - parsed.kind.as_deref(), - Some("https://toolpath.net/kinds/agent-coding-session/v1.1.0") + PATH_KIND_AGENT_CODING_SESSION, + "https://toolpath.net/kinds/agent-coding-session/v1.1.0" ); }