@selesai/code 0.5.19 → 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-intercom/broker/spawn.test.ts +31 -0
- package/dist/extensions/pi-intercom/broker/spawn.ts +5 -3
- 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 +2 -1
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } 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 { DEFAULT_WATCHDOG_CONFIG } from "../../src/watchdog/settings.ts";
|
|
8
|
+
import { MainWatchdogRuntime, type WatchdogReviewFunction } from "../../src/watchdog/runtime.ts";
|
|
9
|
+
import type { ResolvedWatchdogConfig, WatchdogLspResult, WatchdogSettingsResult, WatchdogWarning } from "../../src/watchdog/types.ts";
|
|
10
|
+
|
|
11
|
+
function cloneConfig(): ResolvedWatchdogConfig {
|
|
12
|
+
return {
|
|
13
|
+
...DEFAULT_WATCHDOG_CONFIG,
|
|
14
|
+
guidance: { ...DEFAULT_WATCHDOG_CONFIG.guidance },
|
|
15
|
+
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.autoFollow },
|
|
16
|
+
main: { ...DEFAULT_WATCHDOG_CONFIG.main },
|
|
17
|
+
children: {
|
|
18
|
+
...DEFAULT_WATCHDOG_CONFIG.children,
|
|
19
|
+
autoFollow: { ...DEFAULT_WATCHDOG_CONFIG.children.autoFollow },
|
|
20
|
+
overrides: { ...DEFAULT_WATCHDOG_CONFIG.children.overrides },
|
|
21
|
+
},
|
|
22
|
+
asyncCompletion: { ...DEFAULT_WATCHDOG_CONFIG.asyncCompletion },
|
|
23
|
+
lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp },
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function configResult(config: ResolvedWatchdogConfig): WatchdogSettingsResult {
|
|
28
|
+
return { ok: true, config, errors: [], sources: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function enabledConfig(overrides: Partial<ResolvedWatchdogConfig> = {}): ResolvedWatchdogConfig {
|
|
32
|
+
const config = cloneConfig();
|
|
33
|
+
config.enabled = true;
|
|
34
|
+
config.main.enabled = true;
|
|
35
|
+
config.lsp.enabled = false;
|
|
36
|
+
Object.assign(config, overrides);
|
|
37
|
+
if (overrides.main) config.main = { ...config.main, ...overrides.main };
|
|
38
|
+
if (overrides.lsp) config.lsp = { ...config.lsp, ...overrides.lsp };
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function warning(): WatchdogWarning {
|
|
43
|
+
return {
|
|
44
|
+
severity: "concern",
|
|
45
|
+
summary: "Runtime concern",
|
|
46
|
+
evidence: "The runtime test emitted a concern.",
|
|
47
|
+
recommendedAction: "Review the displayed warning before accepting the turn.",
|
|
48
|
+
source: "main",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tick(): Promise<void> {
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function git(cwd: string, args: string[]): string {
|
|
57
|
+
const result = spawnSync("git", ["-C", cwd, ...args], { encoding: "utf-8" });
|
|
58
|
+
if (result.status !== 0) throw new Error(result.stderr.trim() || result.stdout.trim() || `git ${args.join(" ")} failed`);
|
|
59
|
+
return result.stdout.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function createRepo(): string {
|
|
63
|
+
const repo = fs.mkdtempSync(path.join(os.tmpdir(), "watchdog-runtime-"));
|
|
64
|
+
git(repo, ["init"]);
|
|
65
|
+
git(repo, ["config", "user.email", "watchdog@example.com"]);
|
|
66
|
+
git(repo, ["config", "user.name", "Watchdog Tests"]);
|
|
67
|
+
fs.mkdirSync(path.join(repo, "src"));
|
|
68
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 1;\n", "utf-8");
|
|
69
|
+
git(repo, ["add", "-A"]);
|
|
70
|
+
git(repo, ["commit", "-m", "initial"]);
|
|
71
|
+
return repo;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("main watchdog runtime", () => {
|
|
75
|
+
it("does not inspect the repository until the watchdog is enabled", () => {
|
|
76
|
+
let enabled = false;
|
|
77
|
+
let signatureCalls = 0;
|
|
78
|
+
const runtime = new MainWatchdogRuntime({
|
|
79
|
+
cwd: "/tmp/project",
|
|
80
|
+
resolveConfig: () => configResult(enabled ? enabledConfig() : cloneConfig()),
|
|
81
|
+
reviewChangesOnly: true,
|
|
82
|
+
repoChangeSignature: () => {
|
|
83
|
+
signatureCalls++;
|
|
84
|
+
return { root: "/tmp/project", key: "signature", changedPaths: [] };
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.equal(signatureCalls, 0);
|
|
89
|
+
runtime.handleBeforeAgentStart({ prompt: "disabled" }, { cwd: "/tmp/project" });
|
|
90
|
+
assert.equal(signatureCalls, 0);
|
|
91
|
+
|
|
92
|
+
enabled = true;
|
|
93
|
+
runtime.handleBeforeAgentStart({ prompt: "enabled" }, { cwd: "/tmp/project" });
|
|
94
|
+
assert.equal(signatureCalls, 1);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("does not use stale enabled config to inspect a newly bound default-off session", () => {
|
|
98
|
+
let signatureCalls = 0;
|
|
99
|
+
const runtime = new MainWatchdogRuntime({
|
|
100
|
+
cwd: "/tmp/enabled",
|
|
101
|
+
resolveConfig: (cwd) => configResult(cwd === "/tmp/enabled" ? enabledConfig() : cloneConfig()),
|
|
102
|
+
reviewChangesOnly: true,
|
|
103
|
+
repoChangeSignature: (cwd) => {
|
|
104
|
+
signatureCalls++;
|
|
105
|
+
return { root: cwd, key: cwd, changedPaths: [] };
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.equal(signatureCalls, 1);
|
|
110
|
+
runtime.bindSession({ cwd: "/tmp/default-off" });
|
|
111
|
+
assert.equal(signatureCalls, 1);
|
|
112
|
+
assert.equal(runtime.getSnapshot().enabled, false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("stays default-off and contains invalid config at the watchdog boundary", () => {
|
|
116
|
+
let reviewCalls = 0;
|
|
117
|
+
const runtime = new MainWatchdogRuntime({
|
|
118
|
+
resolveConfig: () => ({
|
|
119
|
+
ok: false,
|
|
120
|
+
config: cloneConfig(),
|
|
121
|
+
errors: [{ scope: "user", path: "/tmp/settings.json", message: "bad watchdog config" }],
|
|
122
|
+
sources: [{ scope: "user", path: "/tmp/settings.json", exists: true }],
|
|
123
|
+
}),
|
|
124
|
+
review: () => {
|
|
125
|
+
reviewCalls++;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
runtime.handleTurnEnd({ type: "turn_end", message: { role: "assistant", content: "Done." }, toolResults: [] }, { cwd: "/tmp/project" });
|
|
130
|
+
|
|
131
|
+
const snapshot = runtime.getSnapshot();
|
|
132
|
+
assert.equal(snapshot.enabled, false);
|
|
133
|
+
assert.equal(snapshot.configOk, false);
|
|
134
|
+
assert.equal(snapshot.status, "idle");
|
|
135
|
+
assert.equal(reviewCalls, 0);
|
|
136
|
+
assert.equal(snapshot.errors[0]?.message, "bad watchdog config");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("queues turn deltas, reviews once at agent_end, displays one warning, and returns idle", async () => {
|
|
140
|
+
let releaseReview!: () => void;
|
|
141
|
+
let started!: () => void;
|
|
142
|
+
const displayed: unknown[] = [];
|
|
143
|
+
const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
|
|
144
|
+
const review: WatchdogReviewFunction = async (request) => {
|
|
145
|
+
started();
|
|
146
|
+
await new Promise<void>((resolve) => { releaseReview = resolve; });
|
|
147
|
+
assert.equal(request.delta, "Assistant:\nWorking");
|
|
148
|
+
assert.equal(request.emitWarning(warning()), true);
|
|
149
|
+
return { stopReason: "stop" };
|
|
150
|
+
};
|
|
151
|
+
const runtime = new MainWatchdogRuntime({
|
|
152
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
153
|
+
review,
|
|
154
|
+
displayWarning: (details) => displayed.push(details),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
runtime.enqueueDelta("Assistant:\nWorking");
|
|
158
|
+
assert.equal(runtime.getSnapshot().status, "queued");
|
|
159
|
+
const end = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
160
|
+
await reviewStarted;
|
|
161
|
+
assert.equal(runtime.getSnapshot().status, "reviewing");
|
|
162
|
+
|
|
163
|
+
releaseReview();
|
|
164
|
+
await end;
|
|
165
|
+
|
|
166
|
+
const snapshot = runtime.getSnapshot();
|
|
167
|
+
assert.equal(snapshot.status, "idle");
|
|
168
|
+
assert.equal(snapshot.lastWarning?.state, "displayed");
|
|
169
|
+
assert.equal(displayed.length, 1);
|
|
170
|
+
assert.equal((displayed[0] as { state?: string }).state, "displayed");
|
|
171
|
+
assert.equal(snapshot.autoFollowQueued, false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("drops stale async warning callbacks after reset", async () => {
|
|
175
|
+
let emitWarning!: (candidate: WatchdogWarning) => boolean;
|
|
176
|
+
let finishReview!: () => void;
|
|
177
|
+
let started!: () => void;
|
|
178
|
+
const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
|
|
179
|
+
const review: WatchdogReviewFunction = async (request) => {
|
|
180
|
+
emitWarning = request.emitWarning;
|
|
181
|
+
started();
|
|
182
|
+
await new Promise<void>((resolve) => { finishReview = resolve; });
|
|
183
|
+
};
|
|
184
|
+
const runtime = new MainWatchdogRuntime({ resolveConfig: () => configResult(enabledConfig()), review });
|
|
185
|
+
|
|
186
|
+
runtime.enqueueDelta("Assistant:\nWorking");
|
|
187
|
+
const end = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
188
|
+
await reviewStarted;
|
|
189
|
+
const epoch = runtime.getSnapshot().epoch;
|
|
190
|
+
runtime.reset("test reset");
|
|
191
|
+
|
|
192
|
+
assert.equal(emitWarning(warning()), false);
|
|
193
|
+
finishReview();
|
|
194
|
+
await end;
|
|
195
|
+
await tick();
|
|
196
|
+
|
|
197
|
+
const snapshot = runtime.getSnapshot();
|
|
198
|
+
assert.equal(snapshot.epoch, epoch + 1);
|
|
199
|
+
assert.equal(snapshot.status, "idle");
|
|
200
|
+
assert.equal(snapshot.lastWarning, undefined);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("treats failed review stop reasons as failed and never auto-follows", async () => {
|
|
204
|
+
const runtime = new MainWatchdogRuntime({
|
|
205
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
206
|
+
review: () => ({ stopReason: "length" }),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
runtime.enqueueDelta("Assistant:\nWorking");
|
|
210
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
211
|
+
|
|
212
|
+
const snapshot = runtime.getSnapshot();
|
|
213
|
+
assert.equal(snapshot.status, "failed");
|
|
214
|
+
assert.match(snapshot.lastError ?? "", /stop reason 'length'/);
|
|
215
|
+
assert.equal(snapshot.failedReviews, 1);
|
|
216
|
+
assert.equal(snapshot.autoFollowQueued, false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("marks unresolved agent-end review work stale on timeout", async () => {
|
|
220
|
+
let started!: () => void;
|
|
221
|
+
const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
|
|
222
|
+
const runtime = new MainWatchdogRuntime({
|
|
223
|
+
resolveConfig: () => configResult(enabledConfig({ agentEndTimeoutMs: 5 })),
|
|
224
|
+
review: async () => {
|
|
225
|
+
started();
|
|
226
|
+
await new Promise<void>(() => {});
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
runtime.enqueueDelta("Assistant:\nStill working");
|
|
231
|
+
const end = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
232
|
+
await reviewStarted;
|
|
233
|
+
await end;
|
|
234
|
+
|
|
235
|
+
const snapshot = runtime.getSnapshot();
|
|
236
|
+
assert.equal(snapshot.status, "stale");
|
|
237
|
+
assert.equal(snapshot.staleReviews, 1);
|
|
238
|
+
assert.equal(snapshot.autoFollowQueued, false);
|
|
239
|
+
runtime.reset("cleanup");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("aborts timed-out review work before another agent-end review can overlap", async () => {
|
|
243
|
+
let activeReviews = 0;
|
|
244
|
+
let maxActiveReviews = 0;
|
|
245
|
+
let aborts = 0;
|
|
246
|
+
const runtime = new MainWatchdogRuntime({
|
|
247
|
+
resolveConfig: () => configResult(enabledConfig({ agentEndTimeoutMs: 5 })),
|
|
248
|
+
review: async (request) => {
|
|
249
|
+
activeReviews++;
|
|
250
|
+
maxActiveReviews = Math.max(maxActiveReviews, activeReviews);
|
|
251
|
+
await new Promise<void>((resolve) => {
|
|
252
|
+
request.signal?.addEventListener("abort", () => {
|
|
253
|
+
aborts++;
|
|
254
|
+
resolve();
|
|
255
|
+
}, { once: true });
|
|
256
|
+
});
|
|
257
|
+
activeReviews--;
|
|
258
|
+
return { stopReason: "aborted" };
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
runtime.enqueueDelta("Assistant:\nFirst review");
|
|
263
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
264
|
+
await tick();
|
|
265
|
+
runtime.enqueueDelta("Assistant:\nSecond review");
|
|
266
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
267
|
+
await tick();
|
|
268
|
+
|
|
269
|
+
assert.equal(aborts, 2);
|
|
270
|
+
assert.equal(activeReviews, 0);
|
|
271
|
+
assert.equal(maxActiveReviews, 1);
|
|
272
|
+
assert.equal(runtime.getSnapshot().staleReviews, 2);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("drops late warning callbacks after agent-end timeout marks a review stale", async () => {
|
|
276
|
+
let emitWarning!: (candidate: WatchdogWarning) => boolean;
|
|
277
|
+
let finishReview!: () => void;
|
|
278
|
+
let started!: () => void;
|
|
279
|
+
const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
|
|
280
|
+
const runtime = new MainWatchdogRuntime({
|
|
281
|
+
resolveConfig: () => configResult(enabledConfig({ agentEndTimeoutMs: 5 })),
|
|
282
|
+
review: async (request) => {
|
|
283
|
+
emitWarning = request.emitWarning;
|
|
284
|
+
started();
|
|
285
|
+
await new Promise<void>((resolve) => { finishReview = resolve; });
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
runtime.enqueueDelta("Assistant:\nStill working");
|
|
290
|
+
const end = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
291
|
+
await reviewStarted;
|
|
292
|
+
await end;
|
|
293
|
+
|
|
294
|
+
assert.equal(emitWarning(warning()), false);
|
|
295
|
+
finishReview();
|
|
296
|
+
await tick();
|
|
297
|
+
|
|
298
|
+
const snapshot = runtime.getSnapshot();
|
|
299
|
+
assert.equal(snapshot.status, "stale");
|
|
300
|
+
assert.equal(snapshot.lastWarning, undefined);
|
|
301
|
+
assert.equal(snapshot.autoFollowQueued, false);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("invalidates an in-flight review when refreshed config disables the watchdog", async () => {
|
|
305
|
+
let enabled = true;
|
|
306
|
+
let emitWarning!: (candidate: WatchdogWarning) => boolean;
|
|
307
|
+
let finishReview!: () => void;
|
|
308
|
+
let started!: () => void;
|
|
309
|
+
const reviewStarted = new Promise<void>((resolve) => { started = resolve; });
|
|
310
|
+
const runtime = new MainWatchdogRuntime({
|
|
311
|
+
resolveConfig: () => configResult(enabled ? enabledConfig() : cloneConfig()),
|
|
312
|
+
review: async (request) => {
|
|
313
|
+
emitWarning = request.emitWarning;
|
|
314
|
+
started();
|
|
315
|
+
await new Promise<void>((resolve) => { finishReview = resolve; });
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
runtime.enqueueDelta("Assistant:\nWorking");
|
|
320
|
+
const end = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
321
|
+
await reviewStarted;
|
|
322
|
+
enabled = false;
|
|
323
|
+
runtime.refreshConfig("/tmp/project");
|
|
324
|
+
|
|
325
|
+
assert.equal(runtime.getSnapshot().enabled, false);
|
|
326
|
+
assert.equal(runtime.getSnapshot().status, "idle");
|
|
327
|
+
assert.equal(emitWarning(warning()), false);
|
|
328
|
+
finishReview();
|
|
329
|
+
await end;
|
|
330
|
+
await tick();
|
|
331
|
+
|
|
332
|
+
assert.equal(runtime.getSnapshot().lastWarning, undefined);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("bounds review input before calling the reviewer", async () => {
|
|
336
|
+
let reviewedDelta = "";
|
|
337
|
+
const runtime = new MainWatchdogRuntime({
|
|
338
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
339
|
+
review: (request) => {
|
|
340
|
+
reviewedDelta = request.delta;
|
|
341
|
+
return { stopReason: "stop" };
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
runtime.enqueueDelta(`Assistant:\n${"x".repeat(30_000)}`);
|
|
346
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
347
|
+
|
|
348
|
+
assert.equal(reviewedDelta.length, 24_000);
|
|
349
|
+
assert.match(reviewedDelta, /^x+$/);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it("does not record duplicate signatures for stale invalidated reviews", async () => {
|
|
353
|
+
let reviewCalls = 0;
|
|
354
|
+
let finishFirstReview!: () => void;
|
|
355
|
+
let firstReviewStarted!: () => void;
|
|
356
|
+
const firstStarted = new Promise<void>((resolve) => { firstReviewStarted = resolve; });
|
|
357
|
+
const runtime = new MainWatchdogRuntime({
|
|
358
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
359
|
+
review: async () => {
|
|
360
|
+
reviewCalls++;
|
|
361
|
+
if (reviewCalls === 1) {
|
|
362
|
+
firstReviewStarted();
|
|
363
|
+
await new Promise<void>((resolve) => { finishFirstReview = resolve; });
|
|
364
|
+
}
|
|
365
|
+
return { stopReason: "stop" };
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
runtime.enqueueDelta("Assistant:\nSame work");
|
|
370
|
+
const firstEnd = runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
371
|
+
await firstStarted;
|
|
372
|
+
runtime.reset("test invalidation");
|
|
373
|
+
finishFirstReview();
|
|
374
|
+
await firstEnd;
|
|
375
|
+
|
|
376
|
+
runtime.enqueueDelta("Assistant:\nSame work");
|
|
377
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
378
|
+
|
|
379
|
+
assert.equal(reviewCalls, 2);
|
|
380
|
+
assert.equal(runtime.getSnapshot().status, "idle");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it("skips duplicate bounded review input within the session", async () => {
|
|
384
|
+
let reviewCalls = 0;
|
|
385
|
+
const runtime = new MainWatchdogRuntime({
|
|
386
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
387
|
+
review: () => {
|
|
388
|
+
reviewCalls++;
|
|
389
|
+
return { stopReason: "stop" };
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
runtime.enqueueDelta("Assistant:\nSame work");
|
|
394
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
395
|
+
runtime.enqueueDelta("Assistant:\nSame work");
|
|
396
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
397
|
+
|
|
398
|
+
assert.equal(reviewCalls, 1);
|
|
399
|
+
assert.equal(runtime.getSnapshot().status, "idle");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("skips edit-gated watchdog reviews when no repo changes occurred", async () => {
|
|
403
|
+
const repo = createRepo();
|
|
404
|
+
let reviewCalls = 0;
|
|
405
|
+
const runtime = new MainWatchdogRuntime({
|
|
406
|
+
cwd: repo,
|
|
407
|
+
reviewChangesOnly: true,
|
|
408
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
409
|
+
review: () => {
|
|
410
|
+
reviewCalls++;
|
|
411
|
+
return { stopReason: "stop" };
|
|
412
|
+
},
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
runtime.handleBeforeAgentStart({ prompt: "Explain the file." }, { cwd: repo });
|
|
416
|
+
runtime.enqueueDelta("Assistant:\nI inspected the repo without editing.");
|
|
417
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
418
|
+
|
|
419
|
+
assert.equal(reviewCalls, 0);
|
|
420
|
+
assert.equal(runtime.getSnapshot(repo).reviewTrigger, "repo-edits");
|
|
421
|
+
assert.equal(runtime.getSnapshot(repo).status, "idle");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it("coalesces multiple repo edits in one turn into one edit-gated review", async () => {
|
|
425
|
+
const repo = createRepo();
|
|
426
|
+
let reviewCalls = 0;
|
|
427
|
+
let reviewedDelta = "";
|
|
428
|
+
const runtime = new MainWatchdogRuntime({
|
|
429
|
+
cwd: repo,
|
|
430
|
+
reviewChangesOnly: true,
|
|
431
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
432
|
+
review: (request) => {
|
|
433
|
+
reviewCalls++;
|
|
434
|
+
reviewedDelta = request.delta;
|
|
435
|
+
return { stopReason: "stop" };
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
440
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 2;\n", "utf-8");
|
|
441
|
+
fs.writeFileSync(path.join(repo, "src", "other.ts"), "export const other = true;\n", "utf-8");
|
|
442
|
+
runtime.enqueueDelta("Assistant:\nEdited two files.");
|
|
443
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
444
|
+
|
|
445
|
+
assert.equal(reviewCalls, 1);
|
|
446
|
+
assert.match(reviewedDelta, /Changed repo paths:/);
|
|
447
|
+
assert.match(reviewedDelta, /src\/file\.ts/);
|
|
448
|
+
assert.match(reviewedDelta, /src\/other\.ts/);
|
|
449
|
+
|
|
450
|
+
runtime.handleBeforeAgentStart({ prompt: "Say more." }, { cwd: repo });
|
|
451
|
+
runtime.enqueueDelta("Assistant:\nNo further edits.");
|
|
452
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
453
|
+
|
|
454
|
+
assert.equal(reviewCalls, 1);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("reviews same-path repo edits with identical turn text when content changes", async () => {
|
|
458
|
+
const repo = createRepo();
|
|
459
|
+
let reviewCalls = 0;
|
|
460
|
+
const reviewedDeltas: string[] = [];
|
|
461
|
+
const runtime = new MainWatchdogRuntime({
|
|
462
|
+
cwd: repo,
|
|
463
|
+
reviewChangesOnly: true,
|
|
464
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
465
|
+
review: (request) => {
|
|
466
|
+
reviewCalls++;
|
|
467
|
+
reviewedDeltas.push(request.delta);
|
|
468
|
+
return { stopReason: "stop" };
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
for (const value of [2, 3]) {
|
|
473
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
474
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), `export const value = ${value};\n`, "utf-8");
|
|
475
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
476
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
assert.equal(reviewCalls, 2);
|
|
480
|
+
assert.match(reviewedDeltas[1] ?? "", /Changed repo paths:/);
|
|
481
|
+
assert.match(reviewedDeltas[1] ?? "", /src\/file\.ts/);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("keeps changed repo paths when bounding large edit-gated review input", async () => {
|
|
485
|
+
const repo = createRepo();
|
|
486
|
+
let reviewedDelta = "";
|
|
487
|
+
const runtime = new MainWatchdogRuntime({
|
|
488
|
+
cwd: repo,
|
|
489
|
+
reviewChangesOnly: true,
|
|
490
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
491
|
+
review: (request) => {
|
|
492
|
+
reviewedDelta = request.delta;
|
|
493
|
+
return { stopReason: "stop" };
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
498
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 4;\n", "utf-8");
|
|
499
|
+
runtime.enqueueDelta(`Assistant:\n${"x".repeat(30_000)}`);
|
|
500
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
501
|
+
|
|
502
|
+
assert.equal(reviewedDelta.length, 24_000);
|
|
503
|
+
assert.match(reviewedDelta, /^Changed repo paths:/);
|
|
504
|
+
assert.match(reviewedDelta, /src\/file\.ts/);
|
|
505
|
+
assert.match(reviewedDelta, /x+$/);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("emits changed-file LSP errors as watchdog blockers before model review", async () => {
|
|
509
|
+
const repo = createRepo();
|
|
510
|
+
const displayed: unknown[] = [];
|
|
511
|
+
let reviewedDelta = "";
|
|
512
|
+
const runtime = new MainWatchdogRuntime({
|
|
513
|
+
cwd: repo,
|
|
514
|
+
reviewChangesOnly: true,
|
|
515
|
+
resolveConfig: () => configResult(enabledConfig({ lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp, enabled: true } })),
|
|
516
|
+
lspDiagnostics: () => ({
|
|
517
|
+
status: "ok",
|
|
518
|
+
provider: "stub-lsp",
|
|
519
|
+
checkedPaths: ["src/file.ts"],
|
|
520
|
+
skippedPaths: [],
|
|
521
|
+
diagnostics: [{
|
|
522
|
+
path: "src/file.ts",
|
|
523
|
+
line: 1,
|
|
524
|
+
column: 14,
|
|
525
|
+
severity: "error",
|
|
526
|
+
source: "typescript",
|
|
527
|
+
code: "TS2322",
|
|
528
|
+
message: "Type 'string' is not assignable to type 'number'.",
|
|
529
|
+
}],
|
|
530
|
+
}),
|
|
531
|
+
review: (request) => {
|
|
532
|
+
reviewedDelta = request.delta;
|
|
533
|
+
assert.equal(request.emitWarning({
|
|
534
|
+
...warning(),
|
|
535
|
+
summary: "Model tried to add a second warning",
|
|
536
|
+
evidence: "The same boundary already displayed an LSP warning.",
|
|
537
|
+
}), false);
|
|
538
|
+
return { stopReason: "stop" };
|
|
539
|
+
},
|
|
540
|
+
displayWarning: (details) => displayed.push(details),
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
544
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value: number = 'bad';\n", "utf-8");
|
|
545
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
546
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
547
|
+
|
|
548
|
+
assert.match(reviewedDelta, /Changed repo paths:/);
|
|
549
|
+
assert.match(reviewedDelta, /LSP diagnostics:/);
|
|
550
|
+
assert.match(reviewedDelta, /TS2322/);
|
|
551
|
+
assert.equal(displayed.length, 1);
|
|
552
|
+
assert.equal((displayed[0] as { source?: string }).source, "lsp");
|
|
553
|
+
assert.equal((displayed[0] as { severity?: string }).severity, "blocker");
|
|
554
|
+
const snapshot = runtime.getSnapshot(repo);
|
|
555
|
+
assert.equal(snapshot.lsp.status, "ok");
|
|
556
|
+
assert.equal(snapshot.lsp.diagnosticCount, 1);
|
|
557
|
+
assert.equal(snapshot.lsp.freshDiagnosticCount, 1);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("records LSP info and hints without sending them to model review", async () => {
|
|
561
|
+
const repo = createRepo();
|
|
562
|
+
const displayed: unknown[] = [];
|
|
563
|
+
let reviewedDelta = "";
|
|
564
|
+
const runtime = new MainWatchdogRuntime({
|
|
565
|
+
cwd: repo,
|
|
566
|
+
reviewChangesOnly: true,
|
|
567
|
+
resolveConfig: () => configResult(enabledConfig({ lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp, enabled: true } })),
|
|
568
|
+
lspDiagnostics: () => ({
|
|
569
|
+
status: "ok",
|
|
570
|
+
provider: "stub-lsp",
|
|
571
|
+
checkedPaths: ["src/file.ts"],
|
|
572
|
+
skippedPaths: [],
|
|
573
|
+
diagnostics: [{
|
|
574
|
+
path: "src/file.ts",
|
|
575
|
+
line: 1,
|
|
576
|
+
column: 14,
|
|
577
|
+
severity: "info",
|
|
578
|
+
source: "typescript",
|
|
579
|
+
message: "Helpful note.",
|
|
580
|
+
}, {
|
|
581
|
+
path: "src/file.ts",
|
|
582
|
+
line: 2,
|
|
583
|
+
column: 4,
|
|
584
|
+
severity: "hint",
|
|
585
|
+
source: "typescript",
|
|
586
|
+
message: "Suggestion.",
|
|
587
|
+
}],
|
|
588
|
+
}),
|
|
589
|
+
review: (request) => {
|
|
590
|
+
reviewedDelta = request.delta;
|
|
591
|
+
return { stopReason: "stop" };
|
|
592
|
+
},
|
|
593
|
+
displayWarning: (details) => displayed.push(details),
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
597
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 7;\n", "utf-8");
|
|
598
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
599
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
600
|
+
|
|
601
|
+
assert.match(reviewedDelta, /Changed repo paths:/);
|
|
602
|
+
assert.doesNotMatch(reviewedDelta, /LSP diagnostics:/);
|
|
603
|
+
assert.equal(displayed.length, 0);
|
|
604
|
+
const snapshot = runtime.getSnapshot(repo);
|
|
605
|
+
assert.equal(snapshot.lsp.diagnosticCount, 2);
|
|
606
|
+
assert.equal(snapshot.lsp.freshDiagnosticCount, 2);
|
|
607
|
+
assert.deepEqual(snapshot.lsp.diagnostics.map((diagnostic) => diagnostic.severity), ["info", "hint"]);
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("aborts and ignores stale LSP diagnostics after reset", async () => {
|
|
611
|
+
const repo = createRepo();
|
|
612
|
+
const displayed: unknown[] = [];
|
|
613
|
+
let lspStarted!: () => void;
|
|
614
|
+
const lspStartedPromise = new Promise<void>((resolve) => { lspStarted = resolve; });
|
|
615
|
+
let signalAborted = false;
|
|
616
|
+
let reviewCalls = 0;
|
|
617
|
+
const runtime = new MainWatchdogRuntime({
|
|
618
|
+
cwd: repo,
|
|
619
|
+
reviewChangesOnly: true,
|
|
620
|
+
resolveConfig: () => configResult(enabledConfig({ lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp, enabled: true } })),
|
|
621
|
+
lspDiagnostics: (request) => new Promise((resolve) => {
|
|
622
|
+
request.signal?.addEventListener("abort", () => {
|
|
623
|
+
signalAborted = true;
|
|
624
|
+
resolve({
|
|
625
|
+
status: "ok",
|
|
626
|
+
provider: "stub-lsp",
|
|
627
|
+
checkedPaths: ["src/file.ts"],
|
|
628
|
+
skippedPaths: [],
|
|
629
|
+
diagnostics: [{
|
|
630
|
+
path: "src/file.ts",
|
|
631
|
+
line: 1,
|
|
632
|
+
column: 14,
|
|
633
|
+
severity: "error",
|
|
634
|
+
source: "typescript",
|
|
635
|
+
code: "TS2322",
|
|
636
|
+
message: "Type 'string' is not assignable to type 'number'.",
|
|
637
|
+
}],
|
|
638
|
+
});
|
|
639
|
+
}, { once: true });
|
|
640
|
+
lspStarted();
|
|
641
|
+
}),
|
|
642
|
+
review: () => {
|
|
643
|
+
reviewCalls++;
|
|
644
|
+
return { stopReason: "stop" };
|
|
645
|
+
},
|
|
646
|
+
displayWarning: (details) => displayed.push(details),
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
650
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value: number = 'bad';\n", "utf-8");
|
|
651
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
652
|
+
const end = runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
653
|
+
await lspStartedPromise;
|
|
654
|
+
runtime.reset("test reset");
|
|
655
|
+
await end;
|
|
656
|
+
|
|
657
|
+
assert.equal(signalAborted, true);
|
|
658
|
+
assert.equal(displayed.length, 0);
|
|
659
|
+
assert.equal(reviewCalls, 0);
|
|
660
|
+
assert.equal(runtime.getSnapshot(repo).status, "idle");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it("clears pending deltas when an edit-gated signature was already reviewed", async () => {
|
|
664
|
+
const repo = createRepo();
|
|
665
|
+
let reviewCalls = 0;
|
|
666
|
+
const runtime = new MainWatchdogRuntime({
|
|
667
|
+
cwd: repo,
|
|
668
|
+
reviewChangesOnly: true,
|
|
669
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
670
|
+
review: () => {
|
|
671
|
+
reviewCalls++;
|
|
672
|
+
return { stopReason: "stop" };
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
677
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 2;\n", "utf-8");
|
|
678
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
679
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
680
|
+
|
|
681
|
+
runtime.enqueueDelta("Assistant:\nDiscussed the already-reviewed edit.");
|
|
682
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
683
|
+
|
|
684
|
+
const snapshot = runtime.getSnapshot(repo);
|
|
685
|
+
assert.equal(reviewCalls, 1);
|
|
686
|
+
assert.equal(snapshot.status, "idle");
|
|
687
|
+
assert.equal(snapshot.bufferedDeltas, 0);
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("dedupes repeated LSP diagnostic identities until diagnostics clear", async () => {
|
|
691
|
+
const repo = createRepo();
|
|
692
|
+
const displayed: unknown[] = [];
|
|
693
|
+
let diagnostics: WatchdogLspResult["diagnostics"] = [{
|
|
694
|
+
path: "src/file.ts",
|
|
695
|
+
line: 1,
|
|
696
|
+
column: 14,
|
|
697
|
+
severity: "warning",
|
|
698
|
+
source: "typescript",
|
|
699
|
+
code: "TS6133",
|
|
700
|
+
message: "'value' is declared but its value is never read.",
|
|
701
|
+
}];
|
|
702
|
+
const runtime = new MainWatchdogRuntime({
|
|
703
|
+
cwd: repo,
|
|
704
|
+
reviewChangesOnly: true,
|
|
705
|
+
resolveConfig: () => configResult(enabledConfig({ lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp, enabled: true } })),
|
|
706
|
+
lspDiagnostics: () => ({ status: "ok", provider: "stub-lsp", checkedPaths: ["src/file.ts"], skippedPaths: [], diagnostics }),
|
|
707
|
+
review: () => ({ stopReason: "stop" }),
|
|
708
|
+
displayWarning: (details) => displayed.push(details),
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
for (const value of [2, 3]) {
|
|
712
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
713
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), `export const value = ${value};\n`, "utf-8");
|
|
714
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
715
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
716
|
+
}
|
|
717
|
+
assert.equal(displayed.length, 1);
|
|
718
|
+
assert.equal(runtime.getSnapshot(repo).lsp.freshDiagnosticCount, 0);
|
|
719
|
+
|
|
720
|
+
diagnostics = [];
|
|
721
|
+
runtime.handleBeforeAgentStart({ prompt: "Fix diagnostics." }, { cwd: repo });
|
|
722
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 4;\n", "utf-8");
|
|
723
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
724
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
725
|
+
|
|
726
|
+
diagnostics = [{
|
|
727
|
+
path: "src/file.ts",
|
|
728
|
+
line: 2,
|
|
729
|
+
column: 1,
|
|
730
|
+
severity: "warning",
|
|
731
|
+
source: "typescript",
|
|
732
|
+
code: "TS6133",
|
|
733
|
+
message: "'value' is declared but its value is never read.",
|
|
734
|
+
}];
|
|
735
|
+
runtime.handleBeforeAgentStart({ prompt: "Reintroduce diagnostics." }, { cwd: repo });
|
|
736
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 5;\n", "utf-8");
|
|
737
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
738
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
739
|
+
|
|
740
|
+
assert.equal(displayed.length, 2);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("records slow LSP checks as timeout status without emitting a warning", async () => {
|
|
744
|
+
const repo = createRepo();
|
|
745
|
+
const displayed: unknown[] = [];
|
|
746
|
+
const runtime = new MainWatchdogRuntime({
|
|
747
|
+
cwd: repo,
|
|
748
|
+
reviewChangesOnly: true,
|
|
749
|
+
resolveConfig: () => configResult(enabledConfig({ lsp: { ...DEFAULT_WATCHDOG_CONFIG.lsp, enabled: true } })),
|
|
750
|
+
lspDiagnostics: () => ({
|
|
751
|
+
status: "timeout",
|
|
752
|
+
provider: "stub-lsp",
|
|
753
|
+
checkedPaths: ["src/file.ts"],
|
|
754
|
+
skippedPaths: [],
|
|
755
|
+
diagnostics: [],
|
|
756
|
+
message: "Timed out waiting for diagnostics.",
|
|
757
|
+
}),
|
|
758
|
+
review: () => ({ stopReason: "stop" }),
|
|
759
|
+
displayWarning: (details) => displayed.push(details),
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
runtime.handleBeforeAgentStart({ prompt: "Patch the feature." }, { cwd: repo });
|
|
763
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 6;\n", "utf-8");
|
|
764
|
+
runtime.enqueueDelta("Assistant:\nEdited the file.");
|
|
765
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
766
|
+
|
|
767
|
+
const snapshot = runtime.getSnapshot(repo);
|
|
768
|
+
assert.equal(displayed.length, 0);
|
|
769
|
+
assert.equal(snapshot.lsp.status, "timeout");
|
|
770
|
+
assert.match(snapshot.lsp.message ?? "", /Timed out/);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("does not review reverted or ignored tmp-only changes in edit-gated mode", async () => {
|
|
774
|
+
const repo = createRepo();
|
|
775
|
+
let reviewCalls = 0;
|
|
776
|
+
const runtime = new MainWatchdogRuntime({
|
|
777
|
+
cwd: repo,
|
|
778
|
+
reviewChangesOnly: true,
|
|
779
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
780
|
+
review: () => {
|
|
781
|
+
reviewCalls++;
|
|
782
|
+
return { stopReason: "stop" };
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
runtime.handleBeforeAgentStart({ prompt: "Try an edit." }, { cwd: repo });
|
|
787
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 3;\n", "utf-8");
|
|
788
|
+
fs.writeFileSync(path.join(repo, "src", "file.ts"), "export const value = 1;\n", "utf-8");
|
|
789
|
+
runtime.enqueueDelta("Assistant:\nEdited then reverted.");
|
|
790
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
791
|
+
|
|
792
|
+
fs.mkdirSync(path.join(repo, "tmp"));
|
|
793
|
+
runtime.handleBeforeAgentStart({ prompt: "Write tmp artifact." }, { cwd: repo });
|
|
794
|
+
fs.writeFileSync(path.join(repo, "tmp", "artifact.md"), "ignore me\n", "utf-8");
|
|
795
|
+
runtime.enqueueDelta("Assistant:\nWrote tmp artifact.");
|
|
796
|
+
await runtime.handleAgentEnd({ type: "agent_end" }, { cwd: repo });
|
|
797
|
+
|
|
798
|
+
assert.equal(reviewCalls, 0);
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
it("hard-caps each successful review to one displayed warning", async () => {
|
|
802
|
+
const displayed: unknown[] = [];
|
|
803
|
+
const runtime = new MainWatchdogRuntime({
|
|
804
|
+
resolveConfig: () => configResult(enabledConfig()),
|
|
805
|
+
displayWarning: (details) => displayed.push(details),
|
|
806
|
+
review: (request) => {
|
|
807
|
+
assert.equal(request.emitWarning(warning()), true);
|
|
808
|
+
assert.equal(request.emitWarning({
|
|
809
|
+
...warning(),
|
|
810
|
+
summary: "Second runtime concern",
|
|
811
|
+
evidence: "The same review tried to emit another concern.",
|
|
812
|
+
}), false);
|
|
813
|
+
return { stopReason: "stop" };
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
runtime.enqueueDelta("Assistant:\nWorking");
|
|
818
|
+
await runtime.handleAgentEnd({ type: "agent_end", messages: [] }, { cwd: "/tmp/project" });
|
|
819
|
+
|
|
820
|
+
assert.equal(displayed.length, 1);
|
|
821
|
+
assert.equal(runtime.getSnapshot().lastWarning?.summary, "Runtime concern");
|
|
822
|
+
assert.equal(runtime.getSnapshot().lastWarning?.state, "displayed");
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it("session on/off overrides explicit main enabled settings", () => {
|
|
826
|
+
const runtime = new MainWatchdogRuntime({
|
|
827
|
+
resolveConfig: (_cwd, options) => {
|
|
828
|
+
const config = enabledConfig({ main: { enabled: false } });
|
|
829
|
+
const session = options?.session as { enabled?: boolean; main?: { enabled?: boolean } } | undefined;
|
|
830
|
+
if (session) {
|
|
831
|
+
config.enabled = session.enabled ?? config.enabled;
|
|
832
|
+
config.main.enabled = session.main?.enabled ?? config.main.enabled;
|
|
833
|
+
}
|
|
834
|
+
return configResult(config);
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
assert.equal(runtime.getSnapshot().enabled, false);
|
|
839
|
+
assert.equal(runtime.setSessionEnabled(true, "/tmp/project").enabled, true);
|
|
840
|
+
assert.equal(runtime.setSessionEnabled(false, "/tmp/project").enabled, false);
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
it("clears session overrides when a new session is bound", () => {
|
|
844
|
+
const runtime = new MainWatchdogRuntime({
|
|
845
|
+
resolveConfig: (_cwd, options) => {
|
|
846
|
+
const config = enabledConfig();
|
|
847
|
+
const session = options?.session as { enabled?: boolean; main?: { enabled?: boolean } } | undefined;
|
|
848
|
+
if (session) {
|
|
849
|
+
config.enabled = session.enabled ?? config.enabled;
|
|
850
|
+
config.main.enabled = session.main?.enabled ?? config.main.enabled;
|
|
851
|
+
}
|
|
852
|
+
return configResult(config);
|
|
853
|
+
},
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
assert.equal(runtime.setSessionEnabled(false, "/tmp/project").enabled, false);
|
|
857
|
+
runtime.bindSession({ cwd: "/tmp/project" });
|
|
858
|
+
|
|
859
|
+
const snapshot = runtime.getSnapshot();
|
|
860
|
+
assert.equal(snapshot.enabled, true);
|
|
861
|
+
assert.equal(snapshot.sessionOverride, undefined);
|
|
862
|
+
});
|
|
863
|
+
});
|