|
| 1 | +// check-delegated-mode-readers.ts |
| 2 | +// |
| 3 | +// Refinement check for the delegated-child mode-reader invariant (issue #1623). |
| 4 | +// |
| 5 | +// check-provider-handoff-scheduler.ts verifies the write side: that |
| 6 | +// selectHandoffExecutionContext stores the task-local mode correctly. |
| 7 | +// This script verifies the read side: that the mode observable by |
| 8 | +// tool-validation readers is the task-local mode, not the shared provider mode. |
| 9 | +// |
| 10 | +// The VS Code-dependent readers (getEnvironmentDetails, |
| 11 | +// presentAssistantMessage) are covered by their vitest regression tests. |
| 12 | +// This script covers the pure-TS parts of the invariant chain and proves |
| 13 | +// that the two sources of mode are observably different, so any reader |
| 14 | +// that uses the wrong source silently produces wrong behavior. |
| 15 | +// |
| 16 | +// Invariant: for any delegated child task C with taskMode = M, |
| 17 | +// toolAllowedForMode(tool, M) ≠ toolAllowedForMode(tool, providerMode) |
| 18 | +// whenever M ≠ providerMode and the two modes differ on the tool's group. |
| 19 | + |
| 20 | +import assert from "node:assert/strict" |
| 21 | + |
| 22 | +import { DEFAULT_MODES } from "../packages/types/src/mode" |
| 23 | + |
| 24 | +import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../src/shared/tools" |
| 25 | +import { selectHandoffExecutionContext, type TaskExecutionContext } from "../src/core/task/providerHandoff" |
| 26 | + |
| 27 | +// --------------------------------------------------------------------------- |
| 28 | +// Minimal inline mode-allows-tool check. |
| 29 | +// Avoids importing src/shared/modes.ts, which pulls in VS Code. |
| 30 | +// Only covers built-in modes (no custom modes, no file-regex options). |
| 31 | +// That is enough to prove the behavioral divergence this check needs. |
| 32 | +// --------------------------------------------------------------------------- |
| 33 | + |
| 34 | +type ModeConfig = (typeof DEFAULT_MODES)[number] |
| 35 | +type GroupEntry = ModeConfig["groups"][number] |
| 36 | + |
| 37 | +function groupName(entry: GroupEntry): string { |
| 38 | + return Array.isArray(entry) ? entry[0] : (entry as string) |
| 39 | +} |
| 40 | + |
| 41 | +function toolAllowedForMode(tool: string, modeSlug: string): boolean { |
| 42 | + const resolvedTool = (TOOL_ALIASES as Record<string, string>)[tool] ?? tool |
| 43 | + if ((ALWAYS_AVAILABLE_TOOLS as readonly string[]).includes(resolvedTool)) return true |
| 44 | + const mode = DEFAULT_MODES.find((m) => m.slug === modeSlug) |
| 45 | + if (!mode) return false |
| 46 | + for (const entry of mode.groups) { |
| 47 | + const groupTools = (TOOL_GROUPS as Record<string, { tools: readonly string[] }>)[groupName(entry)]?.tools ?? [] |
| 48 | + if (groupTools.includes(resolvedTool)) return true |
| 49 | + } |
| 50 | + return false |
| 51 | +} |
| 52 | + |
| 53 | +// --------------------------------------------------------------------------- |
| 54 | +// Scenario: parent in "orchestrator" mode delegates child to "code". |
| 55 | +// Regression behavior: both readers used providerMode ("orchestrator"). |
| 56 | +// Correct behavior: readers use taskMode ("code"). |
| 57 | +// |
| 58 | +// orchestrator groups: [] → apply_diff blocked |
| 59 | +// code groups: [...edit] → apply_diff allowed |
| 60 | +// --------------------------------------------------------------------------- |
| 61 | + |
| 62 | +const parentCtx: TaskExecutionContext = { |
| 63 | + mode: "orchestrator", |
| 64 | + apiConfigName: undefined, |
| 65 | + apiConfiguration: { apiProvider: "anthropic", consecutiveMistakeLimit: 3 }, |
| 66 | +} |
| 67 | + |
| 68 | +// 1. Handoff stores the task-local mode, not the parent mode. |
| 69 | +const childCtx = selectHandoffExecutionContext(parentCtx, "code", parentCtx.mode, false, undefined) |
| 70 | +assert.equal(childCtx.mode, "code", "handoff must store the requested task-local mode") |
| 71 | +assert.notEqual(childCtx.mode, parentCtx.mode, "test scenario requires divergent provider and task modes") |
| 72 | + |
| 73 | +// 2. The two modes produce observably different tool-validation outcomes. |
| 74 | +assert.equal(toolAllowedForMode("apply_diff", "orchestrator"), false, "orchestrator has no edit group") |
| 75 | +assert.equal(toolAllowedForMode("apply_diff", "code"), true, "code has the edit group") |
| 76 | + |
| 77 | +// 3. Regression claim: a reader that consumes providerMode rejects apply_diff; |
| 78 | +// a reader that consumes taskMode correctly allows it. |
| 79 | +const viaProviderMode = toolAllowedForMode("apply_diff", parentCtx.mode) // "orchestrator" — wrong source |
| 80 | +const viaTaskMode = toolAllowedForMode("apply_diff", childCtx.mode) // "code" — correct source |
| 81 | +assert.equal(viaProviderMode, false, "stale provider mode rejects apply_diff (regression behavior)") |
| 82 | +assert.equal(viaTaskMode, true, "task-local mode allows apply_diff (correct behavior)") |
| 83 | + |
| 84 | +// 4. Additional mode pairs that show the same divergence. |
| 85 | +const DIVERGENT_PAIRS: Array<{ |
| 86 | + label: string |
| 87 | + providerMode: string |
| 88 | + taskMode: string |
| 89 | + probe: string |
| 90 | + blockedInProvider: boolean |
| 91 | + allowedInTask: boolean |
| 92 | +}> = [ |
| 93 | + // orchestrator → code: edit tools blocked at provider level, allowed at task level |
| 94 | + { |
| 95 | + label: "orchestrator→code apply_diff", |
| 96 | + providerMode: "orchestrator", |
| 97 | + taskMode: "code", |
| 98 | + probe: "apply_diff", |
| 99 | + blockedInProvider: true, |
| 100 | + allowedInTask: true, |
| 101 | + }, |
| 102 | + // orchestrator → code: command tools blocked at provider level, allowed at task level |
| 103 | + { |
| 104 | + label: "orchestrator→code execute_command", |
| 105 | + providerMode: "orchestrator", |
| 106 | + taskMode: "code", |
| 107 | + probe: "execute_command", |
| 108 | + blockedInProvider: true, |
| 109 | + allowedInTask: true, |
| 110 | + }, |
| 111 | + // code → ask: edit tools allowed at provider level, blocked at task level |
| 112 | + { |
| 113 | + label: "code→ask apply_diff", |
| 114 | + providerMode: "code", |
| 115 | + taskMode: "ask", |
| 116 | + probe: "apply_diff", |
| 117 | + blockedInProvider: false, |
| 118 | + allowedInTask: false, |
| 119 | + }, |
| 120 | + // ask → code: edit tools blocked at provider level, allowed at task level |
| 121 | + { |
| 122 | + label: "ask→code write_to_file", |
| 123 | + providerMode: "ask", |
| 124 | + taskMode: "code", |
| 125 | + probe: "write_to_file", |
| 126 | + blockedInProvider: true, |
| 127 | + allowedInTask: true, |
| 128 | + }, |
| 129 | +] |
| 130 | + |
| 131 | +for (const pair of DIVERGENT_PAIRS) { |
| 132 | + const ctx = selectHandoffExecutionContext( |
| 133 | + { ...parentCtx, mode: pair.providerMode }, |
| 134 | + pair.taskMode, |
| 135 | + pair.providerMode, |
| 136 | + false, |
| 137 | + undefined, |
| 138 | + ) |
| 139 | + assert.equal(ctx.mode, pair.taskMode, `${pair.label}: handoff must store task-local mode`) |
| 140 | + assert.equal( |
| 141 | + toolAllowedForMode(pair.probe, pair.providerMode), |
| 142 | + !pair.blockedInProvider, |
| 143 | + `${pair.label}: wrong provider-mode result`, |
| 144 | + ) |
| 145 | + assert.equal( |
| 146 | + toolAllowedForMode(pair.probe, pair.taskMode), |
| 147 | + pair.allowedInTask, |
| 148 | + `${pair.label}: wrong task-mode result`, |
| 149 | + ) |
| 150 | + // The two sources disagree, so using the wrong one is always observable. |
| 151 | + assert.notEqual( |
| 152 | + toolAllowedForMode(pair.probe, pair.providerMode), |
| 153 | + toolAllowedForMode(pair.probe, pair.taskMode), |
| 154 | + `${pair.label}: provider and task mode must differ on this probe tool`, |
| 155 | + ) |
| 156 | +} |
| 157 | + |
| 158 | +// 5. For every built-in mode as a delegation target: selectHandoffExecutionContext |
| 159 | +// always stores the requested mode, regardless of parent mode. |
| 160 | +for (const mode of DEFAULT_MODES) { |
| 161 | + const ctx = selectHandoffExecutionContext(parentCtx, mode.slug, parentCtx.mode, false, undefined) |
| 162 | + assert.equal(ctx.mode, mode.slug, `handoff must store ${mode.slug}, not parent mode ${parentCtx.mode}`) |
| 163 | +} |
| 164 | + |
| 165 | +console.log( |
| 166 | + `Delegated mode reader check passed: ` + |
| 167 | + `regression scenario verified, ` + |
| 168 | + `${DIVERGENT_PAIRS.length} divergent-mode pairs checked, ` + |
| 169 | + `${DEFAULT_MODES.length}/${DEFAULT_MODES.length} built-in modes verified`, |
| 170 | +) |
0 commit comments