summaryrefslogtreecommitdiff
path: root/internal/executor/nativerunner_test.go
diff options
context:
space:
mode:
authorClaude Sonnet 5 <noreply@anthropic.com>2026-07-03 23:22:42 +0000
committerClaude Sonnet 5 <noreply@anthropic.com>2026-07-03 23:22:42 +0000
commit997cd8b56bc086a02b9c7c006dd62b07b9fcd2f3 (patch)
tree94351bb0aebeb92cb4c0f63f2e0a2636e06c4f1e /internal/executor/nativerunner_test.go
parent787b7fb1aed92c2b701724a7741576053b93cccb (diff)
feat(executor): add DAG auto-cascade-fail + role-typed subtask spawning (Phase 6)
Two prerequisites for safe parallel evaluator fan-out (later phase): 1. Auto-cascade-fail: previously, a failed task's dependents just sat PENDING/QUEUED forever (or until something eventually tried to dispatch them and discovered the dependency was dead). Pool.cascadeFail now fires right after a task lands in a terminal failure state (FAILED/TIMED_OUT/ CANCELLED/BUDGET_EXCEEDED, from handleRunResult, the budget-gate reject path, and the checkDepsReady dependency-failure path), recursively cancelling every not-yet-run dependent (PENDING/QUEUED only -- RUNNING and terminal states are left alone) with a message referencing the upstream failure. A visited-set guards recursion, which turned out to be load-bearing rather than defense-in-depth: task creation does not prevent dependency cycles anywhere in this codebase. Correction to an earlier assumption: internal/executor's waitForDependencies is dead code, never called. The live mechanism is checkDepsReady, invoked synchronously in execute() with a self-requeue via time.AfterFunc. Added a freshness re-check (GetTask, bail if no longer QUEUED) at the top of that block so a task cascade-cancelled while sitting in the requeue loop stops silently instead of hitting an invalid CANCELLED->CANCELLED transition or, worse, still getting dispatched. 2. storeChannel.SpawnSubtask hardcoded Agent.Type: "claude" on every spawned child regardless of what role it should play -- a hard blocker for a Planner/Builder task spawning role-typed evaluator subtasks. SubtaskSpec (internal/agentchannel) gains a Role field; when set, the child task gets Agent.Role instead of a hardcoded Type, so Phase 5's role-resolution picks provider/model from that role's escalation ladder. spec.Role == "" (every existing caller) preserves today's exact behavior byte-for-byte -- proven by an explicit regression test, not just new-feature coverage. Threaded the new `role` parameter through both spawn_subtask transports: the native tool-use loop (internal/agentloop/tools.go) and the MCP tool exposed to ContainerRunner-driven claude/gemini agents (internal/executor/agentmcp.go). go build/vet/test -race -count=1 all pass, full suite (20 packages). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1moSNCJRcP6kykA4tyUSs
Diffstat (limited to 'internal/executor/nativerunner_test.go')
-rw-r--r--internal/executor/nativerunner_test.go38
1 files changed, 38 insertions, 0 deletions
diff --git a/internal/executor/nativerunner_test.go b/internal/executor/nativerunner_test.go
index ab0317c..b081b8e 100644
--- a/internal/executor/nativerunner_test.go
+++ b/internal/executor/nativerunner_test.go
@@ -199,6 +199,44 @@ func TestNativeRunner_Run_ToolLoop_ReportSummaryAndSpawn(t *testing.T) {
}
}
+// TestNativeRunner_Run_ToolLoop_SpawnSubtask_RolePassthrough is an
+// end-to-end-ish test (fake LLM server, real agentloop.Loop/tools.go
+// dispatch, real storeChannel) proving a spawn_subtask tool call carrying a
+// "role" argument reaches SubtaskSpec.Role correctly through
+// internal/agentloop/tools.go's dispatchTool, and from there through
+// storeChannel.SpawnSubtask into the created child task's Agent.Role.
+func TestNativeRunner_Run_ToolLoop_SpawnSubtask_RolePassthrough(t *testing.T) {
+ srv := fakeChatServer(t, []fakeTurn{
+ {toolCalls: []llm.ToolCall{toolCall("c1", "spawn_subtask", `{"name":"eval","instructions":"evaluate","role":"evaluator_quality"}`)}},
+ {content: "finished"},
+ })
+ defer srv.Close()
+
+ r := newLocalRunner(t, srv)
+ tt := localTask()
+ store := &fakeChannelStore{}
+ ch := newStoreChannel(store, tt.ID)
+ exec := &storage.Execution{ID: uuid.New().String(), TaskID: tt.ID}
+
+ if err := r.Run(context.Background(), tt, exec, ch); err != nil {
+ t.Fatalf("Run: %v", err)
+ }
+
+ if len(store.createdTasks) != 1 {
+ t.Fatalf("expected 1 spawned subtask, got %d", len(store.createdTasks))
+ }
+ child := store.createdTasks[0]
+ if child.Agent.Role != "evaluator_quality" {
+ t.Errorf("Agent.Role: want evaluator_quality, got %q", child.Agent.Role)
+ }
+ if child.Agent.Type != "" {
+ t.Errorf("Agent.Type: want empty for role-typed subtask, got %q", child.Agent.Type)
+ }
+ if child.Agent.Instructions != "evaluate" {
+ t.Errorf("Agent.Instructions: want evaluate, got %q", child.Agent.Instructions)
+ }
+}
+
func TestNativeRunner_Run_RecordProgress(t *testing.T) {
srv := fakeChatServer(t, []fakeTurn{
{toolCalls: []llm.ToolCall{toolCall("c1", "record_progress", `{"message":"halfway there"}`)}},