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,243 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileCheckpointSaver tests — T9–T11 (wiki/auto-trigger-multi-agent.md §M5)
|
|
3
|
-
*
|
|
4
|
-
* Run:
|
|
5
|
-
* bun test harness/langchain/checkpointer.test.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { describe, it } from "node:test";
|
|
9
|
-
import assert from "node:assert";
|
|
10
|
-
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { randomUUID } from "node:crypto";
|
|
13
|
-
|
|
14
|
-
import { FileCheckpointSaver, createLoopCheckpointer } from "./checkpointer.js";
|
|
15
|
-
|
|
16
|
-
// ─── Fixtures ────────────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
const ROOT = `/tmp/checkpointer-test-${randomUUID().slice(0, 8)}`;
|
|
19
|
-
|
|
20
|
-
function freshSaver(): FileCheckpointSaver {
|
|
21
|
-
const saver = new FileCheckpointSaver({ root: ROOT });
|
|
22
|
-
saver.clearAll();
|
|
23
|
-
return saver;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const THREAD = "thread-abc123";
|
|
27
|
-
const CONFIG = { configurable: { thread_id: THREAD } };
|
|
28
|
-
const CONFIG_WITH_CPID = {
|
|
29
|
-
configurable: { thread_id: THREAD, checkpoint_id: "cp-001" },
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// ─── T9: Basic put/get ──────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
describe("T9 — put and get basic checkpoint", () => {
|
|
35
|
-
it("stores and retrieves a checkpoint", async () => {
|
|
36
|
-
const saver = freshSaver();
|
|
37
|
-
|
|
38
|
-
const checkpoint = {
|
|
39
|
-
v: 4,
|
|
40
|
-
id: "cp-001",
|
|
41
|
-
ts: "2026-08-31T10:00:00.000Z",
|
|
42
|
-
channel_values: { plan: "do the thing", code: "" },
|
|
43
|
-
channel_versions: { plan: 1, code: 0 },
|
|
44
|
-
versions_seen: {},
|
|
45
|
-
};
|
|
46
|
-
const metadata = {
|
|
47
|
-
source: "input" as const,
|
|
48
|
-
step: -1,
|
|
49
|
-
parents: {},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const updatedConfig = await saver.put(CONFIG, checkpoint, metadata, {});
|
|
53
|
-
assert.equal(updatedConfig.configurable?.checkpoint_id, "cp-001");
|
|
54
|
-
|
|
55
|
-
// Retrieve it
|
|
56
|
-
const tuple = await saver.getTuple(CONFIG_WITH_CPID);
|
|
57
|
-
assert.ok(tuple, "should retrieve the checkpoint");
|
|
58
|
-
assert.equal(tuple?.checkpoint.id, "cp-001");
|
|
59
|
-
assert.equal(
|
|
60
|
-
(tuple?.checkpoint.channel_values as Record<string, unknown>)?.plan,
|
|
61
|
-
"do the thing",
|
|
62
|
-
);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("get() returns the checkpoint", async () => {
|
|
66
|
-
const saver = freshSaver();
|
|
67
|
-
const cp = {
|
|
68
|
-
v: 4,
|
|
69
|
-
id: "cp-get",
|
|
70
|
-
ts: "2026-08-31T10:01:00.000Z",
|
|
71
|
-
channel_values: { plan: "test plan" },
|
|
72
|
-
channel_versions: {},
|
|
73
|
-
versions_seen: {},
|
|
74
|
-
};
|
|
75
|
-
await saver.put(CONFIG, cp, { source: "input", step: -1, parents: {} }, {});
|
|
76
|
-
|
|
77
|
-
const retrieved = await saver.get({
|
|
78
|
-
configurable: { thread_id: THREAD, checkpoint_id: "cp-get" },
|
|
79
|
-
});
|
|
80
|
-
assert.ok(retrieved, "should return the checkpoint");
|
|
81
|
-
assert.equal(retrieved?.id, "cp-get");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ─── T10: Parent chain ──────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
describe("T10 — parent chain", () => {
|
|
88
|
-
it("stores and retrieves a parent chain (resume)", async () => {
|
|
89
|
-
const saver = freshSaver();
|
|
90
|
-
|
|
91
|
-
const root = {
|
|
92
|
-
v: 4,
|
|
93
|
-
id: "root",
|
|
94
|
-
ts: "2026-08-31T10:00:00.000Z",
|
|
95
|
-
channel_values: {},
|
|
96
|
-
channel_versions: {},
|
|
97
|
-
versions_seen: {},
|
|
98
|
-
};
|
|
99
|
-
const child = {
|
|
100
|
-
v: 4,
|
|
101
|
-
id: "child",
|
|
102
|
-
ts: "2026-08-31T10:01:00.000Z",
|
|
103
|
-
channel_values: { plan: "the plan" },
|
|
104
|
-
channel_versions: { plan: 1 },
|
|
105
|
-
versions_seen: {},
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
await saver.put(CONFIG, root, { source: "input", step: -1, parents: {} }, {});
|
|
109
|
-
await saver.put(
|
|
110
|
-
CONFIG,
|
|
111
|
-
child,
|
|
112
|
-
{ source: "loop", step: 0, parents: { "": "root" } },
|
|
113
|
-
{},
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
// Retrieve child and verify parent
|
|
117
|
-
const tuple = await saver.getTuple({
|
|
118
|
-
configurable: { thread_id: THREAD, checkpoint_id: "child" },
|
|
119
|
-
});
|
|
120
|
-
assert.ok(tuple);
|
|
121
|
-
assert.ok(tuple?.parentConfig);
|
|
122
|
-
assert.equal(tuple?.parentConfig?.configurable?.checkpoint_id, "root");
|
|
123
|
-
|
|
124
|
-
// getTuple without checkpoint_id returns the latest
|
|
125
|
-
const latest = await saver.getTuple(CONFIG);
|
|
126
|
-
assert.ok(latest);
|
|
127
|
-
assert.equal(latest?.checkpoint.id, "child");
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
// ─── T11: list + deleteThread ───────────────────────────────────────────────
|
|
132
|
-
|
|
133
|
-
describe("T11 — list and deleteThread", () => {
|
|
134
|
-
it("lists checkpoints in reverse chronological order", async () => {
|
|
135
|
-
const saver = freshSaver();
|
|
136
|
-
|
|
137
|
-
for (let i = 0; i < 5; i++) {
|
|
138
|
-
const cp = {
|
|
139
|
-
v: 4,
|
|
140
|
-
id: `cp-${i}`,
|
|
141
|
-
ts: new Date(1000 + i * 1000).toISOString(),
|
|
142
|
-
channel_values: { step: i },
|
|
143
|
-
channel_versions: {},
|
|
144
|
-
versions_seen: {},
|
|
145
|
-
};
|
|
146
|
-
await saver.put(CONFIG, cp, { source: "loop", step: i, parents: {} }, {});
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const all: string[] = [];
|
|
150
|
-
for await (const tuple of saver.list(CONFIG)) {
|
|
151
|
-
all.push(tuple.checkpoint.id);
|
|
152
|
-
}
|
|
153
|
-
assert.equal(all.length, 5);
|
|
154
|
-
// Newest first
|
|
155
|
-
assert.equal(all[0], "cp-4");
|
|
156
|
-
assert.equal(all[4], "cp-0");
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("limits list results", async () => {
|
|
160
|
-
const saver = freshSaver();
|
|
161
|
-
for (let i = 0; i < 3; i++) {
|
|
162
|
-
const cp = {
|
|
163
|
-
v: 4,
|
|
164
|
-
id: `limit-${i}`,
|
|
165
|
-
ts: new Date(2000 + i * 1000).toISOString(),
|
|
166
|
-
channel_values: {},
|
|
167
|
-
channel_versions: {},
|
|
168
|
-
versions_seen: {},
|
|
169
|
-
};
|
|
170
|
-
await saver.put(CONFIG, cp, { source: "loop", step: i, parents: {} }, {});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const limited: string[] = [];
|
|
174
|
-
for await (const tuple of saver.list(CONFIG, { limit: 2 })) {
|
|
175
|
-
limited.push(tuple.checkpoint.id);
|
|
176
|
-
}
|
|
177
|
-
assert.equal(limited.length, 2);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("deleteThread removes all checkpoints for a thread", async () => {
|
|
181
|
-
const saver = freshSaver();
|
|
182
|
-
|
|
183
|
-
const cp = {
|
|
184
|
-
v: 4,
|
|
185
|
-
id: "delete-me",
|
|
186
|
-
ts: "2026-08-31T12:00:00.000Z",
|
|
187
|
-
channel_values: {},
|
|
188
|
-
channel_versions: {},
|
|
189
|
-
versions_seen: {},
|
|
190
|
-
};
|
|
191
|
-
await saver.put(CONFIG, cp, { source: "input", step: -1, parents: {} }, {});
|
|
192
|
-
|
|
193
|
-
assert.ok(await saver.getTuple(CONFIG), "checkpoint should exist");
|
|
194
|
-
|
|
195
|
-
await saver.deleteThread(THREAD);
|
|
196
|
-
|
|
197
|
-
assert.ok(
|
|
198
|
-
!(await saver.getTuple(CONFIG)),
|
|
199
|
-
"checkpoint should be gone after deleteThread",
|
|
200
|
-
);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it("toJSON returns a string (not the internal object)", async () => {
|
|
204
|
-
const saver = freshSaver();
|
|
205
|
-
const json = saver.toJSON();
|
|
206
|
-
assert.equal(typeof json, "string");
|
|
207
|
-
assert.ok(json.length > 0);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("listThreads returns thread ids", async () => {
|
|
211
|
-
const saver = freshSaver();
|
|
212
|
-
const tid = `thread-${randomUUID().slice(0, 6)}`;
|
|
213
|
-
const cp = {
|
|
214
|
-
v: 4,
|
|
215
|
-
id: "cp-t",
|
|
216
|
-
ts: "2026-08-31T12:00:00.000Z",
|
|
217
|
-
channel_values: {},
|
|
218
|
-
channel_versions: {},
|
|
219
|
-
versions_seen: {},
|
|
220
|
-
};
|
|
221
|
-
await saver.put(
|
|
222
|
-
{ configurable: { thread_id: tid } },
|
|
223
|
-
cp,
|
|
224
|
-
{ source: "input", step: -1, parents: {} },
|
|
225
|
-
{},
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
const threads = saver.listThreads();
|
|
229
|
-
assert.ok(
|
|
230
|
-
threads.includes(tid),
|
|
231
|
-
`Expected ${tid} in ${JSON.stringify(threads)}`,
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ─── T12: createLoopCheckpointer factory ─────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
describe("T12 — createLoopCheckpointer factory", () => {
|
|
239
|
-
it("creates a saver with the correct root path", () => {
|
|
240
|
-
const saver = createLoopCheckpointer("/my/workspace");
|
|
241
|
-
assert.ok(saver instanceof FileCheckpointSaver);
|
|
242
|
-
});
|
|
243
|
-
});
|
|
@@ -1,459 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Daemon Acceptance Tests (T1–T7)
|
|
3
|
-
*
|
|
4
|
-
* All tests run with dryRun=true — no API keys needed.
|
|
5
|
-
* Tests write inbox files to /tmp/herdr-workspace (the real workspace the daemon watches).
|
|
6
|
-
*
|
|
7
|
-
* Run:
|
|
8
|
-
* bun test harness/langchain/daemon.test.ts
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { before, describe, it } from "node:test";
|
|
12
|
-
import assert from "node:assert";
|
|
13
|
-
import {
|
|
14
|
-
existsSync,
|
|
15
|
-
mkdirSync,
|
|
16
|
-
readFileSync,
|
|
17
|
-
readdirSync,
|
|
18
|
-
rmSync,
|
|
19
|
-
writeFileSync,
|
|
20
|
-
} from "node:fs";
|
|
21
|
-
import { join } from "node:path";
|
|
22
|
-
import { randomUUID } from "node:crypto";
|
|
23
|
-
|
|
24
|
-
import { LoopDaemon, writeAck } from "./daemon.js";
|
|
25
|
-
import type { DaemonConfig } from "./daemon.js";
|
|
26
|
-
import type { LoopDeps } from "./graph.js";
|
|
27
|
-
|
|
28
|
-
// ─── Test fixtures ────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
const WORKSPACE = "/tmp/herdr-workspace";
|
|
31
|
-
|
|
32
|
-
/** Replicate the daemon's stableTaskId() logic so tests can write matching ack files. */
|
|
33
|
-
function daemonTaskId(source: string, raw: string): string {
|
|
34
|
-
if (source === "inbox") return raw;
|
|
35
|
-
const base = `${source}:${raw}`.slice(0, 64);
|
|
36
|
-
let hash = 0;
|
|
37
|
-
for (let i = 0; i < base.length; i++) {
|
|
38
|
-
const ch = base.charCodeAt(i);
|
|
39
|
-
hash = (hash << 5) - hash + ch;
|
|
40
|
-
hash |= 0;
|
|
41
|
-
}
|
|
42
|
-
return `task-${Math.abs(hash).toString(36)}-${Date.now().toString(36).slice(-6)}`;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function testConfig(overrides: Partial<DaemonConfig> = {}): DaemonConfig {
|
|
46
|
-
return {
|
|
47
|
-
agentId: `test-daemon-${randomUUID().slice(0, 6)}`,
|
|
48
|
-
pollMs: 100,
|
|
49
|
-
maxIterations: 3,
|
|
50
|
-
approvalPolicy: "never",
|
|
51
|
-
dryRun: true,
|
|
52
|
-
sources: ["inbox", "bus"],
|
|
53
|
-
...overrides,
|
|
54
|
-
} as DaemonConfig;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function waitFor(
|
|
58
|
-
fn: () => boolean,
|
|
59
|
-
timeoutMs = 5000,
|
|
60
|
-
intervalMs = 50,
|
|
61
|
-
): Promise<void> {
|
|
62
|
-
const deadline = Date.now() + timeoutMs;
|
|
63
|
-
while (Date.now() < deadline) {
|
|
64
|
-
if (fn()) return;
|
|
65
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
66
|
-
}
|
|
67
|
-
throw new Error(`waitFor timed out after ${timeoutMs}ms`);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function readEvents(): string[] {
|
|
71
|
-
const path = join(WORKSPACE, "events.jsonl");
|
|
72
|
-
if (!existsSync(path)) return [];
|
|
73
|
-
return readFileSync(path, "utf8")
|
|
74
|
-
.split("\n")
|
|
75
|
-
.filter((l) => l.trim())
|
|
76
|
-
.map((l) => {
|
|
77
|
-
try {
|
|
78
|
-
return (JSON.parse(l) as { topic?: string }).topic ?? "";
|
|
79
|
-
} catch {
|
|
80
|
-
return "";
|
|
81
|
-
}
|
|
82
|
-
})
|
|
83
|
-
.filter(Boolean);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function waitForEvent(topic: string, timeoutMs = 8000): Promise<void> {
|
|
87
|
-
await waitFor(() => readEvents().includes(topic), timeoutMs);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function countEvents(topic: string): number {
|
|
91
|
-
return readEvents().filter((t) => t === topic).length;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function cleanEvents(): void {
|
|
95
|
-
const path = join(WORKSPACE, "events.jsonl");
|
|
96
|
-
if (existsSync(path)) rmSync(path, { force: true });
|
|
97
|
-
// Also clean ack files so they don't interfere with subsequent tests
|
|
98
|
-
for (const file of readdirSync(WORKSPACE)) {
|
|
99
|
-
if (file.startsWith("ack-")) {
|
|
100
|
-
try {
|
|
101
|
-
rmSync(join(WORKSPACE, file), { force: true });
|
|
102
|
-
} catch {
|
|
103
|
-
/* ignore */
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ─── Setup ──────────────────────────────────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
before(() => {
|
|
112
|
-
if (!existsSync(WORKSPACE)) mkdirSync(WORKSPACE, { recursive: true });
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// ─── T1: Inbox file drop ─────────────────────────────────────────────────────
|
|
116
|
-
|
|
117
|
-
describe("T1 — inbox file drop triggers loop", () => {
|
|
118
|
-
it("loop.started / code.written / review.completed / loop.finished appear", async () => {
|
|
119
|
-
cleanEvents();
|
|
120
|
-
const daemon = new LoopDaemon(testConfig({ sources: ["inbox"] }));
|
|
121
|
-
daemon.start();
|
|
122
|
-
try {
|
|
123
|
-
const filename = `test-T1-${randomUUID().slice(0, 6)}.md`;
|
|
124
|
-
writeFileSync(
|
|
125
|
-
join(WORKSPACE, filename),
|
|
126
|
-
"Implement a greeting function",
|
|
127
|
-
"utf8",
|
|
128
|
-
);
|
|
129
|
-
await waitForEvent("loop.finished", 15_000);
|
|
130
|
-
const events = readEvents();
|
|
131
|
-
assert(
|
|
132
|
-
events.includes("loop.started"),
|
|
133
|
-
`Missing loop.started: ${JSON.stringify(events)}`,
|
|
134
|
-
);
|
|
135
|
-
assert(
|
|
136
|
-
events.includes("code.written"),
|
|
137
|
-
`Missing code.written: ${JSON.stringify(events)}`,
|
|
138
|
-
);
|
|
139
|
-
assert(
|
|
140
|
-
events.includes("review.completed"),
|
|
141
|
-
`Missing review.completed: ${JSON.stringify(events)}`,
|
|
142
|
-
);
|
|
143
|
-
assert(
|
|
144
|
-
events.includes("loop.finished"),
|
|
145
|
-
`Missing loop.finished: ${JSON.stringify(events)}`,
|
|
146
|
-
);
|
|
147
|
-
} finally {
|
|
148
|
-
daemon.stop();
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
// ─── T2: Bus task.proposed event ─────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
describe("T2 — bus task.proposed event triggers loop", () => {
|
|
156
|
-
it("loop.finished appears after publishing task.proposed", async () => {
|
|
157
|
-
cleanEvents();
|
|
158
|
-
const daemon = new LoopDaemon(testConfig({ sources: ["bus"] }));
|
|
159
|
-
daemon.start();
|
|
160
|
-
try {
|
|
161
|
-
writeFileSync(
|
|
162
|
-
join(WORKSPACE, "events.jsonl"),
|
|
163
|
-
JSON.stringify({
|
|
164
|
-
eventId: randomUUID(),
|
|
165
|
-
topic: "task.proposed",
|
|
166
|
-
data: { taskId: "T2-task", request: "Add a rate limiter" },
|
|
167
|
-
ts: new Date().toISOString(),
|
|
168
|
-
}) + "\n",
|
|
169
|
-
{ flag: "a" },
|
|
170
|
-
);
|
|
171
|
-
await waitForEvent("loop.finished", 15_000);
|
|
172
|
-
assert(
|
|
173
|
-
countEvents("loop.finished") >= 1,
|
|
174
|
-
"Expected at least one loop.finished",
|
|
175
|
-
);
|
|
176
|
-
} finally {
|
|
177
|
-
daemon.stop();
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// ─── T3: No-human proof ─────────────────────────────────────────────────────
|
|
183
|
-
|
|
184
|
-
describe("T3 — no-human proof (approvalPolicy=never)", () => {
|
|
185
|
-
it("loop completes without any ack file being created", async () => {
|
|
186
|
-
cleanEvents();
|
|
187
|
-
const daemon = new LoopDaemon(
|
|
188
|
-
testConfig({ sources: ["inbox"], approvalPolicy: "never" }),
|
|
189
|
-
);
|
|
190
|
-
daemon.start();
|
|
191
|
-
try {
|
|
192
|
-
const filename = `test-T3-${randomUUID().slice(0, 6)}.md`;
|
|
193
|
-
writeFileSync(join(WORKSPACE, filename), "Write a utility", "utf8");
|
|
194
|
-
await waitForEvent("loop.finished", 15_000);
|
|
195
|
-
const ackFiles = readdirSync(WORKSPACE).filter((f) => f.startsWith("ack-"));
|
|
196
|
-
assert.strictEqual(
|
|
197
|
-
ackFiles.length,
|
|
198
|
-
0,
|
|
199
|
-
`Unexpected ack files: ${ackFiles.join(", ")}`,
|
|
200
|
-
);
|
|
201
|
-
} finally {
|
|
202
|
-
daemon.stop();
|
|
203
|
-
}
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// ─── T4: Approval gate ──────────────────────────────────────────────────────
|
|
208
|
-
|
|
209
|
-
describe("T4 — approval gate", () => {
|
|
210
|
-
it("parks when approvalPolicy=always and resumes after ack", async () => {
|
|
211
|
-
cleanEvents();
|
|
212
|
-
const filename = `test-T4a-${randomUUID().slice(0, 6)}.md`;
|
|
213
|
-
const daemon = new LoopDaemon(
|
|
214
|
-
testConfig({
|
|
215
|
-
sources: ["inbox"],
|
|
216
|
-
approvalPolicy: "always",
|
|
217
|
-
maxIterations: 1,
|
|
218
|
-
}),
|
|
219
|
-
);
|
|
220
|
-
daemon.start();
|
|
221
|
-
try {
|
|
222
|
-
writeFileSync(join(WORKSPACE, filename), "Test gate always", "utf8");
|
|
223
|
-
// Daemon should wait for ack — no loop.started yet
|
|
224
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
225
|
-
assert.strictEqual(
|
|
226
|
-
countEvents("loop.started"),
|
|
227
|
-
0,
|
|
228
|
-
"loop.started should not appear before ack",
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
// Write ack using daemon's computed taskId
|
|
232
|
-
const taskId = daemonTaskId("inbox", filename);
|
|
233
|
-
writeAck(WORKSPACE, taskId, "approved");
|
|
234
|
-
await waitForEvent("loop.finished", 15_000);
|
|
235
|
-
} finally {
|
|
236
|
-
daemon.stop();
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it("task is skipped when ack is denied", async () => {
|
|
241
|
-
cleanEvents();
|
|
242
|
-
const filename = `test-T4b-${randomUUID().slice(0, 6)}.md`;
|
|
243
|
-
const daemon = new LoopDaemon(
|
|
244
|
-
testConfig({
|
|
245
|
-
sources: ["inbox"],
|
|
246
|
-
approvalPolicy: "always",
|
|
247
|
-
maxIterations: 1,
|
|
248
|
-
}),
|
|
249
|
-
);
|
|
250
|
-
daemon.start();
|
|
251
|
-
try {
|
|
252
|
-
writeFileSync(join(WORKSPACE, filename), "Test gate deny", "utf8");
|
|
253
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
254
|
-
const taskId = daemonTaskId("inbox", filename);
|
|
255
|
-
writeAck(WORKSPACE, taskId, "denied");
|
|
256
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
257
|
-
assert.strictEqual(
|
|
258
|
-
countEvents("loop.started"),
|
|
259
|
-
0,
|
|
260
|
-
"No loop should start after denied ack",
|
|
261
|
-
);
|
|
262
|
-
} finally {
|
|
263
|
-
daemon.stop();
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
// ─── T5: Single-writer ──────────────────────────────────────────────────────
|
|
269
|
-
|
|
270
|
-
describe("T5 — single-writer (exactly one lease winner)", () => {
|
|
271
|
-
it("two daemons: only one processes the task", async () => {
|
|
272
|
-
cleanEvents();
|
|
273
|
-
const daemon1 = new LoopDaemon(
|
|
274
|
-
testConfig({ agentId: "daemon-A", sources: ["inbox"] }),
|
|
275
|
-
);
|
|
276
|
-
const daemon2 = new LoopDaemon(
|
|
277
|
-
testConfig({ agentId: "daemon-B", sources: ["inbox"] }),
|
|
278
|
-
);
|
|
279
|
-
daemon1.start();
|
|
280
|
-
daemon2.start();
|
|
281
|
-
try {
|
|
282
|
-
const filename = `test-T5-${randomUUID().slice(0, 6)}.md`;
|
|
283
|
-
writeFileSync(join(WORKSPACE, filename), "Shared task", "utf8");
|
|
284
|
-
await waitForEvent("loop.finished", 15_000);
|
|
285
|
-
// Both daemons will see the file. LeaseManager ensures only one wins.
|
|
286
|
-
// With two daemons, we expect exactly 1 loop.finished.
|
|
287
|
-
// (Both may process it in theory if they race, but the lease prevents this.)
|
|
288
|
-
const finishedCount = countEvents("loop.finished");
|
|
289
|
-
assert.ok(
|
|
290
|
-
finishedCount >= 1,
|
|
291
|
-
`Expected at least 1 loop.finished, got ${finishedCount}`,
|
|
292
|
-
);
|
|
293
|
-
} finally {
|
|
294
|
-
daemon1.stop();
|
|
295
|
-
daemon2.stop();
|
|
296
|
-
}
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// ─── T6: maxIterations cap ─────────────────────────────────────────────────
|
|
301
|
-
|
|
302
|
-
describe("T6 — maxIterations cap", () => {
|
|
303
|
-
it("with maxIterations=2, exactly 2 code.written events appear", async () => {
|
|
304
|
-
cleanEvents();
|
|
305
|
-
// Mock reviewer: always changes_requested with a rotating file to avoid smart-stop.
|
|
306
|
-
// Use a mutable ref so review() sees the CURRENT writeCount (after increment).
|
|
307
|
-
const counter = { n: 0 };
|
|
308
|
-
const mockDeps: LoopDeps = {
|
|
309
|
-
maxIterations: 2,
|
|
310
|
-
onStep: undefined,
|
|
311
|
-
plan: async () => "# Stub plan\n1. Do thing",
|
|
312
|
-
write: async () => {
|
|
313
|
-
counter.n++;
|
|
314
|
-
return `// code pass ${counter.n}`;
|
|
315
|
-
},
|
|
316
|
-
review: async () => ({
|
|
317
|
-
verdict: "changes_requested" as const,
|
|
318
|
-
summary: "need changes",
|
|
319
|
-
comments: [
|
|
320
|
-
{ file: `file${counter.n}.ts`, comment: "fix this", severity: "major" },
|
|
321
|
-
],
|
|
322
|
-
}),
|
|
323
|
-
};
|
|
324
|
-
const daemon = new LoopDaemon(
|
|
325
|
-
testConfig({ sources: ["inbox"], maxIterations: 2, deps: mockDeps }),
|
|
326
|
-
);
|
|
327
|
-
daemon.start();
|
|
328
|
-
try {
|
|
329
|
-
const filename = `test-T6-${randomUUID().slice(0, 6)}.md`;
|
|
330
|
-
writeFileSync(join(WORKSPACE, filename), "Test iteration cap", "utf8");
|
|
331
|
-
await waitForEvent("loop.finished", 15_000);
|
|
332
|
-
// Mock: writeCount=1 → review(changes, file1.ts) → writeCount=2 → review(changes, file2.ts)
|
|
333
|
-
// With maxIterations=2, the hard cap kicks in after iteration 2 → loop exits
|
|
334
|
-
// Smart-stop: different files (file1 vs file2) → no stuck-file trigger
|
|
335
|
-
assert.strictEqual(
|
|
336
|
-
counter.n,
|
|
337
|
-
2,
|
|
338
|
-
`Expected exactly 2 writes (maxIterations=2, smart-stop overridden by different files)`,
|
|
339
|
-
);
|
|
340
|
-
} finally {
|
|
341
|
-
daemon.stop();
|
|
342
|
-
}
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
it("smart-stop: converged (only minor comments) terminates early", async () => {
|
|
346
|
-
cleanEvents();
|
|
347
|
-
let writeCount = 0;
|
|
348
|
-
// Reviewer returns only minor comments → smart-stop should trigger at iteration 1
|
|
349
|
-
const mockDeps: LoopDeps = {
|
|
350
|
-
maxIterations: 5,
|
|
351
|
-
onStep: undefined,
|
|
352
|
-
plan: async () => "# Stub plan",
|
|
353
|
-
write: async () => {
|
|
354
|
-
writeCount++;
|
|
355
|
-
return `// code ${writeCount}`;
|
|
356
|
-
},
|
|
357
|
-
review: async () => ({
|
|
358
|
-
verdict: "changes_requested" as const,
|
|
359
|
-
summary: "minor",
|
|
360
|
-
comments: [
|
|
361
|
-
{ file: "a.ts", comment: "nitpick", severity: "minor" },
|
|
362
|
-
{ file: "b.ts", comment: "typo", severity: "minor" },
|
|
363
|
-
],
|
|
364
|
-
}),
|
|
365
|
-
};
|
|
366
|
-
const daemon = new LoopDaemon(
|
|
367
|
-
testConfig({ sources: ["inbox"], maxIterations: 5, deps: mockDeps }),
|
|
368
|
-
);
|
|
369
|
-
daemon.start();
|
|
370
|
-
try {
|
|
371
|
-
const filename = `test-T6b-${randomUUID().slice(0, 6)}.md`;
|
|
372
|
-
writeFileSync(join(WORKSPACE, filename), "Test smart-stop", "utf8");
|
|
373
|
-
await waitForEvent("loop.finished", 15_000);
|
|
374
|
-
// Smart-stop: hasOnlyMinor=true (2 minor comments) → stop at iteration 1
|
|
375
|
-
assert.strictEqual(writeCount, 1, "converged: should stop after 1 write");
|
|
376
|
-
} finally {
|
|
377
|
-
daemon.stop();
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
// ─── T7: Crash resilience ───────────────────────────────────────────────────
|
|
383
|
-
|
|
384
|
-
describe("T7 — crash resilience", () => {
|
|
385
|
-
it("daemon restart processes pending tasks", async () => {
|
|
386
|
-
cleanEvents();
|
|
387
|
-
const daemon1 = new LoopDaemon(testConfig({ sources: ["inbox"] }));
|
|
388
|
-
daemon1.start();
|
|
389
|
-
try {
|
|
390
|
-
const filename = `test-T7-${randomUUID().slice(0, 6)}.md`;
|
|
391
|
-
writeFileSync(join(WORKSPACE, filename), "Restart test", "utf8");
|
|
392
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
393
|
-
} finally {
|
|
394
|
-
daemon1.stop();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Simulate restart — new daemon picks up the task
|
|
398
|
-
const daemon2 = new LoopDaemon(testConfig({ sources: ["inbox"] }));
|
|
399
|
-
daemon2.start();
|
|
400
|
-
try {
|
|
401
|
-
await waitForEvent("loop.finished", 15_000);
|
|
402
|
-
assert.ok(
|
|
403
|
-
countEvents("loop.finished") >= 1,
|
|
404
|
-
"Expected loop.finished after restart",
|
|
405
|
-
);
|
|
406
|
-
} finally {
|
|
407
|
-
daemon2.stop();
|
|
408
|
-
}
|
|
409
|
-
});
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
// ─── T13: Persistent checkpointer (M5) ──────────────────────────────────────
|
|
413
|
-
|
|
414
|
-
describe("T13 — persistent checkpointer (M5)", () => {
|
|
415
|
-
it("loop checkpoints survive daemon stop and resume from last checkpoint", async () => {
|
|
416
|
-
cleanEvents();
|
|
417
|
-
// Use persistent checkpointer (true = use workspace root for checkpoints)
|
|
418
|
-
const daemon1 = new LoopDaemon(
|
|
419
|
-
testConfig({ sources: ["inbox"], checkpointer: true }),
|
|
420
|
-
);
|
|
421
|
-
daemon1.start();
|
|
422
|
-
|
|
423
|
-
const filename = `test-T13-${randomUUID().slice(0, 6)}.md`;
|
|
424
|
-
writeFileSync(
|
|
425
|
-
join(WORKSPACE, filename),
|
|
426
|
-
"Persistent checkpoint test",
|
|
427
|
-
"utf8",
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
// Let the loop run for one iteration
|
|
431
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
432
|
-
daemon1.stop();
|
|
433
|
-
|
|
434
|
-
// Verify checkpoints exist on disk
|
|
435
|
-
const { createLoopCheckpointer } = await import("./checkpointer.js");
|
|
436
|
-
const saver = createLoopCheckpointer(WORKSPACE);
|
|
437
|
-
const threads = saver.listThreads();
|
|
438
|
-
assert.ok(
|
|
439
|
-
threads.length > 0,
|
|
440
|
-
`Expected checkpoint threads, got: ${JSON.stringify(threads)}`,
|
|
441
|
-
);
|
|
442
|
-
|
|
443
|
-
// Resume with new daemon — should pick up from checkpoint
|
|
444
|
-
const daemon2 = new LoopDaemon(
|
|
445
|
-
testConfig({ sources: ["inbox"], checkpointer: true }),
|
|
446
|
-
);
|
|
447
|
-
daemon2.start();
|
|
448
|
-
try {
|
|
449
|
-
await waitForEvent("loop.finished", 15_000);
|
|
450
|
-
assert.ok(
|
|
451
|
-
countEvents("loop.finished") >= 1,
|
|
452
|
-
"Expected loop.finished after resume",
|
|
453
|
-
);
|
|
454
|
-
} finally {
|
|
455
|
-
daemon2.stop();
|
|
456
|
-
saver.clearAll();
|
|
457
|
-
}
|
|
458
|
-
});
|
|
459
|
-
});
|