@gencow/core 0.1.24 → 0.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/crud.d.ts +2 -2
- package/dist/crud.js +225 -208
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -2
- package/dist/reactive.js +10 -3
- package/dist/retry.js +1 -1
- package/dist/rls-db.d.ts +2 -2
- package/dist/rls-db.js +1 -5
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +16 -6
- package/dist/server.d.ts +0 -1
- package/dist/server.js +0 -1
- package/dist/storage.js +29 -22
- package/dist/v.d.ts +2 -2
- package/dist/workflow.js +4 -11
- package/dist/workflows-api.js +5 -12
- package/package.json +46 -42
- package/src/__tests__/auth.test.ts +90 -86
- package/src/__tests__/crons.test.ts +69 -67
- package/src/__tests__/crud-codegen-integration.test.ts +164 -170
- package/src/__tests__/crud-owner-rls.test.ts +308 -301
- package/src/__tests__/crud.test.ts +694 -711
- package/src/__tests__/dist-exports.test.ts +120 -120
- package/src/__tests__/fixtures/basic/auth.ts +16 -16
- package/src/__tests__/fixtures/basic/drizzle.config.ts +1 -4
- package/src/__tests__/fixtures/basic/index.ts +1 -1
- package/src/__tests__/fixtures/basic/schema.ts +1 -1
- package/src/__tests__/fixtures/basic/tasks.ts +4 -4
- package/src/__tests__/fixtures/common/auth-schema.ts +38 -34
- package/src/__tests__/helpers/basic-rls-fixture.ts +80 -78
- package/src/__tests__/helpers/pglite-migrations.ts +2 -5
- package/src/__tests__/helpers/pglite-rls-session.ts +13 -16
- package/src/__tests__/helpers/seed-like-fill.ts +50 -44
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +4 -7
- package/src/__tests__/httpaction.test.ts +91 -91
- package/src/__tests__/image-optimization.test.ts +570 -574
- package/src/__tests__/load.test.ts +321 -308
- package/src/__tests__/network-sim.test.ts +238 -215
- package/src/__tests__/reactive.test.ts +380 -358
- package/src/__tests__/retry.test.ts +99 -84
- package/src/__tests__/rls-crud-basic.test.ts +172 -245
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +81 -81
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +47 -94
- package/src/__tests__/rls-custom-query-handlers.test.ts +92 -92
- package/src/__tests__/rls-db-leased-connection.test.ts +2 -6
- package/src/__tests__/rls-session-and-policies.test.ts +181 -199
- package/src/__tests__/scheduler-durable-v2.test.ts +199 -181
- package/src/__tests__/scheduler-durable.test.ts +117 -117
- package/src/__tests__/scheduler-exec.test.ts +258 -246
- package/src/__tests__/scheduler.test.ts +129 -111
- package/src/__tests__/storage.test.ts +282 -269
- package/src/__tests__/tsconfig.json +6 -6
- package/src/__tests__/validator.test.ts +236 -232
- package/src/__tests__/workflow.test.ts +309 -286
- package/src/__tests__/ws-integration.test.ts +223 -218
- package/src/__tests__/ws-scale.test.ts +168 -159
- package/src/auth-config.ts +18 -18
- package/src/auth.ts +106 -106
- package/src/crons.ts +77 -77
- package/src/crud.ts +523 -479
- package/src/index.ts +69 -5
- package/src/reactive.ts +357 -331
- package/src/retry.ts +51 -54
- package/src/rls-db.ts +195 -205
- package/src/rls.ts +33 -36
- package/src/scheduler.ts +237 -211
- package/src/server.ts +0 -1
- package/src/storage.ts +632 -593
- package/src/v.ts +119 -114
- package/src/workflow-types.ts +67 -70
- package/src/workflow.ts +99 -116
- package/src/workflows-api.ts +231 -241
- package/src/db.ts +0 -18
|
@@ -8,7 +8,7 @@ import { v } from "../v.js";
|
|
|
8
8
|
import { registerWorkflowsApi } from "../workflows-api.js";
|
|
9
9
|
|
|
10
10
|
const WORKFLOW_TABLE_SETUP = [
|
|
11
|
-
|
|
11
|
+
`CREATE TABLE IF NOT EXISTS _gencow_workflows (
|
|
12
12
|
id TEXT PRIMARY KEY,
|
|
13
13
|
name TEXT NOT NULL,
|
|
14
14
|
args JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
@@ -25,7 +25,7 @@ const WORKFLOW_TABLE_SETUP = [
|
|
|
25
25
|
completed_at TIMESTAMPTZ,
|
|
26
26
|
user_id TEXT
|
|
27
27
|
)`,
|
|
28
|
-
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS _gencow_workflow_steps (
|
|
29
29
|
id SERIAL PRIMARY KEY,
|
|
30
30
|
workflow_id TEXT NOT NULL REFERENCES _gencow_workflows(id) ON DELETE CASCADE,
|
|
31
31
|
step_name TEXT NOT NULL,
|
|
@@ -37,7 +37,7 @@ const WORKFLOW_TABLE_SETUP = [
|
|
|
37
37
|
completed_at TIMESTAMPTZ,
|
|
38
38
|
UNIQUE(workflow_id, step_name)
|
|
39
39
|
)`,
|
|
40
|
-
|
|
40
|
+
`CREATE TABLE IF NOT EXISTS _gencow_workflow_events (
|
|
41
41
|
id TEXT PRIMARY KEY,
|
|
42
42
|
workflow_id TEXT NOT NULL REFERENCES _gencow_workflows(id) ON DELETE CASCADE,
|
|
43
43
|
event_name TEXT NOT NULL,
|
|
@@ -48,155 +48,171 @@ const WORKFLOW_TABLE_SETUP = [
|
|
|
48
48
|
] as const;
|
|
49
49
|
|
|
50
50
|
describe("workflow()", () => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
51
|
+
let client: PGlite;
|
|
52
|
+
let db: ReturnType<typeof drizzle>;
|
|
53
|
+
|
|
54
|
+
function buildCtx(userId: string | null) {
|
|
55
|
+
return {
|
|
56
|
+
db,
|
|
57
|
+
unsafeDb: db,
|
|
58
|
+
auth: {
|
|
59
|
+
getUserIdentity: () => (userId ? { id: userId, email: `${userId}@workflow.test` } : null),
|
|
60
|
+
requireAuth: () => {
|
|
61
|
+
if (!userId) throw new Error("Authentication required");
|
|
62
|
+
return { id: userId, email: `${userId}@workflow.test` };
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
storage: {} as any,
|
|
66
|
+
scheduler: {
|
|
67
|
+
runAfter: () => "unused",
|
|
68
|
+
runAt: () => "unused",
|
|
69
|
+
cancel: () => false,
|
|
70
|
+
cron: () => {},
|
|
71
|
+
registerAction: () => {},
|
|
72
|
+
executeAction: async () => {},
|
|
73
|
+
},
|
|
74
|
+
realtime: { emit: () => {}, refresh: () => {} },
|
|
75
|
+
retry: async (fn: () => Promise<unknown>) => fn(),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
beforeAll(async () => {
|
|
80
|
+
client = new PGlite();
|
|
81
|
+
db = drizzle({ client });
|
|
82
|
+
for (const stmt of WORKFLOW_TABLE_SETUP) {
|
|
83
|
+
await client.exec(stmt);
|
|
81
84
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
registerWorkflowsApi();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
afterAll(async () => {
|
|
89
|
+
await client.close();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("start mutation inserts workflow row and schedules resume action", async () => {
|
|
93
|
+
const name = `agents.workflowStart${Date.now()}`;
|
|
94
|
+
const start = workflow(name, {
|
|
95
|
+
args: { topic: v.string() },
|
|
96
|
+
handler: async () => ({ ok: true }),
|
|
90
97
|
});
|
|
91
98
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
delayMs: 0,
|
|
133
|
-
action: getWorkflowResumeActionName(name),
|
|
134
|
-
args: { workflowId: result.id },
|
|
135
|
-
},
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
const rowResult = await db.execute(sql`
|
|
99
|
+
const scheduled: Array<{ delayMs: number; action: string; args: unknown }> = [];
|
|
100
|
+
const result = await start.handler(
|
|
101
|
+
{
|
|
102
|
+
db,
|
|
103
|
+
unsafeDb: db,
|
|
104
|
+
auth: {
|
|
105
|
+
getUserIdentity: () => ({ id: "user_workflow_start", email: "owner@test.dev" }),
|
|
106
|
+
requireAuth: () => ({ id: "user_workflow_start", email: "owner@test.dev" }),
|
|
107
|
+
},
|
|
108
|
+
storage: {} as any,
|
|
109
|
+
scheduler: {
|
|
110
|
+
runAfter: (delayMs, action, args) => {
|
|
111
|
+
scheduled.push({ delayMs, action, args });
|
|
112
|
+
return "job-start-1";
|
|
113
|
+
},
|
|
114
|
+
runAt: () => "unused",
|
|
115
|
+
cancel: () => false,
|
|
116
|
+
cron: () => {},
|
|
117
|
+
registerAction: () => {},
|
|
118
|
+
executeAction: async () => {},
|
|
119
|
+
},
|
|
120
|
+
realtime: { emit: () => {}, refresh: () => {} },
|
|
121
|
+
retry: async (fn) => fn(),
|
|
122
|
+
},
|
|
123
|
+
{ topic: "durable execution" },
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(result.status).toBe("pending");
|
|
127
|
+
expect(result.name).toBe(name);
|
|
128
|
+
expect(result.scheduledJobId).toBe("job-start-1");
|
|
129
|
+
expect(scheduled).toEqual([
|
|
130
|
+
{
|
|
131
|
+
delayMs: 0,
|
|
132
|
+
action: getWorkflowResumeActionName(name),
|
|
133
|
+
args: { workflowId: result.id },
|
|
134
|
+
},
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
const rowResult = await db.execute(sql`
|
|
139
138
|
SELECT id, name, status, max_retries, user_id, args, realtime_token
|
|
140
139
|
FROM _gencow_workflows
|
|
141
140
|
WHERE id = ${result.id}
|
|
142
141
|
`);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
142
|
+
const row = (
|
|
143
|
+
rowResult as {
|
|
144
|
+
rows: Array<{
|
|
145
|
+
id: string;
|
|
146
|
+
name: string;
|
|
147
|
+
status: string;
|
|
148
|
+
max_retries: number;
|
|
149
|
+
user_id: string | null;
|
|
150
|
+
args: unknown;
|
|
151
|
+
realtime_token: string;
|
|
152
|
+
}>;
|
|
153
|
+
}
|
|
154
|
+
).rows[0];
|
|
155
|
+
|
|
156
|
+
expect(row.id).toBe(result.id);
|
|
157
|
+
expect(row.name).toBe(name);
|
|
158
|
+
expect(row.status).toBe("pending");
|
|
159
|
+
expect(row.max_retries).toBe(3);
|
|
160
|
+
expect(row.user_id).toBe("user_workflow_start");
|
|
161
|
+
expect(row.args).toEqual({ value: { topic: "durable execution" } });
|
|
162
|
+
expect(row.realtime_token.length).toBeGreaterThan(10);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("scheduler enqueue failure rolls back the workflow row", async () => {
|
|
166
|
+
const name = `agents.workflowRollback${Date.now()}`;
|
|
167
|
+
const start = workflow(name, {
|
|
168
|
+
args: { topic: v.string() },
|
|
169
|
+
handler: async () => ({ ok: true }),
|
|
152
170
|
});
|
|
153
171
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
unsafeDb: db,
|
|
164
|
-
auth: {
|
|
165
|
-
getUserIdentity: () => null,
|
|
166
|
-
requireAuth: () => {
|
|
167
|
-
throw new Error("not used");
|
|
168
|
-
},
|
|
172
|
+
await expect(
|
|
173
|
+
start.handler(
|
|
174
|
+
{
|
|
175
|
+
db,
|
|
176
|
+
unsafeDb: db,
|
|
177
|
+
auth: {
|
|
178
|
+
getUserIdentity: () => null,
|
|
179
|
+
requireAuth: () => {
|
|
180
|
+
throw new Error("not used");
|
|
169
181
|
},
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
runAt: () => "unused",
|
|
176
|
-
cancel: () => false,
|
|
177
|
-
cron: () => {},
|
|
178
|
-
registerAction: () => {},
|
|
179
|
-
executeAction: async () => {},
|
|
182
|
+
},
|
|
183
|
+
storage: {} as any,
|
|
184
|
+
scheduler: {
|
|
185
|
+
runAfter: () => {
|
|
186
|
+
throw new Error("scheduler unavailable");
|
|
180
187
|
},
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
runAt: () => "unused",
|
|
189
|
+
cancel: () => false,
|
|
190
|
+
cron: () => {},
|
|
191
|
+
registerAction: () => {},
|
|
192
|
+
executeAction: async () => {},
|
|
193
|
+
},
|
|
194
|
+
realtime: { emit: () => {}, refresh: () => {} },
|
|
195
|
+
retry: async (fn) => fn(),
|
|
196
|
+
},
|
|
197
|
+
{ topic: "should rollback" },
|
|
198
|
+
),
|
|
199
|
+
).rejects.toThrow("scheduler unavailable");
|
|
200
|
+
|
|
201
|
+
const countResult = await db.execute(sql`
|
|
186
202
|
SELECT count(*)::int AS c
|
|
187
203
|
FROM _gencow_workflows
|
|
188
204
|
WHERE name = ${name}
|
|
189
205
|
`);
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
const count = (countResult as { rows: Array<{ c: number }> }).rows[0]?.c ?? 0;
|
|
207
|
+
expect(count).toBe(0);
|
|
208
|
+
});
|
|
193
209
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
it("workflows.get returns full snapshot for the owner and hides private workflows from others", async () => {
|
|
211
|
+
const getWorkflow = getQueryDef("workflows.get");
|
|
212
|
+
expect(getWorkflow).toBeDefined();
|
|
197
213
|
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
const workflowId = `wf_owned_${Date.now()}`;
|
|
215
|
+
await db.execute(sql`
|
|
200
216
|
INSERT INTO _gencow_workflows (
|
|
201
217
|
id,
|
|
202
218
|
name,
|
|
@@ -230,7 +246,7 @@ describe("workflow()", () => {
|
|
|
230
246
|
'owner_workflow'
|
|
231
247
|
)
|
|
232
248
|
`);
|
|
233
|
-
|
|
249
|
+
await db.execute(sql`
|
|
234
250
|
INSERT INTO _gencow_workflow_steps (
|
|
235
251
|
workflow_id,
|
|
236
252
|
step_name,
|
|
@@ -264,41 +280,43 @@ describe("workflow()", () => {
|
|
|
264
280
|
)
|
|
265
281
|
`);
|
|
266
282
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
283
|
+
const ownerSnapshot = await getWorkflow!.handler(buildCtx("owner_workflow") as any, { id: workflowId });
|
|
284
|
+
expect(ownerSnapshot).toMatchObject({
|
|
285
|
+
id: workflowId,
|
|
286
|
+
name: "agents.report",
|
|
287
|
+
status: "running",
|
|
288
|
+
derivedStatus: "running",
|
|
289
|
+
currentStep: "analyze",
|
|
290
|
+
args: { topic: "market map" },
|
|
291
|
+
result: null,
|
|
292
|
+
retryCount: 1,
|
|
293
|
+
maxRetries: 3,
|
|
294
|
+
});
|
|
295
|
+
expect(ownerSnapshot?.realtimeKey).toMatch(new RegExp(`^__gencow\\.workflow\\.state\\.${workflowId}\\.`));
|
|
296
|
+
expect(ownerSnapshot?.steps).toHaveLength(2);
|
|
297
|
+
expect(ownerSnapshot?.steps[0]).toMatchObject({
|
|
298
|
+
name: "search",
|
|
299
|
+
status: "completed",
|
|
300
|
+
output: { hits: 2 },
|
|
301
|
+
});
|
|
302
|
+
expect(ownerSnapshot?.steps[1]).toMatchObject({
|
|
303
|
+
name: "analyze",
|
|
304
|
+
status: "running",
|
|
305
|
+
output: null,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const strangerSnapshot = await getWorkflow!.handler(buildCtx("stranger_workflow") as any, {
|
|
309
|
+
id: workflowId,
|
|
294
310
|
});
|
|
311
|
+
expect(strangerSnapshot).toBeNull();
|
|
312
|
+
});
|
|
295
313
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
314
|
+
it("workflows.get allows ownerless public workflows to be polled without auth", async () => {
|
|
315
|
+
const getWorkflow = getQueryDef("workflows.get");
|
|
316
|
+
expect(getWorkflow).toBeDefined();
|
|
299
317
|
|
|
300
|
-
|
|
301
|
-
|
|
318
|
+
const workflowId = `wf_public_${Date.now()}`;
|
|
319
|
+
await db.execute(sql`
|
|
302
320
|
INSERT INTO _gencow_workflows (
|
|
303
321
|
id,
|
|
304
322
|
name,
|
|
@@ -333,23 +351,23 @@ describe("workflow()", () => {
|
|
|
333
351
|
)
|
|
334
352
|
`);
|
|
335
353
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
});
|
|
344
|
-
expect(snapshot?.realtimeKey).toMatch(new RegExp(`^__gencow\\.workflow\\.state\\.${workflowId}\\.`));
|
|
354
|
+
const snapshot = await getWorkflow!.handler(buildCtx(null) as any, { id: workflowId });
|
|
355
|
+
expect(snapshot).toMatchObject({
|
|
356
|
+
id: workflowId,
|
|
357
|
+
status: "completed",
|
|
358
|
+
derivedStatus: "completed",
|
|
359
|
+
result: { report: "done" },
|
|
360
|
+
steps: [],
|
|
345
361
|
});
|
|
362
|
+
expect(snapshot?.realtimeKey).toMatch(new RegExp(`^__gencow\\.workflow\\.state\\.${workflowId}\\.`));
|
|
363
|
+
});
|
|
346
364
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
365
|
+
it("workflows.signal persists an event and wakes waiting workflows for the owner only", async () => {
|
|
366
|
+
const signalWorkflow = getRegisteredMutations().find((item) => item.name === "workflows.signal");
|
|
367
|
+
expect(signalWorkflow).toBeDefined();
|
|
350
368
|
|
|
351
|
-
|
|
352
|
-
|
|
369
|
+
const workflowId = `wf_signal_${Date.now()}`;
|
|
370
|
+
await db.execute(sql`
|
|
353
371
|
INSERT INTO _gencow_workflows (
|
|
354
372
|
id,
|
|
355
373
|
name,
|
|
@@ -384,83 +402,88 @@ describe("workflow()", () => {
|
|
|
384
402
|
)
|
|
385
403
|
`);
|
|
386
404
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
405
|
+
const scheduled: Array<{ delayMs: number; action: string; args: unknown }> = [];
|
|
406
|
+
const accepted = await signalWorkflow!.handler(
|
|
407
|
+
{
|
|
408
|
+
...buildCtx("signal_owner"),
|
|
409
|
+
scheduler: {
|
|
410
|
+
runAfter: (delayMs, action, args) => {
|
|
411
|
+
scheduled.push({ delayMs, action, args });
|
|
412
|
+
return "job-signal-1";
|
|
413
|
+
},
|
|
414
|
+
runAt: () => "unused",
|
|
415
|
+
cancel: () => false,
|
|
416
|
+
cron: () => {},
|
|
417
|
+
registerAction: () => {},
|
|
418
|
+
executeAction: async () => {},
|
|
419
|
+
},
|
|
420
|
+
} as any,
|
|
421
|
+
{
|
|
422
|
+
id: workflowId,
|
|
423
|
+
event: "approval",
|
|
424
|
+
payload: { approved: true },
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
expect(accepted).toEqual({
|
|
429
|
+
ok: true,
|
|
430
|
+
workflowId,
|
|
431
|
+
event: "approval",
|
|
432
|
+
scheduledJobId: "job-signal-1",
|
|
433
|
+
});
|
|
434
|
+
expect(scheduled).toEqual([
|
|
435
|
+
{
|
|
436
|
+
delayMs: 0,
|
|
437
|
+
action: getWorkflowResumeActionName("agents.waiting"),
|
|
438
|
+
args: { workflowId },
|
|
439
|
+
},
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
const eventResult = await db.execute(sql`
|
|
422
443
|
SELECT workflow_id, event_name, payload, consumed_at IS NULL AS is_unconsumed
|
|
423
444
|
FROM _gencow_workflow_events
|
|
424
445
|
WHERE workflow_id = ${workflowId}
|
|
425
446
|
`);
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
447
|
+
const eventRow = (
|
|
448
|
+
eventResult as {
|
|
449
|
+
rows: Array<{
|
|
450
|
+
workflow_id: string;
|
|
451
|
+
event_name: string;
|
|
452
|
+
payload: unknown;
|
|
453
|
+
is_unconsumed: boolean;
|
|
454
|
+
}>;
|
|
455
|
+
}
|
|
456
|
+
).rows[0];
|
|
457
|
+
expect(eventRow).toEqual({
|
|
458
|
+
workflow_id: workflowId,
|
|
459
|
+
event_name: "approval",
|
|
460
|
+
payload: { value: { approved: true } },
|
|
461
|
+
is_unconsumed: true,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const denied = await signalWorkflow!.handler(buildCtx("someone_else") as any, {
|
|
465
|
+
id: workflowId,
|
|
466
|
+
event: "approval",
|
|
467
|
+
payload: { approved: false },
|
|
468
|
+
});
|
|
469
|
+
expect(denied).toEqual({
|
|
470
|
+
ok: false,
|
|
471
|
+
workflowId,
|
|
472
|
+
event: "approval",
|
|
473
|
+
scheduledJobId: null,
|
|
452
474
|
});
|
|
475
|
+
});
|
|
453
476
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
477
|
+
it("workflows.list requires auth and filters to the current owner", async () => {
|
|
478
|
+
const listWorkflows = getQueryDef("workflows.list");
|
|
479
|
+
expect(listWorkflows).toBeDefined();
|
|
457
480
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
481
|
+
const userACompleted = `wf_user_a_completed_${Date.now()}`;
|
|
482
|
+
const userAWaiting = `wf_user_a_waiting_${Date.now()}`;
|
|
483
|
+
const userASleeping = `wf_user_a_sleeping_${Date.now()}`;
|
|
484
|
+
const userBRunning = `wf_user_b_running_${Date.now()}`;
|
|
462
485
|
|
|
463
|
-
|
|
486
|
+
await db.execute(sql`
|
|
464
487
|
INSERT INTO _gencow_workflows (
|
|
465
488
|
id,
|
|
466
489
|
name,
|
|
@@ -544,40 +567,40 @@ describe("workflow()", () => {
|
|
|
544
567
|
)
|
|
545
568
|
`);
|
|
546
569
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
);
|
|
565
|
-
expect(waitingOnly).toHaveLength(1);
|
|
566
|
-
expect(waitingOnly[0]?.id).toBe(userAWaiting);
|
|
567
|
-
|
|
568
|
-
const sleepingOnly = await listWorkflows!.handler(
|
|
569
|
-
buildCtx("workflow_user_a") as any,
|
|
570
|
-
{ limit: 10, status: "sleeping" }
|
|
571
|
-
);
|
|
572
|
-
expect(sleepingOnly).toHaveLength(1);
|
|
573
|
-
expect(sleepingOnly[0]?.id).toBe(userASleeping);
|
|
574
|
-
|
|
575
|
-
await expect(
|
|
576
|
-
listWorkflows!.handler(buildCtx(null) as any, { limit: 10 })
|
|
577
|
-
).rejects.toThrow("Authentication required");
|
|
578
|
-
|
|
579
|
-
await expect(
|
|
580
|
-
listWorkflows!.handler(buildCtx("workflow_user_a") as any, { limit: 10, status: "mystery" })
|
|
581
|
-
).rejects.toThrow('Argument "status": expected one of pending, running, completed, failed');
|
|
570
|
+
const ownWorkflows = await listWorkflows!.handler(buildCtx("workflow_user_a") as any, { limit: 10 });
|
|
571
|
+
expect(ownWorkflows.map((item: any) => item.id)).toEqual([userAWaiting, userASleeping, userACompleted]);
|
|
572
|
+
expect(ownWorkflows.every((item: any) => item.name === "agents.sync")).toBe(true);
|
|
573
|
+
expect(ownWorkflows.some((item: any) => item.id === userBRunning)).toBe(false);
|
|
574
|
+
expect(ownWorkflows.find((item: any) => item.id === userAWaiting)?.derivedStatus).toBe("waiting");
|
|
575
|
+
expect(ownWorkflows.find((item: any) => item.id === userASleeping)?.derivedStatus).toBe("sleeping");
|
|
576
|
+
|
|
577
|
+
const completedOnly = await listWorkflows!.handler(buildCtx("workflow_user_a") as any, {
|
|
578
|
+
limit: 10,
|
|
579
|
+
status: "completed",
|
|
580
|
+
});
|
|
581
|
+
expect(completedOnly).toHaveLength(1);
|
|
582
|
+
expect(completedOnly[0]?.id).toBe(userACompleted);
|
|
583
|
+
|
|
584
|
+
const waitingOnly = await listWorkflows!.handler(buildCtx("workflow_user_a") as any, {
|
|
585
|
+
limit: 10,
|
|
586
|
+
status: "waiting",
|
|
582
587
|
});
|
|
588
|
+
expect(waitingOnly).toHaveLength(1);
|
|
589
|
+
expect(waitingOnly[0]?.id).toBe(userAWaiting);
|
|
590
|
+
|
|
591
|
+
const sleepingOnly = await listWorkflows!.handler(buildCtx("workflow_user_a") as any, {
|
|
592
|
+
limit: 10,
|
|
593
|
+
status: "sleeping",
|
|
594
|
+
});
|
|
595
|
+
expect(sleepingOnly).toHaveLength(1);
|
|
596
|
+
expect(sleepingOnly[0]?.id).toBe(userASleeping);
|
|
597
|
+
|
|
598
|
+
await expect(listWorkflows!.handler(buildCtx(null) as any, { limit: 10 })).rejects.toThrow(
|
|
599
|
+
"Authentication required",
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
await expect(
|
|
603
|
+
listWorkflows!.handler(buildCtx("workflow_user_a") as any, { limit: 10, status: "mystery" }),
|
|
604
|
+
).rejects.toThrow('Argument "status": expected one of pending, running, completed, failed');
|
|
605
|
+
});
|
|
583
606
|
});
|