@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,101 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { describe, it } from "node:test";
|
|
7
|
+
import { attachPostExitStdioGuard, trySignalChild } from "../../src/shared/post-exit-stdio-guard.ts";
|
|
8
|
+
|
|
9
|
+
function writeScript(name: string, lines: string[]): string {
|
|
10
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-close-grace-"));
|
|
11
|
+
const script = path.join(dir, name);
|
|
12
|
+
fs.writeFileSync(script, lines.join("\n"), { mode: 0o755 });
|
|
13
|
+
return script;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function makeSilentLeakyScript(sleepSeconds: number): string {
|
|
17
|
+
return writeScript("silent-leak.sh", [
|
|
18
|
+
"#!/bin/bash",
|
|
19
|
+
"set -eu",
|
|
20
|
+
"echo done",
|
|
21
|
+
`sleep ${sleepSeconds} &`,
|
|
22
|
+
"disown || true",
|
|
23
|
+
"exit 0",
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function makeChattyLeakyScript(tickMs: number): string {
|
|
28
|
+
return writeScript("chatty-leak.sh", [
|
|
29
|
+
"#!/bin/bash",
|
|
30
|
+
"set -eu",
|
|
31
|
+
"echo start",
|
|
32
|
+
`( while true; do echo tick; sleep ${(tickMs / 1000).toFixed(3)}; done ) &`,
|
|
33
|
+
"disown || true",
|
|
34
|
+
"exit 0",
|
|
35
|
+
]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RunResult {
|
|
39
|
+
resolvedMs: number;
|
|
40
|
+
exitCode: number | null;
|
|
41
|
+
stdout: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function runWithGuard(script: string, idleMs: number, hardMs: number, maxWaitMs: number): Promise<RunResult> {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const start = Date.now();
|
|
47
|
+
const child = spawn("bash", [script], { stdio: ["ignore", "pipe", "pipe"] });
|
|
48
|
+
let stdout = "";
|
|
49
|
+
const clearGuard = attachPostExitStdioGuard(child, { idleMs, hardMs });
|
|
50
|
+
const hardStop = setTimeout(() => {
|
|
51
|
+
try { child.kill("SIGKILL"); } catch {}
|
|
52
|
+
reject(new Error(`promise did not resolve within ${maxWaitMs}ms`));
|
|
53
|
+
}, maxWaitMs);
|
|
54
|
+
hardStop.unref?.();
|
|
55
|
+
|
|
56
|
+
child.stdout.on("data", (chunk: Buffer) => {
|
|
57
|
+
stdout += chunk.toString();
|
|
58
|
+
});
|
|
59
|
+
child.stderr.on("data", () => {});
|
|
60
|
+
child.on("close", (code) => {
|
|
61
|
+
clearTimeout(hardStop);
|
|
62
|
+
clearGuard();
|
|
63
|
+
resolve({ resolvedMs: Date.now() - start, exitCode: code, stdout });
|
|
64
|
+
});
|
|
65
|
+
child.on("error", reject);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe("attachPostExitStdioGuard", () => {
|
|
70
|
+
it("reports whether a termination signal was actually delivered", () => {
|
|
71
|
+
assert.equal(trySignalChild({ kill: () => true }, "SIGTERM"), true);
|
|
72
|
+
assert.equal(trySignalChild({ kill: () => false }, "SIGTERM"), false);
|
|
73
|
+
assert.equal(trySignalChild({ kill: () => { throw new Error("gone"); } }, "SIGTERM"), false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not delay a clean exit", { skip: process.platform === "win32" ? "bash scripts unavailable on Windows" : undefined }, async () => {
|
|
77
|
+
const script = writeScript("clean.sh", ["#!/bin/bash", "set -eu", "echo hello", "exit 0"]);
|
|
78
|
+
const result = await runWithGuard(script, 2000, 8000, 5000);
|
|
79
|
+
assert.equal(result.exitCode, 0);
|
|
80
|
+
assert.match(result.stdout, /hello/);
|
|
81
|
+
assert.ok(result.resolvedMs < 500, `expected fast close, got ${result.resolvedMs}ms`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("cuts off a silent grandchild with the idle timer", { skip: process.platform === "win32" ? "bash scripts unavailable on Windows" : undefined }, async () => {
|
|
85
|
+
const idleMs = 1500;
|
|
86
|
+
const result = await runWithGuard(makeSilentLeakyScript(30), idleMs, 8000, 10000);
|
|
87
|
+
assert.equal(result.exitCode, 0);
|
|
88
|
+
assert.match(result.stdout, /done/);
|
|
89
|
+
assert.ok(result.resolvedMs >= idleMs, `resolved too early: ${result.resolvedMs}ms`);
|
|
90
|
+
assert.ok(result.resolvedMs < idleMs + 2000, `expected idle cutoff, got ${result.resolvedMs}ms`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("cuts off a chatty grandchild with the hard timer", { skip: process.platform === "win32" ? "bash scripts unavailable on Windows" : undefined }, async () => {
|
|
94
|
+
const hardMs = 2000;
|
|
95
|
+
const result = await runWithGuard(makeChattyLeakyScript(200), 1000, hardMs, 10000);
|
|
96
|
+
assert.equal(result.exitCode, 0);
|
|
97
|
+
assert.match(result.stdout, /start/);
|
|
98
|
+
assert.ok(result.resolvedMs >= hardMs - 500, `resolved too early: ${result.resolvedMs}ms`);
|
|
99
|
+
assert.ok(result.resolvedMs < hardMs + 2000, `expected hard cutoff, got ${result.resolvedMs}ms`);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
createCompletionBatcher,
|
|
5
|
+
DEFAULT_COMPLETION_BATCH_CONFIG,
|
|
6
|
+
resolveCompletionBatchConfig,
|
|
7
|
+
} from "../../src/runs/background/completion-batcher.ts";
|
|
8
|
+
|
|
9
|
+
interface FakeJob {
|
|
10
|
+
id: number;
|
|
11
|
+
fireAt: number;
|
|
12
|
+
handler: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createFakeClock() {
|
|
16
|
+
let now = 0;
|
|
17
|
+
let nextId = 1;
|
|
18
|
+
const jobs = new Map<number, FakeJob>();
|
|
19
|
+
const api = {
|
|
20
|
+
setTimeout(handler: () => void, delayMs: number): unknown {
|
|
21
|
+
const id = nextId++;
|
|
22
|
+
jobs.set(id, { id, fireAt: now + delayMs, handler });
|
|
23
|
+
return id;
|
|
24
|
+
},
|
|
25
|
+
clearTimeout(handle: unknown): void {
|
|
26
|
+
if (typeof handle === "number") jobs.delete(handle);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
api,
|
|
31
|
+
now: () => now,
|
|
32
|
+
advance(ms: number): void {
|
|
33
|
+
now += ms;
|
|
34
|
+
const due = [...jobs.values()].filter((job) => job.fireAt <= now).sort((a, b) => a.fireAt - b.fireAt);
|
|
35
|
+
for (const job of due) {
|
|
36
|
+
if (!jobs.has(job.id)) continue;
|
|
37
|
+
jobs.delete(job.id);
|
|
38
|
+
job.handler();
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
pendingCount: () => jobs.size,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function item(label: string): { label: string } {
|
|
46
|
+
return { label };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe("resolveCompletionBatchConfig", () => {
|
|
50
|
+
it("applies defaults when no config is provided", () => {
|
|
51
|
+
assert.deepEqual(resolveCompletionBatchConfig(), DEFAULT_COMPLETION_BATCH_CONFIG);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("override wins over global config", () => {
|
|
55
|
+
const resolved = resolveCompletionBatchConfig({ debounceMs: 50, maxWaitMs: 200 }, { debounceMs: 80 });
|
|
56
|
+
assert.equal(resolved.debounceMs, 80);
|
|
57
|
+
assert.equal(resolved.maxWaitMs, 200);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("rejects invalid booleans and non-positive or fractional integers", () => {
|
|
61
|
+
const resolved = resolveCompletionBatchConfig({ enabled: "false", debounceMs: 0, maxWaitMs: -5, stragglerDebounceMs: 1.5, stragglerWindowMs: Number.NaN } as never);
|
|
62
|
+
assert.equal(resolved.enabled, DEFAULT_COMPLETION_BATCH_CONFIG.enabled);
|
|
63
|
+
assert.equal(resolved.debounceMs, DEFAULT_COMPLETION_BATCH_CONFIG.debounceMs);
|
|
64
|
+
assert.equal(resolved.maxWaitMs, DEFAULT_COMPLETION_BATCH_CONFIG.maxWaitMs);
|
|
65
|
+
assert.equal(resolved.stragglerDebounceMs, DEFAULT_COMPLETION_BATCH_CONFIG.stragglerDebounceMs);
|
|
66
|
+
assert.equal(resolved.stragglerWindowMs, DEFAULT_COMPLETION_BATCH_CONFIG.stragglerWindowMs);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("ignores invalid overrides instead of masking valid global config", () => {
|
|
70
|
+
const resolved = resolveCompletionBatchConfig(
|
|
71
|
+
{ enabled: false, debounceMs: 80 },
|
|
72
|
+
{ enabled: "false", debounceMs: "bad" } as never,
|
|
73
|
+
);
|
|
74
|
+
assert.equal(resolved.enabled, false);
|
|
75
|
+
assert.equal(resolved.debounceMs, 80);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("createCompletionBatcher", () => {
|
|
80
|
+
it("emits immediately when batching is disabled", () => {
|
|
81
|
+
const emitted: string[][] = [];
|
|
82
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
83
|
+
config: { ...DEFAULT_COMPLETION_BATCH_CONFIG, enabled: false },
|
|
84
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
85
|
+
});
|
|
86
|
+
batcher.push(item("a"));
|
|
87
|
+
batcher.push(item("b"));
|
|
88
|
+
assert.deepEqual(emitted, [["a"], ["b"]]);
|
|
89
|
+
batcher.dispose();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("coalesces a burst into one grouped emit when the debounce fires", () => {
|
|
93
|
+
const clock = createFakeClock();
|
|
94
|
+
const emitted: string[][] = [];
|
|
95
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
96
|
+
config: { enabled: true, debounceMs: 150, maxWaitMs: 1000, stragglerDebounceMs: 75, stragglerMaxWaitMs: 400, stragglerWindowMs: 2000 },
|
|
97
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
98
|
+
timers: clock.api,
|
|
99
|
+
now: clock.now,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
batcher.push(item("a"));
|
|
103
|
+
batcher.push(item("b"));
|
|
104
|
+
clock.advance(40);
|
|
105
|
+
batcher.push(item("c"));
|
|
106
|
+
assert.deepEqual(emitted, []);
|
|
107
|
+
|
|
108
|
+
clock.advance(150);
|
|
109
|
+
assert.deepEqual(emitted, [["a", "b", "c"]]);
|
|
110
|
+
assert.equal(clock.pendingCount(), 0);
|
|
111
|
+
batcher.dispose();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("emits at the max-wait cap even when items keep arriving", () => {
|
|
115
|
+
const clock = createFakeClock();
|
|
116
|
+
const emitted: string[][] = [];
|
|
117
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
118
|
+
config: { enabled: true, debounceMs: 100, maxWaitMs: 300, stragglerDebounceMs: 50, stragglerMaxWaitMs: 150, stragglerWindowMs: 2000 },
|
|
119
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
120
|
+
timers: clock.api,
|
|
121
|
+
now: clock.now,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
batcher.push(item("a"));
|
|
125
|
+
// Keep resetting the debounce so it never fires on its own, while staying
|
|
126
|
+
// under the max-wait cap (cumulative 250ms < 300ms).
|
|
127
|
+
for (let step = 0; step < 5; step++) {
|
|
128
|
+
batcher.push(item(`late-${step}`));
|
|
129
|
+
clock.advance(50);
|
|
130
|
+
}
|
|
131
|
+
assert.deepEqual(emitted, []);
|
|
132
|
+
|
|
133
|
+
// Cross the max-wait cap (300ms from the first arrival).
|
|
134
|
+
clock.advance(50);
|
|
135
|
+
assert.deepEqual(emitted, [["a", "late-0", "late-1", "late-2", "late-3", "late-4"]]);
|
|
136
|
+
batcher.dispose();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("flush emits held items immediately and clears timers", () => {
|
|
140
|
+
const clock = createFakeClock();
|
|
141
|
+
const emitted: string[][] = [];
|
|
142
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
143
|
+
config: { ...DEFAULT_COMPLETION_BATCH_CONFIG, enabled: true, debounceMs: 150, maxWaitMs: 1000 },
|
|
144
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
145
|
+
timers: clock.api,
|
|
146
|
+
now: clock.now,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
batcher.push(item("a"));
|
|
150
|
+
batcher.push(item("b"));
|
|
151
|
+
assert.equal(clock.pendingCount(), 2);
|
|
152
|
+
batcher.flush();
|
|
153
|
+
assert.deepEqual(emitted, [["a", "b"]]);
|
|
154
|
+
assert.equal(clock.pendingCount(), 0);
|
|
155
|
+
|
|
156
|
+
// A later timer fire must not re-emit.
|
|
157
|
+
clock.advance(1000);
|
|
158
|
+
assert.deepEqual(emitted, [["a", "b"]]);
|
|
159
|
+
batcher.dispose();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("dispose clears timers without emitting", () => {
|
|
163
|
+
const clock = createFakeClock();
|
|
164
|
+
const emitted: string[][] = [];
|
|
165
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
166
|
+
config: { ...DEFAULT_COMPLETION_BATCH_CONFIG, enabled: true, debounceMs: 150, maxWaitMs: 1000 },
|
|
167
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
168
|
+
timers: clock.api,
|
|
169
|
+
now: clock.now,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
batcher.push(item("a"));
|
|
173
|
+
batcher.dispose();
|
|
174
|
+
assert.deepEqual(emitted, []);
|
|
175
|
+
assert.equal(clock.pendingCount(), 0);
|
|
176
|
+
clock.advance(1000);
|
|
177
|
+
assert.deepEqual(emitted, []);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("creates a shorter straggler group for late siblings", () => {
|
|
181
|
+
const clock = createFakeClock();
|
|
182
|
+
const emitted: string[][] = [];
|
|
183
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
184
|
+
config: { enabled: true, debounceMs: 200, maxWaitMs: 1000, stragglerDebounceMs: 50, stragglerMaxWaitMs: 150, stragglerWindowMs: 2000 },
|
|
185
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
186
|
+
timers: clock.api,
|
|
187
|
+
now: clock.now,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// First group emits via debounce.
|
|
191
|
+
batcher.push(item("a"));
|
|
192
|
+
clock.advance(200);
|
|
193
|
+
assert.deepEqual(emitted, [["a"]]);
|
|
194
|
+
|
|
195
|
+
// A sibling finishing soon after joins a straggler group with the
|
|
196
|
+
// shorter debounce window.
|
|
197
|
+
clock.advance(100);
|
|
198
|
+
batcher.push(item("b"));
|
|
199
|
+
assert.deepEqual(emitted, [["a"]]);
|
|
200
|
+
clock.advance(50);
|
|
201
|
+
assert.deepEqual(emitted, [["a"], ["b"]]);
|
|
202
|
+
batcher.dispose();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("starts a fresh normal group after the straggler window passes", () => {
|
|
206
|
+
const clock = createFakeClock();
|
|
207
|
+
const emitted: string[][] = [];
|
|
208
|
+
const batcher = createCompletionBatcher<{ label: string }>({
|
|
209
|
+
config: { enabled: true, debounceMs: 200, maxWaitMs: 1000, stragglerDebounceMs: 50, stragglerMaxWaitMs: 150, stragglerWindowMs: 500 },
|
|
210
|
+
emit: (items) => emitted.push(items.map((i) => i.label)),
|
|
211
|
+
timers: clock.api,
|
|
212
|
+
now: clock.now,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
batcher.push(item("a"));
|
|
216
|
+
clock.advance(200);
|
|
217
|
+
assert.deepEqual(emitted, [["a"]]);
|
|
218
|
+
|
|
219
|
+
// Past the straggler window: the next arrival is a normal group again.
|
|
220
|
+
clock.advance(600);
|
|
221
|
+
batcher.push(item("b"));
|
|
222
|
+
clock.advance(50);
|
|
223
|
+
assert.deepEqual(emitted, [["a"]]);
|
|
224
|
+
clock.advance(150);
|
|
225
|
+
assert.deepEqual(emitted, [["a"], ["b"]]);
|
|
226
|
+
batcher.dispose();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "../../src/runs/background/completion-dedupe.ts";
|
|
4
|
+
|
|
5
|
+
describe("buildCompletionKey", () => {
|
|
6
|
+
it("uses id as canonical key when present", () => {
|
|
7
|
+
const key = buildCompletionKey({ id: "run-123", agent: "reviewer", timestamp: 123 }, "fallback");
|
|
8
|
+
assert.equal(key, "id:run-123");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("builds deterministic fallback key when id is missing", () => {
|
|
12
|
+
const a = buildCompletionKey({ agent: "reviewer", timestamp: 123, taskIndex: 1, totalTasks: 2, success: true }, "x");
|
|
13
|
+
const b = buildCompletionKey({ agent: "reviewer", timestamp: 123, taskIndex: 1, totalTasks: 2, success: true }, "x");
|
|
14
|
+
assert.equal(a, b);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("markSeenWithTtl", () => {
|
|
19
|
+
it("returns true only for duplicates within ttl", () => {
|
|
20
|
+
const seen = new Map<string, number>();
|
|
21
|
+
const ttlMs = 1000;
|
|
22
|
+
assert.equal(markSeenWithTtl(seen, "k", 100, ttlMs), false);
|
|
23
|
+
assert.equal(markSeenWithTtl(seen, "k", 200, ttlMs), true);
|
|
24
|
+
assert.equal(markSeenWithTtl(seen, "k", 1201, ttlMs), false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("getGlobalSeenMap", () => {
|
|
29
|
+
it("returns the same map for the same global store key", () => {
|
|
30
|
+
const a = getGlobalSeenMap("__test_seen_key__");
|
|
31
|
+
a.set("x", 1);
|
|
32
|
+
const b = getGlobalSeenMap("__test_seen_key__");
|
|
33
|
+
assert.equal(b.get("x"), 1);
|
|
34
|
+
assert.equal(a, b);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
evaluateCompletionMutationGuard,
|
|
8
|
+
expectsImplementationMutation,
|
|
9
|
+
hasMutationToolCall,
|
|
10
|
+
} from "../../src/runs/shared/completion-guard.ts";
|
|
11
|
+
|
|
12
|
+
function assistantToolCall(name: string, args: Record<string, unknown> = {}): Message {
|
|
13
|
+
return {
|
|
14
|
+
role: "assistant",
|
|
15
|
+
content: [{ type: "toolCall", name, arguments: args }],
|
|
16
|
+
} as unknown as Message;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assistantText(text: string): Message {
|
|
20
|
+
return {
|
|
21
|
+
role: "assistant",
|
|
22
|
+
content: [{ type: "text", text }],
|
|
23
|
+
} as unknown as Message;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test("implementation task with no mutation triggers the completion guard", () => {
|
|
27
|
+
const result = evaluateCompletionMutationGuard({
|
|
28
|
+
agent: "worker",
|
|
29
|
+
task: "Implement the approved fix",
|
|
30
|
+
messages: [assistantText("Plan: update the files...")],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(result, {
|
|
34
|
+
expectedMutation: true,
|
|
35
|
+
attemptedMutation: false,
|
|
36
|
+
triggered: true,
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("declared read-only builtin tools suppress implementation-word false positives", () => {
|
|
41
|
+
const result = evaluateCompletionMutationGuard({
|
|
42
|
+
agent: "architect",
|
|
43
|
+
task: "Produce a proposal that implements the approved fix",
|
|
44
|
+
messages: [assistantText("Proposal only")],
|
|
45
|
+
tools: ["read", "grep", "find", "ls"],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
assert.deepEqual(result, {
|
|
49
|
+
expectedMutation: false,
|
|
50
|
+
attemptedMutation: false,
|
|
51
|
+
triggered: false,
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("omitted, empty, bash, unknown, write, and MCP tool capabilities stay conservative", () => {
|
|
56
|
+
const base = {
|
|
57
|
+
agent: "architect",
|
|
58
|
+
task: "Implement the approved source fix",
|
|
59
|
+
messages: [assistantText("Validation only")],
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
assert.equal(evaluateCompletionMutationGuard(base).triggered, true);
|
|
63
|
+
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: [] }).triggered, true);
|
|
64
|
+
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "bash", "ls"] }).triggered, true);
|
|
65
|
+
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "custom_lookup"] }).triggered, true);
|
|
66
|
+
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "write"] }).triggered, true);
|
|
67
|
+
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "grep"], mcpDirectTools: ["github/search"] }).triggered, true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("worker with mutating-capable tools still triggers when no mutation is observed", () => {
|
|
71
|
+
const result = evaluateCompletionMutationGuard({
|
|
72
|
+
agent: "worker",
|
|
73
|
+
task: "Fix the test implementation",
|
|
74
|
+
messages: [assistantText("I will edit it next")],
|
|
75
|
+
tools: ["read", "edit"],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
assert.deepEqual(result, {
|
|
79
|
+
expectedMutation: true,
|
|
80
|
+
attemptedMutation: false,
|
|
81
|
+
triggered: true,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("review-only, research, and framework output instructions do not expect mutation", () => {
|
|
86
|
+
assert.equal(expectsImplementationMutation("worker", "Review only: return findings, do not edit"), false);
|
|
87
|
+
assert.equal(expectsImplementationMutation("worker", "Do not edit files. Tell me how to fix the bug."), false);
|
|
88
|
+
assert.equal(expectsImplementationMutation("worker", "Review the diff and suggest fixes only. Do not edit files."), false);
|
|
89
|
+
assert.equal(expectsImplementationMutation("worker", "Implement this. Do not edit files outside this repo. Do not edit files."), false);
|
|
90
|
+
assert.equal(expectsImplementationMutation("worker", "Investigate why this failed"), false);
|
|
91
|
+
assert.equal(expectsImplementationMutation("researcher", "Research the API behavior"), false);
|
|
92
|
+
assert.equal(expectsImplementationMutation("researcher", "Research this and patch the bug"), false);
|
|
93
|
+
assert.equal(expectsImplementationMutation("reviewer", "Review this and fix any real issues"), false);
|
|
94
|
+
assert.equal(expectsImplementationMutation("reviewer", "Review this and fix any real issues; regardless of findings, apply changes directly"), true);
|
|
95
|
+
assert.equal(expectsImplementationMutation("worker", "[Write to: /tmp/result.md]\n\nSummarize findings"), false);
|
|
96
|
+
assert.equal(expectsImplementationMutation("worker", "Write report"), false);
|
|
97
|
+
assert.equal(expectsImplementationMutation("worker", "Create a report"), false);
|
|
98
|
+
assert.equal(expectsImplementationMutation("worker", "Create a summary"), false);
|
|
99
|
+
assert.equal(expectsImplementationMutation("worker", "Add a report"), false);
|
|
100
|
+
assert.equal(expectsImplementationMutation("worker", "Update a summary"), false);
|
|
101
|
+
assert.equal(expectsImplementationMutation("worker", "Write to {chain_dir}"), false);
|
|
102
|
+
assert.equal(
|
|
103
|
+
expectsImplementationMutation("worker", "Do async work\nUpdate progress at: /tmp/progress.md\n**Output:**\nWrite your findings to exactly this path: /tmp/out.md\nThis path is authoritative for this run.\nIgnore any other output filename or output path mentioned elsewhere."),
|
|
104
|
+
false,
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("worker implementation verbs win over investigative wording", () => {
|
|
109
|
+
assert.equal(expectsImplementationMutation("worker", "Investigate why the worker did not edit files and fix it"), true);
|
|
110
|
+
assert.equal(expectsImplementationMutation("worker", "Research the current code path and patch the bug"), true);
|
|
111
|
+
assert.equal(expectsImplementationMutation("worker", "Fix the bug where no edits were made"), true);
|
|
112
|
+
assert.equal(expectsImplementationMutation("worker", "Implement the fix and return findings."), true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("worker edit intent covers common docs, config, and source tasks", () => {
|
|
116
|
+
assert.equal(expectsImplementationMutation("worker", "Update README to mention the native tool"), true);
|
|
117
|
+
assert.equal(expectsImplementationMutation("worker", "Remove share functionality and all Vercel references"), true);
|
|
118
|
+
assert.equal(expectsImplementationMutation("worker", "Replace the registered command with a render tool"), true);
|
|
119
|
+
assert.equal(expectsImplementationMutation("worker", "Create completion-guard.ts"), true);
|
|
120
|
+
assert.equal(expectsImplementationMutation("worker", "Add tests for the completion guard"), true);
|
|
121
|
+
assert.equal(expectsImplementationMutation("worker", "Implement the approved fixes. Do not edit files outside this repo."), true);
|
|
122
|
+
assert.equal(expectsImplementationMutation("worker", "Implement the fix. Do not edit unrelated files."), true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("edit and write tool calls count as mutation attempts", () => {
|
|
126
|
+
assert.equal(hasMutationToolCall([assistantToolCall("edit", { path: "a.ts" })]), true);
|
|
127
|
+
assert.equal(hasMutationToolCall([assistantToolCall("write", { path: "a.ts" })]), true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("obvious mutating bash commands count as mutation attempts", () => {
|
|
131
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "mkdir -p src && cat > src/file.ts <<'EOF'\nhi\nEOF" })]), true);
|
|
132
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "cat <<'EOF' > src/file.ts\nhi\nEOF" })]), true);
|
|
133
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "python3 -c \"from pathlib import Path; Path('x').write_text('hi')\"" })]), true);
|
|
134
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "node script.js > generated.txt" })]), true);
|
|
135
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "echo 'a > b'" })]), false);
|
|
136
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "node -e \"console.log(a > b)\"" })]), false);
|
|
137
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "python3 <<'PY'\nprint('inspect only')\nPY" })]), false);
|
|
138
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "echo 'rm file'" })]), false);
|
|
139
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "printf \"mkdir x\"" })]), false);
|
|
140
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "git apply patch.diff" })]), true);
|
|
141
|
+
assert.equal(hasMutationToolCall([assistantToolCall("bash", { command: "patch -p0 < fix.patch" })]), true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("implementation task with mutation attempts does not trigger", () => {
|
|
145
|
+
const result = evaluateCompletionMutationGuard({
|
|
146
|
+
agent: "worker",
|
|
147
|
+
task: "Fix the failing test",
|
|
148
|
+
messages: [assistantToolCall("edit", { path: "test.ts" })],
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
assert.equal(result.triggered, false);
|
|
152
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { getConfigDirName, PI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveConfigDirName } from "../../src/shared/utils.ts";
|
|
8
|
+
|
|
9
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10
|
+
let previousPackageRootEnv: string | undefined;
|
|
11
|
+
|
|
12
|
+
describe("config directory resolution", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
previousPackageRootEnv = process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
15
|
+
delete process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (previousPackageRootEnv === undefined) delete process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
20
|
+
else process.env[PI_CODING_AGENT_PACKAGE_ROOT_ENV] = previousPackageRootEnv;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("falls back without importing the Pi peer package at runtime", () => {
|
|
24
|
+
assert.equal(resolveConfigDirName(), ".selesai");
|
|
25
|
+
assert.equal(getConfigDirName(), ".selesai");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("honors an explicitly provided Pi module shape", () => {
|
|
29
|
+
assert.equal(resolveConfigDirName({ CONFIG_DIR_NAME: ".custom-pi" }), ".custom-pi");
|
|
30
|
+
assert.equal(resolveConfigDirName({ CONFIG_DIR_NAME: "" }), ".selesai");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("honors Pi package metadata without importing the peer package", () => {
|
|
34
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-config-dir-"));
|
|
35
|
+
try {
|
|
36
|
+
const packageRoot = path.join(tempDir, "coding-agent");
|
|
37
|
+
const distDir = path.join(packageRoot, "dist");
|
|
38
|
+
fs.mkdirSync(distDir, { recursive: true });
|
|
39
|
+
const cliPath = path.join(distDir, "cli.js");
|
|
40
|
+
fs.writeFileSync(cliPath, "", "utf-8");
|
|
41
|
+
fs.writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify({
|
|
42
|
+
name: "@selesai/code",
|
|
43
|
+
piConfig: { configDir: ".custom-pi" },
|
|
44
|
+
}), "utf-8");
|
|
45
|
+
|
|
46
|
+
assert.equal(resolveConfigDirName(undefined, cliPath), ".custom-pi");
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("uses an explicit Pi package root before the process entrypoint walk", () => {
|
|
53
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagents-config-root-"));
|
|
54
|
+
try {
|
|
55
|
+
const packageRoot = path.join(tempDir, "coding-agent-root");
|
|
56
|
+
fs.mkdirSync(packageRoot, { recursive: true });
|
|
57
|
+
fs.writeFileSync(path.join(packageRoot, "package.json"), JSON.stringify({
|
|
58
|
+
name: "@selesai/code",
|
|
59
|
+
piConfig: { configDir: ".root-pi" },
|
|
60
|
+
}), "utf-8");
|
|
61
|
+
|
|
62
|
+
assert.equal(resolveConfigDirName(undefined, undefined, packageRoot), ".root-pi");
|
|
63
|
+
} finally {
|
|
64
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("does not runtime-import the coding agent peer from shared utils", () => {
|
|
69
|
+
const source = fs.readFileSync(path.join(repoRoot, "src/shared/utils.ts"), "utf-8");
|
|
70
|
+
assert.doesNotMatch(source, /import\s+[^;]*from\s+["']@earendil-works\/pi-coding-agent["']/);
|
|
71
|
+
});
|
|
72
|
+
});
|