use pretty_assertions::assert_eq;
use crate::agent::{CursorTool, PromptPolicy};
use crate::permission::PermissionMode;
use super::*;
fn allowed(mode: PermissionMode, tools: &[CursorTool]) -> AllowedTools {
map_permission_mode(mode, tools).expect("test tool list maps")
}
fn request(tools: &[CursorTool], model: Option<&str>, mode: PermissionMode) -> CursorSpawnRequest {
CursorSpawnRequest {
model: model.map(str::to_string),
allowed: allowed(mode, tools),
cwd: PathBuf::from("/tmp/project"),
}
}
fn forbidden_flags_absent(args: &[String]) {
for flag in ["--force", "--exclude-tools", "--show-thinking", "--resume"] {
assert!(
!args.iter().any(|arg| arg == flag),
"{flag} must never appear: {args:?}"
);
}
}
#[test]
fn builds_explicit_safe_spawn_args() {
let plan = build_spawn_plan(&request(
&[CursorTool::Read],
Some("composer-2.5"),
PermissionMode::Plan,
));
assert_eq!(
plan.args,
[
"-p",
"--output-format",
"stream-json",
"--stream-partial-output",
"--trust",
"--model",
"composer-2.5",
"--mode",
"plan",
"--allowed-tools",
"read_tool_call",
]
);
forbidden_flags_absent(&plan.args);
assert_eq!(plan.cwd, PathBuf::from("/tmp/project"));
let full = build_spawn_plan(&request(
&[CursorTool::Read, CursorTool::Edit, CursorTool::Shell],
Some("composer-2.5"),
PermissionMode::Bypass,
));
assert_eq!(
full.args,
[
"-p",
"--output-format",
"stream-json",
"--stream-partial-output",
"--trust",
"--model",
"composer-2.5",
"--allowed-tools",
"read_tool_call,edit_tool_call,shell_tool_call",
]
);
forbidden_flags_absent(&full.args);
let omitted_model =
build_spawn_plan(&request(&[CursorTool::Read], None, PermissionMode::Bypass));
assert!(!omitted_model.args.iter().any(|arg| arg == "--model"));
assert!(!omitted_model.args.iter().any(|arg| arg == "--mode"));
let finalized = finalize_spawn_args(&plan, "11111111-1111-4111-8111-111111111111");
assert_eq!(
finalized[plan.args.len()..],
[
OsString::from("--new-session-id"),
OsString::from("11111111-1111-4111-8111-111111111111"),
]
);
}
#[test]
fn rho_permission_modes_map_to_cursor_modes() {
let mixed = [CursorTool::Read, CursorTool::Edit, CursorTool::Shell];
let writes = [CursorTool::Edit, CursorTool::Shell];
let reads = [CursorTool::Read, CursorTool::Grep];
for (mode, tools, expected) in [
(
PermissionMode::Plan,
mixed.as_slice(),
Ok((CursorPermissionMode::Plan, vec![CursorTool::Read])),
),
(
PermissionMode::Bypass,
mixed.as_slice(),
Ok((CursorPermissionMode::Full, mixed.to_vec())),
),
(
PermissionMode::Plan,
reads.as_slice(),
Ok((CursorPermissionMode::Plan, reads.to_vec())),
),
(
PermissionMode::Plan,
writes.as_slice(),
Err(CursorSpawnError::NoToolsAllowed),
),
(
PermissionMode::Bypass,
&[][..],
Err(CursorSpawnError::NoToolsAllowed),
),
(
PermissionMode::Auto,
mixed.as_slice(),
Err(CursorSpawnError::ApprovalUnsupported(PermissionMode::Auto)),
),
(
PermissionMode::AllowEdits,
mixed.as_slice(),
Err(CursorSpawnError::ApprovalUnsupported(
PermissionMode::AllowEdits,
)),
),
(
PermissionMode::Supervised,
mixed.as_slice(),
Err(CursorSpawnError::ApprovalUnsupported(
PermissionMode::Supervised,
)),
),
] {
let got = map_permission_mode(mode, tools)
.map(|allowed| (allowed.mode(), allowed.tools().to_vec()));
assert_eq!(got, expected);
}
}
#[test]
fn frozen_identity_keeps_model_and_never_permission_flags() {
let generated = build_spawn_plan(&request(
&[CursorTool::Read, CursorTool::Edit],
Some("composer-2.5"),
PermissionMode::Bypass,
))
.args;
let frozen = vec![
"-p".into(),
"--model".into(),
"frozen-model".into(),
"--mode".into(),
"plan".into(),
"--allowed-tools".into(),
"shell_tool_call".into(),
"--force".into(),
"--exclude-tools".into(),
"edit_tool_call".into(),
];
let args = apply_frozen_identity_args(generated, &frozen);
assert!(
args.windows(2)
.any(|pair| pair == ["--model", "frozen-model"]),
"frozen model must overlay generated: {args:?}"
);
assert!(
!args.windows(2).any(|pair| pair == ["--mode", "plan"]),
"frozen --mode must not survive: {args:?}"
);
assert!(
args.windows(2)
.any(|pair| pair == ["--allowed-tools", "read_tool_call,edit_tool_call"]),
"generated allow list must remain: {args:?}"
);
forbidden_flags_absent(&args);
}
#[test]
fn compose_prompt_extends_or_passes_through() {
assert_eq!(
compose_prompt(&PromptPolicy::Extend("Be terse.".into()), "hello").unwrap(),
"Be terse.\n\n---\n\nhello"
);
assert_eq!(
compose_prompt(&PromptPolicy::Extend(String::new()), "hello").unwrap(),
"hello"
);
assert_eq!(
compose_prompt(&PromptPolicy::Replace("nope".into()), "hello"),
Err(CursorPromptError::ReplaceUnsupported)
);
}