@selesai/code 0.2.1 → 0.3.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/dist/config.d.ts +4 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -45
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +101 -51
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +10 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +93 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manager.test.d.ts +2 -0
- package/dist/core/package-manager.test.d.ts.map +1 -0
- package/dist/core/package-manager.test.js +20 -0
- package/dist/core/package-manager.test.js.map +1 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +10 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/defaults/models.json +64 -113
- package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
- package/dist/extensions/caveman/index.js +116 -0
- package/dist/extensions/caveman/package.json +8 -0
- package/dist/extensions/caveman/test/extension.test.js +185 -0
- package/dist/extensions/caveman/test/helpers.test.js +67 -0
- package/dist/extensions/copy-turn.ts +2 -2
- package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
- package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
- package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
- package/dist/extensions/pi-intercom/config.ts +2 -2
- package/dist/extensions/pi-intercom/index.ts +2 -2
- package/dist/extensions/pi-intercom/package.json +2 -2
- package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
- package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
- package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +40 -47
- package/dist/extensions/pi-powerline-footer/package.json +2 -2
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
- package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
- package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
- package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
- package/dist/extensions/pi-powerline-footer/types.ts +7 -7
- package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
- package/dist/extensions/pi-rewind-hook/index.ts +1 -1
- package/dist/extensions/pi-rewind-hook/package.json +1 -1
- package/dist/extensions/pi-subagents/README.md +224 -52
- package/dist/extensions/pi-subagents/agents/architect.md +188 -0
- package/dist/extensions/pi-subagents/agents/builder.md +120 -0
- package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
- package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
- package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
- package/dist/extensions/pi-subagents/install.mjs +93 -0
- package/dist/extensions/pi-subagents/package.json +4 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
- package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
- package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
- package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
- package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
- package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
- package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
- package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
- package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
- package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
- package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
- package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
- package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
- package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
- package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
- package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
- package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
- package/dist/extensions/pi-web-agent/package.json +2 -2
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
- package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
- package/dist/extensions/question/helpers.ts +3 -3
- package/dist/extensions/question/index.ts +2 -2
- package/dist/extensions/question/navigation.ts +2 -2
- package/dist/extensions/question/package.json +2 -2
- package/dist/extensions/question/question-list.ts +2 -2
- package/dist/extensions/question/tests/question-list.test.ts +2 -2
- package/dist/extensions/question/tui-adapter.ts +2 -2
- package/dist/extensions/question/types.ts +2 -2
- package/dist/extensions/question/ui-protocol.ts +2 -2
- package/dist/extensions/rtk.ts +2 -2
- package/dist/extensions/tokenin-onboarding.ts +2 -2
- package/dist/extensions/undo.ts +1 -1
- package/dist/extensions/web-agent-onboarding.ts +3 -3
- package/dist/extensions/workflow/adapter.ts +16 -8
- package/dist/extensions/workflow/extension.ts +1 -1
- package/dist/extensions/workflow/package.json +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +19 -5
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/skills/caveman/SKILL.md +49 -0
- package/dist/skills/handoff-text/SKILL.md +13 -0
- package/dist/utils/thinking-tags.d.ts +7 -0
- package/dist/utils/thinking-tags.d.ts.map +1 -0
- package/dist/utils/thinking-tags.js +63 -0
- package/dist/utils/thinking-tags.js.map +1 -0
- package/docs/docs.json +4 -0
- package/docs/extensions.md +2 -0
- package/docs/settings.md +15 -0
- package/docs/shared-host-extensions.md +109 -0
- package/package.json +2 -2
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { after, before, describe, it } from "node:test";
|
|
7
|
+
import {
|
|
8
|
+
SCHEDULED_RUNS_DIR,
|
|
9
|
+
ScheduledRunManager,
|
|
10
|
+
createScheduledRunManager,
|
|
11
|
+
isScheduledRunAction,
|
|
12
|
+
parseScheduledRunTime,
|
|
13
|
+
scheduledRunStorePath,
|
|
14
|
+
scheduledRunsEnabled,
|
|
15
|
+
} from "../../src/runs/background/scheduled-runs.ts";
|
|
16
|
+
import type { ExtensionConfig } from "../../src/shared/types.ts";
|
|
17
|
+
import type { ExtensionContext } from "@selesai/code";
|
|
18
|
+
|
|
19
|
+
type TimerHandle = number;
|
|
20
|
+
|
|
21
|
+
class FakeTimers {
|
|
22
|
+
readonly scheduled = new Map<TimerHandle, { cb: () => void; delay: number }>();
|
|
23
|
+
private nextHandle = 1;
|
|
24
|
+
|
|
25
|
+
setTimeout = (cb: () => void, delay: number): TimerHandle => {
|
|
26
|
+
const handle = this.nextHandle++;
|
|
27
|
+
this.scheduled.set(handle, { cb, delay });
|
|
28
|
+
return handle;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
clearTimeout = (handle: TimerHandle): void => {
|
|
32
|
+
this.scheduled.delete(handle);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
fireAll(): void {
|
|
36
|
+
// Snapshot first: fire() re-arms by clearing the fired handle and adding a new one.
|
|
37
|
+
const entries = [...this.scheduled.values()];
|
|
38
|
+
for (const entry of entries) entry.cb();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pendingCount(): number {
|
|
42
|
+
return this.scheduled.size;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type LaunchRecord = {
|
|
47
|
+
params: Record<string, unknown>;
|
|
48
|
+
ctx: ExtensionContext;
|
|
49
|
+
signal: AbortSignal;
|
|
50
|
+
resolve: (result: { content: Array<{ type: "text"; text: string }>; details: Record<string, unknown>; isError?: boolean }) => void;
|
|
51
|
+
promise: Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown>; isError?: boolean }>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type TestHarness = {
|
|
55
|
+
manager: ScheduledRunManager;
|
|
56
|
+
timers: FakeTimers;
|
|
57
|
+
launches: LaunchRecord[];
|
|
58
|
+
clock: { now: number };
|
|
59
|
+
storeRoot: string;
|
|
60
|
+
ctx: ExtensionContext;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function makeCtx(cwd = "/project", sessionFile = "/sessions/abc.jsonl"): ExtensionContext {
|
|
64
|
+
return {
|
|
65
|
+
cwd,
|
|
66
|
+
sessionManager: {
|
|
67
|
+
getSessionFile: () => sessionFile,
|
|
68
|
+
getSessionId: () => path.basename(sessionFile, ".jsonl"),
|
|
69
|
+
},
|
|
70
|
+
} as unknown as ExtensionContext;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function makeConfig(overrides: Partial<ExtensionConfig["scheduledRuns"]> = {}): ExtensionConfig {
|
|
74
|
+
return { scheduledRuns: { enabled: true, ...overrides } };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function flushMicrotasks(times = 10): Promise<void> {
|
|
78
|
+
for (let i = 0; i < times; i++) await Promise.resolve();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function createHarness(options: { config?: ExtensionConfig; storeRoot?: string; now?: number } = {}): TestHarness {
|
|
82
|
+
const timers = new FakeTimers();
|
|
83
|
+
const launches: LaunchRecord[] = [];
|
|
84
|
+
const clock = { now: options.now ?? 1_000_000 };
|
|
85
|
+
const storeRoot = options.storeRoot ?? fs.mkdtempSync(path.join(os.tmpdir(), "sched-test-"));
|
|
86
|
+
const manager = createScheduledRunManager({
|
|
87
|
+
config: options.config ?? makeConfig(),
|
|
88
|
+
storeRoot,
|
|
89
|
+
now: () => clock.now,
|
|
90
|
+
randomId: () => "id-" + randomUUID().slice(0, 4),
|
|
91
|
+
timers,
|
|
92
|
+
launch: (params, ctx, signal) => {
|
|
93
|
+
let resolve!: LaunchRecord["resolve"];
|
|
94
|
+
const promise = new Promise<{ content: Array<{ type: "text"; text: string }>; details: Record<string, unknown>; isError?: boolean }>((res) => {
|
|
95
|
+
resolve = res;
|
|
96
|
+
});
|
|
97
|
+
launches.push({ params: params as unknown as Record<string, unknown>, ctx, signal, resolve, promise });
|
|
98
|
+
return promise as ReturnType<TestHarness["manager"]["handleToolCall"]>;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
const ctx = makeCtx();
|
|
102
|
+
return { manager, timers, launches, clock, storeRoot, ctx };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractId(result: { content: Array<{ type: "text"; text: string }> }): string {
|
|
106
|
+
const text = result.content[0]!.text;
|
|
107
|
+
const match = text.match(/Scheduled subagent run (id-[a-z0-9]+)/);
|
|
108
|
+
assert.ok(match, `expected scheduled run id in: ${text}`);
|
|
109
|
+
return match[1]!;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isError(result: { isError?: boolean }): boolean {
|
|
113
|
+
return result.isError === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
describe("scheduled-runs helpers", () => {
|
|
117
|
+
it("isScheduledRunAction narrows the four actions", () => {
|
|
118
|
+
assert.equal(isScheduledRunAction("schedule"), true);
|
|
119
|
+
assert.equal(isScheduledRunAction("schedule-cancel"), true);
|
|
120
|
+
assert.equal(isScheduledRunAction("status"), false);
|
|
121
|
+
assert.equal(isScheduledRunAction(undefined), false);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("scheduledRunsEnabled is true only when explicitly enabled", () => {
|
|
125
|
+
assert.equal(scheduledRunsEnabled({}), false);
|
|
126
|
+
assert.equal(scheduledRunsEnabled({ scheduledRuns: {} }), false);
|
|
127
|
+
assert.equal(scheduledRunsEnabled({ scheduledRuns: { enabled: false } }), false);
|
|
128
|
+
assert.equal(scheduledRunsEnabled({ scheduledRuns: { enabled: true } }), true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("scheduledRunStorePath is stable per cwd+session and distinct otherwise", () => {
|
|
132
|
+
const root = path.join("tmp", "sched-root");
|
|
133
|
+
const project = path.join("project");
|
|
134
|
+
const other = path.join("other");
|
|
135
|
+
const a = scheduledRunStorePath(project, "sess1", root);
|
|
136
|
+
const aAgain = scheduledRunStorePath(project, "sess1", root);
|
|
137
|
+
const b = scheduledRunStorePath(project, "sess2", root);
|
|
138
|
+
const c = scheduledRunStorePath(other, "sess1", root);
|
|
139
|
+
assert.equal(a, aAgain);
|
|
140
|
+
assert.notEqual(a, b);
|
|
141
|
+
assert.notEqual(a, c);
|
|
142
|
+
assert.ok(a.startsWith(root));
|
|
143
|
+
assert.ok(a.endsWith(".json"));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("parseScheduledRunTime handles relative delays and ISO timestamps", () => {
|
|
147
|
+
const base = 1_000_000;
|
|
148
|
+
assert.equal(parseScheduledRunTime("+10m", base), base + 10 * 60_000);
|
|
149
|
+
assert.equal(parseScheduledRunTime("+1h", base), base + 3_600_000);
|
|
150
|
+
assert.equal(parseScheduledRunTime("+2d", base), base + 2 * 86_400_000);
|
|
151
|
+
assert.equal(parseScheduledRunTime("+30s", base), base + 30_000);
|
|
152
|
+
assert.equal(parseScheduledRunTime("2030-01-01T00:00:00Z", base), new Date("2030-01-01T00:00:00Z").getTime());
|
|
153
|
+
assert.equal(parseScheduledRunTime("2030-01-01T09:00:00+05:30", base), new Date("2030-01-01T09:00:00+05:30").getTime());
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("parseScheduledRunTime rejects past, zero, malformed, and ambiguous schedules", () => {
|
|
157
|
+
assert.throws(() => parseScheduledRunTime("+0m", 1_000_000), /positive/);
|
|
158
|
+
assert.throws(() => parseScheduledRunTime("+9000000000000d", 1_000_000), /too large/);
|
|
159
|
+
assert.throws(() => parseScheduledRunTime("2020-01-01T00:00:00Z", Date.now() + 60_000), /in the past/);
|
|
160
|
+
assert.throws(() => parseScheduledRunTime("2030-01-01T00:00:00", 1_000_000), /must include a timezone/);
|
|
161
|
+
assert.throws(() => parseScheduledRunTime("2030-02-30T00:00:00Z", 1_000_000), /valid future ISO/);
|
|
162
|
+
assert.throws(() => parseScheduledRunTime("next tuesday", 1_000_000), /Invalid schedule/);
|
|
163
|
+
assert.throws(() => parseScheduledRunTime("+5w", 1_000_000), /Invalid schedule/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("ScheduledRunManager create/list/status/cancel", () => {
|
|
168
|
+
let storeRoots: string[] = [];
|
|
169
|
+
after(() => {
|
|
170
|
+
for (const root of storeRoots) {
|
|
171
|
+
try {
|
|
172
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
173
|
+
} catch {
|
|
174
|
+
// best-effort cleanup of temp test dirs
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
function freshHarness(options: { config?: ExtensionConfig; now?: number } = {}): TestHarness {
|
|
180
|
+
const harness = createHarness(options);
|
|
181
|
+
storeRoots.push(harness.storeRoot);
|
|
182
|
+
return harness;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
it("rejects schedule actions when the feature is disabled", async () => {
|
|
186
|
+
const harness = freshHarness({ config: makeConfig({ enabled: false }) });
|
|
187
|
+
const result = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m" }, harness.ctx);
|
|
188
|
+
assert.equal(isError(result), true);
|
|
189
|
+
assert.match(result.content[0]!.text, /disabled/);
|
|
190
|
+
assert.equal(harness.timers.pendingCount(), 0);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("creates a scheduled job and arms a timer", async () => {
|
|
194
|
+
const harness = freshHarness();
|
|
195
|
+
const result = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "review later", schedule: "+10m", scheduleName: "nightly review" }, harness.ctx);
|
|
196
|
+
assert.equal(isError(result), false);
|
|
197
|
+
assert.match(result.content[0]!.text, /Scheduled subagent run id-/);
|
|
198
|
+
assert.match(result.content[0]!.text, /nightly review/);
|
|
199
|
+
assert.equal(harness.timers.pendingCount(), 1);
|
|
200
|
+
const list = await harness.manager.handleToolCall({ action: "schedule-list" }, harness.ctx);
|
|
201
|
+
assert.match(list.content[0]!.text, /nightly review/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("requires exactly one execution mode", async () => {
|
|
205
|
+
const harness = freshHarness();
|
|
206
|
+
// tasks + chain is genuinely ambiguous (both are execution arrays)
|
|
207
|
+
const chainAndTasks = await harness.manager.handleToolCall({ action: "schedule", tasks: [{ agent: "scout", task: "x" }], chain: [{ agent: "scout", task: "y" }], schedule: "+10m" }, harness.ctx);
|
|
208
|
+
assert.match(chainAndTasks.content[0]!.text, /exactly one execution mode/);
|
|
209
|
+
// no execution mode at all
|
|
210
|
+
const none = await harness.manager.handleToolCall({ action: "schedule", schedule: "+10m" }, harness.ctx);
|
|
211
|
+
assert.match(none.content[0]!.text, /exactly one execution mode/);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("requires a schedule and rejects fork/async-false/clarify-true", async () => {
|
|
215
|
+
const harness = freshHarness();
|
|
216
|
+
const noSchedule = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x" }, harness.ctx);
|
|
217
|
+
assert.match(noSchedule.content[0]!.text, /requires schedule/);
|
|
218
|
+
const fork = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m", context: "fork" }, harness.ctx);
|
|
219
|
+
assert.match(fork.content[0]!.text, /fresh context/);
|
|
220
|
+
const sync = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m", async: false }, harness.ctx);
|
|
221
|
+
assert.match(sync.content[0]!.text, /always async/);
|
|
222
|
+
const clarify = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m", clarify: true }, harness.ctx);
|
|
223
|
+
assert.match(clarify.content[0]!.text, /clarify/);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("rejects a past schedule time", async () => {
|
|
227
|
+
const harness = freshHarness({ now: Date.now() + 60_000 });
|
|
228
|
+
const result = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "2020-01-01T00:00:00Z" }, harness.ctx);
|
|
229
|
+
assert.match(result.content[0]!.text, /in the past/);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("enforces maxPending", async () => {
|
|
233
|
+
const harness = freshHarness({ config: makeConfig({ maxPending: 1 }) });
|
|
234
|
+
const first = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m" }, harness.ctx);
|
|
235
|
+
assert.equal(isError(first), false);
|
|
236
|
+
const second = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "y", schedule: "+20m" }, harness.ctx);
|
|
237
|
+
assert.match(second.content[0]!.text, /limit reached/);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it("status resolves by exact id and prefix, and rejects ambiguous/missing", async () => {
|
|
241
|
+
const harness = freshHarness();
|
|
242
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m" }, harness.ctx);
|
|
243
|
+
const id = extractId(created);
|
|
244
|
+
const status = await harness.manager.handleToolCall({ action: "schedule-status", id }, harness.ctx);
|
|
245
|
+
assert.match(status.content[0]!.text, /State: scheduled/);
|
|
246
|
+
assert.match(status.content[0]!.text, /Cancel: subagent/);
|
|
247
|
+
const byPrefix = await harness.manager.handleToolCall({ action: "schedule-status", id: id.slice(0, 4) }, harness.ctx);
|
|
248
|
+
assert.match(byPrefix.content[0]!.text, /State: scheduled/);
|
|
249
|
+
const missing = await harness.manager.handleToolCall({ action: "schedule-status", id: "nope" }, harness.ctx);
|
|
250
|
+
assert.match(missing.content[0]!.text, /not found/);
|
|
251
|
+
const noId = await harness.manager.handleToolCall({ action: "schedule-status" }, harness.ctx);
|
|
252
|
+
assert.match(noId.content[0]!.text, /requires id/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("cancel clears the timer and marks the job canceled", async () => {
|
|
256
|
+
const harness = freshHarness();
|
|
257
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m" }, harness.ctx);
|
|
258
|
+
const id = extractId(created);
|
|
259
|
+
assert.equal(harness.timers.pendingCount(), 1);
|
|
260
|
+
const canceled = await harness.manager.handleToolCall({ action: "schedule-cancel", id }, harness.ctx);
|
|
261
|
+
assert.match(canceled.content[0]!.text, /Canceled scheduled subagent run/);
|
|
262
|
+
assert.equal(harness.timers.pendingCount(), 0);
|
|
263
|
+
const status = await harness.manager.handleToolCall({ action: "schedule-status", id }, harness.ctx);
|
|
264
|
+
assert.match(status.content[0]!.text, /State: canceled/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("cancel refuses a terminal job", async () => {
|
|
268
|
+
const harness = freshHarness();
|
|
269
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m" }, harness.ctx);
|
|
270
|
+
const id = extractId(created);
|
|
271
|
+
await harness.manager.handleToolCall({ action: "schedule-cancel", id }, harness.ctx);
|
|
272
|
+
const again = await harness.manager.handleToolCall({ action: "schedule-cancel", id }, harness.ctx);
|
|
273
|
+
assert.match(again.content[0]!.text, /already canceled/);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("reports malformed persisted job records instead of dropping them", async () => {
|
|
277
|
+
const harness = freshHarness();
|
|
278
|
+
const sessionId = harness.ctx.sessionManager.getSessionFile()!;
|
|
279
|
+
const storePath = scheduledRunStorePath(harness.ctx.cwd, sessionId, harness.storeRoot);
|
|
280
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
281
|
+
fs.writeFileSync(storePath, JSON.stringify({ version: 1, cwd: harness.ctx.cwd, sessionId, jobs: [{ id: "bad" }] }), "utf-8");
|
|
282
|
+
const result = await harness.manager.handleToolCall({ action: "schedule-list" }, harness.ctx);
|
|
283
|
+
assert.equal(isError(result), true);
|
|
284
|
+
assert.match(result.content[0]!.text, /job 0 has invalid string fields/);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("reports JSON parse errors from a corrupted persisted store", async () => {
|
|
288
|
+
const harness = freshHarness();
|
|
289
|
+
const sessionId = harness.ctx.sessionManager.getSessionFile()!;
|
|
290
|
+
const storePath = scheduledRunStorePath(harness.ctx.cwd, sessionId, harness.storeRoot);
|
|
291
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
292
|
+
fs.writeFileSync(storePath, "{ not-json", "utf-8");
|
|
293
|
+
const result = await harness.manager.handleToolCall({ action: "schedule-list" }, harness.ctx);
|
|
294
|
+
assert.equal(isError(result), true);
|
|
295
|
+
assert.match(result.content[0]!.text, /Failed to parse scheduled subagent store/);
|
|
296
|
+
assert.match(result.content[0]!.text, /JSON/);
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe("ScheduledRunManager firing", () => {
|
|
301
|
+
let storeRoots: string[] = [];
|
|
302
|
+
after(() => {
|
|
303
|
+
for (const root of storeRoots) {
|
|
304
|
+
try {
|
|
305
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
306
|
+
} catch {
|
|
307
|
+
// best-effort cleanup of temp test dirs
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
function freshHarness(options: { config?: ExtensionConfig; now?: number } = {}): TestHarness {
|
|
313
|
+
const harness = createHarness(options);
|
|
314
|
+
storeRoots.push(harness.storeRoot);
|
|
315
|
+
return harness;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
it("launches the sanitized async run when the timer fires", async () => {
|
|
319
|
+
const harness = freshHarness();
|
|
320
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "review", schedule: "+10m", scheduleName: "rev" }, harness.ctx);
|
|
321
|
+
const id = extractId(created);
|
|
322
|
+
harness.clock.now += 10 * 60_000; // advance to runAt so fire() launches instead of re-arming
|
|
323
|
+
harness.timers.fireAll();
|
|
324
|
+
assert.equal(harness.launches.length, 1, "launch should be invoked once when the timer fires");
|
|
325
|
+
const launch = harness.launches[0]!;
|
|
326
|
+
assert.equal(launch.params.async, true);
|
|
327
|
+
assert.equal(launch.params.clarify, false);
|
|
328
|
+
assert.equal(launch.params.context, "fresh");
|
|
329
|
+
assert.equal(launch.params.agent, "scout");
|
|
330
|
+
assert.equal(launch.params.task, "review");
|
|
331
|
+
assert.equal((launch.params as { action?: string }).action, undefined, "action must be stripped before launch");
|
|
332
|
+
assert.equal((launch.params as { schedule?: string }).schedule, undefined, "schedule must be stripped before launch");
|
|
333
|
+
|
|
334
|
+
launch.resolve({
|
|
335
|
+
content: [{ type: "text", text: "Async: scout [run-xyz]" }],
|
|
336
|
+
details: { mode: "single", runId: "run-xyz", asyncId: "run-xyz", asyncDir: "/tmp/async-xyz", results: [] },
|
|
337
|
+
});
|
|
338
|
+
await flushMicrotasks();
|
|
339
|
+
|
|
340
|
+
const status = await harness.manager.handleToolCall({ action: "schedule-status", id }, harness.ctx);
|
|
341
|
+
assert.match(status.content[0]!.text, /State: fired/);
|
|
342
|
+
assert.match(status.content[0]!.text, /Launched async run: run-xyz/);
|
|
343
|
+
assert.match(status.content[0]!.text, /Async dir: \/tmp\/async-xyz/);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("marks the job failed when launch returns an error result", async () => {
|
|
347
|
+
const harness = freshHarness();
|
|
348
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+1m" }, harness.ctx);
|
|
349
|
+
const id = extractId(created);
|
|
350
|
+
harness.clock.now += 60_000; // advance to runAt
|
|
351
|
+
harness.timers.fireAll();
|
|
352
|
+
assert.equal(harness.launches.length, 1);
|
|
353
|
+
harness.launches[0]!.resolve({
|
|
354
|
+
content: [{ type: "text", text: "Async mode requires jiti" }],
|
|
355
|
+
details: { mode: "single", results: [] },
|
|
356
|
+
isError: true,
|
|
357
|
+
});
|
|
358
|
+
await flushMicrotasks();
|
|
359
|
+
const status = await harness.manager.handleToolCall({ action: "schedule-status", id }, harness.ctx);
|
|
360
|
+
assert.match(status.content[0]!.text, /State: failed/);
|
|
361
|
+
assert.match(status.content[0]!.text, /Async mode requires jiti/);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("marks the job failed when launch throws", async () => {
|
|
365
|
+
const storeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sched-throw-"));
|
|
366
|
+
storeRoots.push(storeRoot);
|
|
367
|
+
const ctx = makeCtx();
|
|
368
|
+
const timers = new FakeTimers();
|
|
369
|
+
const clock = { now: 1_000_000 };
|
|
370
|
+
const throwingManager = createScheduledRunManager({
|
|
371
|
+
config: makeConfig(),
|
|
372
|
+
storeRoot,
|
|
373
|
+
now: () => clock.now,
|
|
374
|
+
randomId: () => "id-" + randomUUID().slice(0, 4),
|
|
375
|
+
timers,
|
|
376
|
+
launch: () => Promise.reject(new Error("spawn ENOENT")),
|
|
377
|
+
});
|
|
378
|
+
throwingManager.bindSession(ctx);
|
|
379
|
+
const created = await throwingManager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+1m" }, ctx);
|
|
380
|
+
const id = extractId(created);
|
|
381
|
+
clock.now += 60_000; // advance to runAt
|
|
382
|
+
timers.fireAll();
|
|
383
|
+
await flushMicrotasks();
|
|
384
|
+
const status = await throwingManager.handleToolCall({ action: "schedule-status", id }, ctx);
|
|
385
|
+
assert.match(status.content[0]!.text, /State: failed/);
|
|
386
|
+
assert.match(status.content[0]!.text, /spawn ENOENT/);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("re-arms instead of launching when a capped timer fires before runAt", async () => {
|
|
390
|
+
const harness = freshHarness({ now: 1_000_000 });
|
|
391
|
+
// +25d exceeds MAX_TIMER_DELAY_MS so the armed timer is capped and would fire "early" in fake time.
|
|
392
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+25d" }, harness.ctx);
|
|
393
|
+
const id = extractId(created);
|
|
394
|
+
harness.timers.fireAll();
|
|
395
|
+
assert.equal(harness.launches.length, 0, "must not launch before runAt");
|
|
396
|
+
assert.equal(harness.timers.pendingCount(), 1, "must re-arm a new timer");
|
|
397
|
+
// Advance time to runAt and fire again -> now it launches.
|
|
398
|
+
harness.clock.now = 1_000_000 + 25 * 86_400_000;
|
|
399
|
+
harness.timers.fireAll();
|
|
400
|
+
assert.equal(harness.launches.length, 1, "must launch once runAt is reached");
|
|
401
|
+
harness.launches[0]!.resolve({
|
|
402
|
+
content: [{ type: "text", text: "Async: scout [run-1]" }],
|
|
403
|
+
details: { mode: "single", runId: "run-1", asyncId: "run-1", results: [] },
|
|
404
|
+
});
|
|
405
|
+
await flushMicrotasks();
|
|
406
|
+
const status = await harness.manager.handleToolCall({ action: "schedule-status", id }, harness.ctx);
|
|
407
|
+
assert.match(status.content[0]!.text, /State: fired/);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("cancel before fire prevents the launch", async () => {
|
|
411
|
+
const harness = freshHarness();
|
|
412
|
+
const created = await harness.manager.handleToolCall({ action: "schedule", agent: "scout", task: "x", schedule: "+10m" }, harness.ctx);
|
|
413
|
+
const id = extractId(created);
|
|
414
|
+
await harness.manager.handleToolCall({ action: "schedule-cancel", id }, harness.ctx);
|
|
415
|
+
harness.timers.fireAll();
|
|
416
|
+
assert.equal(harness.launches.length, 0, "a canceled job must not launch");
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("ScheduledRunManager restart restore", () => {
|
|
421
|
+
let storeRoots: string[] = [];
|
|
422
|
+
after(() => {
|
|
423
|
+
for (const root of storeRoots) {
|
|
424
|
+
try {
|
|
425
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
426
|
+
} catch {
|
|
427
|
+
// best-effort cleanup of temp test dirs
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it("re-arms pending jobs and marks stale jobs missed after a restart", async () => {
|
|
433
|
+
const storeRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sched-restore-"));
|
|
434
|
+
storeRoots.push(storeRoot);
|
|
435
|
+
const ctx = makeCtx();
|
|
436
|
+
const clock = { now: 1_000_000 };
|
|
437
|
+
const timersA = new FakeTimers();
|
|
438
|
+
const managerA = createScheduledRunManager({
|
|
439
|
+
config: makeConfig({ maxLatenessMs: 60_000 }),
|
|
440
|
+
storeRoot,
|
|
441
|
+
now: () => clock.now,
|
|
442
|
+
randomId: () => "id-" + randomUUID().slice(0, 4),
|
|
443
|
+
timers: timersA,
|
|
444
|
+
launch: () => Promise.resolve({ content: [{ type: "text", text: "ok" }], details: { mode: "single", results: [] } }),
|
|
445
|
+
});
|
|
446
|
+
managerA.bindSession(ctx);
|
|
447
|
+
const soon = await managerA.handleToolCall({ action: "schedule", agent: "scout", task: "soon", schedule: "+10m" }, ctx);
|
|
448
|
+
const later = await managerA.handleToolCall({ action: "schedule", agent: "scout", task: "later", schedule: "+1h" }, ctx);
|
|
449
|
+
const soonId = extractId(soon);
|
|
450
|
+
const laterId = extractId(later);
|
|
451
|
+
assert.equal(timersA.pendingCount(), 2);
|
|
452
|
+
managerA.stop();
|
|
453
|
+
|
|
454
|
+
// Simulate Pi being offline past the first job's window but before the second fires.
|
|
455
|
+
clock.now = 1_000_000 + 20 * 60_000; // +20m: soon is 10m overdue (> maxLateness 1m), later is still 40m out.
|
|
456
|
+
const timersB = new FakeTimers();
|
|
457
|
+
const managerB = createScheduledRunManager({
|
|
458
|
+
config: makeConfig({ maxLatenessMs: 60_000 }),
|
|
459
|
+
storeRoot,
|
|
460
|
+
now: () => clock.now,
|
|
461
|
+
randomId: () => "id-" + randomUUID().slice(0, 4),
|
|
462
|
+
timers: timersB,
|
|
463
|
+
launch: () => Promise.resolve({ content: [{ type: "text", text: "ok" }], details: { mode: "single", results: [] } }),
|
|
464
|
+
});
|
|
465
|
+
managerB.bindSession(ctx);
|
|
466
|
+
const soonStatus = await managerB.handleToolCall({ action: "schedule-status", id: soonId }, ctx);
|
|
467
|
+
assert.match(soonStatus.content[0]!.text, /State: missed/);
|
|
468
|
+
const laterStatus = await managerB.handleToolCall({ action: "schedule-status", id: laterId }, ctx);
|
|
469
|
+
assert.match(laterStatus.content[0]!.text, /State: scheduled/);
|
|
470
|
+
assert.equal(timersB.pendingCount(), 1, "only the still-future job should be re-armed");
|
|
471
|
+
managerB.stop();
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it("store path lives under the scheduled runs dir by default", () => {
|
|
475
|
+
const p = scheduledRunStorePath("/project", "sess1");
|
|
476
|
+
assert.ok(p.startsWith(SCHEDULED_RUNS_DIR), `${p} should be under ${SCHEDULED_RUNS_DIR}`);
|
|
477
|
+
});
|
|
478
|
+
});
|