pi-harness-runtime 1.1.439 → 1.1.441
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/harness/e2e/auto-test-loop.ts +8 -2
- package/harness/e2e/browser-scrapers/base-browser-scraper.ts +2 -0
- package/harness/e2e/browser-scrapers/chrome-profile-manager.ts +2 -0
- package/harness/e2e/browser-scrapers/codex-browser-scraper.ts +2 -0
- package/harness/e2e/tools-detector.ts +2 -0
- package/harness/index.ts +2 -1
- package/harness/key-loader.ts +2 -0
- package/harness/langchain/checkpointer.ts +2 -0
- package/harness/langchain/cron-cli.ts +2 -0
- package/harness/langchain/graph.ts +2 -0
- package/harness/master-planner.ts +1 -1
- package/harness/notification-center.ts +2 -0
- package/harness/output-limit-handler.ts +1 -1
- package/harness/project-detector/detector.ts +2 -4
- package/harness/secret-provider.ts +2 -0
- package/harness/task-graph.ts +460 -113
- package/index.ts +16 -7
- package/package.json +7 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/dist/extension.d.ts.map +1 -1
- package/packages/agent-policy/dist/extension.js +2 -0
- package/packages/agent-policy/dist/extension.js.map +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/agent-policy/src/extension.ts +2 -0
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/dist/minimax-browser-auth.d.ts.map +1 -1
- package/packages/auth/dist/minimax-browser-auth.js +23 -1
- package/packages/auth/dist/minimax-browser-auth.js.map +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/auth/src/minimax-browser-auth.ts +30 -5
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/dist/inbox.d.ts.map +1 -1
- package/packages/autonomous-runtime/dist/inbox.js +2 -0
- package/packages/autonomous-runtime/dist/inbox.js.map +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/autonomous-runtime/src/inbox.ts +2 -0
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/dist/src/clipboard.d.ts.map +1 -1
- package/packages/clipboard/dist/src/clipboard.js +2 -0
- package/packages/clipboard/dist/src/clipboard.js.map +1 -1
- package/packages/clipboard/dist/src/copy-sync.d.ts.map +1 -1
- package/packages/clipboard/dist/src/copy-sync.js +2 -0
- package/packages/clipboard/dist/src/copy-sync.js.map +1 -1
- package/packages/clipboard/dist/src/gist-relay.d.ts.map +1 -1
- package/packages/clipboard/dist/src/gist-relay.js +2 -0
- package/packages/clipboard/dist/src/gist-relay.js.map +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard/src/clipboard.ts +2 -0
- package/packages/clipboard/src/copy-sync.ts +2 -0
- package/packages/clipboard/src/gist-relay.ts +2 -0
- package/packages/clipboard-plugin/dist/types.d.ts +9 -2
- package/packages/clipboard-plugin/dist/types.d.ts.map +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/clipboard-plugin/src/types.ts +9 -2
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/delegation-store.js +1 -4
- package/packages/contracts/dist/workspace-env.js +28 -1
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/delegation-store.ts +9 -4
- package/packages/contracts/src/workspace-env.ts +29 -4
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/dist/src/store.d.ts.map +1 -1
- package/packages/event-store/dist/src/store.js +2 -0
- package/packages/event-store/dist/src/store.js.map +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/event-store/src/store.ts +3 -0
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/dist/analyzer.d.ts.map +1 -1
- package/packages/generic-web-plugin/dist/analyzer.js +2 -0
- package/packages/generic-web-plugin/dist/analyzer.js.map +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/generic-web-plugin/src/analyzer.ts +3 -0
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/dist/mcp-client.d.ts.map +1 -1
- package/packages/knowledge-retrieval/dist/mcp-client.js +0 -1
- package/packages/knowledge-retrieval/dist/mcp-client.js.map +1 -1
- package/packages/knowledge-retrieval/package.json +1 -4
- package/packages/knowledge-retrieval/src/mcp-client.ts +0 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/logging/package.json +1 -1
- package/packages/mcp-adapter/dist/server.d.ts.map +1 -1
- package/packages/mcp-adapter/dist/server.js +2 -0
- package/packages/mcp-adapter/dist/server.js.map +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/mcp-adapter/src/server.ts +3 -0
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/dist/index.d.ts.map +1 -1
- package/packages/paperclip-integration/dist/index.js +2 -0
- package/packages/paperclip-integration/dist/index.js.map +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/paperclip-integration/src/index.ts +2 -0
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/dist/audit.d.ts.map +1 -1
- package/packages/privilege-broker/dist/audit.js +2 -0
- package/packages/privilege-broker/dist/audit.js.map +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/privilege-broker/src/audit.ts +2 -0
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/dist/compiler.d.ts.map +1 -1
- package/packages/prompt-compiler/dist/compiler.js +2 -0
- package/packages/prompt-compiler/dist/compiler.js.map +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-compiler/src/compiler.ts +2 -0
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/dist/client.d.ts.map +1 -1
- package/packages/skill-mcp-client/dist/client.js +0 -1
- package/packages/skill-mcp-client/dist/client.js.map +1 -1
- package/packages/skill-mcp-client/package.json +1 -4
- package/packages/skill-mcp-client/src/client.ts +0 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/dist/parser.d.ts.map +1 -1
- package/packages/skills/dist/parser.js +2 -0
- package/packages/skills/dist/parser.js.map +1 -1
- package/packages/skills/dist/skill-versioning.d.ts.map +1 -1
- package/packages/skills/dist/skill-versioning.js +2 -0
- package/packages/skills/dist/skill-versioning.js.map +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/skills/src/parser.ts +3 -0
- package/packages/skills/src/skill-versioning.ts +2 -0
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/tooling-preference/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/two-phase-review/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/dist/index.js +0 -2
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/webmcp-tester/src/index.ts +0 -3
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/dist/trigger.d.ts.map +1 -1
- package/packages/write-review/dist/trigger.js +1 -2
- package/packages/write-review/dist/trigger.js.map +1 -1
- package/packages/write-review/package.json +1 -1
- package/packages/write-review/src/trigger.ts +1 -2
- package/scripts/lib/runtime-imports.ts +189 -3
- package/scripts/release-all.ts +5 -3
- package/scripts/telegram-webhook-server.ts +2 -0
- package/scripts/typecheck-packages.mjs +7 -4
- package/scripts/verify-package-contents.ts +161 -2
- package/harness/e2e/tools-detector.test.ts +0 -156
- package/harness/glm-quota-countdown.test.ts +0 -323
- package/harness/langchain/agents.test.ts +0 -39
- package/harness/langchain/checkpointer.test.ts +0 -243
- package/harness/langchain/daemon.test.ts +0 -459
- package/harness/langchain/model-router.test.ts +0 -191
- package/harness/langchain/ping-pong-middleware.test.ts +0 -69
- package/harness/langchain/scoreboard.test.ts +0 -245
- package/harness/langchain/status-line.test.ts +0 -106
- package/harness/langchain/surge.test.ts +0 -467
- package/harness/langchain/widget.test.ts +0 -251
- package/harness/skill-sync.test.ts +0 -57
- package/harness/ssh-detach-interceptor.test.ts +0 -263
- package/harness/ssh-hang-recovery.test.ts +0 -86
- package/packages/a2a-adapter/test/a2a.test.ts +0 -151
- package/packages/agent-policy/test/interlock.test.ts +0 -101
- package/packages/agent-policy/test/policy-retention.test.ts +0 -380
- package/packages/agent-policy/test/policy.test.ts +0 -387
- package/packages/auth/test/auth.test.ts +0 -238
- package/packages/autonomous-refactor/test/refactor.test.ts +0 -202
- package/packages/autonomous-runtime/test/types.test.ts +0 -95
- package/packages/cache-strategy/test/cache.test.ts +0 -137
- package/packages/capability-registry/test/registry.test.ts +0 -204
- package/packages/checkpoint/test/engine.test.ts +0 -265
- package/packages/cli-plugin-sdk/test/cli.test.ts +0 -45
- package/packages/code-generation/test/generator.test.ts +0 -238
- package/packages/code-review/test/engine.test.ts +0 -302
- package/packages/codex-adapter/test/provider.test.ts +0 -142
- package/packages/codex-watcher/test/watch.test.ts +0 -453
- package/packages/context-compiler/test/context-compiler.test.ts +0 -746
- package/packages/context-manager/test/context.test.ts +0 -198
- package/packages/contracts/test/delegation.test.ts +0 -350
- package/packages/contracts/test/workspace-env.test.ts +0 -802
- package/packages/cookie-sanitizer/test/infer.test.ts +0 -110
- package/packages/cookie-sanitizer/test/normalize.test.ts +0 -113
- package/packages/cookie-sanitizer/test/parse.test.ts +0 -204
- package/packages/cookie-sanitizer/test/sync.test.ts +0 -237
- package/packages/cookie-sanitizer/test/watcher.test.ts +0 -27
- package/packages/cost-optimizer/test/cost-optimizer.test.ts +0 -438
- package/packages/dependency-analyzer/test/analyzer.test.ts +0 -49
- package/packages/django-plugin/test/analyzer.test.ts +0 -195
- package/packages/doc-generator/test/docs.test.ts +0 -168
- package/packages/durable-review/test/acceptance.test.ts +0 -357
- package/packages/durable-review/test/smoke.test.ts +0 -107
- package/packages/evaluation-engine/test/engine.test.ts +0 -277
- package/packages/evaluation-runner/test/runner.test.ts +0 -168
- package/packages/event-bus/test/bus.test.ts +0 -100
- package/packages/event-store/test/store.test.ts +0 -115
- package/packages/experience-replay/test/replay.test.ts +0 -267
- package/packages/feedback-collector/test/collector.test.ts +0 -142
- package/packages/framework-detector/test/detector.test.ts +0 -75
- package/packages/framework-plugin-sdk/test/manager.test.ts +0 -320
- package/packages/frappe-plugin/test/analyzer.test.ts +0 -250
- package/packages/generic-web-plugin/test/analyzer.test.ts +0 -134
- package/packages/health-monitor/test/health.test.ts +0 -231
- package/packages/intent-analyzer/test/analyzer.test.ts +0 -72
- package/packages/intent-analyzer/test/ping-pong.test.ts +0 -96
- package/packages/jev-judge/tests/auto-continue-context.test.ts +0 -280
- package/packages/jev-judge/tests/continuation-controller.test.ts +0 -1396
- package/packages/jev-judge/tests/jev-provider-chain.test.ts +0 -536
- package/packages/jev-judge/tests/outcome-application.test.ts +0 -132
- package/packages/jev-judge/tests/question-risk.test.ts +0 -129
- package/packages/jev-judge/tests/typesafe-provider.test.ts +0 -640
- package/packages/jev-judge/tests/validation-ledger.test.ts +0 -181
- package/packages/jev-judge/tests/validation-plan.test.ts +0 -170
- package/packages/jev-judge/tests/validation-tie-breaker.test.ts +0 -88
- package/packages/laravel-plugin/test/analyzer.test.ts +0 -220
- package/packages/learning-engine/test/engine.test.ts +0 -331
- package/packages/logging/test/buffer.test.ts +0 -44
- package/packages/logging/test/classification.test.ts +0 -137
- package/packages/logging/test/config.test.ts +0 -97
- package/packages/logging/test/fanout.test.ts +0 -186
- package/packages/logging/test/identity-transport.test.ts +0 -197
- package/packages/logging/test/incidents.test.ts +0 -404
- package/packages/logging/test/ingest-schema.test.ts +0 -184
- package/packages/logging/test/integration.test.ts +0 -140
- package/packages/logging/test/log-http.test.ts +0 -246
- package/packages/logging/test/redact.test.ts +0 -109
- package/packages/logging/test/sampler.test.ts +0 -57
- package/packages/logging/test/server.test.ts +0 -444
- package/packages/logging/test/shipper.test.ts +0 -152
- package/packages/logging/test/spool.test.ts +0 -96
- package/packages/mcp-adapter/test/server.test.ts +0 -91
- package/packages/memory-engine/test/engine.test.ts +0 -456
- package/packages/model-registry/test/registry.test.ts +0 -342
- package/packages/nextjs-plugin/test/analyzer.test.ts +0 -149
- package/packages/notification/test/approval-payload.test.ts +0 -269
- package/packages/notification/test/detail-format.test.ts +0 -106
- package/packages/notification/test/notification.test.ts +0 -375
- package/packages/notification/test/pending-question-lifecycle.test.ts +0 -195
- package/packages/notification/test/pending-question.test.ts +0 -331
- package/packages/notification/test/tooling-proposal-payload.test.ts +0 -212
- package/packages/observability/test/observability.test.ts +0 -1350
- package/packages/performance-optimizer/test/optimizer.test.ts +0 -267
- package/packages/project-analyzer/test/index.test.ts +0 -1719
- package/packages/prompt-compiler/test/prompt-compiler.test.ts +0 -813
- package/packages/prompt-versioning/test/versioning.test.ts +0 -112
- package/packages/provider-adapter-sdk/test/sdk.test.ts +0 -220
- package/packages/provider-router/test/provider-router.test.ts +0 -405
- package/packages/provider-selector/test/selector.test.ts +0 -192
- package/packages/providers/test/providers.test.ts +0 -225
- package/packages/qdrant-skills/test/collection-info.test.ts +0 -165
- package/packages/qdrant-skills/test/embedder-batch.test.ts +0 -184
- package/packages/qdrant-skills/test/embedder.test.ts +0 -97
- package/packages/quota-manager/test/quota.test.ts +0 -432
- package/packages/rate-limiter/test/limiter.test.ts +0 -128
- package/packages/react-vite-plugin/test/analyzer.test.ts +0 -139
- package/packages/relay/test/client.test.ts +0 -446
- package/packages/relay/test/relay-e2e.test.ts +0 -299
- package/packages/relay/test/relay-endpoint.test.ts +0 -278
- package/packages/release-manager/test/release.test.ts +0 -215
- package/packages/requirement-compiler/test/requirement-compiler.test.ts +0 -743
- package/packages/runtime/test/runtime-api.test.ts +0 -169
- package/packages/runtime/test/runtime.test.ts +0 -297
- package/packages/scheduler/test/scheduler.test.ts +0 -125
- package/packages/session/test/manager.test.ts +0 -233
- package/packages/shared-context/test/shared-context.test.ts +0 -114
- package/packages/skill-registry/test/registry.test.ts +0 -508
- package/packages/skills/test/init-skills.test.ts +0 -114
- package/packages/skills/test/remote-skills.test.ts +0 -360
- package/packages/skills/test/remote-source.test.ts +0 -248
- package/packages/skills/test/sanitize.test.ts +0 -250
- package/packages/skills/test/vector-indexing.test.ts +0 -205
- package/packages/skills/test/vector-schema.test.ts +0 -180
- package/packages/skills/test/vector-threshold.test.ts +0 -108
- package/packages/sprint-planner/test/planner.test.ts +0 -187
- package/packages/task-compiler/test/task-compiler.test.ts +0 -628
- package/packages/test-data-generator/test/generator.test.ts +0 -265
- package/packages/test-generator/test/generator.test.ts +0 -156
- package/packages/token-estimation/test/token-estimation.test.ts +0 -207
- package/packages/token-optimizer/test/optimizer.test.ts +0 -141
- package/packages/tooling-preference/tests/changed-files.test.ts +0 -124
- package/packages/tooling-preference/tests/detect.test.ts +0 -188
- package/packages/tooling-preference/tests/proposal.test.ts +0 -107
- package/packages/tooling-preference/tests/quality-check.test.ts +0 -272
- package/packages/tooling-preference/tests/registry.test.ts +0 -51
- package/packages/tooling-preference/tests/store.test.ts +0 -99
- package/packages/trajectory/test/store.test.ts +0 -250
- package/packages/tui/test/harness-footer.test.ts +0 -559
- package/packages/tui/test/harness-header.test.ts +0 -590
- package/packages/two-phase-review/test/classify.test.ts +0 -102
- package/packages/two-phase-review/test/helpers.ts +0 -72
- package/packages/two-phase-review/test/rules.test.ts +0 -150
- package/packages/two-phase-review/test/runtime.test.ts +0 -362
- package/packages/two-phase-review/test/scope.test.ts +0 -186
- package/packages/two-phase-review/test/signals.test.ts +0 -183
- package/packages/two-phase-review/test/state.test.ts +0 -201
- package/packages/wiki/test/basic.test.ts +0 -242
- package/packages/workspace-scanner/test/scanner.test.ts +0 -43
- package/packages/worktree/test/worktree.test.ts +0 -163
|
@@ -1,1396 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression tests for the continuation guard lifecycle.
|
|
3
|
-
*
|
|
4
|
-
* Bug under test: the dispatch guard was set on a successful dispatch and only
|
|
5
|
-
* released via clearQueue() on a non-continue path, so the *next* dispatch was
|
|
6
|
-
* blocked forever - while the retry budget was still consumed by every blocked
|
|
7
|
-
* attempt. Result: one continuation, then a silent stop.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { describe, it, expect } from "bun:test";
|
|
11
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
import { TodoContinuationController } from "../src/continuation-controller.js";
|
|
15
|
-
import { MockTodoProvider, type TodoContext } from "../src/todo-provider.js";
|
|
16
|
-
import { ValidationLedger } from "../src/validation-ledger.js";
|
|
17
|
-
|
|
18
|
-
function makeCtx(overrides: Partial<TodoContext> = {}): TodoContext {
|
|
19
|
-
return {
|
|
20
|
-
sessionManager: {
|
|
21
|
-
getSessionId: () => "session-test",
|
|
22
|
-
getBranch: () => [],
|
|
23
|
-
},
|
|
24
|
-
isIdle: () => true,
|
|
25
|
-
hasPendingMessages: () => false,
|
|
26
|
-
...overrides,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeController(opts: {
|
|
31
|
-
provider: MockTodoProvider;
|
|
32
|
-
dispatch: (content: string) => Promise<void>;
|
|
33
|
-
ctx?: TodoContext;
|
|
34
|
-
maxSteerAttempts?: number;
|
|
35
|
-
circuitBreakerCooldownMs?: number;
|
|
36
|
-
guardReleaseTimeoutMs?: number;
|
|
37
|
-
guardReleaseAfterDropMs?: number;
|
|
38
|
-
inFlightTimeoutMs?: number;
|
|
39
|
-
maxConsecutiveFailures?: number;
|
|
40
|
-
}) {
|
|
41
|
-
return new TodoContinuationController(opts.ctx ?? makeCtx(), {
|
|
42
|
-
dispatch: opts.dispatch,
|
|
43
|
-
todoProvider: opts.provider,
|
|
44
|
-
maxSteerAttempts: opts.maxSteerAttempts ?? 3,
|
|
45
|
-
circuitBreakerCooldownMs: opts.circuitBreakerCooldownMs,
|
|
46
|
-
guardReleaseTimeoutMs: opts.guardReleaseTimeoutMs,
|
|
47
|
-
guardReleaseAfterDropMs: opts.guardReleaseAfterDropMs,
|
|
48
|
-
inFlightTimeoutMs: opts.inFlightTimeoutMs,
|
|
49
|
-
maxConsecutiveFailures: opts.maxConsecutiveFailures,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe("TodoContinuationController - guard lifecycle", () => {
|
|
54
|
-
it("dispatches on every settled cycle until the attempt budget is spent", async () => {
|
|
55
|
-
const provider = new MockTodoProvider();
|
|
56
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
57
|
-
|
|
58
|
-
const sent: string[] = [];
|
|
59
|
-
const controller = makeController({
|
|
60
|
-
provider,
|
|
61
|
-
dispatch: async (content) => {
|
|
62
|
-
sent.push(content);
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Four consecutive idle cycles: decide -> dispatch -> (agent settles)
|
|
67
|
-
for (let cycle = 1; cycle <= 4; cycle++) {
|
|
68
|
-
controller.releaseContinuationGuard(`settled ${cycle}`);
|
|
69
|
-
const result = await controller.decide();
|
|
70
|
-
|
|
71
|
-
if (result.action === "continue" && result.nextItemId) {
|
|
72
|
-
const steer = await controller.steerToContinue(result.nextItemId);
|
|
73
|
-
expect(steer.dispatched).toBe(true);
|
|
74
|
-
} else {
|
|
75
|
-
expect(result.action).toBe("stop");
|
|
76
|
-
expect(result.reason).toContain("max attempts");
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// 3 successful dispatches, then the budget stops further attempts
|
|
81
|
-
expect(sent.length).toBe(3);
|
|
82
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(3);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("does not consume the attempt budget when a dispatch is blocked", async () => {
|
|
86
|
-
const provider = new MockTodoProvider();
|
|
87
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
88
|
-
|
|
89
|
-
let busy = false;
|
|
90
|
-
const sent: string[] = [];
|
|
91
|
-
const controller = makeController({
|
|
92
|
-
provider,
|
|
93
|
-
ctx: makeCtx({ isIdle: () => !busy }),
|
|
94
|
-
dispatch: async (content) => {
|
|
95
|
-
sent.push(content);
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// One real dispatch spends one attempt.
|
|
100
|
-
const first = await controller.steerToContinue(1);
|
|
101
|
-
expect(first.dispatched).toBe(true);
|
|
102
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(1);
|
|
103
|
-
|
|
104
|
-
// The guard blocks the next dispatch because the session is busy.
|
|
105
|
-
busy = true;
|
|
106
|
-
const blocked = await controller.steerToContinue(1);
|
|
107
|
-
expect(blocked.dispatched).toBe(false);
|
|
108
|
-
expect(blocked.reason).toContain("session not idle");
|
|
109
|
-
expect(blocked.blockedDispatches).toBeGreaterThan(0);
|
|
110
|
-
// The counter is untouched: a blocked attempt is not an attempt.
|
|
111
|
-
expect(blocked.attempts).toBe(1);
|
|
112
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(1);
|
|
113
|
-
expect(sent.length).toBe(1);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it("blocks a duplicate dispatch until the guard is released", async () => {
|
|
117
|
-
const provider = new MockTodoProvider();
|
|
118
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
119
|
-
|
|
120
|
-
const sent: string[] = [];
|
|
121
|
-
const controller = makeController({
|
|
122
|
-
provider,
|
|
123
|
-
dispatch: async (content) => {
|
|
124
|
-
sent.push(content);
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const first = await controller.steerToContinue(1);
|
|
129
|
-
expect(first.dispatched).toBe(true);
|
|
130
|
-
|
|
131
|
-
// A second settle without releasing the guard must not dispatch again
|
|
132
|
-
const second = await controller.steerToContinue(1);
|
|
133
|
-
expect(second.dispatched).toBe(false);
|
|
134
|
-
expect(sent.length).toBe(1);
|
|
135
|
-
|
|
136
|
-
// Releasing the guard (the verified lifecycle transition) allows the next one
|
|
137
|
-
controller.releaseContinuationGuard("agent_settled");
|
|
138
|
-
const third = await controller.steerToContinue(1);
|
|
139
|
-
expect(third.dispatched).toBe(true);
|
|
140
|
-
expect(sent.length).toBe(2);
|
|
141
|
-
expect(third.attempts).toBe(2);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("does not dispatch while explicitly paused and resumes afterwards", async () => {
|
|
145
|
-
const provider = new MockTodoProvider();
|
|
146
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
147
|
-
|
|
148
|
-
const sent: string[] = [];
|
|
149
|
-
const controller = makeController({
|
|
150
|
-
provider,
|
|
151
|
-
dispatch: async (content) => {
|
|
152
|
-
sent.push(content);
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
controller.explicitPause();
|
|
157
|
-
const paused = await controller.steerToContinue(1);
|
|
158
|
-
expect(paused.dispatched).toBe(false);
|
|
159
|
-
expect(sent.length).toBe(0);
|
|
160
|
-
|
|
161
|
-
controller.resume();
|
|
162
|
-
const resumed = await controller.steerToContinue(1);
|
|
163
|
-
expect(resumed.dispatched).toBe(true);
|
|
164
|
-
expect(sent.length).toBe(1);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("skips an item that was completed before the dispatch", async () => {
|
|
168
|
-
const provider = new MockTodoProvider();
|
|
169
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
170
|
-
|
|
171
|
-
const controller = makeController({
|
|
172
|
-
provider,
|
|
173
|
-
dispatch: async () => {
|
|
174
|
-
throw new Error("must not dispatch");
|
|
175
|
-
},
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
provider.setStatus(1, "completed");
|
|
179
|
-
const result = await controller.steerToContinue(1);
|
|
180
|
-
expect(result.dispatched).toBe(false);
|
|
181
|
-
expect(result.blockedDispatches).toBeGreaterThan(0);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it("reports a failed send with the real reason and spends no attempt", async () => {
|
|
185
|
-
const provider = new MockTodoProvider();
|
|
186
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
187
|
-
|
|
188
|
-
const controller = makeController({
|
|
189
|
-
provider,
|
|
190
|
-
dispatch: async () => {
|
|
191
|
-
throw new Error("transport is down");
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const result = await controller.steerToContinue(1);
|
|
196
|
-
expect(result.dispatched).toBe(false);
|
|
197
|
-
// The reason must name the real failure, not "blocked by guard".
|
|
198
|
-
expect(result.reason).toContain("dispatch failed");
|
|
199
|
-
expect(result.reason).toContain("transport is down");
|
|
200
|
-
expect(result.attempts).toBe(0);
|
|
201
|
-
expect(result.blockedDispatches).toBe(0);
|
|
202
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("keeps the in-flight guard while a dispatch is on the wire", async () => {
|
|
206
|
-
const provider = new MockTodoProvider();
|
|
207
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
208
|
-
|
|
209
|
-
let releaseDispatch: (() => void) | null = null;
|
|
210
|
-
const sent: string[] = [];
|
|
211
|
-
const controller = makeController({
|
|
212
|
-
provider,
|
|
213
|
-
dispatch: async (content) => {
|
|
214
|
-
sent.push(content);
|
|
215
|
-
await new Promise<void>((resolve) => {
|
|
216
|
-
releaseDispatch = resolve;
|
|
217
|
-
});
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
const first = controller.steerToContinue(1);
|
|
222
|
-
// Let the dispatch reach the in-flight state.
|
|
223
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
224
|
-
expect(controller.getDiagnostics().inFlight).toBe(true);
|
|
225
|
-
|
|
226
|
-
// A settle arriving while the dispatch is in flight must NOT release the
|
|
227
|
-
// in-flight guard - otherwise a duplicate continuation can be sent.
|
|
228
|
-
controller.releaseContinuationGuard("agent_settled");
|
|
229
|
-
expect(controller.getDiagnostics().inFlight).toBe(true);
|
|
230
|
-
|
|
231
|
-
const second = await controller.steerToContinue(1);
|
|
232
|
-
expect(second.dispatched).toBe(false);
|
|
233
|
-
expect(second.reason).toContain("in flight");
|
|
234
|
-
expect(sent.length).toBe(1);
|
|
235
|
-
|
|
236
|
-
releaseDispatch?.();
|
|
237
|
-
const firstResult = await first;
|
|
238
|
-
expect(firstResult.dispatched).toBe(true);
|
|
239
|
-
expect(controller.getDiagnostics().inFlight).toBe(false);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it("routes continueItem() through the guarded dispatch", async () => {
|
|
243
|
-
const provider = new MockTodoProvider();
|
|
244
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
245
|
-
|
|
246
|
-
const sent: string[] = [];
|
|
247
|
-
const controller = makeController({
|
|
248
|
-
provider,
|
|
249
|
-
dispatch: async (content) => {
|
|
250
|
-
sent.push(content);
|
|
251
|
-
},
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
const item = { id: 1, subject: "keep going", status: "in_progress" } as never;
|
|
255
|
-
await controller.continueItem(item);
|
|
256
|
-
expect(sent.length).toBe(1);
|
|
257
|
-
expect(sent[0]).toContain("keep going");
|
|
258
|
-
// A successful continuation consumes one attempt.
|
|
259
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(1);
|
|
260
|
-
// The guard is armed: a second continuation is blocked.
|
|
261
|
-
await controller.continueItem(item);
|
|
262
|
-
expect(sent.length).toBe(1);
|
|
263
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(1);
|
|
264
|
-
|
|
265
|
-
// After the guard is released the next continuation is allowed again.
|
|
266
|
-
controller.releaseContinuationGuard("agent_settled");
|
|
267
|
-
await controller.continueItem(item);
|
|
268
|
-
expect(sent.length).toBe(2);
|
|
269
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(2);
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it("does not spend an attempt when continueItem() is blocked", async () => {
|
|
273
|
-
const provider = new MockTodoProvider();
|
|
274
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
275
|
-
|
|
276
|
-
const sent: string[] = [];
|
|
277
|
-
const controller = makeController({
|
|
278
|
-
provider,
|
|
279
|
-
ctx: makeCtx({ isIdle: () => false }),
|
|
280
|
-
dispatch: async (content) => {
|
|
281
|
-
sent.push(content);
|
|
282
|
-
},
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const item = { id: 1, subject: "keep going", status: "in_progress" } as never;
|
|
286
|
-
await controller.continueItem(item);
|
|
287
|
-
expect(sent.length).toBe(0);
|
|
288
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(0);
|
|
289
|
-
expect(controller.getDiagnostics().blockedDispatches).toBeGreaterThan(0);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("cancels the guard timer on pause", async () => {
|
|
293
|
-
const provider = new MockTodoProvider();
|
|
294
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
295
|
-
|
|
296
|
-
const controller = makeController({
|
|
297
|
-
provider,
|
|
298
|
-
dispatch: async () => {},
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
await controller.steerToContinue(1);
|
|
302
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
303
|
-
|
|
304
|
-
controller.explicitPause();
|
|
305
|
-
// A pause must disarm the guard completely: a queued-but-cancelled
|
|
306
|
-
// release timer would leave the queue flag stuck at true and block
|
|
307
|
-
// every later dispatch.
|
|
308
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
309
|
-
|
|
310
|
-
// A paused controller must not dispatch again.
|
|
311
|
-
const paused = await controller.steerToContinue(1);
|
|
312
|
-
expect(paused.dispatched).toBe(false);
|
|
313
|
-
expect(paused.reason).toContain("paused");
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it("keeps the in-flight guard when the queue is cleared mid-dispatch", async () => {
|
|
317
|
-
const provider = new MockTodoProvider();
|
|
318
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
319
|
-
|
|
320
|
-
let releaseDispatch: (() => void) | null = null;
|
|
321
|
-
const sent: string[] = [];
|
|
322
|
-
const controller = makeController({
|
|
323
|
-
provider,
|
|
324
|
-
dispatch: async (content) => {
|
|
325
|
-
sent.push(content);
|
|
326
|
-
await new Promise<void>((resolve) => {
|
|
327
|
-
releaseDispatch = resolve;
|
|
328
|
-
});
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const first = controller.steerToContinue(1);
|
|
333
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
334
|
-
expect(controller.getDiagnostics().inFlight).toBe(true);
|
|
335
|
-
|
|
336
|
-
// clearQueue() must NOT release the in-flight guard - otherwise two
|
|
337
|
-
// continuations can be on the wire at the same time.
|
|
338
|
-
controller.clearQueue();
|
|
339
|
-
expect(controller.getDiagnostics().inFlight).toBe(true);
|
|
340
|
-
|
|
341
|
-
const second = await controller.steerToContinue(1);
|
|
342
|
-
expect(second.dispatched).toBe(false);
|
|
343
|
-
expect(second.reason).toContain("in flight");
|
|
344
|
-
expect(sent.length).toBe(1);
|
|
345
|
-
|
|
346
|
-
releaseDispatch?.();
|
|
347
|
-
await first;
|
|
348
|
-
expect(controller.getDiagnostics().inFlight).toBe(false);
|
|
349
|
-
// The cancelled dispatch must not leave the guard armed.
|
|
350
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it("does not disarm the guard of a later dispatch after an idle clearQueue()", async () => {
|
|
354
|
-
const provider = new MockTodoProvider();
|
|
355
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
356
|
-
|
|
357
|
-
const sent: string[] = [];
|
|
358
|
-
const controller = makeController({
|
|
359
|
-
provider,
|
|
360
|
-
dispatch: async (content) => {
|
|
361
|
-
sent.push(content);
|
|
362
|
-
},
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// A clearQueue() while NO dispatch is running must not cancel the guard
|
|
366
|
-
// of the NEXT dispatch.
|
|
367
|
-
controller.clearQueue();
|
|
368
|
-
|
|
369
|
-
await controller.steerToContinue(1);
|
|
370
|
-
expect(sent.length).toBe(1);
|
|
371
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
372
|
-
|
|
373
|
-
// The guard is armed: a second continuation is refused.
|
|
374
|
-
const second = await controller.steerToContinue(1);
|
|
375
|
-
expect(second.dispatched).toBe(false);
|
|
376
|
-
expect(sent.length).toBe(1);
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
it("drops a guard release that arrives while a dispatch is in flight", async () => {
|
|
380
|
-
const provider = new MockTodoProvider();
|
|
381
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
382
|
-
|
|
383
|
-
let releaseDispatch: (() => void) | null = null;
|
|
384
|
-
const controller = makeController({
|
|
385
|
-
provider,
|
|
386
|
-
dispatch: async () => {
|
|
387
|
-
await new Promise<void>((resolve) => {
|
|
388
|
-
releaseDispatch = resolve;
|
|
389
|
-
});
|
|
390
|
-
},
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
const first = controller.steerToContinue(1);
|
|
394
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
395
|
-
|
|
396
|
-
// A release that arrives mid-dispatch belongs to the turn that was
|
|
397
|
-
// already running - it must NOT disarm the guard of the dispatch that
|
|
398
|
-
// is settling now.
|
|
399
|
-
controller.releaseContinuationGuard("agent_settled");
|
|
400
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
401
|
-
|
|
402
|
-
releaseDispatch?.();
|
|
403
|
-
await first;
|
|
404
|
-
|
|
405
|
-
// The guard of the completed dispatch is still armed; the next settle
|
|
406
|
-
// releases it.
|
|
407
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
408
|
-
controller.releaseContinuationGuard("agent_settled");
|
|
409
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("opens the circuit breaker after repeated send failures", async () => {
|
|
413
|
-
const provider = new MockTodoProvider();
|
|
414
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
415
|
-
|
|
416
|
-
let fail = true;
|
|
417
|
-
let sends = 0;
|
|
418
|
-
const controller = makeController({
|
|
419
|
-
provider,
|
|
420
|
-
dispatch: async () => {
|
|
421
|
-
sends++;
|
|
422
|
-
if (fail) throw new Error("transport is down");
|
|
423
|
-
},
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
const reasons: string[] = [];
|
|
427
|
-
for (let i = 0; i < 3; i++) {
|
|
428
|
-
const result = await controller.steerToContinue(1);
|
|
429
|
-
expect(result.dispatched).toBe(false);
|
|
430
|
-
reasons.push(result.reason ?? "");
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// The first failures are reported verbatim; the third one trips the
|
|
434
|
-
// breaker.
|
|
435
|
-
expect(reasons[0]).toContain("dispatch failed");
|
|
436
|
-
expect(reasons[2]).toContain("circuit breaker open after 3 consecutive failures");
|
|
437
|
-
// A failing transport never consumes the attempt budget.
|
|
438
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(0);
|
|
439
|
-
expect(controller.getDiagnostics().circuitBreakerOpen).toBe(true);
|
|
440
|
-
|
|
441
|
-
// The breaker really breaks: the next continuation is refused BEFORE
|
|
442
|
-
// the transport is used at all.
|
|
443
|
-
const blocked = await controller.steerToContinue(1);
|
|
444
|
-
expect(blocked.dispatched).toBe(false);
|
|
445
|
-
expect(blocked.reason).toContain("circuit breaker open");
|
|
446
|
-
expect(sends).toBe(3);
|
|
447
|
-
|
|
448
|
-
// An explicit resume closes the breaker and a dispatch succeeds again.
|
|
449
|
-
fail = false;
|
|
450
|
-
controller.resume();
|
|
451
|
-
const ok = await controller.steerToContinue(1);
|
|
452
|
-
expect(ok.dispatched).toBe(true);
|
|
453
|
-
expect(sends).toBe(4);
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
it("keeps the attempt counter when a later attempt is blocked", async () => {
|
|
457
|
-
const provider = new MockTodoProvider();
|
|
458
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
459
|
-
provider.addItem({ subject: "other work", status: "pending" } as never);
|
|
460
|
-
|
|
461
|
-
let busy = false;
|
|
462
|
-
const sent: string[] = [];
|
|
463
|
-
const controller = makeController({
|
|
464
|
-
provider,
|
|
465
|
-
ctx: makeCtx({ isIdle: () => !busy }),
|
|
466
|
-
dispatch: async (content) => {
|
|
467
|
-
sent.push(content);
|
|
468
|
-
},
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
// One real dispatch consumes one attempt.
|
|
472
|
-
const first = await controller.steerToContinue(1);
|
|
473
|
-
expect(first.dispatched).toBe(true);
|
|
474
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(1);
|
|
475
|
-
|
|
476
|
-
// A blocked attempt must not change the counter.
|
|
477
|
-
busy = true;
|
|
478
|
-
const blocked = await controller.steerToContinue(1);
|
|
479
|
-
expect(blocked.dispatched).toBe(false);
|
|
480
|
-
expect(blocked.reason).toContain("not idle");
|
|
481
|
-
expect(controller.getDiagnostics().steerAttemptsForCurrentItem).toBe(1);
|
|
482
|
-
expect(sent.length).toBe(1);
|
|
483
|
-
|
|
484
|
-
// The budget is per item: another item starts with a fresh budget.
|
|
485
|
-
busy = false;
|
|
486
|
-
controller.releaseContinuationGuard("agent settled");
|
|
487
|
-
const other = await controller.steerToContinue(2);
|
|
488
|
-
expect(other.dispatched).toBe(true);
|
|
489
|
-
expect(other.attempts).toBe(1);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
it("stops after the session-wide attempt cap even when items alternate", async () => {
|
|
493
|
-
const provider = new MockTodoProvider();
|
|
494
|
-
// Four items: the per-item budget alone (3 each) would allow 12
|
|
495
|
-
// dispatches, the session-wide cap (9) stops the run.
|
|
496
|
-
for (let i = 0; i < 4; i++) {
|
|
497
|
-
provider.addItem({ subject: `item ${i}`, status: "pending" } as never);
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
const sent: string[] = [];
|
|
501
|
-
const controller = makeController({
|
|
502
|
-
provider,
|
|
503
|
-
maxSteerAttempts: 3,
|
|
504
|
-
dispatch: async (content) => {
|
|
505
|
-
sent.push(content);
|
|
506
|
-
},
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// Alternate between the items: the per-item budget never runs out.
|
|
510
|
-
let stopReason = "";
|
|
511
|
-
for (let i = 0; i < 20; i++) {
|
|
512
|
-
controller.releaseContinuationGuard(`settled ${i}`);
|
|
513
|
-
const result = await controller.steerToContinue((i % 4) + 1);
|
|
514
|
-
if (!result.dispatched) {
|
|
515
|
-
stopReason = result.reason ?? "";
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
expect(controller.getDiagnostics().totalAttempts).toBe(9);
|
|
521
|
-
expect(sent.length).toBe(9);
|
|
522
|
-
expect(stopReason).toContain("max total attempts");
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it("releases the in-flight guard when a dispatch hangs and keeps a newer dispatch safe", async () => {
|
|
526
|
-
const provider = new MockTodoProvider();
|
|
527
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
528
|
-
|
|
529
|
-
let releaseFirst: (() => void) | null = null;
|
|
530
|
-
let sends = 0;
|
|
531
|
-
const controller = makeController({
|
|
532
|
-
provider,
|
|
533
|
-
inFlightTimeoutMs: 30,
|
|
534
|
-
dispatch: async () => {
|
|
535
|
-
sends++;
|
|
536
|
-
if (sends === 1) {
|
|
537
|
-
await new Promise<void>((resolve) => {
|
|
538
|
-
releaseFirst = resolve;
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
},
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
const first = controller.steerToContinue(1);
|
|
545
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
546
|
-
|
|
547
|
-
// The watchdog released the hung dispatch.
|
|
548
|
-
expect(controller.getDiagnostics().inFlight).toBe(false);
|
|
549
|
-
|
|
550
|
-
// The next continuation is dispatched again.
|
|
551
|
-
const second = await controller.steerToContinue(1);
|
|
552
|
-
expect(second.dispatched).toBe(true);
|
|
553
|
-
expect(sends).toBe(2);
|
|
554
|
-
|
|
555
|
-
// The stale dispatch settles late: it must not clear the guard of the
|
|
556
|
-
// newer dispatch.
|
|
557
|
-
releaseFirst?.();
|
|
558
|
-
await first;
|
|
559
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
560
|
-
expect(controller.getDiagnostics().inFlight).toBe(false);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
it("aborts a dispatch that was cancelled while the snapshot was read", async () => {
|
|
564
|
-
const provider = new MockTodoProvider();
|
|
565
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
566
|
-
|
|
567
|
-
let releaseSnapshot: (() => void) | null = null;
|
|
568
|
-
const original = provider.getSnapshot.bind(provider);
|
|
569
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
570
|
-
await new Promise<void>((resolve) => {
|
|
571
|
-
releaseSnapshot = resolve;
|
|
572
|
-
});
|
|
573
|
-
return original();
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const sent: string[] = [];
|
|
577
|
-
const controller = makeController({
|
|
578
|
-
provider,
|
|
579
|
-
dispatch: async (content) => {
|
|
580
|
-
sent.push(content);
|
|
581
|
-
},
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
const pending = controller.steerToContinue(1);
|
|
585
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
586
|
-
|
|
587
|
-
// The queue is cleared while the snapshot read is still pending.
|
|
588
|
-
controller.clearQueue();
|
|
589
|
-
releaseSnapshot?.();
|
|
590
|
-
const result = await pending;
|
|
591
|
-
|
|
592
|
-
expect(result.dispatched).toBe(false);
|
|
593
|
-
expect(result.reason).toContain("cancelled before send");
|
|
594
|
-
expect(sent.length).toBe(0);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
it("aborts a dispatch that was paused while the snapshot was read", async () => {
|
|
598
|
-
const provider = new MockTodoProvider();
|
|
599
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
600
|
-
|
|
601
|
-
let releaseSnapshot: (() => void) | null = null;
|
|
602
|
-
const original = provider.getSnapshot.bind(provider);
|
|
603
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
604
|
-
await new Promise<void>((resolve) => {
|
|
605
|
-
releaseSnapshot = resolve;
|
|
606
|
-
});
|
|
607
|
-
return original();
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
const sent: string[] = [];
|
|
611
|
-
const controller = makeController({
|
|
612
|
-
provider,
|
|
613
|
-
dispatch: async (content) => {
|
|
614
|
-
sent.push(content);
|
|
615
|
-
},
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
const pending = controller.steerToContinue(1);
|
|
619
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
620
|
-
|
|
621
|
-
// The user sends a message while the snapshot read is pending.
|
|
622
|
-
controller.tempPause();
|
|
623
|
-
releaseSnapshot?.();
|
|
624
|
-
const result = await pending;
|
|
625
|
-
|
|
626
|
-
expect(result.dispatched).toBe(false);
|
|
627
|
-
expect(result.reason).toContain("paused before send");
|
|
628
|
-
expect(sent.length).toBe(0);
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it("releases the guard when the todo read hangs", async () => {
|
|
632
|
-
const provider = new MockTodoProvider();
|
|
633
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
634
|
-
|
|
635
|
-
// The provider never answers the FIRST read: the dispatch must not hold
|
|
636
|
-
// the guard forever.
|
|
637
|
-
const original = provider.getSnapshot.bind(provider);
|
|
638
|
-
let hang = true;
|
|
639
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
640
|
-
if (hang) {
|
|
641
|
-
hang = false;
|
|
642
|
-
await new Promise<void>(() => {});
|
|
643
|
-
}
|
|
644
|
-
return original();
|
|
645
|
-
};
|
|
646
|
-
|
|
647
|
-
const controller = makeController({
|
|
648
|
-
provider,
|
|
649
|
-
inFlightTimeoutMs: 30,
|
|
650
|
-
dispatch: async () => {},
|
|
651
|
-
});
|
|
652
|
-
|
|
653
|
-
const pending = controller.steerToContinue(1);
|
|
654
|
-
pending.catch(() => {}); // The read never answers: the promise stays pending
|
|
655
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
656
|
-
|
|
657
|
-
// The watchdog released the guard even though the read never finished.
|
|
658
|
-
expect(controller.getDiagnostics().inFlight).toBe(false);
|
|
659
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
660
|
-
|
|
661
|
-
// A later dispatch is possible again.
|
|
662
|
-
const next = await controller.steerToContinue(1);
|
|
663
|
-
expect(next.dispatched).toBe(true);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
it("does not let a stale failure clear the guard of a newer dispatch", async () => {
|
|
667
|
-
const provider = new MockTodoProvider();
|
|
668
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
669
|
-
|
|
670
|
-
let rejectFirst: ((err: Error) => void) | null = null;
|
|
671
|
-
let sends = 0;
|
|
672
|
-
const controller = makeController({
|
|
673
|
-
provider,
|
|
674
|
-
inFlightTimeoutMs: 30,
|
|
675
|
-
dispatch: async () => {
|
|
676
|
-
sends++;
|
|
677
|
-
if (sends === 1) {
|
|
678
|
-
await new Promise<void>((_resolve, reject) => {
|
|
679
|
-
rejectFirst = reject;
|
|
680
|
-
});
|
|
681
|
-
}
|
|
682
|
-
},
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
const first = controller.steerToContinue(1);
|
|
686
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
687
|
-
|
|
688
|
-
// The watchdog released the hung dispatch; a new one takes over.
|
|
689
|
-
const second = await controller.steerToContinue(1);
|
|
690
|
-
expect(second.dispatched).toBe(true);
|
|
691
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
692
|
-
|
|
693
|
-
// The stale dispatch fails late: it must not disarm the new guard.
|
|
694
|
-
rejectFirst?.(new Error("transport died"));
|
|
695
|
-
const firstResult = await first;
|
|
696
|
-
expect(firstResult.dispatched).toBe(false);
|
|
697
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
698
|
-
});
|
|
699
|
-
|
|
700
|
-
it("clears a cancel intent raised while the dispatch hung", async () => {
|
|
701
|
-
const provider = new MockTodoProvider();
|
|
702
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
703
|
-
|
|
704
|
-
let hangSend = true;
|
|
705
|
-
const sent: string[] = [];
|
|
706
|
-
const controller = makeController({
|
|
707
|
-
provider,
|
|
708
|
-
inFlightTimeoutMs: 30,
|
|
709
|
-
dispatch: async (content) => {
|
|
710
|
-
sent.push(content);
|
|
711
|
-
if (hangSend) await new Promise<void>(() => {});
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
const stalled = controller.steerToContinue(1);
|
|
716
|
-
stalled.catch(() => {});
|
|
717
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
718
|
-
|
|
719
|
-
// A cancel intent for the hung dispatch.
|
|
720
|
-
controller.clearQueue();
|
|
721
|
-
expect(controller.getDiagnostics().cancelRequested).toBe(true);
|
|
722
|
-
|
|
723
|
-
// The watchdog force-releases the dispatch and drops the intent: the
|
|
724
|
-
// next legitimate dispatch must not be cancelled by it.
|
|
725
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
726
|
-
expect(controller.getDiagnostics().cancelRequested).toBe(false);
|
|
727
|
-
|
|
728
|
-
hangSend = false;
|
|
729
|
-
const next = await controller.steerToContinue(1);
|
|
730
|
-
expect(next.dispatched).toBe(true);
|
|
731
|
-
});
|
|
732
|
-
|
|
733
|
-
it("does not let a stale failure trip the circuit breaker", async () => {
|
|
734
|
-
const provider = new MockTodoProvider();
|
|
735
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
736
|
-
|
|
737
|
-
let rejectStale: ((err: Error) => void) | null = null;
|
|
738
|
-
let call = 0;
|
|
739
|
-
const controller = makeController({
|
|
740
|
-
provider,
|
|
741
|
-
inFlightTimeoutMs: 30,
|
|
742
|
-
dispatch: async () => {
|
|
743
|
-
call++;
|
|
744
|
-
if (call === 1) {
|
|
745
|
-
// The first send hangs and fails LATE.
|
|
746
|
-
await new Promise<void>((_resolve, reject) => {
|
|
747
|
-
rejectStale = reject;
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
throw new Error("transport is down");
|
|
751
|
-
},
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
const stale = controller.steerToContinue(1);
|
|
755
|
-
stale.catch(() => {});
|
|
756
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
757
|
-
// The watchdog released the first dispatch.
|
|
758
|
-
expect(controller.getDiagnostics().consecutiveFailures).toBe(0);
|
|
759
|
-
|
|
760
|
-
// The second dispatch fails for real: one failure.
|
|
761
|
-
const second = await controller.steerToContinue(1);
|
|
762
|
-
expect(second.dispatched).toBe(false);
|
|
763
|
-
expect(controller.getDiagnostics().consecutiveFailures).toBe(1);
|
|
764
|
-
|
|
765
|
-
// The stale failure arrives late: it must not count.
|
|
766
|
-
rejectStale?.(new Error("late failure"));
|
|
767
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
768
|
-
expect(controller.getDiagnostics().consecutiveFailures).toBe(1);
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
it("does not let a stale dispatch consume the cancel intent of a newer one", async () => {
|
|
772
|
-
const provider = new MockTodoProvider();
|
|
773
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
774
|
-
|
|
775
|
-
const original = provider.getSnapshot.bind(provider);
|
|
776
|
-
const resolvers: (() => void)[] = [];
|
|
777
|
-
let reads = 0;
|
|
778
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
779
|
-
reads++;
|
|
780
|
-
if (reads <= 2) {
|
|
781
|
-
// The first two reads hang: dispatch A and dispatch B.
|
|
782
|
-
await new Promise<void>((resolve) => {
|
|
783
|
-
resolvers.push(resolve);
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
return original();
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
const sent: string[] = [];
|
|
790
|
-
const controller = makeController({
|
|
791
|
-
provider,
|
|
792
|
-
inFlightTimeoutMs: 30,
|
|
793
|
-
dispatch: async (content) => {
|
|
794
|
-
sent.push(content);
|
|
795
|
-
},
|
|
796
|
-
});
|
|
797
|
-
|
|
798
|
-
// Dispatch A hangs in the todo read.
|
|
799
|
-
const pendingA = controller.steerToContinue(1);
|
|
800
|
-
pendingA.catch(() => {});
|
|
801
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
802
|
-
// The watchdog released A: it is now a stale dispatch.
|
|
803
|
-
expect(controller.getDiagnostics().inFlight).toBe(false);
|
|
804
|
-
|
|
805
|
-
// Dispatch B takes the guard and hangs in the todo read as well.
|
|
806
|
-
const pendingB = controller.steerToContinue(1);
|
|
807
|
-
pendingB.catch(() => {});
|
|
808
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
809
|
-
|
|
810
|
-
// A cancel intent is raised for dispatch B.
|
|
811
|
-
controller.clearQueue();
|
|
812
|
-
expect(controller.getDiagnostics().cancelRequested).toBe(true);
|
|
813
|
-
|
|
814
|
-
// A wakes up: it must report "superseded" and must NOT steal B's
|
|
815
|
-
// cancel intent.
|
|
816
|
-
resolvers[0]?.();
|
|
817
|
-
const staleResult = await pendingA;
|
|
818
|
-
expect(staleResult.dispatched).toBe(false);
|
|
819
|
-
expect(staleResult.reason).toContain("superseded");
|
|
820
|
-
expect(controller.getDiagnostics().cancelRequested).toBe(true);
|
|
821
|
-
expect(sent.length).toBe(0);
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
it("marks a retry after a stalled dispatch and clears the stall afterwards", async () => {
|
|
825
|
-
const provider = new MockTodoProvider();
|
|
826
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
827
|
-
|
|
828
|
-
let hangSend = true;
|
|
829
|
-
const sent: string[] = [];
|
|
830
|
-
const controller = makeController({
|
|
831
|
-
provider,
|
|
832
|
-
inFlightTimeoutMs: 30,
|
|
833
|
-
dispatch: async (content) => {
|
|
834
|
-
sent.push(content);
|
|
835
|
-
if (hangSend) await new Promise<void>(() => {});
|
|
836
|
-
},
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
// The send hangs: the watchdog force-releases the dispatch.
|
|
840
|
-
const stalled = controller.steerToContinue(1);
|
|
841
|
-
stalled.catch(() => {});
|
|
842
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
843
|
-
expect(sent.length).toBe(1);
|
|
844
|
-
expect(sent[0]).not.toContain("retry after a stalled dispatch");
|
|
845
|
-
|
|
846
|
-
// The retry warns about the possible duplicate.
|
|
847
|
-
hangSend = false;
|
|
848
|
-
const retry = await controller.steerToContinue(1);
|
|
849
|
-
expect(retry.dispatched).toBe(true);
|
|
850
|
-
expect(sent.length).toBe(2);
|
|
851
|
-
expect(sent[1]).toContain("retry after a stalled dispatch");
|
|
852
|
-
|
|
853
|
-
// The stall is cleared: a third attempt is a normal continuation.
|
|
854
|
-
controller.releaseContinuationGuard("settled");
|
|
855
|
-
const third = await controller.steerToContinue(1);
|
|
856
|
-
expect(third.dispatched).toBe(true);
|
|
857
|
-
expect(sent[2]).not.toContain("retry after a stalled dispatch");
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
it("marks a retry after a stalled dispatch on the continueItem path too", async () => {
|
|
861
|
-
const provider = new MockTodoProvider();
|
|
862
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
863
|
-
|
|
864
|
-
let hangSend = true;
|
|
865
|
-
const sent: string[] = [];
|
|
866
|
-
const controller = makeController({
|
|
867
|
-
provider,
|
|
868
|
-
inFlightTimeoutMs: 30,
|
|
869
|
-
dispatch: async (content) => {
|
|
870
|
-
sent.push(content);
|
|
871
|
-
if (hangSend) await new Promise<void>(() => {});
|
|
872
|
-
},
|
|
873
|
-
});
|
|
874
|
-
|
|
875
|
-
const item = (await provider.getSnapshot()).items[0];
|
|
876
|
-
const stalled = controller.continueItem(item);
|
|
877
|
-
stalled.catch(() => {});
|
|
878
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
879
|
-
|
|
880
|
-
hangSend = false;
|
|
881
|
-
await controller.continueItem(item);
|
|
882
|
-
expect(sent.length).toBe(2);
|
|
883
|
-
expect(sent[1]).toContain("retry after a stalled dispatch");
|
|
884
|
-
|
|
885
|
-
// The stall marker is cleared: the next continuation is a normal one.
|
|
886
|
-
controller.releaseContinuationGuard("settled");
|
|
887
|
-
await controller.continueItem(item);
|
|
888
|
-
expect(sent.length).toBe(3);
|
|
889
|
-
expect(sent[2]).not.toContain("retry after a stalled dispatch");
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
it("does not clear the temporary pause while a dispatch is in flight", async () => {
|
|
893
|
-
const provider = new MockTodoProvider();
|
|
894
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
895
|
-
|
|
896
|
-
let releaseSnapshot: (() => void) | null = null;
|
|
897
|
-
const original = provider.getSnapshot.bind(provider);
|
|
898
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
899
|
-
await new Promise<void>((resolve) => {
|
|
900
|
-
releaseSnapshot = resolve;
|
|
901
|
-
});
|
|
902
|
-
return original();
|
|
903
|
-
};
|
|
904
|
-
|
|
905
|
-
const controller = makeController({ provider, dispatch: async () => {} });
|
|
906
|
-
|
|
907
|
-
const pending = controller.steerToContinue(1);
|
|
908
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
909
|
-
|
|
910
|
-
// A user message arrives while the dispatch reads the snapshot.
|
|
911
|
-
controller.tempPause();
|
|
912
|
-
// A decision that runs concurrently must not drop the pause.
|
|
913
|
-
await controller.decide();
|
|
914
|
-
expect(controller.getDiagnostics().userPaused).toBe(true);
|
|
915
|
-
|
|
916
|
-
releaseSnapshot?.();
|
|
917
|
-
const result = await pending;
|
|
918
|
-
expect(result.dispatched).toBe(false);
|
|
919
|
-
expect(result.reason).toContain("paused before send");
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
it("records the dropped guard release in the diagnostics", async () => {
|
|
923
|
-
const provider = new MockTodoProvider();
|
|
924
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
925
|
-
|
|
926
|
-
let releaseSend: (() => void) | null = null;
|
|
927
|
-
const controller = makeController({
|
|
928
|
-
provider,
|
|
929
|
-
dispatch: async () => {
|
|
930
|
-
await new Promise<void>((resolve) => {
|
|
931
|
-
releaseSend = resolve;
|
|
932
|
-
});
|
|
933
|
-
},
|
|
934
|
-
});
|
|
935
|
-
|
|
936
|
-
const pending = controller.steerToContinue(1);
|
|
937
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
938
|
-
|
|
939
|
-
// A release arrives while the dispatch is on the wire.
|
|
940
|
-
controller.releaseContinuationGuard("agent settled");
|
|
941
|
-
expect(controller.getDiagnostics().pendingGuardRelease).toBe("agent settled");
|
|
942
|
-
|
|
943
|
-
releaseSend?.();
|
|
944
|
-
await pending;
|
|
945
|
-
|
|
946
|
-
expect(controller.getDiagnostics().pendingGuardRelease).toBeNull();
|
|
947
|
-
expect(controller.getDiagnostics().lastDroppedGuardRelease).toBe("agent settled");
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
it("releases the guard shortly after a dropped mid-dispatch release", async () => {
|
|
951
|
-
const provider = new MockTodoProvider();
|
|
952
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
953
|
-
|
|
954
|
-
let releaseSend: (() => void) | null = null;
|
|
955
|
-
const controller = makeController({
|
|
956
|
-
provider,
|
|
957
|
-
guardReleaseAfterDropMs: 30,
|
|
958
|
-
guardReleaseTimeoutMs: 60_000,
|
|
959
|
-
dispatch: async () => {
|
|
960
|
-
await new Promise<void>((resolve) => {
|
|
961
|
-
releaseSend = resolve;
|
|
962
|
-
});
|
|
963
|
-
},
|
|
964
|
-
});
|
|
965
|
-
|
|
966
|
-
const pending = controller.steerToContinue(1);
|
|
967
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
968
|
-
controller.releaseContinuationGuard("agent settled");
|
|
969
|
-
releaseSend?.();
|
|
970
|
-
await pending;
|
|
971
|
-
|
|
972
|
-
// Right after the drop the guard is still armed...
|
|
973
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
974
|
-
// ...but it is released after the short delay, not after the 60s safety
|
|
975
|
-
// net: the auto-continue must not stall for a minute.
|
|
976
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
977
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
it("does not mark an item as stalled when the todo read hangs", async () => {
|
|
981
|
-
const provider = new MockTodoProvider();
|
|
982
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
983
|
-
|
|
984
|
-
const original = provider.getSnapshot.bind(provider);
|
|
985
|
-
let hang = true;
|
|
986
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
987
|
-
if (hang) {
|
|
988
|
-
hang = false;
|
|
989
|
-
await new Promise<void>(() => {});
|
|
990
|
-
}
|
|
991
|
-
return original();
|
|
992
|
-
};
|
|
993
|
-
|
|
994
|
-
const sent: string[] = [];
|
|
995
|
-
const controller = makeController({
|
|
996
|
-
provider,
|
|
997
|
-
inFlightTimeoutMs: 30,
|
|
998
|
-
dispatch: async (content) => {
|
|
999
|
-
sent.push(content);
|
|
1000
|
-
},
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
const pending = controller.steerToContinue(1);
|
|
1004
|
-
pending.catch(() => {});
|
|
1005
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
1006
|
-
|
|
1007
|
-
// The read hung: no message was ever on the wire, so the retry must
|
|
1008
|
-
// NOT warn about a possible duplicate.
|
|
1009
|
-
const next = await controller.steerToContinue(1);
|
|
1010
|
-
expect(next.dispatched).toBe(true);
|
|
1011
|
-
expect(sent[0]).not.toContain("retry after a stalled dispatch");
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
it("does not let a stale success close an open circuit breaker", async () => {
|
|
1015
|
-
const provider = new MockTodoProvider();
|
|
1016
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1017
|
-
|
|
1018
|
-
let releaseStale: (() => void) | null = null;
|
|
1019
|
-
let call = 0;
|
|
1020
|
-
const controller = makeController({
|
|
1021
|
-
provider,
|
|
1022
|
-
inFlightTimeoutMs: 30,
|
|
1023
|
-
maxConsecutiveFailures: 1,
|
|
1024
|
-
dispatch: async () => {
|
|
1025
|
-
call++;
|
|
1026
|
-
if (call === 1) {
|
|
1027
|
-
// The first send hangs and succeeds LATE.
|
|
1028
|
-
await new Promise<void>((resolve) => {
|
|
1029
|
-
releaseStale = resolve;
|
|
1030
|
-
});
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
throw new Error("transport is down");
|
|
1034
|
-
},
|
|
1035
|
-
});
|
|
1036
|
-
|
|
1037
|
-
const stale = controller.steerToContinue(1);
|
|
1038
|
-
stale.catch(() => {});
|
|
1039
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
1040
|
-
expect(controller.getDiagnostics().consecutiveFailures).toBe(0);
|
|
1041
|
-
|
|
1042
|
-
// The second dispatch fails: with maxConsecutiveFailures = 1 the
|
|
1043
|
-
// breaker opens.
|
|
1044
|
-
const second = await controller.steerToContinue(1);
|
|
1045
|
-
expect(second.dispatched).toBe(false);
|
|
1046
|
-
expect(controller.getDiagnostics().consecutiveFailures).toBe(1);
|
|
1047
|
-
expect(controller.getDiagnostics().circuitBreakerOpen).toBe(true);
|
|
1048
|
-
|
|
1049
|
-
// The stale success arrives late: it must not close the breaker.
|
|
1050
|
-
releaseStale?.();
|
|
1051
|
-
await new Promise((r) => setTimeout(r, 10));
|
|
1052
|
-
expect(controller.getDiagnostics().consecutiveFailures).toBe(1);
|
|
1053
|
-
expect(controller.getDiagnostics().circuitBreakerOpen).toBe(true);
|
|
1054
|
-
});
|
|
1055
|
-
|
|
1056
|
-
it("reports a todo item with id 0 in the state", async () => {
|
|
1057
|
-
const provider = new MockTodoProvider();
|
|
1058
|
-
provider.addItem({ subject: "zero", status: "in_progress" } as never);
|
|
1059
|
-
// A provider whose item has the id 0 (a falsy but valid id).
|
|
1060
|
-
const original = provider.getSnapshot.bind(provider);
|
|
1061
|
-
const originalInProgress = provider.getInProgress.bind(provider);
|
|
1062
|
-
const originalReady = provider.getReady.bind(provider);
|
|
1063
|
-
const zeroed = <T extends { id: number }>(items: T[]): T[] =>
|
|
1064
|
-
items.map((i) => ({ ...i, id: 0 }));
|
|
1065
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
1066
|
-
const snapshot = await original();
|
|
1067
|
-
return { ...snapshot, items: zeroed(snapshot.items) };
|
|
1068
|
-
};
|
|
1069
|
-
(provider as unknown as { getInProgress: () => Promise<unknown> }).getInProgress = async () =>
|
|
1070
|
-
zeroed(await originalInProgress());
|
|
1071
|
-
(provider as unknown as { getReady: () => Promise<unknown> }).getReady = async () =>
|
|
1072
|
-
zeroed(await originalReady());
|
|
1073
|
-
|
|
1074
|
-
const controller = makeController({ provider, dispatch: async () => {} });
|
|
1075
|
-
const result = await controller.decide();
|
|
1076
|
-
expect(result.action).toBe("continue");
|
|
1077
|
-
// id 0 must not be reported as "no item".
|
|
1078
|
-
expect(controller.getState().current).toEqual({ id: 0 });
|
|
1079
|
-
expect(controller.getState().next).toEqual({ id: 0 });
|
|
1080
|
-
});
|
|
1081
|
-
|
|
1082
|
-
it("clears the selected item from the state when there is nothing to do", async () => {
|
|
1083
|
-
const provider = new MockTodoProvider();
|
|
1084
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1085
|
-
|
|
1086
|
-
const controller = makeController({ provider, dispatch: async () => {} });
|
|
1087
|
-
const result = await controller.decide();
|
|
1088
|
-
expect(result.action).toBe("continue");
|
|
1089
|
-
expect(controller.getState().current).not.toBeNull();
|
|
1090
|
-
|
|
1091
|
-
// The work is done: the next decision stops and the state no longer
|
|
1092
|
-
// reports the last continued item.
|
|
1093
|
-
provider.setStatus(1, "completed");
|
|
1094
|
-
const stopped = await controller.decide();
|
|
1095
|
-
expect(stopped.action).toBe("stop");
|
|
1096
|
-
expect(controller.getState().current).toBeNull();
|
|
1097
|
-
expect(controller.getState().next).toBeNull();
|
|
1098
|
-
});
|
|
1099
|
-
|
|
1100
|
-
it("reports the delivered message only after a successful send", async () => {
|
|
1101
|
-
const provider = new MockTodoProvider();
|
|
1102
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1103
|
-
|
|
1104
|
-
const controller = makeController({
|
|
1105
|
-
provider,
|
|
1106
|
-
dispatch: async () => {
|
|
1107
|
-
throw new Error("transport died");
|
|
1108
|
-
},
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
await controller.steerToContinue(1);
|
|
1112
|
-
expect(controller.getDiagnostics().lastSteerMessage).toBeNull();
|
|
1113
|
-
});
|
|
1114
|
-
|
|
1115
|
-
it("resets the cancel intent and the diagnostics on dispose()", async () => {
|
|
1116
|
-
const provider = new MockTodoProvider();
|
|
1117
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1118
|
-
|
|
1119
|
-
let releaseSnapshot: (() => void) | null = null;
|
|
1120
|
-
const original = provider.getSnapshot.bind(provider);
|
|
1121
|
-
(provider as unknown as { getSnapshot: () => Promise<unknown> }).getSnapshot = async () => {
|
|
1122
|
-
await new Promise<void>((resolve) => {
|
|
1123
|
-
releaseSnapshot = resolve;
|
|
1124
|
-
});
|
|
1125
|
-
return original();
|
|
1126
|
-
};
|
|
1127
|
-
|
|
1128
|
-
const controller = makeController({ provider, dispatch: async () => {} });
|
|
1129
|
-
|
|
1130
|
-
const pending = controller.steerToContinue(1);
|
|
1131
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
1132
|
-
controller.clearQueue();
|
|
1133
|
-
expect(controller.getDiagnostics().cancelRequested).toBe(true);
|
|
1134
|
-
|
|
1135
|
-
controller.dispose();
|
|
1136
|
-
expect(controller.getDiagnostics().cancelRequested).toBe(false);
|
|
1137
|
-
expect(controller.getDiagnostics().disposed).toBe(true);
|
|
1138
|
-
|
|
1139
|
-
releaseSnapshot?.();
|
|
1140
|
-
await pending;
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
it("auto-releases the guard when the release timer expires", async () => {
|
|
1144
|
-
const provider = new MockTodoProvider();
|
|
1145
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1146
|
-
|
|
1147
|
-
const controller = makeController({
|
|
1148
|
-
provider,
|
|
1149
|
-
guardReleaseTimeoutMs: 30,
|
|
1150
|
-
dispatch: async () => {},
|
|
1151
|
-
});
|
|
1152
|
-
|
|
1153
|
-
await controller.steerToContinue(1);
|
|
1154
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(true);
|
|
1155
|
-
|
|
1156
|
-
// The safety net for a lost agent_settled event.
|
|
1157
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
1158
|
-
expect(controller.getDiagnostics().continuationQueued).toBe(false);
|
|
1159
|
-
});
|
|
1160
|
-
|
|
1161
|
-
it("does not dispatch while a temporary pause is active and resumes afterwards", async () => {
|
|
1162
|
-
const provider = new MockTodoProvider();
|
|
1163
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1164
|
-
|
|
1165
|
-
const sent: string[] = [];
|
|
1166
|
-
const controller = makeController({
|
|
1167
|
-
provider,
|
|
1168
|
-
dispatch: async (content) => {
|
|
1169
|
-
sent.push(content);
|
|
1170
|
-
},
|
|
1171
|
-
});
|
|
1172
|
-
|
|
1173
|
-
controller.tempPause();
|
|
1174
|
-
const blocked = await controller.steerToContinue(1);
|
|
1175
|
-
expect(blocked.dispatched).toBe(false);
|
|
1176
|
-
expect(blocked.reason).toContain("user input pending");
|
|
1177
|
-
expect(sent.length).toBe(0);
|
|
1178
|
-
|
|
1179
|
-
// The next decision clears the temporary pause.
|
|
1180
|
-
await controller.decide();
|
|
1181
|
-
const ok = await controller.steerToContinue(1);
|
|
1182
|
-
expect(ok.dispatched).toBe(true);
|
|
1183
|
-
expect(sent.length).toBe(1);
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
it("does not dispatch after dispose()", async () => {
|
|
1187
|
-
const provider = new MockTodoProvider();
|
|
1188
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1189
|
-
|
|
1190
|
-
const sent: string[] = [];
|
|
1191
|
-
const controller = makeController({
|
|
1192
|
-
provider,
|
|
1193
|
-
dispatch: async (content) => {
|
|
1194
|
-
sent.push(content);
|
|
1195
|
-
},
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
controller.dispose();
|
|
1199
|
-
const result = await controller.steerToContinue(1);
|
|
1200
|
-
expect(result.dispatched).toBe(false);
|
|
1201
|
-
expect(result.reason).toContain("disposed");
|
|
1202
|
-
expect(sent.length).toBe(0);
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
it("allows a trial dispatch after the circuit breaker cooldown", async () => {
|
|
1206
|
-
const provider = new MockTodoProvider();
|
|
1207
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1208
|
-
|
|
1209
|
-
let fail = true;
|
|
1210
|
-
let sends = 0;
|
|
1211
|
-
const controller = makeController({
|
|
1212
|
-
provider,
|
|
1213
|
-
circuitBreakerCooldownMs: 30,
|
|
1214
|
-
dispatch: async () => {
|
|
1215
|
-
sends++;
|
|
1216
|
-
if (fail) throw new Error("transport is down");
|
|
1217
|
-
},
|
|
1218
|
-
});
|
|
1219
|
-
|
|
1220
|
-
for (let i = 0; i < 3; i++) await controller.steerToContinue(1);
|
|
1221
|
-
expect(controller.getDiagnostics().circuitBreakerOpen).toBe(true);
|
|
1222
|
-
|
|
1223
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
1224
|
-
expect(controller.getDiagnostics().circuitBreakerOpen).toBe(false);
|
|
1225
|
-
|
|
1226
|
-
// The transport is still broken: the trial fails and the breaker opens
|
|
1227
|
-
// again.
|
|
1228
|
-
fail = true;
|
|
1229
|
-
const trial = await controller.steerToContinue(1);
|
|
1230
|
-
expect(trial.dispatched).toBe(false);
|
|
1231
|
-
expect(sends).toBe(4);
|
|
1232
|
-
});
|
|
1233
|
-
});
|
|
1234
|
-
|
|
1235
|
-
describe("TodoContinuationController - staged validation context", () => {
|
|
1236
|
-
it("adds the declared validation plan to the continuation message", async () => {
|
|
1237
|
-
const provider = new MockTodoProvider();
|
|
1238
|
-
provider.addItem({
|
|
1239
|
-
subject: "seed data",
|
|
1240
|
-
status: "in_progress",
|
|
1241
|
-
metadata: {
|
|
1242
|
-
validationPlan: {
|
|
1243
|
-
fast: { command: "bun test tests/fast", expectedDurationMs: 18_000, datasetSize: 100 },
|
|
1244
|
-
full: { command: "bun test tests/full", expectedDurationMs: 2_400_000, datasetSize: 10_000 },
|
|
1245
|
-
budgetMinutes: 90,
|
|
1246
|
-
finalTierRequired: true,
|
|
1247
|
-
},
|
|
1248
|
-
},
|
|
1249
|
-
} as never);
|
|
1250
|
-
|
|
1251
|
-
const sent: string[] = [];
|
|
1252
|
-
const controller = makeController({
|
|
1253
|
-
provider,
|
|
1254
|
-
dispatch: async (content) => {
|
|
1255
|
-
sent.push(content);
|
|
1256
|
-
},
|
|
1257
|
-
});
|
|
1258
|
-
|
|
1259
|
-
const result = await controller.steerToContinue(1);
|
|
1260
|
-
expect(result.dispatched).toBe(true);
|
|
1261
|
-
expect(sent.length).toBe(1);
|
|
1262
|
-
|
|
1263
|
-
const message = sent[0];
|
|
1264
|
-
expect(message).toContain("Continue todo item ID 1.");
|
|
1265
|
-
expect(message).toContain("VALIDATION PLAN (staged)");
|
|
1266
|
-
expect(message).toContain("bun test tests/fast");
|
|
1267
|
-
expect(message).toContain("bun test tests/full");
|
|
1268
|
-
expect(message).toContain("Run the fast tier after every edit");
|
|
1269
|
-
expect(message).toContain("Time budget: 90m");
|
|
1270
|
-
});
|
|
1271
|
-
|
|
1272
|
-
it("leaves the message untouched when no plan is declared", async () => {
|
|
1273
|
-
const provider = new MockTodoProvider();
|
|
1274
|
-
provider.addItem({ subject: "keep going", status: "in_progress" } as never);
|
|
1275
|
-
|
|
1276
|
-
const sent: string[] = [];
|
|
1277
|
-
const controller = makeController({
|
|
1278
|
-
provider,
|
|
1279
|
-
dispatch: async (content) => {
|
|
1280
|
-
sent.push(content);
|
|
1281
|
-
},
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
await controller.steerToContinue(1);
|
|
1285
|
-
expect(sent[0]).toBe("Continue todo item ID 1.");
|
|
1286
|
-
expect(sent[0]).not.toContain("VALIDATION PLAN");
|
|
1287
|
-
});
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
describe("TodoContinuationController - validation budget and ledger", () => {
|
|
1291
|
-
it("pauses the item when the declared time budget is spent", async () => {
|
|
1292
|
-
const provider = new MockTodoProvider();
|
|
1293
|
-
provider.addItem({
|
|
1294
|
-
subject: "budgeted work",
|
|
1295
|
-
status: "in_progress",
|
|
1296
|
-
metadata: {
|
|
1297
|
-
validationPlan: {
|
|
1298
|
-
fast: { command: "bun test tests/fast" },
|
|
1299
|
-
budgetMinutes: 0,
|
|
1300
|
-
},
|
|
1301
|
-
},
|
|
1302
|
-
} as never);
|
|
1303
|
-
|
|
1304
|
-
const sent: string[] = [];
|
|
1305
|
-
const controller = makeController({
|
|
1306
|
-
provider,
|
|
1307
|
-
dispatch: async (content) => {
|
|
1308
|
-
sent.push(content);
|
|
1309
|
-
},
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
const result = await controller.decide();
|
|
1313
|
-
expect(result.action).toBe("stop");
|
|
1314
|
-
expect(result.reason).toContain("budget exhausted");
|
|
1315
|
-
expect(result.budget?.exhausted).toBe(true);
|
|
1316
|
-
expect(sent.length).toBe(0);
|
|
1317
|
-
});
|
|
1318
|
-
|
|
1319
|
-
it("does not put a continuation on the wire when the budget is spent", async () => {
|
|
1320
|
-
const provider = new MockTodoProvider();
|
|
1321
|
-
provider.addItem({
|
|
1322
|
-
subject: "budgeted work",
|
|
1323
|
-
status: "in_progress",
|
|
1324
|
-
metadata: {
|
|
1325
|
-
validationPlan: {
|
|
1326
|
-
fast: { command: "bun test tests/fast" },
|
|
1327
|
-
budgetMinutes: 0.001, // 60ms
|
|
1328
|
-
},
|
|
1329
|
-
},
|
|
1330
|
-
} as never);
|
|
1331
|
-
|
|
1332
|
-
const sent: string[] = [];
|
|
1333
|
-
const controller = makeController({
|
|
1334
|
-
provider,
|
|
1335
|
-
dispatch: async (content) => {
|
|
1336
|
-
sent.push(content);
|
|
1337
|
-
},
|
|
1338
|
-
});
|
|
1339
|
-
|
|
1340
|
-
// The first continuation is inside the budget.
|
|
1341
|
-
const first = await controller.steerToContinue(1);
|
|
1342
|
-
expect(first.dispatched).toBe(true);
|
|
1343
|
-
expect(sent.length).toBe(1);
|
|
1344
|
-
|
|
1345
|
-
// After the budget is spent the item pauses at a safe boundary.
|
|
1346
|
-
await new Promise((resolve) => setTimeout(resolve, 90));
|
|
1347
|
-
controller.releaseContinuationGuard("settled");
|
|
1348
|
-
const second = await controller.steerToContinue(1);
|
|
1349
|
-
expect(second.dispatched).toBe(false);
|
|
1350
|
-
expect(second.reason).toContain("budget exhausted");
|
|
1351
|
-
expect(sent.length).toBe(1);
|
|
1352
|
-
});
|
|
1353
|
-
|
|
1354
|
-
it("reports the recorded tier state in the continuation message", async () => {
|
|
1355
|
-
const dir = mkdtempSync(join(tmpdir(), "validation-ledger-"));
|
|
1356
|
-
try {
|
|
1357
|
-
const provider = new MockTodoProvider();
|
|
1358
|
-
provider.addItem({
|
|
1359
|
-
subject: "staged work",
|
|
1360
|
-
status: "in_progress",
|
|
1361
|
-
metadata: {
|
|
1362
|
-
validationPlan: {
|
|
1363
|
-
fast: { command: "bun test tests/fast" },
|
|
1364
|
-
full: { command: "bun test tests/full" },
|
|
1365
|
-
finalTierRequired: true,
|
|
1366
|
-
},
|
|
1367
|
-
},
|
|
1368
|
-
} as never);
|
|
1369
|
-
|
|
1370
|
-
const ledger = new ValidationLedger({ path: join(dir, "runs.jsonl") });
|
|
1371
|
-
ledger.record({ tier: "fast", status: "passed", itemId: 1 });
|
|
1372
|
-
|
|
1373
|
-
const sent: string[] = [];
|
|
1374
|
-
const controller = new TodoContinuationController(makeCtx(), {
|
|
1375
|
-
dispatch: async (content) => {
|
|
1376
|
-
sent.push(content);
|
|
1377
|
-
},
|
|
1378
|
-
todoProvider: provider,
|
|
1379
|
-
validationLedger: ledger,
|
|
1380
|
-
});
|
|
1381
|
-
|
|
1382
|
-
await controller.steerToContinue(1);
|
|
1383
|
-
expect(sent[0]).toContain("Fast tier: passed");
|
|
1384
|
-
expect(sent[0]).toContain("do not complete this todo while it is pending or failed");
|
|
1385
|
-
|
|
1386
|
-
// Once the final tier passed, the message says so.
|
|
1387
|
-
ledger.record({ tier: "full", status: "passed", itemId: 1 });
|
|
1388
|
-
controller.releaseContinuationGuard("settled");
|
|
1389
|
-
await controller.steerToContinue(1);
|
|
1390
|
-
expect(sent[1]).toContain("Full tier: passed");
|
|
1391
|
-
expect(sent[1]).toContain("The final tier already passed");
|
|
1392
|
-
} finally {
|
|
1393
|
-
rmSync(dir, { recursive: true, force: true });
|
|
1394
|
-
}
|
|
1395
|
-
});
|
|
1396
|
-
});
|