Files
claw-code/rust/crates/rusty-claude-cli/tests/output_format_contract.rs

952 lines
35 KiB
Rust
Raw Normal View History

Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
Prevent cross-worktree session bleed during managed session resume/load ROADMAP #41 was still leaving a phantom-completion class open: managed sessions could be resumed from the wrong workspace, and the CLI/runtime paths were split between partially isolated storage and older helper flows. This squashes the verified team work into one deliverable that routes managed session operations through the per-worktree SessionStore, rejects workspace mismatches explicitly, extends lane-event taxonomy for workspace mismatch reporting, and updates the affected CLI regression fixtures/docs so the new contract is enforced without losing same- workspace legacy coverage. Constraint: Keep same-workspace legacy flat sessions readable while blocking cross-worktree misuse Constraint: No new dependencies; stay within the ROADMAP #41 changed-file scope Rejected: Leave team auto-checkpoint history as final branch state | noisy/non-lore history for a single roadmap fix Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve workspace_root validation on future resume/load helpers; do not reintroduce path-only fallback without equivalent mismatch checks Tested: cargo test -p runtime session_control -- --nocapture; cargo test -p rusty-claude-cli resume -- --nocapture; cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults; cargo test -p rusty-claude-cli --test output_format_contract; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test --workspace --exclude compat-harness; cargo check --workspace --all-targets; git diff --check Not-tested: cargo clippy --workspace --all-targets -- -D warnings (pre-existing failures in unchanged rust/crates/rusty-claude-cli/build.rs) Related: ROADMAP #41
2026-04-11 16:08:01 +00:00
use runtime::Session;
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
use serde_json::Value;
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
#[test]
fn help_emits_json_when_requested() {
let root = unique_temp_dir("help-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let parsed = assert_json_command(&root, &["--output-format", "json", "help"]);
assert_eq!(parsed["kind"], "help");
assert!(parsed["message"]
.as_str()
.expect("help text")
.contains("Usage:"));
}
#[test]
fn version_emits_json_when_requested() {
let root = unique_temp_dir("version-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let parsed = assert_json_command(&root, &["--output-format", "json", "version"]);
assert_eq!(parsed["kind"], "version");
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
}
#[test]
fn status_and_sandbox_emit_json_when_requested() {
let root = unique_temp_dir("status-sandbox-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let status = assert_json_command(&root, &["--output-format", "json", "status"]);
assert_eq!(status["kind"], "status");
assert!(status["workspace"]["cwd"].as_str().is_some());
let sandbox = assert_json_command(&root, &["--output-format", "json", "sandbox"]);
assert_eq!(sandbox["kind"], "sandbox");
assert!(sandbox["filesystem_mode"].as_str().is_some());
}
#[test]
fn acp_guidance_emits_json_when_requested() {
let root = unique_temp_dir("acp-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let acp = assert_json_command(&root, &["--output-format", "json", "acp"]);
assert_eq!(acp["kind"], "acp");
assert_eq!(acp["status"], "discoverability_only");
assert_eq!(acp["supported"], false);
assert_eq!(acp["serve_alias_only"], true);
assert_eq!(acp["discoverability_tracking"], "ROADMAP #64a");
assert_eq!(acp["tracking"], "ROADMAP #76");
assert!(acp["message"]
.as_str()
.expect("acp message")
.contains("discoverability alias"));
}
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
#[test]
fn inventory_commands_emit_structured_json_when_requested() {
let root = unique_temp_dir("inventory-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let isolated_home = root.join("home");
let isolated_config = root.join("config-home");
let isolated_codex = root.join("codex-home");
fs::create_dir_all(&isolated_home).expect("isolated home should exist");
let agents = assert_json_command_with_env(
&root,
&["--output-format", "json", "agents"],
&[
("HOME", isolated_home.to_str().expect("utf8 home")),
(
"CLAW_CONFIG_HOME",
isolated_config.to_str().expect("utf8 config home"),
),
(
"CODEX_HOME",
isolated_codex.to_str().expect("utf8 codex home"),
),
],
);
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
assert_eq!(agents["kind"], "agents");
assert_eq!(agents["action"], "list");
assert_eq!(agents["count"], 0);
assert_eq!(agents["summary"]["active"], 0);
assert!(agents["agents"]
.as_array()
.expect("agents array")
.is_empty());
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
let mcp = assert_json_command(&root, &["--output-format", "json", "mcp"]);
assert_eq!(mcp["kind"], "mcp");
assert_eq!(mcp["action"], "list");
let skills = assert_json_command(&root, &["--output-format", "json", "skills"]);
assert_eq!(skills["kind"], "skills");
assert_eq!(skills["action"], "list");
}
#[test]
fn agents_command_emits_structured_agent_entries_when_requested() {
let root = unique_temp_dir("agents-json-populated");
let workspace = root.join("workspace");
let project_agents = workspace.join(".codex").join("agents");
let home = root.join("home");
let user_agents = home.join(".codex").join("agents");
let isolated_config = root.join("config-home");
let isolated_codex = root.join("codex-home");
fs::create_dir_all(&workspace).expect("workspace should exist");
write_agent(
&project_agents,
"planner",
"Project planner",
"gpt-5.4",
"medium",
);
write_agent(
&project_agents,
"verifier",
"Verification agent",
"gpt-5.4-mini",
"high",
);
write_agent(
&user_agents,
"planner",
"User planner",
"gpt-5.4-mini",
"high",
);
let parsed = assert_json_command_with_env(
&workspace,
&["--output-format", "json", "agents"],
&[
("HOME", home.to_str().expect("utf8 home")),
(
"CLAW_CONFIG_HOME",
isolated_config.to_str().expect("utf8 config home"),
),
(
"CODEX_HOME",
isolated_codex.to_str().expect("utf8 codex home"),
),
],
);
assert_eq!(parsed["kind"], "agents");
assert_eq!(parsed["action"], "list");
assert_eq!(parsed["count"], 3);
assert_eq!(parsed["summary"]["active"], 2);
assert_eq!(parsed["summary"]["shadowed"], 1);
assert_eq!(parsed["agents"][0]["name"], "planner");
assert_eq!(parsed["agents"][0]["source"]["id"], "project_claw");
assert_eq!(parsed["agents"][0]["active"], true);
assert_eq!(parsed["agents"][1]["name"], "verifier");
assert_eq!(parsed["agents"][2]["name"], "planner");
assert_eq!(parsed["agents"][2]["active"], false);
assert_eq!(parsed["agents"][2]["shadowed_by"]["id"], "project_claw");
}
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
#[test]
fn bootstrap_and_system_prompt_emit_json_when_requested() {
let root = unique_temp_dir("bootstrap-system-prompt-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let plan = assert_json_command(&root, &["--output-format", "json", "bootstrap-plan"]);
assert_eq!(plan["kind"], "bootstrap-plan");
assert!(plan["phases"].as_array().expect("phases").len() > 1);
let prompt = assert_json_command(&root, &["--output-format", "json", "system-prompt"]);
assert_eq!(prompt["kind"], "system-prompt");
assert!(prompt["message"]
.as_str()
.expect("prompt text")
.contains("interactive agent"));
}
#[test]
fn dump_manifests_and_init_emit_json_when_requested() {
let root = unique_temp_dir("manifest-init-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let upstream = write_upstream_fixture(&root);
let manifests = assert_json_command(
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
&root,
&[
"--output-format",
"json",
"dump-manifests",
"--manifests-dir",
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
upstream.to_str().expect("utf8 upstream"),
],
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
);
assert_eq!(manifests["kind"], "dump-manifests");
assert_eq!(manifests["commands"], 1);
assert_eq!(manifests["tools"], 1);
let workspace = root.join("workspace");
fs::create_dir_all(&workspace).expect("workspace should exist");
let init = assert_json_command(&workspace, &["--output-format", "json", "init"]);
assert_eq!(init["kind"], "init");
assert!(workspace.join("CLAUDE.md").exists());
}
#[test]
fn doctor_and_resume_status_emit_json_when_requested() {
let root = unique_temp_dir("doctor-resume-json");
fs::create_dir_all(&root).expect("temp dir should exist");
let doctor = assert_json_command(&root, &["--output-format", "json", "doctor"]);
assert_eq!(doctor["kind"], "doctor");
assert!(doctor["message"].is_string());
let summary = doctor["summary"].as_object().expect("doctor summary");
assert!(summary["ok"].as_u64().is_some());
assert!(summary["warnings"].as_u64().is_some());
assert!(summary["failures"].as_u64().is_some());
let checks = doctor["checks"].as_array().expect("doctor checks");
assert_eq!(checks.len(), 6);
let check_names = checks
.iter()
.map(|check| {
assert!(check["status"].as_str().is_some());
assert!(check["summary"].as_str().is_some());
assert!(check["details"].is_array());
check["name"].as_str().expect("doctor check name")
})
.collect::<Vec<_>>();
assert_eq!(
check_names,
vec![
"auth",
"config",
"install source",
"workspace",
"sandbox",
"system"
]
);
let install_source = checks
.iter()
.find(|check| check["name"] == "install source")
.expect("install source check");
assert_eq!(
install_source["official_repo"],
"https://github.com/ultraworkers/claw-code"
);
assert_eq!(
install_source["deprecated_install"],
"cargo install claw-code"
);
let workspace = checks
.iter()
.find(|check| check["name"] == "workspace")
.expect("workspace check");
assert!(workspace["cwd"].as_str().is_some());
assert!(workspace["in_git_repo"].is_boolean());
let sandbox = checks
.iter()
.find(|check| check["name"] == "sandbox")
.expect("sandbox check");
assert!(sandbox["filesystem_mode"].as_str().is_some());
assert!(sandbox["enabled"].is_boolean());
assert!(sandbox["fallback_reason"].is_null() || sandbox["fallback_reason"].is_string());
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
Prevent cross-worktree session bleed during managed session resume/load ROADMAP #41 was still leaving a phantom-completion class open: managed sessions could be resumed from the wrong workspace, and the CLI/runtime paths were split between partially isolated storage and older helper flows. This squashes the verified team work into one deliverable that routes managed session operations through the per-worktree SessionStore, rejects workspace mismatches explicitly, extends lane-event taxonomy for workspace mismatch reporting, and updates the affected CLI regression fixtures/docs so the new contract is enforced without losing same- workspace legacy coverage. Constraint: Keep same-workspace legacy flat sessions readable while blocking cross-worktree misuse Constraint: No new dependencies; stay within the ROADMAP #41 changed-file scope Rejected: Leave team auto-checkpoint history as final branch state | noisy/non-lore history for a single roadmap fix Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve workspace_root validation on future resume/load helpers; do not reintroduce path-only fallback without equivalent mismatch checks Tested: cargo test -p runtime session_control -- --nocapture; cargo test -p rusty-claude-cli resume -- --nocapture; cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults; cargo test -p rusty-claude-cli --test output_format_contract; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test --workspace --exclude compat-harness; cargo check --workspace --all-targets; git diff --check Not-tested: cargo clippy --workspace --all-targets -- -D warnings (pre-existing failures in unchanged rust/crates/rusty-claude-cli/build.rs) Related: ROADMAP #41
2026-04-11 16:08:01 +00:00
let session_path = write_session_fixture(&root, "resume-json", Some("hello"));
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
let resumed = assert_json_command(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/status",
],
);
assert_eq!(resumed["kind"], "status");
// model is null in resume mode (not known without --model flag)
assert!(resumed["model"].is_null());
assert_eq!(resumed["usage"]["messages"], 1);
assert!(resumed["workspace"]["cwd"].as_str().is_some());
assert!(resumed["sandbox"]["filesystem_mode"].as_str().is_some());
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
}
#[test]
fn resumed_inventory_commands_emit_structured_json_when_requested() {
let root = unique_temp_dir("resume-inventory-json");
let config_home = root.join("config-home");
let home = root.join("home");
fs::create_dir_all(&config_home).expect("config home should exist");
fs::create_dir_all(&home).expect("home should exist");
Prevent cross-worktree session bleed during managed session resume/load ROADMAP #41 was still leaving a phantom-completion class open: managed sessions could be resumed from the wrong workspace, and the CLI/runtime paths were split between partially isolated storage and older helper flows. This squashes the verified team work into one deliverable that routes managed session operations through the per-worktree SessionStore, rejects workspace mismatches explicitly, extends lane-event taxonomy for workspace mismatch reporting, and updates the affected CLI regression fixtures/docs so the new contract is enforced without losing same- workspace legacy coverage. Constraint: Keep same-workspace legacy flat sessions readable while blocking cross-worktree misuse Constraint: No new dependencies; stay within the ROADMAP #41 changed-file scope Rejected: Leave team auto-checkpoint history as final branch state | noisy/non-lore history for a single roadmap fix Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve workspace_root validation on future resume/load helpers; do not reintroduce path-only fallback without equivalent mismatch checks Tested: cargo test -p runtime session_control -- --nocapture; cargo test -p rusty-claude-cli resume -- --nocapture; cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults; cargo test -p rusty-claude-cli --test output_format_contract; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test --workspace --exclude compat-harness; cargo check --workspace --all-targets; git diff --check Not-tested: cargo clippy --workspace --all-targets -- -D warnings (pre-existing failures in unchanged rust/crates/rusty-claude-cli/build.rs) Related: ROADMAP #41
2026-04-11 16:08:01 +00:00
let session_path = write_session_fixture(&root, "resume-inventory-json", Some("inventory"));
let mcp = assert_json_command_with_env(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/mcp",
],
&[
(
"CLAW_CONFIG_HOME",
config_home.to_str().expect("utf8 config home"),
),
("HOME", home.to_str().expect("utf8 home")),
],
);
assert_eq!(mcp["kind"], "mcp");
assert_eq!(mcp["action"], "list");
assert!(mcp["servers"].is_array());
let skills = assert_json_command_with_env(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/skills",
],
&[
(
"CLAW_CONFIG_HOME",
config_home.to_str().expect("utf8 config home"),
),
("HOME", home.to_str().expect("utf8 home")),
],
);
assert_eq!(skills["kind"], "skills");
assert_eq!(skills["action"], "list");
assert!(skills["summary"]["total"].is_number());
assert!(skills["skills"].is_array());
}
#[test]
fn resumed_version_and_init_emit_structured_json_when_requested() {
let root = unique_temp_dir("resume-version-init-json");
fs::create_dir_all(&root).expect("temp dir should exist");
Prevent cross-worktree session bleed during managed session resume/load ROADMAP #41 was still leaving a phantom-completion class open: managed sessions could be resumed from the wrong workspace, and the CLI/runtime paths were split between partially isolated storage and older helper flows. This squashes the verified team work into one deliverable that routes managed session operations through the per-worktree SessionStore, rejects workspace mismatches explicitly, extends lane-event taxonomy for workspace mismatch reporting, and updates the affected CLI regression fixtures/docs so the new contract is enforced without losing same- workspace legacy coverage. Constraint: Keep same-workspace legacy flat sessions readable while blocking cross-worktree misuse Constraint: No new dependencies; stay within the ROADMAP #41 changed-file scope Rejected: Leave team auto-checkpoint history as final branch state | noisy/non-lore history for a single roadmap fix Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve workspace_root validation on future resume/load helpers; do not reintroduce path-only fallback without equivalent mismatch checks Tested: cargo test -p runtime session_control -- --nocapture; cargo test -p rusty-claude-cli resume -- --nocapture; cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults; cargo test -p rusty-claude-cli --test output_format_contract; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test --workspace --exclude compat-harness; cargo check --workspace --all-targets; git diff --check Not-tested: cargo clippy --workspace --all-targets -- -D warnings (pre-existing failures in unchanged rust/crates/rusty-claude-cli/build.rs) Related: ROADMAP #41
2026-04-11 16:08:01 +00:00
let session_path = write_session_fixture(&root, "resume-version-init-json", None);
let version = assert_json_command(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/version",
],
);
assert_eq!(version["kind"], "version");
assert_eq!(version["version"], env!("CARGO_PKG_VERSION"));
let init = assert_json_command(
&root,
&[
"--output-format",
"json",
"--resume",
session_path.to_str().expect("utf8 session path"),
"/init",
],
);
assert_eq!(init["kind"], "init");
assert!(root.join("CLAUDE.md").exists());
}
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {
assert_json_command_with_env(current_dir, args, &[])
}
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
/// #247 regression helper: run claw expecting a non-zero exit and return
/// the JSON error envelope parsed from stdout. Asserts exit != 0 and that
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
/// the envelope includes `type: "error"` at the very least.
///
/// #168c: Error envelopes under --output-format json are now emitted to
/// STDOUT (not stderr). This matches the emission contract that stdout
/// carries the contractual envelope (success OR error) while stderr is
/// reserved for non-contractual diagnostics.
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
fn assert_json_error_envelope(current_dir: &Path, args: &[&str]) -> Value {
let output = run_claw(current_dir, args, &[]);
assert!(
!output.status.success(),
"command unexpectedly succeeded; stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
// #168c: The JSON envelope is written to STDOUT for error cases under
// --output-format json (see main.rs). Previously was stderr.
let envelope: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|err| {
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
panic!(
"stdout should be a JSON error envelope but failed to parse: {err}\nstdout bytes:\n{}\nstderr bytes:\n{}",
String::from_utf8_lossy(&output.stdout),
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
String::from_utf8_lossy(&output.stderr)
)
});
assert_eq!(
envelope["type"], "error",
"envelope should carry type=error"
);
envelope
}
/// #168c regression test: under `--output-format json`, error envelopes
/// must be emitted to STDOUT (not stderr). This is the emission contract:
/// stdout carries the JSON envelope regardless of success/error; stderr
/// is reserved for non-contractual diagnostics.
///
/// Refutes cycle #84's "bootstrap silent failure" claim (cycle #87 controlled
/// matrix showed errors were on stderr, not silent; cycle #88 locked the
/// emission contract to require stdout).
#[test]
fn error_envelope_emitted_to_stdout_under_output_format_json_168c() {
let root = unique_temp_dir("168c-emission-stdout");
fs::create_dir_all(&root).expect("temp dir should exist");
// Trigger an error via `prompt` without arg (known cli_parse error).
let output = run_claw(&root, &["--output-format", "json", "prompt"], &[]);
// Exit code must be non-zero (error).
assert!(
!output.status.success(),
"prompt without arg must fail; stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
// #168c primary assertion: stdout carries the JSON envelope.
let stdout_text = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout_text.trim().is_empty(),
"stdout must contain JSON envelope under --output-format json (#168c emission contract). stderr was:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let envelope: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|err| {
panic!(
"stdout should be valid JSON under --output-format json (#168c): {err}\nstdout bytes:\n{stdout_text}"
)
});
assert_eq!(envelope["type"], "error", "envelope must be typed error");
assert!(
envelope["kind"].as_str().is_some(),
"envelope must carry machine-readable kind"
);
// #168c secondary assertion: stderr should NOT carry the JSON envelope
// (it may be empty or contain non-JSON diagnostics, but the envelope
// belongs on stdout under --output-format json).
let stderr_text = String::from_utf8_lossy(&output.stderr);
let stderr_trimmed = stderr_text.trim();
if !stderr_trimmed.is_empty() {
// If stderr has content, it must NOT be the JSON envelope.
let stderr_is_json: Result<Value, _> = serde_json::from_slice(&output.stderr);
assert!(
stderr_is_json.is_err(),
"stderr must not duplicate the JSON envelope (#168c); stderr was:\n{stderr_trimmed}"
);
}
}
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
#[test]
fn prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247() {
// #247: `claw prompt` with no argument must classify as `cli_parse`
// (not `unknown`) and the JSON envelope must carry the same actionable
// `Run claw --help for usage.` hint that text-mode stderr appends.
let root = unique_temp_dir("247-prompt-no-arg");
fs::create_dir_all(&root).expect("temp dir should exist");
let envelope = assert_json_error_envelope(&root, &["--output-format", "json", "prompt"]);
assert_eq!(
envelope["kind"], "cli_parse",
"prompt subcommand without arg should classify as cli_parse, envelope: {envelope}"
);
assert_eq!(
envelope["error"], "prompt subcommand requires a prompt string",
"short reason should match the raw error, envelope: {envelope}"
);
assert_eq!(
envelope["hint"],
"Run `claw --help` for usage.",
"JSON envelope must carry the same help-runbook hint as text mode, envelope: {envelope}"
);
}
#[test]
fn empty_positional_arg_emits_cli_parse_envelope_247() {
// #247: `claw ""` must classify as `cli_parse`, not `unknown`. The
// message itself embeds a ``run `claw --help`` pointer so the explicit
// hint field is allowed to remain null to avoid duplication — what
// matters for the typed-error contract is that `kind == cli_parse`.
let root = unique_temp_dir("247-empty-arg");
fs::create_dir_all(&root).expect("temp dir should exist");
let envelope = assert_json_error_envelope(&root, &["--output-format", "json", ""]);
assert_eq!(
envelope["kind"], "cli_parse",
"empty-prompt error should classify as cli_parse, envelope: {envelope}"
);
let short = envelope["error"]
.as_str()
.expect("error field should be a string");
assert!(
short.starts_with("empty prompt:"),
"short reason should preserve the original empty-prompt message, got: {short}"
);
}
#[test]
fn whitespace_only_positional_arg_emits_cli_parse_envelope_247() {
// #247: same rule for `claw " "` — any whitespace-only prompt must
// flow through the empty-prompt path and classify as `cli_parse`.
let root = unique_temp_dir("247-whitespace-arg");
fs::create_dir_all(&root).expect("temp dir should exist");
let envelope = assert_json_error_envelope(&root, &["--output-format", "json", " "]);
assert_eq!(
envelope["kind"], "cli_parse",
"whitespace-only prompt should classify as cli_parse, envelope: {envelope}"
);
}
/// #168c Phase 0 Task 2: No-silent guarantee.
///
/// Under `--output-format json`, every verb must satisfy the emission contract:
/// either emit a valid JSON envelope to stdout (with exit 0 for success, or
/// exit != 0 for error), OR exit with an error code. Silent success (exit 0
/// with empty stdout) is forbidden under the JSON contract because consumers
/// cannot distinguish success from broken emission.
///
/// This test iterates a catalog of clawable verbs and asserts:
/// 1. Each verb produces stdout output when exit == 0 (no silent success)
/// 2. The stdout output parses as JSON (emission contract integrity)
/// 3. Error cases (exit != 0) produce JSON on stdout (#168c routing fix)
///
/// Phase 0 Task 2 deliverable: prevents regressions in the emission contract
/// for the full set of discoverable verbs.
#[test]
fn emission_contract_no_silent_success_under_output_format_json_168c_task2() {
let root = unique_temp_dir("168c-task2-no-silent");
fs::create_dir_all(&root).expect("temp dir should exist");
// Verbs expected to succeed (exit 0) with non-empty JSON on stdout.
// Covers the discovery-safe subset — verbs that don't require external
// credentials or network and should be safely invokable in CI.
let safe_success_verbs: &[(&str, &[&str])] = &[
("help", &["help"]),
("version", &["version"]),
("list-sessions", &["list-sessions"]),
("doctor", &["doctor"]),
("mcp", &["mcp"]),
("skills", &["skills"]),
("agents", &["agents"]),
("sandbox", &["sandbox"]),
("status", &["status"]),
("system-prompt", &["system-prompt"]),
("bootstrap-plan", &["bootstrap-plan", "test"]),
("acp", &["acp"]),
];
for (verb, args) in safe_success_verbs {
let mut full_args = vec!["--output-format", "json"];
full_args.extend_from_slice(args);
let output = run_claw(&root, &full_args, &[]);
// Emission contract clause 1: if exit == 0, stdout must be non-empty.
if output.status.success() {
let stdout_text = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout_text.trim().is_empty(),
"#168c Task 2 emission contract violation: `{verb}` exit 0 with empty stdout (silent success). stderr was:\n{}",
String::from_utf8_lossy(&output.stderr)
);
// Emission contract clause 2: stdout must be valid JSON.
let envelope: Result<Value, _> = serde_json::from_slice(&output.stdout);
assert!(
envelope.is_ok(),
"#168c Task 2 emission contract violation: `{verb}` stdout is not valid JSON:\n{stdout_text}"
);
}
// If exit != 0, it's an error path; #168c primary test covers error routing.
}
// Verbs expected to fail (exit != 0) in test env (require external state).
// Emission contract clause 3: error paths must still emit JSON on stdout.
let safe_error_verbs: &[(&str, &[&str])] = &[
("prompt-no-arg", &["prompt"]),
("doctor-bad-arg", &["doctor", "--foo"]),
];
for (label, args) in safe_error_verbs {
let mut full_args = vec!["--output-format", "json"];
full_args.extend_from_slice(args);
let output = run_claw(&root, &full_args, &[]);
assert!(
!output.status.success(),
"{label} was expected to fail but exited 0"
);
// #168c: error envelopes must be on stdout.
let stdout_text = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout_text.trim().is_empty(),
"#168c Task 2 emission contract violation: {label} failed with empty stdout. stderr was:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let envelope: Result<Value, _> = serde_json::from_slice(&output.stdout);
assert!(
envelope.is_ok(),
"#168c Task 2 emission contract violation: {label} stdout not valid JSON:\n{stdout_text}"
);
let envelope = envelope.unwrap();
assert_eq!(
envelope["type"], "error",
"{label} error envelope must carry type=error, got: {envelope}"
);
}
}
test(#168c Task 4): add v1.5 emission baseline shape parity guard Phase 0 Task 4 of the JSON Productization Program: CI shape parity guard. This test locks the v1.5 emission baseline (documented in SCHEMAS.md § v1.5 Emission Baseline) so any future PR that introduces shape drift in a documented verb fails this test at PR time. Complements Task 2 (no-silent guarantee) by asserting SPECIFIC top-level key sets, not just 'stdout is non-empty valid JSON'. If a verb adds/removes a top-level field, this test fails with a clear error message pointing to SCHEMAS.md § v1.5 Emission Baseline for update guidance. Coverage: - 8 success-path verbs with locked shape (help, version, doctor, skills, agents, system-prompt, bootstrap-plan, list-sessions) - 2 error-path cases with locked error envelope shape (prompt-no-arg, doctor --foo) Key enforcement rules: - Success envelope: exact key set match per verb - Error envelope: {error, hint, kind, type} (4 keys, all verbs) - list-sessions deliberately kept as {command, sessions} (Phase 1 target) Test design intent: - Locks CURRENT (possibly imperfect) shape, NOT target shape - Forces PR authors to update both code + SCHEMAS.md + test together - Makes Phase 1 shape normalization PRs visible: 'update this test' Phase 0 now COMPLETE: - Task 1 ✅ Stream routing fix (cycle #89) - Task 2 ✅ No-silent guarantee (cycle #90) - Task 3 ✅ Per-verb emission inventory SCHEMAS.md (cycle #91) - Task 4 ✅ CI shape parity guard (this cycle) Tests: 18 output_format_contract tests all pass (+1 from Task 4). v1.5 emission baseline now locked by code + tests + docs. Refs: #168c, cycle #92, Phase 0 Task 4 (final)
2026-04-23 06:38:18 +09:00
/// #168c Phase 0 Task 4: Shape parity / regression guard.
///
/// Locks the v1.5 emission baseline (documented in SCHEMAS.md § v1.5 Emission
/// Baseline) so any future PR that introduces shape drift in a documented
/// verb fails this test at PR time.
///
/// This complements Task 2 (no-silent guarantee) by asserting the SPECIFIC
/// top-level key sets documented in the catalog. If a verb adds/removes a
/// top-level field, this test fails — forcing the PR author to:
/// (a) update SCHEMAS.md § v1.5 Emission Baseline with the new shape, and
/// (b) acknowledge the v1.5 baseline is changing.
///
/// Phase 0 Task 4 deliverable: prevents undocumented shape drift in v1.5
/// baseline before Phase 1 (shape normalization) begins.
///
/// Note: This test intentionally asserts the CURRENT (possibly imperfect)
/// shape, NOT the target. Phase 1 will update these expectations as shapes
/// normalize.
#[test]
fn v1_5_emission_baseline_shape_parity_168c_task4() {
let root = unique_temp_dir("168c-task4-shape-parity");
fs::create_dir_all(&root).expect("temp dir should exist");
// v1.5 baseline per-verb shape catalog (from SCHEMAS.md § v1.5 Emission Baseline).
// Each entry: (verb, args, expected_top_level_keys_sorted).
//
// This catalog was captured by the cycle #87 controlled matrix and is
// enforced by SCHEMAS.md § v1.5 Emission Baseline documentation.
let baseline: &[(&str, &[&str], &[&str])] = &[
// Verbs using `kind` field (12 of 13 success paths)
("help", &["help"], &["kind", "message"]),
(
"version",
&["version"],
&["git_sha", "kind", "message", "target", "version"],
),
(
"doctor",
&["doctor"],
&["checks", "has_failures", "kind", "message", "report", "summary"],
),
(
"skills",
&["skills"],
&["action", "kind", "skills", "summary"],
),
(
"agents",
&["agents"],
&["action", "agents", "count", "kind", "summary", "working_directory"],
),
(
"system-prompt",
&["system-prompt"],
&["kind", "message", "sections"],
),
(
"bootstrap-plan",
&["bootstrap-plan", "test"],
&["kind", "phases"],
),
// Verb using `command` field (the 1-of-13 deviation — Phase 1 target)
(
"list-sessions",
&["list-sessions"],
&["command", "sessions"],
),
];
for (verb, args, expected_keys) in baseline {
let mut full_args = vec!["--output-format", "json"];
full_args.extend_from_slice(args);
let output = run_claw(&root, &full_args, &[]);
assert!(
output.status.success(),
"#168c Task 4: `{verb}` expected success path but exited with {:?}. stdout:\n{}\nstderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
let envelope: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|err| {
panic!(
"#168c Task 4: `{verb}` stdout not valid JSON: {err}\nstdout:\n{}",
String::from_utf8_lossy(&output.stdout)
)
});
let actual_keys: Vec<String> = envelope
.as_object()
.unwrap_or_else(|| panic!("#168c Task 4: `{verb}` envelope not a JSON object"))
.keys()
.cloned()
.collect();
let mut actual_sorted = actual_keys.clone();
actual_sorted.sort();
let mut expected_sorted: Vec<String> = expected_keys.iter().map(|s| s.to_string()).collect();
expected_sorted.sort();
assert_eq!(
actual_sorted, expected_sorted,
"#168c Task 4: shape drift detected in `{verb}`!\n\
Expected top-level keys (v1.5 baseline): {expected_sorted:?}\n\
Actual top-level keys: {actual_sorted:?}\n\
If this is intentional, update:\n\
1. SCHEMAS.md § v1.5 Emission Baseline catalog\n\
2. This test's `baseline` array\n\
Envelope: {envelope}"
);
}
// Error envelope shape parity (all error paths).
// Standard v1.5 error envelope: {error, hint, kind, type} (always 4 keys).
let error_cases: &[(&str, &[&str])] = &[
("prompt-no-arg", &["prompt"]),
("doctor-bad-arg", &["doctor", "--foo"]),
];
let expected_error_keys = ["error", "hint", "kind", "type"];
let mut expected_error_sorted: Vec<String> =
expected_error_keys.iter().map(|s| s.to_string()).collect();
expected_error_sorted.sort();
for (label, args) in error_cases {
let mut full_args = vec!["--output-format", "json"];
full_args.extend_from_slice(args);
let output = run_claw(&root, &full_args, &[]);
assert!(
!output.status.success(),
"{label}: expected error exit, got success"
);
let envelope: Value = serde_json::from_slice(&output.stdout).unwrap_or_else(|err| {
panic!(
"#168c Task 4: {label} stdout not valid JSON: {err}\nstdout:\n{}",
String::from_utf8_lossy(&output.stdout)
)
});
let actual_keys: Vec<String> = envelope
.as_object()
.unwrap_or_else(|| panic!("#168c Task 4: {label} envelope not a JSON object"))
.keys()
.cloned()
.collect();
let mut actual_sorted = actual_keys.clone();
actual_sorted.sort();
assert_eq!(
actual_sorted, expected_error_sorted,
"#168c Task 4: error envelope shape drift detected in {label}!\n\
Expected v1.5 error envelope keys: {expected_error_sorted:?}\n\
Actual keys: {actual_sorted:?}\n\
If this is intentional, update SCHEMAS.md § Standard Error Envelope (v1.5).\n\
Envelope: {envelope}"
);
}
}
fix: #247 classify prompt-related parse errors + unify JSON hint plumbing Cycle #34 dogfood follow-through on Jobdori cycle #33 pinpoint (#247 filed at fbcbe9d). Closes the two typed-error contract drifts surfaced in that pinpoint against the Rust `claw` binary. ## What was wrong 1. `classify_error_kind()` (main.rs:~251) used substring matching but did NOT match two common prompt-related parse errors: - "prompt subcommand requires a prompt string" - "empty prompt: provide a subcommand..." Both fell through to `"unknown"`. §4.44 typed-error contract specifies `parse | usage | unknown` as distinct classes, so claws dispatching on `error.kind == "cli_parse"` missed those paths entirely. 2. JSON mode dropped the `Run `claw --help` for usage.` hint. Text mode appends it at stderr-print time (main.rs:~234) AFTER split_error_hint() has already serialized the envelope, so JSON consumers never saw it. Text-mode humans got an actionable pointer; machine consumers did not. ## Fix Two small, targeted edits: 1. `classify_error_kind()`: add explicit branches for "prompt subcommand requires" and "empty prompt:" (the latter anchored with `starts_with` so it never hijacks unrelated error messages containing the word). Both route to `cli_parse`. 2. JSON error render path in `main()`: after calling split_error_hint(), if the message carried no embedded hint AND kind is `cli_parse` AND the short-reason does not already embed a `claw --help` pointer, synthesize the same `Run `claw --help` for usage.` trailer that text-mode stderr appends. The embedded-pointer check prevents duplication on the `empty prompt: ... (run `claw --help`)` message which already carries inline guidance. ## Verification Direct repro on the compiled binary: $ claw --output-format json prompt {"error":"prompt subcommand requires a prompt string", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} $ claw --output-format json "" {"error":"empty prompt: provide a subcommand (run `claw --help`) or a non-empty prompt string", "hint":null,"kind":"cli_parse","type":"error"} $ claw --output-format json doctor --foo # regression guard {"error":"unrecognized argument `--foo` for subcommand `doctor`", "hint":"Run `claw --help` for usage.", "kind":"cli_parse","type":"error"} Text mode unchanged in shape; `[error-kind: ...]` prefix now reads `cli_parse` for the two previously-misclassified paths. ## Regression coverage - Unit test `classify_error_kind_covers_prompt_parse_errors_247`: locks both patterns route to `cli_parse` AND that generic "prompt"-containing messages still fall through to `unknown`. - Integration tests in `tests/output_format_contract.rs`: * prompt_subcommand_without_arg_emits_cli_parse_envelope_with_hint_247 * empty_positional_arg_emits_cli_parse_envelope_247 * whitespace_only_positional_arg_emits_cli_parse_envelope_247 * unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard - Full rusty-claude-cli test suite: 218 tests pass (180 bin unit + 15 output_format_contract + 12 resume_slash + 7 compact + 3 mock + 1 cli). ## Family / related Joins §4.44 typed-envelope contract gap family closure: #130, #179, #181, and now **#247**. All four quartet items now have real fixes landed on the canonical binary surface rather than only the Python harness. ROADMAP.md: #247 marked CLOSED with before/after evidence preserved.
2026-04-22 22:43:14 +09:00
#[test]
fn unrecognized_argument_still_classifies_as_cli_parse_247_regression_guard() {
// #247 regression guard: the new empty-prompt / prompt-subcommand
// patterns must NOT hijack the existing #77 unrecognized-argument
// classification. `claw doctor --foo` must still surface as cli_parse
// with the runbook hint present.
let root = unique_temp_dir("247-unrecognized-arg");
fs::create_dir_all(&root).expect("temp dir should exist");
let envelope =
assert_json_error_envelope(&root, &["--output-format", "json", "doctor", "--foo"]);
assert_eq!(
envelope["kind"], "cli_parse",
"unrecognized-argument must remain cli_parse, envelope: {envelope}"
);
assert_eq!(
envelope["hint"],
"Run `claw --help` for usage.",
"unrecognized-argument hint should stay intact, envelope: {envelope}"
);
}
#[test]
fn v1_5_action_field_appears_only_in_3_inventory_verbs_172() {
// #172: SCHEMAS.md v1.5 Emission Baseline claims `action` field appears
// only in 3 inventory verbs: mcp, skills, agents. This test is a
// regression guard for that truthfulness claim. If a new verb adds
// `action`, or one of the 3 removes it, this test fails and forces
// the SCHEMAS.md documentation to stay in sync with reality.
//
// Discovered during cycle #98 probe: earlier SCHEMAS.md draft said
// "only in 4 inventory verbs" but reality was only 3 (list-sessions
// uses `command` instead of `action`). Doc was corrected; this test
// locks the 3-verb invariant.
let root = unique_temp_dir("172-action-inventory");
fs::create_dir_all(&root).expect("temp dir should exist");
let verbs_with_action: &[&str] = &["mcp", "skills", "agents"];
let verbs_without_action: &[&str] = &[
"help",
"version",
"doctor",
"status",
"sandbox",
"system-prompt",
"bootstrap-plan",
"list-sessions",
];
for verb in verbs_with_action {
let envelope = assert_json_command(&root, &["--output-format", "json", verb]);
assert!(
envelope.get("action").is_some(),
"#172: `{verb}` should have `action` field per v1.5 baseline, but envelope: {envelope}"
);
}
for verb in verbs_without_action {
let envelope = assert_json_command(&root, &["--output-format", "json", verb]);
assert!(
envelope.get("action").is_none(),
"#172: `{verb}` should NOT have `action` field per v1.5 baseline (only 3 inventory verbs: mcp/skills/agents should have it), but envelope: {envelope}"
);
}
}
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
fn assert_json_command_with_env(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Value {
let output = run_claw(current_dir, args, envs);
assert!(
output.status.success(),
"stdout:\n{}\n\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice(&output.stdout).expect("stdout should be valid json")
}
fn run_claw(current_dir: &Path, args: &[&str], envs: &[(&str, &str)]) -> Output {
let mut command = Command::new(env!("CARGO_BIN_EXE_claw"));
command.current_dir(current_dir).args(args);
for (key, value) in envs {
command.env(key, value);
}
command.output().expect("claw should launch")
}
fn write_upstream_fixture(root: &Path) -> PathBuf {
let upstream = root.join("claw-code");
let src = upstream.join("src");
let entrypoints = src.join("entrypoints");
fs::create_dir_all(&entrypoints).expect("upstream entrypoints dir should exist");
fs::write(
src.join("commands.ts"),
"import FooCommand from './commands/foo'\n",
)
.expect("commands fixture should write");
fs::write(
src.join("tools.ts"),
"import ReadTool from './tools/read'\n",
)
.expect("tools fixture should write");
fs::write(
entrypoints.join("cli.tsx"),
"if (args[0] === '--version') {}\nstartupProfiler()\n",
)
.expect("cli fixture should write");
upstream
}
Prevent cross-worktree session bleed during managed session resume/load ROADMAP #41 was still leaving a phantom-completion class open: managed sessions could be resumed from the wrong workspace, and the CLI/runtime paths were split between partially isolated storage and older helper flows. This squashes the verified team work into one deliverable that routes managed session operations through the per-worktree SessionStore, rejects workspace mismatches explicitly, extends lane-event taxonomy for workspace mismatch reporting, and updates the affected CLI regression fixtures/docs so the new contract is enforced without losing same- workspace legacy coverage. Constraint: Keep same-workspace legacy flat sessions readable while blocking cross-worktree misuse Constraint: No new dependencies; stay within the ROADMAP #41 changed-file scope Rejected: Leave team auto-checkpoint history as final branch state | noisy/non-lore history for a single roadmap fix Confidence: high Scope-risk: moderate Reversibility: clean Directive: Preserve workspace_root validation on future resume/load helpers; do not reintroduce path-only fallback without equivalent mismatch checks Tested: cargo test -p runtime session_control -- --nocapture; cargo test -p rusty-claude-cli resume -- --nocapture; cargo test -p rusty-claude-cli --test cli_flags_and_config_defaults; cargo test -p rusty-claude-cli --test output_format_contract; cargo test -p rusty-claude-cli --test resume_slash_commands; cargo test --workspace --exclude compat-harness; cargo check --workspace --all-targets; git diff --check Not-tested: cargo clippy --workspace --all-targets -- -D warnings (pre-existing failures in unchanged rust/crates/rusty-claude-cli/build.rs) Related: ROADMAP #41
2026-04-11 16:08:01 +00:00
fn write_session_fixture(root: &Path, session_id: &str, user_text: Option<&str>) -> PathBuf {
let session_path = root.join("session.jsonl");
let mut session = Session::new()
.with_workspace_root(root.to_path_buf())
.with_persistence_path(session_path.clone());
session.session_id = session_id.to_string();
if let Some(text) = user_text {
session
.push_user_text(text)
.expect("session fixture message should persist");
} else {
session
.save_to_path(&session_path)
.expect("session fixture should persist");
}
session_path
}
fn write_agent(root: &Path, name: &str, description: &str, model: &str, reasoning: &str) {
fs::create_dir_all(root).expect("agent root should exist");
fs::write(
root.join(format!("{name}.toml")),
format!(
"name = \"{name}\"\ndescription = \"{description}\"\nmodel = \"{model}\"\nmodel_reasoning_effort = \"{reasoning}\"\n"
),
)
.expect("agent fixture should write");
}
Close the clawability backlog with deterministic CLI output and lane lineage Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage. Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata Confidence: medium Scope-risk: moderate Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths Tested: python .github/scripts/check_doc_source_of_truth.py Tested: cd rust && cargo fmt --all --check Tested: cd rust && cargo test --workspace Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
2026-04-05 18:40:33 +00:00
fn unique_temp_dir(label: &str) -> PathBuf {
let millis = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_millis();
let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!(
"claw-output-format-{label}-{}-{millis}-{counter}",
std::process::id()
))
}