diff --git a/crates/geth/tests/bootstrap.rs b/crates/geth/tests/bootstrap.rs index f0ae8b8..c40976e 100644 --- a/crates/geth/tests/bootstrap.rs +++ b/crates/geth/tests/bootstrap.rs @@ -32,6 +32,40 @@ fn run_geth(home: &std::path::Path, args: &[&str]) -> std::process::Output { .expect("run geth") } +fn json_fixture(name: &str) -> serde_json::Value { + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/json") + .join(name); + serde_json::from_str(&std::fs::read_to_string(&path).expect("read json fixture")) + .expect("decode json fixture") +} + +fn command_json(output: &std::process::Output) -> serde_json::Value { + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).expect("decode command json") +} + +fn normalize_node_id_json(mut value: serde_json::Value) -> serde_json::Value { + let object = value.as_object_mut().expect("node id json object"); + object.insert( + "agent_id".to_owned(), + serde_json::Value::String("agent:".to_owned()), + ); + object.insert( + "node_id".to_owned(), + serde_json::Value::String("node:".to_owned()), + ); + object.insert( + "endpoint_id".to_owned(), + serde_json::Value::String("".to_owned()), + ); + value +} + fn spawn_daemon(home: &std::path::Path) -> Child { Command::new(geth_bin()) .env("GETH_HOME", home) @@ -248,6 +282,43 @@ fn geth_status_against_running_daemon() { )); } +#[test] +fn stable_json_output_matches_fixtures() { + let home = tempfile::tempdir().expect("tempdir"); + if !unix_sockets_available(home.path()) { + return; + } + assert!(run_geth(home.path(), &["init"]).status.success()); + std::fs::write( + home.path().join("config.toml"), + "[iroh]\nrelay_mode = \"disabled\"\nlocal_discovery = false\n", + ) + .expect("write config"); + + let mut daemon = spawn_daemon(home.path()); + wait_for_socket(&home.path().join("run/geth.sock")); + + let node_id = run_geth(home.path(), &["--json", "node", "id"]); + let resource_list = run_geth(home.path(), &["--json", "resource", "list"]); + let sync_status = run_geth(home.path(), &["--json", "sync", "status"]); + + let _ = daemon.kill(); + let _ = daemon.wait(); + + assert_eq!( + normalize_node_id_json(command_json(&node_id)), + json_fixture("node-id.json") + ); + assert_eq!( + command_json(&resource_list), + json_fixture("resource-list-fresh.json") + ); + assert_eq!( + command_json(&sync_status), + json_fixture("sync-status-empty.json") + ); +} + #[test] fn peer_ping_uses_daemon_owned_iroh_endpoint() { if skip_iroh_integration_tests() { diff --git a/crates/geth/tests/fixtures/json/README.md b/crates/geth/tests/fixtures/json/README.md new file mode 100644 index 0000000..820f93d --- /dev/null +++ b/crates/geth/tests/fixtures/json/README.md @@ -0,0 +1,9 @@ +# JSON Fixtures + +These fixtures define reviewed, script-facing `geth --json` shapes. Tests may +normalize volatile local values such as generated node IDs, agent IDs, endpoint +IDs, and temporary paths before comparing output. + +Changing a fixture is a compatibility decision. Update the matching roadmap +item and compatibility notes when a stable field is added, removed, renamed, or +retyped. diff --git a/crates/geth/tests/fixtures/json/node-id.json b/crates/geth/tests/fixtures/json/node-id.json new file mode 100644 index 0000000..97c769a --- /dev/null +++ b/crates/geth/tests/fixtures/json/node-id.json @@ -0,0 +1,6 @@ +{ + "type": "node-id", + "agent_id": "agent:", + "node_id": "node:", + "endpoint_id": "" +} diff --git a/crates/geth/tests/fixtures/json/resource-list-fresh.json b/crates/geth/tests/fixtures/json/resource-list-fresh.json new file mode 100644 index 0000000..10c2f96 --- /dev/null +++ b/crates/geth/tests/fixtures/json/resource-list-fresh.json @@ -0,0 +1,17 @@ +{ + "type": "resource-list", + "resources": [ + { + "id": "resource:cas:local", + "kind": "cas", + "name": "local-cas", + "authority": { + "kind": "local" + }, + "local_role": "owner", + "replication": "local-only", + "retention": "keep", + "status": "active" + } + ] +} diff --git a/crates/geth/tests/fixtures/json/sync-status-empty.json b/crates/geth/tests/fixtures/json/sync-status-empty.json new file mode 100644 index 0000000..7926f21 --- /dev/null +++ b/crates/geth/tests/fixtures/json/sync-status-empty.json @@ -0,0 +1,5 @@ +{ + "type": "sync-status", + "peers": [], + "note": "sync status is local daemon health for best-effort live sync; signed logs remain the durable source of truth" +} diff --git a/docs/production-readiness-roadmap.md b/docs/production-readiness-roadmap.md index 56a7a80..60f3f7b 100644 --- a/docs/production-readiness-roadmap.md +++ b/docs/production-readiness-roadmap.md @@ -79,7 +79,7 @@ behavior. - `[x]` Sync stream selection, watermarks, and run result helpers live in a sync-focused module. - `[x]` Per-module sync handlers have consistent interfaces. - - `[ ]` `geth sync status --json` output remains stable. + - `[x]` `geth sync status --json` output remains stable. ## Phase 2: Stable Automation Contracts @@ -94,12 +94,12 @@ and downstream projects. - `[x]` Peer wire protocol compatibility is documented. - `[x]` SQLite and signed-operation compatibility are documented. -- `[ ]` Add golden JSON tests. +- `[~]` Add golden JSON tests. Acceptance criteria: - - `[ ]` Important script-facing commands have stable JSON fixture tests. + - `[x]` Important script-facing commands have stable JSON fixture tests. - `[ ]` Error JSON includes stable codes for common operator and automation failures. - - `[ ]` Fixture updates require intentional review. + - `[x]` Fixture updates require intentional review. - `[~]` Expand protocol roundtrip tests. Acceptance criteria: