@selesai/code 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/package-manager.d.ts +10 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +93 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manager.test.d.ts +2 -0
- package/dist/core/package-manager.test.d.ts.map +1 -0
- package/dist/core/package-manager.test.js +20 -0
- package/dist/core/package-manager.test.js.map +1 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +10 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
- package/dist/extensions/caveman/index.js +116 -0
- package/dist/extensions/caveman/package.json +8 -0
- package/dist/extensions/caveman/test/extension.test.js +185 -0
- package/dist/extensions/caveman/test/helpers.test.js +67 -0
- package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
- package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
- package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
- package/dist/extensions/pi-intercom/config.ts +2 -2
- package/dist/extensions/pi-intercom/index.ts +2 -2
- package/dist/extensions/pi-intercom/package.json +2 -2
- package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
- package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
- package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +9 -16
- package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +2 -9
- package/dist/extensions/pi-rewind-hook/index.ts +1 -1
- package/dist/extensions/pi-rewind-hook/package.json +1 -1
- package/dist/extensions/pi-subagents/README.md +224 -52
- package/dist/extensions/pi-subagents/install.mjs +93 -0
- package/dist/extensions/pi-subagents/package.json +4 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
- package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
- package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
- package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
- package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
- package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
- package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +100 -15
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
- package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
- package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
- package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
- package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
- package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
- package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
- package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
- package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
- package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
- package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +19 -5
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/skills/caveman/SKILL.md +49 -0
- package/dist/skills/handoff-text/SKILL.md +13 -0
- package/docs/docs.json +4 -0
- package/docs/extensions.md +2 -0
- package/docs/settings.md +15 -0
- package/docs/shared-host-extensions.md +109 -0
- package/package.json +1 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
|
|
4
|
+
interface DetectErrorResult {
|
|
5
|
+
hasError: boolean;
|
|
6
|
+
errorType?: string;
|
|
7
|
+
details?: string;
|
|
8
|
+
exitCode?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type DetectSubagentError = (messages: unknown[]) => DetectErrorResult;
|
|
12
|
+
|
|
13
|
+
let detectSubagentError: DetectSubagentError | undefined;
|
|
14
|
+
let available = true;
|
|
15
|
+
try {
|
|
16
|
+
({ detectSubagentError } = await import("../../src/shared/utils.ts"));
|
|
17
|
+
} catch {
|
|
18
|
+
// Skip in lean unit mode when runtime-only imports are unavailable.
|
|
19
|
+
available = false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Helper to create a tool result message (success or error).
|
|
24
|
+
*/
|
|
25
|
+
function toolResult(toolName: string, text: string, isError = false): Record<string, unknown> {
|
|
26
|
+
return {
|
|
27
|
+
role: "toolResult",
|
|
28
|
+
toolCallId: `call-${Math.random().toString(36).slice(2, 8)}`,
|
|
29
|
+
toolName,
|
|
30
|
+
content: [{ type: "text", text }],
|
|
31
|
+
isError,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assistantMsg(text: string): Record<string, unknown> {
|
|
36
|
+
return {
|
|
37
|
+
role: "assistant",
|
|
38
|
+
content: [{ type: "text", text }],
|
|
39
|
+
api: "test",
|
|
40
|
+
provider: "test",
|
|
41
|
+
model: "test",
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Assistant message with only a tool call, no text content */
|
|
46
|
+
function assistantToolCall(toolName: string): Record<string, unknown> {
|
|
47
|
+
return {
|
|
48
|
+
role: "assistant",
|
|
49
|
+
content: [{ type: "toolCall", name: toolName, input: {} }],
|
|
50
|
+
api: "test",
|
|
51
|
+
provider: "test",
|
|
52
|
+
model: "test",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("detectSubagentError", { skip: !available ? "utils not importable" : undefined }, () => {
|
|
57
|
+
// ---- Basic detection (must still work) ----
|
|
58
|
+
|
|
59
|
+
it("returns no error for empty messages", () => {
|
|
60
|
+
assert.equal(detectSubagentError([]).hasError, false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("returns no error when all tool results succeed", () => {
|
|
64
|
+
const messages = [
|
|
65
|
+
toolResult("read", "file contents here"),
|
|
66
|
+
toolResult("bash", "ls output"),
|
|
67
|
+
toolResult("read", "more contents"),
|
|
68
|
+
];
|
|
69
|
+
assert.equal(detectSubagentError(messages).hasError, false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("detects isError tool result as failure (no assistant response)", () => {
|
|
73
|
+
const messages = [
|
|
74
|
+
toolResult("read", "file contents"),
|
|
75
|
+
toolResult("read", "EISDIR: illegal operation on a directory, read", true),
|
|
76
|
+
];
|
|
77
|
+
const result = detectSubagentError(messages);
|
|
78
|
+
assert.equal(result.hasError, true);
|
|
79
|
+
assert.equal(result.errorType, "read");
|
|
80
|
+
assert.match(result.details!, /EISDIR/);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("detects bash fatal pattern (permission denied, no assistant response)", () => {
|
|
84
|
+
const messages = [
|
|
85
|
+
toolResult("bash", "ls: permission denied: /root/secret"),
|
|
86
|
+
];
|
|
87
|
+
const result = detectSubagentError(messages);
|
|
88
|
+
assert.equal(result.hasError, true);
|
|
89
|
+
assert.equal(result.errorType, "bash");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("detects bash exit code in output", () => {
|
|
93
|
+
const messages = [
|
|
94
|
+
toolResult("bash", "error: process exited with code 127"),
|
|
95
|
+
];
|
|
96
|
+
const result = detectSubagentError(messages);
|
|
97
|
+
assert.equal(result.hasError, true);
|
|
98
|
+
assert.equal(result.exitCode, 127);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ---- Recovery: errors before the agent's final response are forgiven ----
|
|
102
|
+
|
|
103
|
+
it("ignores error when agent recovered and continued", () => {
|
|
104
|
+
const messages = [
|
|
105
|
+
toolResult("read", "file contents"),
|
|
106
|
+
toolResult("bash", "ok"),
|
|
107
|
+
toolResult("read", "EISDIR: illegal operation on a directory", true),
|
|
108
|
+
toolResult("bash", "directory listing via bash"),
|
|
109
|
+
assistantMsg("Here is my complete review..."),
|
|
110
|
+
];
|
|
111
|
+
const result = detectSubagentError(messages);
|
|
112
|
+
assert.equal(result.hasError, false,
|
|
113
|
+
"error before agent's final text response should be ignored");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("ignores error as final tool result when agent produced text response after", () => {
|
|
117
|
+
// The exact scenario from our review run: agent did all work, last tool
|
|
118
|
+
// call was read on directory → EISDIR, but agent produced 13.5KB review.
|
|
119
|
+
const messages = [
|
|
120
|
+
toolResult("read", "file contents of index.ts"),
|
|
121
|
+
toolResult("read", "file contents of utils.ts"),
|
|
122
|
+
toolResult("bash", "npm test output: 46 pass"),
|
|
123
|
+
toolResult("read", "file contents of settings.ts"),
|
|
124
|
+
toolResult("read", "EISDIR: illegal operation on a directory, read", true),
|
|
125
|
+
assistantMsg("## Complete Review\n\nHere are all my findings..."),
|
|
126
|
+
];
|
|
127
|
+
const result = detectSubagentError(messages);
|
|
128
|
+
assert.equal(result.hasError, false,
|
|
129
|
+
"agent produced substantive output after error — not a failure");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("ignores bash fatal pattern when agent responded after", () => {
|
|
133
|
+
const messages = [
|
|
134
|
+
toolResult("bash", "ls: permission denied: /root/secret"),
|
|
135
|
+
assistantMsg("I couldn't access /root/secret, but I found the data elsewhere."),
|
|
136
|
+
];
|
|
137
|
+
const result = detectSubagentError(messages);
|
|
138
|
+
assert.equal(result.hasError, false,
|
|
139
|
+
"fatal pattern before agent's text response = recovered");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ---- Errors AFTER the last assistant text response are still caught ----
|
|
143
|
+
|
|
144
|
+
it("detects error after agent's last text response", () => {
|
|
145
|
+
const messages = [
|
|
146
|
+
assistantMsg("Here is my analysis..."),
|
|
147
|
+
toolResult("bash", "rm -rf /important", false),
|
|
148
|
+
toolResult("bash", "error: process exited with code 1", false),
|
|
149
|
+
];
|
|
150
|
+
const result = detectSubagentError(messages);
|
|
151
|
+
assert.equal(result.hasError, true);
|
|
152
|
+
assert.equal(result.exitCode, 1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("detects isError after agent's last text response", () => {
|
|
156
|
+
const messages = [
|
|
157
|
+
toolResult("read", "file ok"),
|
|
158
|
+
assistantMsg("Let me try one more thing..."),
|
|
159
|
+
toolResult("write", "Permission denied", true),
|
|
160
|
+
];
|
|
161
|
+
const result = detectSubagentError(messages);
|
|
162
|
+
assert.equal(result.hasError, true);
|
|
163
|
+
assert.equal(result.errorType, "write");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ---- Edge cases ----
|
|
167
|
+
|
|
168
|
+
it("flags error when no assistant messages at all", () => {
|
|
169
|
+
const messages = [
|
|
170
|
+
toolResult("read", "ok"),
|
|
171
|
+
toolResult("bash", "segmentation fault"),
|
|
172
|
+
];
|
|
173
|
+
const result = detectSubagentError(messages);
|
|
174
|
+
assert.equal(result.hasError, true,
|
|
175
|
+
"no assistant response = no recovery evidence");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("does not treat tool-call-only assistant message as recovery", () => {
|
|
179
|
+
// Assistant message that only contains a tool call, no text.
|
|
180
|
+
// The error at index 0 should still be detected because the tool-call-only
|
|
181
|
+
// assistant message doesn't count as recovery. The final tool result is
|
|
182
|
+
// successful to ensure this test actually distinguishes correct behavior.
|
|
183
|
+
const messages = [
|
|
184
|
+
toolResult("bash", "permission denied: /etc/shadow"),
|
|
185
|
+
assistantToolCall("bash"),
|
|
186
|
+
toolResult("bash", "command succeeded"),
|
|
187
|
+
];
|
|
188
|
+
const result = detectSubagentError(messages);
|
|
189
|
+
assert.equal(result.hasError, true,
|
|
190
|
+
"tool-call assistant message without text is not a recovery");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("does not treat empty/whitespace assistant message as recovery", () => {
|
|
194
|
+
const messages = [
|
|
195
|
+
toolResult("read", "EISDIR: illegal operation on a directory", true),
|
|
196
|
+
assistantMsg(" "),
|
|
197
|
+
];
|
|
198
|
+
const result = detectSubagentError(messages);
|
|
199
|
+
assert.equal(result.hasError, true,
|
|
200
|
+
"whitespace-only assistant message is not a recovery");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("returns no error when only assistant messages (no tool results)", () => {
|
|
204
|
+
const messages = [
|
|
205
|
+
assistantMsg("Hello, I'm ready to help."),
|
|
206
|
+
assistantMsg("Here's my analysis."),
|
|
207
|
+
];
|
|
208
|
+
assert.equal(detectSubagentError(messages).hasError, false);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("handles multiple errors with recovery between them", () => {
|
|
212
|
+
// Error → recovery → error → recovery
|
|
213
|
+
const messages = [
|
|
214
|
+
toolResult("read", "ENOENT: no such file", true),
|
|
215
|
+
assistantMsg("File not found, trying alternative..."),
|
|
216
|
+
toolResult("read", "file contents"),
|
|
217
|
+
toolResult("read", "EISDIR: illegal operation on a directory", true),
|
|
218
|
+
assistantMsg("Got what I needed. Here's the full review."),
|
|
219
|
+
];
|
|
220
|
+
const result = detectSubagentError(messages);
|
|
221
|
+
assert.equal(result.hasError, false,
|
|
222
|
+
"all errors have recovery — agent completed successfully");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// ---- Real-world regression test ----
|
|
226
|
+
|
|
227
|
+
it("real-world: 19-read review run with trailing EISDIR", () => {
|
|
228
|
+
// Simulate the actual _impl-reviewer run that produced a false positive
|
|
229
|
+
const readResults = Array.from({ length: 18 }, (_, i) =>
|
|
230
|
+
toolResult("read", `contents of file ${i + 1}`),
|
|
231
|
+
);
|
|
232
|
+
const messages = [
|
|
233
|
+
...readResults,
|
|
234
|
+
toolResult("bash", "npm test\n46 pass\n2 fail\nTests 48"),
|
|
235
|
+
toolResult("read", "EISDIR: illegal operation on a directory, read", true),
|
|
236
|
+
assistantMsg("## Implementation Review\n\n" + "x".repeat(13000)),
|
|
237
|
+
];
|
|
238
|
+
const result = detectSubagentError(messages);
|
|
239
|
+
assert.equal(result.hasError, false,
|
|
240
|
+
"complete review with trailing EISDIR must not be flagged as failure");
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
+
import { createEventBus, createTempDir, makeMinimalCtx, removeTempDir, tryImport } from "../support/helpers.ts";
|
|
7
|
+
|
|
8
|
+
const originalHome = process.env.HOME;
|
|
9
|
+
const originalUserProfile = process.env.USERPROFILE;
|
|
10
|
+
const importHome = createTempDir("pi-doctor-executor-import-home-");
|
|
11
|
+
process.env.HOME = importHome;
|
|
12
|
+
process.env.USERPROFILE = importHome;
|
|
13
|
+
let executorMod: any;
|
|
14
|
+
try {
|
|
15
|
+
executorMod = await tryImport<any>("./src/runs/foreground/subagent-executor.ts");
|
|
16
|
+
} finally {
|
|
17
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
18
|
+
else process.env.HOME = originalHome;
|
|
19
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
20
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
21
|
+
removeTempDir(importHome);
|
|
22
|
+
}
|
|
23
|
+
const createSubagentExecutor = executorMod?.createSubagentExecutor;
|
|
24
|
+
|
|
25
|
+
function makeState(cwd: string) {
|
|
26
|
+
return {
|
|
27
|
+
baseCwd: cwd,
|
|
28
|
+
currentSessionId: null,
|
|
29
|
+
asyncJobs: new Map(),
|
|
30
|
+
foregroundControls: new Map(),
|
|
31
|
+
lastForegroundControlId: null,
|
|
32
|
+
cleanupTimers: new Map(),
|
|
33
|
+
lastUiContext: null,
|
|
34
|
+
poller: null,
|
|
35
|
+
completionSeen: new Map(),
|
|
36
|
+
watcher: null,
|
|
37
|
+
watcherRestartTimer: null,
|
|
38
|
+
resultFileCoalescer: { schedule: () => false, clear: () => {} },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("doctor action executor routing", { skip: !createSubagentExecutor ? "executor not importable" : undefined }, () => {
|
|
43
|
+
let tempDir = "";
|
|
44
|
+
let tempHome = "";
|
|
45
|
+
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
tempDir = createTempDir("pi-doctor-executor-project-");
|
|
48
|
+
tempHome = createTempDir("pi-doctor-executor-home-");
|
|
49
|
+
process.env.HOME = tempHome;
|
|
50
|
+
process.env.USERPROFILE = tempHome;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
if (originalHome === undefined) delete process.env.HOME;
|
|
55
|
+
else process.env.HOME = originalHome;
|
|
56
|
+
if (originalUserProfile === undefined) delete process.env.USERPROFILE;
|
|
57
|
+
else process.env.USERPROFILE = originalUserProfile;
|
|
58
|
+
removeTempDir(tempDir);
|
|
59
|
+
removeTempDir(tempHome);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns a doctor report for the tool action", async () => {
|
|
63
|
+
const sessionFile = path.join(tempDir, "sessions", "parent.jsonl");
|
|
64
|
+
fs.mkdirSync(path.dirname(sessionFile), { recursive: true });
|
|
65
|
+
fs.writeFileSync(sessionFile, "");
|
|
66
|
+
const executor = createSubagentExecutor({
|
|
67
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
68
|
+
state: makeState(tempDir),
|
|
69
|
+
config: { defaultSessionDir: path.join(tempDir, "configured-sessions") },
|
|
70
|
+
asyncByDefault: false,
|
|
71
|
+
tempArtifactsDir: tempDir,
|
|
72
|
+
getSubagentSessionRoot: () => tempDir,
|
|
73
|
+
expandTilde: (value: string) => value,
|
|
74
|
+
discoverAgents: () => ({ agents: [] }),
|
|
75
|
+
});
|
|
76
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
77
|
+
ctx.sessionManager.getSessionFile = () => sessionFile;
|
|
78
|
+
ctx.sessionManager.getSessionId = () => "session-doctor";
|
|
79
|
+
|
|
80
|
+
const result = await executor.execute(
|
|
81
|
+
"doctor-id",
|
|
82
|
+
{ action: "doctor" },
|
|
83
|
+
new AbortController().signal,
|
|
84
|
+
undefined,
|
|
85
|
+
ctx,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
assert.equal(result.isError, undefined);
|
|
89
|
+
const text = result.content[0]?.text ?? "";
|
|
90
|
+
assert.match(text, /^Subagents doctor report/);
|
|
91
|
+
assert.match(text, /- configured session dir: .*configured-sessions/);
|
|
92
|
+
assert.match(text, /- supervisor channel: available \(native:pi-subagents-supervisor-channel\)/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("reports session manager failures without failing the doctor action", async () => {
|
|
96
|
+
const executor = createSubagentExecutor({
|
|
97
|
+
pi: { events: createEventBus(), getSessionName: () => undefined },
|
|
98
|
+
state: makeState(tempDir),
|
|
99
|
+
config: {},
|
|
100
|
+
asyncByDefault: false,
|
|
101
|
+
tempArtifactsDir: tempDir,
|
|
102
|
+
getSubagentSessionRoot: () => tempDir,
|
|
103
|
+
expandTilde: (value: string) => value,
|
|
104
|
+
discoverAgents: () => ({ agents: [] }),
|
|
105
|
+
});
|
|
106
|
+
const ctx = makeMinimalCtx(tempDir);
|
|
107
|
+
ctx.sessionManager.getSessionFile = () => {
|
|
108
|
+
throw new Error("session unavailable");
|
|
109
|
+
};
|
|
110
|
+
ctx.sessionManager.getSessionId = () => {
|
|
111
|
+
throw new Error("session unavailable");
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = await executor.execute(
|
|
115
|
+
"doctor-id",
|
|
116
|
+
{ action: "doctor" },
|
|
117
|
+
new AbortController().signal,
|
|
118
|
+
undefined,
|
|
119
|
+
ctx,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
assert.equal(result.isError, undefined);
|
|
123
|
+
const text = result.content[0]?.text ?? "";
|
|
124
|
+
assert.match(text, /^Subagents doctor report/);
|
|
125
|
+
assert.match(text, /- session manager: failed — Error: session unavailable/);
|
|
126
|
+
assert.match(text, /- current session file: not available/);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for error handling across execution modes.
|
|
3
|
+
*
|
|
4
|
+
* Tests: agent crashes, stderr capture, detectSubagentError override,
|
|
5
|
+
* signal/abort handling, and error propagation in chains.
|
|
6
|
+
*
|
|
7
|
+
* Requires pi packages for execution tests. Skips gracefully if unavailable.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, before, after, beforeEach, afterEach } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import type { MockPi } from "../support/helpers.ts";
|
|
14
|
+
import {
|
|
15
|
+
createMockPi,
|
|
16
|
+
createTempDir,
|
|
17
|
+
removeTempDir,
|
|
18
|
+
makeAgentConfigs,
|
|
19
|
+
makeAgent,
|
|
20
|
+
makeMinimalCtx,
|
|
21
|
+
events,
|
|
22
|
+
tryImport,
|
|
23
|
+
} from "../support/helpers.ts";
|
|
24
|
+
|
|
25
|
+
// Top-level await
|
|
26
|
+
const utils = await tryImport<any>("./src/shared/utils.ts");
|
|
27
|
+
const execution = await tryImport<any>("./src/runs/foreground/execution.ts");
|
|
28
|
+
const chainMod = await tryImport<any>("./src/runs/foreground/chain-execution.ts");
|
|
29
|
+
|
|
30
|
+
const piAvailable = !!(execution && utils);
|
|
31
|
+
const chainAvailable = !!chainMod;
|
|
32
|
+
|
|
33
|
+
const runSync = execution?.runSync;
|
|
34
|
+
const detectSubagentError = utils?.detectSubagentError;
|
|
35
|
+
const executeChain = chainMod?.executeChain;
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// detectSubagentError
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe("detectSubagentError", { skip: !detectSubagentError ? "utils not importable" : undefined }, () => {
|
|
42
|
+
it("returns no error for successful messages", () => {
|
|
43
|
+
const messages = [
|
|
44
|
+
{ role: "assistant", content: [{ type: "text", text: "Let me check..." }] },
|
|
45
|
+
{ role: "toolResult", toolName: "bash", isError: false, content: [{ type: "text", text: "OK" }] },
|
|
46
|
+
{ role: "assistant", content: [{ type: "text", text: "All good!" }] },
|
|
47
|
+
];
|
|
48
|
+
const result = detectSubagentError(messages);
|
|
49
|
+
assert.equal(result.hasError, false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("detects fatal bash error in last tool result", () => {
|
|
53
|
+
const messages = [
|
|
54
|
+
{ role: "assistant", content: [{ type: "text", text: "Running..." }] },
|
|
55
|
+
{
|
|
56
|
+
role: "toolResult",
|
|
57
|
+
toolName: "bash",
|
|
58
|
+
isError: false,
|
|
59
|
+
content: [{ type: "text", text: "command not found" }],
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
const result = detectSubagentError(messages);
|
|
63
|
+
assert.equal(result.hasError, true);
|
|
64
|
+
assert.equal(result.errorType, "bash");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("detects non-zero exit code in bash output", () => {
|
|
68
|
+
const messages = [
|
|
69
|
+
{ role: "assistant", content: [{ type: "text", text: "Running..." }] },
|
|
70
|
+
{
|
|
71
|
+
role: "toolResult",
|
|
72
|
+
toolName: "bash",
|
|
73
|
+
isError: false,
|
|
74
|
+
content: [{ type: "text", text: "Error: process exited with code 127" }],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
const result = detectSubagentError(messages);
|
|
78
|
+
assert.equal(result.hasError, true);
|
|
79
|
+
assert.equal(result.exitCode, 127);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("ignores errors before last successful tool result", () => {
|
|
83
|
+
const messages = [
|
|
84
|
+
{ role: "assistant", content: [{ type: "text", text: "Trying..." }] },
|
|
85
|
+
{ role: "toolResult", toolName: "bash", isError: true, content: [{ type: "text", text: "EISDIR" }] },
|
|
86
|
+
{ role: "assistant", content: [{ type: "text", text: "Let me fix that..." }] },
|
|
87
|
+
{ role: "toolResult", toolName: "bash", isError: false, content: [{ type: "text", text: "OK" }] },
|
|
88
|
+
{ role: "assistant", content: [{ type: "text", text: "Fixed!" }] },
|
|
89
|
+
];
|
|
90
|
+
const result = detectSubagentError(messages);
|
|
91
|
+
assert.equal(result.hasError, false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("detects isError on tool result", () => {
|
|
95
|
+
const messages = [
|
|
96
|
+
{ role: "assistant", content: [{ type: "text", text: "Running..." }] },
|
|
97
|
+
{
|
|
98
|
+
role: "toolResult",
|
|
99
|
+
toolName: "write",
|
|
100
|
+
isError: true,
|
|
101
|
+
content: [{ type: "text", text: "Permission denied" }],
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
const result = detectSubagentError(messages);
|
|
105
|
+
assert.equal(result.hasError, true);
|
|
106
|
+
assert.equal(result.errorType, "write");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// runSync error handling
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
describe("runSync error handling", { skip: !piAvailable ? "pi packages not available" : undefined }, () => {
|
|
115
|
+
let tempDir: string;
|
|
116
|
+
let mockPi: MockPi;
|
|
117
|
+
|
|
118
|
+
before(() => {
|
|
119
|
+
mockPi = createMockPi();
|
|
120
|
+
mockPi.install();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
after(() => {
|
|
124
|
+
mockPi.uninstall();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
beforeEach(() => {
|
|
128
|
+
tempDir = createTempDir();
|
|
129
|
+
mockPi.reset();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
removeTempDir(tempDir);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("captures stderr on non-zero exit", async () => {
|
|
137
|
+
mockPi.onCall({ exitCode: 2, stderr: "Fatal: out of memory" });
|
|
138
|
+
const agents = makeAgentConfigs(["crash"]);
|
|
139
|
+
|
|
140
|
+
const result = await runSync(tempDir, agents, "crash", "Do heavy work", {});
|
|
141
|
+
|
|
142
|
+
assert.equal(result.exitCode, 2);
|
|
143
|
+
assert.ok(result.error?.includes("out of memory"));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("detectSubagentError overrides exit 0 on hidden failure", async () => {
|
|
147
|
+
mockPi.onCall({
|
|
148
|
+
jsonl: [
|
|
149
|
+
events.toolStart("bash", { command: "deploy" }),
|
|
150
|
+
events.toolEnd("bash"),
|
|
151
|
+
events.toolResult("bash", "connection refused"),
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
const agents = makeAgentConfigs(["deployer"]);
|
|
155
|
+
|
|
156
|
+
const result = await runSync(tempDir, agents, "deployer", "Deploy app", {});
|
|
157
|
+
|
|
158
|
+
assert.notEqual(result.exitCode, 0, "should detect hidden failure");
|
|
159
|
+
assert.ok(result.error?.includes("connection refused"));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("handles abort signal (completes faster than delay)", async () => {
|
|
163
|
+
mockPi.onCall({ delay: 10000 });
|
|
164
|
+
const agents = makeAgentConfigs(["slow"]);
|
|
165
|
+
const controller = new AbortController();
|
|
166
|
+
|
|
167
|
+
const start = Date.now();
|
|
168
|
+
setTimeout(() => controller.abort(), 200);
|
|
169
|
+
|
|
170
|
+
const result = await runSync(tempDir, agents, "slow", "Slow task", {
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
});
|
|
173
|
+
const elapsed = Date.now() - start;
|
|
174
|
+
|
|
175
|
+
// Key: should complete much faster than the 10s delay
|
|
176
|
+
assert.ok(elapsed < 5000, `should abort early, took ${elapsed}ms`);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// Chain error propagation
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
describe("chain error propagation", { skip: !chainAvailable ? "chain module not available" : undefined }, () => {
|
|
185
|
+
let tempDir: string;
|
|
186
|
+
let mockPi: MockPi;
|
|
187
|
+
|
|
188
|
+
before(() => {
|
|
189
|
+
mockPi = createMockPi();
|
|
190
|
+
mockPi.install();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
after(() => {
|
|
194
|
+
mockPi.uninstall();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
beforeEach(() => {
|
|
198
|
+
tempDir = createTempDir();
|
|
199
|
+
mockPi.reset();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
afterEach(() => {
|
|
203
|
+
removeTempDir(tempDir);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
function makeChainParams(chain: any[], agents: any[]) {
|
|
207
|
+
return {
|
|
208
|
+
chain,
|
|
209
|
+
agents,
|
|
210
|
+
ctx: makeMinimalCtx(tempDir),
|
|
211
|
+
runId: "test-err",
|
|
212
|
+
shareEnabled: false,
|
|
213
|
+
sessionDirForIndex: () => undefined,
|
|
214
|
+
artifactsDir: path.join(tempDir, "artifacts"),
|
|
215
|
+
artifactConfig: { enabled: false },
|
|
216
|
+
clarify: false,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
it("preserves error context from failed step", async () => {
|
|
221
|
+
mockPi.onCall({ exitCode: 1, stderr: "Step 1 exploded" });
|
|
222
|
+
const agents = [makeAgent("step1"), makeAgent("step2")];
|
|
223
|
+
|
|
224
|
+
const result = await executeChain(
|
|
225
|
+
makeChainParams(
|
|
226
|
+
[{ agent: "step1", task: "Fail here" }, { agent: "step2" }],
|
|
227
|
+
agents,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
assert.ok(result.isError);
|
|
232
|
+
const failedResult = result.details.results[0];
|
|
233
|
+
assert.equal(failedResult.exitCode, 1);
|
|
234
|
+
assert.ok(failedResult.error?.includes("exploded"));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("reports currentStepIndex on failure", async () => {
|
|
238
|
+
mockPi.onCall({ exitCode: 1 });
|
|
239
|
+
const agents = [makeAgent("a"), makeAgent("b"), makeAgent("c")];
|
|
240
|
+
|
|
241
|
+
const result = await executeChain(
|
|
242
|
+
makeChainParams(
|
|
243
|
+
[{ agent: "a", task: "First" }, { agent: "b" }, { agent: "c" }],
|
|
244
|
+
agents,
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
assert.ok(result.isError);
|
|
249
|
+
assert.equal(result.details.currentStepIndex, 0);
|
|
250
|
+
assert.equal(result.details.results.length, 1);
|
|
251
|
+
});
|
|
252
|
+
});
|