226
}
227
228
/// The first `session/update` whose payload satisfies `matches`.
229
+ ///
230
+ /// Only used by tests that run with the permission gate off, so a
231
+ /// `session/request_permission` means the run is misconfigured — say that
232
+ /// rather than waiting out the timeout on a request nobody will answer.
233
fn wait_for_update(&mut self, what: &str, matches: impl Fn(&Value) -> bool) -> Value {
234
let message = self.wait_for(what, |message| {
235
+ assert!(
236
+ message["method"] != "session/request_permission",
237
+ "unexpected permission request while waiting for {what}: {message}"
238
+ );
239
message["method"] == "session/update" && matches(&message["params"]["update"])
240
});
241
message["params"]["update"].clone()
393
394
#[test]
395
fn a_slow_commands_progress_reaches_the_client_while_it_runs() {
388
- // `SIGIT_PERMISSIONS=allow` skips the gate, so the only thing between the
389
- // two updates below is the command itself.
390
- let endpoint = start_fake_endpoint(vec![
391
- sse_tool_call("call_1", "run_command", r#"{"command":"sleep 2"}"#),
392
- sse_text("done"),
393
- ]);
394
-
396
let scratch = std::env::temp_dir().join(format!("sigit_acp_progress_{}", std::process::id()));
397
let config_dir = scratch.join("config");
398
let cwd = scratch.join("cwd");
399
std::fs::create_dir_all(&config_dir).unwrap();
400
std::fs::create_dir_all(&cwd).unwrap();
401
402
+ // The command has to take a couple of seconds through `sh -c` / `cmd /C`.
403
+ // `sleep` is neither a cmd builtin nor on a Windows runner's PATH (Git's
404
+ // usr\bin, which carries the MSYS coreutils, is deliberately left off it),
405
+ // and the Windows job hung on it rather than failing fast. `ping` ships in
406
+ // System32, waits a second between echoes, and never reads stdin.
407
+ #[cfg(unix)]
408
+ let command = "sleep 2";
409
+ #[cfg(windows)]
410
+ let command = "ping -n 3 127.0.0.1";
411
+
412
+ // `SIGIT_PERMISSIONS=allow` skips the gate, so the only thing between the
413
+ // two updates below is the command itself.
414
+ let endpoint = start_fake_endpoint(vec![
415
+ sse_tool_call(
416
+ "call_1",
417
+ "run_command",
418
+ &json!({"command": command, "cwd": cwd}).to_string(),
419
+ ),
420
+ sse_text("done"),
421
+ ]);
422
+
423
let mut agent = spawn_agent_with_permissions(endpoint.port, &config_dir, Some("allow"));
424
425
let id = agent.request(
451
"a running tool call must be announced as in_progress: {started}"
452
);
453
432
- agent.wait_for_update("the tool call finishing", |update| {
454
+ let finished = agent.wait_for_update("the tool call finishing", |update| {
455
update["sessionUpdate"] == "tool_call_update"
456
&& update["toolCallId"] == "call_1"
457
&& update["status"] == "completed"
458
});
459
438
- // `sleep 2` runs between the two updates. If the turn blocked the
439
- // connection's actors, both would only reach the client once the command
440
- // had already finished, and the editor would never draw a spinner.
460
+ // The command runs between the two updates. If the turn blocked the
461
+ // connection's actors, both would only reach the client once it had
462
+ // already finished, and the editor would never draw a spinner.
463
assert!(
464
in_progress_at.elapsed() >= Duration::from_millis(1_000),
465
"in_progress arrived only {:?} before completed — the client had no \
444
- window to show progress in",
445
- in_progress_at.elapsed()
466
+ window to show progress in. `{command}` said: {}",
467
+ in_progress_at.elapsed(),
468
+ finished["rawOutput"]
469
);
470
471
let response = agent.wait_for_response(prompt_id);