@rigkit/engine 0.2.2 → 0.2.3
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 +82 -50
- package/src/authoring.typecheck.ts +67 -0
- package/src/db/schema/core.ts +3 -7
- package/src/engine.test.ts +77 -103
- package/src/engine.ts +285 -474
- package/src/provider/types.ts +2 -30
- package/src/state.ts +6 -14
- package/src/types.ts +223 -109
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/authoring.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RESERVED_WORKFLOW_OPERATION_IDS } from "./types.ts";
|
|
1
2
|
import type {
|
|
2
3
|
EnvResolver,
|
|
3
4
|
RigkitConfigDefinition,
|
|
@@ -5,14 +6,14 @@ import type {
|
|
|
5
6
|
OutputSchema,
|
|
6
7
|
OutputSchemaValue,
|
|
7
8
|
WorkflowDefinition,
|
|
8
|
-
WorkflowCreateDefinition,
|
|
9
|
-
WorkflowCreateHandler,
|
|
10
9
|
WorkflowInputFieldDefinition,
|
|
11
10
|
WorkflowInputShape,
|
|
12
11
|
WorkflowOperationDefinition,
|
|
13
12
|
WorkflowOperationInputBuilder,
|
|
14
13
|
WorkflowOperationInputHelpers,
|
|
15
14
|
WorkflowOperationOptions,
|
|
15
|
+
WorkflowWorkspaceOperationDefinition,
|
|
16
|
+
WorkflowWorkspaceOperationOptions,
|
|
16
17
|
WorkflowNodeDefinition,
|
|
17
18
|
WorkflowProviderDefinition,
|
|
18
19
|
WorkflowProviderMap,
|
|
@@ -25,7 +26,7 @@ import type {
|
|
|
25
26
|
} from "./types.ts";
|
|
26
27
|
|
|
27
28
|
const reservedTaskContextKeys = new Set(["ctx", "runtime", "providers"]);
|
|
28
|
-
const reservedHostOperationIds = new Set(
|
|
29
|
+
const reservedHostOperationIds = new Set<string>(RESERVED_WORKFLOW_OPERATION_IDS);
|
|
29
30
|
|
|
30
31
|
const readEnv = (name: string, fallback?: string): string => {
|
|
31
32
|
const value = process.env[name];
|
|
@@ -90,12 +91,11 @@ export function defineProvider<
|
|
|
90
91
|
const ProviderId extends string,
|
|
91
92
|
const Config extends object,
|
|
92
93
|
Runtime = unknown,
|
|
93
|
-
WorkspaceContext extends object = object,
|
|
94
94
|
>(
|
|
95
95
|
providerId: ProviderId,
|
|
96
|
-
config: WorkflowProviderDefinition<ProviderId, Config, Runtime
|
|
96
|
+
config: WorkflowProviderDefinition<ProviderId, Config, Runtime>["config"],
|
|
97
97
|
plugin?: unknown,
|
|
98
|
-
): WorkflowProviderDefinition<ProviderId, Config, Runtime
|
|
98
|
+
): WorkflowProviderDefinition<ProviderId, Config, Runtime> {
|
|
99
99
|
return {
|
|
100
100
|
kind: "rigkit.provider",
|
|
101
101
|
providerId,
|
|
@@ -120,14 +120,28 @@ export function isProviderDefinition(value: unknown): value is WorkflowProviderD
|
|
|
120
120
|
return Boolean(value && typeof value === "object" && getKind(value) === "rigkit.provider");
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
function createSequence<
|
|
123
|
+
function createSequence<
|
|
124
|
+
Providers extends WorkflowProviderMap,
|
|
125
|
+
InputContext extends JsonObject,
|
|
126
|
+
OutputContext extends JsonObject,
|
|
127
|
+
WorkspaceData extends object = JsonObject,
|
|
128
|
+
OperationIds extends string = never,
|
|
129
|
+
WorkspaceOperationIds extends string = never,
|
|
130
|
+
>(
|
|
124
131
|
app: WorkflowDefinition<string, Providers>,
|
|
125
132
|
name: string,
|
|
126
133
|
children: readonly WorkflowNodeDefinition<Providers, any, any>[],
|
|
127
|
-
workspace?: WorkflowWorkspaceDefinition<Providers, OutputContext>,
|
|
128
|
-
create?: WorkflowCreateDefinition<Providers, OutputContext>,
|
|
134
|
+
workspace?: WorkflowWorkspaceDefinition<Providers, OutputContext, any>,
|
|
129
135
|
operations: readonly WorkflowOperationDefinition<Providers, any>[] = [],
|
|
130
|
-
|
|
136
|
+
workspaceOperations: readonly WorkflowWorkspaceOperationDefinition<Providers, OutputContext, WorkspaceData, any>[] = [],
|
|
137
|
+
): WorkflowSequenceBuilder<
|
|
138
|
+
Providers,
|
|
139
|
+
InputContext,
|
|
140
|
+
OutputContext,
|
|
141
|
+
WorkspaceData,
|
|
142
|
+
OperationIds,
|
|
143
|
+
WorkspaceOperationIds
|
|
144
|
+
> {
|
|
131
145
|
const node = {
|
|
132
146
|
kind: "rigkit.workflow-node" as const,
|
|
133
147
|
nodeKind: "sequence" as const,
|
|
@@ -135,15 +149,15 @@ function createSequence<Providers extends WorkflowProviderMap, InputContext exte
|
|
|
135
149
|
workflow: app,
|
|
136
150
|
children,
|
|
137
151
|
workspaceDefinition: workspace,
|
|
138
|
-
createDefinition: create,
|
|
139
152
|
operations,
|
|
153
|
+
workspaceOperations,
|
|
140
154
|
task: (
|
|
141
155
|
taskName: string,
|
|
142
156
|
optionsOrHandler: WorkflowTaskOptions | WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
143
157
|
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
144
158
|
) => {
|
|
145
159
|
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
146
|
-
return createSequence(app, name, [...children, task], workspace,
|
|
160
|
+
return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
|
|
147
161
|
},
|
|
148
162
|
step: (
|
|
149
163
|
taskName: string,
|
|
@@ -151,11 +165,11 @@ function createSequence<Providers extends WorkflowProviderMap, InputContext exte
|
|
|
151
165
|
maybeHandler?: WorkflowTaskHandler<Providers, OutputContext, any>,
|
|
152
166
|
) => {
|
|
153
167
|
const task = createTask(app, taskName, optionsOrHandler as any, maybeHandler as any);
|
|
154
|
-
return createSequence(app, name, [...children, task], workspace,
|
|
168
|
+
return createSequence(app, name, [...children, task], workspace, operations, workspaceOperations);
|
|
155
169
|
},
|
|
156
170
|
add: (child: WorkflowNodeDefinition<Providers, any, any>) => {
|
|
157
171
|
assertSameWorkflow(app, child);
|
|
158
|
-
return createSequence(app, name, [...children, child], workspace,
|
|
172
|
+
return createSequence(app, name, [...children, child], workspace, operations, workspaceOperations);
|
|
159
173
|
},
|
|
160
174
|
parallel: (branches: Record<string, WorkflowNodeDefinition<Providers, any, any>>) => {
|
|
161
175
|
for (const [branchName, branch] of Object.entries(branches)) {
|
|
@@ -173,17 +187,39 @@ function createSequence<Providers extends WorkflowProviderMap, InputContext exte
|
|
|
173
187
|
workflow: app,
|
|
174
188
|
branches,
|
|
175
189
|
};
|
|
176
|
-
return createSequence(app, name, [...children, parallelNode], workspace,
|
|
190
|
+
return createSequence(app, name, [...children, parallelNode], workspace, operations, workspaceOperations);
|
|
191
|
+
},
|
|
192
|
+
workspace: (definition: WorkflowWorkspaceDefinition<Providers, OutputContext, any>) =>
|
|
193
|
+
createSequence(app, name, children, definition, operations, workspaceOperations),
|
|
194
|
+
operation: (id: string, options: WorkflowOperationOptions<Providers, any>) => {
|
|
195
|
+
const operation = createOperation(id, options);
|
|
196
|
+
assertUniqueOperationId(operations, operation.id, "Operation");
|
|
197
|
+
return createSequence(app, name, children, workspace, [...operations, operation], workspaceOperations);
|
|
198
|
+
},
|
|
199
|
+
workspaceOperation: (id: string, options: WorkflowWorkspaceOperationOptions<Providers, OutputContext, any, any>) => {
|
|
200
|
+
const operation = createWorkspaceOperation(id, options);
|
|
201
|
+
assertUniqueOperationId(workspaceOperations, operation.id, "Workspace operation");
|
|
202
|
+
return createSequence(app, name, children, workspace, operations, [...workspaceOperations, operation]);
|
|
177
203
|
},
|
|
178
|
-
workspace: (definition: WorkflowWorkspaceDefinition<Providers, OutputContext>) =>
|
|
179
|
-
createSequence(app, name, children, definition, create, operations),
|
|
180
|
-
create: (handler: WorkflowCreateHandler<Providers, OutputContext, any>) =>
|
|
181
|
-
createSequence(app, name, children, workspace, { handler }, operations),
|
|
182
|
-
operation: (id: string, options: WorkflowOperationOptions<Providers, any>) =>
|
|
183
|
-
createSequence(app, name, children, workspace, create, [...operations, createOperation(id, options)]),
|
|
184
204
|
};
|
|
185
205
|
|
|
186
|
-
return node as unknown as WorkflowSequenceBuilder<
|
|
206
|
+
return node as unknown as WorkflowSequenceBuilder<
|
|
207
|
+
Providers,
|
|
208
|
+
InputContext,
|
|
209
|
+
OutputContext,
|
|
210
|
+
WorkspaceData,
|
|
211
|
+
OperationIds,
|
|
212
|
+
WorkspaceOperationIds
|
|
213
|
+
>;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function assertUniqueOperationId(
|
|
217
|
+
operations: readonly { id: string }[],
|
|
218
|
+
id: string,
|
|
219
|
+
label: string,
|
|
220
|
+
): void {
|
|
221
|
+
if (!operations.some((operation) => operation.id === id)) return;
|
|
222
|
+
throw new Error(`${label} id ${id} is already defined`);
|
|
187
223
|
}
|
|
188
224
|
|
|
189
225
|
function createOperation<Providers extends WorkflowProviderMap, Input extends object>(
|
|
@@ -199,9 +235,6 @@ function createOperation<Providers extends WorkflowProviderMap, Input extends ob
|
|
|
199
235
|
id: normalized,
|
|
200
236
|
title: options.title,
|
|
201
237
|
description: options.description,
|
|
202
|
-
createsWorkspace: options.createsWorkspace,
|
|
203
|
-
requiredHostMethods: normalizeHostMethodRequirements(options.requiredHostMethods),
|
|
204
|
-
requiredHostCapabilities: normalizeHostCapabilityRequirements(options.requiredHostCapabilities),
|
|
205
238
|
input: typeof options.input === "function"
|
|
206
239
|
? options.input(createOperationInputHelpers())
|
|
207
240
|
: options.input,
|
|
@@ -209,31 +242,30 @@ function createOperation<Providers extends WorkflowProviderMap, Input extends ob
|
|
|
209
242
|
};
|
|
210
243
|
}
|
|
211
244
|
|
|
212
|
-
function
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
});
|
|
245
|
+
function createWorkspaceOperation<
|
|
246
|
+
Providers extends WorkflowProviderMap,
|
|
247
|
+
Context extends JsonObject,
|
|
248
|
+
Data extends JsonObject,
|
|
249
|
+
Input extends object,
|
|
250
|
+
>(
|
|
251
|
+
id: string,
|
|
252
|
+
options: WorkflowWorkspaceOperationOptions<Providers, Context, Data, Input>,
|
|
253
|
+
): WorkflowWorkspaceOperationDefinition<Providers, Context, Data, Input> {
|
|
254
|
+
const normalized = id.trim();
|
|
255
|
+
if (!normalized) throw new Error(`Workspace operation ids must be non-empty`);
|
|
256
|
+
if (normalized.includes("/")) throw new Error(`Workspace operation ids cannot contain "/"`);
|
|
257
|
+
if (reservedHostOperationIds.has(normalized)) {
|
|
258
|
+
throw new Error(`Workspace operation id ${normalized} is reserved by the Rigkit host`);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
id: normalized,
|
|
262
|
+
title: options.title,
|
|
263
|
+
description: options.description,
|
|
264
|
+
input: typeof options.input === "function"
|
|
265
|
+
? options.input(createOperationInputHelpers())
|
|
266
|
+
: options.input,
|
|
267
|
+
run: options.run,
|
|
268
|
+
};
|
|
237
269
|
}
|
|
238
270
|
|
|
239
271
|
function createOperationInputHelpers(): WorkflowOperationInputHelpers {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { sequence } from "./authoring.ts";
|
|
2
|
+
|
|
3
|
+
sequence("normal-operation-ids")
|
|
4
|
+
.operation("open" as const, {
|
|
5
|
+
run: async () => null,
|
|
6
|
+
})
|
|
7
|
+
// @ts-expect-error duplicate operation ids are rejected for literal ids
|
|
8
|
+
.operation("open" as const, {
|
|
9
|
+
run: async () => null,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
sequence("workspace-operation-ids")
|
|
13
|
+
.step("prepare", async () => ({ snapshotId: "snap-1" }))
|
|
14
|
+
.workspace({
|
|
15
|
+
create: async ({ workflow, workspace }) => {
|
|
16
|
+
const snapshotId: string = workflow.ctx.snapshotId;
|
|
17
|
+
const name: string = workspace.name;
|
|
18
|
+
void snapshotId;
|
|
19
|
+
void name;
|
|
20
|
+
return { vmId: "vm-1", test: "ok" };
|
|
21
|
+
},
|
|
22
|
+
remove: async ({ workspace }) => {
|
|
23
|
+
const vmId: string = workspace.ctx.vmId;
|
|
24
|
+
const test: string = workspace.ctx.test;
|
|
25
|
+
void vmId;
|
|
26
|
+
void test;
|
|
27
|
+
// @ts-expect-error workspace context is read-only
|
|
28
|
+
workspace.ctx.vmId = "next";
|
|
29
|
+
// @ts-expect-error provider resources are not part of the workspace authoring API
|
|
30
|
+
workspace.resources;
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
.workspaceOperation("open-cmux" as const, {
|
|
34
|
+
run: async ({ workspace }) => {
|
|
35
|
+
const vmId: string = workspace.ctx.vmId;
|
|
36
|
+
void vmId;
|
|
37
|
+
// @ts-expect-error missing data properties are rejected
|
|
38
|
+
workspace.ctx.missing;
|
|
39
|
+
return null;
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
// @ts-expect-error duplicate workspace operation ids are rejected for literal ids
|
|
43
|
+
.workspaceOperation("open-cmux" as const, {
|
|
44
|
+
run: async () => null,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
sequence("reserved-operation-id")
|
|
48
|
+
// @ts-expect-error reserved operation ids are rejected for literal ids
|
|
49
|
+
.operation("create" as const, {
|
|
50
|
+
run: async () => null,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
sequence("reserved-workspace-operation-id")
|
|
54
|
+
.workspace({
|
|
55
|
+
create: async () => ({}),
|
|
56
|
+
remove: async () => {},
|
|
57
|
+
})
|
|
58
|
+
// @ts-expect-error reserved workspace operation ids are rejected for literal ids
|
|
59
|
+
.workspaceOperation("remove" as const, {
|
|
60
|
+
run: async () => null,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
sequence("slash-operation-id")
|
|
64
|
+
// @ts-expect-error operation ids cannot contain slashes
|
|
65
|
+
.operation("workspace/open" as const, {
|
|
66
|
+
run: async () => null,
|
|
67
|
+
});
|
package/src/db/schema/core.ts
CHANGED
|
@@ -6,19 +6,15 @@ export const workspaces = sqliteTable(
|
|
|
6
6
|
{
|
|
7
7
|
id: text("id").primaryKey(),
|
|
8
8
|
name: text("name").notNull(),
|
|
9
|
-
providerId: text("provider_id").notNull(),
|
|
10
9
|
workflow: text("workflow").notNull(),
|
|
11
|
-
|
|
12
|
-
snapshotId: text("snapshot_id"),
|
|
13
|
-
sourceRef: text("source_ref_json", { mode: "json" }).$type<JsonValue>().notNull(),
|
|
14
|
-
context: text("context_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
10
|
+
workflowCtx: text("workflow_ctx_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
15
11
|
createdAt: text("created_at").notNull(),
|
|
16
12
|
updatedAt: text("updated_at").notNull(),
|
|
17
|
-
|
|
13
|
+
ctx: text("ctx_json", { mode: "json" }).$type<Record<string, JsonValue>>().notNull(),
|
|
18
14
|
},
|
|
19
15
|
(table) => [
|
|
20
16
|
uniqueIndex("workspaces_name_idx").on(table.name),
|
|
21
|
-
index("
|
|
17
|
+
index("workspaces_workflow_idx").on(table.workflow),
|
|
22
18
|
],
|
|
23
19
|
);
|
|
24
20
|
|
package/src/engine.test.ts
CHANGED
|
@@ -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 () => {
|
|
@@ -58,38 +57,48 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
58
57
|
return { vm: ctx.left.vm, summary: 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("
|
|
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);
|
|
159
168
|
|
|
160
|
-
|
|
161
|
-
expect(
|
|
162
|
-
expect(provider.
|
|
163
|
-
expect(provider.hasFile("workspace-work", "/tmp/open-work")).toBe(true);
|
|
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);
|
|
164
172
|
|
|
165
|
-
|
|
166
|
-
expect(workspaceSnapshot.metadata.snapshotId).toBe("snap-3");
|
|
167
|
-
expect(workspaceSnapshot.nodeName).toBe("verified-work");
|
|
168
|
-
|
|
169
|
-
await engine.deleteWorkspace({ workspace: "work" });
|
|
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"),
|
|
@@ -188,27 +193,36 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
188
193
|
repoPath: "/workspace/repo",
|
|
189
194
|
};
|
|
190
195
|
})
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
.workspace({
|
|
197
|
+
create: async ({ workflow, workspace, providers }) => {
|
|
198
|
+
const vm = await providers.test.fromSnapshot(workflow.ctx.vm);
|
|
199
|
+
await vm.exec("touch /tmp/create-" + workspace.name, { name: "create workspace" });
|
|
200
|
+
return {
|
|
201
|
+
name: workspace.name,
|
|
202
|
+
vmId: vm.vmId,
|
|
203
|
+
sourceSnapshot: workflow.ctx.vm,
|
|
204
|
+
repoPath: workflow.ctx.repoPath,
|
|
205
|
+
ready: true,
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
remove: async () => {},
|
|
201
209
|
})
|
|
202
210
|
.operation("inspect", {
|
|
203
211
|
input: (workflow) => workflow.workspaceInput({ name: "workspace", position: 0 }),
|
|
204
212
|
run: async ({ input, local }) => {
|
|
205
213
|
await local.open("created://" + input.workspace.name);
|
|
206
214
|
return {
|
|
207
|
-
vmId: input.workspace.
|
|
208
|
-
repoPath: input.workspace.
|
|
209
|
-
ready: input.workspace.
|
|
215
|
+
vmId: input.workspace.ctx.vmId,
|
|
216
|
+
repoPath: input.workspace.ctx.repoPath,
|
|
217
|
+
ready: input.workspace.ctx.ready,
|
|
210
218
|
};
|
|
211
219
|
},
|
|
220
|
+
})
|
|
221
|
+
.workspaceOperation("status", {
|
|
222
|
+
run: async ({ workspace }) => ({
|
|
223
|
+
workspace: workspace.name,
|
|
224
|
+
vmId: workspace.ctx.vmId,
|
|
225
|
+
}),
|
|
212
226
|
});
|
|
213
227
|
|
|
214
228
|
export default defineConfig({
|
|
@@ -238,9 +252,7 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
238
252
|
|
|
239
253
|
const workspace = await engine.fork({ name: "created" });
|
|
240
254
|
expect(workspace.name).toBe("created");
|
|
241
|
-
expect(workspace.
|
|
242
|
-
expect(workspace.resourceId).toBe("vm-2");
|
|
243
|
-
expect(workspace.metadata).toMatchObject({
|
|
255
|
+
expect(workspace.ctx).toMatchObject({
|
|
244
256
|
name: "created",
|
|
245
257
|
vmId: "vm-2",
|
|
246
258
|
repoPath: "/workspace/repo",
|
|
@@ -255,6 +267,9 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
255
267
|
ready: true,
|
|
256
268
|
});
|
|
257
269
|
expect(opened).toEqual(["created://created"]);
|
|
270
|
+
|
|
271
|
+
const status = await engine.runRuntimeOperation({ operation: "created/status" });
|
|
272
|
+
expect(status).toEqual({ workspace: "created", vmId: "vm-2" });
|
|
258
273
|
});
|
|
259
274
|
|
|
260
275
|
test("loads multiple workflows from defineConfig", async () => {
|
|
@@ -391,20 +406,16 @@ describe("DevMachineEngine workflow runtime", () => {
|
|
|
391
406
|
state.saveWorkspace({
|
|
392
407
|
id: "workspace-2",
|
|
393
408
|
name: "demo",
|
|
394
|
-
providerId: "freestyle",
|
|
395
409
|
workflow: "smoke",
|
|
396
|
-
|
|
397
|
-
snapshotId: "snap-2",
|
|
398
|
-
sourceRef: { snapshotId: "snap-2" },
|
|
399
|
-
context: { ready: true },
|
|
410
|
+
workflowCtx: { ready: true },
|
|
400
411
|
createdAt: now,
|
|
401
412
|
updatedAt: now,
|
|
402
|
-
|
|
413
|
+
ctx: { ready: true },
|
|
403
414
|
});
|
|
404
415
|
expect(state.getWorkspace("demo")).toMatchObject({
|
|
405
416
|
name: "demo",
|
|
406
417
|
workflow: "smoke",
|
|
407
|
-
|
|
418
|
+
ctx: { ready: true },
|
|
408
419
|
});
|
|
409
420
|
});
|
|
410
421
|
|
|
@@ -775,14 +786,13 @@ type FakeVm = {
|
|
|
775
786
|
type FakeRuntime = {
|
|
776
787
|
createVm(): Promise<FakeVm>;
|
|
777
788
|
fromSnapshot(ref: FakeSnapshotRef): Promise<FakeVm>;
|
|
778
|
-
|
|
789
|
+
fromId(vmId: string): FakeVm;
|
|
779
790
|
openTerminal(label: string, command: string): Promise<{ finished: true }>;
|
|
780
791
|
};
|
|
781
792
|
|
|
782
|
-
class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime
|
|
793
|
+
class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime> {
|
|
783
794
|
readonly providerId = "test";
|
|
784
795
|
snapshots: FakeSnapshotRef[] = [];
|
|
785
|
-
workspaceContextResourceIds: string[] = [];
|
|
786
796
|
private nextVm = 1;
|
|
787
797
|
private files = new Map<string, Set<string>>();
|
|
788
798
|
terminalStopped = 0;
|
|
@@ -797,7 +807,7 @@ class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime, {
|
|
|
797
807
|
return {
|
|
798
808
|
createVm: async () => this.createVm(context),
|
|
799
809
|
fromSnapshot: async () => this.createVm(context),
|
|
800
|
-
|
|
810
|
+
fromId: (vmId) => this.vmRuntime({ vmId }, context),
|
|
801
811
|
openTerminal: async (label, command) => {
|
|
802
812
|
const completed = this.options.terminalCompleted ?? Promise.resolve({ finished: true as const });
|
|
803
813
|
return await context.interaction.present({
|
|
@@ -817,42 +827,6 @@ class FakeWorkflowProvider implements WorkflowProviderController<FakeRuntime, {
|
|
|
817
827
|
return isFakeSnapshotRef(ref);
|
|
818
828
|
}
|
|
819
829
|
|
|
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
830
|
hasFile(vmId: string, path: string): boolean {
|
|
857
831
|
return this.files.get(vmId)?.has(path) ?? false;
|
|
858
832
|
}
|