| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "strings" |
| 6 | "testing" |
| 7 | |
| 8 | "reasonix/internal/agent/testutil" |
| 9 | "reasonix/internal/event" |
| 10 | "reasonix/internal/provider" |
| 11 | "reasonix/internal/tool" |
| 12 | ) |
| 13 | |
| 14 | // A read-only subagent refusing its own bash call must reach the frontend as a |
| 15 | // blocked result, not as a completed one: the command never ran. |
| 16 | func TestReadOnlySubagentBashRefusalReachesSinkAsBlocked(t *testing.T) { |
| 17 | parent := tool.NewRegistry() |
| 18 | parent.Add(subagentRegistryTool{ |
| 19 | name: "bash", |
| 20 | schema: `{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}`, |
| 21 | result: "ran", |
| 22 | }) |
| 23 | sub := ReadOnlySubagentToolRegistry(parent, nil) |
| 24 | |
| 25 | mp := testutil.NewMock("m", |
| 26 | testutil.Turn{ToolCalls: []provider.ToolCall{ |
| 27 | {ID: "call-1", Name: "bash", Arguments: `{"command":"node --version 2>&1; bash --version"}`}, |
| 28 | }}, |
| 29 | testutil.Turn{Text: "done"}, |
| 30 | ) |
| 31 | sink := &recordSink{} |
| 32 | a := New(mp, sub, NewSession(""), Options{}, sink) |
| 33 | if err := a.Run(context.Background(), "probe the toolchain"); err != nil { |
| 34 | t.Fatalf("Run: %v", err) |
| 35 | } |
| 36 | |
| 37 | results := sink.kinds(event.ToolResult) |
| 38 | if len(results) != 1 { |
| 39 | t.Fatalf("ToolResult events = %d, want 1", len(results)) |
| 40 | } |
| 41 | got := results[0].Tool |
| 42 | if got.Err == "" { |
| 43 | t.Fatalf("refused call must carry an error so a frontend can show it as blocked: %+v", got) |
| 44 | } |
| 45 | if !strings.Contains(got.Output, "blocked:") { |
| 46 | t.Fatalf("model-facing output should state the refusal, got %q", got.Output) |
| 47 | } |
| 48 | if got.Output == "ran" { |
| 49 | t.Fatal("refused command must not execute") |
| 50 | } |
| 51 | if got.Execution == nil || got.Execution.State != tool.ShellStateNotRun { |
| 52 | t.Fatalf("refused bash card needs not-run shell metadata, got %+v", got.Execution) |
| 53 | } |
| 54 | } |
| 55 |