Skip to content

Commit 0863bf7

Browse files
author
SqlRush
committed
Cover shell background progress states
1 parent 5a31109 commit 0863bf7

5 files changed

Lines changed: 152 additions & 3 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ M5 补充:Bash 和 Unix PowerShell 取消进程时现在会先对受管 proces
175175

176176
M5 补充:Bash 前台输出和 `BashOutput` 现在都走统一 tool-result budget 截断/落盘路径;`BashOutput` 增加 100k 最大结果限制,大后台输出会保存完整内容并返回 `full_output_path` 元数据。
177177

178-
M5 补充:Bash/PowerShell 后台任务现在会发 `*_background_started``*_background_finished` tool progress 事件,包含后台 ID、shell/status、exit/timed_out/cancelled、duration、时间戳和输出字节数,不携带 command 文本。
178+
M5 补充:Bash/PowerShell 后台任务现在会发 `*_background_started``*_background_finished` tool progress 事件,包含后台 ID、shell/status、exit/timed_out/cancelled、duration、时间戳和输出字节数,不携带 command 文本;completed、timed_out、cancelled 终态均已覆盖测试
179179

180180
M5 补充:Bash/PowerShell 现在接受官方 `dangerouslyDisableSandbox` semantic boolean 输入,并在 structured content 中记录该请求;真实 sandbox adapter/override 行为仍按 sandbox parity 项继续推进。
181181

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ test/parity/ # golden tests against TS/official behavior
206206
- 本轮补充:Bash/PowerShell 前台执行现在区分调用方 cancellation、timeout 和普通非零退出;context 取消会返回 `cancelled=true``timed_out=false``exit_code=-1`,并显示 cancelled 状态文本。
207207
- 本轮补充:Bash 和 Unix PowerShell 工具取消现在会先向受管 process group 发 SIGTERM,再用短 `WaitDelay` 兜底 SIGKILL;Bash 前台/后台取消测试通过 `trap TERM` 证明 cleanup signal 可被脚本收到。
208208
- 本轮补充:`BashOutput` 现在设置 100k 最大结果大小,和前台 `Bash` 一起覆盖大输出 tool-result preview 截断、完整输出落盘及 `full_output_path` 元数据。
209-
- 本轮补充:Bash/PowerShell 后台命令现在会通过 tool progress 通道发 started/finished 事件,记录后台 ID、shell/status、exit/timed_out/cancelled、duration、时间戳和 stdout/stderr byte count,且测试确保 progress 不携带 command 文本。
209+
- 本轮补充:Bash/PowerShell 后台命令现在会通过 tool progress 通道发 started/finished 事件,记录后台 ID、shell/status、exit/timed_out/cancelled、duration、时间戳和 stdout/stderr byte count,且 completed、timed_out、cancelled 终态测试确保 progress 不携带 command 文本。
210210
- 本轮补充:WebSearch domain filters 现在在 schema 层声明 array `items:string`,通用 tool schema validator 同步支持 `items` 校验;`allowed_domains`/`blocked_domains` 会拒绝空字符串、URL/port、非法 wildcard 和非域名 label。
211211
- 本轮补充:通用 tool schema validator 现在支持 `enum`,可直接执行 Grep output mode、NotebookEdit edit mode/cell type、Todo status/priority、Task target/action、LSP severity 等工具 schema 的枚举契约。
212212
- 本轮补充:通用 tool schema validator 现在支持数字 `minimum`/`maximum`,可直接执行 LSPDiagnostics `limit` 等工具 schema 的数值范围契约。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ Covered behavior:
206206
- `Bash`/`PowerShell` foreground execution now distinguishes caller cancellation from timeout and ordinary nonzero exit in structured results: cancelled commands return `cancelled=true`, `timed_out=false`, `exit_code=-1`, and visible cancelled status text.
207207
- `Bash` and Unix `PowerShell` process cancellation now sends SIGTERM to the managed process group first and uses a short `WaitDelay` SIGKILL fallback, allowing cooperative shell cleanup while still bounding ignored-signal commands.
208208
- `BashOutput` now has the same 100k result budget surface as foreground `Bash`, so oversized background output is truncated in the visible tool result and persisted with `full_output_path` metadata.
209-
- `Bash` and `PowerShell` background commands now emit tool progress events when the background task starts and finishes, carrying only task IDs, shell/status fields, exit/timed-out/cancelled state, duration, timestamps, and output byte counts without command text.
209+
- `Bash` and `PowerShell` background commands now emit tool progress events when the background task starts and finishes, carrying only task IDs, shell/status fields, exit/timed-out/cancelled state, duration, timestamps, and output byte counts without command text; completed, timed-out, and cancelled finish paths are covered.
210210
- `Bash`/`PowerShell` now accept the official `dangerouslyDisableSandbox` semantic boolean input and record it in structured results; full sandbox adapter and override execution semantics remain a separate incomplete parity area.
211211
- `Bash`/`PowerShell` now surface `dangerouslyDisableSandbox` to the permission engine: normal/default/auto/plan/acceptEdits modes ask for confirmation, `dontAsk` denies it, and only available `bypassPermissions` mode allows it, preventing read-only classification or allow rules from silently approving sandbox override requests.
212212
- Settings now propagate `sandbox.allowUnsandboxedCommands` into the permission context; when it is `false`, sandbox override requests are denied even in `bypassPermissions`, and settings validation flags non-boolean values for the known sandbox boolean fields.

internal/tools/bash/tools_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,80 @@ func TestBashBackgroundProgressEvents(t *testing.T) {
625625
}
626626
}
627627

628+
func TestBashBackgroundTimeoutProgressEvent(t *testing.T) {
629+
executor := bashExecutor(t)
630+
ctx := WithBackgroundState(tool.Context{
631+
Context: context.Background(),
632+
Metadata: map[string]any{},
633+
}, NewBackgroundState())
634+
progressCh := make(chan contracts.ToolProgress, 8)
635+
result, err := executor.Execute(ctx, contracts.ToolUse{
636+
ID: "toolu_bash_background_timeout_progress",
637+
Name: "Bash",
638+
Input: json.RawMessage(`{"command":"sleep 1","run_in_background":true,"timeout":50}`),
639+
}, tool.ProgressFunc(func(progress contracts.ToolProgress) error {
640+
progressCh <- progress
641+
return nil
642+
}))
643+
if err != nil {
644+
t.Fatal(err)
645+
}
646+
bashID := result.StructuredContent["bash_id"].(string)
647+
finished := waitForBashProgress(t, progressCh, "bash_background_finished")
648+
if finished.ToolUseID != "toolu_bash_background_timeout_progress" || finished.Data["bash_id"] != bashID || finished.Data["status"] != "timed_out" {
649+
t.Fatalf("finished progress = %#v", finished)
650+
}
651+
if finished.Data["exit_code"] != -1 || finished.Data["timed_out"] != true || finished.Data["cancelled"] != false {
652+
t.Fatalf("timeout progress status = %#v", finished.Data)
653+
}
654+
if _, ok := finished.Data["command"]; ok {
655+
t.Fatalf("timeout progress should not expose command: %#v", finished.Data)
656+
}
657+
}
658+
659+
func TestBashBackgroundCancelProgressEvent(t *testing.T) {
660+
executor := bashExecutor(t)
661+
ctx := WithBackgroundState(tool.Context{
662+
Context: context.Background(),
663+
Metadata: map[string]any{},
664+
}, NewBackgroundState())
665+
progressCh := make(chan contracts.ToolProgress, 8)
666+
result, err := executor.Execute(ctx, contracts.ToolUse{
667+
ID: "toolu_bash_background_cancel_progress",
668+
Name: "Bash",
669+
Input: json.RawMessage(`{"command":"trap 'printf term; exit 42' TERM; while :; do sleep 1; done","run_in_background":true,"timeout":5000}`),
670+
}, tool.ProgressFunc(func(progress contracts.ToolProgress) error {
671+
progressCh <- progress
672+
return nil
673+
}))
674+
if err != nil {
675+
t.Fatal(err)
676+
}
677+
bashID := result.StructuredContent["bash_id"].(string)
678+
started := waitForBashProgress(t, progressCh, "bash_background_started")
679+
if started.Data["bash_id"] != bashID {
680+
t.Fatalf("started progress = %#v", started)
681+
}
682+
time.Sleep(50 * time.Millisecond)
683+
if _, err := executor.Execute(ctx, contracts.ToolUse{
684+
ID: "toolu_bash_background_cancel_progress_kill",
685+
Name: "KillBash",
686+
Input: json.RawMessage(`{"bash_id":` + strconvQuote(bashID) + `}`),
687+
}, nil); err != nil {
688+
t.Fatal(err)
689+
}
690+
finished := waitForBashProgress(t, progressCh, "bash_background_finished")
691+
if finished.ToolUseID != "toolu_bash_background_cancel_progress" || finished.Data["bash_id"] != bashID || finished.Data["status"] != "cancelled" {
692+
t.Fatalf("finished progress = %#v", finished)
693+
}
694+
if finished.Data["exit_code"] != -1 || finished.Data["timed_out"] != false || finished.Data["cancelled"] != true {
695+
t.Fatalf("cancel progress status = %#v", finished.Data)
696+
}
697+
if _, ok := finished.Data["command"]; ok {
698+
t.Fatalf("cancel progress should not expose command: %#v", finished.Data)
699+
}
700+
}
701+
628702
func TestBashBackgroundTimeout(t *testing.T) {
629703
executor := bashExecutor(t)
630704
ctx := WithBackgroundState(tool.Context{

internal/tools/powershell/tools_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,81 @@ func TestPowerShellBackgroundProgressEvents(t *testing.T) {
529529
}
530530
}
531531

532+
func TestPowerShellBackgroundTimeoutProgressEvent(t *testing.T) {
533+
requirePowerShell(t)
534+
executor := powerShellExecutor(t)
535+
ctx := WithBackgroundState(tool.Context{
536+
Context: context.Background(),
537+
Metadata: map[string]any{},
538+
}, NewBackgroundState())
539+
progressCh := make(chan contracts.ToolProgress, 8)
540+
result, err := executor.Execute(ctx, contracts.ToolUse{
541+
ID: "toolu_powershell_background_timeout_progress",
542+
Name: "PowerShell",
543+
Input: json.RawMessage(`{"command":"Start-Sleep -Milliseconds 1000","run_in_background":true,"timeout":50}`),
544+
}, tool.ProgressFunc(func(progress contracts.ToolProgress) error {
545+
progressCh <- progress
546+
return nil
547+
}))
548+
if err != nil {
549+
t.Fatal(err)
550+
}
551+
powerShellID := result.StructuredContent["powershell_id"].(string)
552+
finished := waitForPowerShellProgress(t, progressCh, "powershell_background_finished")
553+
if finished.ToolUseID != "toolu_powershell_background_timeout_progress" || finished.Data["powershell_id"] != powerShellID || finished.Data["status"] != "timed_out" {
554+
t.Fatalf("finished progress = %#v", finished)
555+
}
556+
if finished.Data["exit_code"] != -1 || finished.Data["timed_out"] != true || finished.Data["cancelled"] != false {
557+
t.Fatalf("timeout progress status = %#v", finished.Data)
558+
}
559+
if _, ok := finished.Data["command"]; ok {
560+
t.Fatalf("timeout progress should not expose command: %#v", finished.Data)
561+
}
562+
}
563+
564+
func TestPowerShellBackgroundCancelProgressEvent(t *testing.T) {
565+
requirePowerShell(t)
566+
executor := powerShellExecutor(t)
567+
ctx := WithBackgroundState(tool.Context{
568+
Context: context.Background(),
569+
Metadata: map[string]any{},
570+
}, NewBackgroundState())
571+
progressCh := make(chan contracts.ToolProgress, 8)
572+
result, err := executor.Execute(ctx, contracts.ToolUse{
573+
ID: "toolu_powershell_background_cancel_progress",
574+
Name: "PowerShell",
575+
Input: json.RawMessage(`{"command":"Start-Sleep -Seconds 5","run_in_background":true,"timeout":5000}`),
576+
}, tool.ProgressFunc(func(progress contracts.ToolProgress) error {
577+
progressCh <- progress
578+
return nil
579+
}))
580+
if err != nil {
581+
t.Fatal(err)
582+
}
583+
powerShellID := result.StructuredContent["powershell_id"].(string)
584+
started := waitForPowerShellProgress(t, progressCh, "powershell_background_started")
585+
if started.Data["powershell_id"] != powerShellID {
586+
t.Fatalf("started progress = %#v", started)
587+
}
588+
if _, err := executor.Execute(ctx, contracts.ToolUse{
589+
ID: "toolu_powershell_background_cancel_progress_kill",
590+
Name: "KillPowerShell",
591+
Input: json.RawMessage(`{"powershell_id":` + strconvQuote(powerShellID) + `}`),
592+
}, nil); err != nil {
593+
t.Fatal(err)
594+
}
595+
finished := waitForPowerShellProgress(t, progressCh, "powershell_background_finished")
596+
if finished.ToolUseID != "toolu_powershell_background_cancel_progress" || finished.Data["powershell_id"] != powerShellID || finished.Data["status"] != "cancelled" {
597+
t.Fatalf("finished progress = %#v", finished)
598+
}
599+
if finished.Data["exit_code"] != -1 || finished.Data["timed_out"] != false || finished.Data["cancelled"] != true {
600+
t.Fatalf("cancel progress status = %#v", finished.Data)
601+
}
602+
if _, ok := finished.Data["command"]; ok {
603+
t.Fatalf("cancel progress should not expose command: %#v", finished.Data)
604+
}
605+
}
606+
532607
func TestPowerShellBackgroundTimeout(t *testing.T) {
533608
requirePowerShell(t)
534609
executor := powerShellExecutor(t)

0 commit comments

Comments
 (0)