mirror of
https://github.com/instructkr/claw-code.git
synced 2026-06-06 06:27:08 +08:00
Merge remote-tracking branch 'upstream/main' into worktree-api-timeout-retry-v2
# Conflicts: # rust/crates/runtime/src/config.rs # rust/crates/runtime/src/lib.rs
This commit is contained in:
@@ -12,7 +12,6 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
api = { path = "../api" }
|
||||
commands = { path = "../commands" }
|
||||
compat-harness = { path = "../compat-harness" }
|
||||
crossterm = "0.28"
|
||||
pulldown-cmark = "0.13"
|
||||
rustyline = "15"
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
// Get git SHA (short hash)
|
||||
let git_sha = Command::new("git")
|
||||
.args(["rev-parse", "--short", "HEAD"])
|
||||
fn command_output(program: &str, args: &[&str]) -> Option<String> {
|
||||
Command::new(program)
|
||||
.args(args)
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|output| {
|
||||
@@ -14,11 +13,37 @@ fn main() {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string());
|
||||
.map(|value| value.trim().to_string())
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let git_sha =
|
||||
command_output("git", &["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
|
||||
let git_sha_short = command_output("git", &["rev-parse", "--short=12", "HEAD"])
|
||||
.or_else(|| git_sha.get(..git_sha.len().min(12)).map(str::to_string))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let git_dirty = command_output("git", &["status", "--porcelain"])
|
||||
.map(|status| (!status.trim().is_empty()).to_string())
|
||||
.unwrap_or_else(|| "false".to_string());
|
||||
let git_branch = command_output("git", &["branch", "--show-current"])
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let git_commit_date = command_output("git", &["show", "-s", "--format=%cI", "HEAD"])
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let git_commit_timestamp = command_output("git", &["show", "-s", "--format=%ct", "HEAD"])
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
let rustc_version =
|
||||
command_output("rustc", &["--version"]).unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
println!("cargo:rustc-env=GIT_SHA={git_sha}");
|
||||
println!("cargo:rustc-env=GIT_SHA_SHORT={git_sha_short}");
|
||||
println!("cargo:rustc-env=GIT_DIRTY={git_dirty}");
|
||||
println!("cargo:rustc-env=GIT_BRANCH={git_branch}");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_DATE={git_commit_date}");
|
||||
println!("cargo:rustc-env=GIT_COMMIT_TIMESTAMP={git_commit_timestamp}");
|
||||
println!("cargo:rustc-env=RUSTC_VERSION={rustc_version}");
|
||||
|
||||
// TARGET is always set by Cargo during build
|
||||
// TARGET is always set by Cargo during build.
|
||||
let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
|
||||
println!("cargo:rustc-env=TARGET={target}");
|
||||
|
||||
@@ -35,23 +60,12 @@ fn main() {
|
||||
})
|
||||
.or_else(|| std::env::var("BUILD_DATE").ok())
|
||||
.unwrap_or_else(|| {
|
||||
// Fall back to current date via `date` command
|
||||
Command::new("date")
|
||||
.args(["+%Y-%m-%d"])
|
||||
.output()
|
||||
.ok()
|
||||
.and_then(|o| {
|
||||
if o.status.success() {
|
||||
String::from_utf8(o.stdout).ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string())
|
||||
command_output("date", &["+%Y-%m-%d"]).unwrap_or_else(|| "unknown".to_string())
|
||||
});
|
||||
println!("cargo:rustc-env=BUILD_DATE={build_date}");
|
||||
|
||||
// Rerun if git state changes
|
||||
println!("cargo:rerun-if-changed=.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=.git/refs");
|
||||
// Rerun if git state changes. Paths are relative to this package root.
|
||||
println!("cargo:rerun-if-changed=../../../.git/HEAD");
|
||||
println!("cargo:rerun-if-changed=../../../.git/refs");
|
||||
println!("cargo:rerun-if-changed=../../../.git/index");
|
||||
}
|
||||
|
||||
@@ -4,7 +4,14 @@ use std::path::{Path, PathBuf};
|
||||
const STARTER_CLAW_JSON: &str = concat!(
|
||||
"{\n",
|
||||
" \"permissions\": {\n",
|
||||
" \"defaultMode\": \"dontAsk\"\n",
|
||||
" \"defaultMode\": \"acceptEdits\"\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
);
|
||||
const STARTER_SETTINGS_JSON: &str = concat!(
|
||||
"{\n",
|
||||
" \"permissions\": {\n",
|
||||
" \"defaultMode\": \"acceptEdits\"\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
);
|
||||
@@ -15,6 +22,8 @@ const GITIGNORE_ENTRIES: [&str; 3] = [".claw/settings.local.json", ".claw/sessio
|
||||
pub(crate) enum InitStatus {
|
||||
Created,
|
||||
Updated,
|
||||
Partial,
|
||||
Deferred,
|
||||
Skipped,
|
||||
}
|
||||
|
||||
@@ -24,6 +33,8 @@ impl InitStatus {
|
||||
match self {
|
||||
Self::Created => "created",
|
||||
Self::Updated => "updated",
|
||||
Self::Partial => "partial (created missing sub-files)",
|
||||
Self::Deferred => "deferred (created on first session save)",
|
||||
Self::Skipped => "skipped (already exists)",
|
||||
}
|
||||
}
|
||||
@@ -36,6 +47,8 @@ impl InitStatus {
|
||||
match self {
|
||||
Self::Created => "created",
|
||||
Self::Updated => "updated",
|
||||
Self::Partial => "partial",
|
||||
Self::Deferred => "deferred",
|
||||
Self::Skipped => "skipped",
|
||||
}
|
||||
}
|
||||
@@ -123,9 +136,30 @@ pub(crate) fn initialize_repo(cwd: &Path) -> Result<InitReport, Box<dyn std::err
|
||||
let mut artifacts = Vec::new();
|
||||
|
||||
let claw_dir = cwd.join(".claw");
|
||||
let claw_dir_status = ensure_dir(&claw_dir)?;
|
||||
let settings_json = claw_dir.join("settings.json");
|
||||
let settings_status = write_file_if_missing(&settings_json, STARTER_SETTINGS_JSON)?;
|
||||
let claw_dir_status =
|
||||
if claw_dir_status == InitStatus::Skipped && settings_status == InitStatus::Created {
|
||||
InitStatus::Partial
|
||||
} else {
|
||||
claw_dir_status
|
||||
};
|
||||
artifacts.push(InitArtifact {
|
||||
name: ".claw/",
|
||||
status: ensure_dir(&claw_dir)?,
|
||||
status: claw_dir_status,
|
||||
});
|
||||
artifacts.push(InitArtifact {
|
||||
name: ".claw/settings.json",
|
||||
status: settings_status,
|
||||
});
|
||||
artifacts.push(InitArtifact {
|
||||
name: ".claw/sessions/",
|
||||
status: if claw_dir.join("sessions").is_dir() {
|
||||
InitStatus::Skipped
|
||||
} else {
|
||||
InitStatus::Deferred
|
||||
},
|
||||
});
|
||||
|
||||
let claw_json = cwd.join(".claw.json");
|
||||
@@ -414,11 +448,26 @@ mod tests {
|
||||
concat!(
|
||||
"{\n",
|
||||
" \"permissions\": {\n",
|
||||
" \"defaultMode\": \"dontAsk\"\n",
|
||||
" \"defaultMode\": \"acceptEdits\"\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(root.join(".claw").join("settings.json"))
|
||||
.expect("read project settings"),
|
||||
concat!(
|
||||
"{\n",
|
||||
" \"permissions\": {\n",
|
||||
" \"defaultMode\": \"acceptEdits\"\n",
|
||||
" }\n",
|
||||
"}\n",
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
!root.join(".claw").join("sessions").exists(),
|
||||
"sessions directory should be deferred until first session save"
|
||||
);
|
||||
let gitignore = fs::read_to_string(root.join(".gitignore")).expect("read gitignore");
|
||||
assert!(gitignore.contains(".claw/settings.local.json"));
|
||||
assert!(gitignore.contains(".claw/sessions/"));
|
||||
@@ -436,14 +485,24 @@ mod tests {
|
||||
fs::create_dir_all(&root).expect("create root");
|
||||
fs::write(root.join("CLAUDE.md"), "custom guidance\n").expect("write existing claude md");
|
||||
fs::write(root.join(".gitignore"), ".claw/settings.local.json\n").expect("write gitignore");
|
||||
fs::create_dir_all(root.join(".claw")).expect("create existing .claw dir");
|
||||
|
||||
let first = initialize_repo(&root).expect("first init should succeed");
|
||||
assert!(first
|
||||
.render()
|
||||
.contains("CLAUDE.md skipped (already exists)"));
|
||||
assert_eq!(
|
||||
first.artifacts_with_status(InitStatus::Partial),
|
||||
vec![".claw/".to_string()],
|
||||
"existing .claw/ should report partial when init creates missing settings.json"
|
||||
);
|
||||
assert!(root.join(".claw").join("settings.json").is_file());
|
||||
|
||||
let second = initialize_repo(&root).expect("second init should succeed");
|
||||
let second_rendered = second.render();
|
||||
assert!(second_rendered.contains(".claw/"));
|
||||
assert!(second_rendered.contains(".claw/settings.json"));
|
||||
assert!(second_rendered.contains(".claw/sessions/"));
|
||||
assert!(second_rendered.contains(".claw.json"));
|
||||
assert!(second_rendered.contains("skipped (already exists)"));
|
||||
assert!(second_rendered.contains(".gitignore skipped (already exists)"));
|
||||
@@ -474,16 +533,22 @@ mod tests {
|
||||
created_names,
|
||||
vec![
|
||||
".claw/".to_string(),
|
||||
".claw/settings.json".to_string(),
|
||||
".claw.json".to_string(),
|
||||
".gitignore".to_string(),
|
||||
"CLAUDE.md".to_string(),
|
||||
],
|
||||
"fresh init should place all four artifacts in created[]"
|
||||
"fresh init should place created artifacts in created[]"
|
||||
);
|
||||
assert!(
|
||||
fresh.artifacts_with_status(InitStatus::Skipped).is_empty(),
|
||||
"fresh init should have no skipped artifacts"
|
||||
);
|
||||
assert_eq!(
|
||||
fresh.artifacts_with_status(InitStatus::Deferred),
|
||||
vec![".claw/sessions/".to_string()],
|
||||
"fresh init should report session storage as deferred"
|
||||
);
|
||||
|
||||
let second = initialize_repo(&root).expect("second init should succeed");
|
||||
let skipped_names = second.artifacts_with_status(InitStatus::Skipped);
|
||||
@@ -491,27 +556,38 @@ mod tests {
|
||||
skipped_names,
|
||||
vec![
|
||||
".claw/".to_string(),
|
||||
".claw/settings.json".to_string(),
|
||||
".claw.json".to_string(),
|
||||
".gitignore".to_string(),
|
||||
"CLAUDE.md".to_string(),
|
||||
],
|
||||
"idempotent init should place all four artifacts in skipped[]"
|
||||
"idempotent init should place existing artifacts in skipped[]"
|
||||
);
|
||||
assert!(
|
||||
second.artifacts_with_status(InitStatus::Created).is_empty(),
|
||||
"idempotent init should have no created artifacts"
|
||||
);
|
||||
assert_eq!(
|
||||
second.artifacts_with_status(InitStatus::Deferred),
|
||||
vec![".claw/sessions/".to_string()],
|
||||
"idempotent init should keep session storage deferred until first save"
|
||||
);
|
||||
|
||||
// artifact_json_entries() uses the machine-stable `json_tag()` which
|
||||
// never changes wording (unlike `label()` which says "skipped (already exists)").
|
||||
let entries = second.artifact_json_entries();
|
||||
assert_eq!(entries.len(), 4);
|
||||
assert_eq!(entries.len(), 6);
|
||||
for entry in &entries {
|
||||
let name = entry.get("name").and_then(|v| v.as_str()).unwrap();
|
||||
let status = entry.get("status").and_then(|v| v.as_str()).unwrap();
|
||||
assert_eq!(
|
||||
status, "skipped",
|
||||
"machine status tag should be the bare word 'skipped', not label()'s 'skipped (already exists)'"
|
||||
);
|
||||
if name == ".claw/sessions/" {
|
||||
assert_eq!(status, "deferred");
|
||||
} else {
|
||||
assert_eq!(
|
||||
status, "skipped",
|
||||
"machine status tag should be the bare word 'skipped', not label()'s 'skipped (already exists)'"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root).expect("cleanup temp dir");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
#![allow(clippy::while_let_on_iterator)]
|
||||
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Output, Stdio};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -246,8 +247,121 @@ stderr:
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_subcommand_json_help_fails_fast_when_stdin_closed() {
|
||||
let workspace = unique_temp_dir("compact-nontty-json-help");
|
||||
fn prompt_subcommand_reads_prompt_from_stdin_when_no_positional_arg_423() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
let base_url = server.base_url();
|
||||
|
||||
let workspace = unique_temp_dir("prompt-stdin-423");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
|
||||
let prompt = format!("{SCENARIO_PREFIX}streaming_text\n");
|
||||
let output = run_claw_with_stdin(
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&base_url,
|
||||
&[
|
||||
"prompt",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--compact",
|
||||
"--permission-mode",
|
||||
"read-only",
|
||||
"--model",
|
||||
"sonnet",
|
||||
],
|
||||
&prompt,
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"prompt stdin run should succeed\nstdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let parsed: Value = serde_json::from_slice(&output.stdout).expect("stdout should parse");
|
||||
assert_eq!(
|
||||
parsed["message"],
|
||||
"Mock streaming says hello from the parity harness."
|
||||
);
|
||||
let captured = runtime.block_on(server.captured_requests());
|
||||
assert!(
|
||||
captured
|
||||
.iter()
|
||||
.any(|request| request.raw_body.contains("PARITY_SCENARIO:streaming_text")),
|
||||
"stdin prompt should reach the provider request: {captured:?}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_subcommand_stdin_flag_appends_pipe_context_423() {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("tokio runtime should build");
|
||||
let server = runtime
|
||||
.block_on(MockAnthropicService::spawn())
|
||||
.expect("mock service should start");
|
||||
let base_url = server.base_url();
|
||||
|
||||
let workspace = unique_temp_dir("prompt-stdin-flag-423");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
|
||||
let prompt_context = format!("{SCENARIO_PREFIX}streaming_text\n");
|
||||
let output = run_claw_with_stdin(
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&base_url,
|
||||
&[
|
||||
"prompt",
|
||||
"Use stdin context",
|
||||
"--stdin",
|
||||
"--output-format",
|
||||
"json",
|
||||
"--compact",
|
||||
"--permission-mode",
|
||||
"read-only",
|
||||
"--model",
|
||||
"sonnet",
|
||||
],
|
||||
&prompt_context,
|
||||
);
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"prompt --stdin run should succeed\nstdout:\n{}\n\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
let captured = runtime.block_on(server.captured_requests());
|
||||
let provider_body = captured
|
||||
.iter()
|
||||
.find(|request| request.raw_body.contains("Use stdin context"))
|
||||
.expect("merged prompt should reach provider");
|
||||
assert!(
|
||||
provider_body
|
||||
.raw_body
|
||||
.contains("PARITY_SCENARIO:streaming_text"),
|
||||
"merged prompt should include stdin context: {provider_body:?}"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(&workspace).expect("workspace cleanup should succeed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_subcommand_json_fails_fast_when_stdin_closed() {
|
||||
let workspace = unique_temp_dir("compact-nontty-json");
|
||||
let config_home = workspace.join("config-home");
|
||||
let home = workspace.join("home");
|
||||
fs::create_dir_all(&workspace).expect("workspace should exist");
|
||||
@@ -258,19 +372,19 @@ fn compact_subcommand_json_help_fails_fast_when_stdin_closed() {
|
||||
&workspace,
|
||||
&config_home,
|
||||
&home,
|
||||
&["compact", "--output-format", "json", "--help"],
|
||||
&["compact", "--output-format", "json"],
|
||||
Duration::from_secs(2),
|
||||
);
|
||||
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"compact json help should fail non-zero"
|
||||
"compact json should fail non-zero"
|
||||
);
|
||||
// #819/#820/#823: JSON abort envelopes route to stdout
|
||||
let stderr = String::from_utf8(output.stderr).expect("stderr should be utf8");
|
||||
assert!(
|
||||
stderr.trim().is_empty() || !stderr.trim_start().starts_with('{'),
|
||||
"compact json help should not emit JSON envelope to stderr (#819/#820/#823): {stderr}"
|
||||
"compact json should not emit JSON envelope to stderr (#819/#820/#823): {stderr}"
|
||||
);
|
||||
let stdout = String::from_utf8(output.stdout).expect("stdout should be utf8");
|
||||
let parsed: Value =
|
||||
@@ -356,6 +470,39 @@ fn run_claw(
|
||||
command.output().expect("claw should launch")
|
||||
}
|
||||
|
||||
fn run_claw_with_stdin(
|
||||
cwd: &std::path::Path,
|
||||
config_home: &std::path::Path,
|
||||
home: &std::path::Path,
|
||||
base_url: &str,
|
||||
args: &[&str],
|
||||
stdin: &str,
|
||||
) -> Output {
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_claw"))
|
||||
.current_dir(cwd)
|
||||
.env_clear()
|
||||
.env("ANTHROPIC_API_KEY", "test-compact-key")
|
||||
.env("ANTHROPIC_BASE_URL", base_url)
|
||||
.env("CLAW_CONFIG_HOME", config_home)
|
||||
.env("HOME", home)
|
||||
.env("NO_COLOR", "1")
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.args(args)
|
||||
.spawn()
|
||||
.expect("claw should launch");
|
||||
child
|
||||
.stdin
|
||||
.as_mut()
|
||||
.expect("stdin should be piped")
|
||||
.write_all(stdin.as_bytes())
|
||||
.expect("stdin should write");
|
||||
child.stdin.take();
|
||||
child.wait_with_output().expect("output should collect")
|
||||
}
|
||||
|
||||
fn run_claw_closed_stdin_with_timeout(
|
||||
cwd: &std::path::Path,
|
||||
config_home: &std::path::Path,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -222,6 +222,73 @@ fn resume_latest_restores_the_most_recent_managed_session() {
|
||||
assert!(stdout.contains(newer_path.to_str().expect("utf8 path")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_latest_missing_session_fails_without_creating_session_dirs_435() {
|
||||
// given
|
||||
let temp_dir = unique_temp_dir("resume-latest-missing-435");
|
||||
let project_dir = temp_dir.join("project");
|
||||
let config_home = temp_dir.join("config-home");
|
||||
let home = temp_dir.join("home");
|
||||
fs::create_dir_all(&project_dir).expect("project dir should exist");
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
fs::create_dir_all(&home).expect("home should exist");
|
||||
let envs = [
|
||||
(
|
||||
"CLAW_CONFIG_HOME",
|
||||
config_home.to_str().expect("utf8 config home"),
|
||||
),
|
||||
("HOME", home.to_str().expect("utf8 home")),
|
||||
("ANTHROPIC_API_KEY", ""),
|
||||
("ANTHROPIC_AUTH_TOKEN", ""),
|
||||
("OPENAI_API_KEY", ""),
|
||||
];
|
||||
|
||||
// when — both text and JSON resume failures should be non-zero and read-only.
|
||||
let text = run_claw_with_env(&project_dir, &["--resume", "latest"], &envs);
|
||||
let json = run_claw_with_env(
|
||||
&project_dir,
|
||||
&["--output-format", "json", "--resume", "latest"],
|
||||
&envs,
|
||||
);
|
||||
|
||||
// then
|
||||
assert_eq!(
|
||||
text.status.code(),
|
||||
Some(1),
|
||||
"text resume failure must be non-zero"
|
||||
);
|
||||
assert!(
|
||||
text.stdout.is_empty(),
|
||||
"text resume failure should not claim success on stdout: {}",
|
||||
String::from_utf8_lossy(&text.stdout)
|
||||
);
|
||||
let text_stderr = String::from_utf8_lossy(&text.stderr);
|
||||
assert!(
|
||||
text_stderr.contains("no managed sessions found"),
|
||||
"text failure should explain missing sessions: {text_stderr}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
json.status.code(),
|
||||
Some(1),
|
||||
"JSON resume failure must be non-zero"
|
||||
);
|
||||
assert!(
|
||||
json.stderr.is_empty(),
|
||||
"JSON resume failure should keep stderr empty: {}",
|
||||
String::from_utf8_lossy(&json.stderr)
|
||||
);
|
||||
let parsed: Value = serde_json::from_slice(&json.stdout)
|
||||
.expect("JSON resume failure should emit JSON to stdout");
|
||||
assert_eq!(parsed["status"], "error");
|
||||
assert_eq!(parsed["action"], "restore");
|
||||
assert_eq!(parsed["error_kind"], "no_managed_sessions");
|
||||
assert!(
|
||||
!project_dir.join(".claw").exists(),
|
||||
"failed resume must not create .claw/session directories"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resumed_status_command_emits_structured_json_when_requested() {
|
||||
// given
|
||||
@@ -268,7 +335,7 @@ fn resumed_status_command_emits_structured_json_when_requested() {
|
||||
assert_eq!(parsed["kind"], "status");
|
||||
// model is null in resume mode (not known without --model flag)
|
||||
assert!(parsed["model"].is_null());
|
||||
assert_eq!(parsed["permission_mode"], "danger-full-access");
|
||||
assert_eq!(parsed["permission_mode"], "workspace-write");
|
||||
assert_eq!(parsed["usage"]["messages"], 1);
|
||||
assert!(parsed["usage"]["turns"].is_number());
|
||||
assert!(parsed["workspace"]["cwd"].as_str().is_some());
|
||||
@@ -396,6 +463,9 @@ fn resumed_version_command_emits_structured_json() {
|
||||
assert!(parsed["version"].as_str().is_some());
|
||||
assert!(parsed["git_sha"].as_str().is_some());
|
||||
assert!(parsed["target"].as_str().is_some());
|
||||
assert!(parsed["git_sha_short"].as_str().is_some());
|
||||
assert!(parsed.get("message").is_none());
|
||||
assert!(parsed["human_readable"].as_str().is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user