@pikku/kysely 0.13.24 → 0.13.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +271 -0
  2. package/dist/src/index.d.ts +4 -1
  3. package/dist/src/index.js +3 -1
  4. package/dist/src/kysely-analytics-service.d.ts +30 -0
  5. package/dist/src/kysely-analytics-service.js +80 -0
  6. package/dist/src/kysely-feature-flag-store.d.ts +37 -0
  7. package/dist/src/kysely-feature-flag-store.js +239 -0
  8. package/dist/src/kysely-scope-service.d.ts +1 -0
  9. package/dist/src/kysely-scope-service.js +29 -0
  10. package/dist/src/kysely-tables.d.ts +45 -0
  11. package/dist/src/schema/analytics.schema.d.ts +17 -0
  12. package/dist/src/schema/analytics.schema.js +48 -0
  13. package/dist/src/schema/flag.schema.d.ts +16 -0
  14. package/dist/src/schema/flag.schema.js +62 -0
  15. package/dist/src/schema/index.d.ts +2 -0
  16. package/dist/src/schema/index.js +6 -0
  17. package/package.json +11 -6
  18. package/run-tests.sh +9 -1
  19. package/bin/pikku-kysely-pure.ts +0 -59
  20. package/dist/src/kysely-lock-vault.d.ts +0 -34
  21. package/dist/src/kysely-lock-vault.js +0 -77
  22. package/dist/src/schema/data-lock.schema.d.ts +0 -15
  23. package/dist/src/schema/data-lock.schema.js +0 -27
  24. package/dist/tsconfig.tsbuildinfo +0 -1
  25. package/src/classification-crypto.test.ts +0 -143
  26. package/src/classification-crypto.ts +0 -163
  27. package/src/classification-plugin.pglite.test.ts +0 -226
  28. package/src/classification-plugin.test.ts +0 -265
  29. package/src/classification-plugin.ts +0 -249
  30. package/src/coercion-plugin.test.ts +0 -160
  31. package/src/coercion-plugin.ts +0 -187
  32. package/src/create-audited-kysely.test.ts +0 -560
  33. package/src/create-audited-kysely.ts +0 -254
  34. package/src/index.ts +0 -81
  35. package/src/kysely-agent-run-scores.ts +0 -47
  36. package/src/kysely-agent-run-service.ts +0 -244
  37. package/src/kysely-agent-run-state-service.ts +0 -200
  38. package/src/kysely-agent-storage-service.ts +0 -632
  39. package/src/kysely-audit-service.test.ts +0 -352
  40. package/src/kysely-audit-service.ts +0 -237
  41. package/src/kysely-channel-store.ts +0 -107
  42. package/src/kysely-credential-service.ts +0 -286
  43. package/src/kysely-deployment-service.ts +0 -163
  44. package/src/kysely-eventhub-store.ts +0 -53
  45. package/src/kysely-json.ts +0 -5
  46. package/src/kysely-scope-service.test.ts +0 -418
  47. package/src/kysely-scope-service.ts +0 -425
  48. package/src/kysely-secret-service.ts +0 -242
  49. package/src/kysely-services.test.ts +0 -405
  50. package/src/kysely-session-store.ts +0 -57
  51. package/src/kysely-tables.test.ts +0 -110
  52. package/src/kysely-tables.ts +0 -434
  53. package/src/kysely-virtual-user-run-store.test.ts +0 -369
  54. package/src/kysely-virtual-user-run-store.ts +0 -235
  55. package/src/kysely-virtual-user-schedule-store.test.ts +0 -187
  56. package/src/kysely-virtual-user-schedule-store.ts +0 -187
  57. package/src/kysely-webhook-service.test.ts +0 -153
  58. package/src/kysely-webhook-service.ts +0 -139
  59. package/src/kysely-workflow-mirror.test.ts +0 -259
  60. package/src/kysely-workflow-mirror.ts +0 -374
  61. package/src/kysely-workflow-run-service.ts +0 -305
  62. package/src/kysely-workflow-service.test.ts +0 -544
  63. package/src/kysely-workflow-service.ts +0 -820
  64. package/src/schema/agent.schema.ts +0 -162
  65. package/src/schema/audit.schema.ts +0 -57
  66. package/src/schema/channel.schema.ts +0 -38
  67. package/src/schema/credential.schema.ts +0 -58
  68. package/src/schema/deployment.schema.ts +0 -47
  69. package/src/schema/index.ts +0 -441
  70. package/src/schema/pikku-schema.types.ts +0 -169
  71. package/src/schema/schema.test.ts +0 -591
  72. package/src/schema/scope.schema.ts +0 -110
  73. package/src/schema/secret.schema.ts +0 -49
  74. package/src/schema/session.schema.ts +0 -20
  75. package/src/schema/virtual-user-schedule.schema.ts +0 -48
  76. package/src/schema/virtual-user.schema.ts +0 -106
  77. package/src/schema/webhook.schema.ts +0 -58
  78. package/src/schema/workflow.schema.ts +0 -150
  79. package/src/serialize-plugin.ts +0 -233
  80. package/tsconfig.json +0 -17
@@ -1,544 +0,0 @@
1
- import { describe, test, beforeEach, afterEach } from 'node:test'
2
- import assert from 'node:assert/strict'
3
- import { CamelCasePlugin, Kysely, SqliteDialect, sql } from 'kysely'
4
- import Database from 'better-sqlite3'
5
-
6
- import type { StepState } from '@pikku/core/workflow'
7
- import type { KyselyPikkuDB } from './kysely-tables.js'
8
- import { SerializePlugin } from './serialize-plugin.js'
9
- import { KyselyWorkflowService } from './kysely-workflow-service.js'
10
- import { applyPikkuSchemas, workflowSchema } from './schema/index.js'
11
-
12
- let db: Kysely<KyselyPikkuDB>
13
- let service: KyselyWorkflowService
14
- let queries: string[]
15
-
16
- /**
17
- * Every statement the service issues, so a test can assert on round-trip count
18
- * rather than on wall-clock — the only measure that stays stable in CI.
19
- */
20
- const createDb = () => {
21
- queries = []
22
- return new Kysely<KyselyPikkuDB>({
23
- dialect: new SqliteDialect({ database: new Database(':memory:') }),
24
- plugins: [new CamelCasePlugin(), new SerializePlugin()],
25
- log: (event) => {
26
- if (event.level === 'query') {
27
- queries.push(event.query.sql)
28
- }
29
- },
30
- })
31
- }
32
-
33
- beforeEach(async () => {
34
- db = createDb()
35
- await applyPikkuSchemas(db, [workflowSchema])
36
- service = new KyselyWorkflowService(db, { wireQueues: false } as any)
37
- await service.init()
38
- })
39
-
40
- afterEach(async () => {
41
- await db.destroy()
42
- })
43
-
44
- const listIndexes = async (table: string): Promise<string[]> => {
45
- const rows = await sql<{
46
- name: string
47
- }>`SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = ${table} AND name NOT LIKE 'sqlite_%'`.execute(
48
- db
49
- )
50
- return rows.rows.map((r) => r.name)
51
- }
52
-
53
- const indexedColumns = async (indexName: string): Promise<string[]> => {
54
- const rows = await sql<{
55
- name: string
56
- }>`SELECT name FROM pragma_index_info(${indexName})`.execute(db)
57
- return rows.rows.map((r) => r.name)
58
- }
59
-
60
- const seedRun = (name = 'wf') =>
61
- service.createRun(name, { foo: 1 }, false, 'hash1', {
62
- type: 'internal',
63
- } as any)
64
-
65
- describe('KyselyWorkflowService — schema indexes', () => {
66
- test('workflow_step_history is indexed by its step, the column every hot query filters on', async () => {
67
- const indexes = await listIndexes('workflow_step_history')
68
- assert.notEqual(
69
- indexes.length,
70
- 0,
71
- 'workflow_step_history has no indexes; the attempt-count subquery and the latest-attempt lookup both seq-scan it'
72
- )
73
-
74
- const columnsPerIndex = await Promise.all(
75
- indexes.map((name) => indexedColumns(name))
76
- )
77
- assert.ok(
78
- columnsPerIndex.some((cols) => cols[0] === 'workflow_step_id'),
79
- `expected an index led by workflow_step_id, got ${JSON.stringify(columnsPerIndex)}`
80
- )
81
- })
82
-
83
- test('the step-history index also orders by created_at, so the latest attempt is a lookup not a sort', async () => {
84
- const indexes = await listIndexes('workflow_step_history')
85
- const columnsPerIndex = await Promise.all(
86
- indexes.map((name) => indexedColumns(name))
87
- )
88
- assert.ok(
89
- columnsPerIndex.some(
90
- (cols) => cols[0] === 'workflow_step_id' && cols[1] === 'created_at'
91
- ),
92
- `expected a (workflow_step_id, created_at) index, got ${JSON.stringify(columnsPerIndex)}`
93
- )
94
- })
95
-
96
- test('workflow_runs is indexed for the status + recency listing', async () => {
97
- const indexes = await listIndexes('workflow_runs')
98
- const columnsPerIndex = await Promise.all(
99
- indexes.map((name) => indexedColumns(name))
100
- )
101
- assert.ok(
102
- columnsPerIndex.some((cols) => cols.includes('status')),
103
- `expected an index covering status, got ${JSON.stringify(columnsPerIndex)}`
104
- )
105
- })
106
-
107
- test('workflow_step is indexed for the per-run status scan the graph runner does every tick', async () => {
108
- const indexes = await listIndexes('workflow_step')
109
- const columnsPerIndex = await Promise.all(
110
- indexes.map((name) => indexedColumns(name))
111
- )
112
- assert.ok(
113
- columnsPerIndex.some(
114
- (cols) => cols[0] === 'workflow_run_id' && cols.includes('status')
115
- ),
116
- `expected a (workflow_run_id, status) index, got ${JSON.stringify(columnsPerIndex)}`
117
- )
118
- })
119
-
120
- test('workflow_versions is indexed for the version lookup', async () => {
121
- const indexes = await listIndexes('workflow_versions')
122
- assert.notEqual(
123
- indexes.length,
124
- 0,
125
- 'workflow_versions is queried on every version-mismatch replay with no index'
126
- )
127
- })
128
-
129
- test('init() is idempotent — a second boot does not throw on existing indexes', async () => {
130
- // A fresh instance, because `init()` returns at its `initialized` guard on
131
- // the same one — so calling it twice re-issues no DDL and never exercises
132
- // the duplicate-index path this is here to cover.
133
- await new KyselyWorkflowService(db, { wireQueues: false } as any).init()
134
-
135
- const indexes = await listIndexes('workflow_step_history')
136
- assert.ok(indexes.length > 0)
137
- })
138
- })
139
-
140
- /**
141
- * A step transition writes the step row and its history row, and nothing else.
142
- * BEGIN/COMMIT are excluded: they are the price of writing the pair atomically,
143
- * and counting them would make the assertion about transaction framing rather
144
- * than about the work being done.
145
- */
146
- const dataStatements = () =>
147
- queries.filter((q) => !/^\s*(begin|commit|rollback)/i.test(q))
148
-
149
- describe('KyselyWorkflowService — step transition round-trips', () => {
150
- const transitions: Array<[string, (stepId: string) => Promise<void>]> = [
151
- ['setStepRunning', (id) => service.setStepRunning(id)],
152
- ['setStepResult', (id) => service.setStepResult(id, { ok: true })],
153
- ['setStepError', (id) => service.setStepError(id, new Error('boom'))],
154
- ]
155
-
156
- for (const [name, run] of transitions) {
157
- test(`${name} writes two rows and reads none`, async () => {
158
- const runId = await seedRun()
159
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', {
160
- x: 1,
161
- })
162
-
163
- queries.length = 0
164
- await run(step.stepId)
165
-
166
- const statements = dataStatements()
167
- assert.equal(
168
- statements.length,
169
- 2,
170
- `${name} issued ${statements.length} statements:\n${statements.join('\n')}`
171
- )
172
- assert.ok(
173
- statements.every((q) => /^\s*update/i.test(q)),
174
- `${name} still reads before it writes — the latest-attempt lookup used to sort an unindexed history table:\n${statements.join('\n')}`
175
- )
176
- })
177
-
178
- test(`${name} writes both rows atomically`, async () => {
179
- const runId = await seedRun()
180
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', {
181
- x: 1,
182
- })
183
-
184
- queries.length = 0
185
- await run(step.stepId)
186
-
187
- assert.ok(
188
- /^\s*begin/i.test(queries[0] ?? ''),
189
- `${name} wrote the step and its history outside a transaction; a crash between them leaves the two disagreeing`
190
- )
191
- assert.ok(/^\s*commit/i.test(queries.at(-1) ?? ''))
192
- })
193
- }
194
- })
195
-
196
- describe('KyselyWorkflowService — step transitions stay consistent', () => {
197
- test('setStepResult writes the step row and its history row together', async () => {
198
- const runId = await seedRun()
199
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
200
- await service.setStepRunning(step.stepId)
201
- await service.setStepResult(step.stepId, { ok: true })
202
-
203
- const state = await service.getStepState(runId, 's1')
204
- assert.equal(state.status, 'succeeded')
205
- assert.deepEqual(state.result, { ok: true })
206
-
207
- const history = await service.getRunHistory(runId)
208
- const latest = history.at(-1)
209
- assert.equal(
210
- latest?.status,
211
- 'succeeded',
212
- 'the step row says succeeded but its latest history row does not'
213
- )
214
- assert.deepEqual(latest?.result, { ok: true })
215
- assert.ok(latest?.succeededAt, 'succeededAt was not stamped on history')
216
- })
217
-
218
- test('setStepError records the failure on both the step and its history', async () => {
219
- const runId = await seedRun()
220
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
221
- await service.setStepRunning(step.stepId)
222
- await service.setStepError(step.stepId, new Error('boom'))
223
-
224
- const state = await service.getStepState(runId, 's1')
225
- assert.equal(state.status, 'failed')
226
- assert.equal(state.error?.message, 'boom')
227
-
228
- const history = await service.getRunHistory(runId)
229
- const latest = history.at(-1)
230
- assert.equal(latest?.status, 'failed')
231
- assert.equal(latest?.error?.message, 'boom')
232
- assert.ok(latest?.failedAt, 'failedAt was not stamped on history')
233
- })
234
-
235
- test('a same-millisecond retry resolves the retry attempt, not the failed one', async () => {
236
- const runId = await seedRun()
237
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
238
- await service.setStepRunning(step.stepId)
239
- await service.setStepError(step.stepId, new Error('first'))
240
-
241
- // No delay: both history rows land in the same millisecond, so created_at
242
- // alone cannot say which is newest.
243
- const retry = await service.createRetryAttempt(step.stepId, 'running')
244
- await service.setStepResult(retry.stepId, { ok: true })
245
-
246
- const history = await service.getRunHistory(runId)
247
- assert.equal(
248
- history.length,
249
- 2,
250
- 'expected one attempt row plus one retry row'
251
- )
252
- assert.deepEqual(
253
- history.map((h) => h.status),
254
- ['failed', 'succeeded'],
255
- `the retry resolved the wrong history row: ${JSON.stringify(
256
- history.map((h) => h.status)
257
- )}`
258
- )
259
- })
260
- })
261
-
262
- describe('KyselyWorkflowService — attempt counting', () => {
263
- test('attemptCount tracks retries', async () => {
264
- const runId = await seedRun()
265
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
266
- assert.equal((await service.getStepState(runId, 's1')).attemptCount, 1)
267
-
268
- await service.setStepError(step.stepId, new Error('one'))
269
- await service.createRetryAttempt(step.stepId, 'running')
270
- assert.equal((await service.getStepState(runId, 's1')).attemptCount, 2)
271
-
272
- await service.setStepError(step.stepId, new Error('two'))
273
- const third = await service.createRetryAttempt(step.stepId, 'running')
274
- assert.equal(third.attemptCount, 3)
275
- assert.equal((await service.getStepState(runId, 's1')).attemptCount, 3)
276
- })
277
-
278
- test('reading a step does not touch the history table', async () => {
279
- const runId = await seedRun()
280
- await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
281
-
282
- queries.length = 0
283
- await service.getStepState(runId, 's1')
284
- assert.ok(
285
- !queries.join('\n').includes('workflow_step_history'),
286
- `the hottest read in the engine still aggregates over the history table:\n${queries.join('\n')}`
287
- )
288
- })
289
-
290
- test('a replay reads every step of a run in one query', async () => {
291
- const runId = await seedRun()
292
- for (let i = 0; i < 6; i++) {
293
- await service.insertStepState(runId, `s${i}`, 'rpc.fn', { i })
294
- }
295
-
296
- queries.length = 0
297
- const steps = await (service as any).listStepStates(runId)
298
- assert.equal(steps.length, 6)
299
- assert.equal(
300
- queries.length,
301
- 1,
302
- `expected one query for the whole run, got ${queries.length}`
303
- )
304
- assert.deepEqual(steps.map((s: any) => s.stepName).sort(), [
305
- 's0',
306
- 's1',
307
- 's2',
308
- 's3',
309
- 's4',
310
- 's5',
311
- ])
312
- })
313
- })
314
-
315
- describe('KyselyWorkflowService — run state writes', () => {
316
- test('concurrent updateRunState calls for different keys do not clobber each other', async () => {
317
- const runId = await seedRun('wf2')
318
-
319
- await Promise.all([
320
- service.updateRunState(runId, 'alpha', 1),
321
- service.updateRunState(runId, 'beta', 2),
322
- service.updateRunState(runId, 'gamma', 3),
323
- ])
324
-
325
- assert.deepEqual(
326
- await service.getRunState(runId),
327
- { alpha: 1, beta: 2, gamma: 3 },
328
- 'a concurrent read-modify-write over the whole state blob lost a key'
329
- )
330
- })
331
-
332
- test('updateRunState costs a single statement', async () => {
333
- const runId = await seedRun('wf3')
334
- queries.length = 0
335
- await service.updateRunState(runId, 'alpha', 1)
336
- assert.ok(
337
- queries.length <= 1,
338
- `updateRunState issued ${queries.length} statements:\n${queries.join('\n')}`
339
- )
340
- })
341
-
342
- test('updateRunState still replaces an existing key', async () => {
343
- const runId = await seedRun('wf4')
344
- await service.updateRunState(runId, 'alpha', 1)
345
- await service.updateRunState(runId, 'alpha', { nested: true })
346
- assert.deepEqual(await service.getRunState(runId), {
347
- alpha: { nested: true },
348
- })
349
- })
350
-
351
- test('values keep their JSON type instead of becoming strings', async () => {
352
- const runId = await seedRun('wf5')
353
-
354
- await service.updateRunState(runId, 'empty', [])
355
- await service.updateRunState(runId, 'zero', 0)
356
- await service.updateRunState(runId, 'no', false)
357
- await service.updateRunState(runId, 'nothing', null)
358
- await service.updateRunState(runId, 'deep', { a: [1, { b: true }] })
359
-
360
- assert.deepEqual(await service.getRunState(runId), {
361
- empty: [],
362
- zero: 0,
363
- no: false,
364
- nothing: null,
365
- deep: { a: [1, { b: true }] },
366
- })
367
- })
368
-
369
- test('a key containing a dot stays one key', async () => {
370
- const runId = await seedRun('wf6')
371
-
372
- await service.updateRunState(runId, 'a.b', 'flat')
373
-
374
- assert.deepEqual(
375
- await service.getRunState(runId),
376
- { 'a.b': 'flat' },
377
- 'the key was read as a nested path instead of a literal name'
378
- )
379
- })
380
-
381
- test('a key containing a quote does not break the statement', async () => {
382
- const runId = await seedRun('wf7')
383
-
384
- await service.updateRunState(runId, 'it\'s "quoted"', 1)
385
-
386
- assert.deepEqual(await service.getRunState(runId), { 'it\'s "quoted"': 1 })
387
- })
388
- })
389
-
390
- /**
391
- * A step whose `current_attempt` is NULL addresses no history row, so the
392
- * history half of a transition silently writes nothing while the step half
393
- * commits — the divergence the transaction exists to prevent.
394
- */
395
- describe('KyselyWorkflowService — a transition that addresses no history row', () => {
396
- const orphanStep = async () => {
397
- const runId = await seedRun('wf-orphan')
398
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
399
- await sql`UPDATE workflow_step SET current_attempt = NULL WHERE workflow_step_id = ${step.stepId}`.execute(
400
- db
401
- )
402
- return { runId, step }
403
- }
404
-
405
- test('the history still records the outcome', async () => {
406
- const { runId, step } = await orphanStep()
407
-
408
- await service.setStepResult(step.stepId, { ok: true })
409
-
410
- const history = await service.getRunHistory(runId)
411
- assert.equal(
412
- history.at(-1)?.status,
413
- 'succeeded',
414
- 'the step row says succeeded but no history row does'
415
- )
416
- assert.deepEqual(history.at(-1)?.result, { ok: true })
417
- })
418
-
419
- test('the step is repaired, so the next transition lands in place', async () => {
420
- const { runId, step } = await orphanStep()
421
-
422
- await service.setStepRunning(step.stepId)
423
- await service.setStepResult(step.stepId, { ok: true })
424
-
425
- const history = await service.getRunHistory(runId)
426
- assert.equal(
427
- history.length,
428
- 1,
429
- 'each transition appended a row instead of updating the one already there'
430
- )
431
- assert.equal(history[0]!.status, 'succeeded')
432
- })
433
- })
434
-
435
- /**
436
- * Dispatch is at-least-once — the relay re-dispatches a step it believes was
437
- * dropped, and a queue can redeliver a job it already handed out — so the claim
438
- * is the only thing standing between a duplicate dispatch and a second
439
- * execution of a side-effecting step.
440
- */
441
- describe('KyselyWorkflowService — claiming a step for execution', () => {
442
- const claim = (runId: string, stepName: string, rpcName = 'rpc.fn') =>
443
- (service as any).claimStepForExecution(
444
- runId,
445
- stepName,
446
- rpcName
447
- ) as Promise<StepState | null>
448
-
449
- test('two dispatches racing for the same pending step: exactly one wins', async () => {
450
- const runId = await seedRun()
451
- await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
452
-
453
- const claims = await Promise.all([claim(runId, 's1'), claim(runId, 's1')])
454
- const winners = claims.filter((c) => c !== null)
455
-
456
- assert.equal(
457
- winners.length,
458
- 1,
459
- `both dispatches claimed the step, so a side-effecting step would run twice: ${JSON.stringify(claims)}`
460
- )
461
- assert.equal((await service.getStepState(runId, 's1')).status, 'running')
462
- })
463
-
464
- test('two dispatches racing to retry the same failed step: exactly one wins', async () => {
465
- const runId = await seedRun()
466
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
467
- await service.setStepRunning(step.stepId)
468
- await service.setStepError(step.stepId, new Error('boom'))
469
-
470
- const claims = await Promise.all([claim(runId, 's1'), claim(runId, 's1')])
471
- const winners = claims.filter((c) => c !== null)
472
-
473
- assert.equal(
474
- winners.length,
475
- 1,
476
- `both dispatches started a retry, so the failed step would be retried twice concurrently: ${JSON.stringify(claims)}`
477
- )
478
- assert.equal(
479
- (await service.getStepState(runId, 's1')).attemptCount,
480
- 2,
481
- 'the retry raced itself and burned more than one attempt'
482
- )
483
- })
484
-
485
- test('a step already claimed is not claimable again', async () => {
486
- const runId = await seedRun()
487
- await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
488
-
489
- assert.notEqual(await claim(runId, 's1'), null)
490
- assert.equal(
491
- await claim(runId, 's1'),
492
- null,
493
- 'a redelivered job re-claimed a step that is already running'
494
- )
495
- })
496
-
497
- test('a succeeded step is not claimable', async () => {
498
- const runId = await seedRun()
499
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
500
- await service.setStepRunning(step.stepId)
501
- await service.setStepResult(step.stepId, { ok: true })
502
-
503
- assert.equal(await claim(runId, 's1'), null)
504
- })
505
-
506
- test('a claim that fails releases the step, so it is retryable', async () => {
507
- const runId = await seedRun()
508
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
509
-
510
- const first = await claim(runId, 's1')
511
- assert.notEqual(first, null)
512
- await service.setStepError(step.stepId, new Error('boom'))
513
-
514
- const retry = await claim(runId, 's1')
515
- assert.notEqual(
516
- retry,
517
- null,
518
- 'the failed step stayed claimed, so it can never be retried'
519
- )
520
- assert.equal(retry?.status, 'running')
521
- assert.equal(retry?.attemptCount, 2)
522
- })
523
-
524
- test('a claim for a different function than the step was dispatched with is refused', async () => {
525
- const runId = await seedRun()
526
- await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
527
-
528
- await assert.rejects(() => claim(runId, 's1', 'rpc.other'))
529
- assert.equal(
530
- (await service.getStepState(runId, 's1')).status,
531
- 'pending',
532
- 'the mismatched claim moved the step anyway'
533
- )
534
- })
535
-
536
- test('a scheduled step is claimable, and only once', async () => {
537
- const runId = await seedRun()
538
- const step = await service.insertStepState(runId, 's1', 'rpc.fn', { x: 1 })
539
- await service.setStepScheduled(step.stepId)
540
-
541
- const claims = await Promise.all([claim(runId, 's1'), claim(runId, 's1')])
542
- assert.equal(claims.filter((c) => c !== null).length, 1)
543
- })
544
- })