mirror of
https://github.com/instructkr/claude-code.git
synced 2026-04-06 19:28:49 +03:00
Close the plan-mode parity gap for worktree-local tool flows
PARITY.md still flags missing plan/worktree entry-exit tools. This change adds EnterPlanMode and ExitPlanMode to the Rust tool registry, stores reversible worktree-local state under .claw/tool-state, and restores or clears the prior local permission override on exit. The round-trip tests cover both restoring an existing local override and cleaning up a tool-created override from an empty local state. Constraint: Must keep the override worktree-local and reversible without mutating higher-scope settings Rejected: Reuse Config alone with no state file | exit could not safely restore absent-vs-local overrides Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep plan-mode state tracking aligned with settings.local.json precedence before adding worktree enter/exit tools Tested: cargo test -p tools Not-tested: interactive CLI prompt-mode invocation of the new tools
This commit is contained in:
@@ -504,6 +504,26 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
|
|||||||
}),
|
}),
|
||||||
required_permission: PermissionMode::WorkspaceWrite,
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
},
|
},
|
||||||
|
ToolSpec {
|
||||||
|
name: "EnterPlanMode",
|
||||||
|
description: "Enable a worktree-local planning mode override and remember the previous local setting for ExitPlanMode.",
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": false
|
||||||
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
|
},
|
||||||
|
ToolSpec {
|
||||||
|
name: "ExitPlanMode",
|
||||||
|
description: "Restore or clear the worktree-local planning mode override created by EnterPlanMode.",
|
||||||
|
input_schema: json!({
|
||||||
|
"type": "object",
|
||||||
|
"properties": {},
|
||||||
|
"additionalProperties": false
|
||||||
|
}),
|
||||||
|
required_permission: PermissionMode::WorkspaceWrite,
|
||||||
|
},
|
||||||
ToolSpec {
|
ToolSpec {
|
||||||
name: "StructuredOutput",
|
name: "StructuredOutput",
|
||||||
description: "Return structured output in the requested format.",
|
description: "Return structured output in the requested format.",
|
||||||
@@ -565,6 +585,8 @@ pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
|
|||||||
"Sleep" => from_value::<SleepInput>(input).and_then(run_sleep),
|
"Sleep" => from_value::<SleepInput>(input).and_then(run_sleep),
|
||||||
"SendUserMessage" | "Brief" => from_value::<BriefInput>(input).and_then(run_brief),
|
"SendUserMessage" | "Brief" => from_value::<BriefInput>(input).and_then(run_brief),
|
||||||
"Config" => from_value::<ConfigInput>(input).and_then(run_config),
|
"Config" => from_value::<ConfigInput>(input).and_then(run_config),
|
||||||
|
"EnterPlanMode" => from_value::<EnterPlanModeInput>(input).and_then(run_enter_plan_mode),
|
||||||
|
"ExitPlanMode" => from_value::<ExitPlanModeInput>(input).and_then(run_exit_plan_mode),
|
||||||
"StructuredOutput" => {
|
"StructuredOutput" => {
|
||||||
from_value::<StructuredOutputInput>(input).and_then(run_structured_output)
|
from_value::<StructuredOutputInput>(input).and_then(run_structured_output)
|
||||||
}
|
}
|
||||||
@@ -658,6 +680,14 @@ fn run_config(input: ConfigInput) -> Result<String, String> {
|
|||||||
to_pretty_json(execute_config(input)?)
|
to_pretty_json(execute_config(input)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_enter_plan_mode(input: EnterPlanModeInput) -> Result<String, String> {
|
||||||
|
to_pretty_json(execute_enter_plan_mode(input)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_exit_plan_mode(input: ExitPlanModeInput) -> Result<String, String> {
|
||||||
|
to_pretty_json(execute_exit_plan_mode(input)?)
|
||||||
|
}
|
||||||
|
|
||||||
fn run_structured_output(input: StructuredOutputInput) -> Result<String, String> {
|
fn run_structured_output(input: StructuredOutputInput) -> Result<String, String> {
|
||||||
to_pretty_json(execute_structured_output(input)?)
|
to_pretty_json(execute_structured_output(input)?)
|
||||||
}
|
}
|
||||||
@@ -810,6 +840,14 @@ struct ConfigInput {
|
|||||||
value: Option<ConfigValue>,
|
value: Option<ConfigValue>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
struct EnterPlanModeInput {}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize)]
|
||||||
|
#[serde(default)]
|
||||||
|
struct ExitPlanModeInput {}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
enum ConfigValue {
|
enum ConfigValue {
|
||||||
@@ -967,6 +1005,32 @@ struct ConfigOutput {
|
|||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct PlanModeState {
|
||||||
|
#[serde(rename = "hadLocalOverride")]
|
||||||
|
had_local_override: bool,
|
||||||
|
#[serde(rename = "previousLocalMode")]
|
||||||
|
previous_local_mode: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct PlanModeOutput {
|
||||||
|
success: bool,
|
||||||
|
operation: String,
|
||||||
|
changed: bool,
|
||||||
|
active: bool,
|
||||||
|
managed: bool,
|
||||||
|
message: String,
|
||||||
|
#[serde(rename = "settingsPath")]
|
||||||
|
settings_path: String,
|
||||||
|
#[serde(rename = "statePath")]
|
||||||
|
state_path: String,
|
||||||
|
#[serde(rename = "previousLocalMode")]
|
||||||
|
previous_local_mode: Option<Value>,
|
||||||
|
#[serde(rename = "currentLocalMode")]
|
||||||
|
current_local_mode: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct StructuredOutputResult {
|
struct StructuredOutputResult {
|
||||||
data: String,
|
data: String,
|
||||||
@@ -2575,6 +2639,148 @@ fn execute_config(input: ConfigInput) -> Result<ConfigOutput, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PERMISSION_DEFAULT_MODE_PATH: &[&str] = &["permissions", "defaultMode"];
|
||||||
|
|
||||||
|
fn execute_enter_plan_mode(_input: EnterPlanModeInput) -> Result<PlanModeOutput, String> {
|
||||||
|
let settings_path = config_file_for_scope(ConfigScope::Settings)?;
|
||||||
|
let state_path = plan_mode_state_file()?;
|
||||||
|
let mut document = read_json_object(&settings_path)?;
|
||||||
|
let current_local_mode = get_nested_value(&document, PERMISSION_DEFAULT_MODE_PATH).cloned();
|
||||||
|
let current_is_plan =
|
||||||
|
matches!(current_local_mode.as_ref(), Some(Value::String(value)) if value == "plan");
|
||||||
|
|
||||||
|
if let Some(state) = read_plan_mode_state(&state_path)? {
|
||||||
|
if current_is_plan {
|
||||||
|
return Ok(PlanModeOutput {
|
||||||
|
success: true,
|
||||||
|
operation: String::from("enter"),
|
||||||
|
changed: false,
|
||||||
|
active: true,
|
||||||
|
managed: true,
|
||||||
|
message: String::from("Plan mode override is already active for this worktree."),
|
||||||
|
settings_path: settings_path.display().to_string(),
|
||||||
|
state_path: state_path.display().to_string(),
|
||||||
|
previous_local_mode: state.previous_local_mode,
|
||||||
|
current_local_mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
clear_plan_mode_state(&state_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_is_plan {
|
||||||
|
return Ok(PlanModeOutput {
|
||||||
|
success: true,
|
||||||
|
operation: String::from("enter"),
|
||||||
|
changed: false,
|
||||||
|
active: true,
|
||||||
|
managed: false,
|
||||||
|
message: String::from(
|
||||||
|
"Worktree-local plan mode is already enabled outside EnterPlanMode; leaving it unchanged.",
|
||||||
|
),
|
||||||
|
settings_path: settings_path.display().to_string(),
|
||||||
|
state_path: state_path.display().to_string(),
|
||||||
|
previous_local_mode: None,
|
||||||
|
current_local_mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = PlanModeState {
|
||||||
|
had_local_override: current_local_mode.is_some(),
|
||||||
|
previous_local_mode: current_local_mode.clone(),
|
||||||
|
};
|
||||||
|
write_plan_mode_state(&state_path, &state)?;
|
||||||
|
set_nested_value(
|
||||||
|
&mut document,
|
||||||
|
PERMISSION_DEFAULT_MODE_PATH,
|
||||||
|
Value::String(String::from("plan")),
|
||||||
|
);
|
||||||
|
write_json_object(&settings_path, &document)?;
|
||||||
|
|
||||||
|
Ok(PlanModeOutput {
|
||||||
|
success: true,
|
||||||
|
operation: String::from("enter"),
|
||||||
|
changed: true,
|
||||||
|
active: true,
|
||||||
|
managed: true,
|
||||||
|
message: String::from("Enabled worktree-local plan mode override."),
|
||||||
|
settings_path: settings_path.display().to_string(),
|
||||||
|
state_path: state_path.display().to_string(),
|
||||||
|
previous_local_mode: state.previous_local_mode,
|
||||||
|
current_local_mode: get_nested_value(&document, PERMISSION_DEFAULT_MODE_PATH).cloned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute_exit_plan_mode(_input: ExitPlanModeInput) -> Result<PlanModeOutput, String> {
|
||||||
|
let settings_path = config_file_for_scope(ConfigScope::Settings)?;
|
||||||
|
let state_path = plan_mode_state_file()?;
|
||||||
|
let mut document = read_json_object(&settings_path)?;
|
||||||
|
let current_local_mode = get_nested_value(&document, PERMISSION_DEFAULT_MODE_PATH).cloned();
|
||||||
|
let current_is_plan =
|
||||||
|
matches!(current_local_mode.as_ref(), Some(Value::String(value)) if value == "plan");
|
||||||
|
|
||||||
|
let Some(state) = read_plan_mode_state(&state_path)? else {
|
||||||
|
return Ok(PlanModeOutput {
|
||||||
|
success: true,
|
||||||
|
operation: String::from("exit"),
|
||||||
|
changed: false,
|
||||||
|
active: current_is_plan,
|
||||||
|
managed: false,
|
||||||
|
message: String::from("No EnterPlanMode override is active for this worktree."),
|
||||||
|
settings_path: settings_path.display().to_string(),
|
||||||
|
state_path: state_path.display().to_string(),
|
||||||
|
previous_local_mode: None,
|
||||||
|
current_local_mode,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if !current_is_plan {
|
||||||
|
clear_plan_mode_state(&state_path)?;
|
||||||
|
return Ok(PlanModeOutput {
|
||||||
|
success: true,
|
||||||
|
operation: String::from("exit"),
|
||||||
|
changed: false,
|
||||||
|
active: false,
|
||||||
|
managed: false,
|
||||||
|
message: String::from(
|
||||||
|
"Cleared stale EnterPlanMode state because plan mode was already changed outside the tool.",
|
||||||
|
),
|
||||||
|
settings_path: settings_path.display().to_string(),
|
||||||
|
state_path: state_path.display().to_string(),
|
||||||
|
previous_local_mode: state.previous_local_mode,
|
||||||
|
current_local_mode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.had_local_override {
|
||||||
|
if let Some(previous_local_mode) = state.previous_local_mode.clone() {
|
||||||
|
set_nested_value(
|
||||||
|
&mut document,
|
||||||
|
PERMISSION_DEFAULT_MODE_PATH,
|
||||||
|
previous_local_mode,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
remove_nested_value(&mut document, PERMISSION_DEFAULT_MODE_PATH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
remove_nested_value(&mut document, PERMISSION_DEFAULT_MODE_PATH);
|
||||||
|
}
|
||||||
|
write_json_object(&settings_path, &document)?;
|
||||||
|
clear_plan_mode_state(&state_path)?;
|
||||||
|
|
||||||
|
Ok(PlanModeOutput {
|
||||||
|
success: true,
|
||||||
|
operation: String::from("exit"),
|
||||||
|
changed: true,
|
||||||
|
active: false,
|
||||||
|
managed: false,
|
||||||
|
message: String::from("Restored the prior worktree-local plan mode setting."),
|
||||||
|
settings_path: settings_path.display().to_string(),
|
||||||
|
state_path: state_path.display().to_string(),
|
||||||
|
previous_local_mode: state.previous_local_mode,
|
||||||
|
current_local_mode: get_nested_value(&document, PERMISSION_DEFAULT_MODE_PATH).cloned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn execute_structured_output(
|
fn execute_structured_output(
|
||||||
input: StructuredOutputInput,
|
input: StructuredOutputInput,
|
||||||
) -> Result<StructuredOutputResult, String> {
|
) -> Result<StructuredOutputResult, String> {
|
||||||
@@ -2902,6 +3108,72 @@ fn set_nested_value(root: &mut serde_json::Map<String, Value>, path: &[&str], ne
|
|||||||
set_nested_value(map, rest, new_value);
|
set_nested_value(map, rest, new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_nested_value(root: &mut serde_json::Map<String, Value>, path: &[&str]) -> bool {
|
||||||
|
let Some((first, rest)) = path.split_first() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if rest.is_empty() {
|
||||||
|
return root.remove(*first).is_some();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut should_remove_parent = false;
|
||||||
|
let removed = root.get_mut(*first).is_some_and(|entry| {
|
||||||
|
entry.as_object_mut().is_some_and(|map| {
|
||||||
|
let removed = remove_nested_value(map, rest);
|
||||||
|
should_remove_parent = removed && map.is_empty();
|
||||||
|
removed
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if should_remove_parent {
|
||||||
|
root.remove(*first);
|
||||||
|
}
|
||||||
|
|
||||||
|
removed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn plan_mode_state_file() -> Result<PathBuf, String> {
|
||||||
|
Ok(config_file_for_scope(ConfigScope::Settings)?
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| String::from("settings.local.json has no parent directory"))?
|
||||||
|
.join("tool-state")
|
||||||
|
.join("plan-mode.json"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_plan_mode_state(path: &Path) -> Result<Option<PlanModeState>, String> {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(contents) => {
|
||||||
|
if contents.trim().is_empty() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
serde_json::from_str(&contents)
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
|
Err(error) => Err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_plan_mode_state(path: &Path, state: &PlanModeState) -> Result<(), String> {
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
|
||||||
|
}
|
||||||
|
std::fs::write(
|
||||||
|
path,
|
||||||
|
serde_json::to_string_pretty(state).map_err(|error| error.to_string())?,
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_plan_mode_state(path: &Path) -> Result<(), String> {
|
||||||
|
match std::fs::remove_file(path) {
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||||
|
Err(error) => Err(error.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn iso8601_timestamp() -> String {
|
fn iso8601_timestamp() -> String {
|
||||||
if let Ok(output) = Command::new("date")
|
if let Ok(output) = Command::new("date")
|
||||||
.args(["-u", "+%Y-%m-%dT%H:%M:%SZ"])
|
.args(["-u", "+%Y-%m-%dT%H:%M:%SZ"])
|
||||||
@@ -3190,6 +3462,8 @@ mod tests {
|
|||||||
assert!(names.contains(&"Sleep"));
|
assert!(names.contains(&"Sleep"));
|
||||||
assert!(names.contains(&"SendUserMessage"));
|
assert!(names.contains(&"SendUserMessage"));
|
||||||
assert!(names.contains(&"Config"));
|
assert!(names.contains(&"Config"));
|
||||||
|
assert!(names.contains(&"EnterPlanMode"));
|
||||||
|
assert!(names.contains(&"ExitPlanMode"));
|
||||||
assert!(names.contains(&"StructuredOutput"));
|
assert!(names.contains(&"StructuredOutput"));
|
||||||
assert!(names.contains(&"REPL"));
|
assert!(names.contains(&"REPL"));
|
||||||
assert!(names.contains(&"PowerShell"));
|
assert!(names.contains(&"PowerShell"));
|
||||||
@@ -4385,6 +4659,140 @@ mod tests {
|
|||||||
let _ = std::fs::remove_dir_all(root);
|
let _ = std::fs::remove_dir_all(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn enter_and_exit_plan_mode_round_trip_existing_local_override() {
|
||||||
|
let _guard = env_lock()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"clawd-plan-mode-{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("time")
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
let home = root.join("home");
|
||||||
|
let cwd = root.join("cwd");
|
||||||
|
std::fs::create_dir_all(home.join(".claw")).expect("home dir");
|
||||||
|
std::fs::create_dir_all(cwd.join(".claw")).expect("cwd dir");
|
||||||
|
std::fs::write(
|
||||||
|
cwd.join(".claw").join("settings.local.json"),
|
||||||
|
r#"{"permissions":{"defaultMode":"acceptEdits"}}"#,
|
||||||
|
)
|
||||||
|
.expect("write local settings");
|
||||||
|
|
||||||
|
let original_home = std::env::var("HOME").ok();
|
||||||
|
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||||
|
let original_dir = std::env::current_dir().expect("cwd");
|
||||||
|
std::env::set_var("HOME", &home);
|
||||||
|
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||||
|
std::env::set_current_dir(&cwd).expect("set cwd");
|
||||||
|
|
||||||
|
let enter = execute_tool("EnterPlanMode", &json!({})).expect("enter plan mode");
|
||||||
|
let enter_output: serde_json::Value = serde_json::from_str(&enter).expect("json");
|
||||||
|
assert_eq!(enter_output["changed"], true);
|
||||||
|
assert_eq!(enter_output["managed"], true);
|
||||||
|
assert_eq!(enter_output["previousLocalMode"], "acceptEdits");
|
||||||
|
assert_eq!(enter_output["currentLocalMode"], "plan");
|
||||||
|
|
||||||
|
let local_settings = std::fs::read_to_string(cwd.join(".claw").join("settings.local.json"))
|
||||||
|
.expect("local settings after enter");
|
||||||
|
assert!(local_settings.contains(r#""defaultMode": "plan""#));
|
||||||
|
let state =
|
||||||
|
std::fs::read_to_string(cwd.join(".claw").join("tool-state").join("plan-mode.json"))
|
||||||
|
.expect("plan mode state");
|
||||||
|
assert!(state.contains(r#""hadLocalOverride": true"#));
|
||||||
|
assert!(state.contains(r#""previousLocalMode": "acceptEdits""#));
|
||||||
|
|
||||||
|
let exit = execute_tool("ExitPlanMode", &json!({})).expect("exit plan mode");
|
||||||
|
let exit_output: serde_json::Value = serde_json::from_str(&exit).expect("json");
|
||||||
|
assert_eq!(exit_output["changed"], true);
|
||||||
|
assert_eq!(exit_output["managed"], false);
|
||||||
|
assert_eq!(exit_output["previousLocalMode"], "acceptEdits");
|
||||||
|
assert_eq!(exit_output["currentLocalMode"], "acceptEdits");
|
||||||
|
|
||||||
|
let local_settings = std::fs::read_to_string(cwd.join(".claw").join("settings.local.json"))
|
||||||
|
.expect("local settings after exit");
|
||||||
|
assert!(local_settings.contains(r#""defaultMode": "acceptEdits""#));
|
||||||
|
assert!(!cwd
|
||||||
|
.join(".claw")
|
||||||
|
.join("tool-state")
|
||||||
|
.join("plan-mode.json")
|
||||||
|
.exists());
|
||||||
|
|
||||||
|
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||||
|
match original_home {
|
||||||
|
Some(value) => std::env::set_var("HOME", value),
|
||||||
|
None => std::env::remove_var("HOME"),
|
||||||
|
}
|
||||||
|
match original_config_home {
|
||||||
|
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||||
|
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exit_plan_mode_clears_override_when_enter_created_it_from_empty_local_state() {
|
||||||
|
let _guard = env_lock()
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
let root = std::env::temp_dir().join(format!(
|
||||||
|
"clawd-plan-mode-empty-{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.expect("time")
|
||||||
|
.as_nanos()
|
||||||
|
));
|
||||||
|
let home = root.join("home");
|
||||||
|
let cwd = root.join("cwd");
|
||||||
|
std::fs::create_dir_all(home.join(".claw")).expect("home dir");
|
||||||
|
std::fs::create_dir_all(cwd.join(".claw")).expect("cwd dir");
|
||||||
|
|
||||||
|
let original_home = std::env::var("HOME").ok();
|
||||||
|
let original_config_home = std::env::var("CLAW_CONFIG_HOME").ok();
|
||||||
|
let original_dir = std::env::current_dir().expect("cwd");
|
||||||
|
std::env::set_var("HOME", &home);
|
||||||
|
std::env::remove_var("CLAW_CONFIG_HOME");
|
||||||
|
std::env::set_current_dir(&cwd).expect("set cwd");
|
||||||
|
|
||||||
|
let enter = execute_tool("EnterPlanMode", &json!({})).expect("enter plan mode");
|
||||||
|
let enter_output: serde_json::Value = serde_json::from_str(&enter).expect("json");
|
||||||
|
assert_eq!(enter_output["previousLocalMode"], serde_json::Value::Null);
|
||||||
|
assert_eq!(enter_output["currentLocalMode"], "plan");
|
||||||
|
|
||||||
|
let exit = execute_tool("ExitPlanMode", &json!({})).expect("exit plan mode");
|
||||||
|
let exit_output: serde_json::Value = serde_json::from_str(&exit).expect("json");
|
||||||
|
assert_eq!(exit_output["changed"], true);
|
||||||
|
assert_eq!(exit_output["currentLocalMode"], serde_json::Value::Null);
|
||||||
|
|
||||||
|
let local_settings = std::fs::read_to_string(cwd.join(".claw").join("settings.local.json"))
|
||||||
|
.expect("local settings after exit");
|
||||||
|
let local_settings_json: serde_json::Value =
|
||||||
|
serde_json::from_str(&local_settings).expect("valid settings json");
|
||||||
|
assert_eq!(
|
||||||
|
local_settings_json.get("permissions"),
|
||||||
|
None,
|
||||||
|
"permissions override should be removed on exit"
|
||||||
|
);
|
||||||
|
assert!(!cwd
|
||||||
|
.join(".claw")
|
||||||
|
.join("tool-state")
|
||||||
|
.join("plan-mode.json")
|
||||||
|
.exists());
|
||||||
|
|
||||||
|
std::env::set_current_dir(&original_dir).expect("restore cwd");
|
||||||
|
match original_home {
|
||||||
|
Some(value) => std::env::set_var("HOME", value),
|
||||||
|
None => std::env::remove_var("HOME"),
|
||||||
|
}
|
||||||
|
match original_config_home {
|
||||||
|
Some(value) => std::env::set_var("CLAW_CONFIG_HOME", value),
|
||||||
|
None => std::env::remove_var("CLAW_CONFIG_HOME"),
|
||||||
|
}
|
||||||
|
let _ = std::fs::remove_dir_all(root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn structured_output_echoes_input_payload() {
|
fn structured_output_echoes_input_payload() {
|
||||||
let result = execute_tool("StructuredOutput", &json!({"ok": true, "items": [1, 2, 3]}))
|
let result = execute_tool("StructuredOutput", &json!({"ok": true, "items": [1, 2, 3]}))
|
||||||
|
|||||||
Reference in New Issue
Block a user