@rigkit/engine 0.2.2 → 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 +108 -61
- package/src/authoring.typecheck.ts +87 -0
- package/src/db/schema/core.ts +3 -7
- package/src/engine.test.ts +270 -131
- package/src/engine.ts +566 -503
- package/src/host-storage.ts +128 -0
- package/src/index.ts +5 -0
- package/src/provider/types.ts +4 -30
- package/src/state.ts +46 -14
- package/src/types.ts +347 -140
- package/src/version.ts +1 -1
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";
|
|
@@ -9,10 +9,9 @@ import { createStateStore } from "./state.ts";
|
|
|
9
9
|
import type {
|
|
10
10
|
BaseProviderPlugin,
|
|
11
11
|
ProviderRuntimeContext,
|
|
12
|
-
SshConnection,
|
|
13
12
|
WorkflowProviderController,
|
|
14
13
|
} from "./provider/types.ts";
|
|
15
|
-
import type { DevMachineEvent, ExecResult, JsonValue
|
|
14
|
+
import type { DevMachineEvent, ExecResult, JsonValue } from "./types.ts";
|
|
16
15
|
|
|
17
16
|
describe("DevMachineEngine workflow runtime", () => {
|
|
18
17
|
test("plans, applies graph nodes, reuses graph cache, and forks workspaces", async () => {
|
|
@@ -28,68 +27,78 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
28
27
|
},
|
|
29
28
|
});
|
|
30
29
|
|
|
31
|
-
const base = app.sequence("base").task("first", async ({
|
|
32
|
-
|
|
30
|
+
const base = app.sequence("base").task("first", async ({ step, test }) => {
|
|
31
|
+
step.log("preparing base\\n", { label: "setup" });
|
|
33
32
|
const vm = await test.createVm();
|
|
34
33
|
await vm.exec("touch /tmp/first", { name: "touch first" });
|
|
35
34
|
if (!(await vm.exists("/tmp/first"))) throw new Error("first was not created");
|
|
36
|
-
return { first: true, vm: await vm.snapshotRef() };
|
|
35
|
+
return { ctx: { first: true, vm: await vm.snapshotRef() } };
|
|
37
36
|
});
|
|
38
37
|
|
|
39
|
-
const left = app.sequence("left").task("second", async ({
|
|
40
|
-
if (!ctx.first) throw new Error("missing first context");
|
|
41
|
-
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);
|
|
42
41
|
await vm.exec("touch /tmp/second", { name: "touch second" });
|
|
43
|
-
return { second: true, vm: await vm.snapshotRef() };
|
|
42
|
+
return { ctx: { second: true, vm: await vm.snapshotRef() } };
|
|
44
43
|
});
|
|
45
44
|
|
|
46
|
-
const right = app.sequence("right").task("data", async ({
|
|
47
|
-
if (!ctx.first) throw new Error("missing first context");
|
|
48
|
-
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" } };
|
|
49
48
|
});
|
|
50
49
|
|
|
51
50
|
export default app
|
|
52
51
|
.sequence("root")
|
|
53
52
|
.add(base)
|
|
54
53
|
.parallel({ left, right })
|
|
55
|
-
.task("join", async ({
|
|
56
|
-
if (!ctx.left.second) throw new Error("missing left context");
|
|
57
|
-
if (ctx.right.data !== "right-ready") throw new Error("missing right context");
|
|
58
|
-
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 } };
|
|
59
58
|
})
|
|
60
59
|
.workspace({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (ctx.summary !== "right-ready") throw new Error("missing final context");
|
|
65
|
-
if (providerContext.authority !== "fake-authority") throw new Error("missing provider context");
|
|
66
|
-
const vm = providers.test.fromWorkspace(workspace);
|
|
60
|
+
create: async ({ providers, workflow, workspace, local }) => {
|
|
61
|
+
if (workflow.ctx.summary !== "right-ready") throw new Error("missing final context");
|
|
62
|
+
const vm = await providers.test.fromSnapshot(workflow.ctx.vm);
|
|
67
63
|
await vm.exec("touch /tmp/workspace-" + workspace.name, { name: "mark workspace" });
|
|
68
|
-
await local.open("
|
|
64
|
+
await local.open("created://" + workspace.name);
|
|
65
|
+
return {
|
|
66
|
+
summary: workflow.ctx.summary,
|
|
67
|
+
repoPath: "/workspace/repo",
|
|
68
|
+
vmId: vm.vmId,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
remove: async ({ providers, workspace, workflow }) => {
|
|
72
|
+
if (workflow.ctx.summary !== "right-ready") throw new Error("missing final context on remove");
|
|
73
|
+
const vm = providers.test.fromId(workspace.ctx.vmId);
|
|
74
|
+
await vm.exec("touch /tmp/remove-" + workspace.name, { name: "mark workspace remove" });
|
|
69
75
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
})
|
|
77
|
+
.workspaceOperation("open", {
|
|
78
|
+
run: async ({ providers, workspace, workflow, local }) => {
|
|
79
|
+
if (workflow.ctx.summary !== "right-ready") throw new Error("missing final context on open");
|
|
80
|
+
if (workspace.ctx.summary !== "right-ready") throw new Error("missing workspace context");
|
|
81
|
+
const vm = providers.test.fromId(workspace.ctx.vmId);
|
|
74
82
|
await vm.exec("touch /tmp/open-" + workspace.name, { name: "mark workspace open" });
|
|
83
|
+
await local.open("open://" + workspace.name);
|
|
75
84
|
},
|
|
76
85
|
})
|
|
77
86
|
.operation("mark", {
|
|
78
|
-
requiredHostCapabilities: [{ id: "cmux.open", schemaHash: "sha256:cmux-open-schema" }],
|
|
79
87
|
input: (workflow) =>
|
|
80
88
|
workflow
|
|
81
89
|
.workspaceInput({ name: "workspace", position: 0 })
|
|
82
90
|
.extend({
|
|
83
91
|
label: workflow.string({ defaultValue: "marked" }),
|
|
84
|
-
|
|
92
|
+
}),
|
|
85
93
|
run: async ({ input, providers, local }) => {
|
|
86
|
-
const vm = providers.test.
|
|
94
|
+
const vm = providers.test.fromId(input.workspace.ctx.vmId);
|
|
87
95
|
await vm.exec("touch /tmp/mark-" + input.workspace.name, { name: "mark via operation" });
|
|
88
96
|
await local.open("mark://" + input.workspace.name);
|
|
89
97
|
return {
|
|
90
98
|
workspace: input.workspace.name,
|
|
91
99
|
label: input.label,
|
|
92
|
-
|
|
100
|
+
repoPath: input.workspace.ctx.repoPath,
|
|
101
|
+
summary: input.workspace.ctx.summary,
|
|
93
102
|
};
|
|
94
103
|
},
|
|
95
104
|
});
|
|
@@ -124,7 +133,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
124
133
|
]);
|
|
125
134
|
|
|
126
135
|
const applied = await engine.apply();
|
|
127
|
-
expect(applied.
|
|
136
|
+
expect(applied.context.vm).toEqual({ provider: "test", kind: "vmSnapshot", snapshotId: "snap-2" });
|
|
128
137
|
expect(events).toContainEqual({
|
|
129
138
|
type: "log.output",
|
|
130
139
|
nodePath: "base.first",
|
|
@@ -140,37 +149,33 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
140
149
|
expect(cached.finalContext?.summary).toBe("right-ready");
|
|
141
150
|
|
|
142
151
|
const workspace = await engine.fork({ name: "work" });
|
|
143
|
-
expect(workspace.snapshotId).toBe("snap-2");
|
|
144
152
|
expect(workspace.name).toBe("work");
|
|
145
|
-
expect(workspace.
|
|
153
|
+
expect(workspace.ctx).toMatchObject({
|
|
154
|
+
summary: "right-ready",
|
|
155
|
+
repoPath: "/workspace/repo",
|
|
156
|
+
vmId: "vm-3",
|
|
157
|
+
});
|
|
146
158
|
expect(engine.listWorkspaces()).toHaveLength(1);
|
|
147
|
-
expect(opened).toEqual(["
|
|
148
|
-
expect(provider.
|
|
149
|
-
expect(provider.hasFile("workspace-work", "/tmp/workspace-work")).toBe(true);
|
|
159
|
+
expect(opened).toEqual(["created://work"]);
|
|
160
|
+
expect(provider.hasFile("vm-3", "/tmp/workspace-work")).toBe(true);
|
|
150
161
|
|
|
151
|
-
const
|
|
152
|
-
expect(
|
|
153
|
-
{ id: "cmux.open", schemaHash: "sha256:cmux-open-schema" },
|
|
154
|
-
]);
|
|
162
|
+
const openOperation = engine.listRuntimeWorkspaceOperations().find((operation) => operation.id === "open");
|
|
163
|
+
expect(openOperation?.id).toBe("open");
|
|
155
164
|
const marked = await engine.runOperation({ operation: "mark", input: { workspace: "work" } });
|
|
156
|
-
expect(marked).toEqual({ workspace: "work", label: "marked",
|
|
157
|
-
expect(opened).toEqual(["
|
|
158
|
-
expect(provider.hasFile("
|
|
159
|
-
|
|
160
|
-
const terminal = await engine.attachTerminal({ workspaceOrVmId: "work", printOnly: true });
|
|
161
|
-
expect(terminal.command).toBe("ssh workspace-work");
|
|
162
|
-
expect(provider.workspaceContextResourceIds).toEqual(["workspace-work", "workspace-work"]);
|
|
163
|
-
expect(provider.hasFile("workspace-work", "/tmp/open-work")).toBe(true);
|
|
165
|
+
expect(marked).toEqual({ workspace: "work", label: "marked", repoPath: "/workspace/repo", summary: "right-ready" });
|
|
166
|
+
expect(opened).toEqual(["created://work", "mark://work"]);
|
|
167
|
+
expect(provider.hasFile("vm-3", "/tmp/mark-work")).toBe(true);
|
|
164
168
|
|
|
165
|
-
|
|
166
|
-
expect(
|
|
167
|
-
expect(
|
|
169
|
+
await engine.runRuntimeOperation({ operation: "work/open" });
|
|
170
|
+
expect(opened).toEqual(["created://work", "mark://work", "open://work"]);
|
|
171
|
+
expect(provider.hasFile("vm-3", "/tmp/open-work")).toBe(true);
|
|
168
172
|
|
|
169
|
-
await engine.
|
|
173
|
+
await engine.runRuntimeOperation({ operation: "work/remove" });
|
|
170
174
|
expect(engine.listWorkspaces()).toHaveLength(0);
|
|
175
|
+
expect(provider.hasFile("vm-3", "/tmp/remove-work")).toBe(true);
|
|
171
176
|
});
|
|
172
177
|
|
|
173
|
-
test("creates workspaces from
|
|
178
|
+
test("creates workspaces from workspace definitions and exposes persisted workspace context", async () => {
|
|
174
179
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
|
|
175
180
|
writeFileSync(
|
|
176
181
|
join(projectDir, "rig.config.ts"),
|
|
@@ -184,31 +189,42 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
184
189
|
const vm = await providers.test.createVm();
|
|
185
190
|
await vm.exec("touch /tmp/template", { name: "prepare template" });
|
|
186
191
|
return {
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
ctx: {
|
|
193
|
+
vm: await vm.snapshotRef(),
|
|
194
|
+
repoPath: "/workspace/repo",
|
|
195
|
+
},
|
|
189
196
|
};
|
|
190
197
|
})
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
.workspace({
|
|
199
|
+
create: async ({ workflow, workspace, providers }) => {
|
|
200
|
+
const vm = await providers.test.fromSnapshot(workflow.ctx.vm);
|
|
201
|
+
await vm.exec("touch /tmp/create-" + workspace.name, { name: "create workspace" });
|
|
202
|
+
return {
|
|
203
|
+
name: workspace.name,
|
|
204
|
+
vmId: vm.vmId,
|
|
205
|
+
sourceSnapshot: workflow.ctx.vm,
|
|
206
|
+
repoPath: workflow.ctx.repoPath,
|
|
207
|
+
ready: true,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
remove: async () => {},
|
|
201
211
|
})
|
|
202
212
|
.operation("inspect", {
|
|
203
213
|
input: (workflow) => workflow.workspaceInput({ name: "workspace", position: 0 }),
|
|
204
214
|
run: async ({ input, local }) => {
|
|
205
215
|
await local.open("created://" + input.workspace.name);
|
|
206
216
|
return {
|
|
207
|
-
vmId: input.workspace.
|
|
208
|
-
repoPath: input.workspace.
|
|
209
|
-
ready: input.workspace.
|
|
217
|
+
vmId: input.workspace.ctx.vmId,
|
|
218
|
+
repoPath: input.workspace.ctx.repoPath,
|
|
219
|
+
ready: input.workspace.ctx.ready,
|
|
210
220
|
};
|
|
211
221
|
},
|
|
222
|
+
})
|
|
223
|
+
.workspaceOperation("status", {
|
|
224
|
+
run: async ({ workspace }) => ({
|
|
225
|
+
workspace: workspace.name,
|
|
226
|
+
vmId: workspace.ctx.vmId,
|
|
227
|
+
}),
|
|
212
228
|
});
|
|
213
229
|
|
|
214
230
|
export default defineConfig({
|
|
@@ -238,9 +254,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
238
254
|
|
|
239
255
|
const workspace = await engine.fork({ name: "created" });
|
|
240
256
|
expect(workspace.name).toBe("created");
|
|
241
|
-
expect(workspace.
|
|
242
|
-
expect(workspace.resourceId).toBe("vm-2");
|
|
243
|
-
expect(workspace.metadata).toMatchObject({
|
|
257
|
+
expect(workspace.ctx).toMatchObject({
|
|
244
258
|
name: "created",
|
|
245
259
|
vmId: "vm-2",
|
|
246
260
|
repoPath: "/workspace/repo",
|
|
@@ -255,6 +269,9 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
255
269
|
ready: true,
|
|
256
270
|
});
|
|
257
271
|
expect(opened).toEqual(["created://created"]);
|
|
272
|
+
|
|
273
|
+
const status = await engine.runRuntimeOperation({ operation: "created/status" });
|
|
274
|
+
expect(status).toEqual({ workspace: "created", vmId: "vm-2" });
|
|
258
275
|
});
|
|
259
276
|
|
|
260
277
|
test("loads multiple workflows from defineConfig", async () => {
|
|
@@ -264,8 +281,8 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
264
281
|
`
|
|
265
282
|
import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
|
|
266
283
|
|
|
267
|
-
const api = sequence("api").step("ready", async () => ({ api: true }));
|
|
268
|
-
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 } }));
|
|
269
286
|
|
|
270
287
|
export default defineConfig({
|
|
271
288
|
providers: {},
|
|
@@ -295,7 +312,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
295
312
|
`
|
|
296
313
|
import { defineConfig, sequence } from "${import.meta.dir}/index.ts";
|
|
297
314
|
|
|
298
|
-
const root = sequence("factory-test").step("ready", async () => ({ ready: true }));
|
|
315
|
+
const root = sequence("factory-test").step("ready", async () => ({ ctx: { ready: true } }));
|
|
299
316
|
|
|
300
317
|
export default defineConfig({
|
|
301
318
|
providers: {},
|
|
@@ -391,20 +408,16 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
391
408
|
state.saveWorkspace({
|
|
392
409
|
id: "workspace-2",
|
|
393
410
|
name: "demo",
|
|
394
|
-
providerId: "freestyle",
|
|
395
411
|
workflow: "smoke",
|
|
396
|
-
|
|
397
|
-
snapshotId: "snap-2",
|
|
398
|
-
sourceRef: { snapshotId: "snap-2" },
|
|
399
|
-
context: { ready: true },
|
|
412
|
+
workflowCtx: { ready: true },
|
|
400
413
|
createdAt: now,
|
|
401
414
|
updatedAt: now,
|
|
402
|
-
|
|
415
|
+
ctx: { ready: true },
|
|
403
416
|
});
|
|
404
417
|
expect(state.getWorkspace("demo")).toMatchObject({
|
|
405
418
|
name: "demo",
|
|
406
419
|
workflow: "smoke",
|
|
407
|
-
|
|
420
|
+
ctx: { ready: true },
|
|
408
421
|
});
|
|
409
422
|
});
|
|
410
423
|
|
|
@@ -421,7 +434,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
421
434
|
const app = workflow("handler-cache", { providers: {} });
|
|
422
435
|
|
|
423
436
|
export default app.sequence("root").task("value", async () => {
|
|
424
|
-
return { value: "${value}" };
|
|
437
|
+
return { ctx: { value: "${value}" } };
|
|
425
438
|
});
|
|
426
439
|
`,
|
|
427
440
|
);
|
|
@@ -479,7 +492,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
479
492
|
},
|
|
480
493
|
});
|
|
481
494
|
|
|
482
|
-
export default app.sequence("root").task("ready", async () => ({ ready: true }));
|
|
495
|
+
export default app.sequence("root").task("ready", async () => ({ ctx: { ready: true } }));
|
|
483
496
|
`,
|
|
484
497
|
);
|
|
485
498
|
|
|
@@ -522,6 +535,73 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
522
535
|
expect(metadata["config.path"]).toBe(join(projectDir, "rig.config.ts"));
|
|
523
536
|
});
|
|
524
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
|
+
|
|
525
605
|
test("rejects task outputs that are not JSON serializable", async () => {
|
|
526
606
|
const projectDir = mkdtempSync(join(tmpdir(), "rigkit-"));
|
|
527
607
|
writeFileSync(
|
|
@@ -536,7 +616,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
536
616
|
});
|
|
537
617
|
|
|
538
618
|
export default app.sequence("bad").task("returns-function", async () => {
|
|
539
|
-
return { fn: () => "nope" };
|
|
619
|
+
return { ctx: { fn: () => "nope" } };
|
|
540
620
|
});
|
|
541
621
|
`,
|
|
542
622
|
);
|
|
@@ -565,7 +645,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
565
645
|
|
|
566
646
|
export default app.sequence("auth").task("login", async ({ test }) => {
|
|
567
647
|
const result = await test.openTerminal("GitHub auth", "gh auth login");
|
|
568
|
-
return { finished: result.finished };
|
|
648
|
+
return { ctx: { finished: result.finished } };
|
|
569
649
|
});
|
|
570
650
|
`,
|
|
571
651
|
);
|
|
@@ -611,7 +691,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
611
691
|
|
|
612
692
|
export default app.sequence("auth").task("login", async ({ test }) => {
|
|
613
693
|
const result = await test.openTerminal("GitHub auth", "gh auth login");
|
|
614
|
-
return { finished: result.finished };
|
|
694
|
+
return { ctx: { finished: result.finished } };
|
|
615
695
|
});
|
|
616
696
|
`,
|
|
617
697
|
);
|
|
@@ -665,7 +745,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
665
745
|
export default app.sequence("setup").task("touch", async ({ test }) => {
|
|
666
746
|
const vm = await test.createVm();
|
|
667
747
|
await vm.exec("touch /tmp/setup", { name: "touch setup" });
|
|
668
|
-
return { vm: await vm.snapshotRef() };
|
|
748
|
+
return { ctx: { vm: await vm.snapshotRef() } };
|
|
669
749
|
});
|
|
670
750
|
`,
|
|
671
751
|
);
|
|
@@ -723,8 +803,8 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
723
803
|
|
|
724
804
|
export default app.sequence("schema").task("value", { output: schema }, async () => {
|
|
725
805
|
return process.env.RIGKIT_SCHEMA_MODE === "next"
|
|
726
|
-
? { value: "ok", next: true }
|
|
727
|
-
: { value: "ok" };
|
|
806
|
+
? { ctx: { value: "ok", next: true } }
|
|
807
|
+
: { ctx: { value: "ok" } };
|
|
728
808
|
});
|
|
729
809
|
`,
|
|
730
810
|
);
|
|
@@ -756,6 +836,94 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
756
836
|
}
|
|
757
837
|
}
|
|
758
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
|
+
});
|
|
759
927
|
});
|
|
760
928
|
|
|
761
929
|
type FakeSnapshotRef = {
|
|
@@ -775,14 +943,13 @@ type FakeVm = {
|
|
|
775
943
|
type FakeRuntime = {
|
|
776
944
|
createVm(): Promise<FakeVm>;
|
|
777
945
|
fromSnapshot(ref: FakeSnapshotRef): Promise<FakeVm>;
|
|
778
|
-
|
|
946
|
+
fromId(vmId: string): FakeVm;
|
|
779
947
|
openTerminal(label: string, command: string): Promise<{ finished: true }>;
|
|
780
948
|
};
|
|
781
949
|
|
|
782
|
-
class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime
|
|
950
|
+
class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime> {
|
|
783
951
|
readonly providerId = "test";
|
|
784
952
|
snapshots: FakeSnapshotRef[] = [];
|
|
785
|
-
workspaceContextResourceIds: string[] = [];
|
|
786
953
|
private nextVm = 1;
|
|
787
954
|
private files = new Map<string, Set<string>>();
|
|
788
955
|
terminalStopped = 0;
|
|
@@ -797,7 +964,7 @@ class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime, {
|
|
|
797
964
|
return {
|
|
798
965
|
createVm: async () => this.createVm(context),
|
|
799
966
|
fromSnapshot: async () => this.createVm(context),
|
|
800
|
-
|
|
967
|
+
fromId: (vmId) => this.vmRuntime({ vmId }, context),
|
|
801
968
|
openTerminal: async (label, command) => {
|
|
802
969
|
const completed = this.options.terminalCompleted ?? Promise.resolve({ finished: true as const });
|
|
803
970
|
return await context.interaction.present({
|
|
@@ -817,42 +984,6 @@ class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime, {
|
|
|
817
984
|
return isFakeSnapshotRef(ref);
|
|
818
985
|
}
|
|
819
986
|
|
|
820
|
-
workspace = {
|
|
821
|
-
canUse: (ref: JsonValue) => isFakeSnapshotRef(ref),
|
|
822
|
-
createWorkspace: async (ref: JsonValue, input: { name: string }) => {
|
|
823
|
-
if (!isFakeSnapshotRef(ref)) throw new Error("bad ref");
|
|
824
|
-
const resourceId = `workspace-${input.name}`;
|
|
825
|
-
this.files.set(resourceId, new Set());
|
|
826
|
-
return {
|
|
827
|
-
providerId: "test",
|
|
828
|
-
resourceId,
|
|
829
|
-
snapshotId: ref.snapshotId,
|
|
830
|
-
sourceRef: ref,
|
|
831
|
-
};
|
|
832
|
-
},
|
|
833
|
-
deleteWorkspace: async () => {},
|
|
834
|
-
snapshotWorkspace: async (workspace: WorkspaceRecord) => {
|
|
835
|
-
const ref = this.createSnapshot({ vmId: workspace.resourceId });
|
|
836
|
-
return {
|
|
837
|
-
providerId: "test",
|
|
838
|
-
resourceId: workspace.resourceId,
|
|
839
|
-
snapshotId: ref.snapshotId,
|
|
840
|
-
sourceRef: ref,
|
|
841
|
-
};
|
|
842
|
-
},
|
|
843
|
-
ssh: async (workspaceOrResourceId: string): Promise<SshConnection> => ({
|
|
844
|
-
kind: "ssh",
|
|
845
|
-
host: "fake",
|
|
846
|
-
username: workspaceOrResourceId,
|
|
847
|
-
auth: { type: "token", token: "fake" },
|
|
848
|
-
command: `ssh ${workspaceOrResourceId}`,
|
|
849
|
-
}),
|
|
850
|
-
workspaceContext: (workspace: WorkspaceRecord) => {
|
|
851
|
-
this.workspaceContextResourceIds.push(workspace.resourceId);
|
|
852
|
-
return { authority: "fake-authority" };
|
|
853
|
-
},
|
|
854
|
-
};
|
|
855
|
-
|
|
856
987
|
hasFile(vmId: string, path: string): boolean {
|
|
857
988
|
return this.files.get(vmId)?.has(path) ?? false;
|
|
858
989
|
}
|
|
@@ -905,6 +1036,14 @@ function result(ok: boolean): ExecResult {
|
|
|
905
1036
|
return { stdout: "", stderr: "", exitCode: ok ? 0 : 1, ok };
|
|
906
1037
|
}
|
|
907
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
|
+
|
|
908
1047
|
function isFakeSnapshotRef(value: unknown): value is FakeSnapshotRef {
|
|
909
1048
|
return Boolean(
|
|
910
1049
|
value &&
|