@selesai/code 0.5.20 → 0.5.21
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/extensions/handoff-new.test.ts +8 -9
- package/dist/extensions/handoff-new.ts +6 -8
- package/dist/extensions/pi-subagents/CHANGELOG.md +1367 -0
- package/dist/extensions/pi-subagents/README.md +602 -220
- package/dist/extensions/pi-subagents/banner.png +0 -0
- package/dist/extensions/pi-subagents/index.ts +1 -0
- package/dist/extensions/pi-subagents/install.mjs +3 -3
- package/dist/extensions/pi-subagents/package-lock.json +3405 -0
- package/dist/extensions/pi-subagents/package.json +26 -14
- package/dist/extensions/pi-subagents/prompts/gather-context-and-clarify.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-cleanup.md +9 -9
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +3 -3
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +7 -7
- package/dist/extensions/pi-subagents/prompts/parallel-research.md +6 -6
- package/dist/extensions/pi-subagents/prompts/parallel-review.md +8 -8
- package/dist/extensions/pi-subagents/prompts/review-loop.md +13 -11
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +267 -176
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +89 -16
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +19 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +218 -120
- package/dist/extensions/pi-subagents/src/agents/frontmatter.ts +67 -13
- package/dist/extensions/pi-subagents/src/agents/proactive-skills.ts +2 -2
- package/dist/extensions/pi-subagents/src/agents/skills.ts +25 -12
- package/dist/extensions/pi-subagents/src/api/background-work.ts +197 -0
- package/dist/extensions/pi-subagents/src/api/capability-ceiling.ts +17 -0
- package/dist/extensions/pi-subagents/src/api/delegation.ts +285 -0
- package/dist/extensions/pi-subagents/src/api/preflight.ts +399 -0
- package/dist/extensions/pi-subagents/src/extension/chain-validation.ts +165 -0
- package/dist/extensions/pi-subagents/src/extension/config.ts +7 -1
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +15 -0
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -1
- package/dist/extensions/pi-subagents/src/extension/index.ts +113 -157
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +41 -4
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +45 -15
- package/dist/extensions/pi-subagents/src/extension/steering-notices.ts +35 -0
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +28 -15
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +4 -3
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +225 -29
- package/dist/extensions/pi-subagents/src/intercom/result-intercom.ts +11 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +8 -10
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +452 -62
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +60 -9
- package/dist/extensions/pi-subagents/src/runs/background/async-resume.ts +198 -52
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +119 -21
- package/dist/extensions/pi-subagents/src/runs/background/auto-drain.ts +67 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-append.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +16 -8
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +6 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-dedupe.ts +2 -11
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +260 -13
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +32 -6
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +171 -90
- package/dist/extensions/pi-subagents/src/runs/background/process-terminal.ts +280 -0
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +149 -88
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +14 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +33 -17
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +9 -1
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +38 -10
- package/dist/extensions/pi-subagents/src/runs/background/steering.ts +237 -0
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +1297 -307
- package/dist/extensions/pi-subagents/src/runs/background/subagent-wait.ts +610 -0
- package/dist/extensions/pi-subagents/src/runs/background/top-level-async.ts +2 -1
- package/dist/extensions/pi-subagents/src/runs/background/wait-config.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/background/wait-tool.ts +26 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/async-steering-action.ts +230 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +22 -6
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +228 -140
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +452 -138
- package/dist/extensions/pi-subagents/src/runs/foreground/foreground-control.ts +90 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +1001 -416
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +443 -132
- package/dist/extensions/pi-subagents/src/runs/shared/agent-contract.ts +38 -0
- package/dist/extensions/pi-subagents/src/runs/shared/capability-ceiling.ts +178 -0
- package/dist/extensions/pi-subagents/src/runs/shared/child-protocol.ts +121 -0
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +41 -93
- package/dist/extensions/pi-subagents/src/runs/shared/context-mode.ts +44 -0
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +11 -9
- package/dist/extensions/pi-subagents/src/runs/shared/long-running-guard.ts +4 -0
- package/dist/extensions/pi-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +36 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +44 -7
- package/dist/extensions/pi-subagents/src/runs/shared/nested-render.ts +4 -1
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-handoff.ts +154 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +18 -0
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +174 -54
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +38 -31
- package/dist/extensions/pi-subagents/src/runs/shared/run-history.ts +90 -5
- package/dist/extensions/pi-subagents/src/runs/shared/session-lease.ts +299 -0
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +61 -6
- package/dist/extensions/pi-subagents/src/runs/shared/spawn-budget.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +112 -7
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-control.ts +14 -6
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +149 -36
- package/dist/extensions/pi-subagents/src/runs/shared/task-intent.ts +181 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-availability.ts +83 -0
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +11 -5
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +50 -4
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +63 -14
- package/dist/extensions/pi-subagents/src/shared/accessible-dir.ts +25 -0
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +37 -7
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +18 -43
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +52 -0
- package/dist/extensions/pi-subagents/src/shared/env.ts +16 -0
- package/dist/extensions/pi-subagents/src/shared/file-system-retry.ts +47 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +28 -3
- package/dist/extensions/pi-subagents/src/shared/launch-contract.ts +123 -0
- package/dist/extensions/pi-subagents/src/shared/model-info.ts +8 -5
- package/dist/extensions/pi-subagents/src/shared/settings.ts +9 -1
- package/dist/extensions/pi-subagents/src/shared/status-format.ts +7 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +522 -51
- package/dist/extensions/pi-subagents/src/shared/utils.ts +59 -53
- package/dist/extensions/pi-subagents/src/slash/delegation-adapters.ts +604 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-json.ts +108 -0
- package/dist/extensions/pi-subagents/src/slash/delegation-request.ts +249 -0
- package/dist/extensions/pi-subagents/src/slash/prompt-template-bridge.ts +353 -345
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/selector.ts +147 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +265 -23
- package/dist/extensions/pi-subagents/src/slash/slash-live-state.ts +2 -2
- package/dist/extensions/pi-subagents/src/slash/subagents-admin.ts +428 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-status.ts +362 -0
- package/dist/extensions/pi-subagents/src/tui/fleet-transcript.ts +472 -0
- package/dist/extensions/pi-subagents/src/tui/fleet.ts +664 -0
- package/dist/extensions/pi-subagents/src/tui/render.ts +115 -31
- package/dist/extensions/pi-subagents/src/watchdog/change-signature.ts +220 -0
- package/dist/extensions/pi-subagents/src/watchdog/child-status.ts +205 -0
- package/dist/extensions/pi-subagents/src/watchdog/emission-guard.ts +123 -0
- package/dist/extensions/pi-subagents/src/watchdog/lsp-diagnostics.ts +532 -0
- package/dist/extensions/pi-subagents/src/watchdog/model-selection.ts +167 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-child.ts +117 -0
- package/dist/extensions/pi-subagents/src/watchdog/register-main.ts +433 -0
- package/dist/extensions/pi-subagents/src/watchdog/render.ts +54 -0
- package/dist/extensions/pi-subagents/src/watchdog/review.ts +298 -0
- package/dist/extensions/pi-subagents/src/watchdog/runtime.ts +714 -0
- package/dist/extensions/pi-subagents/src/watchdog/settings.ts +528 -0
- package/dist/extensions/pi-subagents/src/watchdog/tool-actions.ts +155 -0
- package/dist/extensions/pi-subagents/src/watchdog/turn-delta.ts +161 -0
- package/dist/extensions/pi-subagents/src/watchdog/types.ts +188 -0
- package/dist/extensions/pi-subagents/src/watchdog/warning-format.ts +73 -0
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +130 -2
- package/dist/extensions/pi-subagents/test/integration/acceptance-file-report.test.ts +439 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +1632 -61
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +54 -1
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +98 -1
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +179 -9
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +16 -9
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +6 -5
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +589 -9
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +528 -21
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +227 -3
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +235 -7
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +172 -2
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1502 -18
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +993 -4
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +27 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +7 -1
- package/dist/extensions/pi-subagents/test/support/helpers.ts +32 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +59 -2
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +32 -6
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +25 -24
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +59 -35
- package/dist/extensions/pi-subagents/test/support/session-lease-child.mjs +42 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +703 -69
- package/dist/extensions/pi-subagents/test/unit/accessible-dir.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +15 -15
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +66 -66
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +478 -19
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +228 -7
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +198 -78
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +15 -1
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +102 -7
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +439 -6
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +41 -1
- package/dist/extensions/pi-subagents/test/unit/auto-drain.test.ts +79 -0
- package/dist/extensions/pi-subagents/test/unit/background-work.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling-pi-args.test.ts +84 -0
- package/dist/extensions/pi-subagents/test/unit/capability-ceiling.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/chain-validation.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/child-protocol.test.ts +82 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +46 -4
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +3 -1
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +9 -17
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +175 -3
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +10 -5
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +126 -6
- package/dist/extensions/pi-subagents/test/unit/default-extensions.test.ts +219 -0
- package/dist/extensions/pi-subagents/test/unit/delegation-api.test.ts +913 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +13 -2
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +3 -3
- package/dist/extensions/pi-subagents/test/unit/fleet-status.test.ts +351 -0
- package/dist/extensions/pi-subagents/test/unit/fleet-transcript.test.ts +280 -0
- package/dist/extensions/pi-subagents/test/unit/fleet.test.ts +575 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-control.test.ts +65 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +94 -1
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/host-peer-runtime-imports.test.ts +157 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +367 -5
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +11 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +25 -3
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +341 -10
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +16 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +168 -7
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +85 -7
- package/dist/extensions/pi-subagents/test/unit/parallel-handoff.test.ts +175 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +1 -1
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +243 -16
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +79 -36
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +174 -56
- package/dist/extensions/pi-subagents/test/unit/preflight.test.ts +322 -0
- package/dist/extensions/pi-subagents/test/unit/process-terminal.test.ts +204 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +20 -12
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +117 -4
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +40 -7
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +48 -1
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +21 -3
- package/dist/extensions/pi-subagents/test/unit/selector.test.ts +57 -0
- package/dist/extensions/pi-subagents/test/unit/session-lease.test.ts +326 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +101 -2
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +111 -1
- package/dist/extensions/pi-subagents/test/unit/spawn-budget.test.ts +124 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +6 -4
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +7 -0
- package/dist/extensions/pi-subagents/test/unit/steering-action.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/unit/steering-notices.test.ts +58 -0
- package/dist/extensions/pi-subagents/test/unit/steering.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +25 -4
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +390 -25
- package/dist/extensions/pi-subagents/test/unit/subagent-wait.test.ts +837 -0
- package/dist/extensions/pi-subagents/test/unit/task-intent.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +15 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +47 -1
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +56 -10
- package/dist/extensions/pi-subagents/test/unit/watchdog-change-signature.test.ts +387 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-child-status.test.ts +110 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-emission-guard.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-lsp-diagnostics.test.ts +144 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-model-selection.test.ts +121 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-render.test.ts +76 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-review.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-runtime.test.ts +863 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-settings.test.ts +290 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-tool-actions.test.ts +91 -0
- package/dist/extensions/pi-subagents/test/unit/watchdog-turn-delta.test.ts +130 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +32 -4
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +5 -1
- package/dist/extensions/pi-subagents/test/unit/writer-budget-guidance.test.ts +30 -0
- package/dist/extensions/question/batch.ts +39 -0
- package/dist/extensions/question/dialog-adapter.ts +23 -1
- package/dist/extensions/question/index.ts +237 -7
- package/dist/extensions/question/schemas.ts +28 -15
- package/dist/extensions/question/tests/batch.test.ts +26 -0
- package/dist/extensions/question/tests/ui-protocol.test.ts +23 -3
- package/dist/extensions/question/tui-adapter.ts +2 -3
- package/dist/extensions/question/types.ts +37 -0
- package/dist/extensions/question/ui-protocol.ts +3 -3
- package/dist/skills/batch-grill-me/SKILL.md +3 -1
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
expectsImplementationMutation,
|
|
9
9
|
hasMutationToolCall,
|
|
10
10
|
} from "../../src/runs/shared/completion-guard.ts";
|
|
11
|
+
import { isMutatingTool } from "../../src/runs/shared/long-running-guard.ts";
|
|
11
12
|
|
|
12
13
|
function assistantToolCall(name: string, args: Record<string, unknown> = {}): Message {
|
|
13
14
|
return {
|
|
@@ -52,7 +53,28 @@ test("declared read-only builtin tools suppress implementation-word false positi
|
|
|
52
53
|
});
|
|
53
54
|
});
|
|
54
55
|
|
|
55
|
-
test("
|
|
56
|
+
test("read-only issue drafting tasks do not trigger on suggested fix wording", () => {
|
|
57
|
+
const task = "Draft GitHub issue for pi-subagents bug from current conversation. Include title, environment/context, reproduction steps, actual/expected, logs excerpt, suspected cause, suggested fix. Terse but complete. No tools needed.";
|
|
58
|
+
const result = evaluateCompletionMutationGuard({
|
|
59
|
+
agent: "delegate",
|
|
60
|
+
task,
|
|
61
|
+
messages: [assistantText("Title: completionGuard false positive\n\nSuggested fix: model read-only intent.")],
|
|
62
|
+
tools: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(result, {
|
|
66
|
+
expectedMutation: false,
|
|
67
|
+
attemptedMutation: false,
|
|
68
|
+
triggered: false,
|
|
69
|
+
});
|
|
70
|
+
assert.equal(expectsImplementationMutation("worker", task), false);
|
|
71
|
+
assert.equal(
|
|
72
|
+
expectsImplementationMutation("worker", "Draft GitHub issue for a bug. Include suspected cause and suggested fix."),
|
|
73
|
+
false,
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("omitted, bash, unknown, write, and MCP tool capabilities stay conservative while empty tools are toolless", () => {
|
|
56
78
|
const base = {
|
|
57
79
|
agent: "architect",
|
|
58
80
|
task: "Implement the approved source fix",
|
|
@@ -60,7 +82,7 @@ test("omitted, empty, bash, unknown, write, and MCP tool capabilities stay conse
|
|
|
60
82
|
};
|
|
61
83
|
|
|
62
84
|
assert.equal(evaluateCompletionMutationGuard(base).triggered, true);
|
|
63
|
-
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: [] }).triggered,
|
|
85
|
+
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: [] }).triggered, false);
|
|
64
86
|
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "bash", "ls"] }).triggered, true);
|
|
65
87
|
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "custom_lookup"] }).triggered, true);
|
|
66
88
|
assert.equal(evaluateCompletionMutationGuard({ ...base, tools: ["read", "write"] }).triggered, true);
|
|
@@ -82,6 +104,22 @@ test("worker with mutating-capable tools still triggers when no mutation is obse
|
|
|
82
104
|
});
|
|
83
105
|
});
|
|
84
106
|
|
|
107
|
+
test("oracle review tasks with bash available do not require mutation", () => {
|
|
108
|
+
const task = "Review prep findings and determine what to implement with playbooks instead of before.";
|
|
109
|
+
const result = evaluateCompletionMutationGuard({
|
|
110
|
+
agent: "oracle",
|
|
111
|
+
task,
|
|
112
|
+
messages: [assistantText("Review complete with file-backed findings.")],
|
|
113
|
+
tools: ["read", "grep", "find", "ls", "bash", "intercom"],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
assert.deepEqual(result, {
|
|
117
|
+
expectedMutation: false,
|
|
118
|
+
attemptedMutation: false,
|
|
119
|
+
triggered: false,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
85
123
|
test("review-only, research, and framework output instructions do not expect mutation", () => {
|
|
86
124
|
assert.equal(expectsImplementationMutation("worker", "Review only: return findings, do not edit"), false);
|
|
87
125
|
assert.equal(expectsImplementationMutation("worker", "Do not edit files. Tell me how to fix the bug."), false);
|
|
@@ -105,13 +143,27 @@ test("review-only, research, and framework output instructions do not expect mut
|
|
|
105
143
|
);
|
|
106
144
|
});
|
|
107
145
|
|
|
108
|
-
test("worker implementation verbs win over investigative wording", () => {
|
|
146
|
+
test("worker implementation verbs win over investigative wording and scoped prohibitions", () => {
|
|
109
147
|
assert.equal(expectsImplementationMutation("worker", "Investigate why the worker did not edit files and fix it"), true);
|
|
148
|
+
assert.equal(expectsImplementationMutation("worker", "Do not modify tests; implement the fix"), true);
|
|
149
|
+
assert.equal(expectsImplementationMutation("worker", "Do not modify tests — implement the fix"), true);
|
|
110
150
|
assert.equal(expectsImplementationMutation("worker", "Research the current code path and patch the bug"), true);
|
|
111
151
|
assert.equal(expectsImplementationMutation("worker", "Fix the bug where no edits were made"), true);
|
|
152
|
+
assert.equal(expectsImplementationMutation("worker", "Fix lint"), true);
|
|
153
|
+
assert.equal(expectsImplementationMutation("worker", "Fix the build"), true);
|
|
154
|
+
assert.equal(expectsImplementationMutation("worker", "Fix TypeScript errors"), true);
|
|
155
|
+
assert.equal(expectsImplementationMutation("worker", "Fix CI"), true);
|
|
156
|
+
assert.equal(expectsImplementationMutation("worker", "Fix the failing test"), true);
|
|
157
|
+
assert.equal(expectsImplementationMutation("worker", "Patch the cold start test"), true);
|
|
112
158
|
assert.equal(expectsImplementationMutation("worker", "Implement the fix and return findings."), true);
|
|
113
159
|
});
|
|
114
160
|
|
|
161
|
+
test("non-worker implementation tasks still expect mutation", () => {
|
|
162
|
+
assert.equal(expectsImplementationMutation("delegate", "Fix the bug where no edits were made"), true);
|
|
163
|
+
assert.equal(expectsImplementationMutation("delegate", "Apply the suggested fix to src/runs/shared/completion-guard.ts"), true);
|
|
164
|
+
assert.equal(expectsImplementationMutation("worker", "Draft a GitHub issue, then implement the fix"), true);
|
|
165
|
+
});
|
|
166
|
+
|
|
115
167
|
test("worker edit intent covers common docs, config, and source tasks", () => {
|
|
116
168
|
assert.equal(expectsImplementationMutation("worker", "Update README to mention the native tool"), true);
|
|
117
169
|
assert.equal(expectsImplementationMutation("worker", "Remove share functionality and all Vercel references"), true);
|
|
@@ -150,3 +202,123 @@ test("implementation task with mutation attempts does not trigger", () => {
|
|
|
150
202
|
|
|
151
203
|
assert.equal(result.triggered, false);
|
|
152
204
|
});
|
|
205
|
+
|
|
206
|
+
function assistantThinking(thinking: string): Message {
|
|
207
|
+
return {
|
|
208
|
+
role: "assistant",
|
|
209
|
+
content: [{ type: "thinking", thinking }],
|
|
210
|
+
} as unknown as Message;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
test("Cursor edit/write thinking traces count as mutation attempts", () => {
|
|
214
|
+
assert.equal(
|
|
215
|
+
hasMutationToolCall([assistantThinking("Cursor edit: docs/BACKEND_ARCHITECTURE.md added 10 lines, removed 3 lines\n")]),
|
|
216
|
+
true,
|
|
217
|
+
);
|
|
218
|
+
assert.equal(
|
|
219
|
+
hasMutationToolCall([assistantThinking("Cursor write: src/file.ts created\n")]),
|
|
220
|
+
true,
|
|
221
|
+
);
|
|
222
|
+
assert.equal(
|
|
223
|
+
hasMutationToolCall([assistantThinking("I plan to edit the file next\n")]),
|
|
224
|
+
false,
|
|
225
|
+
);
|
|
226
|
+
assert.equal(
|
|
227
|
+
hasMutationToolCall([assistantThinking("Cursor read: docs/BACKEND_ARCHITECTURE.md\n")]),
|
|
228
|
+
false,
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("Cursor replay tool calls count only edit/write activity as mutation", () => {
|
|
233
|
+
const cursorEdit = { activityTitle: "Cursor edit", path: "docs/BACKEND_ARCHITECTURE.md" };
|
|
234
|
+
const cursorWrite = { activityTitle: "Cursor write", path: "src/file.ts" };
|
|
235
|
+
assert.equal(hasMutationToolCall([assistantToolCall("cursor", cursorEdit)]), true);
|
|
236
|
+
assert.equal(hasMutationToolCall([assistantToolCall("cursor", cursorWrite)]), true);
|
|
237
|
+
assert.equal(hasMutationToolCall([assistantToolCall("cursor", { activityTitle: "Cursor read" })]), false);
|
|
238
|
+
assert.equal(isMutatingTool("cursor", cursorEdit), true);
|
|
239
|
+
assert.equal(isMutatingTool("cursor", { activityTitle: "Cursor read" }), false);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("claimed changedFiles without mutation evidence does not bypass the guard", () => {
|
|
243
|
+
const report = assistantText(`Done.\n\`\`\`acceptance-report\n{\n "changedFiles": ["docs/BACKEND_ARCHITECTURE.md"]\n}\n\`\`\``);
|
|
244
|
+
assert.equal(hasMutationToolCall([report]), false);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
function checkpointDataEntry(beforeCommit: string, afterCommit: string): Message {
|
|
248
|
+
return {
|
|
249
|
+
type: "custom",
|
|
250
|
+
customType: "pi-checkpoint",
|
|
251
|
+
data: { beforeCommit, afterCommit },
|
|
252
|
+
} as unknown as Message;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function checkpointDetailsMessage(beforeCommit: string, afterCommit: string): Message {
|
|
256
|
+
return {
|
|
257
|
+
role: "custom",
|
|
258
|
+
customType: "pi-checkpoint",
|
|
259
|
+
content: [],
|
|
260
|
+
details: { beforeCommit, afterCommit },
|
|
261
|
+
} as unknown as Message;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function checkpointNestedDetailsMessage(beforeCommit: string, afterCommit: string): Message {
|
|
265
|
+
return {
|
|
266
|
+
role: "custom",
|
|
267
|
+
customType: "pi-checkpoint",
|
|
268
|
+
content: [],
|
|
269
|
+
details: { data: { beforeCommit, afterCommit } },
|
|
270
|
+
} as unknown as Message;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
test("provider checkpoint with a changed commit counts as mutation evidence", () => {
|
|
274
|
+
for (const message of [
|
|
275
|
+
checkpointDataEntry("before", "after"),
|
|
276
|
+
checkpointDetailsMessage("before", "after"),
|
|
277
|
+
checkpointNestedDetailsMessage("before", "after"),
|
|
278
|
+
]) {
|
|
279
|
+
assert.equal(hasMutationToolCall([message]), true);
|
|
280
|
+
assert.equal(
|
|
281
|
+
evaluateCompletionMutationGuard({
|
|
282
|
+
agent: "worker",
|
|
283
|
+
task: "Edit the target source file",
|
|
284
|
+
messages: [message],
|
|
285
|
+
}).triggered,
|
|
286
|
+
false,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("unchanged provider checkpoint does not bypass the completion guard", () => {
|
|
292
|
+
for (const message of [
|
|
293
|
+
checkpointDataEntry("same", "same"),
|
|
294
|
+
checkpointDetailsMessage("same", "same"),
|
|
295
|
+
checkpointNestedDetailsMessage("same", "same"),
|
|
296
|
+
]) {
|
|
297
|
+
assert.equal(hasMutationToolCall([message]), false);
|
|
298
|
+
assert.equal(
|
|
299
|
+
evaluateCompletionMutationGuard({
|
|
300
|
+
agent: "worker",
|
|
301
|
+
task: "Edit the target source file",
|
|
302
|
+
messages: [message],
|
|
303
|
+
}).triggered,
|
|
304
|
+
true,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("implementation task with Cursor edit thinking does not trigger", () => {
|
|
310
|
+
const result = evaluateCompletionMutationGuard({
|
|
311
|
+
agent: "worker",
|
|
312
|
+
task: "Edit docs/BACKEND_ARCHITECTURE.md",
|
|
313
|
+
messages: [
|
|
314
|
+
assistantThinking("Cursor edit: docs/BACKEND_ARCHITECTURE.md added 1 line\n"),
|
|
315
|
+
assistantText("Implemented the six simplifications."),
|
|
316
|
+
],
|
|
317
|
+
tools: ["read", "grep", "find", "ls", "bash", "edit", "write", "contact_supervisor"],
|
|
318
|
+
});
|
|
319
|
+
assert.deepEqual(result, {
|
|
320
|
+
expectedMutation: true,
|
|
321
|
+
attemptedMutation: true,
|
|
322
|
+
triggered: false,
|
|
323
|
+
});
|
|
324
|
+
});
|
|
@@ -4,20 +4,20 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { afterEach, beforeEach, describe, it } from "node:test";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { getConfigDirName,
|
|
7
|
+
import { getConfigDirName, SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV, resolveConfigDirName, resolveWatchPath } from "../../src/shared/utils.ts";
|
|
8
8
|
|
|
9
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
10
10
|
let previousPackageRootEnv: string | undefined;
|
|
11
11
|
|
|
12
12
|
describe("config directory resolution", () => {
|
|
13
13
|
beforeEach(() => {
|
|
14
|
-
previousPackageRootEnv = process.env[
|
|
15
|
-
delete process.env[
|
|
14
|
+
previousPackageRootEnv = process.env[SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
15
|
+
delete process.env[SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
afterEach(() => {
|
|
19
|
-
if (previousPackageRootEnv === undefined) delete process.env[
|
|
20
|
-
else process.env[
|
|
19
|
+
if (previousPackageRootEnv === undefined) delete process.env[SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV];
|
|
20
|
+
else process.env[SELESAI_CODING_AGENT_PACKAGE_ROOT_ENV] = previousPackageRootEnv;
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("falls back without importing the Pi peer package at runtime", () => {
|
|
@@ -65,6 +65,11 @@ describe("config directory resolution", () => {
|
|
|
65
65
|
}
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
it("canonicalizes watcher paths and preserves the original path when native realpath fails", () => {
|
|
69
|
+
assert.equal(resolveWatchPath("C:\\SHORT~1\\watch", () => "C:\\Long Path\\watch"), "C:\\Long Path\\watch");
|
|
70
|
+
assert.equal(resolveWatchPath("C:\\SHORT~1\\watch", () => { throw new Error("missing"); }), "C:\\SHORT~1\\watch");
|
|
71
|
+
});
|
|
72
|
+
|
|
68
73
|
it("does not runtime-import the coding agent peer from shared utils", () => {
|
|
69
74
|
const source = fs.readFileSync(path.join(repoRoot, "src/shared/utils.ts"), "utf-8");
|
|
70
75
|
assert.doesNotMatch(source, /import\s+[^;]*from\s+["']@earendil-works\/pi-coding-agent["']/);
|
|
@@ -4,13 +4,25 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { describe, it } from "node:test";
|
|
6
6
|
import {
|
|
7
|
+
closeSteerInbox,
|
|
7
8
|
consumeInterruptRequest,
|
|
9
|
+
consumeSteerAcks,
|
|
10
|
+
consumeSteerCapabilities,
|
|
8
11
|
consumeSteerRequests,
|
|
12
|
+
consumeStopRequest,
|
|
9
13
|
deliverInterruptRequest,
|
|
10
14
|
enqueueStepSteer,
|
|
11
15
|
interruptRequestPath,
|
|
12
16
|
requestAsyncInterrupt,
|
|
13
17
|
requestAsyncSteer,
|
|
18
|
+
requestAsyncStop,
|
|
19
|
+
steerAckPathFromDir,
|
|
20
|
+
steerAcksDir,
|
|
21
|
+
steerInboxClosedPath,
|
|
22
|
+
steerCapabilityPath,
|
|
23
|
+
writeSteerAck,
|
|
24
|
+
writeSteerCapability,
|
|
25
|
+
stopRequestPath,
|
|
14
26
|
steerRequestsDir,
|
|
15
27
|
stepSteerInboxDir,
|
|
16
28
|
watchAsyncControlInbox,
|
|
@@ -40,6 +52,23 @@ describe("control channel: request file", () => {
|
|
|
40
52
|
}
|
|
41
53
|
});
|
|
42
54
|
|
|
55
|
+
it("writes and consumes a stop request", () => {
|
|
56
|
+
const asyncDir = tmpAsyncDir("pi-control-stop-");
|
|
57
|
+
try {
|
|
58
|
+
const requestPath = requestAsyncStop(asyncDir, { source: "test" }, { now: () => 1234 });
|
|
59
|
+
assert.equal(requestPath, stopRequestPath(asyncDir));
|
|
60
|
+
const data = JSON.parse(fs.readFileSync(requestPath, "utf-8"));
|
|
61
|
+
assert.equal(data.type, "stop");
|
|
62
|
+
assert.equal(data.ts, 1234);
|
|
63
|
+
assert.equal(data.source, "test");
|
|
64
|
+
assert.equal(consumeStopRequest(asyncDir), true);
|
|
65
|
+
assert.equal(fs.existsSync(stopRequestPath(asyncDir)), false);
|
|
66
|
+
assert.equal(consumeStopRequest(asyncDir), false);
|
|
67
|
+
} finally {
|
|
68
|
+
cleanup(asyncDir);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
43
72
|
it("keeps the request type authoritative even for untyped callers", () => {
|
|
44
73
|
const asyncDir = tmpAsyncDir("pi-control-write-type-");
|
|
45
74
|
try {
|
|
@@ -80,11 +109,11 @@ describe("control channel: request file", () => {
|
|
|
80
109
|
const asyncDir = tmpAsyncDir("pi-control-steer-");
|
|
81
110
|
try {
|
|
82
111
|
requestAsyncSteer(asyncDir, { message: " later guidance ", targetIndex: 1, id: "b", ts: 200, source: "test" });
|
|
83
|
-
requestAsyncSteer(asyncDir, { message: "first guidance", id: "a", ts: 100 });
|
|
112
|
+
requestAsyncSteer(asyncDir, { message: "first guidance", targetIndexes: [0, 2], id: "a", ts: 100 });
|
|
84
113
|
assert.equal(fs.readdirSync(steerRequestsDir(asyncDir)).length, 2);
|
|
85
114
|
|
|
86
115
|
assert.deepEqual(consumeSteerRequests(asyncDir), [
|
|
87
|
-
{ type: "steer", id: "a", ts: 100, message: "first guidance" },
|
|
116
|
+
{ type: "steer", id: "a", ts: 100, message: "first guidance", targetIndexes: [0, 2] },
|
|
88
117
|
{ type: "steer", id: "b", ts: 200, message: "later guidance", targetIndex: 1, source: "test" },
|
|
89
118
|
]);
|
|
90
119
|
assert.deepEqual(consumeSteerRequests(asyncDir), []);
|
|
@@ -93,6 +122,18 @@ describe("control channel: request file", () => {
|
|
|
93
122
|
}
|
|
94
123
|
});
|
|
95
124
|
|
|
125
|
+
it("rejects requests after the runner closes the steering inbox", () => {
|
|
126
|
+
const asyncDir = tmpAsyncDir("pi-control-steer-closed-");
|
|
127
|
+
try {
|
|
128
|
+
closeSteerInbox(asyncDir, "complete");
|
|
129
|
+
assert.equal(fs.existsSync(steerInboxClosedPath(asyncDir)), true);
|
|
130
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "too late", targetIndexes: [0] }), /no longer accepts steering/);
|
|
131
|
+
assert.equal(fs.existsSync(steerRequestsDir(asyncDir)), false);
|
|
132
|
+
} finally {
|
|
133
|
+
cleanup(asyncDir);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
96
137
|
it("keeps steer request ids out of filesystem paths", () => {
|
|
97
138
|
const asyncDir = tmpAsyncDir("pi-control-steer-safe-name-");
|
|
98
139
|
try {
|
|
@@ -132,9 +173,10 @@ describe("control channel: request file", () => {
|
|
|
132
173
|
it("enqueues a steer request for a specific child inbox", () => {
|
|
133
174
|
const asyncDir = tmpAsyncDir("pi-control-step-steer-");
|
|
134
175
|
try {
|
|
135
|
-
enqueueStepSteer(asyncDir, 2, { type: "steer", id: "s1", ts: 300, message: "focus",
|
|
176
|
+
enqueueStepSteer(asyncDir, 2, { type: "steer", id: "s1", ts: 300, message: "focus", targetIndexes: [0, 1] });
|
|
136
177
|
const request = JSON.parse(fs.readFileSync(path.join(stepSteerInboxDir(asyncDir, 2), fs.readdirSync(stepSteerInboxDir(asyncDir, 2))[0]!), "utf-8"));
|
|
137
178
|
assert.equal(request.targetIndex, 2);
|
|
179
|
+
assert.equal(request.targetIndexes, undefined);
|
|
138
180
|
assert.equal(request.message, "focus");
|
|
139
181
|
} finally {
|
|
140
182
|
cleanup(asyncDir);
|
|
@@ -146,6 +188,44 @@ describe("control channel: request file", () => {
|
|
|
146
188
|
try {
|
|
147
189
|
assert.throws(() => requestAsyncSteer(asyncDir, { message: " " }), /steer message must not be empty/);
|
|
148
190
|
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndex: -1 }), /targetIndex/);
|
|
191
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndex: 1_000_001 }), /targetIndex/);
|
|
192
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndexes: [] }), /targetIndexes/);
|
|
193
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndexes: [0, 0] }), /targetIndexes/);
|
|
194
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndex: 0, targetIndexes: [0] }), /targetIndexes/);
|
|
195
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", targetIndexes: "bad" as unknown as number[] }), /targetIndexes/);
|
|
196
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "x".repeat(128 * 1024 + 1) }), /exceeds/);
|
|
197
|
+
assert.throws(() => requestAsyncSteer(asyncDir, { message: "ok", id: "contains whitespace" }), /malformed/);
|
|
198
|
+
} finally {
|
|
199
|
+
cleanup(asyncDir);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("writes strict capabilities and acknowledgments with safe paths", () => {
|
|
204
|
+
const asyncDir = tmpAsyncDir("pi-control-steer-ack-");
|
|
205
|
+
try {
|
|
206
|
+
const capabilityPath = writeSteerCapability(asyncDir, { index: 0, pid: 42, readyAt: 100, supported: true });
|
|
207
|
+
assert.equal(capabilityPath, steerCapabilityPath(asyncDir, 0));
|
|
208
|
+
writeSteerAck(asyncDir, { requestId: "../request", index: 0, ts: 101, state: "delivered", message: "accepted" });
|
|
209
|
+
assert.equal(path.dirname(steerAckPathFromDir(steerAcksDir(asyncDir, 0), "../request")), steerAcksDir(asyncDir, 0));
|
|
210
|
+
assert.deepEqual(consumeSteerCapabilities(asyncDir), [{ type: "steer-capability", protocolVersion: 1, index: 0, pid: 42, readyAt: 100, supported: true }]);
|
|
211
|
+
assert.deepEqual(consumeSteerAcks(asyncDir), [{ type: "steer-ack", protocolVersion: 1, requestId: "../request", index: 0, ts: 101, state: "delivered", message: "accepted" }]);
|
|
212
|
+
assert.deepEqual(consumeSteerAcks(asyncDir), []);
|
|
213
|
+
} finally {
|
|
214
|
+
cleanup(asyncDir);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("ignores malformed capabilities and acknowledgments", () => {
|
|
219
|
+
const asyncDir = tmpAsyncDir("pi-control-steer-malformed-");
|
|
220
|
+
try {
|
|
221
|
+
fs.mkdirSync(path.join(asyncDir, "control", "steer-capabilities"), { recursive: true });
|
|
222
|
+
fs.writeFileSync(steerCapabilityPath(asyncDir, 0), JSON.stringify({ type: "steer-capability", protocolVersion: 99 }), "utf-8");
|
|
223
|
+
fs.mkdirSync(steerAcksDir(asyncDir, 0), { recursive: true });
|
|
224
|
+
fs.writeFileSync(path.join(steerAcksDir(asyncDir, 0), "bad.json"), JSON.stringify({ type: "steer-ack", protocolVersion: 1, requestId: "", index: 0, ts: 1, state: "delivered", message: "bad" }), "utf-8");
|
|
225
|
+
fs.writeFileSync(path.join(asyncDir, "control", "steer-acks", "1"), "not a directory", "utf-8");
|
|
226
|
+
writeSteerAck(asyncDir, { requestId: "valid", index: 2, ts: 2, state: "delivered", message: "accepted" });
|
|
227
|
+
assert.deepEqual(consumeSteerCapabilities(asyncDir), []);
|
|
228
|
+
assert.deepEqual(consumeSteerAcks(asyncDir), [{ type: "steer-ack", protocolVersion: 1, requestId: "valid", index: 2, ts: 2, state: "delivered", message: "accepted" }]);
|
|
149
229
|
} finally {
|
|
150
230
|
cleanup(asyncDir);
|
|
151
231
|
}
|
|
@@ -234,18 +314,24 @@ describe("control channel: watchAsyncControlInbox", () => {
|
|
|
234
314
|
timers: import("../../src/runs/background/control-channel.ts").ControlChannelTimers;
|
|
235
315
|
trigger: () => void;
|
|
236
316
|
closed: () => boolean;
|
|
317
|
+
watchedDir: () => string | undefined;
|
|
237
318
|
};
|
|
238
319
|
|
|
239
|
-
function harness(): WatchHarness {
|
|
320
|
+
function harness(nativeDir?: string): WatchHarness {
|
|
240
321
|
let listener: (() => void) | undefined;
|
|
241
322
|
let closed = false;
|
|
323
|
+
let watchedDir: string | undefined;
|
|
324
|
+
const realpathSync = ((target: fs.PathLike, options?: unknown) => fs.realpathSync(target, options as BufferEncoding)) as typeof fs.realpathSync;
|
|
325
|
+
realpathSync.native = ((target: fs.PathLike) => nativeDir ?? fs.realpathSync.native(target)) as typeof fs.realpathSync.native;
|
|
242
326
|
const fsImpl = {
|
|
243
327
|
mkdirSync: fs.mkdirSync,
|
|
244
328
|
existsSync: fs.existsSync,
|
|
245
329
|
rmSync: fs.rmSync,
|
|
246
330
|
readdirSync: fs.readdirSync,
|
|
247
331
|
readFileSync: fs.readFileSync,
|
|
248
|
-
|
|
332
|
+
realpathSync,
|
|
333
|
+
watch: ((dir: string, cb: () => void) => {
|
|
334
|
+
watchedDir = dir;
|
|
249
335
|
listener = cb;
|
|
250
336
|
return { close: () => { closed = true; }, on: () => {} };
|
|
251
337
|
}),
|
|
@@ -254,9 +340,22 @@ describe("control channel: watchAsyncControlInbox", () => {
|
|
|
254
340
|
setInterval: (() => ({ unref() {} })) as unknown as typeof setInterval,
|
|
255
341
|
clearInterval: (() => {}) as unknown as typeof clearInterval,
|
|
256
342
|
};
|
|
257
|
-
return { fsImpl, timers, trigger: () => listener?.(), closed: () => closed };
|
|
343
|
+
return { fsImpl, timers, trigger: () => listener?.(), closed: () => closed, watchedDir: () => watchedDir };
|
|
258
344
|
}
|
|
259
345
|
|
|
346
|
+
it("registers the native canonical control inbox path", () => {
|
|
347
|
+
const asyncDir = tmpAsyncDir("pi-control-watch-native-");
|
|
348
|
+
try {
|
|
349
|
+
const nativeDir = path.join(path.dirname(asyncDir), "native-control-path");
|
|
350
|
+
const h = harness(nativeDir);
|
|
351
|
+
const dispose = watchAsyncControlInbox(asyncDir, { onInterrupt() {}, fs: h.fsImpl, timers: h.timers });
|
|
352
|
+
assert.equal(h.watchedDir(), nativeDir);
|
|
353
|
+
dispose();
|
|
354
|
+
} finally {
|
|
355
|
+
cleanup(asyncDir);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
260
359
|
it("fires on a request that arrived before the watcher started", () => {
|
|
261
360
|
const asyncDir = tmpAsyncDir("pi-control-watch-early-");
|
|
262
361
|
try {
|
|
@@ -301,6 +400,27 @@ describe("control channel: watchAsyncControlInbox", () => {
|
|
|
301
400
|
}
|
|
302
401
|
});
|
|
303
402
|
|
|
403
|
+
it("delivers stop requests before interrupt requests", () => {
|
|
404
|
+
const asyncDir = tmpAsyncDir("pi-control-watch-stop-early-");
|
|
405
|
+
try {
|
|
406
|
+
requestAsyncStop(asyncDir);
|
|
407
|
+
requestAsyncInterrupt(asyncDir);
|
|
408
|
+
const events: string[] = [];
|
|
409
|
+
const h = harness();
|
|
410
|
+
const dispose = watchAsyncControlInbox(asyncDir, {
|
|
411
|
+
onInterrupt: () => events.push("interrupt"),
|
|
412
|
+
onStop: () => events.push("stop"),
|
|
413
|
+
fs: h.fsImpl,
|
|
414
|
+
timers: h.timers,
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
assert.deepEqual(events, ["stop", "interrupt"]);
|
|
418
|
+
dispose();
|
|
419
|
+
} finally {
|
|
420
|
+
cleanup(asyncDir);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
|
|
304
424
|
it("delivers steer requests without firing interrupt", () => {
|
|
305
425
|
const asyncDir = tmpAsyncDir("pi-control-watch-steer-");
|
|
306
426
|
try {
|