@mclawnet/scheduler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/executor-oneshot.test.d.ts +2 -0
- package/dist/__tests__/executor-oneshot.test.d.ts.map +1 -0
- package/dist/__tests__/executor-oneshot.test.js +141 -0
- package/dist/__tests__/executor-oneshot.test.js.map +1 -0
- package/dist/__tests__/executor-swarm.test.d.ts +2 -0
- package/dist/__tests__/executor-swarm.test.d.ts.map +1 -0
- package/dist/__tests__/executor-swarm.test.js +142 -0
- package/dist/__tests__/executor-swarm.test.js.map +1 -0
- package/dist/__tests__/json-schedule-repository-prompt-cap.test.d.ts +2 -0
- package/dist/__tests__/json-schedule-repository-prompt-cap.test.d.ts.map +1 -0
- package/dist/__tests__/json-schedule-repository-prompt-cap.test.js +64 -0
- package/dist/__tests__/json-schedule-repository-prompt-cap.test.js.map +1 -0
- package/dist/__tests__/json-schedule-repository.test.d.ts +2 -0
- package/dist/__tests__/json-schedule-repository.test.d.ts.map +1 -0
- package/dist/__tests__/json-schedule-repository.test.js +178 -0
- package/dist/__tests__/json-schedule-repository.test.js.map +1 -0
- package/dist/__tests__/paths.test.d.ts +2 -0
- package/dist/__tests__/paths.test.d.ts.map +1 -0
- package/dist/__tests__/paths.test.js +19 -0
- package/dist/__tests__/paths.test.js.map +1 -0
- package/dist/__tests__/scheduler-maxrun-cap.test.d.ts +2 -0
- package/dist/__tests__/scheduler-maxrun-cap.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-maxrun-cap.test.js +109 -0
- package/dist/__tests__/scheduler-maxrun-cap.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-events-typed.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-events-typed.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-events-typed.test.js +27 -0
- package/dist/__tests__/scheduler-service-events-typed.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-register.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-register.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-register.test.js +54 -0
- package/dist/__tests__/scheduler-service-register.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-tick-23.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-tick-23.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-tick-23.test.js +72 -0
- package/dist/__tests__/scheduler-service-tick-23.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-tick-24.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-tick-24.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-tick-24.test.js +80 -0
- package/dist/__tests__/scheduler-service-tick-24.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-tick-25.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-tick-25.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-tick-25.test.js +111 -0
- package/dist/__tests__/scheduler-service-tick-25.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-tick-26.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-tick-26.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-tick-26.test.js +97 -0
- package/dist/__tests__/scheduler-service-tick-26.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-tick-27.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-tick-27.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-tick-27.test.js +69 -0
- package/dist/__tests__/scheduler-service-tick-27.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-tick-coalesce.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-tick-coalesce.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-tick-coalesce.test.js +119 -0
- package/dist/__tests__/scheduler-service-tick-coalesce.test.js.map +1 -0
- package/dist/__tests__/scheduler-service-trigger-gating.test.d.ts +2 -0
- package/dist/__tests__/scheduler-service-trigger-gating.test.d.ts.map +1 -0
- package/dist/__tests__/scheduler-service-trigger-gating.test.js +55 -0
- package/dist/__tests__/scheduler-service-trigger-gating.test.js.map +1 -0
- package/dist/__tests__/tick-helpers.d.ts +17 -0
- package/dist/__tests__/tick-helpers.d.ts.map +1 -0
- package/dist/__tests__/tick-helpers.js +97 -0
- package/dist/__tests__/tick-helpers.js.map +1 -0
- package/dist/executor.d.ts +20 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +2 -0
- package/dist/executor.js.map +1 -0
- package/dist/executors/oneshot.d.ts +22 -0
- package/dist/executors/oneshot.d.ts.map +1 -0
- package/dist/executors/oneshot.js +55 -0
- package/dist/executors/oneshot.js.map +1 -0
- package/dist/executors/swarm.d.ts +26 -0
- package/dist/executors/swarm.d.ts.map +1 -0
- package/dist/executors/swarm.js +40 -0
- package/dist/executors/swarm.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schedule-repository.d.ts +39 -0
- package/dist/json-schedule-repository.d.ts.map +1 -0
- package/dist/json-schedule-repository.js +374 -0
- package/dist/json-schedule-repository.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +22 -0
- package/dist/paths.js.map +1 -0
- package/dist/scheduler-service.d.ts +71 -0
- package/dist/scheduler-service.d.ts.map +1 -0
- package/dist/scheduler-service.js +395 -0
- package/dist/scheduler-service.js.map +1 -0
- package/dist/types.d.ts +92 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import FakeTimers from "@sinonjs/fake-timers";
|
|
6
|
+
import { SchedulerService } from "../scheduler-service.js";
|
|
7
|
+
import { JsonScheduleRepository } from "../json-schedule-repository.js";
|
|
8
|
+
import { makeDeferredExecutor, makeImmediateExecutor, sample, drainFactory, } from "./tick-helpers.js";
|
|
9
|
+
let home;
|
|
10
|
+
let repo;
|
|
11
|
+
let clock;
|
|
12
|
+
let drain;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
home = mkdtempSync(join(tmpdir(), "sched-tick-"));
|
|
15
|
+
repo = new JsonScheduleRepository({ home });
|
|
16
|
+
clock = FakeTimers.install({
|
|
17
|
+
now: new Date("2026-04-30T00:00:00.000Z"),
|
|
18
|
+
shouldAdvanceTime: true,
|
|
19
|
+
advanceTimeDelta: 20,
|
|
20
|
+
});
|
|
21
|
+
drain = drainFactory(clock);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
clock.uninstall();
|
|
25
|
+
});
|
|
26
|
+
describe("SchedulerService — triggerNow and cancel APIs (Task 2.6)", () => {
|
|
27
|
+
it("triggerNow runs immediately and returns the persisted run", async () => {
|
|
28
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-tick-26a", cronExpression: "0 9 * * *" }));
|
|
29
|
+
const exec = makeImmediateExecutor({ exitCode: 0, summary: "manual" });
|
|
30
|
+
const svc = new SchedulerService(repo, {
|
|
31
|
+
oneshot: exec,
|
|
32
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
33
|
+
});
|
|
34
|
+
await svc.start();
|
|
35
|
+
const run = await svc.triggerNow(s.id);
|
|
36
|
+
expect(run.status).toBe("completed");
|
|
37
|
+
expect(run.summary).toBe("manual");
|
|
38
|
+
expect(exec.invocations).toHaveLength(1);
|
|
39
|
+
await svc.stop();
|
|
40
|
+
});
|
|
41
|
+
it("triggerNow respects single-flight and returns cancelled record", async () => {
|
|
42
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-tick-26b" }));
|
|
43
|
+
const exec = makeDeferredExecutor();
|
|
44
|
+
const svc = new SchedulerService(repo, {
|
|
45
|
+
oneshot: exec,
|
|
46
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
47
|
+
});
|
|
48
|
+
await svc.start();
|
|
49
|
+
await clock.tickAsync(60_000);
|
|
50
|
+
await drain();
|
|
51
|
+
const second = await svc.triggerNow(s.id);
|
|
52
|
+
expect(second.status).toBe("cancelled");
|
|
53
|
+
expect(second.error).toBe("previous_run_in_progress");
|
|
54
|
+
exec.resolveNext({ exitCode: 0 });
|
|
55
|
+
await drain();
|
|
56
|
+
await svc.stop();
|
|
57
|
+
});
|
|
58
|
+
it("cancel(runId) aborts an in-flight executor and marks the run cancelled", async () => {
|
|
59
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-tick-26c" }));
|
|
60
|
+
let receivedSignal;
|
|
61
|
+
const exec = {
|
|
62
|
+
mode: "oneshot",
|
|
63
|
+
async execute(ctx) {
|
|
64
|
+
receivedSignal = ctx.signal;
|
|
65
|
+
await new Promise((resolve) => {
|
|
66
|
+
ctx.signal.addEventListener("abort", () => resolve(), { once: true });
|
|
67
|
+
});
|
|
68
|
+
return { exitCode: 0, error: "cancelled" };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const svc = new SchedulerService(repo, {
|
|
72
|
+
oneshot: exec,
|
|
73
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
74
|
+
});
|
|
75
|
+
await svc.start();
|
|
76
|
+
await clock.tickAsync(60_000);
|
|
77
|
+
await drain();
|
|
78
|
+
const inflight = await repo.listRuns(s.id);
|
|
79
|
+
expect(inflight).toHaveLength(1);
|
|
80
|
+
expect(inflight[0].status).toBe("running");
|
|
81
|
+
const ok = svc.cancel(inflight[0].id);
|
|
82
|
+
expect(ok).toBe(true);
|
|
83
|
+
expect(receivedSignal?.aborted).toBe(true);
|
|
84
|
+
await drain();
|
|
85
|
+
const after = await repo.listRuns(s.id);
|
|
86
|
+
expect(after[0].status).toBe("cancelled");
|
|
87
|
+
await svc.stop();
|
|
88
|
+
});
|
|
89
|
+
it("cancel returns false for unknown run ids", async () => {
|
|
90
|
+
const svc = new SchedulerService(repo, {
|
|
91
|
+
oneshot: makeImmediateExecutor({ exitCode: 0 }),
|
|
92
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
93
|
+
});
|
|
94
|
+
expect(svc.cancel("does-not-exist")).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
//# sourceMappingURL=scheduler-service-tick-26.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-tick-26.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-26.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-tick-27.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-27.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import FakeTimers from "@sinonjs/fake-timers";
|
|
6
|
+
import { Cron } from "croner";
|
|
7
|
+
import { SchedulerService } from "../scheduler-service.js";
|
|
8
|
+
import { JsonScheduleRepository } from "../json-schedule-repository.js";
|
|
9
|
+
import { makeDeferredExecutor, makeImmediateExecutor, sample, drainFactory, } from "./tick-helpers.js";
|
|
10
|
+
let home;
|
|
11
|
+
let repo;
|
|
12
|
+
let clock;
|
|
13
|
+
let drain;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
home = mkdtempSync(join(tmpdir(), "sched-tick-"));
|
|
16
|
+
repo = new JsonScheduleRepository({ home });
|
|
17
|
+
clock = FakeTimers.install({
|
|
18
|
+
now: new Date("2026-04-30T00:00:00.000Z"),
|
|
19
|
+
shouldAdvanceTime: true,
|
|
20
|
+
advanceTimeDelta: 20,
|
|
21
|
+
});
|
|
22
|
+
drain = drainFactory(clock);
|
|
23
|
+
});
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
clock.uninstall();
|
|
26
|
+
});
|
|
27
|
+
describe("SchedulerService.tick — lastRunAt + nextRunAt persistence (Task 2.7)", () => {
|
|
28
|
+
it("persists lastRunAt + nextRunAt after each successful tick", async () => {
|
|
29
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-tick-27a", cronExpression: "*/5 * * * *", timezone: "UTC" }));
|
|
30
|
+
const exec = makeImmediateExecutor({ exitCode: 0 });
|
|
31
|
+
const svc = new SchedulerService(repo, {
|
|
32
|
+
oneshot: exec,
|
|
33
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
34
|
+
});
|
|
35
|
+
await svc.start();
|
|
36
|
+
await clock.tickAsync(5 * 60_000);
|
|
37
|
+
await drain();
|
|
38
|
+
const reloaded = await repo.getById(s.id);
|
|
39
|
+
expect(reloaded?.lastRunAt).toBeTruthy();
|
|
40
|
+
expect(reloaded?.nextRunAt).toBeTruthy();
|
|
41
|
+
const expectedNext = new Cron("*/5 * * * *", { timezone: "UTC", paused: true })
|
|
42
|
+
.nextRun(new Date(reloaded.lastRunAt));
|
|
43
|
+
expect(reloaded?.nextRunAt).toBe(expectedNext.toISOString());
|
|
44
|
+
expect(new Date(reloaded.nextRunAt).getTime()).toBeGreaterThan(new Date(reloaded.lastRunAt).getTime());
|
|
45
|
+
await svc.stop();
|
|
46
|
+
});
|
|
47
|
+
it("persists lastRunAt + nextRunAt for cancelled (single-flight) runs too", async () => {
|
|
48
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-tick-27b" }));
|
|
49
|
+
const exec = makeDeferredExecutor();
|
|
50
|
+
const svc = new SchedulerService(repo, {
|
|
51
|
+
oneshot: exec,
|
|
52
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
53
|
+
});
|
|
54
|
+
await svc.start();
|
|
55
|
+
await clock.tickAsync(60_000);
|
|
56
|
+
await drain();
|
|
57
|
+
await clock.tickAsync(60_000);
|
|
58
|
+
await drain();
|
|
59
|
+
const reloaded = await repo.getById(s.id);
|
|
60
|
+
expect(reloaded?.lastRunAt).toBeTruthy();
|
|
61
|
+
expect(reloaded?.nextRunAt).toBeTruthy();
|
|
62
|
+
expect(new Date(reloaded.nextRunAt).getTime())
|
|
63
|
+
.toBeGreaterThan(new Date(reloaded.lastRunAt).getTime());
|
|
64
|
+
exec.resolveNext({ exitCode: 0 });
|
|
65
|
+
await drain();
|
|
66
|
+
await svc.stop();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
//# sourceMappingURL=scheduler-service-tick-27.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-tick-27.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-27.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-tick-coalesce.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-coalesce.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import FakeTimers from "@sinonjs/fake-timers";
|
|
6
|
+
import { SchedulerService } from "../scheduler-service.js";
|
|
7
|
+
import { JsonScheduleRepository } from "../json-schedule-repository.js";
|
|
8
|
+
import { makeImmediateExecutor, sample, drainUntilFactory, } from "./tick-helpers.js";
|
|
9
|
+
let home;
|
|
10
|
+
let repo;
|
|
11
|
+
let clock;
|
|
12
|
+
let drainUntil;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
home = mkdtempSync(join(tmpdir(), "sched-tick-"));
|
|
15
|
+
repo = new JsonScheduleRepository({ home });
|
|
16
|
+
clock = FakeTimers.install({
|
|
17
|
+
now: new Date("2026-04-30T00:00:00.000Z"),
|
|
18
|
+
shouldAdvanceTime: true,
|
|
19
|
+
advanceTimeDelta: 20,
|
|
20
|
+
});
|
|
21
|
+
drainUntil = drainUntilFactory(clock);
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
clock.uninstall();
|
|
25
|
+
});
|
|
26
|
+
describe("SchedulerService.tick — coalesced repo.update + change emit (Phase 2 batch 2 fix)", () => {
|
|
27
|
+
it("emits exactly one 'change' event per failed tick with both counter and timestamps", async () => {
|
|
28
|
+
const s = await repo.create(sample({
|
|
29
|
+
workDir: "/tmp/proj-tick-coal-a",
|
|
30
|
+
maxConsecutiveFailures: 5,
|
|
31
|
+
}));
|
|
32
|
+
const exec = makeImmediateExecutor({ exitCode: 1, error: "boom" });
|
|
33
|
+
const svc = new SchedulerService(repo, {
|
|
34
|
+
oneshot: exec,
|
|
35
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
36
|
+
});
|
|
37
|
+
const changes = [];
|
|
38
|
+
svc.events.on("change", (sched) => { changes.push(sched); });
|
|
39
|
+
await svc.start();
|
|
40
|
+
await clock.tickAsync(60_000);
|
|
41
|
+
await drainUntil(() => changes.length >= 1);
|
|
42
|
+
expect(changes).toHaveLength(1);
|
|
43
|
+
const evt = changes[0];
|
|
44
|
+
expect(evt.consecutiveFailures).toBe(1);
|
|
45
|
+
expect(evt.lastRunAt).toBeTruthy();
|
|
46
|
+
expect(evt.nextRunAt).toBeTruthy();
|
|
47
|
+
await svc.stop();
|
|
48
|
+
});
|
|
49
|
+
it("clears nextRunAt and emits one 'change' when circuit breaker pauses the schedule", async () => {
|
|
50
|
+
const s = await repo.create(sample({
|
|
51
|
+
workDir: "/tmp/proj-tick-coal-b",
|
|
52
|
+
maxConsecutiveFailures: 5,
|
|
53
|
+
}));
|
|
54
|
+
const exec = makeImmediateExecutor({ exitCode: 1, error: "boom" });
|
|
55
|
+
const svc = new SchedulerService(repo, {
|
|
56
|
+
oneshot: exec,
|
|
57
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
58
|
+
});
|
|
59
|
+
const allChanges = [];
|
|
60
|
+
const pausedChanges = [];
|
|
61
|
+
svc.events.on("change", (sched) => {
|
|
62
|
+
allChanges.push(sched);
|
|
63
|
+
if (sched.status === "paused")
|
|
64
|
+
pausedChanges.push(sched);
|
|
65
|
+
});
|
|
66
|
+
await svc.start();
|
|
67
|
+
for (let i = 0; i < 5; i++) {
|
|
68
|
+
await clock.tickAsync(60_000);
|
|
69
|
+
// Wait for THIS iter's change to land before advancing to next cron
|
|
70
|
+
// boundary. Using predicate drainUntil avoids over-shooting timer
|
|
71
|
+
// budgets that might cross the next 60s boundary on slow runners.
|
|
72
|
+
await drainUntil(() => allChanges.length >= i + 1);
|
|
73
|
+
}
|
|
74
|
+
const reloaded = await repo.getById(s.id);
|
|
75
|
+
expect(reloaded?.status).toBe("paused");
|
|
76
|
+
expect(reloaded?.consecutiveFailures).toBe(5);
|
|
77
|
+
expect(reloaded?.lastRunAt).toBeTruthy();
|
|
78
|
+
expect(reloaded?.nextRunAt).toBeUndefined();
|
|
79
|
+
expect(pausedChanges).toHaveLength(1);
|
|
80
|
+
expect(pausedChanges[0].status).toBe("paused");
|
|
81
|
+
expect(pausedChanges[0].nextRunAt).toBeUndefined();
|
|
82
|
+
await svc.stop();
|
|
83
|
+
});
|
|
84
|
+
it("emits exactly one 'change' for a successful tick that resets the counter", async () => {
|
|
85
|
+
await repo.create(sample({
|
|
86
|
+
workDir: "/tmp/proj-tick-coal-c",
|
|
87
|
+
maxConsecutiveFailures: 5,
|
|
88
|
+
}));
|
|
89
|
+
// First fail once to set counter to 1, then succeed.
|
|
90
|
+
let calls = 0;
|
|
91
|
+
const svc = new SchedulerService(repo, {
|
|
92
|
+
oneshot: {
|
|
93
|
+
mode: "oneshot",
|
|
94
|
+
async execute() {
|
|
95
|
+
calls += 1;
|
|
96
|
+
return calls === 1 ? { exitCode: 1, error: "x" } : { exitCode: 0 };
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
100
|
+
});
|
|
101
|
+
const changes = [];
|
|
102
|
+
svc.events.on("change", (sched) => { changes.push(sched); });
|
|
103
|
+
await svc.start();
|
|
104
|
+
await clock.tickAsync(60_000);
|
|
105
|
+
await drainUntil(() => changes.length >= 1);
|
|
106
|
+
// One change after fail.
|
|
107
|
+
expect(changes).toHaveLength(1);
|
|
108
|
+
expect(changes[0].consecutiveFailures).toBe(1);
|
|
109
|
+
await clock.tickAsync(60_000);
|
|
110
|
+
await drainUntil(() => changes.length >= 2);
|
|
111
|
+
// One additional change after success.
|
|
112
|
+
expect(changes).toHaveLength(2);
|
|
113
|
+
expect(changes[1].consecutiveFailures).toBe(0);
|
|
114
|
+
expect(changes[1].lastRunAt).toBeTruthy();
|
|
115
|
+
expect(changes[1].nextRunAt).toBeTruthy();
|
|
116
|
+
await svc.stop();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
//# sourceMappingURL=scheduler-service-tick-coalesce.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-tick-coalesce.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-tick-coalesce.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-trigger-gating.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-trigger-gating.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import FakeTimers from "@sinonjs/fake-timers";
|
|
6
|
+
import { SchedulerService } from "../scheduler-service.js";
|
|
7
|
+
import { JsonScheduleRepository } from "../json-schedule-repository.js";
|
|
8
|
+
import { makeImmediateExecutor, sample } from "./tick-helpers.js";
|
|
9
|
+
let home;
|
|
10
|
+
let repo;
|
|
11
|
+
let clock;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
home = mkdtempSync(join(tmpdir(), "sched-tick-"));
|
|
14
|
+
repo = new JsonScheduleRepository({ home });
|
|
15
|
+
clock = FakeTimers.install({
|
|
16
|
+
now: new Date("2026-04-30T00:00:00.000Z"),
|
|
17
|
+
shouldAdvanceTime: true,
|
|
18
|
+
advanceTimeDelta: 20,
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
clock.uninstall();
|
|
23
|
+
});
|
|
24
|
+
describe("SchedulerService.triggerNow — gating (Phase 2 batch 2 fix)", () => {
|
|
25
|
+
it("throws when the scheduler has been stopped", async () => {
|
|
26
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-trig-stop" }));
|
|
27
|
+
const svc = new SchedulerService(repo, {
|
|
28
|
+
oneshot: makeImmediateExecutor({ exitCode: 0 }),
|
|
29
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
30
|
+
});
|
|
31
|
+
await svc.start();
|
|
32
|
+
await svc.stop();
|
|
33
|
+
await expect(svc.triggerNow(s.id)).rejects.toThrow(/scheduler stopped/);
|
|
34
|
+
});
|
|
35
|
+
it("returns a synthetic cancelled run with error 'schedule_paused' when the schedule is paused", async () => {
|
|
36
|
+
const s = await repo.create(sample({ workDir: "/tmp/proj-trig-paused" }));
|
|
37
|
+
// Mark paused directly via the public patch.
|
|
38
|
+
await repo.update(s.id, { status: "paused" });
|
|
39
|
+
const exec = makeImmediateExecutor({ exitCode: 0 });
|
|
40
|
+
const svc = new SchedulerService(repo, {
|
|
41
|
+
oneshot: exec,
|
|
42
|
+
swarm: makeImmediateExecutor({ exitCode: 0 }),
|
|
43
|
+
});
|
|
44
|
+
await svc.start();
|
|
45
|
+
const run = await svc.triggerNow(s.id);
|
|
46
|
+
expect(run.status).toBe("cancelled");
|
|
47
|
+
expect(run.error).toBe("schedule_paused");
|
|
48
|
+
expect(exec.invocations).toHaveLength(0);
|
|
49
|
+
// The cancelled run should be persisted in the runs file.
|
|
50
|
+
const persisted = await repo.listRuns(s.id);
|
|
51
|
+
expect(persisted.some(r => r.status === "cancelled" && r.error === "schedule_paused")).toBe(true);
|
|
52
|
+
await svc.stop();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=scheduler-service-trigger-gating.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scheduler-service-trigger-gating.test.js","sourceRoot":"","sources":["../../src/__tests__/scheduler-service-trigger-gating.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AACrE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,UAAU,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AACxE,OAAO,EAAE,qBAAqB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAElE,IAAI,IAAY,CAAC;AACjB,IAAI,IAA4B,CAAC;AACjC,IAAI,KAAgC,CAAC;AAErC,UAAU,CAAC,GAAG,EAAE;IACd,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IAClD,IAAI,GAAG,IAAI,sBAAsB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;IAC5C,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC;QACzB,GAAG,EAAE,IAAI,IAAI,CAAC,0BAA0B,CAAC;QACzC,iBAAiB,EAAE,IAAI;QACvB,gBAAgB,EAAE,EAAE;KACrB,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,SAAS,CAAC,GAAG,EAAE;IACb,KAAK,CAAC,SAAS,EAAE,CAAC;AACpB,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,4DAA4D,EAAE,GAAG,EAAE;IAC1E,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,qBAAqB,EAAE,CAAC,CAAC,CAAC;QACxE,MAAM,GAAG,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE;YACrC,OAAO,EAAE,qBAAqB,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;YAC/C,KAAK,EAAE,qBAAqB,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;SAC9C,CAAC,CAAC;QACH,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QAClB,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4FAA4F,EAAE,KAAK,IAAI,EAAE;QAC1G,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC,CAAC;QAC1E,6CAA6C;QAC7C,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC9C,MAAM,IAAI,GAAG,qBAAqB,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,gBAAgB,CAAC,IAAI,EAAE;YACrC,OAAO,EAAE,IAAI;YACb,KAAK,EAAE,qBAAqB,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,CAAC;SAC9C,CAAC,CAAC;QACH,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;QAElB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACvC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACrC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;QAC1C,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QAEzC,0DAA0D;QAC1D,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,CAAC,KAAK,KAAK,iBAAiB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAElG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IACnB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type FakeTimers from "@sinonjs/fake-timers";
|
|
2
|
+
import type { ScheduleExecutor, ExecutorContext, ExecutorResult } from "../executor.js";
|
|
3
|
+
import type { CreateScheduleInput } from "../types.js";
|
|
4
|
+
export declare const sample: (over?: Partial<CreateScheduleInput>) => CreateScheduleInput;
|
|
5
|
+
export interface DeferredExecutor extends ScheduleExecutor {
|
|
6
|
+
invocations: ExecutorContext[];
|
|
7
|
+
resolveNext: (result: ExecutorResult) => void;
|
|
8
|
+
rejectNext: (err: Error) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function makeDeferredExecutor(mode?: "swarm" | "oneshot"): DeferredExecutor;
|
|
11
|
+
export interface ImmediateExecutor extends ScheduleExecutor {
|
|
12
|
+
invocations: ExecutorContext[];
|
|
13
|
+
}
|
|
14
|
+
export declare function makeImmediateExecutor(result: ExecutorResult, mode?: "swarm" | "oneshot"): ImmediateExecutor;
|
|
15
|
+
export declare function drainFactory(clock: FakeTimers.InstalledClock, iters?: number): () => Promise<void>;
|
|
16
|
+
export declare function drainUntilFactory(clock: FakeTimers.InstalledClock, maxIters?: number): (cond: () => boolean) => Promise<void>;
|
|
17
|
+
//# sourceMappingURL=tick-helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick-helpers.d.ts","sourceRoot":"","sources":["../../src/__tests__/tick-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,UAAU,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EACV,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAClD,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,eAAO,MAAM,MAAM,GAAI,OAAM,OAAO,CAAC,mBAAmB,CAAM,KAAG,mBAS/D,CAAC;AAEH,MAAM,WAAW,gBAAiB,SAAQ,gBAAgB;IACxD,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B,WAAW,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,CAAC;IAC9C,UAAU,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,GAAE,OAAO,GAAG,SAAqB,GAAG,gBAAgB,CAyB5F;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,WAAW,EAAE,eAAe,EAAE,CAAC;CAChC;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,cAAc,EACtB,IAAI,GAAE,OAAO,GAAG,SAAqB,GACpC,iBAAiB,CAUnB;AAmBD,wBAAgB,YAAY,CAC1B,KAAK,EAAE,UAAU,CAAC,cAAc,EAChC,KAAK,SAAM,GACV,MAAM,OAAO,CAAC,IAAI,CAAC,CAMrB;AAaD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,UAAU,CAAC,cAAc,EAChC,QAAQ,SAAO,GACd,CAAC,IAAI,EAAE,MAAM,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAYxC"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export const sample = (over = {}) => ({
|
|
2
|
+
agentId: "agent-A",
|
|
3
|
+
workDir: "/tmp/proj-tick-1",
|
|
4
|
+
label: "Test",
|
|
5
|
+
mode: "oneshot",
|
|
6
|
+
cronExpression: "* * * * *",
|
|
7
|
+
timezone: "UTC",
|
|
8
|
+
launchConfig: { prompt: "hi" },
|
|
9
|
+
...over,
|
|
10
|
+
});
|
|
11
|
+
export function makeDeferredExecutor(mode = "oneshot") {
|
|
12
|
+
const invocations = [];
|
|
13
|
+
const resolvers = [];
|
|
14
|
+
const rejecters = [];
|
|
15
|
+
return {
|
|
16
|
+
mode,
|
|
17
|
+
invocations,
|
|
18
|
+
resolveNext: (result) => {
|
|
19
|
+
const r = resolvers.shift();
|
|
20
|
+
rejecters.shift();
|
|
21
|
+
if (r)
|
|
22
|
+
r(result);
|
|
23
|
+
},
|
|
24
|
+
rejectNext: (err) => {
|
|
25
|
+
resolvers.shift();
|
|
26
|
+
const rj = rejecters.shift();
|
|
27
|
+
if (rj)
|
|
28
|
+
rj(err);
|
|
29
|
+
},
|
|
30
|
+
async execute(ctx) {
|
|
31
|
+
invocations.push(ctx);
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
resolvers.push(resolve);
|
|
34
|
+
rejecters.push(reject);
|
|
35
|
+
});
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function makeImmediateExecutor(result, mode = "oneshot") {
|
|
40
|
+
const invocations = [];
|
|
41
|
+
return {
|
|
42
|
+
mode,
|
|
43
|
+
invocations,
|
|
44
|
+
async execute(ctx) {
|
|
45
|
+
invocations.push(ctx);
|
|
46
|
+
return result;
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
// proper-lockfile uses setTimeout for retry/touch loops, so a single
|
|
51
|
+
// clock.tickAsync(50) is not enough to flush the tick→appendRun→updateRun
|
|
52
|
+
// chain. Drain in 50ms steps to clear all queued timers.
|
|
53
|
+
//
|
|
54
|
+
// Trade-off: too short and slow CI runners don't finish the proper-lockfile
|
|
55
|
+
// retry chain (the change-event count comes up short). Too long and we cross
|
|
56
|
+
// another 60_000ms cron boundary inside a "tickAsync(60s) + drain" iter,
|
|
57
|
+
// causing extra fires that overlap-and-clobber the previous chain (the
|
|
58
|
+
// breaker counter stops counting cleanly).
|
|
59
|
+
//
|
|
60
|
+
// 200 iters = 10s fake time. Each "tickAsync(60_000) + drain" iter therefore
|
|
61
|
+
// advances 70s of fake time, leaving 50s of headroom before the next cron
|
|
62
|
+
// boundary — safe for the multi-iter loop pattern in tick-25/26/etc.
|
|
63
|
+
//
|
|
64
|
+
// For tests that need to wait for a *specific* event to land, prefer the
|
|
65
|
+
// predicate-based drainUntilFactory below — it stops the moment the
|
|
66
|
+
// condition flips, both faster and more deterministic on slow CI runners.
|
|
67
|
+
export function drainFactory(clock, iters = 200) {
|
|
68
|
+
return async function drain() {
|
|
69
|
+
for (let i = 0; i < iters; i++) {
|
|
70
|
+
await clock.tickAsync(50);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
// Predicate-based drain: keep ticking 50ms until `cond()` is true (or until
|
|
75
|
+
// `maxIters` steps elapse — guards against busy-loops if the condition never
|
|
76
|
+
// becomes true). Stops as soon as the condition flips, so:
|
|
77
|
+
// - on local: returns in a few iters (no wasted fake-time accumulation)
|
|
78
|
+
// - on slow CI: keeps draining proper-lockfile retry chains until the
|
|
79
|
+
// awaited side-effect actually lands
|
|
80
|
+
// - never accumulates more fake-time than necessary, so it won't
|
|
81
|
+
// accidentally roll past the next 60s cron boundary
|
|
82
|
+
//
|
|
83
|
+
// Use in place of plain `await drain()` when a test wants to assert a
|
|
84
|
+
// specific post-state right after a `clock.tickAsync(60_000)`.
|
|
85
|
+
export function drainUntilFactory(clock, maxIters = 1200) {
|
|
86
|
+
return async function drainUntil(cond) {
|
|
87
|
+
for (let i = 0; i < maxIters; i++) {
|
|
88
|
+
if (cond())
|
|
89
|
+
return;
|
|
90
|
+
await clock.tickAsync(50);
|
|
91
|
+
}
|
|
92
|
+
if (!cond()) {
|
|
93
|
+
throw new Error(`drainUntil: condition still false after ${maxIters} iters (${maxIters * 50}ms fake time)`);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=tick-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tick-helpers.js","sourceRoot":"","sources":["../../src/__tests__/tick-helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Schedule, ScheduleRun } from "./types.js";
|
|
2
|
+
export interface ExecutorContext {
|
|
3
|
+
schedule: Schedule;
|
|
4
|
+
run: ScheduleRun;
|
|
5
|
+
signal: AbortSignal;
|
|
6
|
+
onStdout: (chunk: string) => void;
|
|
7
|
+
onStderr: (chunk: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export interface ExecutorResult {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
summary?: string;
|
|
12
|
+
swarmId?: string;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ScheduleExecutor {
|
|
17
|
+
readonly mode: "swarm" | "oneshot";
|
|
18
|
+
execute(ctx: ExecutorContext): Promise<ExecutorResult>;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC;IACjB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,OAAO,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CACxD"}
|
package/dist/executor.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.js","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ScheduleExecutor } from "../executor.js";
|
|
2
|
+
export interface SpawnFn {
|
|
3
|
+
(args: string[], opts: {
|
|
4
|
+
cwd: string;
|
|
5
|
+
env: NodeJS.ProcessEnv;
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
exitCode: number;
|
|
9
|
+
stdout: string;
|
|
10
|
+
stderr: string;
|
|
11
|
+
lastAssistantText?: string;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* OneShot executor: invokes `claude -p` (or any compatible spawnFn) with the
|
|
16
|
+
* scheduled prompt and returns the resulting exit code + summary text. The
|
|
17
|
+
* spawnFn abstraction keeps this module decoupled from a real child_process
|
|
18
|
+
* implementation so it can be unit-tested in isolation and swapped at the
|
|
19
|
+
* runtime layer (Phase 6).
|
|
20
|
+
*/
|
|
21
|
+
export declare function createOneShotExecutor(spawnFn: SpawnFn): ScheduleExecutor;
|
|
22
|
+
//# sourceMappingURL=oneshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oneshot.d.ts","sourceRoot":"","sources":["../../src/executors/oneshot.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAEvD,MAAM,WAAW,OAAO;IACtB,CACE,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,GACjE,OAAO,CAAC;QACT,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC,CAAC;CACJ;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,GAAG,gBAAgB,CA8CxE"}
|