@cohortapp/agent-sdk 2.16.0 → 2.18.4
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/.claude/settings.json +18 -0
- package/.env.example +23 -7
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/poller-daemon-setup.md +4 -1
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/context/budget.mjs +327 -0
- package/lib/context/history-scope.mjs +138 -0
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/economics.mjs +9 -0
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +95 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +82 -4
- package/lib/org/inbound/hydrate.mjs +555 -51
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +15 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +104 -59
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +804 -157
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/lib/session-router.mjs +310 -42
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +121 -12
- package/scripts/daemon/responder.mjs +315 -146
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -453
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -300
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -295
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -344
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -185
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -1,344 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dynamic-jobs.test.mjs — node:test coverage for agent-created scheduled jobs.
|
|
3
|
-
*
|
|
4
|
-
* Every test gets its own AGENT_ROOT under tmpdir() and cleans up. Time is
|
|
5
|
-
* always injected via `now` so the suite is hermetic and offline. Real
|
|
6
|
-
* assertions: parseWhen output, persistence, cancel, and the coalescing /
|
|
7
|
-
* TTL / notBefore firing decision.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { test } from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { promises as fsp } from "fs";
|
|
13
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
14
|
-
import { tmpdir } from "os";
|
|
15
|
-
import { join } from "path";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
parseWhen,
|
|
19
|
-
nextCronAfter,
|
|
20
|
-
createJob,
|
|
21
|
-
listJobs,
|
|
22
|
-
cancelJob,
|
|
23
|
-
deferJob,
|
|
24
|
-
dueJobs,
|
|
25
|
-
readStore,
|
|
26
|
-
getJobsPath,
|
|
27
|
-
JOBS_RELATIVE,
|
|
28
|
-
DEFAULT_TTL_MS,
|
|
29
|
-
} from "./dynamic-jobs.mjs";
|
|
30
|
-
|
|
31
|
-
// 2026-06-10T12:00:00Z is a Wednesday — the fixed reference clock.
|
|
32
|
-
const WED_NOON = Date.UTC(2026, 5, 10, 12, 0, 0);
|
|
33
|
-
|
|
34
|
-
async function makeAgentRoot() {
|
|
35
|
-
const path = join(
|
|
36
|
-
tmpdir(),
|
|
37
|
-
`dynamic-jobs-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
38
|
-
);
|
|
39
|
-
await fsp.mkdir(path, { recursive: true });
|
|
40
|
-
return path;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function rmRoot(path) {
|
|
44
|
-
try { await fsp.rm(path, { recursive: true, force: true }); } catch { /* */ }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
// parseWhen
|
|
49
|
-
// ---------------------------------------------------------------------------
|
|
50
|
-
|
|
51
|
-
test("parseWhen: 'Friday 9am' resolves to the next Friday 9am one-off", () => {
|
|
52
|
-
const r = parseWhen("remind X Friday 9am", { now: WED_NOON, tz: "UTC" });
|
|
53
|
-
assert.ok(r && typeof r.at === "number", "should be a one-off {at}");
|
|
54
|
-
const d = new Date(r.at);
|
|
55
|
-
assert.equal(d.getUTCDay(), 5, "Friday");
|
|
56
|
-
assert.equal(d.getUTCHours(), 9);
|
|
57
|
-
assert.equal(d.getUTCMinutes(), 0);
|
|
58
|
-
// From Wed Jun 10 → Fri Jun 12.
|
|
59
|
-
assert.equal(d.toISOString(), "2026-06-12T09:00:00.000Z");
|
|
60
|
-
assert.equal(r.tz, "UTC");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("parseWhen: 'every weekday 8am' is a recurring cron Mon-Fri", () => {
|
|
64
|
-
const r = parseWhen("every weekday 8am summarize the channel", { now: WED_NOON, tz: "UTC" });
|
|
65
|
-
assert.ok(r && r.cron, "should be recurring {cron}");
|
|
66
|
-
assert.equal(r.cron, "0 8 * * 1-5");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("parseWhen: 'in 2 hours' is a relative one-off", () => {
|
|
70
|
-
const r = parseWhen("ping me in 2 hours", { now: WED_NOON, tz: "UTC" });
|
|
71
|
-
assert.equal(r.at, WED_NOON + 2 * 60 * 60 * 1000);
|
|
72
|
-
assert.equal(new Date(r.at).toISOString(), "2026-06-10T14:00:00.000Z");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("parseWhen: relative units (min/day/week)", () => {
|
|
76
|
-
assert.equal(parseWhen("in 30 minutes", { now: WED_NOON }).at, WED_NOON + 30 * 60_000);
|
|
77
|
-
assert.equal(parseWhen("in 3 days", { now: WED_NOON }).at, WED_NOON + 3 * 86_400_000);
|
|
78
|
-
assert.equal(parseWhen("in 1 week", { now: WED_NOON }).at, WED_NOON + 7 * 86_400_000);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("parseWhen: timezone shifts the resolved instant", () => {
|
|
82
|
-
// 9am Dubai (+4) on Friday → 05:00Z.
|
|
83
|
-
const r = parseWhen("Friday 9am", { now: WED_NOON, tz: "Asia/Dubai" });
|
|
84
|
-
assert.equal(new Date(r.at).toISOString(), "2026-06-12T05:00:00.000Z");
|
|
85
|
-
assert.equal(r.tz, "Asia/Dubai");
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("parseWhen: recurring variants — daily, specific weekday, weekend", () => {
|
|
89
|
-
assert.equal(parseWhen("every day at 7am", { now: WED_NOON }).cron, "0 7 * * *");
|
|
90
|
-
assert.equal(parseWhen("daily 7am", { now: WED_NOON }).cron, "0 7 * * *");
|
|
91
|
-
assert.equal(parseWhen("every monday at 9am", { now: WED_NOON }).cron, "0 9 * * 1");
|
|
92
|
-
assert.equal(parseWhen("every weekend at 10am", { now: WED_NOON }).cron, "0 10 * * 0,6");
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("parseWhen: tomorrow/today and 24h clock", () => {
|
|
96
|
-
assert.equal(
|
|
97
|
-
new Date(parseWhen("tomorrow 9am", { now: WED_NOON }).at).toISOString(),
|
|
98
|
-
"2026-06-11T09:00:00.000Z"
|
|
99
|
-
);
|
|
100
|
-
// 24h time with no am/pm.
|
|
101
|
-
assert.equal(
|
|
102
|
-
new Date(parseWhen("today 14:30", { now: WED_NOON }).at).toISOString(),
|
|
103
|
-
"2026-06-10T14:30:00.000Z"
|
|
104
|
-
);
|
|
105
|
-
// noon / midnight keywords.
|
|
106
|
-
assert.equal(new Date(parseWhen("today noon", { now: WED_NOON }).at).getUTCHours(), 12);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test("parseWhen: bare time rolls to the next occurrence if already past", () => {
|
|
110
|
-
// It's noon; "9am" already passed today → resolves to tomorrow 9am.
|
|
111
|
-
const r = parseWhen("9am", { now: WED_NOON, tz: "UTC" });
|
|
112
|
-
assert.equal(new Date(r.at).toISOString(), "2026-06-11T09:00:00.000Z");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("parseWhen: returns null for unparseable / empty input", () => {
|
|
116
|
-
assert.equal(parseWhen("", { now: WED_NOON }), null);
|
|
117
|
-
assert.equal(parseWhen("just some words", { now: WED_NOON }), null);
|
|
118
|
-
assert.equal(parseWhen(null), null);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// ---------------------------------------------------------------------------
|
|
122
|
-
// nextCronAfter
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
|
|
125
|
-
test("nextCronAfter: weekday 8am from Wed noon → Thu 8am", () => {
|
|
126
|
-
const next = nextCronAfter("0 8 * * 1-5", WED_NOON, "UTC");
|
|
127
|
-
assert.equal(new Date(next).toISOString(), "2026-06-11T08:00:00.000Z");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("nextCronAfter: returns null for a malformed expression", () => {
|
|
131
|
-
assert.equal(nextCronAfter("not a cron", WED_NOON), null);
|
|
132
|
-
assert.equal(nextCronAfter("0 8 * *", WED_NOON), null); // 4 fields
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// ---------------------------------------------------------------------------
|
|
136
|
-
// createJob / listJobs / persistence
|
|
137
|
-
// ---------------------------------------------------------------------------
|
|
138
|
-
|
|
139
|
-
test("createJob persists a one-off to state/scheduling/jobs.json", async () => {
|
|
140
|
-
const root = await makeAgentRoot();
|
|
141
|
-
try {
|
|
142
|
-
const when = parseWhen("Friday 9am", { now: WED_NOON, tz: "UTC" });
|
|
143
|
-
const job = createJob({ agentRoot: root, when, action: "reminder", payload: { who: "Sam" }, now: WED_NOON });
|
|
144
|
-
assert.equal(job.kind, "once");
|
|
145
|
-
assert.equal(job.action, "reminder");
|
|
146
|
-
assert.deepEqual(job.payload, { who: "Sam" });
|
|
147
|
-
assert.equal(job.status, "active");
|
|
148
|
-
assert.equal(job.next_run_at, when.at);
|
|
149
|
-
|
|
150
|
-
// On disk, atomically, at the expected path.
|
|
151
|
-
const path = getJobsPath(root);
|
|
152
|
-
assert.equal(path, join(root, JOBS_RELATIVE));
|
|
153
|
-
assert.ok(existsSync(path));
|
|
154
|
-
const onDisk = JSON.parse(readFileSync(path, "utf-8"));
|
|
155
|
-
assert.equal(onDisk.jobs.length, 1);
|
|
156
|
-
assert.equal(onDisk.jobs[0].id, job.id);
|
|
157
|
-
} finally { await rmRoot(root); }
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("createJob computes next_run_at for recurring cron jobs", async () => {
|
|
161
|
-
const root = await makeAgentRoot();
|
|
162
|
-
try {
|
|
163
|
-
const when = parseWhen("every weekday 8am", { now: WED_NOON });
|
|
164
|
-
const job = createJob({ agentRoot: root, when, action: "summary", now: WED_NOON });
|
|
165
|
-
assert.equal(job.kind, "recurring");
|
|
166
|
-
assert.equal(job.cron, "0 8 * * 1-5");
|
|
167
|
-
// First fire is the next weekday 8am after now.
|
|
168
|
-
assert.equal(new Date(job.next_run_at).toISOString(), "2026-06-11T08:00:00.000Z");
|
|
169
|
-
} finally { await rmRoot(root); }
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("createJob rejects missing when/action", async () => {
|
|
173
|
-
const root = await makeAgentRoot();
|
|
174
|
-
try {
|
|
175
|
-
assert.throws(() => createJob({ agentRoot: root, action: "x" }), /when/);
|
|
176
|
-
assert.throws(() => createJob({ agentRoot: root, when: { at: 1 } }), /action/);
|
|
177
|
-
} finally { await rmRoot(root); }
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test("listJobs returns active jobs only by default", async () => {
|
|
181
|
-
const root = await makeAgentRoot();
|
|
182
|
-
try {
|
|
183
|
-
const a = createJob({ agentRoot: root, when: { at: WED_NOON + 1000 }, action: "a", now: WED_NOON });
|
|
184
|
-
createJob({ agentRoot: root, when: { at: WED_NOON + 2000 }, action: "b", now: WED_NOON });
|
|
185
|
-
cancelJob(root, a.id, { now: WED_NOON });
|
|
186
|
-
const active = listJobs(root);
|
|
187
|
-
assert.equal(active.length, 1);
|
|
188
|
-
assert.equal(active[0].action, "b");
|
|
189
|
-
assert.equal(listJobs(root, { all: true }).length, 2);
|
|
190
|
-
} finally { await rmRoot(root); }
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
// cancelJob
|
|
195
|
-
// ---------------------------------------------------------------------------
|
|
196
|
-
|
|
197
|
-
test("cancelJob marks the job cancelled and stops it firing", async () => {
|
|
198
|
-
const root = await makeAgentRoot();
|
|
199
|
-
try {
|
|
200
|
-
const job = createJob({ agentRoot: root, when: { at: WED_NOON - 1000 }, action: "reminder", now: WED_NOON });
|
|
201
|
-
assert.equal(cancelJob(root, job.id, { now: WED_NOON }), true);
|
|
202
|
-
// Cancelled → not returned by dueJobs even though it is past-due.
|
|
203
|
-
const fired = dueJobs(root, { now: WED_NOON });
|
|
204
|
-
assert.equal(fired.length, 0);
|
|
205
|
-
const stored = readStore(root).jobs[0];
|
|
206
|
-
assert.equal(stored.status, "cancelled");
|
|
207
|
-
assert.ok(stored.cancelled_at);
|
|
208
|
-
// Cancelling an unknown / already-cancelled id returns false.
|
|
209
|
-
assert.equal(cancelJob(root, job.id, { now: WED_NOON }), false);
|
|
210
|
-
assert.equal(cancelJob(root, "job-nope", { now: WED_NOON }), false);
|
|
211
|
-
} finally { await rmRoot(root); }
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// dueJobs — one-off firing + TTL
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
test("dueJobs fires a due one-off once, then marks it done", async () => {
|
|
219
|
-
const root = await makeAgentRoot();
|
|
220
|
-
try {
|
|
221
|
-
const fireAt = WED_NOON;
|
|
222
|
-
createJob({ agentRoot: root, when: { at: fireAt }, action: "reminder", payload: { x: 1 }, now: WED_NOON - 1000 });
|
|
223
|
-
// Not yet due (1ms before).
|
|
224
|
-
assert.equal(dueJobs(root, { now: fireAt - 1 }).length, 0);
|
|
225
|
-
// Due now.
|
|
226
|
-
const fired = dueJobs(root, { now: fireAt });
|
|
227
|
-
assert.equal(fired.length, 1);
|
|
228
|
-
assert.deepEqual(fired[0].payload, { x: 1 });
|
|
229
|
-
// Fired once: a second poll returns nothing, job is done.
|
|
230
|
-
assert.equal(dueJobs(root, { now: fireAt + 1000 }).length, 0);
|
|
231
|
-
assert.equal(readStore(root).jobs[0].status, "done");
|
|
232
|
-
} finally { await rmRoot(root); }
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("dueJobs expires (does NOT fire) a one-off older than its TTL", async () => {
|
|
236
|
-
const root = await makeAgentRoot();
|
|
237
|
-
try {
|
|
238
|
-
const fireAt = WED_NOON;
|
|
239
|
-
createJob({ agentRoot: root, when: { at: fireAt }, action: "reminder", ttlMs: 60_000, now: WED_NOON - 1000 });
|
|
240
|
-
// Poll well past the TTL — daemon was down. Must NOT deliver the stale job.
|
|
241
|
-
const fired = dueJobs(root, { now: fireAt + 5 * 60_000 });
|
|
242
|
-
assert.equal(fired.length, 0, "stale one-off must not fire late");
|
|
243
|
-
assert.equal(readStore(root).jobs[0].status, "expired");
|
|
244
|
-
} finally { await rmRoot(root); }
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ---------------------------------------------------------------------------
|
|
248
|
-
// dueJobs — recurring coalescing (the missed-window-no-burst guarantee)
|
|
249
|
-
// ---------------------------------------------------------------------------
|
|
250
|
-
|
|
251
|
-
test("dueJobs coalesces a multi-day recurring outage into ONE fire", async () => {
|
|
252
|
-
const root = await makeAgentRoot();
|
|
253
|
-
try {
|
|
254
|
-
// Daily 8am job created Wed; first fire targets Thu Jun 11 08:00Z.
|
|
255
|
-
const when = parseWhen("every day at 8am", { now: WED_NOON });
|
|
256
|
-
const job = createJob({ agentRoot: root, when, action: "brief", now: WED_NOON });
|
|
257
|
-
assert.equal(new Date(job.next_run_at).toISOString(), "2026-06-11T08:00:00.000Z");
|
|
258
|
-
|
|
259
|
-
// Daemon was DOWN for three days. We come back online Sat Jun 13 at 09:00Z,
|
|
260
|
-
// having missed Thu, Fri, and Sat 08:00 windows.
|
|
261
|
-
const recover = Date.UTC(2026, 5, 13, 9, 0, 0);
|
|
262
|
-
const fired = dueJobs(root, { now: recover });
|
|
263
|
-
// Exactly ONE fire, not a burst of three.
|
|
264
|
-
assert.equal(fired.length, 1, "a 3-day outage must replay at most one brief");
|
|
265
|
-
|
|
266
|
-
// next_run_at fast-forwarded to the NEXT future boundary (Sun Jun 14 08:00).
|
|
267
|
-
const stored = readStore(root).jobs[0];
|
|
268
|
-
assert.equal(stored.status, "active");
|
|
269
|
-
assert.equal(new Date(stored.next_run_at).toISOString(), "2026-06-14T08:00:00.000Z");
|
|
270
|
-
|
|
271
|
-
// And a second immediate poll fires nothing (already coalesced forward).
|
|
272
|
-
assert.equal(dueJobs(root, { now: recover + 1000 }).length, 0);
|
|
273
|
-
} finally { await rmRoot(root); }
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
test("dueJobs fires a recurring job on each successive window (no premature fire)", async () => {
|
|
277
|
-
const root = await makeAgentRoot();
|
|
278
|
-
try {
|
|
279
|
-
const when = parseWhen("every day at 8am", { now: WED_NOON });
|
|
280
|
-
createJob({ agentRoot: root, when, action: "brief", now: WED_NOON });
|
|
281
|
-
const thu8 = Date.UTC(2026, 5, 11, 8, 0, 0);
|
|
282
|
-
const fri8 = Date.UTC(2026, 5, 12, 8, 0, 0);
|
|
283
|
-
// Before Thu 8am: nothing.
|
|
284
|
-
assert.equal(dueJobs(root, { now: thu8 - 60_000 }).length, 0);
|
|
285
|
-
// Thu 8am: fires.
|
|
286
|
-
assert.equal(dueJobs(root, { now: thu8 }).length, 1);
|
|
287
|
-
// Between Thu and Fri: nothing.
|
|
288
|
-
assert.equal(dueJobs(root, { now: thu8 + 6 * 3_600_000 }).length, 0);
|
|
289
|
-
// Fri 8am: fires again.
|
|
290
|
-
assert.equal(dueJobs(root, { now: fri8 }).length, 1);
|
|
291
|
-
} finally { await rmRoot(root); }
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
// dueJobs — notBefore deferred redelivery
|
|
296
|
-
// ---------------------------------------------------------------------------
|
|
297
|
-
|
|
298
|
-
test("deferJob hides a due job until not_before, then it fires", async () => {
|
|
299
|
-
const root = await makeAgentRoot();
|
|
300
|
-
try {
|
|
301
|
-
const fireAt = WED_NOON;
|
|
302
|
-
const job = createJob({ agentRoot: root, when: { at: fireAt }, action: "reminder", now: WED_NOON - 1000 });
|
|
303
|
-
// Consumer defers it 10 minutes (e.g. under load).
|
|
304
|
-
const notBefore = fireAt + 10 * 60_000;
|
|
305
|
-
assert.equal(deferJob(root, job.id, notBefore), true);
|
|
306
|
-
|
|
307
|
-
// Due by time, but suppressed by not_before.
|
|
308
|
-
assert.equal(dueJobs(root, { now: fireAt }).length, 0);
|
|
309
|
-
assert.equal(dueJobs(root, { now: notBefore - 1 }).length, 0);
|
|
310
|
-
|
|
311
|
-
// At not_before it becomes eligible and fires (within TTL).
|
|
312
|
-
const fired = dueJobs(root, { now: notBefore });
|
|
313
|
-
assert.equal(fired.length, 1);
|
|
314
|
-
// The defer marker is cleared on fire.
|
|
315
|
-
assert.equal(readStore(root).jobs[0].not_before, undefined);
|
|
316
|
-
} finally { await rmRoot(root); }
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
// Fail-open store reads
|
|
321
|
-
// ---------------------------------------------------------------------------
|
|
322
|
-
|
|
323
|
-
test("readStore fails open on a corrupt jobs.json", async () => {
|
|
324
|
-
const root = await makeAgentRoot();
|
|
325
|
-
try {
|
|
326
|
-
const path = getJobsPath(root);
|
|
327
|
-
await fsp.mkdir(join(root, "state/scheduling"), { recursive: true });
|
|
328
|
-
await fsp.writeFile(path, "{ this is not json");
|
|
329
|
-
// Must not throw; reads as empty.
|
|
330
|
-
assert.deepEqual(readStore(root), { jobs: [] });
|
|
331
|
-
assert.deepEqual(dueJobs(root, { now: WED_NOON }), []);
|
|
332
|
-
// And we can still create on top of it (overwrites with a valid store).
|
|
333
|
-
const job = createJob({ agentRoot: root, when: { at: WED_NOON }, action: "x", now: WED_NOON });
|
|
334
|
-
assert.equal(readStore(root).jobs[0].id, job.id);
|
|
335
|
-
} finally { await rmRoot(root); }
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test("DEFAULT_TTL_MS is the fallback one-off staleness window", async () => {
|
|
339
|
-
const root = await makeAgentRoot();
|
|
340
|
-
try {
|
|
341
|
-
const job = createJob({ agentRoot: root, when: { at: WED_NOON }, action: "x", now: WED_NOON });
|
|
342
|
-
assert.equal(job.ttl_ms, DEFAULT_TTL_MS);
|
|
343
|
-
} finally { await rmRoot(root); }
|
|
344
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* jitter.test.mjs — node:test coverage for per-agent deterministic jitter.
|
|
3
|
-
*
|
|
4
|
-
* Pure functions: no filesystem, no clock dependency beyond the explicit
|
|
5
|
-
* `at` argument to tzOffsetFor. Real assertions on stability, spread, and
|
|
6
|
-
* timezone correctness.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { test } from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
hashFraction,
|
|
14
|
-
jitterOffset,
|
|
15
|
-
jitterFor,
|
|
16
|
-
tzOffsetFor,
|
|
17
|
-
DEFAULT_WINDOW_MS,
|
|
18
|
-
} from "./jitter.mjs";
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// hashFraction
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
test("hashFraction returns a stable fraction in [0,1)", () => {
|
|
25
|
-
const a = hashFraction("alex morning-brief");
|
|
26
|
-
const b = hashFraction("alex morning-brief");
|
|
27
|
-
assert.equal(a, b, "same key must hash to the same fraction");
|
|
28
|
-
assert.ok(a >= 0 && a < 1, `fraction out of range: ${a}`);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("hashFraction is roughly uniform across many keys", () => {
|
|
32
|
-
// 10 buckets; 2000 keys; every bucket should get a non-trivial share.
|
|
33
|
-
const buckets = new Array(10).fill(0);
|
|
34
|
-
for (let i = 0; i < 2000; i++) {
|
|
35
|
-
const f = hashFraction(`agent-${i}`);
|
|
36
|
-
buckets[Math.floor(f * 10)]++;
|
|
37
|
-
}
|
|
38
|
-
for (const count of buckets) {
|
|
39
|
-
assert.ok(count > 100, `bucket too sparse (${count}/2000) — not uniform`);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// jitterOffset / jitterFor — stability
|
|
45
|
-
// ---------------------------------------------------------------------------
|
|
46
|
-
|
|
47
|
-
test("jitterFor is stable per (agent,cadence)", () => {
|
|
48
|
-
const base = 1_700_000_000_000;
|
|
49
|
-
const first = jitterFor("robin", "daily-digest", base, DEFAULT_WINDOW_MS);
|
|
50
|
-
const again = jitterFor("robin", "daily-digest", base, DEFAULT_WINDOW_MS);
|
|
51
|
-
assert.equal(first, again, "jitter must be deterministic for an agent");
|
|
52
|
-
// Different cadence → (almost surely) different slot.
|
|
53
|
-
const other = jitterFor("robin", "weekly-memo", base, DEFAULT_WINDOW_MS);
|
|
54
|
-
assert.notEqual(first, other);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("jitterOffset stays strictly within [0, windowMs)", () => {
|
|
58
|
-
const windowMs = 300_000;
|
|
59
|
-
for (let i = 0; i < 500; i++) {
|
|
60
|
-
const off = jitterOffset(`agent-${i}`, "morning-brief", windowMs);
|
|
61
|
-
assert.ok(off >= 0 && off < windowMs, `offset ${off} out of window`);
|
|
62
|
-
assert.ok(Number.isInteger(off), "offset must be integer ms");
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("jitterFor adds the offset to the base time", () => {
|
|
67
|
-
const base = 1_700_000_000_000;
|
|
68
|
-
const off = jitterOffset("alex", "inbox", DEFAULT_WINDOW_MS);
|
|
69
|
-
assert.equal(jitterFor("alex", "inbox", base, DEFAULT_WINDOW_MS), base + off);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("zero or invalid window yields zero offset (no jitter)", () => {
|
|
73
|
-
assert.equal(jitterOffset("alex", "c", 0), 0);
|
|
74
|
-
assert.equal(jitterOffset("alex", "c", -5), 0);
|
|
75
|
-
assert.equal(jitterOffset("alex", "c", NaN), 0);
|
|
76
|
-
// jitterFor with a bad base falls back to base 0.
|
|
77
|
-
assert.equal(jitterFor("alex", "c", NaN, 0), 0);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Fleet spread — the thundering-herd fix
|
|
82
|
-
// ---------------------------------------------------------------------------
|
|
83
|
-
|
|
84
|
-
test("a 60-agent fleet spreads across the window without clumping", () => {
|
|
85
|
-
const windowMs = 5 * 60 * 1000; // 5 minutes
|
|
86
|
-
const cadence = "morning-brief";
|
|
87
|
-
const offsets = [];
|
|
88
|
-
for (let i = 0; i < 60; i++) {
|
|
89
|
-
offsets.push(jitterOffset(`agent-${String(i).padStart(2, "0")}`, cadence, windowMs));
|
|
90
|
-
}
|
|
91
|
-
// All within the window.
|
|
92
|
-
for (const o of offsets) assert.ok(o >= 0 && o < windowMs);
|
|
93
|
-
|
|
94
|
-
// Spread: the offsets should cover a wide span, not collapse to one instant.
|
|
95
|
-
const min = Math.min(...offsets);
|
|
96
|
-
const max = Math.max(...offsets);
|
|
97
|
-
assert.ok(max - min > windowMs * 0.7, `spread too narrow: ${max - min}ms of ${windowMs}ms`);
|
|
98
|
-
|
|
99
|
-
// No thundering herd: with a 5-min window, no single 10s slot holds more
|
|
100
|
-
// than a small fraction of the fleet.
|
|
101
|
-
const slotMs = 10_000;
|
|
102
|
-
const slotCounts = new Map();
|
|
103
|
-
for (const o of offsets) {
|
|
104
|
-
const slot = Math.floor(o / slotMs);
|
|
105
|
-
slotCounts.set(slot, (slotCounts.get(slot) || 0) + 1);
|
|
106
|
-
}
|
|
107
|
-
const maxPerSlot = Math.max(...slotCounts.values());
|
|
108
|
-
assert.ok(maxPerSlot <= 8, `too many agents (${maxPerSlot}) in one 10s slot`);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// tzOffsetFor
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
test("tzOffsetFor returns correct fixed-offset zones", () => {
|
|
116
|
-
// UTC is always 0.
|
|
117
|
-
assert.equal(tzOffsetFor("UTC", Date.UTC(2026, 0, 15, 12, 0, 0)), 0);
|
|
118
|
-
// Asia/Dubai is a fixed +4 (no DST) → +240 minutes.
|
|
119
|
-
assert.equal(tzOffsetFor("Asia/Dubai", Date.UTC(2026, 0, 15, 12, 0, 0)), 240);
|
|
120
|
-
// Asia/Kolkata is +5:30 → +330 minutes.
|
|
121
|
-
assert.equal(tzOffsetFor("Asia/Kolkata", Date.UTC(2026, 0, 15, 12, 0, 0)), 330);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("tzOffsetFor is DST-aware for America/New_York", () => {
|
|
125
|
-
// Mid-January: EST = UTC-5 → -300.
|
|
126
|
-
assert.equal(tzOffsetFor("America/New_York", Date.UTC(2026, 0, 15, 17, 0, 0)), -300);
|
|
127
|
-
// Mid-July: EDT = UTC-4 → -240.
|
|
128
|
-
assert.equal(tzOffsetFor("America/New_York", Date.UTC(2026, 6, 15, 17, 0, 0)), -240);
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
test("tzOffsetFor fails open to 0 for an invalid timezone", () => {
|
|
132
|
-
assert.equal(tzOffsetFor("Not/AZone", Date.UTC(2026, 0, 1)), 0);
|
|
133
|
-
assert.equal(tzOffsetFor("", Date.UTC(2026, 0, 1)), 0);
|
|
134
|
-
assert.equal(tzOffsetFor(null, Date.UTC(2026, 0, 1)), 0);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("tzOffsetFor accepts a Date or a ms epoch", () => {
|
|
138
|
-
const ms = Date.UTC(2026, 0, 15, 12, 0, 0);
|
|
139
|
-
assert.equal(tzOffsetFor("Asia/Dubai", new Date(ms)), tzOffsetFor("Asia/Dubai", ms));
|
|
140
|
-
});
|