@elaraai/e3-api-tests 0.0.2-beta.31 → 0.0.2-beta.33

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 (42) hide show
  1. package/README.md +23 -21
  2. package/dist/src/index.d.ts +18 -23
  3. package/dist/src/index.d.ts.map +1 -1
  4. package/dist/src/index.js +13 -47
  5. package/dist/src/index.js.map +1 -1
  6. package/dist/src/setup.d.ts +11 -0
  7. package/dist/src/setup.d.ts.map +1 -0
  8. package/dist/src/setup.js +6 -0
  9. package/dist/src/setup.js.map +1 -0
  10. package/dist/src/suites/cli.d.ts +3 -2
  11. package/dist/src/suites/cli.d.ts.map +1 -1
  12. package/dist/src/suites/cli.js +47 -37
  13. package/dist/src/suites/cli.js.map +1 -1
  14. package/dist/src/suites/dataflow.d.ts +3 -2
  15. package/dist/src/suites/dataflow.d.ts.map +1 -1
  16. package/dist/src/suites/dataflow.js +128 -206
  17. package/dist/src/suites/dataflow.js.map +1 -1
  18. package/dist/src/suites/datasets.d.ts +3 -2
  19. package/dist/src/suites/datasets.d.ts.map +1 -1
  20. package/dist/src/suites/datasets.js +22 -22
  21. package/dist/src/suites/datasets.js.map +1 -1
  22. package/dist/src/suites/packages.d.ts +3 -2
  23. package/dist/src/suites/packages.d.ts.map +1 -1
  24. package/dist/src/suites/packages.js +24 -25
  25. package/dist/src/suites/packages.js.map +1 -1
  26. package/dist/src/suites/platform.d.ts +3 -2
  27. package/dist/src/suites/platform.d.ts.map +1 -1
  28. package/dist/src/suites/platform.js +21 -21
  29. package/dist/src/suites/platform.js.map +1 -1
  30. package/dist/src/suites/repository.d.ts +3 -2
  31. package/dist/src/suites/repository.d.ts.map +1 -1
  32. package/dist/src/suites/repository.js +19 -19
  33. package/dist/src/suites/repository.js.map +1 -1
  34. package/dist/src/suites/transfer.d.ts +3 -2
  35. package/dist/src/suites/transfer.d.ts.map +1 -1
  36. package/dist/src/suites/transfer.js +43 -36
  37. package/dist/src/suites/transfer.js.map +1 -1
  38. package/dist/src/suites/workspaces.d.ts +3 -2
  39. package/dist/src/suites/workspaces.d.ts.map +1 -1
  40. package/dist/src/suites/workspaces.js +31 -42
  41. package/dist/src/suites/workspaces.js.map +1 -1
  42. package/package.json +1 -1
@@ -7,31 +7,65 @@
7
7
  *
8
8
  * Tests: start, execute (blocking), poll for completion, logs
9
9
  */
10
- import { describe, it, beforeEach } from 'node:test';
10
+ import { describe, it } from 'node:test';
11
11
  import assert from 'node:assert/strict';
12
12
  import { readFileSync } from 'node:fs';
13
13
  import { packageImport, workspaceCreate, workspaceDeploy, workspaceStatus, dataflowStart, dataflowExecute, dataflowExecution, dataflowCancel, dataflowGraph, taskLogs, ApiError, } from '@elaraai/e3-api-client';
14
14
  import { createPackageZip, createDiamondPackageZip, createFailingPackageZip, createSlowPackageZip, createParallelMixedPackageZip, createFailingDiamondPackageZip, createWideParallelPackageZip, } from '../fixtures.js';
15
+ /** Helper: import package, create workspace, deploy */
16
+ function withDeployed(setup, createZip, pkgName, wsName) {
17
+ return async (t) => {
18
+ const ctx = await setup(t);
19
+ const opts = await ctx.opts();
20
+ const zipPath = await createZip(ctx.tempDir, pkgName, '1.0.0');
21
+ const packageZip = readFileSync(zipPath);
22
+ await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
23
+ await workspaceCreate(ctx.config.baseUrl, ctx.repoName, wsName, opts);
24
+ await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, wsName, `${pkgName}@1.0.0`, opts);
25
+ return ctx;
26
+ };
27
+ }
15
28
  /**
16
29
  * Register dataflow execution tests.
17
30
  *
18
- * @param getContext - Function that returns the current test context
31
+ * @param setup - Factory that creates a fresh test context per test
19
32
  */
20
- export function dataflowTests(getContext) {
21
- describe('dataflow', () => {
22
- describe('simple execution', () => {
23
- beforeEach(async () => {
24
- const ctx = getContext();
25
- const opts = await ctx.opts();
26
- // Create and import a simple package
27
- const zipPath = await createPackageZip(ctx.tempDir, 'exec-pkg', '1.0.0');
28
- const packageZip = readFileSync(zipPath);
29
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
30
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'exec-ws', opts);
31
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'exec-ws', 'exec-pkg@1.0.0', opts);
32
- });
33
- it('dataflowExecute runs tasks and returns result (blocking)', async () => {
34
- const ctx = getContext();
33
+ export function dataflowTests(setup) {
34
+ const withSimpleExec = withDeployed(setup, createPackageZip, 'exec-pkg', 'exec-ws');
35
+ const withDiamond = withDeployed(setup, createDiamondPackageZip, 'diamond-pkg', 'diamond-ws');
36
+ const withFailing = withDeployed(setup, createFailingPackageZip, 'fail-pkg', 'fail-ws');
37
+ const withMixed = withDeployed(setup, createParallelMixedPackageZip, 'mixed-pkg', 'mixed-ws');
38
+ const withFailingDiamond = withDeployed(setup, createFailingDiamondPackageZip, 'fdiamond-pkg', 'fdiamond-ws');
39
+ const withWideParallel = async (t) => {
40
+ const ctx = await setup(t);
41
+ const opts = await ctx.opts();
42
+ const zipPath = await createWideParallelPackageZip(ctx.tempDir, 'wide-pkg', '1.0.0', 6);
43
+ const packageZip = readFileSync(zipPath);
44
+ await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
45
+ await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'wide-ws', opts);
46
+ await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'wide-ws', 'wide-pkg@1.0.0', opts);
47
+ return ctx;
48
+ };
49
+ const withSlow = async (t) => {
50
+ const ctx = await setup(t);
51
+ const opts = await ctx.opts();
52
+ const zipPath = await createSlowPackageZip(ctx.tempDir, 'slow-pkg', '1.0.0', 30);
53
+ const packageZip = readFileSync(zipPath);
54
+ await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
55
+ await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'slow-ws', opts);
56
+ await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'slow-ws', 'slow-pkg@1.0.0', opts);
57
+ return ctx;
58
+ };
59
+ const withNoExec = withDeployed(setup, createPackageZip, 'noexec-pkg', 'noexec-ws');
60
+ const withCache = withDeployed(setup, createPackageZip, 'cache-pkg', 'cache-ws');
61
+ const withFilter = withDeployed(setup, createDiamondPackageZip, 'filter-pkg', 'filter-ws');
62
+ const withGraph = withDeployed(setup, createDiamondPackageZip, 'graph-pkg', 'graph-ws');
63
+ const withLogPag = withDeployed(setup, createPackageZip, 'logpag-pkg', 'logpag-ws');
64
+ const withEvtPag = withDeployed(setup, createDiamondPackageZip, 'evtpag-pkg', 'evtpag-ws');
65
+ describe('dataflow', { concurrency: true }, () => {
66
+ describe('simple execution', { concurrency: true }, () => {
67
+ it('dataflowExecute runs tasks and returns result (blocking)', async (t) => {
68
+ const ctx = await withSimpleExec(t);
35
69
  const opts = await ctx.opts();
36
70
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'exec-ws', { force: true }, opts);
37
71
  // Verify execution result
@@ -50,8 +84,8 @@ export function dataflowTests(getContext) {
50
84
  assert.ok(outputDataset, 'Output dataset .tasks.compute.output should exist');
51
85
  assert.strictEqual(outputDataset.status.type, 'up-to-date');
52
86
  });
53
- it('dataflowStart triggers execution (non-blocking)', async () => {
54
- const ctx = getContext();
87
+ it('dataflowStart triggers execution (non-blocking)', async (t) => {
88
+ const ctx = await withSimpleExec(t);
55
89
  const opts = await ctx.opts();
56
90
  // Should return immediately
57
91
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'exec-ws', { force: true }, opts);
@@ -71,8 +105,8 @@ export function dataflowTests(getContext) {
71
105
  // Verify execution completed
72
106
  assert.strictEqual(status.tasks[0].status.type, 'up-to-date');
73
107
  });
74
- it('dataflowExecution returns execution state', async () => {
75
- const ctx = getContext();
108
+ it('dataflowExecution returns execution state', async (t) => {
109
+ const ctx = await withSimpleExec(t);
76
110
  const opts = await ctx.opts();
77
111
  // Start execution
78
112
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'exec-ws', { force: true }, opts);
@@ -97,8 +131,8 @@ export function dataflowTests(getContext) {
97
131
  }
98
132
  assert.fail('Execution did not complete in time');
99
133
  });
100
- it('taskLogs returns logs after execution', async () => {
101
- const ctx = getContext();
134
+ it('taskLogs returns logs after execution', async (t) => {
135
+ const ctx = await withSimpleExec(t);
102
136
  const opts = await ctx.opts();
103
137
  // Execute first
104
138
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'exec-ws', { force: true }, opts);
@@ -112,19 +146,9 @@ export function dataflowTests(getContext) {
112
146
  assert.ok(typeof logs.complete === 'boolean');
113
147
  });
114
148
  });
115
- describe('diamond dependency execution', () => {
116
- beforeEach(async () => {
117
- const ctx = getContext();
118
- const opts = await ctx.opts();
119
- // Create and import diamond package (left, right, merge tasks)
120
- const zipPath = await createDiamondPackageZip(ctx.tempDir, 'diamond-pkg', '1.0.0');
121
- const packageZip = readFileSync(zipPath);
122
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
123
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'diamond-ws', opts);
124
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'diamond-ws', 'diamond-pkg@1.0.0', opts);
125
- });
126
- it('executes diamond dependency graph correctly', async () => {
127
- const ctx = getContext();
149
+ describe('diamond dependency execution', { concurrency: true }, () => {
150
+ it('executes diamond dependency graph correctly', async (t) => {
151
+ const ctx = await withDiamond(t);
128
152
  const opts = await ctx.opts();
129
153
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'diamond-ws', { force: true }, opts);
130
154
  // Should execute all three tasks
@@ -137,8 +161,8 @@ export function dataflowTests(getContext) {
137
161
  assert.strictEqual(task.state.type, 'success', `Task ${task.name} should succeed`);
138
162
  }
139
163
  });
140
- it('tracks events during execution', async () => {
141
- const ctx = getContext();
164
+ it('tracks events during execution', async (t) => {
165
+ const ctx = await withDiamond(t);
142
166
  const opts = await ctx.opts();
143
167
  // Start execution
144
168
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'diamond-ws', { force: true }, opts);
@@ -162,19 +186,9 @@ export function dataflowTests(getContext) {
162
186
  assert.ok(completeEvents.length >= 3, `Expected at least 3 complete events, got ${completeEvents.length}`);
163
187
  });
164
188
  });
165
- describe('failed execution', () => {
166
- beforeEach(async () => {
167
- const ctx = getContext();
168
- const opts = await ctx.opts();
169
- // Create and import a package with a failing task
170
- const zipPath = await createFailingPackageZip(ctx.tempDir, 'fail-pkg', '1.0.0');
171
- const packageZip = readFileSync(zipPath);
172
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
173
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'fail-ws', opts);
174
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'fail-ws', 'fail-pkg@1.0.0', opts);
175
- });
176
- it('dataflowExecute returns failure result when task fails', async () => {
177
- const ctx = getContext();
189
+ describe('failed execution', { concurrency: true }, () => {
190
+ it('dataflowExecute returns failure result when task fails', async (t) => {
191
+ const ctx = await withFailing(t);
178
192
  const opts = await ctx.opts();
179
193
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'fail-ws', { force: true }, opts);
180
194
  // Execution should report failure
@@ -185,8 +199,8 @@ export function dataflowTests(getContext) {
185
199
  assert.strictEqual(result.tasks[0].name, 'failing');
186
200
  assert.strictEqual(result.tasks[0].state.type, 'failed');
187
201
  });
188
- it('dataflowExecution shows failed status after task failure', async () => {
189
- const ctx = getContext();
202
+ it('dataflowExecution shows failed status after task failure', async (t) => {
203
+ const ctx = await withFailing(t);
190
204
  const opts = await ctx.opts();
191
205
  // Start execution
192
206
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'fail-ws', { force: true }, opts);
@@ -210,8 +224,8 @@ export function dataflowTests(getContext) {
210
224
  }
211
225
  assert.fail('Execution did not complete in time');
212
226
  });
213
- it('can restart execution after failure', async () => {
214
- const ctx = getContext();
227
+ it('can restart execution after failure', async (t) => {
228
+ const ctx = await withFailing(t);
215
229
  const opts = await ctx.opts();
216
230
  // First execution - should fail
217
231
  const result1 = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'fail-ws', { force: true }, opts);
@@ -222,19 +236,10 @@ export function dataflowTests(getContext) {
222
236
  assert.strictEqual(result2.failed, 1n);
223
237
  });
224
238
  });
225
- describe('parallel task failures', () => {
226
- describe('mixed success/failure', () => {
227
- beforeEach(async () => {
228
- const ctx = getContext();
229
- const opts = await ctx.opts();
230
- const zipPath = await createParallelMixedPackageZip(ctx.tempDir, 'mixed-pkg', '1.0.0');
231
- const packageZip = readFileSync(zipPath);
232
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
233
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'mixed-ws', opts);
234
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'mixed-ws', 'mixed-pkg@1.0.0', opts);
235
- });
236
- it('parallel tasks with mixed success/failure complete without stalling', async () => {
237
- const ctx = getContext();
239
+ describe('parallel task failures', { concurrency: true }, () => {
240
+ describe('mixed success/failure', { concurrency: true }, () => {
241
+ it('parallel tasks with mixed success/failure complete without stalling', async (t) => {
242
+ const ctx = await withMixed(t);
238
243
  const opts = await ctx.opts();
239
244
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'mixed-ws', { force: true }, opts);
240
245
  // Dataflow should complete (not stall) and report failure
@@ -245,16 +250,14 @@ export function dataflowTests(getContext) {
245
250
  assert.ok(failC, 'fail_c task should be in results');
246
251
  assert.strictEqual(failC.state.type, 'failed');
247
252
  // Tasks that did execute should have succeeded
248
- // (orchestrator may stop launching new tasks after a failure,
249
- // so not all succeed tasks are guaranteed to have run)
250
253
  for (const task of result.tasks) {
251
254
  if (task.name !== 'fail_c') {
252
255
  assert.strictEqual(task.state.type, 'success', `Task ${task.name} should succeed`);
253
256
  }
254
257
  }
255
258
  });
256
- it('failed task logs are accessible', async () => {
257
- const ctx = getContext();
259
+ it('failed task logs are accessible', async (t) => {
260
+ const ctx = await withMixed(t);
258
261
  const opts = await ctx.opts();
259
262
  // Execute first to generate logs
260
263
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'mixed-ws', { force: true }, opts);
@@ -263,8 +266,8 @@ export function dataflowTests(getContext) {
263
266
  assert.ok(typeof logs.data === 'string', 'logs.data should be a string');
264
267
  assert.ok(typeof logs.complete === 'boolean', 'logs.complete should be a boolean');
265
268
  });
266
- it('workspace status reflects failed tasks correctly', async () => {
267
- const ctx = getContext();
269
+ it('workspace status reflects failed tasks correctly', async (t) => {
270
+ const ctx = await withMixed(t);
268
271
  const opts = await ctx.opts();
269
272
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'mixed-ws', { force: true }, opts);
270
273
  const status = await workspaceStatus(ctx.config.baseUrl, ctx.repoName, 'mixed-ws', opts);
@@ -278,18 +281,9 @@ export function dataflowTests(getContext) {
278
281
  }
279
282
  });
280
283
  });
281
- describe('diamond with upstream failure', () => {
282
- beforeEach(async () => {
283
- const ctx = getContext();
284
- const opts = await ctx.opts();
285
- const zipPath = await createFailingDiamondPackageZip(ctx.tempDir, 'fdiamond-pkg', '1.0.0');
286
- const packageZip = readFileSync(zipPath);
287
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
288
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'fdiamond-ws', opts);
289
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'fdiamond-ws', 'fdiamond-pkg@1.0.0', opts);
290
- });
291
- it('diamond with upstream failure skips dependents', async () => {
292
- const ctx = getContext();
284
+ describe('diamond with upstream failure', { concurrency: true }, () => {
285
+ it('diamond with upstream failure skips dependents', async (t) => {
286
+ const ctx = await withFailingDiamond(t);
293
287
  const opts = await ctx.opts();
294
288
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'fdiamond-ws', { force: true }, opts);
295
289
  assert.strictEqual(result.success, false);
@@ -306,8 +300,8 @@ export function dataflowTests(getContext) {
306
300
  assert.strictEqual(rightTask.state.type, 'failed');
307
301
  assert.strictEqual(mergeTask.state.type, 'skipped');
308
302
  });
309
- it('taskLogs returns execution_not_found for skipped task', async () => {
310
- const ctx = getContext();
303
+ it('taskLogs returns execution_not_found for skipped task', async (t) => {
304
+ const ctx = await withFailingDiamond(t);
311
305
  const opts = await ctx.opts();
312
306
  // Execute — merge will be skipped because right fails
313
307
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'fdiamond-ws', { force: true }, opts);
@@ -321,18 +315,9 @@ export function dataflowTests(getContext) {
321
315
  }
322
316
  });
323
317
  });
324
- describe('wide parallel execution', () => {
325
- beforeEach(async () => {
326
- const ctx = getContext();
327
- const opts = await ctx.opts();
328
- const zipPath = await createWideParallelPackageZip(ctx.tempDir, 'wide-pkg', '1.0.0', 6);
329
- const packageZip = readFileSync(zipPath);
330
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
331
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'wide-ws', opts);
332
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'wide-ws', 'wide-pkg@1.0.0', opts);
333
- });
334
- it('wide parallel execution completes correctly', async () => {
335
- const ctx = getContext();
318
+ describe('wide parallel execution', { concurrency: true }, () => {
319
+ it('wide parallel execution completes correctly', async (t) => {
320
+ const ctx = await withWideParallel(t);
336
321
  const opts = await ctx.opts();
337
322
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'wide-ws', { force: true }, opts);
338
323
  assert.strictEqual(result.success, true);
@@ -345,19 +330,11 @@ export function dataflowTests(getContext) {
345
330
  });
346
331
  });
347
332
  });
333
+ // Concurrent execution tests must remain serial within their describe
334
+ // because they test locking behavior with timing-sensitive operations
348
335
  describe('concurrent execution', () => {
349
- beforeEach(async () => {
350
- const ctx = getContext();
351
- const opts = await ctx.opts();
352
- // Create and import a slow package
353
- const zipPath = await createSlowPackageZip(ctx.tempDir, 'slow-pkg', '1.0.0', 30);
354
- const packageZip = readFileSync(zipPath);
355
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
356
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'slow-ws', opts);
357
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'slow-ws', 'slow-pkg@1.0.0', opts);
358
- });
359
- it('rejects second dataflowStart while execution is running', async () => {
360
- const ctx = getContext();
336
+ it('rejects second dataflowStart while execution is running', async (t) => {
337
+ const ctx = await withSlow(t);
361
338
  const opts = await ctx.opts();
362
339
  // Start first execution (non-blocking)
363
340
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'slow-ws', { force: true }, opts);
@@ -375,8 +352,8 @@ export function dataflowTests(getContext) {
375
352
  assert.ok(message.includes('lock') || message.includes('running') || message.includes('busy'), `Expected lock-related error, got: ${err.message}`);
376
353
  }
377
354
  });
378
- it('rejects dataflowExecute while execution is running', async () => {
379
- const ctx = getContext();
355
+ it('rejects dataflowExecute while execution is running', async (t) => {
356
+ const ctx = await withSlow(t);
380
357
  const opts = await ctx.opts();
381
358
  // Start first execution (non-blocking)
382
359
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'slow-ws', { force: true }, opts);
@@ -393,8 +370,8 @@ export function dataflowTests(getContext) {
393
370
  assert.ok(message.includes('lock') || message.includes('running') || message.includes('busy'), `Expected lock-related error, got: ${err.message}`);
394
371
  }
395
372
  });
396
- it('dataflowCancel stops a running execution', async () => {
397
- const ctx = getContext();
373
+ it('dataflowCancel stops a running execution', async (t) => {
374
+ const ctx = await withSlow(t);
398
375
  const opts = await ctx.opts();
399
376
  // Start slow execution
400
377
  await dataflowStart(ctx.config.baseUrl, ctx.repoName, 'slow-ws', { force: true }, opts);
@@ -406,8 +383,8 @@ export function dataflowTests(getContext) {
406
383
  const state = await dataflowExecution(ctx.config.baseUrl, ctx.repoName, 'slow-ws', {}, opts);
407
384
  assert.strictEqual(state.status.type, 'aborted');
408
385
  });
409
- it('dataflowCancel returns error when no execution is running', async () => {
410
- const ctx = getContext();
386
+ it('dataflowCancel returns error when no execution is running', async (t) => {
387
+ const ctx = await withSlow(t);
411
388
  const opts = await ctx.opts();
412
389
  // Try to cancel when nothing is running
413
390
  try {
@@ -420,19 +397,9 @@ export function dataflowTests(getContext) {
420
397
  }
421
398
  });
422
399
  });
423
- describe('execution not found', () => {
424
- beforeEach(async () => {
425
- const ctx = getContext();
426
- const opts = await ctx.opts();
427
- // Create and import a simple package, deploy but do NOT execute
428
- const zipPath = await createPackageZip(ctx.tempDir, 'noexec-pkg', '1.0.0');
429
- const packageZip = readFileSync(zipPath);
430
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
431
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'noexec-ws', opts);
432
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'noexec-ws', 'noexec-pkg@1.0.0', opts);
433
- });
434
- it('taskLogs returns execution_not_found for never-executed task', async () => {
435
- const ctx = getContext();
400
+ describe('execution not found', { concurrency: true }, () => {
401
+ it('taskLogs returns execution_not_found for never-executed task', async (t) => {
402
+ const ctx = await withNoExec(t);
436
403
  const opts = await ctx.opts();
437
404
  try {
438
405
  await taskLogs(ctx.config.baseUrl, ctx.repoName, 'noexec-ws', 'compute', { stream: 'stdout' }, opts);
@@ -443,8 +410,8 @@ export function dataflowTests(getContext) {
443
410
  assert.strictEqual(err.code, 'execution_not_found');
444
411
  }
445
412
  });
446
- it('taskLogs returns task_not_found for non-existent task', async () => {
447
- const ctx = getContext();
413
+ it('taskLogs returns task_not_found for non-existent task', async (t) => {
414
+ const ctx = await withNoExec(t);
448
415
  const opts = await ctx.opts();
449
416
  try {
450
417
  await taskLogs(ctx.config.baseUrl, ctx.repoName, 'noexec-ws', 'no_such_task', { stream: 'stdout' }, opts);
@@ -455,8 +422,8 @@ export function dataflowTests(getContext) {
455
422
  assert.strictEqual(err.code, 'task_not_found');
456
423
  }
457
424
  });
458
- it('taskLogs returns workspace_not_found for non-existent workspace', async () => {
459
- const ctx = getContext();
425
+ it('taskLogs returns workspace_not_found for non-existent workspace', async (t) => {
426
+ const ctx = await withNoExec(t);
460
427
  const opts = await ctx.opts();
461
428
  try {
462
429
  await taskLogs(ctx.config.baseUrl, ctx.repoName, 'no_such_ws', 'compute', { stream: 'stdout' }, opts);
@@ -468,9 +435,9 @@ export function dataflowTests(getContext) {
468
435
  }
469
436
  });
470
437
  });
471
- describe('workspace error handling', () => {
472
- it('dataflowExecute returns error for non-existent workspace', async () => {
473
- const ctx = getContext();
438
+ describe('workspace error handling', { concurrency: true }, () => {
439
+ it('dataflowExecute returns error for non-existent workspace', async (t) => {
440
+ const ctx = await setup(t);
474
441
  const opts = await ctx.opts();
475
442
  try {
476
443
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'no_such_ws', { force: true }, opts);
@@ -481,8 +448,8 @@ export function dataflowTests(getContext) {
481
448
  assert.ok(err.code === 'workspace_not_found' || err.code === 'workspace_not_deployed', `Expected workspace_not_found or workspace_not_deployed, got ${err.code}`);
482
449
  }
483
450
  });
484
- it('dataflowGraph returns error for non-existent workspace', async () => {
485
- const ctx = getContext();
451
+ it('dataflowGraph returns error for non-existent workspace', async (t) => {
452
+ const ctx = await setup(t);
486
453
  const opts = await ctx.opts();
487
454
  try {
488
455
  await dataflowGraph(ctx.config.baseUrl, ctx.repoName, 'no_such_ws', opts);
@@ -493,8 +460,8 @@ export function dataflowTests(getContext) {
493
460
  assert.ok(err.code === 'workspace_not_found' || err.code === 'workspace_not_deployed', `Expected workspace_not_found or workspace_not_deployed, got ${err.code}`);
494
461
  }
495
462
  });
496
- it('workspaceStatus returns error for non-existent workspace', async () => {
497
- const ctx = getContext();
463
+ it('workspaceStatus returns error for non-existent workspace', async (t) => {
464
+ const ctx = await setup(t);
498
465
  const opts = await ctx.opts();
499
466
  try {
500
467
  await workspaceStatus(ctx.config.baseUrl, ctx.repoName, 'no_such_ws', opts);
@@ -506,18 +473,9 @@ export function dataflowTests(getContext) {
506
473
  }
507
474
  });
508
475
  });
509
- describe('cache behavior', () => {
510
- beforeEach(async () => {
511
- const ctx = getContext();
512
- const opts = await ctx.opts();
513
- const zipPath = await createPackageZip(ctx.tempDir, 'cache-pkg', '1.0.0');
514
- const packageZip = readFileSync(zipPath);
515
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
516
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'cache-ws', opts);
517
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'cache-ws', 'cache-pkg@1.0.0', opts);
518
- });
519
- it('second execution uses cached results', async () => {
520
- const ctx = getContext();
476
+ describe('cache behavior', { concurrency: true }, () => {
477
+ it('second execution uses cached results', async (t) => {
478
+ const ctx = await withCache(t);
521
479
  const opts = await ctx.opts();
522
480
  // First execution - should execute the task
523
481
  const result1 = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'cache-ws', { force: false }, opts);
@@ -529,8 +487,8 @@ export function dataflowTests(getContext) {
529
487
  assert.ok(result2.cached > 0n, `Expected cached > 0, got ${result2.cached}`);
530
488
  assert.strictEqual(result2.executed, 0n);
531
489
  });
532
- it('force bypasses cache', async () => {
533
- const ctx = getContext();
490
+ it('force bypasses cache', async (t) => {
491
+ const ctx = await withCache(t);
534
492
  const opts = await ctx.opts();
535
493
  // First execution
536
494
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'cache-ws', { force: false }, opts);
@@ -540,18 +498,9 @@ export function dataflowTests(getContext) {
540
498
  assert.ok(result.executed > 0n, `Expected executed > 0, got ${result.executed}`);
541
499
  });
542
500
  });
543
- describe('task filter', () => {
544
- beforeEach(async () => {
545
- const ctx = getContext();
546
- const opts = await ctx.opts();
547
- const zipPath = await createDiamondPackageZip(ctx.tempDir, 'filter-pkg', '1.0.0');
548
- const packageZip = readFileSync(zipPath);
549
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
550
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'filter-ws', opts);
551
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'filter-ws', 'filter-pkg@1.0.0', opts);
552
- });
553
- it('filter runs only the specified task', async () => {
554
- const ctx = getContext();
501
+ describe('task filter', { concurrency: true }, () => {
502
+ it('filter runs only the specified task', async (t) => {
503
+ const ctx = await withFilter(t);
555
504
  const opts = await ctx.opts();
556
505
  const result = await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'filter-ws', { force: true, filter: 'left' }, opts);
557
506
  assert.strictEqual(result.success, true);
@@ -560,8 +509,8 @@ export function dataflowTests(getContext) {
560
509
  assert.strictEqual(executedTasks.length, 1, `Expected 1 executed task, got ${executedTasks.length}`);
561
510
  assert.strictEqual(executedTasks[0].name, 'left');
562
511
  });
563
- it('filter with non-existent task returns error', async () => {
564
- const ctx = getContext();
512
+ it('filter with non-existent task returns error', async (t) => {
513
+ const ctx = await withFilter(t);
565
514
  const opts = await ctx.opts();
566
515
  try {
567
516
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'filter-ws', { force: true, filter: 'no_such_task' }, opts);
@@ -573,18 +522,9 @@ export function dataflowTests(getContext) {
573
522
  }
574
523
  });
575
524
  });
576
- describe('dependency graph', () => {
577
- beforeEach(async () => {
578
- const ctx = getContext();
579
- const opts = await ctx.opts();
580
- const zipPath = await createDiamondPackageZip(ctx.tempDir, 'graph-pkg', '1.0.0');
581
- const packageZip = readFileSync(zipPath);
582
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
583
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'graph-ws', opts);
584
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'graph-ws', 'graph-pkg@1.0.0', opts);
585
- });
586
- it('dataflowGraph returns correct structure', async () => {
587
- const ctx = getContext();
525
+ describe('dependency graph', { concurrency: true }, () => {
526
+ it('dataflowGraph returns correct structure', async (t) => {
527
+ const ctx = await withGraph(t);
588
528
  const opts = await ctx.opts();
589
529
  const graph = await dataflowGraph(ctx.config.baseUrl, ctx.repoName, 'graph-ws', opts);
590
530
  // Should have 3 tasks: left, right, merge
@@ -604,18 +544,9 @@ export function dataflowTests(getContext) {
604
544
  assert.strictEqual(merge.dependsOn.length, 2);
605
545
  });
606
546
  });
607
- describe('log pagination', () => {
608
- beforeEach(async () => {
609
- const ctx = getContext();
610
- const opts = await ctx.opts();
611
- const zipPath = await createPackageZip(ctx.tempDir, 'logpag-pkg', '1.0.0');
612
- const packageZip = readFileSync(zipPath);
613
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
614
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'logpag-ws', opts);
615
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'logpag-ws', 'logpag-pkg@1.0.0', opts);
616
- });
617
- it('taskLogs supports offset and limit', async () => {
618
- const ctx = getContext();
547
+ describe('log pagination', { concurrency: true }, () => {
548
+ it('taskLogs supports offset and limit', async (t) => {
549
+ const ctx = await withLogPag(t);
619
550
  const opts = await ctx.opts();
620
551
  // Execute to generate logs
621
552
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'logpag-ws', { force: true }, opts);
@@ -635,18 +566,9 @@ export function dataflowTests(getContext) {
635
566
  assert.strictEqual(chunk2.data, full.data.slice(5));
636
567
  });
637
568
  });
638
- describe('event pagination', () => {
639
- beforeEach(async () => {
640
- const ctx = getContext();
641
- const opts = await ctx.opts();
642
- const zipPath = await createDiamondPackageZip(ctx.tempDir, 'evtpag-pkg', '1.0.0');
643
- const packageZip = readFileSync(zipPath);
644
- await packageImport(ctx.config.baseUrl, ctx.repoName, packageZip, opts);
645
- await workspaceCreate(ctx.config.baseUrl, ctx.repoName, 'evtpag-ws', opts);
646
- await workspaceDeploy(ctx.config.baseUrl, ctx.repoName, 'evtpag-ws', 'evtpag-pkg@1.0.0', opts);
647
- });
648
- it('dataflowExecution supports event offset and limit', async () => {
649
- const ctx = getContext();
569
+ describe('event pagination', { concurrency: true }, () => {
570
+ it('dataflowExecution supports event offset and limit', async (t) => {
571
+ const ctx = await withEvtPag(t);
650
572
  const opts = await ctx.opts();
651
573
  // Execute and wait for completion
652
574
  await dataflowExecute(ctx.config.baseUrl, ctx.repoName, 'evtpag-ws', { force: true }, opts);