@elaraai/e3-core 0.0.2-beta.9 → 1.0.0

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 (285) hide show
  1. package/LICENSE.md +4 -0
  2. package/README.md +74 -35
  3. package/dist/src/dataflow/api-compat.d.ts +90 -0
  4. package/dist/src/dataflow/api-compat.d.ts.map +1 -0
  5. package/dist/src/dataflow/api-compat.js +139 -0
  6. package/dist/src/dataflow/api-compat.js.map +1 -0
  7. package/dist/src/dataflow/api-compat.spec.d.ts +6 -0
  8. package/dist/src/dataflow/api-compat.spec.d.ts.map +1 -0
  9. package/dist/src/dataflow/api-compat.spec.js +182 -0
  10. package/dist/src/dataflow/api-compat.spec.js.map +1 -0
  11. package/dist/src/dataflow/index.d.ts +18 -0
  12. package/dist/src/dataflow/index.d.ts.map +1 -0
  13. package/dist/src/dataflow/index.js +23 -0
  14. package/dist/src/dataflow/index.js.map +1 -0
  15. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
  16. package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
  17. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +729 -0
  18. package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
  19. package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
  20. package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
  21. package/dist/src/dataflow/orchestrator/index.js +12 -0
  22. package/dist/src/dataflow/orchestrator/index.js.map +1 -0
  23. package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
  24. package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
  25. package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
  26. package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
  27. package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
  28. package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
  29. package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
  30. package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
  31. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
  32. package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
  33. package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
  34. package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
  35. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts +6 -0
  36. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts.map +1 -0
  37. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js +114 -0
  38. package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js.map +1 -0
  39. package/dist/src/dataflow/state-store/index.d.ts +13 -0
  40. package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
  41. package/dist/src/dataflow/state-store/index.js +13 -0
  42. package/dist/src/dataflow/state-store/index.js.map +1 -0
  43. package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
  44. package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
  45. package/dist/src/dataflow/state-store/interfaces.js +6 -0
  46. package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
  47. package/dist/src/dataflow/steps.d.ts +222 -0
  48. package/dist/src/dataflow/steps.d.ts.map +1 -0
  49. package/dist/src/dataflow/steps.js +707 -0
  50. package/dist/src/dataflow/steps.js.map +1 -0
  51. package/dist/src/dataflow/steps.spec.d.ts +6 -0
  52. package/dist/src/dataflow/steps.spec.d.ts.map +1 -0
  53. package/dist/src/dataflow/steps.spec.js +343 -0
  54. package/dist/src/dataflow/steps.spec.js.map +1 -0
  55. package/dist/src/dataflow/types.d.ts +127 -0
  56. package/dist/src/dataflow/types.d.ts.map +1 -0
  57. package/dist/src/dataflow/types.js +7 -0
  58. package/dist/src/dataflow/types.js.map +1 -0
  59. package/dist/src/dataflow-orchestration.spec.d.ts +6 -0
  60. package/dist/src/dataflow-orchestration.spec.d.ts.map +1 -0
  61. package/dist/src/dataflow-orchestration.spec.js +1025 -0
  62. package/dist/src/dataflow-orchestration.spec.js.map +1 -0
  63. package/dist/src/dataflow.d.ts +113 -38
  64. package/dist/src/dataflow.d.ts.map +1 -1
  65. package/dist/src/dataflow.js +269 -416
  66. package/dist/src/dataflow.js.map +1 -1
  67. package/dist/src/dataflow.spec.d.ts +6 -0
  68. package/dist/src/dataflow.spec.d.ts.map +1 -0
  69. package/dist/src/dataflow.spec.js +663 -0
  70. package/dist/src/dataflow.spec.js.map +1 -0
  71. package/dist/src/dataset-refs.d.ts +124 -0
  72. package/dist/src/dataset-refs.d.ts.map +1 -0
  73. package/dist/src/dataset-refs.js +319 -0
  74. package/dist/src/dataset-refs.js.map +1 -0
  75. package/dist/src/errors.d.ts +39 -9
  76. package/dist/src/errors.d.ts.map +1 -1
  77. package/dist/src/errors.js +51 -8
  78. package/dist/src/errors.js.map +1 -1
  79. package/dist/src/errors.spec.d.ts +6 -0
  80. package/dist/src/errors.spec.d.ts.map +1 -0
  81. package/dist/src/errors.spec.js +276 -0
  82. package/dist/src/errors.spec.js.map +1 -0
  83. package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
  84. package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
  85. package/dist/src/execution/LocalTaskRunner.js +399 -0
  86. package/dist/src/execution/LocalTaskRunner.js.map +1 -0
  87. package/dist/src/execution/MockTaskRunner.d.ts +49 -0
  88. package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
  89. package/dist/src/execution/MockTaskRunner.js +54 -0
  90. package/dist/src/execution/MockTaskRunner.js.map +1 -0
  91. package/dist/src/execution/index.d.ts +16 -0
  92. package/dist/src/execution/index.d.ts.map +1 -0
  93. package/dist/src/execution/index.js +8 -0
  94. package/dist/src/execution/index.js.map +1 -0
  95. package/dist/src/execution/interfaces.d.ts +246 -0
  96. package/dist/src/execution/interfaces.d.ts.map +1 -0
  97. package/dist/src/execution/interfaces.js +6 -0
  98. package/dist/src/execution/interfaces.js.map +1 -0
  99. package/dist/src/execution/processHelpers.d.ts +20 -0
  100. package/dist/src/execution/processHelpers.d.ts.map +1 -0
  101. package/dist/src/execution/processHelpers.js +62 -0
  102. package/dist/src/execution/processHelpers.js.map +1 -0
  103. package/dist/src/executions.d.ts +71 -104
  104. package/dist/src/executions.d.ts.map +1 -1
  105. package/dist/src/executions.js +113 -481
  106. package/dist/src/executions.js.map +1 -1
  107. package/dist/src/executions.spec.d.ts +6 -0
  108. package/dist/src/executions.spec.d.ts.map +1 -0
  109. package/dist/src/executions.spec.js +387 -0
  110. package/dist/src/executions.spec.js.map +1 -0
  111. package/dist/src/formats.d.ts +18 -2
  112. package/dist/src/formats.d.ts.map +1 -1
  113. package/dist/src/formats.js +34 -2
  114. package/dist/src/formats.js.map +1 -1
  115. package/dist/src/gc.spec.d.ts +6 -0
  116. package/dist/src/gc.spec.d.ts.map +1 -0
  117. package/dist/src/gc.spec.js +512 -0
  118. package/dist/src/gc.spec.js.map +1 -0
  119. package/dist/src/index.d.ts +20 -10
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +48 -18
  122. package/dist/src/index.js.map +1 -1
  123. package/dist/src/objects.d.ts +7 -53
  124. package/dist/src/objects.d.ts.map +1 -1
  125. package/dist/src/objects.js +13 -232
  126. package/dist/src/objects.js.map +1 -1
  127. package/dist/src/objects.spec.d.ts +6 -0
  128. package/dist/src/objects.spec.d.ts.map +1 -0
  129. package/dist/src/objects.spec.js +247 -0
  130. package/dist/src/objects.spec.js.map +1 -0
  131. package/dist/src/packages.d.ts +41 -14
  132. package/dist/src/packages.d.ts.map +1 -1
  133. package/dist/src/packages.js +151 -89
  134. package/dist/src/packages.js.map +1 -1
  135. package/dist/src/packages.spec.d.ts +6 -0
  136. package/dist/src/packages.spec.d.ts.map +1 -0
  137. package/dist/src/packages.spec.js +324 -0
  138. package/dist/src/packages.spec.js.map +1 -0
  139. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
  140. package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
  141. package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
  142. package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
  143. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts +6 -0
  144. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts.map +1 -0
  145. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js +187 -0
  146. package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js.map +1 -0
  147. package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
  148. package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
  149. package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
  150. package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
  151. package/dist/src/storage/in-memory/index.d.ts +12 -0
  152. package/dist/src/storage/in-memory/index.d.ts.map +1 -0
  153. package/dist/src/storage/in-memory/index.js +12 -0
  154. package/dist/src/storage/in-memory/index.js.map +1 -0
  155. package/dist/src/storage/index.d.ts +18 -0
  156. package/dist/src/storage/index.d.ts.map +1 -0
  157. package/dist/src/storage/index.js +10 -0
  158. package/dist/src/storage/index.js.map +1 -0
  159. package/dist/src/storage/interfaces.d.ts +581 -0
  160. package/dist/src/storage/interfaces.d.ts.map +1 -0
  161. package/dist/src/storage/interfaces.js +6 -0
  162. package/dist/src/storage/interfaces.js.map +1 -0
  163. package/dist/src/storage/local/LocalBackend.d.ts +56 -0
  164. package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
  165. package/dist/src/storage/local/LocalBackend.js +145 -0
  166. package/dist/src/storage/local/LocalBackend.js.map +1 -0
  167. package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
  168. package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
  169. package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
  170. package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
  171. package/dist/src/storage/local/LocalLockService.d.ts +111 -0
  172. package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
  173. package/dist/src/storage/local/LocalLockService.js +364 -0
  174. package/dist/src/storage/local/LocalLockService.js.map +1 -0
  175. package/dist/src/storage/local/LocalLockService.spec.d.ts +6 -0
  176. package/dist/src/storage/local/LocalLockService.spec.d.ts.map +1 -0
  177. package/dist/src/storage/local/LocalLockService.spec.js +148 -0
  178. package/dist/src/storage/local/LocalLockService.spec.js.map +1 -0
  179. package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
  180. package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
  181. package/dist/src/storage/local/LocalLogStore.js +66 -0
  182. package/dist/src/storage/local/LocalLogStore.js.map +1 -0
  183. package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
  184. package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
  185. package/dist/src/storage/local/LocalObjectStore.js +300 -0
  186. package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
  187. package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
  188. package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
  189. package/dist/src/storage/local/LocalRefStore.js +337 -0
  190. package/dist/src/storage/local/LocalRefStore.js.map +1 -0
  191. package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
  192. package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
  193. package/dist/src/storage/local/LocalRepoStore.js +365 -0
  194. package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
  195. package/dist/src/storage/local/LocalRepoStore.spec.d.ts +6 -0
  196. package/dist/src/storage/local/LocalRepoStore.spec.d.ts.map +1 -0
  197. package/dist/src/storage/local/LocalRepoStore.spec.js +255 -0
  198. package/dist/src/storage/local/LocalRepoStore.spec.js.map +1 -0
  199. package/dist/src/storage/local/gc.d.ts +92 -0
  200. package/dist/src/storage/local/gc.d.ts.map +1 -0
  201. package/dist/src/storage/local/gc.js +377 -0
  202. package/dist/src/storage/local/gc.js.map +1 -0
  203. package/dist/src/storage/local/index.d.ts +18 -0
  204. package/dist/src/storage/local/index.d.ts.map +1 -0
  205. package/dist/src/storage/local/index.js +18 -0
  206. package/dist/src/storage/local/index.js.map +1 -0
  207. package/dist/src/storage/local/localHelpers.d.ts +25 -0
  208. package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
  209. package/dist/src/storage/local/localHelpers.js +69 -0
  210. package/dist/src/storage/local/localHelpers.js.map +1 -0
  211. package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
  212. package/dist/src/storage/local/repository.d.ts.map +1 -0
  213. package/dist/src/{repository.js → storage/local/repository.js} +31 -29
  214. package/dist/src/storage/local/repository.js.map +1 -0
  215. package/dist/src/storage/local/repository.spec.d.ts +6 -0
  216. package/dist/src/storage/local/repository.spec.d.ts.map +1 -0
  217. package/dist/src/storage/local/repository.spec.js +186 -0
  218. package/dist/src/storage/local/repository.spec.js.map +1 -0
  219. package/dist/src/tasks.d.ts +16 -10
  220. package/dist/src/tasks.d.ts.map +1 -1
  221. package/dist/src/tasks.js +35 -41
  222. package/dist/src/tasks.js.map +1 -1
  223. package/dist/src/tasks.spec.d.ts +6 -0
  224. package/dist/src/tasks.spec.d.ts.map +1 -0
  225. package/dist/src/tasks.spec.js +105 -0
  226. package/dist/src/tasks.spec.js.map +1 -0
  227. package/dist/src/test-helpers.d.ts +5 -4
  228. package/dist/src/test-helpers.d.ts.map +1 -1
  229. package/dist/src/test-helpers.js +9 -21
  230. package/dist/src/test-helpers.js.map +1 -1
  231. package/dist/src/transfer/InMemoryTransferBackend.d.ts +75 -0
  232. package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
  233. package/dist/src/transfer/InMemoryTransferBackend.js +211 -0
  234. package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
  235. package/dist/src/transfer/index.d.ts +9 -0
  236. package/dist/src/transfer/index.d.ts.map +1 -0
  237. package/dist/src/transfer/index.js +11 -0
  238. package/dist/src/transfer/index.js.map +1 -0
  239. package/dist/src/transfer/interfaces.d.ts +103 -0
  240. package/dist/src/transfer/interfaces.d.ts.map +1 -0
  241. package/dist/src/transfer/interfaces.js +6 -0
  242. package/dist/src/transfer/interfaces.js.map +1 -0
  243. package/dist/src/transfer/process.d.ts +55 -0
  244. package/dist/src/transfer/process.d.ts.map +1 -0
  245. package/dist/src/transfer/process.js +144 -0
  246. package/dist/src/transfer/process.js.map +1 -0
  247. package/dist/src/transfer/types.d.ts +106 -0
  248. package/dist/src/transfer/types.d.ts.map +1 -0
  249. package/dist/src/transfer/types.js +61 -0
  250. package/dist/src/transfer/types.js.map +1 -0
  251. package/dist/src/trees.d.ts +102 -63
  252. package/dist/src/trees.d.ts.map +1 -1
  253. package/dist/src/trees.js +319 -479
  254. package/dist/src/trees.js.map +1 -1
  255. package/dist/src/trees.spec.d.ts +6 -0
  256. package/dist/src/trees.spec.d.ts.map +1 -0
  257. package/dist/src/trees.spec.js +635 -0
  258. package/dist/src/trees.spec.js.map +1 -0
  259. package/dist/src/uuid.d.ts +26 -0
  260. package/dist/src/uuid.d.ts.map +1 -0
  261. package/dist/src/uuid.js +80 -0
  262. package/dist/src/uuid.js.map +1 -0
  263. package/dist/src/workspaceStatus.d.ts +6 -4
  264. package/dist/src/workspaceStatus.d.ts.map +1 -1
  265. package/dist/src/workspaceStatus.js +46 -60
  266. package/dist/src/workspaceStatus.js.map +1 -1
  267. package/dist/src/workspaces.d.ts +46 -47
  268. package/dist/src/workspaces.d.ts.map +1 -1
  269. package/dist/src/workspaces.js +281 -221
  270. package/dist/src/workspaces.js.map +1 -1
  271. package/dist/src/workspaces.spec.d.ts +6 -0
  272. package/dist/src/workspaces.spec.d.ts.map +1 -0
  273. package/dist/src/workspaces.spec.js +273 -0
  274. package/dist/src/workspaces.spec.js.map +1 -0
  275. package/package.json +15 -15
  276. package/dist/src/gc.d.ts +0 -54
  277. package/dist/src/gc.d.ts.map +0 -1
  278. package/dist/src/gc.js +0 -233
  279. package/dist/src/gc.js.map +0 -1
  280. package/dist/src/repository.d.ts.map +0 -1
  281. package/dist/src/repository.js.map +0 -1
  282. package/dist/src/workspaceLock.d.ts +0 -67
  283. package/dist/src/workspaceLock.d.ts.map +0 -1
  284. package/dist/src/workspaceLock.js +0 -217
  285. package/dist/src/workspaceLock.js.map +0 -1
@@ -0,0 +1,663 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Licensed under BSL 1.1. See LICENSE for details.
4
+ */
5
+ /**
6
+ * Tests for dataflow.ts - DAG execution
7
+ */
8
+ import { describe, it, beforeEach, afterEach } from 'node:test';
9
+ import assert from 'node:assert';
10
+ import { join } from 'node:path';
11
+ import { mkdirSync, writeFileSync } from 'node:fs';
12
+ import { variant, StringType, ArrayType, encodeBeast2For, East, IRType } from '@elaraai/east';
13
+ import { TaskObjectType, PackageObjectType, } from '@elaraai/e3-types';
14
+ import { dataflowExecute, dataflowGetGraph, dataflowGetReadyTasks, dataflowGetDependentsToSkip, } from './dataflow.js';
15
+ import { objectWrite } from './storage/local/LocalObjectStore.js';
16
+ import { workspaceDeploy } from './workspaces.js';
17
+ import { workspaceGetDataset, workspaceSetDataset } from './trees.js';
18
+ import { WorkspaceLockError, DataflowAbortedError } from './errors.js';
19
+ import { createTestRepo, removeTestRepo } from './test-helpers.js';
20
+ import { LocalStorage } from './storage/local/index.js';
21
+ describe('dataflow', () => {
22
+ let testRepo;
23
+ let storage;
24
+ beforeEach(() => {
25
+ testRepo = createTestRepo();
26
+ storage = new LocalStorage();
27
+ });
28
+ afterEach(() => {
29
+ removeTestRepo(testRepo);
30
+ });
31
+ /**
32
+ * Helper to create a command IR object.
33
+ *
34
+ * Creates an East FunctionIR: (inputs: Array<String>, output: String) -> Array<String>
35
+ * that returns the provided command parts as a literal array.
36
+ */
37
+ async function createCommandIr(repoPath, parts) {
38
+ // Build an East function that returns the command array
39
+ // The function signature is: (inputs: Array<String>, output: String) -> Array<String>
40
+ const commandFn = East.function([ArrayType(StringType), StringType], ArrayType(StringType), ($, inputs, output) => {
41
+ // Build the result array, substituting inputs[i] and output as needed
42
+ const result = [];
43
+ for (const part of parts) {
44
+ if (part === '{input}' || part === '{input0}') {
45
+ result.push(inputs.get(0n));
46
+ }
47
+ else if (part.match(/^\{input(\d+)\}$/)) {
48
+ const idx = BigInt(part.match(/^\{input(\d+)\}$/)[1]);
49
+ result.push(inputs.get(idx));
50
+ }
51
+ else if (part === '{output}') {
52
+ result.push(output);
53
+ }
54
+ else {
55
+ result.push(part);
56
+ }
57
+ }
58
+ return result;
59
+ });
60
+ const ir = commandFn.toIR().ir;
61
+ const encoder = encodeBeast2For(IRType);
62
+ return objectWrite(repoPath, encoder(ir));
63
+ }
64
+ // Helper to create a package with tasks
65
+ async function createPackageWithTasks(repoPath, tasks, structure, _initialData) {
66
+ const taskEncoder = encodeBeast2For(TaskObjectType);
67
+ const tasksMap = new Map();
68
+ for (const t of tasks) {
69
+ // Create command IR for this task
70
+ const commandIrHash = await createCommandIr(repoPath, t.command);
71
+ const taskObj = {
72
+ commandIr: commandIrHash,
73
+ inputs: t.inputs,
74
+ output: t.output,
75
+ kind: variant('none', null), metadata: variant('none', null),
76
+ };
77
+ const taskHash = await objectWrite(repoPath, taskEncoder(taskObj));
78
+ tasksMap.set(t.name, taskHash);
79
+ }
80
+ // Create package object (no root tree — per-dataset refs are used instead)
81
+ const pkgEncoder = encodeBeast2For(PackageObjectType);
82
+ const pkgObj = {
83
+ data: {
84
+ structure,
85
+ refs: new Map(),
86
+ },
87
+ tasks: tasksMap,
88
+ };
89
+ const pkgHash = await objectWrite(repoPath, pkgEncoder(pkgObj));
90
+ // Write package ref - the ref file is at packages/<name>/<version> (version is the file, not a directory)
91
+ const pkgDir = join(repoPath, 'packages', 'test');
92
+ mkdirSync(pkgDir, { recursive: true });
93
+ writeFileSync(join(pkgDir, '1.0.0'), pkgHash + '\n');
94
+ return pkgHash;
95
+ }
96
+ describe('dataflowGetGraph', () => {
97
+ it('returns empty graph for package with no tasks', async () => {
98
+ // Create a minimal package with no tasks
99
+ const structure = {
100
+ type: 'struct',
101
+ value: new Map([['input', { type: 'value', value: { type: StringType, writable: true } }]]),
102
+ };
103
+ await createPackageWithTasks(testRepo, [], structure);
104
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
105
+ const graph = await dataflowGetGraph(storage, testRepo, 'test-ws');
106
+ assert.strictEqual(graph.tasks.length, 0);
107
+ });
108
+ it('returns task dependencies', async () => {
109
+ // Create a package with two tasks: A -> B
110
+ const structure = {
111
+ type: 'struct',
112
+ value: new Map([
113
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
114
+ ['middle', { type: 'value', value: { type: StringType, writable: true } }],
115
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
116
+ ]),
117
+ };
118
+ const inputPath = [variant('field', 'input')];
119
+ const middlePath = [variant('field', 'middle')];
120
+ const outputPath = [variant('field', 'output')];
121
+ await createPackageWithTasks(testRepo, [
122
+ { name: 'task-a', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: middlePath },
123
+ { name: 'task-b', command: ['cp', '{input}', '{output}'], inputs: [middlePath], output: outputPath },
124
+ ], structure);
125
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
126
+ const graph = await dataflowGetGraph(storage, testRepo, 'test-ws');
127
+ assert.strictEqual(graph.tasks.length, 2);
128
+ const taskA = graph.tasks.find((t) => t.name === 'task-a');
129
+ const taskB = graph.tasks.find((t) => t.name === 'task-b');
130
+ assert.ok(taskA);
131
+ assert.ok(taskB);
132
+ assert.deepStrictEqual(taskA.dependsOn, []); // A depends on external input
133
+ assert.deepStrictEqual(taskB.dependsOn, ['task-a']); // B depends on A
134
+ });
135
+ });
136
+ describe('dataflowGetReadyTasks', () => {
137
+ it('returns all tasks when none have dependencies', () => {
138
+ const graph = {
139
+ tasks: [
140
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
141
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: [] },
142
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: [] },
143
+ ],
144
+ };
145
+ const ready = dataflowGetReadyTasks(graph, new Set());
146
+ assert.deepStrictEqual(ready.sort(), ['a', 'b', 'c']);
147
+ });
148
+ it('returns only tasks with satisfied dependencies', () => {
149
+ // Diamond: A -> B, A -> C, B -> D, C -> D
150
+ const graph = {
151
+ tasks: [
152
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
153
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
154
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: ['a'] },
155
+ { name: 'd', hash: 'h4', inputs: [], output: 'out-d', dependsOn: ['b', 'c'] },
156
+ ],
157
+ };
158
+ // Initially only A is ready
159
+ let ready = dataflowGetReadyTasks(graph, new Set());
160
+ assert.deepStrictEqual(ready, ['a']);
161
+ // After A completes, B and C are ready
162
+ ready = dataflowGetReadyTasks(graph, new Set(['a']));
163
+ assert.deepStrictEqual(ready.sort(), ['b', 'c']);
164
+ // After A and B complete, C is ready (D still waiting for C)
165
+ ready = dataflowGetReadyTasks(graph, new Set(['a', 'b']));
166
+ assert.deepStrictEqual(ready, ['c']);
167
+ // After A, B, C complete, D is ready
168
+ ready = dataflowGetReadyTasks(graph, new Set(['a', 'b', 'c']));
169
+ assert.deepStrictEqual(ready, ['d']);
170
+ // After all complete, nothing is ready
171
+ ready = dataflowGetReadyTasks(graph, new Set(['a', 'b', 'c', 'd']));
172
+ assert.deepStrictEqual(ready, []);
173
+ });
174
+ it('excludes already completed tasks', () => {
175
+ const graph = {
176
+ tasks: [
177
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
178
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: [] },
179
+ ],
180
+ };
181
+ const ready = dataflowGetReadyTasks(graph, new Set(['a']));
182
+ assert.deepStrictEqual(ready, ['b']);
183
+ });
184
+ });
185
+ describe('dataflowGetDependentsToSkip', () => {
186
+ it('returns empty array when no tasks depend on failed task', () => {
187
+ const graph = {
188
+ tasks: [
189
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
190
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: [] },
191
+ ],
192
+ };
193
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(), new Set());
194
+ assert.deepStrictEqual(toSkip, []);
195
+ });
196
+ it('returns direct dependents', () => {
197
+ const graph = {
198
+ tasks: [
199
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
200
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
201
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: ['a'] },
202
+ ],
203
+ };
204
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(), new Set());
205
+ assert.deepStrictEqual(toSkip.sort(), ['b', 'c']);
206
+ });
207
+ it('returns transitive dependents', () => {
208
+ // a -> b -> c -> d
209
+ const graph = {
210
+ tasks: [
211
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
212
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
213
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: ['b'] },
214
+ { name: 'd', hash: 'h4', inputs: [], output: 'out-d', dependsOn: ['c'] },
215
+ ],
216
+ };
217
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(), new Set());
218
+ assert.deepStrictEqual(toSkip.sort(), ['b', 'c', 'd']);
219
+ });
220
+ it('handles diamond dependencies', () => {
221
+ // a -> b -> d
222
+ // a -> c -> d
223
+ const graph = {
224
+ tasks: [
225
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
226
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
227
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: ['a'] },
228
+ { name: 'd', hash: 'h4', inputs: [], output: 'out-d', dependsOn: ['b', 'c'] },
229
+ ],
230
+ };
231
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(), new Set());
232
+ assert.deepStrictEqual(toSkip.sort(), ['b', 'c', 'd']);
233
+ });
234
+ it('excludes already completed tasks', () => {
235
+ const graph = {
236
+ tasks: [
237
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
238
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
239
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: ['a'] },
240
+ ],
241
+ };
242
+ // b is already completed
243
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(['b']), new Set());
244
+ assert.deepStrictEqual(toSkip, ['c']);
245
+ });
246
+ it('excludes already skipped tasks', () => {
247
+ const graph = {
248
+ tasks: [
249
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
250
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
251
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: ['b'] },
252
+ ],
253
+ };
254
+ // b is already skipped
255
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(), new Set(['b']));
256
+ // c depends on b which is already skipped, so only c should be returned (not b again)
257
+ // But since b is skipped, we skip it, and c is a transitive dependent through b
258
+ assert.deepStrictEqual(toSkip, ['c']);
259
+ });
260
+ it('does not skip tasks that have alternative paths', () => {
261
+ // a (fails) -> b -> d
262
+ // c (success) -> d
263
+ // d depends on both b and c. If a fails, b is skipped, but d might still be reachable via c
264
+ // However, our function finds ALL transitive dependents - the caller decides what to do
265
+ const graph = {
266
+ tasks: [
267
+ { name: 'a', hash: 'h1', inputs: [], output: 'out-a', dependsOn: [] },
268
+ { name: 'b', hash: 'h2', inputs: [], output: 'out-b', dependsOn: ['a'] },
269
+ { name: 'c', hash: 'h3', inputs: [], output: 'out-c', dependsOn: [] },
270
+ { name: 'd', hash: 'h4', inputs: [], output: 'out-d', dependsOn: ['b', 'c'] },
271
+ ],
272
+ };
273
+ // d is a transitive dependent of a through b
274
+ const toSkip = dataflowGetDependentsToSkip(graph, 'a', new Set(), new Set());
275
+ assert.deepStrictEqual(toSkip.sort(), ['b', 'd']);
276
+ });
277
+ });
278
+ describe('dataflowExecute', () => {
279
+ it('executes single task', async () => {
280
+ // Create package with one task
281
+ const structure = {
282
+ type: 'struct',
283
+ value: new Map([
284
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
285
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
286
+ ]),
287
+ };
288
+ const inputPath = [variant('field', 'input')];
289
+ const outputPath = [variant('field', 'output')];
290
+ // Create input value
291
+ const inputEncoder = encodeBeast2For(StringType);
292
+ const inputHash = await objectWrite(testRepo, inputEncoder('hello world'));
293
+ await createPackageWithTasks(testRepo, [{ name: 'copy-task', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: outputPath }], structure, {
294
+ input: {
295
+ value: 'hello world',
296
+ ref: { type: 'value', value: inputHash },
297
+ },
298
+ });
299
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
300
+ // Set the input value in workspace
301
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'hello world', StringType);
302
+ const result = await dataflowExecute(storage, testRepo, 'test-ws');
303
+ assert.strictEqual(result.success, true);
304
+ assert.strictEqual(result.executed, 1);
305
+ assert.strictEqual(result.failed, 0);
306
+ assert.strictEqual(result.tasks.length, 1);
307
+ assert.strictEqual(result.tasks[0].state, 'success');
308
+ // Verify output was written
309
+ const outputValue = await workspaceGetDataset(storage, testRepo, 'test-ws', outputPath);
310
+ assert.strictEqual(outputValue, 'hello world');
311
+ });
312
+ it('executes task chain in order', async () => {
313
+ // Create package with A -> B chain
314
+ const structure = {
315
+ type: 'struct',
316
+ value: new Map([
317
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
318
+ ['middle', { type: 'value', value: { type: StringType, writable: true } }],
319
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
320
+ ]),
321
+ };
322
+ const inputPath = [variant('field', 'input')];
323
+ const middlePath = [variant('field', 'middle')];
324
+ const outputPath = [variant('field', 'output')];
325
+ // Create input value
326
+ const inputEncoder = encodeBeast2For(StringType);
327
+ const inputHash = await objectWrite(testRepo, inputEncoder('chain test'));
328
+ await createPackageWithTasks(testRepo, [
329
+ { name: 'task-a', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: middlePath },
330
+ { name: 'task-b', command: ['cp', '{input}', '{output}'], inputs: [middlePath], output: outputPath },
331
+ ], structure, {
332
+ input: {
333
+ value: 'chain test',
334
+ ref: { type: 'value', value: inputHash },
335
+ },
336
+ });
337
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
338
+ // Set the input value in workspace
339
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'chain test', StringType);
340
+ const completedOrder = [];
341
+ const result = await dataflowExecute(storage, testRepo, 'test-ws', {
342
+ onTaskComplete: (r) => completedOrder.push(r.name),
343
+ });
344
+ assert.strictEqual(result.success, true);
345
+ assert.strictEqual(result.executed, 2);
346
+ assert.strictEqual(completedOrder[0], 'task-a'); // A must complete before B
347
+ assert.strictEqual(completedOrder[1], 'task-b');
348
+ // Verify final output
349
+ const outputValue = await workspaceGetDataset(storage, testRepo, 'test-ws', outputPath);
350
+ assert.strictEqual(outputValue, 'chain test');
351
+ });
352
+ it('handles task failure with fail-fast', async () => {
353
+ // Create package with A (fails) -> B (should be skipped)
354
+ const structure = {
355
+ type: 'struct',
356
+ value: new Map([
357
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
358
+ ['middle', { type: 'value', value: { type: StringType, writable: true } }],
359
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
360
+ ]),
361
+ };
362
+ const inputPath = [variant('field', 'input')];
363
+ const middlePath = [variant('field', 'middle')];
364
+ const outputPath = [variant('field', 'output')];
365
+ // Create input value
366
+ const inputEncoder = encodeBeast2For(StringType);
367
+ const inputHash = await objectWrite(testRepo, inputEncoder('fail test'));
368
+ await createPackageWithTasks(testRepo, [
369
+ { name: 'task-a', command: ['bash', '-c', 'exit 1'], inputs: [inputPath], output: middlePath },
370
+ { name: 'task-b', command: ['cp', '{input}', '{output}'], inputs: [middlePath], output: outputPath },
371
+ ], structure, {
372
+ input: {
373
+ value: 'fail test',
374
+ ref: { type: 'value', value: inputHash },
375
+ },
376
+ });
377
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
378
+ // Set the input value
379
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'fail test', StringType);
380
+ const result = await dataflowExecute(storage, testRepo, 'test-ws');
381
+ assert.strictEqual(result.success, false);
382
+ assert.strictEqual(result.failed, 1);
383
+ assert.strictEqual(result.skipped, 1);
384
+ const taskA = result.tasks.find((t) => t.name === 'task-a');
385
+ const taskB = result.tasks.find((t) => t.name === 'task-b');
386
+ assert.ok(taskA);
387
+ assert.ok(taskB);
388
+ assert.strictEqual(taskA.state, 'failed');
389
+ assert.strictEqual(taskB.state, 'skipped');
390
+ });
391
+ it('caches successful task results', async () => {
392
+ // Create package
393
+ const structure = {
394
+ type: 'struct',
395
+ value: new Map([
396
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
397
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
398
+ ]),
399
+ };
400
+ const inputPath = [variant('field', 'input')];
401
+ const outputPath = [variant('field', 'output')];
402
+ const inputEncoder = encodeBeast2For(StringType);
403
+ const inputHash = await objectWrite(testRepo, inputEncoder('cache test'));
404
+ await createPackageWithTasks(testRepo, [{ name: 'copy-task', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: outputPath }], structure, {
405
+ input: {
406
+ value: 'cache test',
407
+ ref: { type: 'value', value: inputHash },
408
+ },
409
+ });
410
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
411
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'cache test', StringType);
412
+ // First execution
413
+ const result1 = await dataflowExecute(storage, testRepo, 'test-ws');
414
+ assert.strictEqual(result1.executed, 1);
415
+ assert.strictEqual(result1.cached, 0);
416
+ // Second execution should be cached (output already assigned)
417
+ const result2 = await dataflowExecute(storage, testRepo, 'test-ws');
418
+ assert.strictEqual(result2.executed, 0);
419
+ assert.strictEqual(result2.cached, 1);
420
+ });
421
+ it('respects concurrency limit', async () => {
422
+ // Create package with 4 parallel tasks
423
+ const structure = {
424
+ type: 'struct',
425
+ value: new Map([
426
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
427
+ ['out1', { type: 'value', value: { type: StringType, writable: true } }],
428
+ ['out2', { type: 'value', value: { type: StringType, writable: true } }],
429
+ ['out3', { type: 'value', value: { type: StringType, writable: true } }],
430
+ ['out4', { type: 'value', value: { type: StringType, writable: true } }],
431
+ ]),
432
+ };
433
+ const inputPath = [variant('field', 'input')];
434
+ const inputEncoder = encodeBeast2For(StringType);
435
+ const inputHash = await objectWrite(testRepo, inputEncoder('parallel test'));
436
+ // Use a slow command
437
+ const slowCopyCmd = ['bash', '-c', 'sleep 0.1; cp "$1" "$2"', '--', '{input}', '{output}'];
438
+ await createPackageWithTasks(testRepo, [
439
+ { name: 'task-1', command: slowCopyCmd, inputs: [inputPath], output: [variant('field', 'out1')] },
440
+ { name: 'task-2', command: slowCopyCmd, inputs: [inputPath], output: [variant('field', 'out2')] },
441
+ { name: 'task-3', command: slowCopyCmd, inputs: [inputPath], output: [variant('field', 'out3')] },
442
+ { name: 'task-4', command: slowCopyCmd, inputs: [inputPath], output: [variant('field', 'out4')] },
443
+ ], structure, {
444
+ input: {
445
+ value: 'parallel test',
446
+ ref: { type: 'value', value: inputHash },
447
+ },
448
+ });
449
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
450
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'parallel test', StringType);
451
+ // Track concurrent execution count
452
+ let currentConcurrent = 0;
453
+ let maxConcurrent = 0;
454
+ const result = await dataflowExecute(storage, testRepo, 'test-ws', {
455
+ concurrency: 2,
456
+ onTaskStart: () => {
457
+ currentConcurrent++;
458
+ maxConcurrent = Math.max(maxConcurrent, currentConcurrent);
459
+ },
460
+ onTaskComplete: () => {
461
+ currentConcurrent--;
462
+ },
463
+ });
464
+ assert.strictEqual(result.success, true);
465
+ assert.strictEqual(result.executed, 4);
466
+ assert.ok(maxConcurrent <= 2, `Max concurrent was ${maxConcurrent}, expected <= 2`);
467
+ });
468
+ it('rejects concurrent dataflow execution on same workspace', async () => {
469
+ // Create a simple package with a slow task
470
+ const structure = {
471
+ type: 'struct',
472
+ value: new Map([
473
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
474
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
475
+ ]),
476
+ };
477
+ const inputPath = [variant('field', 'input')];
478
+ const outputPath = [variant('field', 'output')];
479
+ const inputEncoder = encodeBeast2For(StringType);
480
+ const inputHash = await objectWrite(testRepo, inputEncoder('test'));
481
+ // Use sleep to make the task take some time
482
+ const slowCopyCmd = ['bash', '-c', 'sleep 0.3; cp "$1" "$2"', '--', '{input}', '{output}'];
483
+ await createPackageWithTasks(testRepo, [{ name: 'slow-task', command: slowCopyCmd, inputs: [inputPath], output: outputPath }], structure, {
484
+ input: {
485
+ value: 'test',
486
+ ref: { type: 'value', value: inputHash },
487
+ },
488
+ });
489
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
490
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'test', StringType);
491
+ // Start first execution (don't await)
492
+ const firstExecution = dataflowExecute(storage, testRepo, 'test-ws');
493
+ // Give it a moment to acquire the lock
494
+ await new Promise(resolve => setTimeout(resolve, 150));
495
+ // Try to start second execution - should fail with WorkspaceLockError
496
+ await assert.rejects(dataflowExecute(storage, testRepo, 'test-ws'), (err) => {
497
+ assert.ok(err instanceof WorkspaceLockError, `Expected WorkspaceLockError, got ${err.constructor.name}`);
498
+ assert.strictEqual(err.workspace, 'test-ws');
499
+ return true;
500
+ });
501
+ // Wait for first execution to complete
502
+ const result = await firstExecution;
503
+ assert.strictEqual(result.success, true);
504
+ });
505
+ it('allows sequential dataflow executions', async () => {
506
+ // Create a simple package
507
+ const structure = {
508
+ type: 'struct',
509
+ value: new Map([
510
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
511
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
512
+ ]),
513
+ };
514
+ const inputPath = [variant('field', 'input')];
515
+ const outputPath = [variant('field', 'output')];
516
+ const inputEncoder = encodeBeast2For(StringType);
517
+ const inputHash = await objectWrite(testRepo, inputEncoder('test'));
518
+ await createPackageWithTasks(testRepo, [{ name: 'task', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: outputPath }], structure, {
519
+ input: {
520
+ value: 'test',
521
+ ref: { type: 'value', value: inputHash },
522
+ },
523
+ });
524
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
525
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'test', StringType);
526
+ // First execution
527
+ const result1 = await dataflowExecute(storage, testRepo, 'test-ws');
528
+ assert.strictEqual(result1.success, true);
529
+ // Second execution (should succeed because first released the lock)
530
+ const result2 = await dataflowExecute(storage, testRepo, 'test-ws');
531
+ assert.strictEqual(result2.success, true);
532
+ });
533
+ it('allows external lock management', async () => {
534
+ // Create a simple package
535
+ const structure = {
536
+ type: 'struct',
537
+ value: new Map([
538
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
539
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
540
+ ]),
541
+ };
542
+ const inputPath = [variant('field', 'input')];
543
+ const outputPath = [variant('field', 'output')];
544
+ const inputEncoder = encodeBeast2For(StringType);
545
+ const inputHash = await objectWrite(testRepo, inputEncoder('test'));
546
+ await createPackageWithTasks(testRepo, [{ name: 'task', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: outputPath }], structure, {
547
+ input: {
548
+ value: 'test',
549
+ ref: { type: 'value', value: inputHash },
550
+ },
551
+ });
552
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
553
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'test', StringType);
554
+ // Acquire lock externally
555
+ const lock = await storage.locks.acquire(testRepo, 'test-ws', variant('dataflow', null));
556
+ assert.ok(lock);
557
+ try {
558
+ // Execute with external lock
559
+ const result = await dataflowExecute(storage, testRepo, 'test-ws', { lock });
560
+ assert.strictEqual(result.success, true);
561
+ // Lock should still be held (we can't acquire another)
562
+ const attemptedLock = await storage.locks.acquire(testRepo, 'test-ws', variant('dataflow', null));
563
+ assert.strictEqual(attemptedLock, null);
564
+ }
565
+ finally {
566
+ await lock.release();
567
+ }
568
+ // Now lock should be released - can acquire again
569
+ const lock2 = await storage.locks.acquire(testRepo, 'test-ws', variant('dataflow', null));
570
+ assert.ok(lock2);
571
+ await lock2.release();
572
+ });
573
+ it('aborts execution when signal is triggered', async () => {
574
+ // Create a package with a slow task
575
+ const structure = {
576
+ type: 'struct',
577
+ value: new Map([
578
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
579
+ ['output', { type: 'value', value: { type: StringType, writable: true } }],
580
+ ]),
581
+ };
582
+ const inputPath = [variant('field', 'input')];
583
+ const outputPath = [variant('field', 'output')];
584
+ const inputEncoder = encodeBeast2For(StringType);
585
+ const inputHash = await objectWrite(testRepo, inputEncoder('test'));
586
+ // Task that sleeps for 2 seconds
587
+ const slowCmd = ['bash', '-c', 'sleep 2; cp "$1" "$2"', '--', '{input}', '{output}'];
588
+ await createPackageWithTasks(testRepo, [{ name: 'slow-task', command: slowCmd, inputs: [inputPath], output: outputPath }], structure, {
589
+ input: {
590
+ value: 'test',
591
+ ref: { type: 'value', value: inputHash },
592
+ },
593
+ });
594
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
595
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'test', StringType);
596
+ const controller = new AbortController();
597
+ // Start execution
598
+ const executionPromise = dataflowExecute(storage, testRepo, 'test-ws', {
599
+ signal: controller.signal,
600
+ });
601
+ // Abort after a short delay
602
+ await new Promise(resolve => setTimeout(resolve, 200));
603
+ controller.abort();
604
+ // Should throw DataflowAbortedError
605
+ await assert.rejects(executionPromise, (err) => {
606
+ assert.ok(err instanceof DataflowAbortedError, `Expected DataflowAbortedError, got ${err.constructor.name}`);
607
+ return true;
608
+ });
609
+ });
610
+ it('includes partial results in DataflowAbortedError', async () => {
611
+ // Create a package with two tasks: one fast, one slow
612
+ const structure = {
613
+ type: 'struct',
614
+ value: new Map([
615
+ ['input', { type: 'value', value: { type: StringType, writable: true } }],
616
+ ['fast_output', { type: 'value', value: { type: StringType, writable: true } }],
617
+ ['slow_output', { type: 'value', value: { type: StringType, writable: true } }],
618
+ ]),
619
+ };
620
+ const inputPath = [variant('field', 'input')];
621
+ const fastOutputPath = [variant('field', 'fast_output')];
622
+ const slowOutputPath = [variant('field', 'slow_output')];
623
+ const inputEncoder = encodeBeast2For(StringType);
624
+ const inputHash = await objectWrite(testRepo, inputEncoder('test'));
625
+ // Fast task completes quickly, slow task takes long
626
+ await createPackageWithTasks(testRepo, [
627
+ { name: 'fast-task', command: ['cp', '{input}', '{output}'], inputs: [inputPath], output: fastOutputPath },
628
+ { name: 'slow-task', command: ['bash', '-c', 'sleep 2; cp "$1" "$2"', '--', '{input}', '{output}'], inputs: [inputPath], output: slowOutputPath },
629
+ ], structure, {
630
+ input: {
631
+ value: 'test',
632
+ ref: { type: 'value', value: inputHash },
633
+ },
634
+ });
635
+ await workspaceDeploy(storage, testRepo, 'test-ws', 'test', '1.0.0');
636
+ await workspaceSetDataset(storage, testRepo, 'test-ws', inputPath, 'test', StringType);
637
+ const controller = new AbortController();
638
+ // Start execution with concurrency 2 so both tasks start
639
+ const executionPromise = dataflowExecute(storage, testRepo, 'test-ws', {
640
+ signal: controller.signal,
641
+ concurrency: 2,
642
+ });
643
+ // Wait for fast task to complete, then abort
644
+ await new Promise(resolve => setTimeout(resolve, 300));
645
+ controller.abort();
646
+ // Should throw with partial results
647
+ try {
648
+ await executionPromise;
649
+ assert.fail('Expected DataflowAbortedError');
650
+ }
651
+ catch (err) {
652
+ assert.ok(err instanceof DataflowAbortedError);
653
+ const abortErr = err;
654
+ assert.ok(abortErr.partialResults);
655
+ // Fast task should have completed
656
+ const fastResult = abortErr.partialResults.find(r => r.name === 'fast-task');
657
+ assert.ok(fastResult, 'Fast task should be in partial results');
658
+ assert.strictEqual(fastResult.state, 'success');
659
+ }
660
+ });
661
+ });
662
+ });
663
+ //# sourceMappingURL=dataflow.spec.js.map