@rigkit/engine 0.2.3 → 0.2.4
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/package.json +1 -1
- package/src/authoring.ts +28 -13
- package/src/authoring.typecheck.ts +21 -1
- package/src/engine.test.ts +193 -28
- package/src/engine.ts +282 -30
- package/src/host-storage.ts +128 -0
- package/src/index.ts +5 -0
- package/src/provider/types.ts +2 -0
- package/src/state.ts +40 -0
- package/src/types.ts +139 -46
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/authoring.ts
CHANGED
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
WorkflowWorkspaceDefinition,
|
|
26
26
|
} from "./types.ts";
|
|
27
27
|
|
|
28
|
-
const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers"]);
|
|
28
|
+
const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers", "step"]);
|
|
29
29
|
const reservedHostOperationIds = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
|
|
30
30
|
|
|
31
31
|
const readEnv = (name: string, fallback?: string): string => {
|
|
@@ -51,9 +51,17 @@ export function workflow<const Name extends string, const Providers extends Work
|
|
|
51
51
|
providers: options.providers,
|
|
52
52
|
sequence: <InputContext extends JsonObject = {}>(sequenceName: string) =>
|
|
53
53
|
createSequence(app as unknown as WorkflowDefinition<string, Providers>, sequenceName, []),
|
|
54
|
-
task: (
|
|
54
|
+
task: (
|
|
55
|
+
taskName: string,
|
|
56
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
|
|
57
|
+
maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
|
|
58
|
+
) =>
|
|
55
59
|
createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
|
|
56
|
-
step: (
|
|
60
|
+
step: (
|
|
61
|
+
taskName: string,
|
|
62
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, {}, never, any>,
|
|
63
|
+
maybeHandler?: WorkflowTaskHandler<Providers, {}, never, any>,
|
|
64
|
+
) =>
|
|
57
65
|
createTask(app as unknown as WorkflowDefinition<string, Providers>, taskName, optionsOrHandler as any, maybeHandler as any),
|
|
58
66
|
};
|
|
59
67
|
|
|
@@ -127,6 +135,7 @@ function createSequence<
|
|
|
127
135
|
WorkspaceData extends object = JsonObject,
|
|
128
136
|
OperationIds extends string = never,
|
|
129
137
|
WorkspaceOperationIds extends string = never,
|
|
138
|
+
PreviousTaskIds extends string = never,
|
|
130
139
|
>(
|
|
131
140
|
app: WorkflowDefinition<string, Providers>,
|
|
132
141
|
name: string,
|
|
@@ -140,7 +149,8 @@ function createSequence<
|
|
|
140
149
|
OutputContext,
|
|
141
150
|
WorkspaceData,
|
|
142
151
|
OperationIds,
|
|
143
|
-
WorkspaceOperationIds
|
|
152
|
+
WorkspaceOperationIds,
|
|
153
|
+
PreviousTaskIds
|
|
144
154
|
> {
|
|
145
155
|
const node = {
|
|
146
156
|
kind: "rigkit.workflow-node" as const,
|
|
@@ -153,16 +163,16 @@ function createSequence<
|
|
|
153
163
|
workspaceOperations,
|
|
154
164
|
task: (
|
|
155
165
|
taskName: string,
|
|
156
|
-
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
157
|
-
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
166
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
167
|
+
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
158
168
|
) => {
|
|
159
169
|
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
160
170
|
return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
|
|
161
171
|
},
|
|
162
172
|
step: (
|
|
163
173
|
taskName: string,
|
|
164
|
-
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
165
|
-
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
174
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
175
|
+
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, PreviousTaskIds, any>,
|
|
166
176
|
) => {
|
|
167
177
|
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
168
178
|
return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
|
|
@@ -209,7 +219,8 @@ function createSequence<
|
|
|
209
219
|
OutputContext,
|
|
210
220
|
WorkspaceData,
|
|
211
221
|
OperationIds,
|
|
212
|
-
WorkspaceOperationIds
|
|
222
|
+
WorkspaceOperationIds,
|
|
223
|
+
PreviousTaskIds
|
|
213
224
|
>;
|
|
214
225
|
}
|
|
215
226
|
|
|
@@ -319,15 +330,19 @@ function createOperationInputBuilder<Input extends object>(
|
|
|
319
330
|
} as WorkflowOperationInputBuilder<Input>;
|
|
320
331
|
}
|
|
321
332
|
|
|
322
|
-
function createTask<
|
|
333
|
+
function createTask<
|
|
334
|
+
Providers extends WorkflowProviderMap,
|
|
335
|
+
InputContext extends JsonObject,
|
|
336
|
+
PreviousTaskIds extends string = string,
|
|
337
|
+
>(
|
|
323
338
|
app: WorkflowDefinition<string, Providers>,
|
|
324
339
|
name: string,
|
|
325
|
-
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, any>,
|
|
326
|
-
maybeHandler?: WorkflowTaskHandler<Providers, InputContext, any>,
|
|
340
|
+
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
|
|
341
|
+
maybeHandler?: WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>,
|
|
327
342
|
): WorkflowTaskNode<Providers, InputContext, any> {
|
|
328
343
|
const options = typeof optionsOrHandler === "function" ? undefined : optionsOrHandler;
|
|
329
344
|
const handler = (typeof optionsOrHandler === "function" ? optionsOrHandler : maybeHandler) as
|
|
330
|
-
| WorkflowTaskHandler<Providers, InputContext, any>
|
|
345
|
+
| WorkflowTaskHandler<Providers, InputContext, PreviousTaskIds, any>
|
|
331
346
|
| undefined;
|
|
332
347
|
if (!handler) throw new Error(`Task ${name} is missing a handler`);
|
|
333
348
|
|
|
@@ -10,7 +10,7 @@ sequence("normal-operation-ids")
|
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
sequence("workspace-operation-ids")
|
|
13
|
-
.step("prepare", async () => ({ snapshotId: "snap-1" }))
|
|
13
|
+
.step("prepare", async () => ({ ctx: { snapshotId: "snap-1" } }))
|
|
14
14
|
.workspace({
|
|
15
15
|
create: async ({ workflow, workspace }) => {
|
|
16
16
|
const snapshotId: string = workflow.ctx.snapshotId;
|
|
@@ -44,6 +44,26 @@ sequence("workspace-operation-ids")
|
|
|
44
44
|
run: async () => null,
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
sequence("typed-step-invalidation")
|
|
48
|
+
.step("github-auth", async () => ({ ctx: { token: "ok" } }))
|
|
49
|
+
.step("check-auth", async ({ step }) => {
|
|
50
|
+
const token: string = step.ctx.token;
|
|
51
|
+
void token;
|
|
52
|
+
return step.invalidate("github-auth");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
sequence("typed-step-invalidation-targets")
|
|
56
|
+
.step("github-auth", async () => ({ ctx: { token: "ok" } }))
|
|
57
|
+
.step("check-auth", async ({ step }) => {
|
|
58
|
+
// @ts-expect-error invalidation target must be a previous task id
|
|
59
|
+
return step.invalidate("missing-auth");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
sequence("duplicate-step-id")
|
|
63
|
+
.step("prepare" as const, async () => ({ ctx: { snapshotId: "snap-1" } }))
|
|
64
|
+
// @ts-expect-error duplicate task ids are rejected for literal ids
|
|
65
|
+
.step("prepare" as const, async ({ step }) => ({ ctx: step.ctx }));
|
|
66
|
+
|
|
47
67
|
sequence("reserved-operation-id")
|
|
48
68
|
// @ts-expect-error reserved operation ids are rejected for literal ids
|
|
49
69
|
.operation("create" as const, {
|
package/src/engine.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { Database } from "bun:sqlite";
|
|
3
|
-
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { createDevMachineEngine, type InteractionPresentationRequest } from "./engine.ts";
|
|
@@ -27,34 +27,34 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
27
27
|
},
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
const base = app.sequence("base").task("first", async ({
|
|
31
|
-
|
|
30
|
+
const base = app.sequence("base").task("first", async ({ step, test }) => {
|
|
31
|
+
step.log("preparing base\\n", { label: "setup" });
|
|
32
32
|
const vm = await test.createVm();
|
|
33
33
|
await vm.exec("touch /tmp/first", { name: "touch first" });
|
|
34
34
|
if (!(await vm.exists("/tmp/first"))) throw new Error("first was not created");
|
|
35
|
-
return { first: true, vm: await vm.snapshotRef() };
|
|
35
|
+
return { ctx: { first: true, vm: await vm.snapshotRef() } };
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
const left = app.sequence("left").task("second", async ({
|
|
39
|
-
if (!ctx.first) throw new Error("missing first context");
|
|
40
|
-
const vm = await test.fromSnapshot(ctx.vm);
|
|
38
|
+
const left = app.sequence("left").task("second", async ({ step, test }) => {
|
|
39
|
+
if (!step.ctx.first) throw new Error("missing first context");
|
|
40
|
+
const vm = await test.fromSnapshot(step.ctx.vm);
|
|
41
41
|
await vm.exec("touch /tmp/second", { name: "touch second" });
|
|
42
|
-
return { second: true, vm: await vm.snapshotRef() };
|
|
42
|
+
return { ctx: { second: true, vm: await vm.snapshotRef() } };
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
const right = app.sequence("right").task("data", async ({
|
|
46
|
-
if (!ctx.first) throw new Error("missing first context");
|
|
47
|
-
return { data: "right-ready" };
|
|
45
|
+
const right = app.sequence("right").task("data", async ({ step }) => {
|
|
46
|
+
if (!step.ctx.first) throw new Error("missing first context");
|
|
47
|
+
return { ctx: { data: "right-ready" } };
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
export default app
|
|
51
51
|
.sequence("root")
|
|
52
52
|
.add(base)
|
|
53
53
|
.parallel({ left, right })
|
|
54
|
-
.task("join", async ({
|
|
55
|
-
if (!ctx.left.second) throw new Error("missing left context");
|
|
56
|
-
if (ctx.right.data !== "right-ready") throw new Error("missing right context");
|
|
57
|
-
return { vm: ctx.left.vm, summary: ctx.right.data };
|
|
54
|
+
.task("join", async ({ step }) => {
|
|
55
|
+
if (!step.ctx.left.second) throw new Error("missing left context");
|
|
56
|
+
if (step.ctx.right.data !== "right-ready") throw new Error("missing right context");
|
|
57
|
+
return { ctx: { vm: step.ctx.left.vm, summary: step.ctx.right.data } };
|
|
58
58
|
})
|
|
59
59
|
.workspace({
|
|
60
60
|
create: async ({ providers, workflow, workspace, local }) => {
|
|
@@ -189,8 +189,10 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
189
189
|
const vm = await providers.test.createVm();
|
|
190
190
|
await vm.exec("touch /tmp/template", { name: "prepare template" });
|
|
191
191
|
return {
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
ctx: {
|
|
193
|
+
vm: await vm.snapshotRef(),
|
|
194
|
+
repoPath: "/workspace/repo",
|
|
195
|
+
},
|
|
194
196
|
};
|
|
195
197
|
})
|
|
196
198
|
.workspace({
|
|
@@ -279,8 +281,8 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
279
281
|
`
|
|
280
282
|
import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
|
|
281
283
|
|
|
282
|
-
const api = sequence("api").step("ready", async () => ({ api: true }));
|
|
283
|
-
const web = sequence("web").step("ready", async () => ({ web: true }));
|
|
284
|
+
const api = sequence("api").step("ready", async () => ({ ctx: { api: true } }));
|
|
285
|
+
const web = sequence("web").step("ready", async () => ({ ctx: { web: true } }));
|
|
284
286
|
|
|
285
287
|
export default defineConfig({
|
|
286
288
|
providers: {},
|
|
@@ -310,7 +312,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
310
312
|
`
|
|
311
313
|
import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
|
|
312
314
|
|
|
313
|
-
const root = sequence("factory-test").step("ready", async () => ({ ready: true }));
|
|
315
|
+
const root = sequence("factory-test").step("ready", async () => ({ ctx: { ready: true } }));
|
|
314
316
|
|
|
315
317
|
export default defineConfig({
|
|
316
318
|
providers: {},
|
|
@@ -432,7 +434,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
432
434
|
const app = workflow("handler-cache", { providers: {} });
|
|
433
435
|
|
|
434
436
|
export default app.sequence("root").task("value", async () => {
|
|
435
|
-
return { value: "${value}" };
|
|
437
|
+
return { ctx: { value: "${value}" } };
|
|
436
438
|
});
|
|
437
439
|
`,
|
|
438
440
|
);
|
|
@@ -490,7 +492,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
490
492
|
},
|
|
491
493
|
});
|
|
492
494
|
|
|
493
|
-
export default app.sequence("root").task("ready", async () => ({ ready: true }));
|
|
495
|
+
export default app.sequence("root").task("ready", async () => ({ ctx: { ready: true } }));
|
|
494
496
|
`,
|
|
495
497
|
);
|
|
496
498
|
|
|
@@ -533,6 +535,73 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
533
535
|
expect(metadata["config.path"]).toBe(join(projectDir, "rig.config.ts"));
|
|
534
536
|
});
|
|
535
537
|
|
|
538
|
+
test("stores provider host JSON state outside project state", async () => {
|
|
539
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
|
|
540
|
+
const hostStorageDir = join(projectDir, ".host-storage");
|
|
541
|
+
const opened: string[] = [];
|
|
542
|
+
const plugin: BaseProviderPlugin = {
|
|
543
|
+
providerId: "test",
|
|
544
|
+
async createProvider({ storage, hostStorage, local }) {
|
|
545
|
+
storage.set("project", { value: "state" });
|
|
546
|
+
hostStorage.set("token", { value: "secret" });
|
|
547
|
+
await local.open("rigkit://provider-auth");
|
|
548
|
+
return new FakeWorkflowProvider();
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
writeFileSync(
|
|
553
|
+
join(projectDir, "rig.config.ts"),
|
|
554
|
+
`
|
|
555
|
+
import { defineProvider, workflow } from "${import.meta.dir}/index.ts";
|
|
556
|
+
|
|
557
|
+
const app = workflow("provider-host-storage", {
|
|
558
|
+
providers: {
|
|
559
|
+
test: defineProvider("test", {}),
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
export default app.sequence("root").task("ready", async () => ({ ctx: { ready: true } }));
|
|
564
|
+
`,
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const engine = await createDevMachineEngine({
|
|
568
|
+
projectDir,
|
|
569
|
+
hostStorageDir,
|
|
570
|
+
providers: [plugin],
|
|
571
|
+
local: {
|
|
572
|
+
open: async (target) => {
|
|
573
|
+
opened.push(target);
|
|
574
|
+
},
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
await engine.load();
|
|
579
|
+
await engine.plan();
|
|
580
|
+
|
|
581
|
+
expect(opened).toEqual(["rigkit://provider-auth"]);
|
|
582
|
+
|
|
583
|
+
const files = readdirSync(hostStorageDir);
|
|
584
|
+
expect(files).toHaveLength(1);
|
|
585
|
+
const hostState = JSON.parse(readFileSync(join(hostStorageDir, files[0]!), "utf8"));
|
|
586
|
+
expect(hostState.records.token.value.value).toBe("secret");
|
|
587
|
+
|
|
588
|
+
const main = new Database(engine.getProjectInfo().statePath);
|
|
589
|
+
const projectRow = main
|
|
590
|
+
.query<{ value_json: string }, []>(
|
|
591
|
+
"select value_json from provider_state where provider_id = 'test' and key = 'project'",
|
|
592
|
+
)
|
|
593
|
+
.get();
|
|
594
|
+
const leakedHostRow = main
|
|
595
|
+
.query<{ value_json: string }, []>(
|
|
596
|
+
"select value_json from provider_state where provider_id = 'test' and key = 'token'",
|
|
597
|
+
)
|
|
598
|
+
.get();
|
|
599
|
+
main.close();
|
|
600
|
+
|
|
601
|
+
expect(projectRow ? JSON.parse(projectRow.value_json).value : undefined).toBe("state");
|
|
602
|
+
expect(leakedHostRow).toBeNull();
|
|
603
|
+
});
|
|
604
|
+
|
|
536
605
|
test("rejects task outputs that are not JSON serializable", async () => {
|
|
537
606
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
|
|
538
607
|
writeFileSync(
|
|
@@ -547,7 +616,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
547
616
|
});
|
|
548
617
|
|
|
549
618
|
export default app.sequence("bad").task("returns-function", async () => {
|
|
550
|
-
return { fn: () => "nope" };
|
|
619
|
+
return { ctx: { fn: () => "nope" } };
|
|
551
620
|
});
|
|
552
621
|
`,
|
|
553
622
|
);
|
|
@@ -576,7 +645,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
576
645
|
|
|
577
646
|
export default app.sequence("auth").task("login", async ({ test }) => {
|
|
578
647
|
const result = await test.openTerminal("GitHub auth", "gh auth login");
|
|
579
|
-
return { finished: result.finished };
|
|
648
|
+
return { ctx: { finished: result.finished } };
|
|
580
649
|
});
|
|
581
650
|
`,
|
|
582
651
|
);
|
|
@@ -622,7 +691,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
622
691
|
|
|
623
692
|
export default app.sequence("auth").task("login", async ({ test }) => {
|
|
624
693
|
const result = await test.openTerminal("GitHub auth", "gh auth login");
|
|
625
|
-
return { finished: result.finished };
|
|
694
|
+
return { ctx: { finished: result.finished } };
|
|
626
695
|
});
|
|
627
696
|
`,
|
|
628
697
|
);
|
|
@@ -676,7 +745,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
676
745
|
export default app.sequence("setup").task("touch", async ({ test }) => {
|
|
677
746
|
const vm = await test.createVm();
|
|
678
747
|
await vm.exec("touch /tmp/setup", { name: "touch setup" });
|
|
679
|
-
return { vm: await vm.snapshotRef() };
|
|
748
|
+
return { ctx: { vm: await vm.snapshotRef() } };
|
|
680
749
|
});
|
|
681
750
|
`,
|
|
682
751
|
);
|
|
@@ -734,8 +803,8 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
734
803
|
|
|
735
804
|
export default app.sequence("schema").task("value", { output: schema }, async () => {
|
|
736
805
|
return process.env.RIGKIT_SCHEMA_MODE === "next"
|
|
737
|
-
? { value: "ok", next: true }
|
|
738
|
-
: { value: "ok" };
|
|
806
|
+
? { ctx: { value: "ok", next: true } }
|
|
807
|
+
: { ctx: { value: "ok" } };
|
|
739
808
|
});
|
|
740
809
|
`,
|
|
741
810
|
);
|
|
@@ -767,6 +836,94 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
767
836
|
}
|
|
768
837
|
}
|
|
769
838
|
});
|
|
839
|
+
|
|
840
|
+
test("expires task cache when cacheTTL has elapsed", async () => {
|
|
841
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-cache-ttl-"));
|
|
842
|
+
writeFileSync(
|
|
843
|
+
join(projectDir, "rig.config.ts"),
|
|
844
|
+
`
|
|
845
|
+
import { sequence } from "${import.meta.dir}/index.ts";
|
|
846
|
+
|
|
847
|
+
export default sequence("ttl").task("daily-check", { cacheTTL: "1d" }, async () => {
|
|
848
|
+
return { ctx: { checked: true } };
|
|
849
|
+
});
|
|
850
|
+
`,
|
|
851
|
+
);
|
|
852
|
+
|
|
853
|
+
const engine = await createDevMachineEngine({ projectDir });
|
|
854
|
+
await engine.load();
|
|
855
|
+
await engine.apply();
|
|
856
|
+
expect((await engine.plan()).cachedNodeCount).toBe(1);
|
|
857
|
+
|
|
858
|
+
const db = new Database(engine.getProjectInfo().statePath);
|
|
859
|
+
db.run("update workflow_node_runs set created_at = ?", [
|
|
860
|
+
new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
861
|
+
]);
|
|
862
|
+
db.close();
|
|
863
|
+
|
|
864
|
+
const expired = await engine.plan();
|
|
865
|
+
expect(expired.cachedNodeCount).toBe(0);
|
|
866
|
+
expect(expired.nodes[0]?.status).toBe("pending");
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
test("step.invalidate invalidates a previous task and replays from that point", async () => {
|
|
870
|
+
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-invalidate-"));
|
|
871
|
+
const previous = {
|
|
872
|
+
authCount: process.env.RIGKIT_AUTH_COUNT,
|
|
873
|
+
checkCount: process.env.RIGKIT_CHECK_COUNT,
|
|
874
|
+
forceReauth: process.env.RIGKIT_FORCE_REAUTH,
|
|
875
|
+
};
|
|
876
|
+
process.env.RIGKIT_AUTH_COUNT = "0";
|
|
877
|
+
process.env.RIGKIT_CHECK_COUNT = "0";
|
|
878
|
+
process.env.RIGKIT_FORCE_REAUTH = "0";
|
|
879
|
+
|
|
880
|
+
writeFileSync(
|
|
881
|
+
join(projectDir, "rig.config.ts"),
|
|
882
|
+
`
|
|
883
|
+
import { sequence } from "${import.meta.dir}/index.ts";
|
|
884
|
+
|
|
885
|
+
export default sequence("reauth")
|
|
886
|
+
.task("prepare", async () => ({ ctx: { prepared: true } }))
|
|
887
|
+
.task("github-auth", async () => {
|
|
888
|
+
const count = Number(process.env.RIGKIT_AUTH_COUNT ?? "0") + 1;
|
|
889
|
+
process.env.RIGKIT_AUTH_COUNT = String(count);
|
|
890
|
+
return { ctx: { token: "token-" + count } };
|
|
891
|
+
})
|
|
892
|
+
.task("check-auth", { cacheTTL: 0 }, async ({ step }) => {
|
|
893
|
+
const count = Number(process.env.RIGKIT_CHECK_COUNT ?? "0") + 1;
|
|
894
|
+
process.env.RIGKIT_CHECK_COUNT = String(count);
|
|
895
|
+
if (process.env.RIGKIT_FORCE_REAUTH === "1") {
|
|
896
|
+
process.env.RIGKIT_FORCE_REAUTH = "0";
|
|
897
|
+
return step.invalidate("github-auth");
|
|
898
|
+
}
|
|
899
|
+
return { ctx: step.ctx };
|
|
900
|
+
});
|
|
901
|
+
`,
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
try {
|
|
905
|
+
const engine = await createDevMachineEngine({ projectDir });
|
|
906
|
+
await engine.load();
|
|
907
|
+
|
|
908
|
+
const first = await engine.apply();
|
|
909
|
+
expect(first.context.token).toBe("token-1");
|
|
910
|
+
expect(process.env.RIGKIT_AUTH_COUNT).toBe("1");
|
|
911
|
+
expect(process.env.RIGKIT_CHECK_COUNT).toBe("1");
|
|
912
|
+
|
|
913
|
+
process.env.RIGKIT_FORCE_REAUTH = "1";
|
|
914
|
+
const second = await engine.apply();
|
|
915
|
+
expect(second.context.token).toBe("token-2");
|
|
916
|
+
expect(process.env.RIGKIT_AUTH_COUNT).toBe("2");
|
|
917
|
+
expect(process.env.RIGKIT_CHECK_COUNT).toBe("3");
|
|
918
|
+
|
|
919
|
+
const validRuns = engine.listNodeRuns().filter((run) => !run.invalidated);
|
|
920
|
+
expect(validRuns.map((run) => run.nodePath).sort()).toEqual(["check-auth", "github-auth", "prepare"]);
|
|
921
|
+
} finally {
|
|
922
|
+
restoreEnv("RIGKIT_AUTH_COUNT", previous.authCount);
|
|
923
|
+
restoreEnv("RIGKIT_CHECK_COUNT", previous.checkCount);
|
|
924
|
+
restoreEnv("RIGKIT_FORCE_REAUTH", previous.forceReauth);
|
|
925
|
+
}
|
|
926
|
+
});
|
|
770
927
|
});
|
|
771
928
|
|
|
772
929
|
type FakeSnapshotRef = {
|
|
@@ -879,6 +1036,14 @@ function result(ok: boolean): ExecResult {
|
|
|
879
1036
|
return { stdout: "", stderr: "", exitCode: ok ? 0 : 1, ok };
|
|
880
1037
|
}
|
|
881
1038
|
|
|
1039
|
+
function restoreEnv(name: string, value: string | undefined): void {
|
|
1040
|
+
if (value === undefined) {
|
|
1041
|
+
delete process.env[name];
|
|
1042
|
+
} else {
|
|
1043
|
+
process.env[name] = value;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
882
1047
|
function isFakeSnapshotRef(value: unknown): value is FakeSnapshotRef {
|
|
883
1048
|
return Boolean(
|
|
884
1049
|
value &&
|