mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
test(windows): align three Full Access tests with the shipped posture and a real Windows shell
`Test (windows-latest)` is a required check and has failed on main since before
the runtime-split work, with the same three tests every time, which blocks every
pull request. All three trace to bdcc8cfc5f ("let Full Access actually grant
Full Access"), which made Full Access skip the deterministic floors and let a
shell command actually run; those tests were never run on Windows against that
change.
- `windows_full_access_node_image_kill_is_denied_before_any_shell_effect`
still expected the Windows Node-launcher hold under Full Access, which that
commit removed on purpose (its policy test, `auto_review.rs`, now asserts Full
Access skips the hold). The test now runs in the Auto posture, where the hold
and denial still apply, via the existing `auto_review_message_op`; renamed
`windows_auto_review_node_image_kill_...`. Full Access is deliberately
unchanged.
- `full_access_permission_allow_runs_background_destructive_shell_without_a_hold`
read its sentinel file once after TurnComplete. The command is backgrounded
and Windows starts PowerShell from a temp .ps1 (four quotes in the command),
so the file was still `guarded` and the shell is killed on engine shutdown. It
now polls, up to 60s, before shutting the engine down.
- `acp_full_access_keeps_repo_law_without_permission_modal` hit `Rig::prompt`'s
20s deadline on Windows (the one ACP test that runs a real shell, now that
Full Access executes it). The deadline is 120s on Windows, 20s elsewhere.
Both sentinel checks share one new helper, `test_support::read_shell_sentinel`,
that tolerates a UTF-16 BOM, CRLF and the quotes `cmd`/PowerShell `echo` keep,
and compare the normalized text against the fixture's destructive-looking
sentinel string.
Evidence: macOS only. nextest on the two engine tests, the ACP test and the
sibling `yolo_mode_does_not_prompt_for_background_shell` plus `test(acp_server)`:
40 passed, 0 failed; rustfmt clean. NOT verified on Windows: the node-kill test
is cfg(windows) and `cargo check --target x86_64-pc-windows-msvc` cannot run
here (ring needs a Windows C toolchain), and whether the ACP timeout was shell
cold-start latency or a real foreground-exit hang is a 55%-confidence reading;
Windows CI is the proof.
Refs #6934
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
1 parent
f723f64f33
commit
f7f60c356d
4 files changed
+37
-11
No files matched your search
@@ -1235,7 +1235,7 @@ mod tests {
|
||||
let mut reader = codewhale_app_server::BoundedLines::new(BufReader::new(input));
|
||||
let mut output = Vec::new();
|
||||
let result = tokio::time::timeout(
|
||||
Duration::from_secs(20),
|
||||
Duration::from_secs(if cfg!(windows) { 120 } else { 20 }),
|
||||
self.server.drive_prompt(
|
||||
json!({"sessionId":session,"prompt":text}),
|
||||
&mut reader,
|
||||
@@ -2483,8 +2483,8 @@ mod tests {
|
||||
"allowed"
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(rig.workspace.join("floor-sentinel.txt"))?,
|
||||
"rm -rf /\n"
|
||||
crate::test_support::read_shell_sentinel(&rig.workspace.join("floor-sentinel.txt")),
|
||||
"rm -rf /"
|
||||
);
|
||||
assert!(!rig.workspace.join("CHANGELOG.md").exists());
|
||||
assert!(
|
||||
|
||||
@@ -905,7 +905,7 @@ async fn collect_guardian_journey_with_receipts(
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_full_access_node_image_kill_is_denied_before_any_shell_effect() {
|
||||
fn windows_auto_review_node_image_kill_is_denied_before_any_shell_effect() {
|
||||
use crate::llm_client::mock::{MockLlmClient, canned};
|
||||
|
||||
with_artifact_home(|home| {
|
||||
@@ -940,12 +940,9 @@ fn windows_full_access_node_image_kill_is_denied_before_any_shell_effect() {
|
||||
let (engine, handle) = Engine::new_with_model_client(
|
||||
deterministic_engine_config(&workspace), &config, mock.clone(),
|
||||
);
|
||||
let mut op = external_user_message_op("Attempt the supplied cleanup.", AppMode::Agent, &config);
|
||||
let Op::SendMessage(turn) = &mut op else { unreachable!("model turn fixture") };
|
||||
turn.auto_approve = true;
|
||||
turn.approval_mode = ApprovalMode::Bypass;
|
||||
let op = auto_review_message_op("Attempt the supplied cleanup.", &config);
|
||||
let task = tokio::spawn(engine.run());
|
||||
handle.send(op).await.expect("send Full Access trajectory");
|
||||
handle.send(op).await.expect("send Auto-Review trajectory");
|
||||
let (completion, _, _, receipts) = collect_guardian_journey_with_receipts(&handle, "windows-node-kill").await;
|
||||
let error = completion.expect_err("runtime safety floor must deny before shell execution");
|
||||
assert!(matches!(error, crate::tools::spec::ToolError::PermissionDenied { .. }));
|
||||
|
||||
@@ -333,13 +333,24 @@ async fn full_access_permission_allow_runs_background_destructive_shell_without_
|
||||
}
|
||||
drop(rx);
|
||||
|
||||
let granted = tokio::time::timeout(std::time::Duration::from_secs(60), async {
|
||||
loop {
|
||||
let text = crate::test_support::read_shell_sentinel(&victim);
|
||||
if !text.is_empty() && text != "guarded" {
|
||||
break text;
|
||||
}
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("the granted command never wrote its sentinel");
|
||||
|
||||
handle.send(Op::Shutdown).await.expect("shutdown engine");
|
||||
run_task.await.expect("engine task");
|
||||
assert!(saw_tool_result);
|
||||
assert!(saw_complete);
|
||||
assert_eq!(
|
||||
fs::read_to_string(&victim).expect("read guarded fixture"),
|
||||
"rm -rf /\n",
|
||||
granted, "rm -rf /",
|
||||
"the granted command must run to completion"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -555,6 +555,24 @@ pub(crate) fn trust_workspace(workspace: &Path) {
|
||||
crate::config::save_workspace_trust(workspace).expect("trust test workspace");
|
||||
}
|
||||
|
||||
pub(crate) fn read_shell_sentinel(path: &Path) -> String {
|
||||
let bytes = std::fs::read(path).unwrap_or_default();
|
||||
let text = if bytes.starts_with(&[0xFF, 0xFE]) {
|
||||
let units: Vec<u16> = bytes[2..]
|
||||
.chunks_exact(2)
|
||||
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
|
||||
.collect();
|
||||
String::from_utf16_lossy(&units)
|
||||
} else {
|
||||
String::from_utf8_lossy(&bytes).into_owned()
|
||||
};
|
||||
text.trim_start_matches('\u{feff}')
|
||||
.replace("\r\n", "\n")
|
||||
.replace('"', "")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn test_tui_options(workspace: impl AsRef<Path>) -> crate::tui::app::TuiOptions {
|
||||
let workspace = workspace.as_ref().to_path_buf();
|
||||
crate::tui::app::TuiOptions {
|
||||
|
||||
Reference in new issue
Block a user