@ag-eco/agentplate-cli 0.13.0
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/LICENSE +21 -0
- package/README.md +462 -0
- package/agents/ap-co-creation.md +90 -0
- package/agents/builder.md +144 -0
- package/agents/coordinator.md +377 -0
- package/agents/lead.md +435 -0
- package/agents/merger.md +164 -0
- package/agents/monitor.md +214 -0
- package/agents/orchestrator.md +239 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +125 -0
- package/agents/supervisor.md +427 -0
- package/package.json +66 -0
- package/src/agents/capabilities.test.ts +85 -0
- package/src/agents/capabilities.ts +125 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/copilot-hooks-deployer.test.ts +162 -0
- package/src/agents/copilot-hooks-deployer.ts +93 -0
- package/src/agents/guard-rules.test.ts +372 -0
- package/src/agents/guard-rules.ts +97 -0
- package/src/agents/headless-mail-injector.test.ts +709 -0
- package/src/agents/headless-mail-injector.ts +377 -0
- package/src/agents/headless-prompt.test.ts +102 -0
- package/src/agents/headless-prompt.ts +68 -0
- package/src/agents/hooks-deployer.test.ts +3119 -0
- package/src/agents/hooks-deployer.ts +804 -0
- package/src/agents/identity.test.ts +604 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/mail-poll-detect.test.ts +153 -0
- package/src/agents/mail-poll-detect.ts +73 -0
- package/src/agents/manifest.test.ts +1026 -0
- package/src/agents/manifest.ts +376 -0
- package/src/agents/overlay.test.ts +1058 -0
- package/src/agents/overlay.ts +490 -0
- package/src/agents/scope-detect.test.ts +190 -0
- package/src/agents/scope-detect.ts +146 -0
- package/src/agents/turn-lock.test.ts +181 -0
- package/src/agents/turn-lock.ts +235 -0
- package/src/agents/turn-runner-dispatch.test.ts +182 -0
- package/src/agents/turn-runner-dispatch.ts +105 -0
- package/src/agents/turn-runner.test.ts +2312 -0
- package/src/agents/turn-runner.ts +1383 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +230 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +328 -0
- package/src/commands/agents.ts +299 -0
- package/src/commands/clean.test.ts +797 -0
- package/src/commands/clean.ts +791 -0
- package/src/commands/completions.test.ts +348 -0
- package/src/commands/completions.ts +981 -0
- package/src/commands/coordinator.test.ts +2975 -0
- package/src/commands/coordinator.ts +1841 -0
- package/src/commands/costs.test.ts +1183 -0
- package/src/commands/costs.ts +599 -0
- package/src/commands/dashboard.test.ts +954 -0
- package/src/commands/dashboard.ts +1212 -0
- package/src/commands/discover.test.ts +288 -0
- package/src/commands/discover.ts +202 -0
- package/src/commands/doctor.test.ts +303 -0
- package/src/commands/doctor.ts +311 -0
- package/src/commands/ecosystem.test.ts +226 -0
- package/src/commands/ecosystem.ts +248 -0
- package/src/commands/errors.test.ts +654 -0
- package/src/commands/errors.ts +197 -0
- package/src/commands/feed.test.ts +709 -0
- package/src/commands/feed.ts +260 -0
- package/src/commands/group.test.ts +475 -0
- package/src/commands/group.ts +546 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +263 -0
- package/src/commands/init.test.ts +1011 -0
- package/src/commands/init.ts +967 -0
- package/src/commands/inspect.test.ts +1239 -0
- package/src/commands/inspect.ts +648 -0
- package/src/commands/log.test.ts +1913 -0
- package/src/commands/log.ts +958 -0
- package/src/commands/logs.test.ts +801 -0
- package/src/commands/logs.ts +483 -0
- package/src/commands/mail.test.ts +1501 -0
- package/src/commands/mail.ts +848 -0
- package/src/commands/merge.test.ts +864 -0
- package/src/commands/merge.ts +381 -0
- package/src/commands/metrics.test.ts +458 -0
- package/src/commands/metrics.ts +129 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +409 -0
- package/src/commands/nudge.test.ts +579 -0
- package/src/commands/nudge.ts +646 -0
- package/src/commands/orchestrator.ts +42 -0
- package/src/commands/prime.test.ts +612 -0
- package/src/commands/prime.ts +359 -0
- package/src/commands/replay.test.ts +757 -0
- package/src/commands/replay.ts +231 -0
- package/src/commands/run.test.ts +469 -0
- package/src/commands/run.ts +353 -0
- package/src/commands/serve/agent-actions.test.ts +210 -0
- package/src/commands/serve/agent-actions.ts +192 -0
- package/src/commands/serve/build.test.ts +202 -0
- package/src/commands/serve/build.ts +206 -0
- package/src/commands/serve/coordinator-actions.test.ts +339 -0
- package/src/commands/serve/coordinator-actions.ts +410 -0
- package/src/commands/serve/dev.test.ts +168 -0
- package/src/commands/serve/dev.ts +117 -0
- package/src/commands/serve/mail-actions.test.ts +312 -0
- package/src/commands/serve/mail-actions.ts +167 -0
- package/src/commands/serve/rest.test.ts +1680 -0
- package/src/commands/serve/rest.ts +1130 -0
- package/src/commands/serve/static.ts +51 -0
- package/src/commands/serve/ws.test.ts +361 -0
- package/src/commands/serve/ws.ts +332 -0
- package/src/commands/serve.test.ts +459 -0
- package/src/commands/serve.ts +654 -0
- package/src/commands/sling.test.ts +1583 -0
- package/src/commands/sling.ts +1351 -0
- package/src/commands/spec.test.ts +179 -0
- package/src/commands/spec.ts +105 -0
- package/src/commands/status.test.ts +614 -0
- package/src/commands/status.ts +403 -0
- package/src/commands/stop.test.ts +964 -0
- package/src/commands/stop.ts +319 -0
- package/src/commands/supervisor.test.ts +185 -0
- package/src/commands/supervisor.ts +537 -0
- package/src/commands/trace.test.ts +762 -0
- package/src/commands/trace.ts +205 -0
- package/src/commands/update.test.ts +466 -0
- package/src/commands/update.ts +263 -0
- package/src/commands/upgrade.test.ts +48 -0
- package/src/commands/upgrade.ts +240 -0
- package/src/commands/watch.test.ts +257 -0
- package/src/commands/watch.ts +308 -0
- package/src/commands/worktree.test.ts +1297 -0
- package/src/commands/worktree.ts +451 -0
- package/src/config.test.ts +1535 -0
- package/src/config.ts +1064 -0
- package/src/doctor/agents.test.ts +523 -0
- package/src/doctor/agents.ts +399 -0
- package/src/doctor/config-check.test.ts +191 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +807 -0
- package/src/doctor/consistency.ts +347 -0
- package/src/doctor/databases.test.ts +350 -0
- package/src/doctor/databases.ts +243 -0
- package/src/doctor/dependencies.test.ts +296 -0
- package/src/doctor/dependencies.ts +272 -0
- package/src/doctor/ecosystem.test.ts +308 -0
- package/src/doctor/ecosystem.ts +156 -0
- package/src/doctor/logs.test.ts +253 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +315 -0
- package/src/doctor/merge-queue.ts +167 -0
- package/src/doctor/providers.test.ts +409 -0
- package/src/doctor/providers.ts +250 -0
- package/src/doctor/serve.test.ts +95 -0
- package/src/doctor/serve.ts +86 -0
- package/src/doctor/structure.test.ts +423 -0
- package/src/doctor/structure.ts +285 -0
- package/src/doctor/types.ts +43 -0
- package/src/doctor/version.test.ts +241 -0
- package/src/doctor/version.ts +132 -0
- package/src/doctor/watchdog.test.ts +167 -0
- package/src/doctor/watchdog.ts +214 -0
- package/src/e2e/init-sling-lifecycle.test.ts +283 -0
- package/src/errors.test.ts +350 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tailer.test.ts +719 -0
- package/src/events/tailer.ts +332 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +533 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/insights/quality-gates.test.ts +141 -0
- package/src/insights/quality-gates.ts +156 -0
- package/src/json.test.ts +72 -0
- package/src/json.ts +53 -0
- package/src/loam/client.test.ts +752 -0
- package/src/loam/client.ts +664 -0
- package/src/logging/color.test.ts +252 -0
- package/src/logging/color.ts +105 -0
- package/src/logging/format.test.ts +110 -0
- package/src/logging/format.ts +255 -0
- package/src/logging/logger.test.ts +814 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +110 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/logging/theme.ts +140 -0
- package/src/mail/broadcast.test.ts +204 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +774 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +898 -0
- package/src/mail/store.ts +425 -0
- package/src/merge/lock.test.ts +149 -0
- package/src/merge/lock.ts +140 -0
- package/src/merge/predict.test.ts +387 -0
- package/src/merge/predict.ts +249 -0
- package/src/merge/queue.test.ts +426 -0
- package/src/merge/queue.ts +246 -0
- package/src/merge/resolver.test.ts +1993 -0
- package/src/merge/resolver.ts +926 -0
- package/src/metrics/pricing.test.ts +258 -0
- package/src/metrics/pricing.ts +135 -0
- package/src/metrics/store.test.ts +978 -0
- package/src/metrics/store.ts +501 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +483 -0
- package/src/metrics/transcript.ts +114 -0
- package/src/runtimes/__fixtures__/claude-stream-fixture.ts +22 -0
- package/src/runtimes/aider.test.ts +124 -0
- package/src/runtimes/aider.ts +147 -0
- package/src/runtimes/amp.test.ts +164 -0
- package/src/runtimes/amp.ts +154 -0
- package/src/runtimes/claude.test.ts +1474 -0
- package/src/runtimes/claude.ts +579 -0
- package/src/runtimes/codex.test.ts +805 -0
- package/src/runtimes/codex.ts +273 -0
- package/src/runtimes/connections.test.ts +214 -0
- package/src/runtimes/connections.ts +103 -0
- package/src/runtimes/copilot.test.ts +707 -0
- package/src/runtimes/copilot.ts +316 -0
- package/src/runtimes/cursor.test.ts +497 -0
- package/src/runtimes/cursor.ts +205 -0
- package/src/runtimes/gemini.test.ts +537 -0
- package/src/runtimes/gemini.ts +243 -0
- package/src/runtimes/goose.test.ts +133 -0
- package/src/runtimes/goose.ts +157 -0
- package/src/runtimes/headless-connection.test.ts +264 -0
- package/src/runtimes/headless-connection.ts +158 -0
- package/src/runtimes/opencode.test.ts +325 -0
- package/src/runtimes/opencode.ts +188 -0
- package/src/runtimes/pi-guards.test.ts +486 -0
- package/src/runtimes/pi-guards.ts +367 -0
- package/src/runtimes/pi.test.ts +789 -0
- package/src/runtimes/pi.ts +305 -0
- package/src/runtimes/registry.test.ts +196 -0
- package/src/runtimes/registry.ts +99 -0
- package/src/runtimes/sapling.test.ts +1267 -0
- package/src/runtimes/sapling.ts +710 -0
- package/src/runtimes/types.ts +266 -0
- package/src/schema-consistency.test.ts +246 -0
- package/src/sessions/compat.test.ts +281 -0
- package/src/sessions/compat.ts +105 -0
- package/src/sessions/store.test.ts +1748 -0
- package/src/sessions/store.ts +858 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +145 -0
- package/src/test-setup.test.ts +31 -0
- package/src/test-setup.ts +28 -0
- package/src/tools/loam/api.ts +368 -0
- package/src/tools/loam/cli.ts +278 -0
- package/src/tools/loam/commands/add.ts +52 -0
- package/src/tools/loam/commands/archive.ts +214 -0
- package/src/tools/loam/commands/audit.ts +276 -0
- package/src/tools/loam/commands/compact.ts +1062 -0
- package/src/tools/loam/commands/completions.ts +79 -0
- package/src/tools/loam/commands/config.ts +381 -0
- package/src/tools/loam/commands/delete-domain.ts +121 -0
- package/src/tools/loam/commands/delete.ts +316 -0
- package/src/tools/loam/commands/diff.ts +200 -0
- package/src/tools/loam/commands/doctor.ts +1113 -0
- package/src/tools/loam/commands/edit.ts +226 -0
- package/src/tools/loam/commands/init.ts +31 -0
- package/src/tools/loam/commands/learn.ts +179 -0
- package/src/tools/loam/commands/move.ts +323 -0
- package/src/tools/loam/commands/onboard.ts +374 -0
- package/src/tools/loam/commands/outcome.ts +185 -0
- package/src/tools/loam/commands/prime.ts +688 -0
- package/src/tools/loam/commands/prune.ts +614 -0
- package/src/tools/loam/commands/query.ts +218 -0
- package/src/tools/loam/commands/rank.ts +180 -0
- package/src/tools/loam/commands/ready.ts +189 -0
- package/src/tools/loam/commands/record.ts +1210 -0
- package/src/tools/loam/commands/restore.ts +166 -0
- package/src/tools/loam/commands/search.ts +327 -0
- package/src/tools/loam/commands/setup.ts +887 -0
- package/src/tools/loam/commands/status.ts +103 -0
- package/src/tools/loam/commands/sync.ts +298 -0
- package/src/tools/loam/commands/update.ts +19 -0
- package/src/tools/loam/commands/upgrade.ts +93 -0
- package/src/tools/loam/commands/validate.ts +190 -0
- package/src/tools/loam/index.ts +62 -0
- package/src/tools/loam/log.ts +127 -0
- package/src/tools/loam/registry/builtins.ts +409 -0
- package/src/tools/loam/registry/custom.ts +431 -0
- package/src/tools/loam/registry/init.ts +55 -0
- package/src/tools/loam/registry/template.ts +40 -0
- package/src/tools/loam/registry/type-registry.ts +113 -0
- package/src/tools/loam/schemas/config-schema.ts +489 -0
- package/src/tools/loam/schemas/config.ts +245 -0
- package/src/tools/loam/schemas/index.ts +18 -0
- package/src/tools/loam/schemas/record-schema.ts +191 -0
- package/src/tools/loam/schemas/record.ts +115 -0
- package/src/tools/loam/utils/active-work.ts +205 -0
- package/src/tools/loam/utils/anchor-validity.ts +80 -0
- package/src/tools/loam/utils/archive.ts +146 -0
- package/src/tools/loam/utils/audit.ts +667 -0
- package/src/tools/loam/utils/bm25.ts +238 -0
- package/src/tools/loam/utils/budget.ts +142 -0
- package/src/tools/loam/utils/config.ts +344 -0
- package/src/tools/loam/utils/dir-anchors.ts +62 -0
- package/src/tools/loam/utils/domain-rules.ts +114 -0
- package/src/tools/loam/utils/expertise.ts +393 -0
- package/src/tools/loam/utils/format-helpers.ts +96 -0
- package/src/tools/loam/utils/format.ts +1234 -0
- package/src/tools/loam/utils/git-context.ts +50 -0
- package/src/tools/loam/utils/git.ts +183 -0
- package/src/tools/loam/utils/hooks.ts +299 -0
- package/src/tools/loam/utils/index.ts +52 -0
- package/src/tools/loam/utils/json-output.ts +13 -0
- package/src/tools/loam/utils/lock.ts +76 -0
- package/src/tools/loam/utils/markers.ts +48 -0
- package/src/tools/loam/utils/numeric-flags.ts +20 -0
- package/src/tools/loam/utils/palette.ts +44 -0
- package/src/tools/loam/utils/prime-ranking.ts +135 -0
- package/src/tools/loam/utils/recipe-discovery.ts +195 -0
- package/src/tools/loam/utils/runtime-flags.ts +28 -0
- package/src/tools/loam/utils/scoring.ts +94 -0
- package/src/tools/loam/utils/version.ts +116 -0
- package/src/tools/sprout/commands/block.ts +64 -0
- package/src/tools/sprout/commands/blocked.ts +86 -0
- package/src/tools/sprout/commands/close.ts +129 -0
- package/src/tools/sprout/commands/completions.ts +198 -0
- package/src/tools/sprout/commands/config.ts +238 -0
- package/src/tools/sprout/commands/create.ts +164 -0
- package/src/tools/sprout/commands/dep.ts +148 -0
- package/src/tools/sprout/commands/doctor.ts +979 -0
- package/src/tools/sprout/commands/init.ts +83 -0
- package/src/tools/sprout/commands/label.ts +178 -0
- package/src/tools/sprout/commands/list.ts +210 -0
- package/src/tools/sprout/commands/migrate.ts +133 -0
- package/src/tools/sprout/commands/onboard.ts +207 -0
- package/src/tools/sprout/commands/plan-show.ts +278 -0
- package/src/tools/sprout/commands/plan.ts +2526 -0
- package/src/tools/sprout/commands/prime.ts +399 -0
- package/src/tools/sprout/commands/ready.ts +245 -0
- package/src/tools/sprout/commands/search.ts +221 -0
- package/src/tools/sprout/commands/show.ts +277 -0
- package/src/tools/sprout/commands/stats.ts +146 -0
- package/src/tools/sprout/commands/sync.ts +134 -0
- package/src/tools/sprout/commands/tpl.ts +364 -0
- package/src/tools/sprout/commands/unblock.ts +115 -0
- package/src/tools/sprout/commands/update.ts +257 -0
- package/src/tools/sprout/commands/upgrade.ts +91 -0
- package/src/tools/sprout/config-schema.ts +152 -0
- package/src/tools/sprout/config.ts +355 -0
- package/src/tools/sprout/filter.ts +107 -0
- package/src/tools/sprout/format.ts +43 -0
- package/src/tools/sprout/id.ts +22 -0
- package/src/tools/sprout/index.ts +204 -0
- package/src/tools/sprout/log.ts +76 -0
- package/src/tools/sprout/markers.ts +22 -0
- package/src/tools/sprout/output.ts +121 -0
- package/src/tools/sprout/plan-backref.ts +93 -0
- package/src/tools/sprout/plan-context.ts +81 -0
- package/src/tools/sprout/plan-domain.ts +139 -0
- package/src/tools/sprout/plan-lifecycle.ts +65 -0
- package/src/tools/sprout/plan-loam.ts +207 -0
- package/src/tools/sprout/plan-schema.ts +209 -0
- package/src/tools/sprout/sort.ts +31 -0
- package/src/tools/sprout/store.ts +172 -0
- package/src/tools/sprout/types.ts +118 -0
- package/src/tools/sprout/validation.ts +119 -0
- package/src/tools/sprout/version.ts +1 -0
- package/src/tools/sprout/yaml.ts +387 -0
- package/src/tools/trellis/commands/archive.ts +87 -0
- package/src/tools/trellis/commands/completions.ts +610 -0
- package/src/tools/trellis/commands/config.ts +382 -0
- package/src/tools/trellis/commands/create.ts +252 -0
- package/src/tools/trellis/commands/diff.ts +150 -0
- package/src/tools/trellis/commands/doctor.ts +771 -0
- package/src/tools/trellis/commands/emit.ts +365 -0
- package/src/tools/trellis/commands/history.ts +83 -0
- package/src/tools/trellis/commands/import.ts +198 -0
- package/src/tools/trellis/commands/init.ts +81 -0
- package/src/tools/trellis/commands/list.ts +103 -0
- package/src/tools/trellis/commands/onboard.ts +156 -0
- package/src/tools/trellis/commands/pin.ts +172 -0
- package/src/tools/trellis/commands/prime.ts +193 -0
- package/src/tools/trellis/commands/render.ts +122 -0
- package/src/tools/trellis/commands/schema.ts +353 -0
- package/src/tools/trellis/commands/show.ts +115 -0
- package/src/tools/trellis/commands/stats.ts +65 -0
- package/src/tools/trellis/commands/sync.ts +112 -0
- package/src/tools/trellis/commands/tree.ts +123 -0
- package/src/tools/trellis/commands/update.ts +330 -0
- package/src/tools/trellis/commands/upgrade.ts +95 -0
- package/src/tools/trellis/commands/validate.ts +166 -0
- package/src/tools/trellis/config-schema.ts +81 -0
- package/src/tools/trellis/config.ts +108 -0
- package/src/tools/trellis/frontmatter.ts +348 -0
- package/src/tools/trellis/id.ts +24 -0
- package/src/tools/trellis/index.ts +209 -0
- package/src/tools/trellis/markers.ts +28 -0
- package/src/tools/trellis/output.ts +84 -0
- package/src/tools/trellis/render.ts +212 -0
- package/src/tools/trellis/store.ts +144 -0
- package/src/tools/trellis/types.ts +82 -0
- package/src/tools/trellis/validate.ts +199 -0
- package/src/tools/trellis/yaml.ts +309 -0
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +90 -0
- package/src/tracker/factory.ts +65 -0
- package/src/tracker/sprout.test.ts +461 -0
- package/src/tracker/sprout.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/trellis/client.test.ts +107 -0
- package/src/trellis/client.ts +179 -0
- package/src/types.ts +970 -0
- package/src/utils/bin.test.ts +10 -0
- package/src/utils/bin.ts +37 -0
- package/src/utils/browser.test.ts +49 -0
- package/src/utils/browser.ts +48 -0
- package/src/utils/fs.test.ts +119 -0
- package/src/utils/fs.ts +62 -0
- package/src/utils/pid.test.ts +152 -0
- package/src/utils/pid.ts +130 -0
- package/src/utils/process-scan.test.ts +53 -0
- package/src/utils/process-scan.ts +76 -0
- package/src/utils/time.test.ts +43 -0
- package/src/utils/time.ts +37 -0
- package/src/utils/version.test.ts +33 -0
- package/src/utils/version.ts +70 -0
- package/src/version.ts +5 -0
- package/src/watchdog/daemon.test.ts +3721 -0
- package/src/watchdog/daemon.ts +1257 -0
- package/src/watchdog/health.test.ts +830 -0
- package/src/watchdog/health.ts +434 -0
- package/src/watchdog/triage.test.ts +205 -0
- package/src/watchdog/triage.ts +205 -0
- package/src/worktree/manager.test.ts +720 -0
- package/src/worktree/manager.ts +405 -0
- package/src/worktree/process.test.ts +172 -0
- package/src/worktree/process.ts +131 -0
- package/src/worktree/tmux.test.ts +1616 -0
- package/src/worktree/tmux.ts +721 -0
- package/templates/CLAUDE.md.tmpl +100 -0
- package/templates/copilot-hooks.json.tmpl +13 -0
- package/templates/hooks.json.tmpl +109 -0
- package/templates/overlay.md.tmpl +88 -0
- package/ui/dist/apple-touch-icon-bdy6teep.png +0 -0
- package/ui/dist/chunk-8s31f05k.css +1 -0
- package/ui/dist/chunk-vm5rz679.js +300 -0
- package/ui/dist/favicon-nzb39vza.svg +4 -0
- package/ui/dist/index.html +17 -0
|
@@ -0,0 +1,1535 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdir, mkdtemp, realpath } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
clearProjectRootOverride,
|
|
7
|
+
clearWarningsSeen,
|
|
8
|
+
DEFAULT_CONFIG,
|
|
9
|
+
DEFAULT_QUALITY_GATES,
|
|
10
|
+
loadConfig,
|
|
11
|
+
resolveProjectRoot,
|
|
12
|
+
setProjectRootOverride,
|
|
13
|
+
} from "./config.ts";
|
|
14
|
+
import { ValidationError } from "./errors.ts";
|
|
15
|
+
import { cleanupTempDir, createTempGitRepo, runGitInDir } from "./test-helpers.ts";
|
|
16
|
+
|
|
17
|
+
describe("loadConfig", () => {
|
|
18
|
+
let tempDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-test-"));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(async () => {
|
|
25
|
+
await cleanupTempDir(tempDir);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function writeConfig(yaml: string): Promise<void> {
|
|
29
|
+
const agentplateDir = join(tempDir, ".agentplate");
|
|
30
|
+
await Bun.write(join(agentplateDir, "config.yaml"), yaml);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureAgentplateDir(): Promise<void> {
|
|
34
|
+
const { mkdir } = await import("node:fs/promises");
|
|
35
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("returns defaults when no config file exists", async () => {
|
|
39
|
+
const config = await loadConfig(tempDir);
|
|
40
|
+
|
|
41
|
+
expect(config.project.root).toBe(tempDir);
|
|
42
|
+
expect(config.project.canonicalBranch).toBe("main");
|
|
43
|
+
expect(config.agents.maxConcurrent).toBe(25);
|
|
44
|
+
expect(config.agents.maxDepth).toBe(2);
|
|
45
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
46
|
+
expect(config.loam.enabled).toBe(true);
|
|
47
|
+
expect(config.loam.primeFormat).toBe("markdown");
|
|
48
|
+
expect(config.logging.verbose).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("sets project.name from directory name", async () => {
|
|
52
|
+
const config = await loadConfig(tempDir);
|
|
53
|
+
const parts = tempDir.split("/");
|
|
54
|
+
const expectedName = parts[parts.length - 1] ?? "unknown";
|
|
55
|
+
expect(config.project.name).toBe(expectedName);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("merges config file values over defaults", async () => {
|
|
59
|
+
await ensureAgentplateDir();
|
|
60
|
+
await writeConfig(`
|
|
61
|
+
project:
|
|
62
|
+
canonicalBranch: develop
|
|
63
|
+
agents:
|
|
64
|
+
maxConcurrent: 10
|
|
65
|
+
`);
|
|
66
|
+
|
|
67
|
+
const config = await loadConfig(tempDir);
|
|
68
|
+
|
|
69
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
70
|
+
expect(config.agents.maxConcurrent).toBe(10);
|
|
71
|
+
// Non-overridden values keep defaults
|
|
72
|
+
expect(config.agents.maxDepth).toBe(2);
|
|
73
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("always sets project.root to the actual projectRoot", async () => {
|
|
77
|
+
await ensureAgentplateDir();
|
|
78
|
+
await writeConfig(`
|
|
79
|
+
project:
|
|
80
|
+
root: /some/wrong/path
|
|
81
|
+
`);
|
|
82
|
+
|
|
83
|
+
const config = await loadConfig(tempDir);
|
|
84
|
+
expect(config.project.root).toBe(tempDir);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("parses boolean values correctly", async () => {
|
|
88
|
+
await ensureAgentplateDir();
|
|
89
|
+
await writeConfig(`
|
|
90
|
+
beads:
|
|
91
|
+
enabled: false
|
|
92
|
+
loam:
|
|
93
|
+
enabled: true
|
|
94
|
+
logging:
|
|
95
|
+
verbose: true
|
|
96
|
+
redactSecrets: false
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
const config = await loadConfig(tempDir);
|
|
100
|
+
|
|
101
|
+
expect(config.taskTracker.enabled).toBe(false);
|
|
102
|
+
expect(config.loam.enabled).toBe(true);
|
|
103
|
+
expect(config.logging.verbose).toBe(true);
|
|
104
|
+
expect(config.logging.redactSecrets).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("parses empty array literal", async () => {
|
|
108
|
+
await ensureAgentplateDir();
|
|
109
|
+
await writeConfig(`
|
|
110
|
+
loam:
|
|
111
|
+
domains: []
|
|
112
|
+
`);
|
|
113
|
+
|
|
114
|
+
const config = await loadConfig(tempDir);
|
|
115
|
+
expect(config.loam.domains).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("parses numeric values including underscore-separated", async () => {
|
|
119
|
+
await ensureAgentplateDir();
|
|
120
|
+
await writeConfig(`
|
|
121
|
+
agents:
|
|
122
|
+
staggerDelayMs: 5000
|
|
123
|
+
watchdog:
|
|
124
|
+
tier0IntervalMs: 60000
|
|
125
|
+
staleThresholdMs: 120000
|
|
126
|
+
zombieThresholdMs: 300000
|
|
127
|
+
`);
|
|
128
|
+
|
|
129
|
+
const config = await loadConfig(tempDir);
|
|
130
|
+
expect(config.agents.staggerDelayMs).toBe(5000);
|
|
131
|
+
expect(config.watchdog.tier0IntervalMs).toBe(60000);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("handles quoted string values", async () => {
|
|
135
|
+
await ensureAgentplateDir();
|
|
136
|
+
await writeConfig(`
|
|
137
|
+
project:
|
|
138
|
+
canonicalBranch: "develop"
|
|
139
|
+
`);
|
|
140
|
+
|
|
141
|
+
const config = await loadConfig(tempDir);
|
|
142
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("ignores comments and empty lines", async () => {
|
|
146
|
+
await ensureAgentplateDir();
|
|
147
|
+
await writeConfig(`
|
|
148
|
+
# This is a comment
|
|
149
|
+
project:
|
|
150
|
+
canonicalBranch: develop # inline comment
|
|
151
|
+
|
|
152
|
+
# Another comment
|
|
153
|
+
agents:
|
|
154
|
+
maxConcurrent: 3
|
|
155
|
+
`);
|
|
156
|
+
|
|
157
|
+
const config = await loadConfig(tempDir);
|
|
158
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
159
|
+
expect(config.agents.maxConcurrent).toBe(3);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("config.local.yaml overrides values from config.yaml", async () => {
|
|
163
|
+
await ensureAgentplateDir();
|
|
164
|
+
await writeConfig(`
|
|
165
|
+
project:
|
|
166
|
+
canonicalBranch: develop
|
|
167
|
+
agents:
|
|
168
|
+
maxConcurrent: 10
|
|
169
|
+
`);
|
|
170
|
+
await Bun.write(
|
|
171
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
172
|
+
`agents:\n maxConcurrent: 4\n`,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const config = await loadConfig(tempDir);
|
|
176
|
+
// Local override wins
|
|
177
|
+
expect(config.agents.maxConcurrent).toBe(4);
|
|
178
|
+
// Non-overridden value from config.yaml preserved
|
|
179
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("config.local.yaml works when config.yaml does not exist", async () => {
|
|
183
|
+
await ensureAgentplateDir();
|
|
184
|
+
// No config.yaml, only config.local.yaml
|
|
185
|
+
await Bun.write(
|
|
186
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
187
|
+
`agents:\n maxConcurrent: 3\n`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const config = await loadConfig(tempDir);
|
|
191
|
+
expect(config.agents.maxConcurrent).toBe(3);
|
|
192
|
+
// Defaults still applied
|
|
193
|
+
expect(config.project.canonicalBranch).toBe("main");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("values from config.local.yaml are validated", async () => {
|
|
197
|
+
await ensureAgentplateDir();
|
|
198
|
+
await writeConfig(`
|
|
199
|
+
project:
|
|
200
|
+
canonicalBranch: main
|
|
201
|
+
`);
|
|
202
|
+
await Bun.write(
|
|
203
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
204
|
+
`agents:\n maxConcurrent: -1\n`,
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("config.local.yaml deep merges nested objects", async () => {
|
|
211
|
+
await ensureAgentplateDir();
|
|
212
|
+
await writeConfig(`
|
|
213
|
+
watchdog:
|
|
214
|
+
tier0Enabled: false
|
|
215
|
+
staleThresholdMs: 120000
|
|
216
|
+
`);
|
|
217
|
+
await Bun.write(
|
|
218
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
219
|
+
`watchdog:\n tier0Enabled: true\n`,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const config = await loadConfig(tempDir);
|
|
223
|
+
// Local override
|
|
224
|
+
expect(config.watchdog.tier0Enabled).toBe(true);
|
|
225
|
+
// Non-overridden value from config.yaml preserved
|
|
226
|
+
expect(config.watchdog.staleThresholdMs).toBe(120000);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("parses providers section from config.yaml", async () => {
|
|
230
|
+
await ensureAgentplateDir();
|
|
231
|
+
await writeConfig(`
|
|
232
|
+
providers:
|
|
233
|
+
openrouter:
|
|
234
|
+
type: gateway
|
|
235
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
236
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
237
|
+
`);
|
|
238
|
+
const config = await loadConfig(tempDir);
|
|
239
|
+
expect(config.providers.openrouter).toEqual({
|
|
240
|
+
type: "gateway",
|
|
241
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
242
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
243
|
+
});
|
|
244
|
+
// Default anthropic provider preserved via deep merge
|
|
245
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("config.local.yaml overrides provider settings", async () => {
|
|
249
|
+
await ensureAgentplateDir();
|
|
250
|
+
await writeConfig(`
|
|
251
|
+
providers:
|
|
252
|
+
anthropic:
|
|
253
|
+
type: native
|
|
254
|
+
`);
|
|
255
|
+
await Bun.write(
|
|
256
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
257
|
+
`providers:\n anthropic:\n type: gateway\n baseUrl: http://localhost:8080\n authTokenEnv: ANTHROPIC_GATEWAY_KEY\n`,
|
|
258
|
+
);
|
|
259
|
+
const config = await loadConfig(tempDir);
|
|
260
|
+
expect(config.providers.anthropic).toEqual({
|
|
261
|
+
type: "gateway",
|
|
262
|
+
baseUrl: "http://localhost:8080",
|
|
263
|
+
authTokenEnv: "ANTHROPIC_GATEWAY_KEY",
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("empty providers section preserves defaults", async () => {
|
|
268
|
+
await ensureAgentplateDir();
|
|
269
|
+
await writeConfig(`
|
|
270
|
+
providers:
|
|
271
|
+
`);
|
|
272
|
+
const config = await loadConfig(tempDir);
|
|
273
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("multiple providers parsed correctly", async () => {
|
|
277
|
+
await ensureAgentplateDir();
|
|
278
|
+
await writeConfig(`
|
|
279
|
+
providers:
|
|
280
|
+
anthropic:
|
|
281
|
+
type: native
|
|
282
|
+
openrouter:
|
|
283
|
+
type: gateway
|
|
284
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
285
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
286
|
+
litellm:
|
|
287
|
+
type: gateway
|
|
288
|
+
baseUrl: http://localhost:4000
|
|
289
|
+
authTokenEnv: LITELLM_API_KEY
|
|
290
|
+
`);
|
|
291
|
+
const config = await loadConfig(tempDir);
|
|
292
|
+
expect(Object.keys(config.providers).length).toBe(3);
|
|
293
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
294
|
+
expect(config.providers.openrouter).toEqual({
|
|
295
|
+
type: "gateway",
|
|
296
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
297
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
298
|
+
});
|
|
299
|
+
expect(config.providers.litellm).toEqual({
|
|
300
|
+
type: "gateway",
|
|
301
|
+
baseUrl: "http://localhost:4000",
|
|
302
|
+
authTokenEnv: "LITELLM_API_KEY",
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("config.local.yaml adds new provider alongside config.yaml providers", async () => {
|
|
307
|
+
await ensureAgentplateDir();
|
|
308
|
+
await writeConfig(`
|
|
309
|
+
providers:
|
|
310
|
+
openrouter:
|
|
311
|
+
type: gateway
|
|
312
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
313
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
314
|
+
`);
|
|
315
|
+
await Bun.write(
|
|
316
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
317
|
+
`providers:\n litellm:\n type: gateway\n baseUrl: http://localhost:4000\n authTokenEnv: LITELLM_API_KEY\n`,
|
|
318
|
+
);
|
|
319
|
+
const config = await loadConfig(tempDir);
|
|
320
|
+
// All three providers present: default anthropic + openrouter from config.yaml + litellm from config.local.yaml
|
|
321
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
322
|
+
expect(config.providers.openrouter).toEqual({
|
|
323
|
+
type: "gateway",
|
|
324
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
325
|
+
authTokenEnv: "OPENROUTER_API_KEY",
|
|
326
|
+
});
|
|
327
|
+
expect(config.providers.litellm).toEqual({
|
|
328
|
+
type: "gateway",
|
|
329
|
+
baseUrl: "http://localhost:4000",
|
|
330
|
+
authTokenEnv: "LITELLM_API_KEY",
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
test("simple model strings still work without providers section", async () => {
|
|
335
|
+
await ensureAgentplateDir();
|
|
336
|
+
await writeConfig(`
|
|
337
|
+
models:
|
|
338
|
+
coordinator: sonnet
|
|
339
|
+
builder: opus
|
|
340
|
+
monitor: haiku
|
|
341
|
+
`);
|
|
342
|
+
const config = await loadConfig(tempDir);
|
|
343
|
+
expect(config.models.coordinator).toBe("sonnet");
|
|
344
|
+
expect(config.models.builder).toBe("opus");
|
|
345
|
+
expect(config.models.monitor).toBe("haiku");
|
|
346
|
+
// Default anthropic provider still present even without explicit providers section
|
|
347
|
+
expect(config.providers.anthropic).toEqual({ type: "native" });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("migrates deprecated watchdog tier1/tier2 keys to tier0/tier1", async () => {
|
|
351
|
+
await ensureAgentplateDir();
|
|
352
|
+
await writeConfig(`
|
|
353
|
+
watchdog:
|
|
354
|
+
tier1Enabled: true
|
|
355
|
+
tier1IntervalMs: 45000
|
|
356
|
+
tier2Enabled: true
|
|
357
|
+
`);
|
|
358
|
+
|
|
359
|
+
const config = await loadConfig(tempDir);
|
|
360
|
+
// Old tier1 (mechanical daemon) → new tier0
|
|
361
|
+
expect(config.watchdog.tier0Enabled).toBe(true);
|
|
362
|
+
expect(config.watchdog.tier0IntervalMs).toBe(45000);
|
|
363
|
+
// Old tier2 (AI triage) → new tier1
|
|
364
|
+
expect(config.watchdog.tier1Enabled).toBe(true);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("new-style tier keys take precedence over deprecated keys", async () => {
|
|
368
|
+
await ensureAgentplateDir();
|
|
369
|
+
await writeConfig(`
|
|
370
|
+
watchdog:
|
|
371
|
+
tier0Enabled: false
|
|
372
|
+
tier0IntervalMs: 20000
|
|
373
|
+
tier1Enabled: true
|
|
374
|
+
triageTimeoutMs: 15000
|
|
375
|
+
`);
|
|
376
|
+
|
|
377
|
+
const config = await loadConfig(tempDir);
|
|
378
|
+
// New keys used directly — no migration needed
|
|
379
|
+
expect(config.watchdog.tier0Enabled).toBe(false);
|
|
380
|
+
expect(config.watchdog.tier0IntervalMs).toBe(20000);
|
|
381
|
+
expect(config.watchdog.tier1Enabled).toBe(true);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("migrates deprecated beads: key to taskTracker:", async () => {
|
|
385
|
+
await ensureAgentplateDir();
|
|
386
|
+
await writeConfig(`
|
|
387
|
+
beads:
|
|
388
|
+
enabled: false
|
|
389
|
+
`);
|
|
390
|
+
|
|
391
|
+
const config = await loadConfig(tempDir);
|
|
392
|
+
expect(config.taskTracker.backend).toBe("beads");
|
|
393
|
+
expect(config.taskTracker.enabled).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("migrates deprecated sprout: key to taskTracker:", async () => {
|
|
397
|
+
await ensureAgentplateDir();
|
|
398
|
+
await writeConfig(`
|
|
399
|
+
sprout:
|
|
400
|
+
enabled: true
|
|
401
|
+
`);
|
|
402
|
+
|
|
403
|
+
const config = await loadConfig(tempDir);
|
|
404
|
+
expect(config.taskTracker.backend).toBe("sprout");
|
|
405
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("taskTracker: key takes precedence over legacy keys", async () => {
|
|
409
|
+
await ensureAgentplateDir();
|
|
410
|
+
await writeConfig(`
|
|
411
|
+
taskTracker:
|
|
412
|
+
backend: auto
|
|
413
|
+
enabled: true
|
|
414
|
+
beads:
|
|
415
|
+
enabled: false
|
|
416
|
+
`);
|
|
417
|
+
|
|
418
|
+
const config = await loadConfig(tempDir);
|
|
419
|
+
// taskTracker present — beads key ignored
|
|
420
|
+
expect(config.taskTracker.backend).toBe("auto");
|
|
421
|
+
expect(config.taskTracker.enabled).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("defaults taskTracker.backend to auto", async () => {
|
|
425
|
+
const config = await loadConfig(tempDir);
|
|
426
|
+
expect(config.taskTracker.backend).toBe("auto");
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
describe("validateConfig", () => {
|
|
431
|
+
let tempDir: string;
|
|
432
|
+
|
|
433
|
+
beforeEach(async () => {
|
|
434
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-test-"));
|
|
435
|
+
const { mkdir } = await import("node:fs/promises");
|
|
436
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
437
|
+
clearWarningsSeen();
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
afterEach(async () => {
|
|
441
|
+
clearWarningsSeen();
|
|
442
|
+
await cleanupTempDir(tempDir);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
async function writeConfig(yaml: string): Promise<void> {
|
|
446
|
+
await Bun.write(join(tempDir, ".agentplate", "config.yaml"), yaml);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
test("rejects negative maxConcurrent", async () => {
|
|
450
|
+
await writeConfig(`
|
|
451
|
+
agents:
|
|
452
|
+
maxConcurrent: -1
|
|
453
|
+
`);
|
|
454
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("rejects zero maxConcurrent", async () => {
|
|
458
|
+
await writeConfig(`
|
|
459
|
+
agents:
|
|
460
|
+
maxConcurrent: 0
|
|
461
|
+
`);
|
|
462
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("rejects negative maxDepth", async () => {
|
|
466
|
+
await writeConfig(`
|
|
467
|
+
agents:
|
|
468
|
+
maxDepth: -1
|
|
469
|
+
`);
|
|
470
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("rejects negative staggerDelayMs", async () => {
|
|
474
|
+
await writeConfig(`
|
|
475
|
+
agents:
|
|
476
|
+
staggerDelayMs: -100
|
|
477
|
+
`);
|
|
478
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
test("accepts maxSessionsPerRun of 0 (unlimited)", async () => {
|
|
482
|
+
await writeConfig(`
|
|
483
|
+
agents:
|
|
484
|
+
maxSessionsPerRun: 0
|
|
485
|
+
`);
|
|
486
|
+
const config = await loadConfig(tempDir);
|
|
487
|
+
expect(config.agents.maxSessionsPerRun).toBe(0);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
test("accepts positive maxSessionsPerRun", async () => {
|
|
491
|
+
await writeConfig(`
|
|
492
|
+
agents:
|
|
493
|
+
maxSessionsPerRun: 20
|
|
494
|
+
`);
|
|
495
|
+
const config = await loadConfig(tempDir);
|
|
496
|
+
expect(config.agents.maxSessionsPerRun).toBe(20);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("rejects negative maxSessionsPerRun", async () => {
|
|
500
|
+
await writeConfig(`
|
|
501
|
+
agents:
|
|
502
|
+
maxSessionsPerRun: -1
|
|
503
|
+
`);
|
|
504
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("rejects non-integer maxSessionsPerRun", async () => {
|
|
508
|
+
await writeConfig(`
|
|
509
|
+
agents:
|
|
510
|
+
maxSessionsPerRun: 1.5
|
|
511
|
+
`);
|
|
512
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("validates maxAgentsPerLead must be non-negative", async () => {
|
|
516
|
+
await writeConfig("agents:\n maxAgentsPerLead: -1\n");
|
|
517
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test("accepts maxAgentsPerLead of 0 (unlimited)", async () => {
|
|
521
|
+
await writeConfig("agents:\n maxAgentsPerLead: 0\n");
|
|
522
|
+
const config = await loadConfig(tempDir);
|
|
523
|
+
expect(config.agents.maxAgentsPerLead).toBe(0);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("reads maxAgentsPerLead from config", async () => {
|
|
527
|
+
await writeConfig("agents:\n maxAgentsPerLead: 8\n");
|
|
528
|
+
const config = await loadConfig(tempDir);
|
|
529
|
+
expect(config.agents.maxAgentsPerLead).toBe(8);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
test("defaults maxAgentsPerLead to 5", async () => {
|
|
533
|
+
const config = await loadConfig(tempDir);
|
|
534
|
+
expect(config.agents.maxAgentsPerLead).toBe(5);
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
test("rejects invalid loam.primeFormat", async () => {
|
|
538
|
+
await writeConfig(`
|
|
539
|
+
loam:
|
|
540
|
+
primeFormat: yaml
|
|
541
|
+
`);
|
|
542
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("rejects invalid taskTracker.backend", async () => {
|
|
546
|
+
await writeConfig(`
|
|
547
|
+
taskTracker:
|
|
548
|
+
backend: invalid
|
|
549
|
+
enabled: true
|
|
550
|
+
`);
|
|
551
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test("rejects zombieThresholdMs <= staleThresholdMs", async () => {
|
|
555
|
+
await writeConfig(`
|
|
556
|
+
watchdog:
|
|
557
|
+
staleThresholdMs: 300000
|
|
558
|
+
zombieThresholdMs: 300000
|
|
559
|
+
`);
|
|
560
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
test("rejects non-positive tier0IntervalMs when tier0 is enabled", async () => {
|
|
564
|
+
await writeConfig(`
|
|
565
|
+
watchdog:
|
|
566
|
+
tier0Enabled: true
|
|
567
|
+
tier0IntervalMs: 0
|
|
568
|
+
`);
|
|
569
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
// rpcTimeoutMs tests
|
|
573
|
+
test("defaults rpcTimeoutMs to 5000", async () => {
|
|
574
|
+
const config = await loadConfig(tempDir);
|
|
575
|
+
expect(config.watchdog.rpcTimeoutMs).toBe(5000);
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test("accepts valid rpcTimeoutMs", async () => {
|
|
579
|
+
await writeConfig(`
|
|
580
|
+
watchdog:
|
|
581
|
+
rpcTimeoutMs: 10000
|
|
582
|
+
`);
|
|
583
|
+
const config = await loadConfig(tempDir);
|
|
584
|
+
expect(config.watchdog.rpcTimeoutMs).toBe(10000);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
test("rejects rpcTimeoutMs below 1000", async () => {
|
|
588
|
+
await writeConfig(`
|
|
589
|
+
watchdog:
|
|
590
|
+
rpcTimeoutMs: 999
|
|
591
|
+
`);
|
|
592
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
test("rejects rpcTimeoutMs above 30000", async () => {
|
|
596
|
+
await writeConfig(`
|
|
597
|
+
watchdog:
|
|
598
|
+
rpcTimeoutMs: 30001
|
|
599
|
+
`);
|
|
600
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// triageTimeoutMs tests
|
|
604
|
+
test("defaults triageTimeoutMs to 30000", async () => {
|
|
605
|
+
const config = await loadConfig(tempDir);
|
|
606
|
+
expect(config.watchdog.triageTimeoutMs).toBe(30000);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("accepts valid triageTimeoutMs", async () => {
|
|
610
|
+
await writeConfig(`
|
|
611
|
+
watchdog:
|
|
612
|
+
triageTimeoutMs: 60000
|
|
613
|
+
`);
|
|
614
|
+
const config = await loadConfig(tempDir);
|
|
615
|
+
expect(config.watchdog.triageTimeoutMs).toBe(60000);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
test("rejects triageTimeoutMs below 5000", async () => {
|
|
619
|
+
await writeConfig(`
|
|
620
|
+
watchdog:
|
|
621
|
+
triageTimeoutMs: 4999
|
|
622
|
+
`);
|
|
623
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("rejects triageTimeoutMs above 120000", async () => {
|
|
627
|
+
await writeConfig(`
|
|
628
|
+
watchdog:
|
|
629
|
+
triageTimeoutMs: 120001
|
|
630
|
+
`);
|
|
631
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
test("rejects triageTimeoutMs >= tier0IntervalMs when tier1 is enabled", async () => {
|
|
635
|
+
// Must include tier0Enabled to avoid deprecated-key migration that would remap tier1Enabled
|
|
636
|
+
await writeConfig(`
|
|
637
|
+
watchdog:
|
|
638
|
+
tier0Enabled: true
|
|
639
|
+
tier1Enabled: true
|
|
640
|
+
tier0IntervalMs: 30000
|
|
641
|
+
triageTimeoutMs: 30000
|
|
642
|
+
`);
|
|
643
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("accepts triageTimeoutMs < tier0IntervalMs when tier1 is enabled", async () => {
|
|
647
|
+
await writeConfig(`
|
|
648
|
+
watchdog:
|
|
649
|
+
tier0Enabled: true
|
|
650
|
+
tier1Enabled: true
|
|
651
|
+
tier0IntervalMs: 60000
|
|
652
|
+
triageTimeoutMs: 30000
|
|
653
|
+
`);
|
|
654
|
+
const config = await loadConfig(tempDir);
|
|
655
|
+
expect(config.watchdog.triageTimeoutMs).toBe(30000);
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
test("allows triageTimeoutMs >= tier0IntervalMs when tier1 is disabled", async () => {
|
|
659
|
+
await writeConfig(`
|
|
660
|
+
watchdog:
|
|
661
|
+
tier0Enabled: true
|
|
662
|
+
tier1Enabled: false
|
|
663
|
+
tier0IntervalMs: 30000
|
|
664
|
+
triageTimeoutMs: 30000
|
|
665
|
+
`);
|
|
666
|
+
const config = await loadConfig(tempDir);
|
|
667
|
+
expect(config.watchdog.triageTimeoutMs).toBe(30000);
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// maxEscalationLevel tests
|
|
671
|
+
test("defaults maxEscalationLevel to 3", async () => {
|
|
672
|
+
const config = await loadConfig(tempDir);
|
|
673
|
+
expect(config.watchdog.maxEscalationLevel).toBe(3);
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
test("accepts valid maxEscalationLevel", async () => {
|
|
677
|
+
await writeConfig(`
|
|
678
|
+
watchdog:
|
|
679
|
+
maxEscalationLevel: 5
|
|
680
|
+
`);
|
|
681
|
+
const config = await loadConfig(tempDir);
|
|
682
|
+
expect(config.watchdog.maxEscalationLevel).toBe(5);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
test("rejects maxEscalationLevel below 1", async () => {
|
|
686
|
+
await writeConfig(`
|
|
687
|
+
watchdog:
|
|
688
|
+
maxEscalationLevel: 0
|
|
689
|
+
`);
|
|
690
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
test("rejects maxEscalationLevel above 5", async () => {
|
|
694
|
+
await writeConfig(`
|
|
695
|
+
watchdog:
|
|
696
|
+
maxEscalationLevel: 6
|
|
697
|
+
`);
|
|
698
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
test("accepts maxEscalationLevel boundary values 1 and 5", async () => {
|
|
702
|
+
await writeConfig(`
|
|
703
|
+
watchdog:
|
|
704
|
+
maxEscalationLevel: 1
|
|
705
|
+
`);
|
|
706
|
+
let config = await loadConfig(tempDir);
|
|
707
|
+
expect(config.watchdog.maxEscalationLevel).toBe(1);
|
|
708
|
+
|
|
709
|
+
await writeConfig(`
|
|
710
|
+
watchdog:
|
|
711
|
+
maxEscalationLevel: 5
|
|
712
|
+
`);
|
|
713
|
+
config = await loadConfig(tempDir);
|
|
714
|
+
expect(config.watchdog.maxEscalationLevel).toBe(5);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("accepts empty models section", async () => {
|
|
718
|
+
await writeConfig(`
|
|
719
|
+
models:
|
|
720
|
+
`);
|
|
721
|
+
const config = await loadConfig(tempDir);
|
|
722
|
+
expect(config.models).toBeDefined();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
test("accepts valid model names in models section", async () => {
|
|
726
|
+
await writeConfig(`
|
|
727
|
+
models:
|
|
728
|
+
coordinator: sonnet
|
|
729
|
+
monitor: haiku
|
|
730
|
+
builder: opus
|
|
731
|
+
`);
|
|
732
|
+
const config = await loadConfig(tempDir);
|
|
733
|
+
expect(config.models.coordinator).toBe("sonnet");
|
|
734
|
+
expect(config.models.monitor).toBe("haiku");
|
|
735
|
+
expect(config.models.builder).toBe("opus");
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("rejects invalid model name in models section", async () => {
|
|
739
|
+
await writeConfig(`
|
|
740
|
+
models:
|
|
741
|
+
coordinator: gpt4
|
|
742
|
+
`);
|
|
743
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
// Provider validation tests
|
|
747
|
+
|
|
748
|
+
test("rejects provider with invalid type", async () => {
|
|
749
|
+
await writeConfig(`
|
|
750
|
+
providers:
|
|
751
|
+
custom:
|
|
752
|
+
type: custom
|
|
753
|
+
`);
|
|
754
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
test("rejects gateway provider without baseUrl", async () => {
|
|
758
|
+
await writeConfig(`
|
|
759
|
+
providers:
|
|
760
|
+
mygateway:
|
|
761
|
+
type: gateway
|
|
762
|
+
authTokenEnv: MY_TOKEN
|
|
763
|
+
`);
|
|
764
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("rejects gateway provider without authTokenEnv", async () => {
|
|
768
|
+
await writeConfig(`
|
|
769
|
+
providers:
|
|
770
|
+
mygateway:
|
|
771
|
+
type: gateway
|
|
772
|
+
baseUrl: https://example.com
|
|
773
|
+
`);
|
|
774
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("accepts native provider without baseUrl or authTokenEnv", async () => {
|
|
778
|
+
await writeConfig(`
|
|
779
|
+
providers:
|
|
780
|
+
mylocal:
|
|
781
|
+
type: native
|
|
782
|
+
`);
|
|
783
|
+
const config = await loadConfig(tempDir);
|
|
784
|
+
expect(config.providers.mylocal).toEqual({ type: "native" });
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
// Model validation tests
|
|
788
|
+
|
|
789
|
+
test("accepts provider-prefixed model ref when provider exists", async () => {
|
|
790
|
+
await writeConfig(`
|
|
791
|
+
providers:
|
|
792
|
+
openrouter:
|
|
793
|
+
type: gateway
|
|
794
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
795
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
796
|
+
models:
|
|
797
|
+
coordinator: openrouter/openai/gpt-5.3
|
|
798
|
+
`);
|
|
799
|
+
const config = await loadConfig(tempDir);
|
|
800
|
+
expect(config.models.coordinator).toBe("openrouter/openai/gpt-5.3");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("rejects provider-prefixed model ref when provider is unknown", async () => {
|
|
804
|
+
await writeConfig(`
|
|
805
|
+
models:
|
|
806
|
+
coordinator: unknown/model
|
|
807
|
+
`);
|
|
808
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test("rejects model ref with deeply nested slashes when provider unknown", async () => {
|
|
812
|
+
await writeConfig(`
|
|
813
|
+
models:
|
|
814
|
+
coordinator: unknown/openai/gpt-5.3/latest
|
|
815
|
+
`);
|
|
816
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
test("accepts model ref with deeply nested slashes when provider exists", async () => {
|
|
820
|
+
await writeConfig(`
|
|
821
|
+
providers:
|
|
822
|
+
openrouter:
|
|
823
|
+
type: gateway
|
|
824
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
825
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
826
|
+
models:
|
|
827
|
+
coordinator: openrouter/openai/gpt-5.3/variant
|
|
828
|
+
`);
|
|
829
|
+
const config = await loadConfig(tempDir);
|
|
830
|
+
expect(config.models.coordinator).toBe("openrouter/openai/gpt-5.3/variant");
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("rejects bare invalid model name", async () => {
|
|
834
|
+
await writeConfig(`
|
|
835
|
+
models:
|
|
836
|
+
coordinator: gpt4
|
|
837
|
+
`);
|
|
838
|
+
const err = await loadConfig(tempDir).catch((e: unknown) => e);
|
|
839
|
+
expect(err).toBeInstanceOf(ValidationError);
|
|
840
|
+
expect((err as ValidationError).message).toContain("provider-prefixed ref");
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
test("accepts bare model name when runtime.default is codex", async () => {
|
|
844
|
+
await writeConfig(`
|
|
845
|
+
runtime:
|
|
846
|
+
default: codex
|
|
847
|
+
models:
|
|
848
|
+
coordinator: gpt-5.3-codex
|
|
849
|
+
`);
|
|
850
|
+
const config = await loadConfig(tempDir);
|
|
851
|
+
expect(config.models.coordinator).toBe("gpt-5.3-codex");
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
test("warns on bare non-Anthropic model in tool-heavy role when runtime.default is codex", async () => {
|
|
855
|
+
await writeConfig(`
|
|
856
|
+
runtime:
|
|
857
|
+
default: codex
|
|
858
|
+
models:
|
|
859
|
+
builder: gpt-5.3-codex
|
|
860
|
+
`);
|
|
861
|
+
const origWrite = process.stderr.write;
|
|
862
|
+
let capturedStderr = "";
|
|
863
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
864
|
+
if (typeof s === "string") capturedStderr += s;
|
|
865
|
+
return true;
|
|
866
|
+
}) as typeof process.stderr.write;
|
|
867
|
+
try {
|
|
868
|
+
await loadConfig(tempDir);
|
|
869
|
+
} finally {
|
|
870
|
+
process.stderr.write = origWrite;
|
|
871
|
+
}
|
|
872
|
+
expect(capturedStderr).toContain("WARNING: models.builder uses non-Anthropic model");
|
|
873
|
+
expect(capturedStderr).toContain("gpt-5.3-codex");
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
test("warns on non-Anthropic model in tool-heavy role", async () => {
|
|
877
|
+
await writeConfig(`
|
|
878
|
+
providers:
|
|
879
|
+
openrouter:
|
|
880
|
+
type: gateway
|
|
881
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
882
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
883
|
+
models:
|
|
884
|
+
builder: openrouter/openai/gpt-4
|
|
885
|
+
`);
|
|
886
|
+
const origWrite = process.stderr.write;
|
|
887
|
+
let capturedStderr = "";
|
|
888
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
889
|
+
if (typeof s === "string") capturedStderr += s;
|
|
890
|
+
return true;
|
|
891
|
+
}) as typeof process.stderr.write;
|
|
892
|
+
try {
|
|
893
|
+
await loadConfig(tempDir);
|
|
894
|
+
} finally {
|
|
895
|
+
process.stderr.write = origWrite;
|
|
896
|
+
}
|
|
897
|
+
expect(capturedStderr).toContain("WARNING: models.builder uses non-Anthropic model");
|
|
898
|
+
expect(capturedStderr).toContain("openrouter/openai/gpt-4");
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
test("warns only once per role/model combination across multiple loadConfig calls", async () => {
|
|
902
|
+
await writeConfig(`
|
|
903
|
+
providers:
|
|
904
|
+
openrouter:
|
|
905
|
+
type: gateway
|
|
906
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
907
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
908
|
+
models:
|
|
909
|
+
builder: openrouter/openai/gpt-4
|
|
910
|
+
`);
|
|
911
|
+
const origWrite = process.stderr.write;
|
|
912
|
+
const stderrLines: string[] = [];
|
|
913
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
914
|
+
if (typeof s === "string") stderrLines.push(s);
|
|
915
|
+
return true;
|
|
916
|
+
}) as typeof process.stderr.write;
|
|
917
|
+
try {
|
|
918
|
+
await loadConfig(tempDir);
|
|
919
|
+
await loadConfig(tempDir);
|
|
920
|
+
await loadConfig(tempDir);
|
|
921
|
+
} finally {
|
|
922
|
+
process.stderr.write = origWrite;
|
|
923
|
+
}
|
|
924
|
+
const warnings = stderrLines.filter((l) => l.includes("WARNING: models.builder"));
|
|
925
|
+
expect(warnings.length).toBe(1);
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
test("does not warn for non-Anthropic model in non-tool-heavy role", async () => {
|
|
929
|
+
await writeConfig(`
|
|
930
|
+
providers:
|
|
931
|
+
openrouter:
|
|
932
|
+
type: gateway
|
|
933
|
+
baseUrl: https://openrouter.ai/api/v1
|
|
934
|
+
authTokenEnv: OPENROUTER_API_KEY
|
|
935
|
+
models:
|
|
936
|
+
coordinator: openrouter/openai/gpt-4
|
|
937
|
+
`);
|
|
938
|
+
const origWrite = process.stderr.write;
|
|
939
|
+
let capturedStderr = "";
|
|
940
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
941
|
+
if (typeof s === "string") capturedStderr += s;
|
|
942
|
+
return true;
|
|
943
|
+
}) as typeof process.stderr.write;
|
|
944
|
+
try {
|
|
945
|
+
await loadConfig(tempDir);
|
|
946
|
+
} finally {
|
|
947
|
+
process.stderr.write = origWrite;
|
|
948
|
+
}
|
|
949
|
+
expect(capturedStderr).not.toContain("WARNING");
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
test("custom qualityGates from config.yaml are loaded", async () => {
|
|
953
|
+
await writeConfig(`
|
|
954
|
+
project:
|
|
955
|
+
canonicalBranch: main
|
|
956
|
+
qualityGates:
|
|
957
|
+
- name: Test
|
|
958
|
+
command: pytest
|
|
959
|
+
description: all tests pass
|
|
960
|
+
- name: Lint
|
|
961
|
+
command: ruff check .
|
|
962
|
+
description: no lint errors
|
|
963
|
+
`);
|
|
964
|
+
const config = await loadConfig(tempDir);
|
|
965
|
+
expect(config.project.qualityGates?.length).toBe(2);
|
|
966
|
+
expect(config.project.qualityGates?.[0]?.command).toBe("pytest");
|
|
967
|
+
expect(config.project.qualityGates?.[1]?.command).toBe("ruff check .");
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
test("rejects qualityGate with empty name", async () => {
|
|
971
|
+
await writeConfig(`
|
|
972
|
+
project:
|
|
973
|
+
canonicalBranch: main
|
|
974
|
+
qualityGates:
|
|
975
|
+
- name: ""
|
|
976
|
+
command: pytest
|
|
977
|
+
description: all tests pass
|
|
978
|
+
`);
|
|
979
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test("rejects qualityGate with empty command", async () => {
|
|
983
|
+
await writeConfig(`
|
|
984
|
+
project:
|
|
985
|
+
canonicalBranch: main
|
|
986
|
+
qualityGates:
|
|
987
|
+
- name: Test
|
|
988
|
+
command: ""
|
|
989
|
+
description: all tests pass
|
|
990
|
+
`);
|
|
991
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
test("resets negative shellInitDelayMs to 0 with warning", async () => {
|
|
995
|
+
await writeConfig("runtime:\n shellInitDelayMs: -100\n");
|
|
996
|
+
const origWrite = process.stderr.write;
|
|
997
|
+
let capturedStderr = "";
|
|
998
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
999
|
+
if (typeof s === "string") capturedStderr += s;
|
|
1000
|
+
return true;
|
|
1001
|
+
}) as typeof process.stderr.write;
|
|
1002
|
+
try {
|
|
1003
|
+
const config = await loadConfig(tempDir);
|
|
1004
|
+
expect(config.runtime?.shellInitDelayMs).toBe(0);
|
|
1005
|
+
} finally {
|
|
1006
|
+
process.stderr.write = origWrite;
|
|
1007
|
+
}
|
|
1008
|
+
expect(capturedStderr).toContain("WARNING: runtime.shellInitDelayMs");
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
test("resets Infinity shellInitDelayMs to 0 with warning", async () => {
|
|
1012
|
+
await writeConfig("runtime:\n shellInitDelayMs: .inf\n");
|
|
1013
|
+
const origWrite = process.stderr.write;
|
|
1014
|
+
let capturedStderr = "";
|
|
1015
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
1016
|
+
if (typeof s === "string") capturedStderr += s;
|
|
1017
|
+
return true;
|
|
1018
|
+
}) as typeof process.stderr.write;
|
|
1019
|
+
try {
|
|
1020
|
+
const config = await loadConfig(tempDir);
|
|
1021
|
+
expect(config.runtime?.shellInitDelayMs).toBe(0);
|
|
1022
|
+
} finally {
|
|
1023
|
+
process.stderr.write = origWrite;
|
|
1024
|
+
}
|
|
1025
|
+
expect(capturedStderr).toContain("WARNING: runtime.shellInitDelayMs");
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
test("warns when shellInitDelayMs exceeds 30s", async () => {
|
|
1029
|
+
await writeConfig("runtime:\n shellInitDelayMs: 60000\n");
|
|
1030
|
+
const origWrite = process.stderr.write;
|
|
1031
|
+
let capturedStderr = "";
|
|
1032
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
1033
|
+
if (typeof s === "string") capturedStderr += s;
|
|
1034
|
+
return true;
|
|
1035
|
+
}) as typeof process.stderr.write;
|
|
1036
|
+
try {
|
|
1037
|
+
const config = await loadConfig(tempDir);
|
|
1038
|
+
expect(config.runtime?.shellInitDelayMs).toBe(60000);
|
|
1039
|
+
} finally {
|
|
1040
|
+
process.stderr.write = origWrite;
|
|
1041
|
+
}
|
|
1042
|
+
expect(capturedStderr).toContain("WARNING: runtime.shellInitDelayMs is 60000ms");
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test("accepts valid shellInitDelayMs without warning", async () => {
|
|
1046
|
+
await writeConfig("runtime:\n shellInitDelayMs: 2000\n");
|
|
1047
|
+
const origWrite = process.stderr.write;
|
|
1048
|
+
let capturedStderr = "";
|
|
1049
|
+
process.stderr.write = ((s: string | Uint8Array) => {
|
|
1050
|
+
if (typeof s === "string") capturedStderr += s;
|
|
1051
|
+
return true;
|
|
1052
|
+
}) as typeof process.stderr.write;
|
|
1053
|
+
try {
|
|
1054
|
+
const config = await loadConfig(tempDir);
|
|
1055
|
+
expect(config.runtime?.shellInitDelayMs).toBe(2000);
|
|
1056
|
+
} finally {
|
|
1057
|
+
process.stderr.write = origWrite;
|
|
1058
|
+
}
|
|
1059
|
+
expect(capturedStderr).not.toContain("shellInitDelayMs");
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
test("rejects qualityGate with empty description", async () => {
|
|
1063
|
+
await writeConfig(`
|
|
1064
|
+
project:
|
|
1065
|
+
canonicalBranch: main
|
|
1066
|
+
qualityGates:
|
|
1067
|
+
- name: Test
|
|
1068
|
+
command: pytest
|
|
1069
|
+
description: ""
|
|
1070
|
+
`);
|
|
1071
|
+
await expect(loadConfig(tempDir)).rejects.toThrow(ValidationError);
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
describe("resolveProjectRoot", () => {
|
|
1076
|
+
let repoDir: string;
|
|
1077
|
+
|
|
1078
|
+
afterEach(async () => {
|
|
1079
|
+
if (repoDir) {
|
|
1080
|
+
// Remove worktrees before cleaning up
|
|
1081
|
+
try {
|
|
1082
|
+
await runGitInDir(repoDir, ["worktree", "prune"]);
|
|
1083
|
+
} catch {
|
|
1084
|
+
// Best effort
|
|
1085
|
+
}
|
|
1086
|
+
await cleanupTempDir(repoDir);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
test("returns startDir when .agentplate/config.yaml exists there", async () => {
|
|
1091
|
+
repoDir = await createTempGitRepo();
|
|
1092
|
+
await mkdir(join(repoDir, ".agentplate"), { recursive: true });
|
|
1093
|
+
await Bun.write(
|
|
1094
|
+
join(repoDir, ".agentplate", "config.yaml"),
|
|
1095
|
+
"project:\n canonicalBranch: main\n",
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
const result = await resolveProjectRoot(repoDir);
|
|
1099
|
+
expect(result).toBe(repoDir);
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
test("resolves worktree to main project root", async () => {
|
|
1103
|
+
repoDir = await createTempGitRepo();
|
|
1104
|
+
// Resolve symlinks (macOS /var -> /private/var) to match git's output
|
|
1105
|
+
repoDir = await realpath(repoDir);
|
|
1106
|
+
await mkdir(join(repoDir, ".agentplate"), { recursive: true });
|
|
1107
|
+
await Bun.write(
|
|
1108
|
+
join(repoDir, ".agentplate", "config.yaml"),
|
|
1109
|
+
"project:\n canonicalBranch: main\n",
|
|
1110
|
+
);
|
|
1111
|
+
|
|
1112
|
+
// Create a worktree like agentplate sling does
|
|
1113
|
+
const worktreeDir = join(repoDir, ".agentplate", "worktrees", "test-agent");
|
|
1114
|
+
await mkdir(join(repoDir, ".agentplate", "worktrees"), { recursive: true });
|
|
1115
|
+
await runGitInDir(repoDir, [
|
|
1116
|
+
"worktree",
|
|
1117
|
+
"add",
|
|
1118
|
+
"-b",
|
|
1119
|
+
"agentplate/test-agent/task-1",
|
|
1120
|
+
worktreeDir,
|
|
1121
|
+
]);
|
|
1122
|
+
|
|
1123
|
+
// resolveProjectRoot from the worktree should return the main repo
|
|
1124
|
+
const result = await resolveProjectRoot(worktreeDir);
|
|
1125
|
+
expect(result).toBe(repoDir);
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test("resolves worktree to main root even when config.yaml is committed (regression)", async () => {
|
|
1129
|
+
repoDir = await createTempGitRepo();
|
|
1130
|
+
repoDir = await realpath(repoDir);
|
|
1131
|
+
|
|
1132
|
+
// Commit .agentplate/config.yaml so the worktree gets a copy via git
|
|
1133
|
+
// (this is what agentplate init does — the file is tracked)
|
|
1134
|
+
await mkdir(join(repoDir, ".agentplate"), { recursive: true });
|
|
1135
|
+
await Bun.write(
|
|
1136
|
+
join(repoDir, ".agentplate", "config.yaml"),
|
|
1137
|
+
"project:\n canonicalBranch: main\n",
|
|
1138
|
+
);
|
|
1139
|
+
await runGitInDir(repoDir, ["add", ".agentplate/config.yaml"]);
|
|
1140
|
+
await runGitInDir(repoDir, ["commit", "-m", "add agentplate config"]);
|
|
1141
|
+
|
|
1142
|
+
// Create a worktree — it will now have .agentplate/config.yaml from git
|
|
1143
|
+
const worktreeDir = join(repoDir, ".agentplate", "worktrees", "mail-scout");
|
|
1144
|
+
await mkdir(join(repoDir, ".agentplate", "worktrees"), { recursive: true });
|
|
1145
|
+
await runGitInDir(repoDir, [
|
|
1146
|
+
"worktree",
|
|
1147
|
+
"add",
|
|
1148
|
+
"-b",
|
|
1149
|
+
"agentplate/mail-scout/task-1",
|
|
1150
|
+
worktreeDir,
|
|
1151
|
+
]);
|
|
1152
|
+
|
|
1153
|
+
// Must resolve to main repo root, NOT the worktree
|
|
1154
|
+
// (even though worktree has its own .agentplate/config.yaml)
|
|
1155
|
+
const result = await resolveProjectRoot(worktreeDir);
|
|
1156
|
+
expect(result).toBe(repoDir);
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
test("loadConfig resolves correct root from worktree", async () => {
|
|
1160
|
+
repoDir = await createTempGitRepo();
|
|
1161
|
+
// Resolve symlinks (macOS /var -> /private/var) to match git's output
|
|
1162
|
+
repoDir = await realpath(repoDir);
|
|
1163
|
+
await mkdir(join(repoDir, ".agentplate"), { recursive: true });
|
|
1164
|
+
await Bun.write(
|
|
1165
|
+
join(repoDir, ".agentplate", "config.yaml"),
|
|
1166
|
+
"project:\n canonicalBranch: develop\n",
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const worktreeDir = join(repoDir, ".agentplate", "worktrees", "agent-2");
|
|
1170
|
+
await mkdir(join(repoDir, ".agentplate", "worktrees"), { recursive: true });
|
|
1171
|
+
await runGitInDir(repoDir, ["worktree", "add", "-b", "agentplate/agent-2/task-2", worktreeDir]);
|
|
1172
|
+
|
|
1173
|
+
// loadConfig from the worktree should resolve to the main project root
|
|
1174
|
+
const config = await loadConfig(worktreeDir);
|
|
1175
|
+
expect(config.project.root).toBe(repoDir);
|
|
1176
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
describe("resolveProjectRoot — env var and walk-up", () => {
|
|
1181
|
+
let tempDir: string;
|
|
1182
|
+
let savedEnv: string | undefined;
|
|
1183
|
+
|
|
1184
|
+
beforeEach(async () => {
|
|
1185
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-envtest-"));
|
|
1186
|
+
savedEnv = process.env.AGENTPLATE_PROJECT_ROOT;
|
|
1187
|
+
delete process.env.AGENTPLATE_PROJECT_ROOT;
|
|
1188
|
+
clearProjectRootOverride();
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
afterEach(async () => {
|
|
1192
|
+
if (savedEnv !== undefined) {
|
|
1193
|
+
process.env.AGENTPLATE_PROJECT_ROOT = savedEnv;
|
|
1194
|
+
} else {
|
|
1195
|
+
delete process.env.AGENTPLATE_PROJECT_ROOT;
|
|
1196
|
+
}
|
|
1197
|
+
clearProjectRootOverride();
|
|
1198
|
+
await cleanupTempDir(tempDir);
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
test("AGENTPLATE_PROJECT_ROOT env var is returned immediately", async () => {
|
|
1202
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
1203
|
+
await Bun.write(
|
|
1204
|
+
join(tempDir, ".agentplate", "config.yaml"),
|
|
1205
|
+
"project:\n canonicalBranch: main\n",
|
|
1206
|
+
);
|
|
1207
|
+
process.env.AGENTPLATE_PROJECT_ROOT = tempDir;
|
|
1208
|
+
const result = await resolveProjectRoot("/some/unrelated/path");
|
|
1209
|
+
expect(result).toBe(tempDir);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
test("env var beats walk-up worktree resolution", async () => {
|
|
1213
|
+
// Set up a parent root with config.yaml
|
|
1214
|
+
const parentRoot = tempDir;
|
|
1215
|
+
await mkdir(join(parentRoot, ".agentplate", "worktrees", "some-agent"), { recursive: true });
|
|
1216
|
+
await Bun.write(
|
|
1217
|
+
join(parentRoot, ".agentplate", "config.yaml"),
|
|
1218
|
+
"project:\n canonicalBranch: main\n",
|
|
1219
|
+
);
|
|
1220
|
+
const worktreePath = join(parentRoot, ".agentplate", "worktrees", "some-agent");
|
|
1221
|
+
// Even though walk-up would resolve parentRoot, env var pointing elsewhere wins
|
|
1222
|
+
const envTarget = await mkdtemp(join(tmpdir(), "agentplate-envtarget-"));
|
|
1223
|
+
try {
|
|
1224
|
+
process.env.AGENTPLATE_PROJECT_ROOT = envTarget;
|
|
1225
|
+
const result = await resolveProjectRoot(worktreePath);
|
|
1226
|
+
expect(result).toBe(envTarget);
|
|
1227
|
+
} finally {
|
|
1228
|
+
await cleanupTempDir(envTarget);
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
test("walk-up resolves submodule path without git", async () => {
|
|
1233
|
+
// Simulate a submodule worktree: {tempDir}/.agentplate/worktrees/my-agent/sub
|
|
1234
|
+
// config.yaml exists at {tempDir}/.agentplate/config.yaml
|
|
1235
|
+
const worktreeBase = join(tempDir, ".agentplate", "worktrees", "my-agent");
|
|
1236
|
+
const subDir = join(worktreeBase, "sub");
|
|
1237
|
+
await mkdir(subDir, { recursive: true });
|
|
1238
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
1239
|
+
await Bun.write(
|
|
1240
|
+
join(tempDir, ".agentplate", "config.yaml"),
|
|
1241
|
+
"project:\n canonicalBranch: main\n",
|
|
1242
|
+
);
|
|
1243
|
+
const result = await resolveProjectRoot(subDir);
|
|
1244
|
+
expect(result).toBe(tempDir);
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
test("walk-up is skipped when parent has no config.yaml", async () => {
|
|
1248
|
+
// Same path structure but NO config.yaml at parentRoot
|
|
1249
|
+
const worktreeBase = join(tempDir, ".agentplate", "worktrees", "my-agent");
|
|
1250
|
+
await mkdir(worktreeBase, { recursive: true });
|
|
1251
|
+
// No config.yaml written — walk-up guard should prevent false resolution
|
|
1252
|
+
const result = await resolveProjectRoot(worktreeBase);
|
|
1253
|
+
// Falls through to startDir fallback
|
|
1254
|
+
expect(result).toBe(worktreeBase);
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
describe("projectRootOverride", () => {
|
|
1259
|
+
let tempDir: string;
|
|
1260
|
+
|
|
1261
|
+
beforeEach(async () => {
|
|
1262
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-test-"));
|
|
1263
|
+
clearProjectRootOverride();
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
afterEach(async () => {
|
|
1267
|
+
clearProjectRootOverride();
|
|
1268
|
+
await cleanupTempDir(tempDir);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
test("setProjectRootOverride makes resolveProjectRoot return the override", async () => {
|
|
1272
|
+
setProjectRootOverride(tempDir);
|
|
1273
|
+
const result = await resolveProjectRoot("/some/other/dir");
|
|
1274
|
+
expect(result).toBe(tempDir);
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
test("clearProjectRootOverride restores normal resolution", async () => {
|
|
1278
|
+
setProjectRootOverride("/completely/fake/path");
|
|
1279
|
+
clearProjectRootOverride();
|
|
1280
|
+
// After clearing, normal resolution returns startDir when no .agentplate present
|
|
1281
|
+
const result = await resolveProjectRoot(tempDir);
|
|
1282
|
+
expect(result).toBe(tempDir);
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
test("loadConfig respects project root override", async () => {
|
|
1286
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
1287
|
+
await Bun.write(
|
|
1288
|
+
join(tempDir, ".agentplate", "config.yaml"),
|
|
1289
|
+
"project:\n canonicalBranch: override-branch\n",
|
|
1290
|
+
);
|
|
1291
|
+
setProjectRootOverride(tempDir);
|
|
1292
|
+
const config = await loadConfig("/completely/different/path");
|
|
1293
|
+
expect(config.project.root).toBe(tempDir);
|
|
1294
|
+
expect(config.project.canonicalBranch).toBe("override-branch");
|
|
1295
|
+
});
|
|
1296
|
+
|
|
1297
|
+
test("override takes precedence over worktree resolution", async () => {
|
|
1298
|
+
// Even if we're in a worktree, the override wins
|
|
1299
|
+
const otherDir = await mkdtemp(join(tmpdir(), "agentplate-other-"));
|
|
1300
|
+
try {
|
|
1301
|
+
await mkdir(join(otherDir, ".agentplate"), { recursive: true });
|
|
1302
|
+
await Bun.write(
|
|
1303
|
+
join(otherDir, ".agentplate", "config.yaml"),
|
|
1304
|
+
"project:\n canonicalBranch: other-branch\n",
|
|
1305
|
+
);
|
|
1306
|
+
setProjectRootOverride(otherDir);
|
|
1307
|
+
const result = await resolveProjectRoot(tempDir);
|
|
1308
|
+
expect(result).toBe(otherDir);
|
|
1309
|
+
} finally {
|
|
1310
|
+
await cleanupTempDir(otherDir);
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
describe("coordinator.exitTriggers", () => {
|
|
1316
|
+
let tempDir: string;
|
|
1317
|
+
|
|
1318
|
+
beforeEach(async () => {
|
|
1319
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-test-"));
|
|
1320
|
+
const { mkdir } = await import("node:fs/promises");
|
|
1321
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
afterEach(async () => {
|
|
1325
|
+
await cleanupTempDir(tempDir);
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
async function writeConfig(yaml: string): Promise<void> {
|
|
1329
|
+
await Bun.write(join(tempDir, ".agentplate", "config.yaml"), yaml);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
test("defaults all exitTriggers to false", async () => {
|
|
1333
|
+
const config = await loadConfig(tempDir);
|
|
1334
|
+
expect(config.coordinator?.exitTriggers.allAgentsDone).toBe(false);
|
|
1335
|
+
expect(config.coordinator?.exitTriggers.taskTrackerEmpty).toBe(false);
|
|
1336
|
+
expect(config.coordinator?.exitTriggers.onShutdownSignal).toBe(false);
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
test("parses coordinator.exitTriggers from config.yaml", async () => {
|
|
1340
|
+
await writeConfig(`
|
|
1341
|
+
coordinator:
|
|
1342
|
+
exitTriggers:
|
|
1343
|
+
allAgentsDone: true
|
|
1344
|
+
taskTrackerEmpty: true
|
|
1345
|
+
onShutdownSignal: false
|
|
1346
|
+
`);
|
|
1347
|
+
const config = await loadConfig(tempDir);
|
|
1348
|
+
expect(config.coordinator?.exitTriggers.allAgentsDone).toBe(true);
|
|
1349
|
+
expect(config.coordinator?.exitTriggers.taskTrackerEmpty).toBe(true);
|
|
1350
|
+
expect(config.coordinator?.exitTriggers.onShutdownSignal).toBe(false);
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
test("partial exitTriggers override keeps unset values at default (false)", async () => {
|
|
1354
|
+
await writeConfig(`
|
|
1355
|
+
coordinator:
|
|
1356
|
+
exitTriggers:
|
|
1357
|
+
onShutdownSignal: true
|
|
1358
|
+
`);
|
|
1359
|
+
const config = await loadConfig(tempDir);
|
|
1360
|
+
expect(config.coordinator?.exitTriggers.allAgentsDone).toBe(false);
|
|
1361
|
+
expect(config.coordinator?.exitTriggers.taskTrackerEmpty).toBe(false);
|
|
1362
|
+
expect(config.coordinator?.exitTriggers.onShutdownSignal).toBe(true);
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
test("config.local.yaml can override exitTriggers", async () => {
|
|
1366
|
+
await writeConfig(`
|
|
1367
|
+
coordinator:
|
|
1368
|
+
exitTriggers:
|
|
1369
|
+
allAgentsDone: false
|
|
1370
|
+
`);
|
|
1371
|
+
await Bun.write(
|
|
1372
|
+
join(tempDir, ".agentplate", "config.local.yaml"),
|
|
1373
|
+
`coordinator:\n exitTriggers:\n allAgentsDone: true\n`,
|
|
1374
|
+
);
|
|
1375
|
+
const config = await loadConfig(tempDir);
|
|
1376
|
+
expect(config.coordinator?.exitTriggers.allAgentsDone).toBe(true);
|
|
1377
|
+
});
|
|
1378
|
+
});
|
|
1379
|
+
|
|
1380
|
+
describe("YAML parser edge cases", () => {
|
|
1381
|
+
let tempDir: string;
|
|
1382
|
+
|
|
1383
|
+
beforeEach(async () => {
|
|
1384
|
+
tempDir = await mkdtemp(join(tmpdir(), "agentplate-test-"));
|
|
1385
|
+
await mkdir(join(tempDir, ".agentplate"), { recursive: true });
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
afterEach(async () => {
|
|
1389
|
+
await cleanupTempDir(tempDir);
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
async function writeConfig(yaml: string): Promise<void> {
|
|
1393
|
+
await Bun.write(join(tempDir, ".agentplate", "config.yaml"), yaml);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
test("inline comments are stripped from values", async () => {
|
|
1397
|
+
await writeConfig(`
|
|
1398
|
+
project:
|
|
1399
|
+
canonicalBranch: develop # this is a comment
|
|
1400
|
+
`);
|
|
1401
|
+
const config = await loadConfig(tempDir);
|
|
1402
|
+
expect(config.project.canonicalBranch).toBe("develop");
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
test("quoted strings containing # are preserved (not treated as comments)", async () => {
|
|
1406
|
+
await writeConfig(`
|
|
1407
|
+
project:
|
|
1408
|
+
canonicalBranch: "feature#branch"
|
|
1409
|
+
`);
|
|
1410
|
+
const config = await loadConfig(tempDir);
|
|
1411
|
+
expect(config.project.canonicalBranch).toBe("feature#branch");
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
test("single-quoted strings containing # are preserved", async () => {
|
|
1415
|
+
await writeConfig(`
|
|
1416
|
+
project:
|
|
1417
|
+
canonicalBranch: 'feature#branch'
|
|
1418
|
+
`);
|
|
1419
|
+
const config = await loadConfig(tempDir);
|
|
1420
|
+
expect(config.project.canonicalBranch).toBe("feature#branch");
|
|
1421
|
+
});
|
|
1422
|
+
|
|
1423
|
+
test("boolean coercion: true/True/TRUE all parse as true", async () => {
|
|
1424
|
+
// Test with three separate configs since they all map to the same field
|
|
1425
|
+
for (const val of ["true", "True", "TRUE"]) {
|
|
1426
|
+
await writeConfig(`loam:\n enabled: ${val}\n`);
|
|
1427
|
+
const config = await loadConfig(tempDir);
|
|
1428
|
+
expect(config.loam.enabled).toBe(true);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
test("boolean coercion: false/False/FALSE all parse as false", async () => {
|
|
1433
|
+
for (const val of ["false", "False", "FALSE"]) {
|
|
1434
|
+
await writeConfig(`loam:\n enabled: ${val}\n`);
|
|
1435
|
+
const config = await loadConfig(tempDir);
|
|
1436
|
+
expect(config.loam.enabled).toBe(false);
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
test("yes/no are treated as plain strings, not booleans", async () => {
|
|
1441
|
+
// The YAML parser does NOT treat yes/no as booleans (unlike YAML 1.1)
|
|
1442
|
+
await writeConfig(`
|
|
1443
|
+
project:
|
|
1444
|
+
canonicalBranch: yes
|
|
1445
|
+
`);
|
|
1446
|
+
const config = await loadConfig(tempDir);
|
|
1447
|
+
// "yes" is a plain string, not coerced to boolean
|
|
1448
|
+
expect(config.project.canonicalBranch).toBe("yes");
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
test("integer number coercion", async () => {
|
|
1452
|
+
await writeConfig(`
|
|
1453
|
+
agents:
|
|
1454
|
+
maxConcurrent: 42
|
|
1455
|
+
`);
|
|
1456
|
+
const config = await loadConfig(tempDir);
|
|
1457
|
+
expect(config.agents.maxConcurrent).toBe(42);
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
test("float number coercion", async () => {
|
|
1461
|
+
// maxSessionsPerRun doesn't accept floats, but the parser itself parses them.
|
|
1462
|
+
// Use a field that passes validation as a number.
|
|
1463
|
+
await writeConfig(`
|
|
1464
|
+
agents:
|
|
1465
|
+
maxSessionsPerRun: 5
|
|
1466
|
+
staggerDelayMs: 1500
|
|
1467
|
+
`);
|
|
1468
|
+
const config = await loadConfig(tempDir);
|
|
1469
|
+
expect(config.agents.staggerDelayMs).toBe(1500);
|
|
1470
|
+
});
|
|
1471
|
+
|
|
1472
|
+
test("underscore-separated numbers are coerced correctly", async () => {
|
|
1473
|
+
await writeConfig(`
|
|
1474
|
+
watchdog:
|
|
1475
|
+
staleThresholdMs: 300_000
|
|
1476
|
+
zombieThresholdMs: 600_000
|
|
1477
|
+
`);
|
|
1478
|
+
const config = await loadConfig(tempDir);
|
|
1479
|
+
expect(config.watchdog.staleThresholdMs).toBe(300_000);
|
|
1480
|
+
expect(config.watchdog.zombieThresholdMs).toBe(600_000);
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
describe("DEFAULT_CONFIG", () => {
|
|
1485
|
+
test("has all required top-level keys", () => {
|
|
1486
|
+
expect(DEFAULT_CONFIG.project).toBeDefined();
|
|
1487
|
+
expect(DEFAULT_CONFIG.agents).toBeDefined();
|
|
1488
|
+
expect(DEFAULT_CONFIG.worktrees).toBeDefined();
|
|
1489
|
+
expect(DEFAULT_CONFIG.taskTracker).toBeDefined();
|
|
1490
|
+
expect(DEFAULT_CONFIG.loam).toBeDefined();
|
|
1491
|
+
expect(DEFAULT_CONFIG.merge).toBeDefined();
|
|
1492
|
+
expect(DEFAULT_CONFIG.providers).toBeDefined();
|
|
1493
|
+
expect(DEFAULT_CONFIG.watchdog).toBeDefined();
|
|
1494
|
+
expect(DEFAULT_CONFIG.models).toBeDefined();
|
|
1495
|
+
expect(DEFAULT_CONFIG.logging).toBeDefined();
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
test("has default providers with anthropic native", () => {
|
|
1499
|
+
expect(DEFAULT_CONFIG.providers).toBeDefined();
|
|
1500
|
+
expect(DEFAULT_CONFIG.providers.anthropic).toEqual({ type: "native" });
|
|
1501
|
+
});
|
|
1502
|
+
|
|
1503
|
+
test("has sensible default values", () => {
|
|
1504
|
+
expect(DEFAULT_CONFIG.project.canonicalBranch).toBe("main");
|
|
1505
|
+
expect(DEFAULT_CONFIG.agents.maxConcurrent).toBe(25);
|
|
1506
|
+
expect(DEFAULT_CONFIG.agents.maxDepth).toBe(2);
|
|
1507
|
+
expect(DEFAULT_CONFIG.agents.staggerDelayMs).toBe(2_000);
|
|
1508
|
+
expect(DEFAULT_CONFIG.agents.maxSessionsPerRun).toBe(0);
|
|
1509
|
+
expect(DEFAULT_CONFIG.watchdog.tier0IntervalMs).toBe(30_000);
|
|
1510
|
+
expect(DEFAULT_CONFIG.watchdog.staleThresholdMs).toBe(300_000);
|
|
1511
|
+
expect(DEFAULT_CONFIG.watchdog.zombieThresholdMs).toBe(600_000);
|
|
1512
|
+
});
|
|
1513
|
+
|
|
1514
|
+
test("includes default qualityGates", () => {
|
|
1515
|
+
expect(DEFAULT_CONFIG.project.qualityGates).toBeDefined();
|
|
1516
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.length).toBe(3);
|
|
1517
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.[0]?.command).toBe("bun test");
|
|
1518
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.[1]?.command).toBe("bun run lint");
|
|
1519
|
+
expect(DEFAULT_CONFIG.project.qualityGates?.[2]?.command).toBe("bun run typecheck");
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
test("has coordinator with exitTriggers defaulting to false", () => {
|
|
1523
|
+
expect(DEFAULT_CONFIG.coordinator).toBeDefined();
|
|
1524
|
+
expect(DEFAULT_CONFIG.coordinator?.exitTriggers.allAgentsDone).toBe(false);
|
|
1525
|
+
expect(DEFAULT_CONFIG.coordinator?.exitTriggers.taskTrackerEmpty).toBe(false);
|
|
1526
|
+
expect(DEFAULT_CONFIG.coordinator?.exitTriggers.onShutdownSignal).toBe(false);
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
test("DEFAULT_QUALITY_GATES matches the project default gates", () => {
|
|
1530
|
+
expect(DEFAULT_QUALITY_GATES).toHaveLength(3);
|
|
1531
|
+
expect(DEFAULT_QUALITY_GATES[0]?.name).toBe("Tests");
|
|
1532
|
+
expect(DEFAULT_QUALITY_GATES[1]?.name).toBe("Lint");
|
|
1533
|
+
expect(DEFAULT_QUALITY_GATES[2]?.name).toBe("Typecheck");
|
|
1534
|
+
});
|
|
1535
|
+
});
|