mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-12 19:14:51 +08: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
This commit is contained in:
@@ -266,7 +266,7 @@ fn command_in(cwd: &Path) -> Command {
|
||||
|
||||
fn write_session(root: &Path, label: &str) -> PathBuf {
|
||||
let session_path = root.join(format!("{label}.jsonl"));
|
||||
let mut session = Session::new();
|
||||
let mut session = Session::new().with_workspace_root(root.to_path_buf());
|
||||
session
|
||||
.push_user_text(format!("session fixture for {label}"))
|
||||
.expect("session write should succeed");
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::process::{Command, Output};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use runtime::Session;
|
||||
use serde_json::Value;
|
||||
|
||||
static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -236,12 +237,7 @@ fn doctor_and_resume_status_emit_json_when_requested() {
|
||||
assert!(sandbox["enabled"].is_boolean());
|
||||
assert!(sandbox["fallback_reason"].is_null() || sandbox["fallback_reason"].is_string());
|
||||
|
||||
let session_path = root.join("session.jsonl");
|
||||
fs::write(
|
||||
&session_path,
|
||||
"{\"type\":\"session_meta\",\"version\":3,\"session_id\":\"resume-json\",\"created_at_ms\":0,\"updated_at_ms\":0}\n{\"type\":\"message\",\"message\":{\"role\":\"user\",\"blocks\":[{\"type\":\"text\",\"text\":\"hello\"}]}}\n",
|
||||
)
|
||||
.expect("session should write");
|
||||
let session_path = write_session_fixture(&root, "resume-json", Some("hello"));
|
||||
let resumed = assert_json_command(
|
||||
&root,
|
||||
&[
|
||||
@@ -268,12 +264,7 @@ fn resumed_inventory_commands_emit_structured_json_when_requested() {
|
||||
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 session_path = write_session_fixture(&root, "resume-inventory-json", Some("inventory"));
|
||||
|
||||
let mcp = assert_json_command_with_env(
|
||||
&root,
|
||||
@@ -324,12 +315,7 @@ 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 session_path = write_session_fixture(&root, "resume-version-init-json", None);
|
||||
|
||||
let version = assert_json_command(
|
||||
&root,
|
||||
@@ -405,6 +391,24 @@ fn write_upstream_fixture(root: &Path) -> PathBuf {
|
||||
upstream
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -20,7 +20,7 @@ fn resumed_binary_accepts_slash_commands_with_arguments() {
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
let export_path = temp_dir.join("notes.txt");
|
||||
|
||||
let mut session = Session::new();
|
||||
let mut session = workspace_session(&temp_dir);
|
||||
session
|
||||
.push_user_text("ship the slash command harness")
|
||||
.expect("session write should succeed");
|
||||
@@ -122,7 +122,7 @@ fn resumed_config_command_loads_settings_files_end_to_end() {
|
||||
fs::create_dir_all(&config_home).expect("config home should exist");
|
||||
|
||||
let session_path = project_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
workspace_session(&project_dir)
|
||||
.with_persistence_path(&session_path)
|
||||
.save_to_path(&session_path)
|
||||
.expect("session should persist");
|
||||
@@ -180,13 +180,11 @@ fn resume_latest_restores_the_most_recent_managed_session() {
|
||||
// given
|
||||
let temp_dir = unique_temp_dir("resume-latest");
|
||||
let project_dir = temp_dir.join("project");
|
||||
let sessions_dir = project_dir.join(".claw").join("sessions");
|
||||
fs::create_dir_all(&sessions_dir).expect("sessions dir should exist");
|
||||
let store = runtime::SessionStore::from_cwd(&project_dir).expect("session store should build");
|
||||
let older_path = store.create_handle("session-older").path;
|
||||
let newer_path = store.create_handle("session-newer").path;
|
||||
|
||||
let older_path = sessions_dir.join("session-older.jsonl");
|
||||
let newer_path = sessions_dir.join("session-newer.jsonl");
|
||||
|
||||
let mut older = Session::new().with_persistence_path(&older_path);
|
||||
let mut older = workspace_session(&project_dir).with_persistence_path(&older_path);
|
||||
older
|
||||
.push_user_text("older session")
|
||||
.expect("older session write should succeed");
|
||||
@@ -194,7 +192,7 @@ fn resume_latest_restores_the_most_recent_managed_session() {
|
||||
.save_to_path(&older_path)
|
||||
.expect("older session should persist");
|
||||
|
||||
let mut newer = Session::new().with_persistence_path(&newer_path);
|
||||
let mut newer = workspace_session(&project_dir).with_persistence_path(&newer_path);
|
||||
newer
|
||||
.push_user_text("newer session")
|
||||
.expect("newer session write should succeed");
|
||||
@@ -229,7 +227,7 @@ fn resumed_status_command_emits_structured_json_when_requested() {
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
|
||||
let mut session = Session::new();
|
||||
let mut session = workspace_session(&temp_dir);
|
||||
session
|
||||
.push_user_text("resume status json fixture")
|
||||
.expect("session write should succeed");
|
||||
@@ -283,7 +281,7 @@ fn resumed_status_surfaces_persisted_model() {
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
|
||||
let mut session = Session::new();
|
||||
let mut session = workspace_session(&temp_dir);
|
||||
session.model = Some("claude-sonnet-4-6".to_string());
|
||||
session
|
||||
.push_user_text("model persistence fixture")
|
||||
@@ -324,7 +322,7 @@ fn resumed_sandbox_command_emits_structured_json_when_requested() {
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
|
||||
Session::new()
|
||||
workspace_session(&temp_dir)
|
||||
.save_to_path(&session_path)
|
||||
.expect("session should persist");
|
||||
|
||||
@@ -365,7 +363,7 @@ fn resumed_version_command_emits_structured_json() {
|
||||
let temp_dir = unique_temp_dir("resume-version-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
workspace_session(&temp_dir)
|
||||
.save_to_path(&session_path)
|
||||
.expect("session should persist");
|
||||
|
||||
@@ -398,7 +396,7 @@ fn resumed_export_command_emits_structured_json() {
|
||||
let temp_dir = unique_temp_dir("resume-export-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
let mut session = Session::new();
|
||||
let mut session = workspace_session(&temp_dir);
|
||||
session
|
||||
.push_user_text("export json fixture")
|
||||
.expect("write ok");
|
||||
@@ -432,7 +430,7 @@ fn resumed_help_command_emits_structured_json() {
|
||||
let temp_dir = unique_temp_dir("resume-help-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
workspace_session(&temp_dir)
|
||||
.save_to_path(&session_path)
|
||||
.expect("persist ok");
|
||||
|
||||
@@ -465,7 +463,7 @@ fn resumed_no_command_emits_restored_json() {
|
||||
let temp_dir = unique_temp_dir("resume-no-cmd-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
let mut session = Session::new();
|
||||
let mut session = workspace_session(&temp_dir);
|
||||
session
|
||||
.push_user_text("restored json fixture")
|
||||
.expect("write ok");
|
||||
@@ -499,7 +497,7 @@ fn resumed_stub_command_emits_not_implemented_json() {
|
||||
let temp_dir = unique_temp_dir("resume-stub-json");
|
||||
fs::create_dir_all(&temp_dir).expect("temp dir should exist");
|
||||
let session_path = temp_dir.join("session.jsonl");
|
||||
Session::new()
|
||||
workspace_session(&temp_dir)
|
||||
.save_to_path(&session_path)
|
||||
.expect("persist ok");
|
||||
|
||||
@@ -533,6 +531,10 @@ fn run_claw(current_dir: &Path, args: &[&str]) -> Output {
|
||||
run_claw_with_env(current_dir, args, &[])
|
||||
}
|
||||
|
||||
fn workspace_session(root: &Path) -> Session {
|
||||
Session::new().with_workspace_root(root.to_path_buf())
|
||||
}
|
||||
|
||||
fn run_claw_with_env(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);
|
||||
|
||||
Reference in New Issue
Block a user