@voyant-travel/workflows-orchestrator 0.107.10

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.
Files changed (61) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +52 -0
  3. package/README.md +76 -0
  4. package/dist/abort-registry.d.ts +6 -0
  5. package/dist/abort-registry.d.ts.map +1 -0
  6. package/dist/abort-registry.js +37 -0
  7. package/dist/concurrency.d.ts +31 -0
  8. package/dist/concurrency.d.ts.map +1 -0
  9. package/dist/concurrency.js +145 -0
  10. package/dist/drive.d.ts +67 -0
  11. package/dist/drive.d.ts.map +1 -0
  12. package/dist/drive.js +373 -0
  13. package/dist/driver-inmemory.d.ts +30 -0
  14. package/dist/driver-inmemory.d.ts.map +1 -0
  15. package/dist/driver-inmemory.js +394 -0
  16. package/dist/event-router.d.ts +51 -0
  17. package/dist/event-router.d.ts.map +1 -0
  18. package/dist/event-router.js +68 -0
  19. package/dist/http-step-handler.d.ts +25 -0
  20. package/dist/http-step-handler.d.ts.map +1 -0
  21. package/dist/http-step-handler.js +78 -0
  22. package/dist/in-memory-store.d.ts +5 -0
  23. package/dist/in-memory-store.d.ts.map +1 -0
  24. package/dist/in-memory-store.js +41 -0
  25. package/dist/index.d.ts +13 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +22 -0
  28. package/dist/journal-helpers.d.ts +3 -0
  29. package/dist/journal-helpers.d.ts.map +1 -0
  30. package/dist/journal-helpers.js +9 -0
  31. package/dist/orchestrator.d.ts +116 -0
  32. package/dist/orchestrator.d.ts.map +1 -0
  33. package/dist/orchestrator.js +411 -0
  34. package/dist/resume-run.d.ts +40 -0
  35. package/dist/resume-run.d.ts.map +1 -0
  36. package/dist/resume-run.js +119 -0
  37. package/dist/schedule.d.ts +51 -0
  38. package/dist/schedule.d.ts.map +1 -0
  39. package/dist/schedule.js +243 -0
  40. package/dist/testing/driver-compliance.d.ts +58 -0
  41. package/dist/testing/driver-compliance.d.ts.map +1 -0
  42. package/dist/testing/driver-compliance.js +667 -0
  43. package/dist/types.d.ts +182 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +4 -0
  46. package/package.json +51 -0
  47. package/src/__tests__/orchestrator-test-support.ts +18 -0
  48. package/src/abort-registry.ts +41 -0
  49. package/src/concurrency.ts +217 -0
  50. package/src/drive.ts +477 -0
  51. package/src/driver-inmemory.ts +511 -0
  52. package/src/event-router.ts +120 -0
  53. package/src/http-step-handler.ts +112 -0
  54. package/src/in-memory-store.ts +44 -0
  55. package/src/index.ts +73 -0
  56. package/src/journal-helpers.ts +11 -0
  57. package/src/orchestrator.ts +527 -0
  58. package/src/resume-run.ts +162 -0
  59. package/src/schedule.ts +310 -0
  60. package/src/testing/driver-compliance.ts +800 -0
  61. package/src/types.ts +201 -0
@@ -0,0 +1,119 @@
1
+ import { emptyJournal } from "./journal-helpers.js";
2
+ export function buildResumeJournal(input) {
3
+ const resumeFromStep = input.resumeFromStep ?? findFirstFailedStep(input.parent);
4
+ if (!resumeFromStep) {
5
+ throw new Error(`run "${input.parent.id}" has no failed step; pass resumeFromStep explicitly to resume it`);
6
+ }
7
+ const journal = emptyJournal();
8
+ journal.metadataState = structuredClone(input.parent.journal.metadataState);
9
+ if (input.seedResults) {
10
+ return buildSeededResumeJournal({
11
+ parentRunId: input.parent.id,
12
+ resumeFromStep,
13
+ seedResults: input.seedResults,
14
+ metadataState: journal.metadataState,
15
+ metadataAppliedCount: input.parent.metadataAppliedCount,
16
+ now: input.now,
17
+ });
18
+ }
19
+ for (const [stepId, entry] of Object.entries(input.parent.journal.stepResults)) {
20
+ if (stepId === resumeFromStep)
21
+ break;
22
+ if (entry.status !== "ok") {
23
+ throw new Error(`step "${stepId}" completed before "${resumeFromStep}" but is not successful; cannot seed resume journal`);
24
+ }
25
+ journal.stepResults[stepId] = structuredClone(entry);
26
+ }
27
+ return {
28
+ resumeFromStep,
29
+ journal,
30
+ metadataAppliedCount: input.parent.metadataAppliedCount,
31
+ };
32
+ }
33
+ export function buildSeededResumeJournal(input) {
34
+ const journal = emptyJournal();
35
+ journal.metadataState = input.metadataState
36
+ ? structuredClone(input.metadataState)
37
+ : {};
38
+ const now = input.now ?? (() => Date.now());
39
+ let at = now();
40
+ for (const [stepId, output] of Object.entries(input.seedResults)) {
41
+ journal.stepResults[stepId] = seededStepEntry(output, at);
42
+ at += 1;
43
+ }
44
+ return {
45
+ resumeFromStep: input.resumeFromStep,
46
+ journal,
47
+ metadataAppliedCount: input.metadataAppliedCount ?? 0,
48
+ };
49
+ }
50
+ const SEED_RESULTS_MAX_ENTRIES = 256;
51
+ const SEED_RESULTS_MAX_STEP_ID_LENGTH = 200;
52
+ const SEED_RESULTS_MAX_SERIALIZED_CHARS = 1_000_000;
53
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: rejecting control chars is the point -- owner: workflows-orchestrator; existing suppression is intentional pending typed cleanup.
54
+ const CONTROL_CHARS = /[\x00-\x1f\x7f]/;
55
+ /**
56
+ * Strict structural validation for caller-supplied `seedResults`
57
+ * (`POST /api/runs/:id/resume`). Seeded entries are written verbatim
58
+ * into the new run's journal as already-completed steps, so they let
59
+ * the caller assert "this step ran and produced this output" — they
60
+ * must be gated behind an operator credential AND shape-checked:
61
+ * a record of bounded, control-character-free step ids to
62
+ * JSON-serializable values, bounded in count and total size.
63
+ */
64
+ export function validateSeedResults(value) {
65
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
66
+ return { ok: false, message: "seedResults must be an object of stepId → output" };
67
+ }
68
+ const entries = Object.entries(value);
69
+ if (entries.length > SEED_RESULTS_MAX_ENTRIES) {
70
+ return {
71
+ ok: false,
72
+ message: `seedResults may contain at most ${SEED_RESULTS_MAX_ENTRIES} entries`,
73
+ };
74
+ }
75
+ for (const [stepId, output] of entries) {
76
+ if (stepId.length === 0 || stepId.length > SEED_RESULTS_MAX_STEP_ID_LENGTH) {
77
+ return {
78
+ ok: false,
79
+ message: `seedResults step ids must be 1-${SEED_RESULTS_MAX_STEP_ID_LENGTH} characters`,
80
+ };
81
+ }
82
+ if (CONTROL_CHARS.test(stepId)) {
83
+ return { ok: false, message: "seedResults step ids must not contain control characters" };
84
+ }
85
+ let serialized;
86
+ try {
87
+ serialized = JSON.stringify(output);
88
+ }
89
+ catch {
90
+ return { ok: false, message: `seedResults["${stepId}"] is not JSON-serializable` };
91
+ }
92
+ if (serialized === undefined) {
93
+ return { ok: false, message: `seedResults["${stepId}"] is not JSON-serializable` };
94
+ }
95
+ if (serialized.length > SEED_RESULTS_MAX_SERIALIZED_CHARS) {
96
+ return {
97
+ ok: false,
98
+ message: `seedResults["${stepId}"] exceeds the ${SEED_RESULTS_MAX_SERIALIZED_CHARS}-character serialized limit`,
99
+ };
100
+ }
101
+ }
102
+ return { ok: true, seedResults: value };
103
+ }
104
+ function findFirstFailedStep(parent) {
105
+ for (const [stepId, entry] of Object.entries(parent.journal.stepResults)) {
106
+ if (entry.status === "err")
107
+ return stepId;
108
+ }
109
+ return undefined;
110
+ }
111
+ function seededStepEntry(output, at) {
112
+ return {
113
+ attempt: 1,
114
+ status: "ok",
115
+ output,
116
+ startedAt: at,
117
+ finishedAt: at,
118
+ };
119
+ }
@@ -0,0 +1,51 @@
1
+ import type { Duration, EnvironmentName, ScheduleDeclaration } from "@voyant-travel/workflows";
2
+ import type { ManifestSchedule, WorkflowManifest } from "@voyant-travel/workflows/protocol";
3
+ export type SchedulableDeclaration = ScheduleDeclaration | ManifestSchedule;
4
+ export interface ScheduleSource {
5
+ id?: string;
6
+ workflowId: string;
7
+ decl: SchedulableDeclaration;
8
+ }
9
+ export interface SchedulerDeps {
10
+ sources: readonly ScheduleSource[];
11
+ onFire: (args: {
12
+ workflowId: string;
13
+ input: unknown;
14
+ scheduleId: string;
15
+ scheduleName?: string;
16
+ fireAt: number;
17
+ }) => Promise<void>;
18
+ now?: () => number;
19
+ environment?: EnvironmentName;
20
+ tickMs?: number;
21
+ setInterval?: typeof setInterval;
22
+ clearInterval?: typeof clearInterval;
23
+ logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void;
24
+ }
25
+ export interface SchedulerHandle {
26
+ start: () => void;
27
+ stop: () => void;
28
+ tick: () => Promise<void>;
29
+ nextFirings: () => {
30
+ workflowId: string;
31
+ scheduleId: string;
32
+ name?: string;
33
+ nextAt: number;
34
+ done: boolean;
35
+ }[];
36
+ sourceCount: () => number;
37
+ }
38
+ export declare function manifestScheduleSources(manifest: WorkflowManifest): ScheduleSource[];
39
+ export declare function createScheduler(deps: SchedulerDeps): SchedulerHandle;
40
+ export declare function computeNextFire(decl: SchedulableDeclaration, fromMs: number): number;
41
+ export interface CronSpec {
42
+ minute: number[];
43
+ hour: number[];
44
+ day: number[];
45
+ month: number[];
46
+ dow: number[];
47
+ }
48
+ export declare function parseCron(expr: string): CronSpec;
49
+ export declare function nextCronFire(spec: CronSpec, fromMs: number): number;
50
+ export declare function toMs(duration: Duration | string | number): number;
51
+ //# sourceMappingURL=schedule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule.d.ts","sourceRoot":"","sources":["../src/schedule.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9F,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAE3F,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,GAAG,gBAAgB,CAAA;AAE3E,MAAM,WAAW,cAAc;IAC7B,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,UAAU,EAAE,MAAM,CAAA;IAClB,IAAI,EAAE,sBAAsB,CAAA;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,SAAS,cAAc,EAAE,CAAA;IAClC,MAAM,EAAE,CAAC,IAAI,EAAE;QACb,UAAU,EAAE,MAAM,CAAA;QAClB,KAAK,EAAE,OAAO,CAAA;QACd,UAAU,EAAE,MAAM,CAAA;QAClB,YAAY,CAAC,EAAE,MAAM,CAAA;QACrB,MAAM,EAAE,MAAM,CAAA;KACf,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;IAClB,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,WAAW,CAAC,EAAE,OAAO,WAAW,CAAA;IAChC,aAAa,CAAC,EAAE,OAAO,aAAa,CAAA;IACpC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAChF;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,IAAI,CAAA;IACjB,IAAI,EAAE,MAAM,IAAI,CAAA;IAChB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IACzB,WAAW,EAAE,MAAM;QACjB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,CAAA;QAClB,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;QACd,IAAI,EAAE,OAAO,CAAA;KACd,EAAE,CAAA;IACH,WAAW,EAAE,MAAM,MAAM,CAAA;CAC1B;AA0BD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,cAAc,EAAE,CAYpF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,CAmHpE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CASpF;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,GAAG,EAAE,MAAM,EAAE,CAAA;IACb,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,GAAG,EAAE,MAAM,EAAE,CAAA;CACd;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAYhD;AA8BD,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAmBnE;AAED,wBAAgB,IAAI,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAqBjE"}
@@ -0,0 +1,243 @@
1
+ function unrefTimer(timer) {
2
+ if (typeof timer === "object" &&
3
+ timer !== null &&
4
+ "unref" in timer &&
5
+ typeof timer.unref === "function") {
6
+ timer.unref();
7
+ }
8
+ }
9
+ export function manifestScheduleSources(manifest) {
10
+ const sources = [];
11
+ for (const workflow of manifest.workflows) {
12
+ workflow.schedules.forEach((decl, index) => {
13
+ sources.push({
14
+ id: `${manifest.versionId}:${workflow.id}:${decl.name ?? index}`,
15
+ workflowId: workflow.id,
16
+ decl,
17
+ });
18
+ });
19
+ }
20
+ return sources;
21
+ }
22
+ export function createScheduler(deps) {
23
+ const now = deps.now ?? (() => Date.now());
24
+ const tickMs = deps.tickMs ?? 1_000;
25
+ const setInt = deps.setInterval ?? setInterval;
26
+ const clearInt = deps.clearInterval ?? clearInterval;
27
+ const env = deps.environment ?? "development";
28
+ const log = deps.logger ?? (() => { });
29
+ const states = [];
30
+ for (const [index, source] of deps.sources.entries()) {
31
+ if (source.decl.enabled === false)
32
+ continue;
33
+ if (source.decl.environments && !source.decl.environments.includes(env))
34
+ continue;
35
+ let firstAt;
36
+ try {
37
+ firstAt = computeNextFire(source.decl, now());
38
+ }
39
+ catch (err) {
40
+ log("warn", `scheduler: skipping source for workflow "${source.workflowId}": ${String(err)}`);
41
+ continue;
42
+ }
43
+ states.push({
44
+ source,
45
+ scheduleId: source.id ?? `${source.workflowId}:${source.decl.name ?? index}`,
46
+ nextAt: firstAt,
47
+ done: false,
48
+ inFlight: false,
49
+ queued: [],
50
+ });
51
+ }
52
+ let timer;
53
+ const advanceAfterFire = (state, firedAt) => {
54
+ if ("at" in state.source.decl) {
55
+ state.done = true;
56
+ return;
57
+ }
58
+ try {
59
+ state.nextAt = computeNextFire(state.source.decl, firedAt);
60
+ }
61
+ catch (err) {
62
+ log("error", `scheduler: cannot compute next fire for "${state.source.workflowId}": ${String(err)}`);
63
+ state.done = true;
64
+ }
65
+ };
66
+ const fire = async (state, fireAt) => {
67
+ try {
68
+ const input = await resolveInput(state.source.decl.input);
69
+ await deps.onFire({
70
+ workflowId: state.source.workflowId,
71
+ input,
72
+ scheduleId: state.scheduleId,
73
+ scheduleName: state.source.decl.name,
74
+ fireAt,
75
+ });
76
+ }
77
+ catch (err) {
78
+ log("error", `scheduler: onFire threw for "${state.source.workflowId}": ${String(err)}`);
79
+ }
80
+ finally {
81
+ const next = state.queued.shift();
82
+ if (next) {
83
+ void fire(state, next.fireAt);
84
+ }
85
+ else {
86
+ state.inFlight = false;
87
+ }
88
+ }
89
+ };
90
+ const doTick = async () => {
91
+ const t = now();
92
+ const ready = states.filter((state) => !state.done && state.nextAt <= t);
93
+ for (const state of ready) {
94
+ const overlap = state.source.decl.overlap ?? "skip";
95
+ if (state.inFlight && overlap === "skip")
96
+ continue;
97
+ const fireAt = state.nextAt;
98
+ if (state.inFlight && overlap === "queue") {
99
+ state.queued.push({ fireAt });
100
+ advanceAfterFire(state, t);
101
+ continue;
102
+ }
103
+ state.inFlight = true;
104
+ const firePromise = fire(state, fireAt);
105
+ advanceAfterFire(state, t);
106
+ if (overlap !== "allow")
107
+ await firePromise;
108
+ }
109
+ };
110
+ return {
111
+ start() {
112
+ if (timer)
113
+ return;
114
+ timer = setInt(() => {
115
+ doTick().catch(() => { });
116
+ }, tickMs);
117
+ unrefTimer(timer);
118
+ },
119
+ stop() {
120
+ if (!timer)
121
+ return;
122
+ clearInt(timer);
123
+ timer = undefined;
124
+ },
125
+ tick: doTick,
126
+ nextFirings() {
127
+ return states.map((state) => ({
128
+ workflowId: state.source.workflowId,
129
+ scheduleId: state.scheduleId,
130
+ name: state.source.decl.name,
131
+ nextAt: state.nextAt,
132
+ done: state.done,
133
+ }));
134
+ },
135
+ sourceCount() {
136
+ return states.length;
137
+ },
138
+ };
139
+ }
140
+ export function computeNextFire(decl, fromMs) {
141
+ if ("cron" in decl && decl.cron !== undefined)
142
+ return nextCronFire(parseCron(decl.cron), fromMs);
143
+ if ("every" in decl && decl.every !== undefined)
144
+ return fromMs + toMs(decl.every);
145
+ if ("at" in decl && decl.at !== undefined) {
146
+ const at = typeof decl.at === "string" ? Date.parse(decl.at) : decl.at.getTime();
147
+ if (!Number.isFinite(at))
148
+ throw new Error(`invalid "at" value: ${String(decl.at)}`);
149
+ return at < fromMs ? Number.POSITIVE_INFINITY : at;
150
+ }
151
+ throw new Error(`schedule declaration missing one of cron/every/at`);
152
+ }
153
+ export function parseCron(expr) {
154
+ const parts = expr.trim().split(/\s+/);
155
+ if (parts.length !== 5) {
156
+ throw new Error(`invalid cron "${expr}" - expected 5 fields (minute hour day month dow)`);
157
+ }
158
+ return {
159
+ minute: parseField(parts[0], 0, 59, "minute"),
160
+ hour: parseField(parts[1], 0, 23, "hour"),
161
+ day: parseField(parts[2], 1, 31, "day"),
162
+ month: parseField(parts[3], 1, 12, "month"),
163
+ dow: parseField(parts[4], 0, 6, "dow"),
164
+ };
165
+ }
166
+ function parseField(f, min, max, label) {
167
+ const out = new Set();
168
+ for (const part of f.split(",")) {
169
+ const stepMatch = /^(.+)\/(\d+)$/.exec(part);
170
+ const body = stepMatch ? stepMatch[1] : part;
171
+ const step = stepMatch ? Number(stepMatch[2]) : 1;
172
+ if (!(step >= 1))
173
+ throw new Error(`cron ${label} step must be >=1 in "${f}"`);
174
+ let lo;
175
+ let hi;
176
+ if (body === "*") {
177
+ lo = min;
178
+ hi = max;
179
+ }
180
+ else if (body.includes("-")) {
181
+ const [a, b] = body.split("-");
182
+ lo = Number(a);
183
+ hi = Number(b);
184
+ }
185
+ else {
186
+ lo = Number(body);
187
+ hi = lo;
188
+ }
189
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo < min || hi > max || lo > hi) {
190
+ throw new Error(`cron ${label} out of range [${min}..${max}] in "${f}"`);
191
+ }
192
+ for (let i = lo; i <= hi; i += step)
193
+ out.add(i);
194
+ }
195
+ return [...out].sort((a, b) => a - b);
196
+ }
197
+ export function nextCronFire(spec, fromMs) {
198
+ const date = new Date(fromMs);
199
+ date.setUTCSeconds(0, 0);
200
+ date.setUTCMinutes(date.getUTCMinutes() + 1);
201
+ const maxIterations = 60 * 24 * 366 * 5;
202
+ for (let i = 0; i < maxIterations; i++) {
203
+ if (spec.minute.includes(date.getUTCMinutes()) &&
204
+ spec.hour.includes(date.getUTCHours()) &&
205
+ spec.day.includes(date.getUTCDate()) &&
206
+ spec.month.includes(date.getUTCMonth() + 1) &&
207
+ spec.dow.includes(date.getUTCDay())) {
208
+ return date.getTime();
209
+ }
210
+ date.setUTCMinutes(date.getUTCMinutes() + 1);
211
+ }
212
+ throw new Error("cron search exceeded 5 years without finding a match");
213
+ }
214
+ export function toMs(duration) {
215
+ if (typeof duration === "number")
216
+ return duration;
217
+ const m = /^(\d+)(ms|s|m|h|d|w)$/.exec(duration);
218
+ if (!m)
219
+ throw new Error(`invalid duration "${duration}"`);
220
+ const n = Number(m[1]);
221
+ switch (m[2]) {
222
+ case "ms":
223
+ return n;
224
+ case "s":
225
+ return n * 1_000;
226
+ case "m":
227
+ return n * 60_000;
228
+ case "h":
229
+ return n * 3_600_000;
230
+ case "d":
231
+ return n * 86_400_000;
232
+ case "w":
233
+ return n * 604_800_000;
234
+ default:
235
+ throw new Error(`invalid duration "${duration}"`);
236
+ }
237
+ }
238
+ async function resolveInput(input) {
239
+ if (typeof input === "function") {
240
+ return await input();
241
+ }
242
+ return input;
243
+ }
@@ -0,0 +1,58 @@
1
+ import type { DriverFactory, DriverFactoryDeps, ServiceResolver } from "@voyant-travel/workflows/driver";
2
+ import type { WorkflowManifest } from "@voyant-travel/workflows/protocol";
3
+ /**
4
+ * Tiny in-memory ServiceResolver builder for compliance tests. Lets a test
5
+ * register named services then assert workflow bodies can resolve them via
6
+ * `ctx.services.resolve(...)`.
7
+ */
8
+ export declare function makeServiceResolver(entries?: Record<string, unknown>): ServiceResolver;
9
+ /**
10
+ * Build a `DriverFactoryDeps` value suitable for compliance tests. Captures
11
+ * log lines so individual tests can assert on them when needed. Pass
12
+ * `services` to register specific entries; defaults to an empty resolver.
13
+ */
14
+ export declare function testFactoryDeps(services?: ServiceResolver): DriverFactoryDeps & {
15
+ logs: Array<[string, string, object?]>;
16
+ };
17
+ /**
18
+ * Build a minimal manifest for tests. The shape matches `WorkflowManifest`;
19
+ * filter-related fields stay empty until the event-router (PR2) lands.
20
+ */
21
+ export declare function buildTestManifest(versionId?: string): WorkflowManifest;
22
+ /**
23
+ * Opt-in capability flags for drivers that don't share a process with
24
+ * step bodies. Default to `true` because in-process drivers (InMemory,
25
+ * Mode 2) satisfy every contract. Out-of-process drivers (Mode 1 / CF
26
+ * edge — orchestrator and tenant live in separate Worker isolates) opt
27
+ * out of in-process-only assertions like `ctx.services` threading.
28
+ */
29
+ export interface DriverComplianceCapabilities {
30
+ /**
31
+ * When true, the framework's `ModuleContainer` is plumbed to step
32
+ * bodies via `ctx.services`. False for Mode 1, where the orchestrator
33
+ * and tenant are separate Workers and a per-tenant container would
34
+ * have to ship across a serialization boundary.
35
+ */
36
+ servicesThreading?: boolean;
37
+ /**
38
+ * When true, `admin.listRuns(...)` returns runs the driver knows about.
39
+ * False for self-host Mode 1, which has no native cross-run query
40
+ * layer (per architecture doc §8.3) — `listRuns` exists but returns
41
+ * an empty page; voyant-cloud provides an index in its repo.
42
+ */
43
+ crossRunQueries?: boolean;
44
+ /**
45
+ * When true, the driver under test runs an in-process DATETIME wakeup
46
+ * loop/timer during compliance tests. False for drivers whose compliance
47
+ * harness intentionally disables or fakes the time wheel.
48
+ */
49
+ autoDatetimeWakeups?: boolean;
50
+ /**
51
+ * When true, workflow-level `WorkflowConfig.concurrency` is enforced
52
+ * for in-process workflow definitions. False for Mode 1 / Cloudflare
53
+ * until it grows a cross-run coordination DO.
54
+ */
55
+ workflowConcurrency?: boolean;
56
+ }
57
+ export declare function runDriverComplianceSuite(name: string, makeFactory: () => DriverFactory, capabilities?: DriverComplianceCapabilities): void;
58
+ //# sourceMappingURL=driver-compliance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver-compliance.d.ts","sourceRoot":"","sources":["../../src/testing/driver-compliance.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,eAAe,EAChB,MAAM,iCAAiC,CAAA;AAExC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAKzE;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,eAAe,CAa1F;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,GAAE,eAAuC,GAChD,iBAAiB,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;CAAE,CAOhE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,SAAe,GAAG,gBAAgB,CAc5E;AA4BD;;;;;;GAMG;AACH,MAAM,WAAW,4BAA4B;IAC3C;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;IAC3B;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAID,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,aAAa,EAChC,YAAY,GAAE,4BAAiC,GAC9C,IAAI,CAqnBN"}