@gencow/core 0.1.26 → 0.1.28
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 +12 -0
- package/dist/crud.js +16 -0
- package/dist/db.d.ts +13 -0
- package/dist/db.js +16 -0
- package/dist/document-types.d.ts +65 -0
- package/dist/document-types.js +15 -0
- package/dist/grounded-answer-types.d.ts +62 -0
- package/dist/grounded-answer-types.js +6 -0
- package/dist/index.d.ts +12 -2
- package/dist/index.js +5 -1
- package/dist/rag-ingest-types.d.ts +39 -0
- package/dist/rag-ingest-types.js +1 -0
- package/dist/rag-operations-types.d.ts +81 -0
- package/dist/rag-operations-types.js +1 -0
- package/dist/rag-schema.d.ts +1557 -0
- package/dist/rag-schema.js +87 -0
- package/dist/reactive.d.ts +13 -0
- package/dist/rls-db.d.ts +9 -2
- package/dist/runtime-env-policy.d.ts +5 -0
- package/dist/runtime-env-policy.js +56 -0
- package/dist/search-types.d.ts +83 -0
- package/dist/search-types.js +1 -0
- package/dist/server.d.ts +1 -2
- package/dist/server.js +0 -1
- package/dist/storage-shared.d.ts +36 -0
- package/dist/storage-shared.js +39 -0
- package/dist/storage.d.ts +2 -26
- package/dist/storage.js +19 -15
- package/dist/workflow-types.d.ts +3 -1
- package/package.json +1 -1
- package/src/crud.ts +33 -0
- package/src/document-types.ts +95 -0
- package/src/grounded-answer-types.ts +78 -0
- package/src/index.ts +68 -2
- package/src/rag-ingest-types.ts +52 -0
- package/src/rag-operations-types.ts +90 -0
- package/src/rag-schema.ts +94 -0
- package/src/reactive.ts +13 -0
- package/src/rls-db.ts +9 -4
- package/src/runtime-env-policy.ts +66 -0
- package/src/search-types.ts +91 -0
- package/src/server.ts +1 -2
- package/src/storage-shared.ts +74 -0
- package/src/storage.ts +29 -46
- package/src/workflow-types.ts +3 -1
- package/src/__tests__/auth.test.ts +0 -118
- package/src/__tests__/crons.test.ts +0 -83
- package/src/__tests__/crud-codegen-integration.test.ts +0 -246
- package/src/__tests__/crud-owner-rls.test.ts +0 -387
- package/src/__tests__/crud.test.ts +0 -930
- package/src/__tests__/dist-exports.test.ts +0 -176
- package/src/__tests__/fixtures/basic/auth.ts +0 -32
- package/src/__tests__/fixtures/basic/drizzle.config.ts +0 -12
- package/src/__tests__/fixtures/basic/index.ts +0 -6
- package/src/__tests__/fixtures/basic/migrations/0000_last_warstar.sql +0 -75
- package/src/__tests__/fixtures/basic/migrations/meta/0000_snapshot.json +0 -497
- package/src/__tests__/fixtures/basic/migrations/meta/_journal.json +0 -13
- package/src/__tests__/fixtures/basic/schema.ts +0 -51
- package/src/__tests__/fixtures/basic/tasks.ts +0 -15
- package/src/__tests__/fixtures/common/auth-schema.ts +0 -67
- package/src/__tests__/helpers/basic-rls-fixture.ts +0 -135
- package/src/__tests__/helpers/pglite-migrations.ts +0 -32
- package/src/__tests__/helpers/pglite-rls-session.ts +0 -51
- package/src/__tests__/helpers/seed-like-fill.ts +0 -202
- package/src/__tests__/helpers/test-gencow-ctx-rls.ts +0 -50
- package/src/__tests__/httpaction.test.ts +0 -122
- package/src/__tests__/image-optimization.test.ts +0 -648
- package/src/__tests__/load.test.ts +0 -389
- package/src/__tests__/network-sim.test.ts +0 -319
- package/src/__tests__/reactive.test.ts +0 -479
- package/src/__tests__/retry.test.ts +0 -113
- package/src/__tests__/rls-crud-basic.test.ts +0 -317
- package/src/__tests__/rls-crud-no-owner-rls-pglite.test.ts +0 -117
- package/src/__tests__/rls-custom-mutation-handlers.test.ts +0 -142
- package/src/__tests__/rls-custom-query-handlers.test.ts +0 -128
- package/src/__tests__/rls-db-leased-connection.test.ts +0 -118
- package/src/__tests__/rls-session-and-policies.test.ts +0 -228
- package/src/__tests__/scheduler-durable-v2.test.ts +0 -288
- package/src/__tests__/scheduler-durable.test.ts +0 -173
- package/src/__tests__/scheduler-exec.test.ts +0 -328
- package/src/__tests__/scheduler.test.ts +0 -187
- package/src/__tests__/storage.test.ts +0 -334
- package/src/__tests__/tsconfig.json +0 -8
- package/src/__tests__/validator.test.ts +0 -323
- package/src/__tests__/workflow.test.ts +0 -606
- package/src/__tests__/ws-integration.test.ts +0 -309
- package/src/__tests__/ws-scale.test.ts +0 -241
- package/src/auth.ts +0 -155
|
@@ -1,606 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
-
import { PGlite } from "@electric-sql/pglite";
|
|
3
|
-
import { drizzle } from "drizzle-orm/pglite";
|
|
4
|
-
import { sql } from "drizzle-orm";
|
|
5
|
-
import { getWorkflowResumeActionName, workflow } from "../workflow.js";
|
|
6
|
-
import { getQueryDef, getRegisteredMutations } from "../reactive.js";
|
|
7
|
-
import { v } from "../v.js";
|
|
8
|
-
import { registerWorkflowsApi } from "../workflows-api.js";
|
|
9
|
-
|
|
10
|
-
const WORKFLOW_TABLE_SETUP = [
|
|
11
|
-
`CREATE TABLE IF NOT EXISTS _gencow_workflows (
|
|
12
|
-
id TEXT PRIMARY KEY,
|
|
13
|
-
name TEXT NOT NULL,
|
|
14
|
-
args JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
15
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
16
|
-
current_step TEXT,
|
|
17
|
-
result JSONB,
|
|
18
|
-
error TEXT,
|
|
19
|
-
realtime_token TEXT NOT NULL DEFAULT '',
|
|
20
|
-
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
21
|
-
max_retries INTEGER NOT NULL DEFAULT 3,
|
|
22
|
-
max_duration_ms BIGINT NOT NULL DEFAULT 1800000,
|
|
23
|
-
started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
24
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
25
|
-
completed_at TIMESTAMPTZ,
|
|
26
|
-
user_id TEXT
|
|
27
|
-
)`,
|
|
28
|
-
`CREATE TABLE IF NOT EXISTS _gencow_workflow_steps (
|
|
29
|
-
id SERIAL PRIMARY KEY,
|
|
30
|
-
workflow_id TEXT NOT NULL REFERENCES _gencow_workflows(id) ON DELETE CASCADE,
|
|
31
|
-
step_name TEXT NOT NULL,
|
|
32
|
-
status TEXT NOT NULL DEFAULT 'pending',
|
|
33
|
-
output JSONB,
|
|
34
|
-
error TEXT,
|
|
35
|
-
started_at TIMESTAMPTZ,
|
|
36
|
-
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
37
|
-
completed_at TIMESTAMPTZ,
|
|
38
|
-
UNIQUE(workflow_id, step_name)
|
|
39
|
-
)`,
|
|
40
|
-
`CREATE TABLE IF NOT EXISTS _gencow_workflow_events (
|
|
41
|
-
id TEXT PRIMARY KEY,
|
|
42
|
-
workflow_id TEXT NOT NULL REFERENCES _gencow_workflows(id) ON DELETE CASCADE,
|
|
43
|
-
event_name TEXT NOT NULL,
|
|
44
|
-
payload JSONB,
|
|
45
|
-
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
46
|
-
consumed_at TIMESTAMPTZ
|
|
47
|
-
)`,
|
|
48
|
-
] as const;
|
|
49
|
-
|
|
50
|
-
describe("workflow()", () => {
|
|
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);
|
|
84
|
-
}
|
|
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 }),
|
|
97
|
-
});
|
|
98
|
-
|
|
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`
|
|
138
|
-
SELECT id, name, status, max_retries, user_id, args, realtime_token
|
|
139
|
-
FROM _gencow_workflows
|
|
140
|
-
WHERE id = ${result.id}
|
|
141
|
-
`);
|
|
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 }),
|
|
170
|
-
});
|
|
171
|
-
|
|
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");
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
storage: {} as any,
|
|
184
|
-
scheduler: {
|
|
185
|
-
runAfter: () => {
|
|
186
|
-
throw new Error("scheduler unavailable");
|
|
187
|
-
},
|
|
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`
|
|
202
|
-
SELECT count(*)::int AS c
|
|
203
|
-
FROM _gencow_workflows
|
|
204
|
-
WHERE name = ${name}
|
|
205
|
-
`);
|
|
206
|
-
const count = (countResult as { rows: Array<{ c: number }> }).rows[0]?.c ?? 0;
|
|
207
|
-
expect(count).toBe(0);
|
|
208
|
-
});
|
|
209
|
-
|
|
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();
|
|
213
|
-
|
|
214
|
-
const workflowId = `wf_owned_${Date.now()}`;
|
|
215
|
-
await db.execute(sql`
|
|
216
|
-
INSERT INTO _gencow_workflows (
|
|
217
|
-
id,
|
|
218
|
-
name,
|
|
219
|
-
args,
|
|
220
|
-
status,
|
|
221
|
-
current_step,
|
|
222
|
-
result,
|
|
223
|
-
error,
|
|
224
|
-
retry_count,
|
|
225
|
-
max_retries,
|
|
226
|
-
max_duration_ms,
|
|
227
|
-
started_at,
|
|
228
|
-
updated_at,
|
|
229
|
-
completed_at,
|
|
230
|
-
user_id
|
|
231
|
-
)
|
|
232
|
-
VALUES (
|
|
233
|
-
${workflowId},
|
|
234
|
-
'agents.report',
|
|
235
|
-
'{"value":{"topic":"market map"}}'::jsonb,
|
|
236
|
-
'running',
|
|
237
|
-
'analyze',
|
|
238
|
-
NULL,
|
|
239
|
-
NULL,
|
|
240
|
-
1,
|
|
241
|
-
3,
|
|
242
|
-
1800000,
|
|
243
|
-
NOW() - INTERVAL '2 minutes',
|
|
244
|
-
NOW() - INTERVAL '5 seconds',
|
|
245
|
-
NULL,
|
|
246
|
-
'owner_workflow'
|
|
247
|
-
)
|
|
248
|
-
`);
|
|
249
|
-
await db.execute(sql`
|
|
250
|
-
INSERT INTO _gencow_workflow_steps (
|
|
251
|
-
workflow_id,
|
|
252
|
-
step_name,
|
|
253
|
-
status,
|
|
254
|
-
output,
|
|
255
|
-
error,
|
|
256
|
-
started_at,
|
|
257
|
-
updated_at,
|
|
258
|
-
completed_at
|
|
259
|
-
)
|
|
260
|
-
VALUES
|
|
261
|
-
(
|
|
262
|
-
${workflowId},
|
|
263
|
-
'search',
|
|
264
|
-
'completed',
|
|
265
|
-
'{"value":{"hits":2}}'::jsonb,
|
|
266
|
-
NULL,
|
|
267
|
-
NOW() - INTERVAL '90 seconds',
|
|
268
|
-
NOW() - INTERVAL '80 seconds',
|
|
269
|
-
NOW() - INTERVAL '80 seconds'
|
|
270
|
-
),
|
|
271
|
-
(
|
|
272
|
-
${workflowId},
|
|
273
|
-
'analyze',
|
|
274
|
-
'running',
|
|
275
|
-
NULL,
|
|
276
|
-
NULL,
|
|
277
|
-
NOW() - INTERVAL '10 seconds',
|
|
278
|
-
NOW() - INTERVAL '5 seconds',
|
|
279
|
-
NULL
|
|
280
|
-
)
|
|
281
|
-
`);
|
|
282
|
-
|
|
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,
|
|
310
|
-
});
|
|
311
|
-
expect(strangerSnapshot).toBeNull();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("workflows.get allows ownerless public workflows to be polled without auth", async () => {
|
|
315
|
-
const getWorkflow = getQueryDef("workflows.get");
|
|
316
|
-
expect(getWorkflow).toBeDefined();
|
|
317
|
-
|
|
318
|
-
const workflowId = `wf_public_${Date.now()}`;
|
|
319
|
-
await db.execute(sql`
|
|
320
|
-
INSERT INTO _gencow_workflows (
|
|
321
|
-
id,
|
|
322
|
-
name,
|
|
323
|
-
args,
|
|
324
|
-
status,
|
|
325
|
-
current_step,
|
|
326
|
-
result,
|
|
327
|
-
error,
|
|
328
|
-
retry_count,
|
|
329
|
-
max_retries,
|
|
330
|
-
max_duration_ms,
|
|
331
|
-
started_at,
|
|
332
|
-
updated_at,
|
|
333
|
-
completed_at,
|
|
334
|
-
user_id
|
|
335
|
-
)
|
|
336
|
-
VALUES (
|
|
337
|
-
${workflowId},
|
|
338
|
-
'agents.public',
|
|
339
|
-
'{"value":{"topic":"public"}}'::jsonb,
|
|
340
|
-
'completed',
|
|
341
|
-
NULL,
|
|
342
|
-
'{"value":{"report":"done"}}'::jsonb,
|
|
343
|
-
NULL,
|
|
344
|
-
0,
|
|
345
|
-
3,
|
|
346
|
-
1800000,
|
|
347
|
-
NOW() - INTERVAL '1 minute',
|
|
348
|
-
NOW() - INTERVAL '5 seconds',
|
|
349
|
-
NOW() - INTERVAL '5 seconds',
|
|
350
|
-
NULL
|
|
351
|
-
)
|
|
352
|
-
`);
|
|
353
|
-
|
|
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: [],
|
|
361
|
-
});
|
|
362
|
-
expect(snapshot?.realtimeKey).toMatch(new RegExp(`^__gencow\\.workflow\\.state\\.${workflowId}\\.`));
|
|
363
|
-
});
|
|
364
|
-
|
|
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();
|
|
368
|
-
|
|
369
|
-
const workflowId = `wf_signal_${Date.now()}`;
|
|
370
|
-
await db.execute(sql`
|
|
371
|
-
INSERT INTO _gencow_workflows (
|
|
372
|
-
id,
|
|
373
|
-
name,
|
|
374
|
-
args,
|
|
375
|
-
status,
|
|
376
|
-
current_step,
|
|
377
|
-
result,
|
|
378
|
-
error,
|
|
379
|
-
retry_count,
|
|
380
|
-
max_retries,
|
|
381
|
-
max_duration_ms,
|
|
382
|
-
started_at,
|
|
383
|
-
updated_at,
|
|
384
|
-
completed_at,
|
|
385
|
-
user_id
|
|
386
|
-
)
|
|
387
|
-
VALUES (
|
|
388
|
-
${workflowId},
|
|
389
|
-
'agents.waiting',
|
|
390
|
-
'{}'::jsonb,
|
|
391
|
-
'pending',
|
|
392
|
-
'wait:approval#1',
|
|
393
|
-
NULL,
|
|
394
|
-
NULL,
|
|
395
|
-
0,
|
|
396
|
-
3,
|
|
397
|
-
1800000,
|
|
398
|
-
NOW() - INTERVAL '20 seconds',
|
|
399
|
-
NOW() - INTERVAL '5 seconds',
|
|
400
|
-
NULL,
|
|
401
|
-
'signal_owner'
|
|
402
|
-
)
|
|
403
|
-
`);
|
|
404
|
-
|
|
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`
|
|
443
|
-
SELECT workflow_id, event_name, payload, consumed_at IS NULL AS is_unconsumed
|
|
444
|
-
FROM _gencow_workflow_events
|
|
445
|
-
WHERE workflow_id = ${workflowId}
|
|
446
|
-
`);
|
|
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,
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
|
|
477
|
-
it("workflows.list requires auth and filters to the current owner", async () => {
|
|
478
|
-
const listWorkflows = getQueryDef("workflows.list");
|
|
479
|
-
expect(listWorkflows).toBeDefined();
|
|
480
|
-
|
|
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()}`;
|
|
485
|
-
|
|
486
|
-
await db.execute(sql`
|
|
487
|
-
INSERT INTO _gencow_workflows (
|
|
488
|
-
id,
|
|
489
|
-
name,
|
|
490
|
-
args,
|
|
491
|
-
status,
|
|
492
|
-
current_step,
|
|
493
|
-
result,
|
|
494
|
-
error,
|
|
495
|
-
retry_count,
|
|
496
|
-
max_retries,
|
|
497
|
-
max_duration_ms,
|
|
498
|
-
started_at,
|
|
499
|
-
updated_at,
|
|
500
|
-
completed_at,
|
|
501
|
-
user_id
|
|
502
|
-
)
|
|
503
|
-
VALUES
|
|
504
|
-
(
|
|
505
|
-
${userACompleted},
|
|
506
|
-
'agents.sync',
|
|
507
|
-
'{}'::jsonb,
|
|
508
|
-
'completed',
|
|
509
|
-
NULL,
|
|
510
|
-
NULL,
|
|
511
|
-
NULL,
|
|
512
|
-
0,
|
|
513
|
-
3,
|
|
514
|
-
1800000,
|
|
515
|
-
NOW() - INTERVAL '30 seconds',
|
|
516
|
-
NOW() - INTERVAL '20 seconds',
|
|
517
|
-
NOW() - INTERVAL '20 seconds',
|
|
518
|
-
'workflow_user_a'
|
|
519
|
-
),
|
|
520
|
-
(
|
|
521
|
-
${userAWaiting},
|
|
522
|
-
'agents.sync',
|
|
523
|
-
'{}'::jsonb,
|
|
524
|
-
'pending',
|
|
525
|
-
'wait:approval#1',
|
|
526
|
-
NULL,
|
|
527
|
-
NULL,
|
|
528
|
-
0,
|
|
529
|
-
3,
|
|
530
|
-
1800000,
|
|
531
|
-
NOW() - INTERVAL '10 seconds',
|
|
532
|
-
NOW() - INTERVAL '10 seconds',
|
|
533
|
-
NULL,
|
|
534
|
-
'workflow_user_a'
|
|
535
|
-
),
|
|
536
|
-
(
|
|
537
|
-
${userASleeping},
|
|
538
|
-
'agents.sync',
|
|
539
|
-
'{}'::jsonb,
|
|
540
|
-
'pending',
|
|
541
|
-
'sleep#1',
|
|
542
|
-
NULL,
|
|
543
|
-
NULL,
|
|
544
|
-
0,
|
|
545
|
-
3,
|
|
546
|
-
1800000,
|
|
547
|
-
NOW() - INTERVAL '15 seconds',
|
|
548
|
-
NOW() - INTERVAL '12 seconds',
|
|
549
|
-
NULL,
|
|
550
|
-
'workflow_user_a'
|
|
551
|
-
),
|
|
552
|
-
(
|
|
553
|
-
${userBRunning},
|
|
554
|
-
'agents.sync',
|
|
555
|
-
'{}'::jsonb,
|
|
556
|
-
'running',
|
|
557
|
-
'analyze',
|
|
558
|
-
NULL,
|
|
559
|
-
NULL,
|
|
560
|
-
0,
|
|
561
|
-
3,
|
|
562
|
-
1800000,
|
|
563
|
-
NOW() - INTERVAL '5 seconds',
|
|
564
|
-
NOW() - INTERVAL '5 seconds',
|
|
565
|
-
NULL,
|
|
566
|
-
'workflow_user_b'
|
|
567
|
-
)
|
|
568
|
-
`);
|
|
569
|
-
|
|
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",
|
|
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
|
-
});
|
|
606
|
-
});
|