@herdctl/core 0.0.1
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +219 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/coverage-final.json +51 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +251 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/config/index.html +191 -0
- package/coverage/src/config/index.ts.html +442 -0
- package/coverage/src/config/interpolate.ts.html +652 -0
- package/coverage/src/config/loader.ts.html +1501 -0
- package/coverage/src/config/merge.ts.html +823 -0
- package/coverage/src/config/parser.ts.html +1213 -0
- package/coverage/src/config/schema.ts.html +1123 -0
- package/coverage/src/fleet-manager/errors.ts.html +2326 -0
- package/coverage/src/fleet-manager/event-types.ts.html +1219 -0
- package/coverage/src/fleet-manager/fleet-manager.ts.html +7030 -0
- package/coverage/src/fleet-manager/index.html +206 -0
- package/coverage/src/fleet-manager/index.ts.html +469 -0
- package/coverage/src/fleet-manager/job-manager.ts.html +2074 -0
- package/coverage/src/fleet-manager/job-queue.ts.html +2479 -0
- package/coverage/src/fleet-manager/types.ts.html +2602 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +181 -0
- package/coverage/src/runner/errors.ts.html +1006 -0
- package/coverage/src/runner/index.html +191 -0
- package/coverage/src/runner/index.ts.html +256 -0
- package/coverage/src/runner/job-executor.ts.html +1429 -0
- package/coverage/src/runner/message-processor.ts.html +1150 -0
- package/coverage/src/runner/sdk-adapter.ts.html +658 -0
- package/coverage/src/runner/types.ts.html +559 -0
- package/coverage/src/scheduler/errors.ts.html +388 -0
- package/coverage/src/scheduler/index.html +206 -0
- package/coverage/src/scheduler/index.ts.html +244 -0
- package/coverage/src/scheduler/interval.ts.html +652 -0
- package/coverage/src/scheduler/schedule-runner.ts.html +1411 -0
- package/coverage/src/scheduler/schedule-state.ts.html +718 -0
- package/coverage/src/scheduler/scheduler.ts.html +1795 -0
- package/coverage/src/scheduler/types.ts.html +733 -0
- package/coverage/src/state/directory.ts.html +736 -0
- package/coverage/src/state/errors.ts.html +376 -0
- package/coverage/src/state/fleet-state.ts.html +937 -0
- package/coverage/src/state/index.html +221 -0
- package/coverage/src/state/index.ts.html +322 -0
- package/coverage/src/state/job-metadata.ts.html +1420 -0
- package/coverage/src/state/job-output.ts.html +1033 -0
- package/coverage/src/state/schemas/fleet-state.ts.html +445 -0
- package/coverage/src/state/schemas/index.html +176 -0
- package/coverage/src/state/schemas/index.ts.html +286 -0
- package/coverage/src/state/schemas/job-metadata.ts.html +628 -0
- package/coverage/src/state/schemas/job-output.ts.html +616 -0
- package/coverage/src/state/schemas/session-info.ts.html +361 -0
- package/coverage/src/state/session.ts.html +844 -0
- package/coverage/src/state/types.ts.html +262 -0
- package/coverage/src/state/utils/atomic.ts.html +748 -0
- package/coverage/src/state/utils/index.html +146 -0
- package/coverage/src/state/utils/index.ts.html +103 -0
- package/coverage/src/state/utils/reads.ts.html +1621 -0
- package/coverage/src/work-sources/adapters/github.ts.html +3583 -0
- package/coverage/src/work-sources/adapters/index.html +131 -0
- package/coverage/src/work-sources/adapters/index.ts.html +277 -0
- package/coverage/src/work-sources/errors.ts.html +298 -0
- package/coverage/src/work-sources/index.html +176 -0
- package/coverage/src/work-sources/index.ts.html +529 -0
- package/coverage/src/work-sources/manager.ts.html +1324 -0
- package/coverage/src/work-sources/registry.ts.html +619 -0
- package/coverage/src/work-sources/types.ts.html +568 -0
- package/dist/config/__tests__/agent.test.d.ts +2 -0
- package/dist/config/__tests__/agent.test.d.ts.map +1 -0
- package/dist/config/__tests__/agent.test.js +752 -0
- package/dist/config/__tests__/agent.test.js.map +1 -0
- package/dist/config/__tests__/interpolate.test.d.ts +2 -0
- package/dist/config/__tests__/interpolate.test.d.ts.map +1 -0
- package/dist/config/__tests__/interpolate.test.js +509 -0
- package/dist/config/__tests__/interpolate.test.js.map +1 -0
- package/dist/config/__tests__/loader.test.d.ts +2 -0
- package/dist/config/__tests__/loader.test.d.ts.map +1 -0
- package/dist/config/__tests__/loader.test.js +631 -0
- package/dist/config/__tests__/loader.test.js.map +1 -0
- package/dist/config/__tests__/merge.test.d.ts +2 -0
- package/dist/config/__tests__/merge.test.d.ts.map +1 -0
- package/dist/config/__tests__/merge.test.js +672 -0
- package/dist/config/__tests__/merge.test.js.map +1 -0
- package/dist/config/__tests__/parser.test.d.ts +2 -0
- package/dist/config/__tests__/parser.test.d.ts.map +1 -0
- package/dist/config/__tests__/parser.test.js +476 -0
- package/dist/config/__tests__/parser.test.js.map +1 -0
- package/dist/config/__tests__/schema.test.d.ts +2 -0
- package/dist/config/__tests__/schema.test.d.ts.map +1 -0
- package/dist/config/__tests__/schema.test.js +776 -0
- package/dist/config/__tests__/schema.test.js.map +1 -0
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +26 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/interpolate.d.ts +76 -0
- package/dist/config/interpolate.d.ts.map +1 -0
- package/dist/config/interpolate.js +143 -0
- package/dist/config/interpolate.js.map +1 -0
- package/dist/config/loader.d.ts +147 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +336 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/merge.d.ts +84 -0
- package/dist/config/merge.d.ts.map +1 -0
- package/dist/config/merge.js +138 -0
- package/dist/config/merge.js.map +1 -0
- package/dist/config/parser.d.ts +143 -0
- package/dist/config/parser.d.ts.map +1 -0
- package/dist/config/parser.js +316 -0
- package/dist/config/parser.js.map +1 -0
- package/dist/config/schema.d.ts +1906 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +268 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/fleet-manager/__tests__/coverage.test.d.ts +13 -0
- package/dist/fleet-manager/__tests__/coverage.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/coverage.test.js +2282 -0
- package/dist/fleet-manager/__tests__/coverage.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/errors.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/errors.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/errors.test.js +557 -0
- package/dist/fleet-manager/__tests__/errors.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.js +368 -0
- package/dist/fleet-manager/__tests__/event-helpers.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/integration.test.d.ts +11 -0
- package/dist/fleet-manager/__tests__/integration.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/integration.test.js +949 -0
- package/dist/fleet-manager/__tests__/integration.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/job-control.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/job-control.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/job-control.test.js +215 -0
- package/dist/fleet-manager/__tests__/job-control.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/job-manager.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/job-manager.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/job-manager.test.js +659 -0
- package/dist/fleet-manager/__tests__/job-manager.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/job-queue.test.d.ts +5 -0
- package/dist/fleet-manager/__tests__/job-queue.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/job-queue.test.js +315 -0
- package/dist/fleet-manager/__tests__/job-queue.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/reload.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/reload.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/reload.test.js +609 -0
- package/dist/fleet-manager/__tests__/reload.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/status-queries.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/status-queries.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/status-queries.test.js +488 -0
- package/dist/fleet-manager/__tests__/status-queries.test.js.map +1 -0
- package/dist/fleet-manager/__tests__/trigger.test.d.ts +7 -0
- package/dist/fleet-manager/__tests__/trigger.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/trigger.test.js +471 -0
- package/dist/fleet-manager/__tests__/trigger.test.js.map +1 -0
- package/dist/fleet-manager/errors.d.ts +407 -0
- package/dist/fleet-manager/errors.d.ts.map +1 -0
- package/dist/fleet-manager/errors.js +569 -0
- package/dist/fleet-manager/errors.js.map +1 -0
- package/dist/fleet-manager/event-types.d.ts +302 -0
- package/dist/fleet-manager/event-types.d.ts.map +1 -0
- package/dist/fleet-manager/event-types.js +9 -0
- package/dist/fleet-manager/event-types.js.map +1 -0
- package/dist/fleet-manager/fleet-manager.d.ts +699 -0
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -0
- package/dist/fleet-manager/fleet-manager.js +1906 -0
- package/dist/fleet-manager/fleet-manager.js.map +1 -0
- package/dist/fleet-manager/index.d.ts +17 -0
- package/dist/fleet-manager/index.d.ts.map +1 -0
- package/dist/fleet-manager/index.js +29 -0
- package/dist/fleet-manager/index.js.map +1 -0
- package/dist/fleet-manager/job-manager.d.ts +271 -0
- package/dist/fleet-manager/job-manager.d.ts.map +1 -0
- package/dist/fleet-manager/job-manager.js +443 -0
- package/dist/fleet-manager/job-manager.js.map +1 -0
- package/dist/fleet-manager/job-queue.d.ts +422 -0
- package/dist/fleet-manager/job-queue.d.ts.map +1 -0
- package/dist/fleet-manager/job-queue.js +448 -0
- package/dist/fleet-manager/job-queue.js.map +1 -0
- package/dist/fleet-manager/types.d.ts +680 -0
- package/dist/fleet-manager/types.d.ts.map +1 -0
- package/dist/fleet-manager/types.js +8 -0
- package/dist/fleet-manager/types.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/runner/__tests__/errors.test.d.ts +2 -0
- package/dist/runner/__tests__/errors.test.d.ts.map +1 -0
- package/dist/runner/__tests__/errors.test.js +264 -0
- package/dist/runner/__tests__/errors.test.js.map +1 -0
- package/dist/runner/__tests__/job-executor.test.d.ts +2 -0
- package/dist/runner/__tests__/job-executor.test.d.ts.map +1 -0
- package/dist/runner/__tests__/job-executor.test.js +1345 -0
- package/dist/runner/__tests__/job-executor.test.js.map +1 -0
- package/dist/runner/__tests__/message-processor.test.d.ts +2 -0
- package/dist/runner/__tests__/message-processor.test.d.ts.map +1 -0
- package/dist/runner/__tests__/message-processor.test.js +768 -0
- package/dist/runner/__tests__/message-processor.test.js.map +1 -0
- package/dist/runner/__tests__/sdk-adapter.test.d.ts +2 -0
- package/dist/runner/__tests__/sdk-adapter.test.d.ts.map +1 -0
- package/dist/runner/__tests__/sdk-adapter.test.js +554 -0
- package/dist/runner/__tests__/sdk-adapter.test.js.map +1 -0
- package/dist/runner/errors.d.ts +121 -0
- package/dist/runner/errors.d.ts.map +1 -0
- package/dist/runner/errors.js +212 -0
- package/dist/runner/errors.js.map +1 -0
- package/dist/runner/index.d.ts +12 -0
- package/dist/runner/index.d.ts.map +1 -0
- package/dist/runner/index.js +15 -0
- package/dist/runner/index.js.map +1 -0
- package/dist/runner/job-executor.d.ts +98 -0
- package/dist/runner/job-executor.d.ts.map +1 -0
- package/dist/runner/job-executor.js +333 -0
- package/dist/runner/job-executor.js.map +1 -0
- package/dist/runner/message-processor.d.ts +45 -0
- package/dist/runner/message-processor.d.ts.map +1 -0
- package/dist/runner/message-processor.js +294 -0
- package/dist/runner/message-processor.js.map +1 -0
- package/dist/runner/sdk-adapter.d.ts +60 -0
- package/dist/runner/sdk-adapter.d.ts.map +1 -0
- package/dist/runner/sdk-adapter.js +138 -0
- package/dist/runner/sdk-adapter.js.map +1 -0
- package/dist/runner/types.d.ts +135 -0
- package/dist/runner/types.d.ts.map +1 -0
- package/dist/runner/types.js +7 -0
- package/dist/runner/types.js.map +1 -0
- package/dist/scheduler/__tests__/errors.test.d.ts +2 -0
- package/dist/scheduler/__tests__/errors.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/errors.test.js +101 -0
- package/dist/scheduler/__tests__/errors.test.js.map +1 -0
- package/dist/scheduler/__tests__/interval.test.d.ts +2 -0
- package/dist/scheduler/__tests__/interval.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/interval.test.js +419 -0
- package/dist/scheduler/__tests__/interval.test.js.map +1 -0
- package/dist/scheduler/__tests__/schedule-runner.test.d.ts +2 -0
- package/dist/scheduler/__tests__/schedule-runner.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/schedule-runner.test.js +634 -0
- package/dist/scheduler/__tests__/schedule-runner.test.js.map +1 -0
- package/dist/scheduler/__tests__/schedule-state.test.d.ts +2 -0
- package/dist/scheduler/__tests__/schedule-state.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/schedule-state.test.js +572 -0
- package/dist/scheduler/__tests__/schedule-state.test.js.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.d.ts +2 -0
- package/dist/scheduler/__tests__/scheduler.test.d.ts.map +1 -0
- package/dist/scheduler/__tests__/scheduler.test.js +987 -0
- package/dist/scheduler/__tests__/scheduler.test.js.map +1 -0
- package/dist/scheduler/errors.d.ts +61 -0
- package/dist/scheduler/errors.d.ts.map +1 -0
- package/dist/scheduler/errors.js +81 -0
- package/dist/scheduler/errors.js.map +1 -0
- package/dist/scheduler/index.d.ts +13 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +17 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/scheduler/interval.d.ts +64 -0
- package/dist/scheduler/interval.d.ts.map +1 -0
- package/dist/scheduler/interval.js +139 -0
- package/dist/scheduler/interval.js.map +1 -0
- package/dist/scheduler/schedule-runner.d.ts +149 -0
- package/dist/scheduler/schedule-runner.d.ts.map +1 -0
- package/dist/scheduler/schedule-runner.js +277 -0
- package/dist/scheduler/schedule-runner.js.map +1 -0
- package/dist/scheduler/schedule-state.d.ts +105 -0
- package/dist/scheduler/schedule-state.d.ts.map +1 -0
- package/dist/scheduler/schedule-state.js +151 -0
- package/dist/scheduler/schedule-state.js.map +1 -0
- package/dist/scheduler/scheduler.d.ts +138 -0
- package/dist/scheduler/scheduler.d.ts.map +1 -0
- package/dist/scheduler/scheduler.js +423 -0
- package/dist/scheduler/scheduler.js.map +1 -0
- package/dist/scheduler/types.d.ts +160 -0
- package/dist/scheduler/types.d.ts.map +1 -0
- package/dist/scheduler/types.js +8 -0
- package/dist/scheduler/types.js.map +1 -0
- package/dist/state/__tests__/directory.test.d.ts +2 -0
- package/dist/state/__tests__/directory.test.d.ts.map +1 -0
- package/dist/state/__tests__/directory.test.js +414 -0
- package/dist/state/__tests__/directory.test.js.map +1 -0
- package/dist/state/__tests__/fleet-state.test.d.ts +2 -0
- package/dist/state/__tests__/fleet-state.test.d.ts.map +1 -0
- package/dist/state/__tests__/fleet-state.test.js +696 -0
- package/dist/state/__tests__/fleet-state.test.js.map +1 -0
- package/dist/state/__tests__/job-metadata-schema.test.d.ts +2 -0
- package/dist/state/__tests__/job-metadata-schema.test.d.ts.map +1 -0
- package/dist/state/__tests__/job-metadata-schema.test.js +329 -0
- package/dist/state/__tests__/job-metadata-schema.test.js.map +1 -0
- package/dist/state/__tests__/job-metadata.test.d.ts +2 -0
- package/dist/state/__tests__/job-metadata.test.d.ts.map +1 -0
- package/dist/state/__tests__/job-metadata.test.js +667 -0
- package/dist/state/__tests__/job-metadata.test.js.map +1 -0
- package/dist/state/__tests__/job-output.test.d.ts +2 -0
- package/dist/state/__tests__/job-output.test.d.ts.map +1 -0
- package/dist/state/__tests__/job-output.test.js +672 -0
- package/dist/state/__tests__/job-output.test.js.map +1 -0
- package/dist/state/__tests__/session-schema.test.d.ts +2 -0
- package/dist/state/__tests__/session-schema.test.d.ts.map +1 -0
- package/dist/state/__tests__/session-schema.test.js +323 -0
- package/dist/state/__tests__/session-schema.test.js.map +1 -0
- package/dist/state/__tests__/session.test.d.ts +2 -0
- package/dist/state/__tests__/session.test.d.ts.map +1 -0
- package/dist/state/__tests__/session.test.js +468 -0
- package/dist/state/__tests__/session.test.js.map +1 -0
- package/dist/state/directory.d.ts +42 -0
- package/dist/state/directory.d.ts.map +1 -0
- package/dist/state/directory.js +170 -0
- package/dist/state/directory.js.map +1 -0
- package/dist/state/errors.d.ts +44 -0
- package/dist/state/errors.d.ts.map +1 -0
- package/dist/state/errors.js +82 -0
- package/dist/state/errors.js.map +1 -0
- package/dist/state/fleet-state.d.ts +126 -0
- package/dist/state/fleet-state.d.ts.map +1 -0
- package/dist/state/fleet-state.js +196 -0
- package/dist/state/fleet-state.js.map +1 -0
- package/dist/state/index.d.ts +21 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +30 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/job-metadata.d.ts +151 -0
- package/dist/state/job-metadata.d.ts.map +1 -0
- package/dist/state/job-metadata.js +287 -0
- package/dist/state/job-metadata.js.map +1 -0
- package/dist/state/job-output.d.ts +116 -0
- package/dist/state/job-output.d.ts.map +1 -0
- package/dist/state/job-output.js +218 -0
- package/dist/state/job-output.js.map +1 -0
- package/dist/state/schemas/__tests__/job-output.test.d.ts +2 -0
- package/dist/state/schemas/__tests__/job-output.test.d.ts.map +1 -0
- package/dist/state/schemas/__tests__/job-output.test.js +279 -0
- package/dist/state/schemas/__tests__/job-output.test.js.map +1 -0
- package/dist/state/schemas/fleet-state.d.ts +249 -0
- package/dist/state/schemas/fleet-state.d.ts.map +1 -0
- package/dist/state/schemas/fleet-state.js +97 -0
- package/dist/state/schemas/fleet-state.js.map +1 -0
- package/dist/state/schemas/index.d.ts +10 -0
- package/dist/state/schemas/index.d.ts.map +1 -0
- package/dist/state/schemas/index.js +10 -0
- package/dist/state/schemas/index.js.map +1 -0
- package/dist/state/schemas/job-metadata.d.ts +118 -0
- package/dist/state/schemas/job-metadata.d.ts.map +1 -0
- package/dist/state/schemas/job-metadata.js +123 -0
- package/dist/state/schemas/job-metadata.js.map +1 -0
- package/dist/state/schemas/job-output.d.ts +291 -0
- package/dist/state/schemas/job-output.d.ts.map +1 -0
- package/dist/state/schemas/job-output.js +132 -0
- package/dist/state/schemas/job-output.js.map +1 -0
- package/dist/state/schemas/session-info.d.ts +65 -0
- package/dist/state/schemas/session-info.d.ts.map +1 -0
- package/dist/state/schemas/session-info.js +58 -0
- package/dist/state/schemas/session-info.js.map +1 -0
- package/dist/state/session.d.ts +92 -0
- package/dist/state/session.d.ts.map +1 -0
- package/dist/state/session.js +173 -0
- package/dist/state/session.js.map +1 -0
- package/dist/state/types.d.ts +54 -0
- package/dist/state/types.d.ts.map +1 -0
- package/dist/state/types.js +18 -0
- package/dist/state/types.js.map +1 -0
- package/dist/state/utils/__tests__/atomic.test.d.ts +2 -0
- package/dist/state/utils/__tests__/atomic.test.d.ts.map +1 -0
- package/dist/state/utils/__tests__/atomic.test.js +537 -0
- package/dist/state/utils/__tests__/atomic.test.js.map +1 -0
- package/dist/state/utils/__tests__/reads.test.d.ts +2 -0
- package/dist/state/utils/__tests__/reads.test.d.ts.map +1 -0
- package/dist/state/utils/__tests__/reads.test.js +792 -0
- package/dist/state/utils/__tests__/reads.test.js.map +1 -0
- package/dist/state/utils/atomic.d.ts +89 -0
- package/dist/state/utils/atomic.d.ts.map +1 -0
- package/dist/state/utils/atomic.js +157 -0
- package/dist/state/utils/atomic.js.map +1 -0
- package/dist/state/utils/index.d.ts +6 -0
- package/dist/state/utils/index.d.ts.map +1 -0
- package/dist/state/utils/index.js +6 -0
- package/dist/state/utils/index.js.map +1 -0
- package/dist/state/utils/reads.d.ts +196 -0
- package/dist/state/utils/reads.d.ts.map +1 -0
- package/dist/state/utils/reads.js +346 -0
- package/dist/state/utils/reads.js.map +1 -0
- package/dist/work-sources/__tests__/github.test.d.ts +2 -0
- package/dist/work-sources/__tests__/github.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/github.test.js +1334 -0
- package/dist/work-sources/__tests__/github.test.js.map +1 -0
- package/dist/work-sources/__tests__/manager.test.d.ts +2 -0
- package/dist/work-sources/__tests__/manager.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/manager.test.js +424 -0
- package/dist/work-sources/__tests__/manager.test.js.map +1 -0
- package/dist/work-sources/__tests__/registry.test.d.ts +2 -0
- package/dist/work-sources/__tests__/registry.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/registry.test.js +381 -0
- package/dist/work-sources/__tests__/registry.test.js.map +1 -0
- package/dist/work-sources/__tests__/types.test.d.ts +2 -0
- package/dist/work-sources/__tests__/types.test.d.ts.map +1 -0
- package/dist/work-sources/__tests__/types.test.js +406 -0
- package/dist/work-sources/__tests__/types.test.js.map +1 -0
- package/dist/work-sources/adapters/github.d.ts +290 -0
- package/dist/work-sources/adapters/github.d.ts.map +1 -0
- package/dist/work-sources/adapters/github.js +803 -0
- package/dist/work-sources/adapters/github.js.map +1 -0
- package/dist/work-sources/adapters/index.d.ts +10 -0
- package/dist/work-sources/adapters/index.d.ts.map +1 -0
- package/dist/work-sources/adapters/index.js +31 -0
- package/dist/work-sources/adapters/index.js.map +1 -0
- package/dist/work-sources/errors.d.ts +40 -0
- package/dist/work-sources/errors.d.ts.map +1 -0
- package/dist/work-sources/errors.js +54 -0
- package/dist/work-sources/errors.js.map +1 -0
- package/dist/work-sources/index.d.ts +105 -0
- package/dist/work-sources/index.d.ts.map +1 -0
- package/dist/work-sources/index.js +24 -0
- package/dist/work-sources/index.js.map +1 -0
- package/dist/work-sources/manager.d.ts +370 -0
- package/dist/work-sources/manager.d.ts.map +1 -0
- package/dist/work-sources/manager.js +61 -0
- package/dist/work-sources/manager.js.map +1 -0
- package/dist/work-sources/registry.d.ts +128 -0
- package/dist/work-sources/registry.d.ts.map +1 -0
- package/dist/work-sources/registry.js +132 -0
- package/dist/work-sources/registry.js.map +1 -0
- package/dist/work-sources/types.d.ts +127 -0
- package/dist/work-sources/types.d.ts.map +1 -0
- package/dist/work-sources/types.js +8 -0
- package/dist/work-sources/types.js.map +1 -0
- package/package.json +23 -0
- package/src/config/__tests__/agent.test.ts +864 -0
- package/src/config/__tests__/interpolate.test.ts +644 -0
- package/src/config/__tests__/loader.test.ts +784 -0
- package/src/config/__tests__/merge.test.ts +751 -0
- package/src/config/__tests__/parser.test.ts +533 -0
- package/src/config/__tests__/schema.test.ts +873 -0
- package/src/config/index.ts +119 -0
- package/src/config/interpolate.ts +189 -0
- package/src/config/loader.ts +472 -0
- package/src/config/merge.ts +246 -0
- package/src/config/parser.ts +376 -0
- package/src/config/schema.ts +346 -0
- package/src/fleet-manager/__tests__/coverage.test.ts +2869 -0
- package/src/fleet-manager/__tests__/errors.test.ts +660 -0
- package/src/fleet-manager/__tests__/event-helpers.test.ts +448 -0
- package/src/fleet-manager/__tests__/integration.test.ts +1209 -0
- package/src/fleet-manager/__tests__/job-control.test.ts +283 -0
- package/src/fleet-manager/__tests__/job-manager.test.ts +869 -0
- package/src/fleet-manager/__tests__/job-queue.test.ts +401 -0
- package/src/fleet-manager/__tests__/reload.test.ts +751 -0
- package/src/fleet-manager/__tests__/status-queries.test.ts +595 -0
- package/src/fleet-manager/__tests__/trigger.test.ts +601 -0
- package/src/fleet-manager/errors.ts +747 -0
- package/src/fleet-manager/event-types.ts +378 -0
- package/src/fleet-manager/fleet-manager.ts +2315 -0
- package/src/fleet-manager/index.ts +128 -0
- package/src/fleet-manager/job-manager.ts +663 -0
- package/src/fleet-manager/job-queue.ts +798 -0
- package/src/fleet-manager/types.ts +839 -0
- package/src/index.ts +32 -0
- package/src/runner/__tests__/errors.test.ts +382 -0
- package/src/runner/__tests__/job-executor.test.ts +1708 -0
- package/src/runner/__tests__/message-processor.test.ts +960 -0
- package/src/runner/__tests__/sdk-adapter.test.ts +626 -0
- package/src/runner/errors.ts +307 -0
- package/src/runner/index.ts +57 -0
- package/src/runner/job-executor.ts +448 -0
- package/src/runner/message-processor.ts +355 -0
- package/src/runner/sdk-adapter.ts +191 -0
- package/src/runner/types.ts +158 -0
- package/src/scheduler/__tests__/errors.test.ts +159 -0
- package/src/scheduler/__tests__/interval.test.ts +515 -0
- package/src/scheduler/__tests__/schedule-runner.test.ts +798 -0
- package/src/scheduler/__tests__/schedule-state.test.ts +671 -0
- package/src/scheduler/__tests__/scheduler.test.ts +1280 -0
- package/src/scheduler/errors.ts +101 -0
- package/src/scheduler/index.ts +53 -0
- package/src/scheduler/interval.ts +189 -0
- package/src/scheduler/schedule-runner.ts +442 -0
- package/src/scheduler/schedule-state.ts +211 -0
- package/src/scheduler/scheduler.ts +570 -0
- package/src/scheduler/types.ts +216 -0
- package/src/state/__tests__/directory.test.ts +595 -0
- package/src/state/__tests__/fleet-state.test.ts +868 -0
- package/src/state/__tests__/job-metadata-schema.test.ts +414 -0
- package/src/state/__tests__/job-metadata.test.ts +831 -0
- package/src/state/__tests__/job-output.test.ts +856 -0
- package/src/state/__tests__/session-schema.test.ts +378 -0
- package/src/state/__tests__/session.test.ts +604 -0
- package/src/state/directory.ts +217 -0
- package/src/state/errors.ts +97 -0
- package/src/state/fleet-state.ts +284 -0
- package/src/state/index.ts +79 -0
- package/src/state/job-metadata.ts +445 -0
- package/src/state/job-output.ts +316 -0
- package/src/state/schemas/__tests__/job-output.test.ts +338 -0
- package/src/state/schemas/fleet-state.ts +120 -0
- package/src/state/schemas/index.ts +67 -0
- package/src/state/schemas/job-metadata.ts +181 -0
- package/src/state/schemas/job-output.ts +177 -0
- package/src/state/schemas/session-info.ts +92 -0
- package/src/state/session.ts +253 -0
- package/src/state/types.ts +59 -0
- package/src/state/utils/__tests__/atomic.test.ts +723 -0
- package/src/state/utils/__tests__/reads.test.ts +1071 -0
- package/src/state/utils/atomic.ts +221 -0
- package/src/state/utils/index.ts +6 -0
- package/src/state/utils/reads.ts +512 -0
- package/src/work-sources/__tests__/github.test.ts +1800 -0
- package/src/work-sources/__tests__/manager.test.ts +529 -0
- package/src/work-sources/__tests__/registry.test.ts +477 -0
- package/src/work-sources/__tests__/types.test.ts +479 -0
- package/src/work-sources/adapters/github.ts +1166 -0
- package/src/work-sources/adapters/index.ts +64 -0
- package/src/work-sources/errors.ts +71 -0
- package/src/work-sources/index.ts +148 -0
- package/src/work-sources/manager.ts +413 -0
- package/src/work-sources/registry.ts +178 -0
- package/src/work-sources/types.ts +161 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,1280 @@
|
|
|
1
|
+
import {
|
|
2
|
+
describe,
|
|
3
|
+
it,
|
|
4
|
+
expect,
|
|
5
|
+
beforeEach,
|
|
6
|
+
afterEach,
|
|
7
|
+
vi,
|
|
8
|
+
type MockInstance,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
import { mkdir, rm, realpath } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { Scheduler } from "../scheduler.js";
|
|
14
|
+
import { SchedulerShutdownError } from "../errors.js";
|
|
15
|
+
import type {
|
|
16
|
+
SchedulerOptions,
|
|
17
|
+
SchedulerLogger,
|
|
18
|
+
TriggerInfo,
|
|
19
|
+
StopOptions,
|
|
20
|
+
} from "../types.js";
|
|
21
|
+
import type { ResolvedAgent } from "../../config/index.js";
|
|
22
|
+
import { writeFleetState, readFleetState } from "../../state/fleet-state.js";
|
|
23
|
+
import type { FleetState } from "../../state/schemas/fleet-state.js";
|
|
24
|
+
|
|
25
|
+
// Helper to create a temp directory
|
|
26
|
+
async function createTempDir(): Promise<string> {
|
|
27
|
+
const baseDir = join(
|
|
28
|
+
tmpdir(),
|
|
29
|
+
`herdctl-scheduler-test-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
30
|
+
);
|
|
31
|
+
await mkdir(baseDir, { recursive: true });
|
|
32
|
+
// Resolve to real path to handle macOS /var -> /private/var symlink
|
|
33
|
+
return await realpath(baseDir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Helper to create a mock logger
|
|
37
|
+
function createMockLogger(): SchedulerLogger & {
|
|
38
|
+
debugs: string[];
|
|
39
|
+
infos: string[];
|
|
40
|
+
warnings: string[];
|
|
41
|
+
errors: string[];
|
|
42
|
+
} {
|
|
43
|
+
const debugs: string[] = [];
|
|
44
|
+
const infos: string[] = [];
|
|
45
|
+
const warnings: string[] = [];
|
|
46
|
+
const errors: string[] = [];
|
|
47
|
+
return {
|
|
48
|
+
debugs,
|
|
49
|
+
infos,
|
|
50
|
+
warnings,
|
|
51
|
+
errors,
|
|
52
|
+
debug: (message: string) => debugs.push(message),
|
|
53
|
+
info: (message: string) => infos.push(message),
|
|
54
|
+
warn: (message: string) => warnings.push(message),
|
|
55
|
+
error: (message: string) => errors.push(message),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Helper to create a test agent
|
|
60
|
+
function createTestAgent(
|
|
61
|
+
name: string,
|
|
62
|
+
schedules?: Record<string, { type: string; interval?: string; prompt?: string }>
|
|
63
|
+
): ResolvedAgent {
|
|
64
|
+
return {
|
|
65
|
+
name,
|
|
66
|
+
configPath: `/fake/path/${name}.yaml`,
|
|
67
|
+
schedules,
|
|
68
|
+
} as ResolvedAgent;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Helper to wait for a short period
|
|
72
|
+
function wait(ms: number): Promise<void> {
|
|
73
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe("Scheduler", () => {
|
|
77
|
+
let tempDir: string;
|
|
78
|
+
let mockLogger: ReturnType<typeof createMockLogger>;
|
|
79
|
+
|
|
80
|
+
beforeEach(async () => {
|
|
81
|
+
tempDir = await createTempDir();
|
|
82
|
+
mockLogger = createMockLogger();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(async () => {
|
|
86
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("constructor", () => {
|
|
90
|
+
it("creates scheduler with default check interval", () => {
|
|
91
|
+
const scheduler = new Scheduler({
|
|
92
|
+
stateDir: tempDir,
|
|
93
|
+
logger: mockLogger,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
97
|
+
expect(scheduler.isRunning()).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("creates scheduler with custom check interval", () => {
|
|
101
|
+
const scheduler = new Scheduler({
|
|
102
|
+
stateDir: tempDir,
|
|
103
|
+
checkInterval: 500,
|
|
104
|
+
logger: mockLogger,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("isRunning", () => {
|
|
112
|
+
it("returns false when stopped", () => {
|
|
113
|
+
const scheduler = new Scheduler({
|
|
114
|
+
stateDir: tempDir,
|
|
115
|
+
logger: mockLogger,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(scheduler.isRunning()).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("returns true when running", async () => {
|
|
122
|
+
const scheduler = new Scheduler({
|
|
123
|
+
stateDir: tempDir,
|
|
124
|
+
checkInterval: 100,
|
|
125
|
+
logger: mockLogger,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Start in background
|
|
129
|
+
const startPromise = scheduler.start([]);
|
|
130
|
+
|
|
131
|
+
// Wait a tick for status to update
|
|
132
|
+
await wait(10);
|
|
133
|
+
expect(scheduler.isRunning()).toBe(true);
|
|
134
|
+
|
|
135
|
+
// Stop the scheduler
|
|
136
|
+
await scheduler.stop();
|
|
137
|
+
await startPromise;
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("getStatus", () => {
|
|
142
|
+
it("returns stopped initially", () => {
|
|
143
|
+
const scheduler = new Scheduler({
|
|
144
|
+
stateDir: tempDir,
|
|
145
|
+
logger: mockLogger,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns running when started", async () => {
|
|
152
|
+
const scheduler = new Scheduler({
|
|
153
|
+
stateDir: tempDir,
|
|
154
|
+
checkInterval: 100,
|
|
155
|
+
logger: mockLogger,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const startPromise = scheduler.start([]);
|
|
159
|
+
await wait(10);
|
|
160
|
+
|
|
161
|
+
expect(scheduler.getStatus()).toBe("running");
|
|
162
|
+
|
|
163
|
+
await scheduler.stop();
|
|
164
|
+
await startPromise;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns stopped after stop", async () => {
|
|
168
|
+
const scheduler = new Scheduler({
|
|
169
|
+
stateDir: tempDir,
|
|
170
|
+
checkInterval: 100,
|
|
171
|
+
logger: mockLogger,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const startPromise = scheduler.start([]);
|
|
175
|
+
await wait(10);
|
|
176
|
+
await scheduler.stop();
|
|
177
|
+
await startPromise;
|
|
178
|
+
|
|
179
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe("getState", () => {
|
|
184
|
+
it("returns initial state", () => {
|
|
185
|
+
const scheduler = new Scheduler({
|
|
186
|
+
stateDir: tempDir,
|
|
187
|
+
logger: mockLogger,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const state = scheduler.getState();
|
|
191
|
+
|
|
192
|
+
expect(state.status).toBe("stopped");
|
|
193
|
+
expect(state.startedAt).toBeNull();
|
|
194
|
+
expect(state.checkCount).toBe(0);
|
|
195
|
+
expect(state.triggerCount).toBe(0);
|
|
196
|
+
expect(state.lastCheckAt).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("updates startedAt when started", async () => {
|
|
200
|
+
const scheduler = new Scheduler({
|
|
201
|
+
stateDir: tempDir,
|
|
202
|
+
checkInterval: 100,
|
|
203
|
+
logger: mockLogger,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const beforeStart = new Date().toISOString();
|
|
207
|
+
const startPromise = scheduler.start([]);
|
|
208
|
+
await wait(10);
|
|
209
|
+
|
|
210
|
+
const state = scheduler.getState();
|
|
211
|
+
expect(state.startedAt).not.toBeNull();
|
|
212
|
+
expect(new Date(state.startedAt!).getTime()).toBeGreaterThanOrEqual(
|
|
213
|
+
new Date(beforeStart).getTime()
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
await scheduler.stop();
|
|
217
|
+
await startPromise;
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("increments checkCount", async () => {
|
|
221
|
+
const scheduler = new Scheduler({
|
|
222
|
+
stateDir: tempDir,
|
|
223
|
+
checkInterval: 50,
|
|
224
|
+
logger: mockLogger,
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
const startPromise = scheduler.start([]);
|
|
228
|
+
|
|
229
|
+
// Wait for a few checks
|
|
230
|
+
await wait(180);
|
|
231
|
+
|
|
232
|
+
const state = scheduler.getState();
|
|
233
|
+
expect(state.checkCount).toBeGreaterThan(0);
|
|
234
|
+
|
|
235
|
+
await scheduler.stop();
|
|
236
|
+
await startPromise;
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("start", () => {
|
|
241
|
+
it("starts the scheduler", async () => {
|
|
242
|
+
const scheduler = new Scheduler({
|
|
243
|
+
stateDir: tempDir,
|
|
244
|
+
checkInterval: 100,
|
|
245
|
+
logger: mockLogger,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const startPromise = scheduler.start([]);
|
|
249
|
+
await wait(10);
|
|
250
|
+
|
|
251
|
+
expect(scheduler.isRunning()).toBe(true);
|
|
252
|
+
expect(mockLogger.infos.some((m) => m.includes("started"))).toBe(true);
|
|
253
|
+
|
|
254
|
+
await scheduler.stop();
|
|
255
|
+
await startPromise;
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("throws if already running", async () => {
|
|
259
|
+
const scheduler = new Scheduler({
|
|
260
|
+
stateDir: tempDir,
|
|
261
|
+
checkInterval: 100,
|
|
262
|
+
logger: mockLogger,
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const startPromise = scheduler.start([]);
|
|
266
|
+
await wait(10);
|
|
267
|
+
|
|
268
|
+
await expect(scheduler.start([])).rejects.toThrow("already running");
|
|
269
|
+
|
|
270
|
+
await scheduler.stop();
|
|
271
|
+
await startPromise;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("logs the number of agents and check interval", async () => {
|
|
275
|
+
const scheduler = new Scheduler({
|
|
276
|
+
stateDir: tempDir,
|
|
277
|
+
checkInterval: 500,
|
|
278
|
+
logger: mockLogger,
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const agents = [
|
|
282
|
+
createTestAgent("agent-1"),
|
|
283
|
+
createTestAgent("agent-2"),
|
|
284
|
+
];
|
|
285
|
+
|
|
286
|
+
const startPromise = scheduler.start(agents);
|
|
287
|
+
await wait(10);
|
|
288
|
+
|
|
289
|
+
expect(
|
|
290
|
+
mockLogger.infos.some(
|
|
291
|
+
(m) => m.includes("2 agents") && m.includes("500ms")
|
|
292
|
+
)
|
|
293
|
+
).toBe(true);
|
|
294
|
+
|
|
295
|
+
await scheduler.stop();
|
|
296
|
+
await startPromise;
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("stop", () => {
|
|
301
|
+
it("stops the scheduler", async () => {
|
|
302
|
+
const scheduler = new Scheduler({
|
|
303
|
+
stateDir: tempDir,
|
|
304
|
+
checkInterval: 100,
|
|
305
|
+
logger: mockLogger,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const startPromise = scheduler.start([]);
|
|
309
|
+
await wait(10);
|
|
310
|
+
|
|
311
|
+
await scheduler.stop();
|
|
312
|
+
await startPromise;
|
|
313
|
+
|
|
314
|
+
expect(scheduler.isRunning()).toBe(false);
|
|
315
|
+
expect(mockLogger.infos.some((m) => m.includes("stopped"))).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("does nothing if already stopped", async () => {
|
|
319
|
+
const scheduler = new Scheduler({
|
|
320
|
+
stateDir: tempDir,
|
|
321
|
+
logger: mockLogger,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await scheduler.stop(); // Should not throw
|
|
325
|
+
|
|
326
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe("setAgents", () => {
|
|
331
|
+
it("updates the agents list", async () => {
|
|
332
|
+
const scheduler = new Scheduler({
|
|
333
|
+
stateDir: tempDir,
|
|
334
|
+
checkInterval: 100,
|
|
335
|
+
logger: mockLogger,
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const startPromise = scheduler.start([]);
|
|
339
|
+
await wait(10);
|
|
340
|
+
|
|
341
|
+
const newAgents = [createTestAgent("new-agent")];
|
|
342
|
+
scheduler.setAgents(newAgents);
|
|
343
|
+
|
|
344
|
+
expect(
|
|
345
|
+
mockLogger.debugs.some((m) => m.includes("Updated agents list"))
|
|
346
|
+
).toBe(true);
|
|
347
|
+
|
|
348
|
+
await scheduler.stop();
|
|
349
|
+
await startPromise;
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe("schedule checking", () => {
|
|
354
|
+
it("skips agents without schedules", async () => {
|
|
355
|
+
const scheduler = new Scheduler({
|
|
356
|
+
stateDir: tempDir,
|
|
357
|
+
checkInterval: 50,
|
|
358
|
+
logger: mockLogger,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const agents = [createTestAgent("no-schedules-agent")];
|
|
362
|
+
|
|
363
|
+
const startPromise = scheduler.start(agents);
|
|
364
|
+
await wait(100);
|
|
365
|
+
|
|
366
|
+
// Should complete check without errors
|
|
367
|
+
const state = scheduler.getState();
|
|
368
|
+
expect(state.checkCount).toBeGreaterThan(0);
|
|
369
|
+
expect(state.triggerCount).toBe(0);
|
|
370
|
+
|
|
371
|
+
await scheduler.stop();
|
|
372
|
+
await startPromise;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("skips non-interval schedule types", async () => {
|
|
376
|
+
const scheduler = new Scheduler({
|
|
377
|
+
stateDir: tempDir,
|
|
378
|
+
checkInterval: 50,
|
|
379
|
+
logger: mockLogger,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const agents = [
|
|
383
|
+
createTestAgent("cron-agent", {
|
|
384
|
+
hourly: { type: "cron", interval: "0 * * * *" },
|
|
385
|
+
}),
|
|
386
|
+
createTestAgent("webhook-agent", {
|
|
387
|
+
webhook: { type: "webhook" },
|
|
388
|
+
}),
|
|
389
|
+
createTestAgent("chat-agent", {
|
|
390
|
+
chat: { type: "chat" },
|
|
391
|
+
}),
|
|
392
|
+
];
|
|
393
|
+
|
|
394
|
+
const startPromise = scheduler.start(agents);
|
|
395
|
+
await wait(100);
|
|
396
|
+
|
|
397
|
+
// Should not trigger any schedules
|
|
398
|
+
const state = scheduler.getState();
|
|
399
|
+
expect(state.triggerCount).toBe(0);
|
|
400
|
+
|
|
401
|
+
await scheduler.stop();
|
|
402
|
+
await startPromise;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("skips disabled schedules", async () => {
|
|
406
|
+
const stateFile = join(tempDir, "state.yaml");
|
|
407
|
+
const initialState: FleetState = {
|
|
408
|
+
fleet: {},
|
|
409
|
+
agents: {
|
|
410
|
+
"test-agent": {
|
|
411
|
+
status: "idle",
|
|
412
|
+
schedules: {
|
|
413
|
+
hourly: {
|
|
414
|
+
last_run_at: null,
|
|
415
|
+
next_run_at: null,
|
|
416
|
+
status: "disabled",
|
|
417
|
+
last_error: null,
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
await writeFleetState(stateFile, initialState);
|
|
424
|
+
|
|
425
|
+
const scheduler = new Scheduler({
|
|
426
|
+
stateDir: tempDir,
|
|
427
|
+
checkInterval: 50,
|
|
428
|
+
logger: mockLogger,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const agents = [
|
|
432
|
+
createTestAgent("test-agent", {
|
|
433
|
+
hourly: { type: "interval", interval: "1s" },
|
|
434
|
+
}),
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
const startPromise = scheduler.start(agents);
|
|
438
|
+
await wait(100);
|
|
439
|
+
|
|
440
|
+
expect(
|
|
441
|
+
mockLogger.debugs.some((m) => m.includes("disabled"))
|
|
442
|
+
).toBe(true);
|
|
443
|
+
expect(scheduler.getState().triggerCount).toBe(0);
|
|
444
|
+
|
|
445
|
+
await scheduler.stop();
|
|
446
|
+
await startPromise;
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it("skips schedules missing interval value", async () => {
|
|
450
|
+
const scheduler = new Scheduler({
|
|
451
|
+
stateDir: tempDir,
|
|
452
|
+
checkInterval: 50,
|
|
453
|
+
logger: mockLogger,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const agents = [
|
|
457
|
+
createTestAgent("test-agent", {
|
|
458
|
+
broken: { type: "interval" }, // Missing interval value
|
|
459
|
+
}),
|
|
460
|
+
];
|
|
461
|
+
|
|
462
|
+
const startPromise = scheduler.start(agents);
|
|
463
|
+
await wait(100);
|
|
464
|
+
|
|
465
|
+
expect(
|
|
466
|
+
mockLogger.warnings.some((m) => m.includes("missing interval value"))
|
|
467
|
+
).toBe(true);
|
|
468
|
+
expect(scheduler.getState().triggerCount).toBe(0);
|
|
469
|
+
|
|
470
|
+
await scheduler.stop();
|
|
471
|
+
await startPromise;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("triggers due interval schedules", async () => {
|
|
475
|
+
const triggers: TriggerInfo[] = [];
|
|
476
|
+
|
|
477
|
+
const scheduler = new Scheduler({
|
|
478
|
+
stateDir: tempDir,
|
|
479
|
+
checkInterval: 50,
|
|
480
|
+
logger: mockLogger,
|
|
481
|
+
onTrigger: async (info) => {
|
|
482
|
+
triggers.push(info);
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
const agents = [
|
|
487
|
+
createTestAgent("test-agent", {
|
|
488
|
+
hourly: { type: "interval", interval: "1s", prompt: "test prompt" },
|
|
489
|
+
}),
|
|
490
|
+
];
|
|
491
|
+
|
|
492
|
+
const startPromise = scheduler.start(agents);
|
|
493
|
+
await wait(150);
|
|
494
|
+
|
|
495
|
+
// First run should trigger immediately (no last_run_at)
|
|
496
|
+
expect(triggers.length).toBeGreaterThan(0);
|
|
497
|
+
expect(triggers[0].agent.name).toBe("test-agent");
|
|
498
|
+
expect(triggers[0].scheduleName).toBe("hourly");
|
|
499
|
+
|
|
500
|
+
await scheduler.stop();
|
|
501
|
+
await startPromise;
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
it("updates schedule state on trigger", async () => {
|
|
505
|
+
const scheduler = new Scheduler({
|
|
506
|
+
stateDir: tempDir,
|
|
507
|
+
checkInterval: 50,
|
|
508
|
+
logger: mockLogger,
|
|
509
|
+
onTrigger: async () => {
|
|
510
|
+
// Simulate work
|
|
511
|
+
await wait(10);
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const agents = [
|
|
516
|
+
createTestAgent("test-agent", {
|
|
517
|
+
hourly: { type: "interval", interval: "1s" },
|
|
518
|
+
}),
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
const startPromise = scheduler.start(agents);
|
|
522
|
+
await wait(150);
|
|
523
|
+
|
|
524
|
+
// Check state was updated
|
|
525
|
+
const stateFile = join(tempDir, "state.yaml");
|
|
526
|
+
const fleetState = await readFleetState(stateFile);
|
|
527
|
+
const scheduleState = fleetState.agents["test-agent"]?.schedules?.hourly;
|
|
528
|
+
|
|
529
|
+
expect(scheduleState).toBeDefined();
|
|
530
|
+
expect(scheduleState?.last_run_at).not.toBeNull();
|
|
531
|
+
expect(scheduleState?.status).toBe("idle"); // Should be idle after completion
|
|
532
|
+
|
|
533
|
+
await scheduler.stop();
|
|
534
|
+
await startPromise;
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it("records error in schedule state on trigger failure", async () => {
|
|
538
|
+
const scheduler = new Scheduler({
|
|
539
|
+
stateDir: tempDir,
|
|
540
|
+
checkInterval: 50,
|
|
541
|
+
logger: mockLogger,
|
|
542
|
+
onTrigger: async () => {
|
|
543
|
+
throw new Error("Trigger failed!");
|
|
544
|
+
},
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
const agents = [
|
|
548
|
+
createTestAgent("test-agent", {
|
|
549
|
+
hourly: { type: "interval", interval: "1s" },
|
|
550
|
+
}),
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const startPromise = scheduler.start(agents);
|
|
554
|
+
await wait(150);
|
|
555
|
+
|
|
556
|
+
// Check error was recorded
|
|
557
|
+
const stateFile = join(tempDir, "state.yaml");
|
|
558
|
+
const fleetState = await readFleetState(stateFile);
|
|
559
|
+
const scheduleState = fleetState.agents["test-agent"]?.schedules?.hourly;
|
|
560
|
+
|
|
561
|
+
expect(scheduleState?.last_error).toBe("Trigger failed!");
|
|
562
|
+
expect(mockLogger.errors.some((m) => m.includes("Trigger failed!"))).toBe(
|
|
563
|
+
true
|
|
564
|
+
);
|
|
565
|
+
|
|
566
|
+
await scheduler.stop();
|
|
567
|
+
await startPromise;
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it("does not trigger already running schedule", async () => {
|
|
571
|
+
let triggerCount = 0;
|
|
572
|
+
let isRunning = false;
|
|
573
|
+
|
|
574
|
+
const scheduler = new Scheduler({
|
|
575
|
+
stateDir: tempDir,
|
|
576
|
+
checkInterval: 30,
|
|
577
|
+
logger: mockLogger,
|
|
578
|
+
onTrigger: async () => {
|
|
579
|
+
if (isRunning) {
|
|
580
|
+
throw new Error("Should not trigger while running!");
|
|
581
|
+
}
|
|
582
|
+
isRunning = true;
|
|
583
|
+
triggerCount++;
|
|
584
|
+
// Simulate long-running job
|
|
585
|
+
await wait(100);
|
|
586
|
+
isRunning = false;
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
const agents = [
|
|
591
|
+
createTestAgent("test-agent", {
|
|
592
|
+
hourly: { type: "interval", interval: "1s" },
|
|
593
|
+
}),
|
|
594
|
+
];
|
|
595
|
+
|
|
596
|
+
const startPromise = scheduler.start(agents);
|
|
597
|
+
|
|
598
|
+
// Wait long enough for multiple checks but only one trigger should complete
|
|
599
|
+
await wait(150);
|
|
600
|
+
|
|
601
|
+
// Should only have triggered once due to running check
|
|
602
|
+
expect(triggerCount).toBe(1);
|
|
603
|
+
|
|
604
|
+
await scheduler.stop();
|
|
605
|
+
await startPromise;
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
describe("error handling", () => {
|
|
610
|
+
it("continues checking after error in check cycle", async () => {
|
|
611
|
+
// Create scheduler that will encounter an error reading state
|
|
612
|
+
// by using a non-existent state dir initially
|
|
613
|
+
const scheduler = new Scheduler({
|
|
614
|
+
stateDir: "/nonexistent/path",
|
|
615
|
+
checkInterval: 50,
|
|
616
|
+
logger: mockLogger,
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const agents = [
|
|
620
|
+
createTestAgent("test-agent", {
|
|
621
|
+
hourly: { type: "interval", interval: "1s" },
|
|
622
|
+
}),
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
const startPromise = scheduler.start(agents);
|
|
626
|
+
await wait(150);
|
|
627
|
+
|
|
628
|
+
// Should have logged errors but kept running
|
|
629
|
+
expect(scheduler.isRunning()).toBe(true);
|
|
630
|
+
expect(scheduler.getState().checkCount).toBeGreaterThan(0);
|
|
631
|
+
|
|
632
|
+
await scheduler.stop();
|
|
633
|
+
await startPromise;
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe("concurrent execution", () => {
|
|
638
|
+
it("tracks running schedules per agent", async () => {
|
|
639
|
+
const runningAgents = new Set<string>();
|
|
640
|
+
let maxConcurrent = 0;
|
|
641
|
+
|
|
642
|
+
// Use a barrier to ensure both triggers are running at the same time
|
|
643
|
+
let triggered = 0;
|
|
644
|
+
let resolveBarrier: () => void;
|
|
645
|
+
const bothStarted = new Promise<void>((resolve) => {
|
|
646
|
+
resolveBarrier = resolve;
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const checkBoth = () => {
|
|
650
|
+
triggered++;
|
|
651
|
+
if (triggered >= 2) resolveBarrier();
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const scheduler = new Scheduler({
|
|
655
|
+
stateDir: tempDir,
|
|
656
|
+
checkInterval: 30,
|
|
657
|
+
logger: mockLogger,
|
|
658
|
+
onTrigger: async (info) => {
|
|
659
|
+
runningAgents.add(`${info.agent.name}/${info.scheduleName}`);
|
|
660
|
+
maxConcurrent = Math.max(maxConcurrent, runningAgents.size);
|
|
661
|
+
// Signal that this trigger started
|
|
662
|
+
checkBoth();
|
|
663
|
+
// Wait long enough for both to be running
|
|
664
|
+
await wait(100);
|
|
665
|
+
runningAgents.delete(`${info.agent.name}/${info.scheduleName}`);
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
const agents = [
|
|
670
|
+
createTestAgent("agent-1", {
|
|
671
|
+
schedule1: { type: "interval", interval: "1s" },
|
|
672
|
+
}),
|
|
673
|
+
createTestAgent("agent-2", {
|
|
674
|
+
schedule2: { type: "interval", interval: "1s" },
|
|
675
|
+
}),
|
|
676
|
+
];
|
|
677
|
+
|
|
678
|
+
const startPromise = scheduler.start(agents);
|
|
679
|
+
|
|
680
|
+
// Wait for both to have started running
|
|
681
|
+
await Promise.race([bothStarted, wait(300)]);
|
|
682
|
+
|
|
683
|
+
// Check max concurrent - may be 1 or 2 depending on timing
|
|
684
|
+
// The important thing is both agents were triggered
|
|
685
|
+
expect(maxConcurrent).toBeGreaterThanOrEqual(1);
|
|
686
|
+
|
|
687
|
+
await scheduler.stop();
|
|
688
|
+
await startPromise;
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
describe("max_concurrent limit", () => {
|
|
693
|
+
it("respects max_concurrent from agent instances config", async () => {
|
|
694
|
+
const triggerCounts = new Map<string, number>();
|
|
695
|
+
let concurrentForAgent2 = 0;
|
|
696
|
+
let maxConcurrentForAgent2 = 0;
|
|
697
|
+
|
|
698
|
+
const scheduler = new Scheduler({
|
|
699
|
+
stateDir: tempDir,
|
|
700
|
+
checkInterval: 30,
|
|
701
|
+
logger: mockLogger,
|
|
702
|
+
onTrigger: async (info) => {
|
|
703
|
+
const key = info.agent.name;
|
|
704
|
+
const count = (triggerCounts.get(key) || 0) + 1;
|
|
705
|
+
triggerCounts.set(key, count);
|
|
706
|
+
|
|
707
|
+
if (info.agent.name === "agent-2") {
|
|
708
|
+
concurrentForAgent2++;
|
|
709
|
+
maxConcurrentForAgent2 = Math.max(
|
|
710
|
+
maxConcurrentForAgent2,
|
|
711
|
+
concurrentForAgent2
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Simulate work
|
|
716
|
+
await wait(80);
|
|
717
|
+
|
|
718
|
+
if (info.agent.name === "agent-2") {
|
|
719
|
+
concurrentForAgent2--;
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
});
|
|
723
|
+
|
|
724
|
+
// Agent with max_concurrent: 2
|
|
725
|
+
const agentWithMaxConcurrent2 = {
|
|
726
|
+
...createTestAgent("agent-2", {
|
|
727
|
+
schedule1: { type: "interval", interval: "1s" },
|
|
728
|
+
schedule2: { type: "interval", interval: "1s" },
|
|
729
|
+
schedule3: { type: "interval", interval: "1s" },
|
|
730
|
+
}),
|
|
731
|
+
instances: { max_concurrent: 2 },
|
|
732
|
+
} as ResolvedAgent;
|
|
733
|
+
|
|
734
|
+
const startPromise = scheduler.start([agentWithMaxConcurrent2]);
|
|
735
|
+
|
|
736
|
+
// Wait enough time for multiple checks
|
|
737
|
+
await wait(200);
|
|
738
|
+
|
|
739
|
+
// Should not exceed max_concurrent of 2
|
|
740
|
+
expect(maxConcurrentForAgent2).toBeLessThanOrEqual(2);
|
|
741
|
+
|
|
742
|
+
await scheduler.stop();
|
|
743
|
+
await startPromise;
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
it("defaults max_concurrent to 1 when not specified", async () => {
|
|
747
|
+
let concurrentForAgent = 0;
|
|
748
|
+
let maxConcurrentForAgent = 0;
|
|
749
|
+
|
|
750
|
+
const scheduler = new Scheduler({
|
|
751
|
+
stateDir: tempDir,
|
|
752
|
+
checkInterval: 30,
|
|
753
|
+
logger: mockLogger,
|
|
754
|
+
onTrigger: async () => {
|
|
755
|
+
concurrentForAgent++;
|
|
756
|
+
maxConcurrentForAgent = Math.max(
|
|
757
|
+
maxConcurrentForAgent,
|
|
758
|
+
concurrentForAgent
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
await wait(80);
|
|
762
|
+
|
|
763
|
+
concurrentForAgent--;
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Agent without instances config - should default to max_concurrent: 1
|
|
768
|
+
const agent = createTestAgent("test-agent", {
|
|
769
|
+
schedule1: { type: "interval", interval: "1s" },
|
|
770
|
+
schedule2: { type: "interval", interval: "1s" },
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const startPromise = scheduler.start([agent]);
|
|
774
|
+
|
|
775
|
+
await wait(200);
|
|
776
|
+
|
|
777
|
+
// Should not exceed default max_concurrent of 1
|
|
778
|
+
expect(maxConcurrentForAgent).toBe(1);
|
|
779
|
+
|
|
780
|
+
await scheduler.stop();
|
|
781
|
+
await startPromise;
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it("skips second schedule when agent is at capacity", async () => {
|
|
785
|
+
// This test verifies that with max_concurrent: 1 and one schedule already
|
|
786
|
+
// running, additional schedules are not triggered until the first completes.
|
|
787
|
+
//
|
|
788
|
+
// Note: Due to how the scheduler iterates through schedules synchronously
|
|
789
|
+
// within a single check cycle, both schedules will be checked and triggered
|
|
790
|
+
// if they're both due at the same time before either is marked as running.
|
|
791
|
+
// The capacity check applies WITHIN each check cycle iteration, so the
|
|
792
|
+
// second schedule is only skipped if a trigger is already in progress
|
|
793
|
+
// from a PREVIOUS check cycle.
|
|
794
|
+
|
|
795
|
+
let triggerCount = 0;
|
|
796
|
+
|
|
797
|
+
const scheduler = new Scheduler({
|
|
798
|
+
stateDir: tempDir,
|
|
799
|
+
checkInterval: 30,
|
|
800
|
+
logger: mockLogger,
|
|
801
|
+
onTrigger: async () => {
|
|
802
|
+
triggerCount++;
|
|
803
|
+
// Long running job to span multiple check cycles
|
|
804
|
+
await wait(150);
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
// Agent with max_concurrent: 1 and one schedule
|
|
809
|
+
// A second schedule that becomes due AFTER the first is running
|
|
810
|
+
// will be skipped
|
|
811
|
+
const agent = createTestAgent("test-agent", {
|
|
812
|
+
schedule1: { type: "interval", interval: "1s" },
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
const startPromise = scheduler.start([agent]);
|
|
816
|
+
|
|
817
|
+
// Wait for first trigger to happen and start running
|
|
818
|
+
await wait(100);
|
|
819
|
+
|
|
820
|
+
// With 1 schedule, it should trigger once initially
|
|
821
|
+
// Then subsequent checks should show "already_running" until it completes
|
|
822
|
+
expect(triggerCount).toBe(1);
|
|
823
|
+
expect(scheduler.getRunningJobCount("test-agent")).toBe(1);
|
|
824
|
+
|
|
825
|
+
// Wait for the job to complete
|
|
826
|
+
await wait(200);
|
|
827
|
+
|
|
828
|
+
// After completion, running count should be 0
|
|
829
|
+
expect(scheduler.getRunningJobCount("test-agent")).toBe(0);
|
|
830
|
+
|
|
831
|
+
await scheduler.stop();
|
|
832
|
+
await startPromise;
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
describe("getRunningJobCount", () => {
|
|
837
|
+
it("returns 0 for agents with no running jobs", () => {
|
|
838
|
+
const scheduler = new Scheduler({
|
|
839
|
+
stateDir: tempDir,
|
|
840
|
+
logger: mockLogger,
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
expect(scheduler.getRunningJobCount("non-existent-agent")).toBe(0);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it("returns correct count during job execution", async () => {
|
|
847
|
+
let runningCountDuringExecution = -1;
|
|
848
|
+
|
|
849
|
+
const scheduler = new Scheduler({
|
|
850
|
+
stateDir: tempDir,
|
|
851
|
+
checkInterval: 30,
|
|
852
|
+
logger: mockLogger,
|
|
853
|
+
onTrigger: async (info) => {
|
|
854
|
+
// Check count while job is running
|
|
855
|
+
runningCountDuringExecution = scheduler.getRunningJobCount(
|
|
856
|
+
info.agent.name
|
|
857
|
+
);
|
|
858
|
+
await wait(50);
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
const agent = createTestAgent("test-agent", {
|
|
863
|
+
hourly: { type: "interval", interval: "1s" },
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
const startPromise = scheduler.start([agent]);
|
|
867
|
+
|
|
868
|
+
await wait(100);
|
|
869
|
+
|
|
870
|
+
// Count should have been 1 during execution
|
|
871
|
+
expect(runningCountDuringExecution).toBe(1);
|
|
872
|
+
|
|
873
|
+
await scheduler.stop();
|
|
874
|
+
await startPromise;
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("returns 0 after job completes", async () => {
|
|
878
|
+
const scheduler = new Scheduler({
|
|
879
|
+
stateDir: tempDir,
|
|
880
|
+
checkInterval: 50,
|
|
881
|
+
logger: mockLogger,
|
|
882
|
+
onTrigger: async () => {
|
|
883
|
+
await wait(20);
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
const agent = createTestAgent("test-agent", {
|
|
888
|
+
hourly: { type: "interval", interval: "10s" }, // Long interval to prevent re-trigger
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const startPromise = scheduler.start([agent]);
|
|
892
|
+
|
|
893
|
+
// Wait for trigger and completion
|
|
894
|
+
await wait(150);
|
|
895
|
+
|
|
896
|
+
// Count should be 0 after completion
|
|
897
|
+
expect(scheduler.getRunningJobCount("test-agent")).toBe(0);
|
|
898
|
+
|
|
899
|
+
await scheduler.stop();
|
|
900
|
+
await startPromise;
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
it("tracks multiple schedules for the same agent", async () => {
|
|
904
|
+
let maxCount = 0;
|
|
905
|
+
|
|
906
|
+
const scheduler = new Scheduler({
|
|
907
|
+
stateDir: tempDir,
|
|
908
|
+
checkInterval: 20,
|
|
909
|
+
logger: mockLogger,
|
|
910
|
+
onTrigger: async (info) => {
|
|
911
|
+
const count = scheduler.getRunningJobCount(info.agent.name);
|
|
912
|
+
maxCount = Math.max(maxCount, count);
|
|
913
|
+
await wait(100);
|
|
914
|
+
},
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Agent with max_concurrent: 3 and multiple schedules
|
|
918
|
+
const agent = {
|
|
919
|
+
...createTestAgent("test-agent", {
|
|
920
|
+
schedule1: { type: "interval", interval: "1s" },
|
|
921
|
+
schedule2: { type: "interval", interval: "1s" },
|
|
922
|
+
schedule3: { type: "interval", interval: "1s" },
|
|
923
|
+
}),
|
|
924
|
+
instances: { max_concurrent: 3 },
|
|
925
|
+
} as ResolvedAgent;
|
|
926
|
+
|
|
927
|
+
const startPromise = scheduler.start([agent]);
|
|
928
|
+
|
|
929
|
+
await wait(150);
|
|
930
|
+
|
|
931
|
+
// Should have tracked multiple concurrent jobs
|
|
932
|
+
expect(maxCount).toBeGreaterThanOrEqual(1);
|
|
933
|
+
|
|
934
|
+
await scheduler.stop();
|
|
935
|
+
await startPromise;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it("decrements count on job failure", async () => {
|
|
939
|
+
let countAfterError = -1;
|
|
940
|
+
|
|
941
|
+
const scheduler = new Scheduler({
|
|
942
|
+
stateDir: tempDir,
|
|
943
|
+
checkInterval: 50,
|
|
944
|
+
logger: mockLogger,
|
|
945
|
+
onTrigger: async () => {
|
|
946
|
+
throw new Error("Job failed!");
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
const agent = createTestAgent("test-agent", {
|
|
951
|
+
hourly: { type: "interval", interval: "10s" },
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
const startPromise = scheduler.start([agent]);
|
|
955
|
+
|
|
956
|
+
// Wait for trigger and error handling
|
|
957
|
+
await wait(150);
|
|
958
|
+
|
|
959
|
+
countAfterError = scheduler.getRunningJobCount("test-agent");
|
|
960
|
+
|
|
961
|
+
// Count should be 0 even after error
|
|
962
|
+
expect(countAfterError).toBe(0);
|
|
963
|
+
|
|
964
|
+
await scheduler.stop();
|
|
965
|
+
await startPromise;
|
|
966
|
+
});
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
describe("graceful shutdown", () => {
|
|
970
|
+
it("waits for running jobs to complete by default", async () => {
|
|
971
|
+
let jobStarted = false;
|
|
972
|
+
let jobCompleted = false;
|
|
973
|
+
let resolveJob: () => void;
|
|
974
|
+
const jobPromise = new Promise<void>((resolve) => {
|
|
975
|
+
resolveJob = resolve;
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
const scheduler = new Scheduler({
|
|
979
|
+
stateDir: tempDir,
|
|
980
|
+
checkInterval: 30,
|
|
981
|
+
logger: mockLogger,
|
|
982
|
+
onTrigger: async () => {
|
|
983
|
+
jobStarted = true;
|
|
984
|
+
await jobPromise;
|
|
985
|
+
jobCompleted = true;
|
|
986
|
+
},
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
const agent = createTestAgent("test-agent", {
|
|
990
|
+
hourly: { type: "interval", interval: "1s" },
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
const startPromise = scheduler.start([agent]);
|
|
994
|
+
await wait(100);
|
|
995
|
+
|
|
996
|
+
// Job should be running
|
|
997
|
+
expect(jobStarted).toBe(true);
|
|
998
|
+
expect(jobCompleted).toBe(false);
|
|
999
|
+
|
|
1000
|
+
// Start shutdown (don't await yet)
|
|
1001
|
+
const stopPromise = scheduler.stop();
|
|
1002
|
+
|
|
1003
|
+
// Give stop a moment to set status
|
|
1004
|
+
await wait(10);
|
|
1005
|
+
|
|
1006
|
+
// Scheduler should be in "stopping" state, waiting for job
|
|
1007
|
+
expect(scheduler.getStatus()).toBe("stopping");
|
|
1008
|
+
|
|
1009
|
+
// Complete the job
|
|
1010
|
+
resolveJob!();
|
|
1011
|
+
|
|
1012
|
+
// Now stop should complete
|
|
1013
|
+
await stopPromise;
|
|
1014
|
+
await startPromise;
|
|
1015
|
+
|
|
1016
|
+
expect(jobCompleted).toBe(true);
|
|
1017
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
1018
|
+
expect(mockLogger.infos.some((m) => m.includes("All running jobs completed"))).toBe(true);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
it("does not wait for jobs when waitForJobs is false", async () => {
|
|
1022
|
+
let jobStarted = false;
|
|
1023
|
+
let jobCompleted = false;
|
|
1024
|
+
|
|
1025
|
+
const scheduler = new Scheduler({
|
|
1026
|
+
stateDir: tempDir,
|
|
1027
|
+
checkInterval: 30,
|
|
1028
|
+
logger: mockLogger,
|
|
1029
|
+
onTrigger: async () => {
|
|
1030
|
+
jobStarted = true;
|
|
1031
|
+
await wait(500); // Long-running job
|
|
1032
|
+
jobCompleted = true;
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const agent = createTestAgent("test-agent", {
|
|
1037
|
+
hourly: { type: "interval", interval: "1s" },
|
|
1038
|
+
});
|
|
1039
|
+
|
|
1040
|
+
const startPromise = scheduler.start([agent]);
|
|
1041
|
+
await wait(100);
|
|
1042
|
+
|
|
1043
|
+
// Job should be running
|
|
1044
|
+
expect(jobStarted).toBe(true);
|
|
1045
|
+
expect(jobCompleted).toBe(false);
|
|
1046
|
+
|
|
1047
|
+
// Stop without waiting for jobs
|
|
1048
|
+
await scheduler.stop({ waitForJobs: false });
|
|
1049
|
+
|
|
1050
|
+
// Scheduler should be stopped immediately, job may still be running
|
|
1051
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
1052
|
+
|
|
1053
|
+
// Clean up the start promise
|
|
1054
|
+
await startPromise;
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
it("throws SchedulerShutdownError on timeout", async () => {
|
|
1058
|
+
let resolveJob: () => void;
|
|
1059
|
+
const jobBlocker = new Promise<void>((resolve) => {
|
|
1060
|
+
resolveJob = resolve;
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
const scheduler = new Scheduler({
|
|
1064
|
+
stateDir: tempDir,
|
|
1065
|
+
checkInterval: 30,
|
|
1066
|
+
logger: mockLogger,
|
|
1067
|
+
onTrigger: async () => {
|
|
1068
|
+
// Job that blocks until we release it
|
|
1069
|
+
await jobBlocker;
|
|
1070
|
+
},
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
const agent = createTestAgent("test-agent", {
|
|
1074
|
+
hourly: { type: "interval", interval: "1s" },
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const startPromise = scheduler.start([agent]);
|
|
1078
|
+
await wait(100);
|
|
1079
|
+
|
|
1080
|
+
// Stop with a short timeout
|
|
1081
|
+
let shutdownError: SchedulerShutdownError | null = null;
|
|
1082
|
+
try {
|
|
1083
|
+
await scheduler.stop({ timeout: 50 });
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
if (error instanceof SchedulerShutdownError) {
|
|
1086
|
+
shutdownError = error;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
expect(shutdownError).not.toBeNull();
|
|
1091
|
+
expect(shutdownError!.timedOut).toBe(true);
|
|
1092
|
+
expect(shutdownError!.runningJobCount).toBe(1);
|
|
1093
|
+
expect(shutdownError!.name).toBe("SchedulerShutdownError");
|
|
1094
|
+
expect(mockLogger.errors.some((m) => m.includes("timed out"))).toBe(true);
|
|
1095
|
+
|
|
1096
|
+
// Clean up - release the job so the test can complete cleanly
|
|
1097
|
+
resolveJob!();
|
|
1098
|
+
await startPromise;
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
it("stops new triggers immediately when stop is called", async () => {
|
|
1102
|
+
let triggerCount = 0;
|
|
1103
|
+
|
|
1104
|
+
const scheduler = new Scheduler({
|
|
1105
|
+
stateDir: tempDir,
|
|
1106
|
+
checkInterval: 20,
|
|
1107
|
+
logger: mockLogger,
|
|
1108
|
+
onTrigger: async () => {
|
|
1109
|
+
triggerCount++;
|
|
1110
|
+
await wait(200); // Long-running job
|
|
1111
|
+
},
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
const agent = createTestAgent("test-agent", {
|
|
1115
|
+
hourly: { type: "interval", interval: "1s" },
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
const startPromise = scheduler.start([agent]);
|
|
1119
|
+
await wait(50);
|
|
1120
|
+
|
|
1121
|
+
// First trigger should start
|
|
1122
|
+
const countAtStop = triggerCount;
|
|
1123
|
+
expect(countAtStop).toBe(1);
|
|
1124
|
+
|
|
1125
|
+
// Stop scheduler (don't wait for jobs to test that no new triggers happen)
|
|
1126
|
+
await scheduler.stop({ waitForJobs: false });
|
|
1127
|
+
await startPromise;
|
|
1128
|
+
|
|
1129
|
+
// Wait a bit to see if any new triggers would have happened
|
|
1130
|
+
await wait(100);
|
|
1131
|
+
|
|
1132
|
+
// No additional triggers should have started after stop
|
|
1133
|
+
expect(triggerCount).toBe(countAtStop);
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
it("handles multiple concurrent running jobs during shutdown", async () => {
|
|
1137
|
+
let runningCount = 0;
|
|
1138
|
+
let maxRunningDuringShutdown = 0;
|
|
1139
|
+
let resolveAll: () => void;
|
|
1140
|
+
const allJobsCanComplete = new Promise<void>((resolve) => {
|
|
1141
|
+
resolveAll = resolve;
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
const scheduler = new Scheduler({
|
|
1145
|
+
stateDir: tempDir,
|
|
1146
|
+
checkInterval: 20,
|
|
1147
|
+
logger: mockLogger,
|
|
1148
|
+
onTrigger: async () => {
|
|
1149
|
+
runningCount++;
|
|
1150
|
+
maxRunningDuringShutdown = Math.max(maxRunningDuringShutdown, runningCount);
|
|
1151
|
+
await allJobsCanComplete;
|
|
1152
|
+
runningCount--;
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// Agent with multiple concurrent schedules
|
|
1157
|
+
const agent = {
|
|
1158
|
+
...createTestAgent("test-agent", {
|
|
1159
|
+
schedule1: { type: "interval", interval: "1s" },
|
|
1160
|
+
schedule2: { type: "interval", interval: "1s" },
|
|
1161
|
+
schedule3: { type: "interval", interval: "1s" },
|
|
1162
|
+
}),
|
|
1163
|
+
instances: { max_concurrent: 3 },
|
|
1164
|
+
} as ResolvedAgent;
|
|
1165
|
+
|
|
1166
|
+
const startPromise = scheduler.start([agent]);
|
|
1167
|
+
await wait(100);
|
|
1168
|
+
|
|
1169
|
+
// Multiple jobs should be running
|
|
1170
|
+
expect(runningCount).toBeGreaterThanOrEqual(1);
|
|
1171
|
+
|
|
1172
|
+
// Start shutdown
|
|
1173
|
+
const stopPromise = scheduler.stop();
|
|
1174
|
+
|
|
1175
|
+
await wait(10);
|
|
1176
|
+
expect(scheduler.getStatus()).toBe("stopping");
|
|
1177
|
+
|
|
1178
|
+
// Release all jobs
|
|
1179
|
+
resolveAll!();
|
|
1180
|
+
|
|
1181
|
+
await stopPromise;
|
|
1182
|
+
await startPromise;
|
|
1183
|
+
|
|
1184
|
+
expect(scheduler.getStatus()).toBe("stopped");
|
|
1185
|
+
expect(runningCount).toBe(0);
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it("returns immediately when there are no running jobs", async () => {
|
|
1189
|
+
const scheduler = new Scheduler({
|
|
1190
|
+
stateDir: tempDir,
|
|
1191
|
+
checkInterval: 100,
|
|
1192
|
+
logger: mockLogger,
|
|
1193
|
+
});
|
|
1194
|
+
|
|
1195
|
+
// Start with no agents (no jobs will be triggered)
|
|
1196
|
+
const startPromise = scheduler.start([]);
|
|
1197
|
+
await wait(50);
|
|
1198
|
+
|
|
1199
|
+
const stopStart = Date.now();
|
|
1200
|
+
await scheduler.stop();
|
|
1201
|
+
const stopDuration = Date.now() - stopStart;
|
|
1202
|
+
|
|
1203
|
+
// Should complete quickly (not wait for timeout)
|
|
1204
|
+
expect(stopDuration).toBeLessThan(100);
|
|
1205
|
+
|
|
1206
|
+
await startPromise;
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it("updates fleet state on shutdown", async () => {
|
|
1210
|
+
const scheduler = new Scheduler({
|
|
1211
|
+
stateDir: tempDir,
|
|
1212
|
+
checkInterval: 50,
|
|
1213
|
+
logger: mockLogger,
|
|
1214
|
+
onTrigger: async () => {
|
|
1215
|
+
await wait(10);
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
|
|
1219
|
+
const agent = createTestAgent("test-agent", {
|
|
1220
|
+
hourly: { type: "interval", interval: "1s" },
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const startPromise = scheduler.start([agent]);
|
|
1224
|
+
await wait(150);
|
|
1225
|
+
|
|
1226
|
+
// Trigger should have completed and updated state
|
|
1227
|
+
const stateFile = join(tempDir, "state.yaml");
|
|
1228
|
+
let fleetState = await readFleetState(stateFile);
|
|
1229
|
+
const scheduleState = fleetState.agents["test-agent"]?.schedules?.hourly;
|
|
1230
|
+
|
|
1231
|
+
expect(scheduleState?.status).toBe("idle");
|
|
1232
|
+
expect(scheduleState?.last_run_at).not.toBeNull();
|
|
1233
|
+
|
|
1234
|
+
await scheduler.stop();
|
|
1235
|
+
await startPromise;
|
|
1236
|
+
|
|
1237
|
+
// State should still be valid after shutdown
|
|
1238
|
+
fleetState = await readFleetState(stateFile);
|
|
1239
|
+
expect(fleetState.agents["test-agent"]?.schedules?.hourly?.status).toBe("idle");
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it("getTotalRunningJobCount returns correct count", async () => {
|
|
1243
|
+
let resolveJob: () => void;
|
|
1244
|
+
const jobPromise = new Promise<void>((resolve) => {
|
|
1245
|
+
resolveJob = resolve;
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
const scheduler = new Scheduler({
|
|
1249
|
+
stateDir: tempDir,
|
|
1250
|
+
checkInterval: 30,
|
|
1251
|
+
logger: mockLogger,
|
|
1252
|
+
onTrigger: async () => {
|
|
1253
|
+
await jobPromise;
|
|
1254
|
+
},
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// Initially should be 0
|
|
1258
|
+
expect(scheduler.getTotalRunningJobCount()).toBe(0);
|
|
1259
|
+
|
|
1260
|
+
const agent = createTestAgent("test-agent", {
|
|
1261
|
+
hourly: { type: "interval", interval: "1s" },
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
const startPromise = scheduler.start([agent]);
|
|
1265
|
+
await wait(100);
|
|
1266
|
+
|
|
1267
|
+
// Should have 1 running job
|
|
1268
|
+
expect(scheduler.getTotalRunningJobCount()).toBe(1);
|
|
1269
|
+
|
|
1270
|
+
resolveJob!();
|
|
1271
|
+
await wait(50);
|
|
1272
|
+
|
|
1273
|
+
// Should be back to 0
|
|
1274
|
+
expect(scheduler.getTotalRunningJobCount()).toBe(0);
|
|
1275
|
+
|
|
1276
|
+
await scheduler.stop();
|
|
1277
|
+
await startPromise;
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
});
|