mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-07 00:24:50 +08:00
Keep resumed JSON command surfaces machine-readable
Resumed slash dispatch was still dropping back to prose for several JSON-capable local commands, which forced automation to special-case direct CLI invocations versus --resume flows. This routes resumed local-command handlers through the same structured JSON payloads used by direct status, sandbox, inventory, version, and init commands, and records the inventory parity audit result in the roadmap. Constraint: Text-mode resumed output must stay unchanged for existing shell users Rejected: Teach callers to scrape resumed text output | brittle and defeats the JSON contract Confidence: high Scope-risk: narrow Reversibility: clean Directive: When a direct local command has a JSON renderer, keep resumed slash dispatch on the same serializer instead of adding one-off format branches Tested: cargo fmt --check; cargo test --workspace; cargo clippy --workspace --all-targets -- -D warnings Not-tested: Live provider-backed REPL resume flows outside the local test harness
This commit is contained in:
@@ -311,6 +311,8 @@ Priority order: P0 = blocks CI/green state, P1 = blocks integration wiring, P2 =
|
|||||||
22. **Opaque failure surface for session/runtime crashes** — repeated dogfood-facing failures can currently collapse to generic wrappers like `Something went wrong while processing your request. Please try again, or use /new to start a fresh session.` without exposing whether the fault was provider auth, session corruption, slash-command dispatch, render failure, or transport/runtime panic. This blocks fast self-recovery and turns actionable clawability bugs into blind retries. **Action:** preserve a short user-safe failure class (`provider_auth`, `session_load`, `command_dispatch`, `render`, `runtime_panic`, etc.), attach a local trace/session id, and ensure operators can jump from the chat-visible error to the exact failure log quickly.
|
22. **Opaque failure surface for session/runtime crashes** — repeated dogfood-facing failures can currently collapse to generic wrappers like `Something went wrong while processing your request. Please try again, or use /new to start a fresh session.` without exposing whether the fault was provider auth, session corruption, slash-command dispatch, render failure, or transport/runtime panic. This blocks fast self-recovery and turns actionable clawability bugs into blind retries. **Action:** preserve a short user-safe failure class (`provider_auth`, `session_load`, `command_dispatch`, `render`, `runtime_panic`, etc.), attach a local trace/session id, and ensure operators can jump from the chat-visible error to the exact failure log quickly.
|
||||||
23. **`doctor --output-format json` check-level structure gap** — **done**: `claw doctor --output-format json` now keeps the human-readable `message`/`report` while also emitting structured per-check diagnostics (`name`, `status`, `summary`, `details`, plus typed fields like workspace paths and sandbox fallback data), with regression coverage in `output_format_contract.rs`.
|
23. **`doctor --output-format json` check-level structure gap** — **done**: `claw doctor --output-format json` now keeps the human-readable `message`/`report` while also emitting structured per-check diagnostics (`name`, `status`, `summary`, `details`, plus typed fields like workspace paths and sandbox fallback data), with regression coverage in `output_format_contract.rs`.
|
||||||
24. **Plugin lifecycle init/shutdown test flakes under workspace-parallel execution** — dogfooding surfaced that `build_runtime_runs_plugin_lifecycle_init_and_shutdown` can fail under `cargo test --workspace` while passing in isolation because sibling tests race on tempdir-backed shell init script paths. This is test brittleness rather than a code-path regression, but it still destabilizes CI confidence and wastes diagnosis cycles. **Action:** isolate temp resources per test robustly (unique dirs + no shared cwd assumptions), audit cleanup timing, and add a regression guard so the plugin lifecycle test remains stable under parallel workspace execution.
|
24. **Plugin lifecycle init/shutdown test flakes under workspace-parallel execution** — dogfooding surfaced that `build_runtime_runs_plugin_lifecycle_init_and_shutdown` can fail under `cargo test --workspace` while passing in isolation because sibling tests race on tempdir-backed shell init script paths. This is test brittleness rather than a code-path regression, but it still destabilizes CI confidence and wastes diagnosis cycles. **Action:** isolate temp resources per test robustly (unique dirs + no shared cwd assumptions), audit cleanup timing, and add a regression guard so the plugin lifecycle test remains stable under parallel workspace execution.
|
||||||
|
26. **Resumed `/sandbox` JSON parity gap** — **done**: direct `claw sandbox --output-format json` already emitted structured JSON, but resumed `claw --output-format json --resume <session> /sandbox` still fell back to prose because resumed slash dispatch only emitted JSON for `/status`. The resumed `/sandbox` path now reuses the same JSON envelope as the direct CLI command, with regression coverage in `rust/crates/rusty-claude-cli/tests/resume_slash_commands.rs`.
|
||||||
|
27. **Resumed inventory JSON parity gap for `/mcp` and `/skills`** — **done**: resumed slash-command inventory calls now honor `--output-format json` via the same structured renderers as direct `claw mcp` / `claw skills`, with regression coverage for resumed `list` output under an isolated config home.
|
||||||
**P3 — Swarm efficiency**
|
**P3 — Swarm efficiency**
|
||||||
13. Swarm branch-lock protocol — **done**: `branch_lock::detect_branch_lock_collisions()` now detects same-branch/same-scope and nested-module collisions before parallel lanes drift into duplicate implementation
|
13. Swarm branch-lock protocol — **done**: `branch_lock::detect_branch_lock_collisions()` now detects same-branch/same-scope and nested-module collisions before parallel lanes drift into duplicate implementation
|
||||||
14. Commit provenance / worktree-aware push events — **done**: lane event provenance now includes branch/worktree/superseded/canonical lineage metadata, and manifest persistence de-dupes superseded commit events before downstream consumers render them
|
14. Commit provenance / worktree-aware push events — **done**: lane event provenance now includes branch/worktree/superseded/canonical lineage metadata, and manifest persistence de-dupes superseded commit events before downstream consumers render them
|
||||||
|
|||||||
@@ -1688,23 +1688,25 @@ fn print_system_prompt(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let report = render_version_report();
|
|
||||||
match output_format {
|
match output_format {
|
||||||
CliOutputFormat::Text => println!("{report}"),
|
CliOutputFormat::Text => println!("{}", render_version_report()),
|
||||||
CliOutputFormat::Json => println!(
|
CliOutputFormat::Json => {
|
||||||
"{}",
|
println!("{}", serde_json::to_string_pretty(&version_json_value())?);
|
||||||
serde_json::to_string_pretty(&json!({
|
}
|
||||||
"kind": "version",
|
|
||||||
"message": report,
|
|
||||||
"version": VERSION,
|
|
||||||
"git_sha": GIT_SHA,
|
|
||||||
"target": BUILD_TARGET,
|
|
||||||
}))?
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn version_json_value() -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"kind": "version",
|
||||||
|
"message": render_version_report(),
|
||||||
|
"version": VERSION,
|
||||||
|
"git_sha": GIT_SHA,
|
||||||
|
"target": BUILD_TARGET,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
|
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
|
||||||
let resolved_path = if session_path.exists() {
|
let resolved_path = if session_path.exists() {
|
||||||
session_path.to_path_buf()
|
session_path.to_path_buf()
|
||||||
@@ -1752,33 +1754,21 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: next_session,
|
session: next_session,
|
||||||
message,
|
message,
|
||||||
|
json,
|
||||||
}) => {
|
}) => {
|
||||||
session = next_session;
|
session = next_session;
|
||||||
if let Some(message) = message {
|
if output_format == CliOutputFormat::Json {
|
||||||
if output_format == CliOutputFormat::Json
|
if let Some(value) = json {
|
||||||
&& matches!(command, SlashCommand::Status)
|
|
||||||
{
|
|
||||||
let tracker = UsageTracker::from_session(&session);
|
|
||||||
let context = status_context(Some(&resolved_path)).expect("status context");
|
|
||||||
let value = status_json_value(
|
|
||||||
"restored-session",
|
|
||||||
StatusUsage {
|
|
||||||
message_count: session.messages.len(),
|
|
||||||
turns: tracker.turns(),
|
|
||||||
latest: tracker.current_turn_usage(),
|
|
||||||
cumulative: tracker.cumulative_usage(),
|
|
||||||
estimated_tokens: 0,
|
|
||||||
},
|
|
||||||
default_permission_mode().as_str(),
|
|
||||||
&context,
|
|
||||||
);
|
|
||||||
println!(
|
println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string_pretty(&value).expect("status json")
|
serde_json::to_string_pretty(&value)
|
||||||
|
.expect("resume command json output")
|
||||||
);
|
);
|
||||||
} else {
|
} else if let Some(message) = message {
|
||||||
println!("{message}");
|
println!("{message}");
|
||||||
}
|
}
|
||||||
|
} else if let Some(message) = message {
|
||||||
|
println!("{message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -1793,6 +1783,7 @@ fn resume_session(session_path: &Path, commands: &[String], output_format: CliOu
|
|||||||
struct ResumeCommandOutcome {
|
struct ResumeCommandOutcome {
|
||||||
session: Session,
|
session: Session,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
|
json: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -2127,6 +2118,7 @@ fn run_resume_command(
|
|||||||
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
SlashCommand::Help => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_repl_help()),
|
message: Some(render_repl_help()),
|
||||||
|
json: None,
|
||||||
}),
|
}),
|
||||||
SlashCommand::Compact => {
|
SlashCommand::Compact => {
|
||||||
let result = runtime::compact_session(
|
let result = runtime::compact_session(
|
||||||
@@ -2143,6 +2135,7 @@ fn run_resume_command(
|
|||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: result.compacted_session,
|
session: result.compacted_session,
|
||||||
message: Some(format_compact_report(removed, kept, skipped)),
|
message: Some(format_compact_report(removed, kept, skipped)),
|
||||||
|
json: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Clear { confirm } => {
|
SlashCommand::Clear { confirm } => {
|
||||||
@@ -2152,6 +2145,7 @@ fn run_resume_command(
|
|||||||
message: Some(
|
message: Some(
|
||||||
"clear: confirmation required; rerun with /clear --confirm".to_string(),
|
"clear: confirmation required; rerun with /clear --confirm".to_string(),
|
||||||
),
|
),
|
||||||
|
json: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let backup_path = write_session_clear_backup(session, session_path)?;
|
let backup_path = write_session_clear_backup(session, session_path)?;
|
||||||
@@ -2167,11 +2161,13 @@ fn run_resume_command(
|
|||||||
backup_path.display(),
|
backup_path.display(),
|
||||||
session_path.display()
|
session_path.display()
|
||||||
)),
|
)),
|
||||||
|
json: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Status => {
|
SlashCommand::Status => {
|
||||||
let tracker = UsageTracker::from_session(session);
|
let tracker = UsageTracker::from_session(session);
|
||||||
let usage = tracker.cumulative_usage();
|
let usage = tracker.cumulative_usage();
|
||||||
|
let context = status_context(Some(session_path))?;
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format_status_report(
|
message: Some(format_status_report(
|
||||||
@@ -2184,7 +2180,19 @@ fn run_resume_command(
|
|||||||
estimated_tokens: 0,
|
estimated_tokens: 0,
|
||||||
},
|
},
|
||||||
default_permission_mode().as_str(),
|
default_permission_mode().as_str(),
|
||||||
&status_context(Some(session_path))?,
|
&context,
|
||||||
|
)),
|
||||||
|
json: Some(status_json_value(
|
||||||
|
"restored-session",
|
||||||
|
StatusUsage {
|
||||||
|
message_count: session.messages.len(),
|
||||||
|
turns: tracker.turns(),
|
||||||
|
latest: tracker.current_turn_usage(),
|
||||||
|
cumulative: usage,
|
||||||
|
estimated_tokens: 0,
|
||||||
|
},
|
||||||
|
default_permission_mode().as_str(),
|
||||||
|
&context,
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -2192,12 +2200,11 @@ fn run_resume_command(
|
|||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
let loader = ConfigLoader::default_for(&cwd);
|
let loader = ConfigLoader::default_for(&cwd);
|
||||||
let runtime_config = loader.load()?;
|
let runtime_config = loader.load()?;
|
||||||
|
let status = resolve_sandbox_status(runtime_config.sandbox(), &cwd);
|
||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format_sandbox_report(&resolve_sandbox_status(
|
message: Some(format_sandbox_report(&status)),
|
||||||
runtime_config.sandbox(),
|
json: Some(sandbox_json_value(&status)),
|
||||||
&cwd,
|
|
||||||
))),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Cost => {
|
SlashCommand::Cost => {
|
||||||
@@ -2205,11 +2212,13 @@ fn run_resume_command(
|
|||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(format_cost_report(usage)),
|
message: Some(format_cost_report(usage)),
|
||||||
|
json: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
|
SlashCommand::Config { section } => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_config_report(section.as_deref())?),
|
message: Some(render_config_report(section.as_deref())?),
|
||||||
|
json: None,
|
||||||
}),
|
}),
|
||||||
SlashCommand::Mcp { action, target } => {
|
SlashCommand::Mcp { action, target } => {
|
||||||
let cwd = env::current_dir()?;
|
let cwd = env::current_dir()?;
|
||||||
@@ -2222,25 +2231,33 @@ fn run_resume_command(
|
|||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
|
message: Some(handle_mcp_slash_command(args.as_deref(), &cwd)?),
|
||||||
|
json: Some(handle_mcp_slash_command_json(args.as_deref(), &cwd)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
SlashCommand::Memory => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_memory_report()?),
|
message: Some(render_memory_report()?),
|
||||||
|
json: None,
|
||||||
}),
|
}),
|
||||||
SlashCommand::Init => Ok(ResumeCommandOutcome {
|
SlashCommand::Init => {
|
||||||
session: session.clone(),
|
let message = init_claude_md()?;
|
||||||
message: Some(init_claude_md()?),
|
Ok(ResumeCommandOutcome {
|
||||||
}),
|
session: session.clone(),
|
||||||
|
message: Some(message.clone()),
|
||||||
|
json: Some(init_json_value(&message)),
|
||||||
|
})
|
||||||
|
}
|
||||||
SlashCommand::Diff => Ok(ResumeCommandOutcome {
|
SlashCommand::Diff => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_diff_report_for(
|
message: Some(render_diff_report_for(
|
||||||
session_path.parent().unwrap_or_else(|| Path::new(".")),
|
session_path.parent().unwrap_or_else(|| Path::new(".")),
|
||||||
)?),
|
)?),
|
||||||
|
json: None,
|
||||||
}),
|
}),
|
||||||
SlashCommand::Version => Ok(ResumeCommandOutcome {
|
SlashCommand::Version => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_version_report()),
|
message: Some(render_version_report()),
|
||||||
|
json: Some(version_json_value()),
|
||||||
}),
|
}),
|
||||||
SlashCommand::Export { path } => {
|
SlashCommand::Export { path } => {
|
||||||
let export_path = resolve_export_path(path.as_deref(), session)?;
|
let export_path = resolve_export_path(path.as_deref(), session)?;
|
||||||
@@ -2252,6 +2269,7 @@ fn run_resume_command(
|
|||||||
export_path.display(),
|
export_path.display(),
|
||||||
session.messages.len(),
|
session.messages.len(),
|
||||||
)),
|
)),
|
||||||
|
json: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Agents { args } => {
|
SlashCommand::Agents { args } => {
|
||||||
@@ -2259,6 +2277,7 @@ fn run_resume_command(
|
|||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(handle_agents_slash_command(args.as_deref(), &cwd)?),
|
message: Some(handle_agents_slash_command(args.as_deref(), &cwd)?),
|
||||||
|
json: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Skills { args } => {
|
SlashCommand::Skills { args } => {
|
||||||
@@ -2266,11 +2285,13 @@ fn run_resume_command(
|
|||||||
Ok(ResumeCommandOutcome {
|
Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
|
message: Some(handle_skills_slash_command(args.as_deref(), &cwd)?),
|
||||||
|
json: Some(handle_skills_slash_command_json(args.as_deref(), &cwd)?),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
SlashCommand::Doctor => Ok(ResumeCommandOutcome {
|
SlashCommand::Doctor => Ok(ResumeCommandOutcome {
|
||||||
session: session.clone(),
|
session: session.clone(),
|
||||||
message: Some(render_doctor_report()?.render()),
|
message: Some(render_doctor_report()?.render()),
|
||||||
|
json: None,
|
||||||
}),
|
}),
|
||||||
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
SlashCommand::Unknown(name) => Err(format_unknown_slash_command(name).into()),
|
||||||
SlashCommand::Bughunter { .. }
|
SlashCommand::Bughunter { .. }
|
||||||
@@ -4259,27 +4280,31 @@ fn print_sandbox_status_snapshot(
|
|||||||
CliOutputFormat::Text => println!("{}", format_sandbox_report(&status)),
|
CliOutputFormat::Text => println!("{}", format_sandbox_report(&status)),
|
||||||
CliOutputFormat::Json => println!(
|
CliOutputFormat::Json => println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string_pretty(&json!({
|
serde_json::to_string_pretty(&sandbox_json_value(&status))?
|
||||||
"kind": "sandbox",
|
|
||||||
"enabled": status.enabled,
|
|
||||||
"active": status.active,
|
|
||||||
"supported": status.supported,
|
|
||||||
"in_container": status.in_container,
|
|
||||||
"requested_namespace": status.requested.namespace_restrictions,
|
|
||||||
"active_namespace": status.namespace_active,
|
|
||||||
"requested_network": status.requested.network_isolation,
|
|
||||||
"active_network": status.network_active,
|
|
||||||
"filesystem_mode": status.filesystem_mode.as_str(),
|
|
||||||
"filesystem_active": status.filesystem_active,
|
|
||||||
"allowed_mounts": status.allowed_mounts,
|
|
||||||
"markers": status.container_markers,
|
|
||||||
"fallback_reason": status.fallback_reason,
|
|
||||||
}))?
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sandbox_json_value(status: &runtime::SandboxStatus) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"kind": "sandbox",
|
||||||
|
"enabled": status.enabled,
|
||||||
|
"active": status.active,
|
||||||
|
"supported": status.supported,
|
||||||
|
"in_container": status.in_container,
|
||||||
|
"requested_namespace": status.requested.namespace_restrictions,
|
||||||
|
"active_namespace": status.namespace_active,
|
||||||
|
"requested_network": status.requested.network_isolation,
|
||||||
|
"active_network": status.network_active,
|
||||||
|
"filesystem_mode": status.filesystem_mode.as_str(),
|
||||||
|
"filesystem_active": status.filesystem_active,
|
||||||
|
"allowed_mounts": status.allowed_mounts,
|
||||||
|
"markers": status.container_markers,
|
||||||
|
"fallback_reason": status.fallback_reason,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn render_help_topic(topic: LocalHelpTopic) -> String {
|
fn render_help_topic(topic: LocalHelpTopic) -> String {
|
||||||
match topic {
|
match topic {
|
||||||
LocalHelpTopic::Status => "Status
|
LocalHelpTopic::Status => "Status
|
||||||
@@ -4436,15 +4461,19 @@ fn run_init(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Er
|
|||||||
CliOutputFormat::Text => println!("{message}"),
|
CliOutputFormat::Text => println!("{message}"),
|
||||||
CliOutputFormat::Json => println!(
|
CliOutputFormat::Json => println!(
|
||||||
"{}",
|
"{}",
|
||||||
serde_json::to_string_pretty(&json!({
|
serde_json::to_string_pretty(&init_json_value(&message))?
|
||||||
"kind": "init",
|
|
||||||
"message": message,
|
|
||||||
}))?
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn init_json_value(message: &str) -> serde_json::Value {
|
||||||
|
json!({
|
||||||
|
"kind": "init",
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
|
fn normalize_permission_mode(mode: &str) -> Option<&'static str> {
|
||||||
match mode.trim() {
|
match mode.trim() {
|
||||||
"read-only" => Some("read-only"),
|
"read-only" => Some("read-only"),
|
||||||
|
|||||||
@@ -259,6 +259,104 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
|||||||
assert!(resumed["sandbox"]["filesystem_mode"].as_str().is_some());
|
assert!(resumed["sandbox"]["filesystem_mode"].as_str().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
|
||||||
|
let session_path = root.join("session.jsonl");
|
||||||
|
fs::write(
|
||||||
|
&session_path,
|
||||||
|
"{\"type\":\"session_meta\",\"version\":3,\"session_id\":\"resume-inventory-json\",\"created_at_ms\":0,\"updated_at_ms\":0}\n{\"type\":\"message\",\"message\":{\"role\":\"user\",\"blocks\":[{\"type\":\"text\",\"text\":\"inventory\"}]}}\n",
|
||||||
|
)
|
||||||
|
.expect("session should write");
|
||||||
|
|
||||||
|
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");
|
||||||
|
|
||||||
|
let session_path = root.join("session.jsonl");
|
||||||
|
fs::write(
|
||||||
|
&session_path,
|
||||||
|
"{\"type\":\"session_meta\",\"version\":3,\"session_id\":\"resume-version-init-json\",\"created_at_ms\":0,\"updated_at_ms\":0}\n",
|
||||||
|
)
|
||||||
|
.expect("session should write");
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {
|
fn assert_json_command(current_dir: &Path, args: &[&str]) -> Value {
|
||||||
assert_json_command_with_env(current_dir, args, &[])
|
assert_json_command_with_env(current_dir, args, &[])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -275,6 +275,49 @@ fn resumed_status_command_emits_structured_json_when_requested() {
|
|||||||
assert!(parsed["sandbox"]["filesystem_mode"].as_str().is_some());
|
assert!(parsed["sandbox"]["filesystem_mode"].as_str().is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resumed_sandbox_command_emits_structured_json_when_requested() {
|
||||||
|
// given
|
||||||
|
let temp_dir = unique_temp_dir("resume-sandbox-json");
|
||||||
|
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||||
|
let session_path = temp_dir.join("session.jsonl");
|
||||||
|
|
||||||
|
Session::new()
|
||||||
|
.save_to_path(&session_path)
|
||||||
|
.expect("session should persist");
|
||||||
|
|
||||||
|
// when
|
||||||
|
let output = run_claw(
|
||||||
|
&temp_dir,
|
||||||
|
&[
|
||||||
|
"--output-format",
|
||||||
|
"json",
|
||||||
|
"--resume",
|
||||||
|
session_path.to_str().expect("utf8 path"),
|
||||||
|
"/sandbox",
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"stdout:\n{}\n\nstderr:\n{}",
|
||||||
|
String::from_utf8_lossy(&output.stdout),
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||||
|
let parsed: Value =
|
||||||
|
serde_json::from_str(stdout.trim()).expect("resume sandbox output should be json");
|
||||||
|
assert_eq!(parsed["kind"], "sandbox");
|
||||||
|
assert!(parsed["enabled"].is_boolean());
|
||||||
|
assert!(parsed["active"].is_boolean());
|
||||||
|
assert!(parsed["supported"].is_boolean());
|
||||||
|
assert!(parsed["filesystem_mode"].as_str().is_some());
|
||||||
|
assert!(parsed["allowed_mounts"].is_array());
|
||||||
|
assert!(parsed["markers"].is_array());
|
||||||
|
}
|
||||||
|
|
||||||
fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
|
fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
|
||||||
run_claw_with_env(current_dir, args, &[])
|
run_claw_with_env(current_dir, args, &[])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user