@elaraai/e3-core 0.0.2-beta.9 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +4 -0
- package/README.md +74 -35
- package/dist/src/dataflow/api-compat.d.ts +90 -0
- package/dist/src/dataflow/api-compat.d.ts.map +1 -0
- package/dist/src/dataflow/api-compat.js +139 -0
- package/dist/src/dataflow/api-compat.js.map +1 -0
- package/dist/src/dataflow/api-compat.spec.d.ts +6 -0
- package/dist/src/dataflow/api-compat.spec.d.ts.map +1 -0
- package/dist/src/dataflow/api-compat.spec.js +182 -0
- package/dist/src/dataflow/api-compat.spec.js.map +1 -0
- package/dist/src/dataflow/index.d.ts +18 -0
- package/dist/src/dataflow/index.d.ts.map +1 -0
- package/dist/src/dataflow/index.js +23 -0
- package/dist/src/dataflow/index.js.map +1 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts +76 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js +729 -0
- package/dist/src/dataflow/orchestrator/LocalOrchestrator.js.map +1 -0
- package/dist/src/dataflow/orchestrator/index.d.ts +12 -0
- package/dist/src/dataflow/orchestrator/index.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/index.js +12 -0
- package/dist/src/dataflow/orchestrator/index.js.map +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts +163 -0
- package/dist/src/dataflow/orchestrator/interfaces.d.ts.map +1 -0
- package/dist/src/dataflow/orchestrator/interfaces.js +52 -0
- package/dist/src/dataflow/orchestrator/interfaces.js.map +1 -0
- package/dist/src/dataflow/state-store/FileStateStore.d.ts +67 -0
- package/dist/src/dataflow/state-store/FileStateStore.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/FileStateStore.js +300 -0
- package/dist/src/dataflow/state-store/FileStateStore.js.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts +42 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.js +229 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.js.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts +6 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js +114 -0
- package/dist/src/dataflow/state-store/InMemoryStateStore.spec.js.map +1 -0
- package/dist/src/dataflow/state-store/index.d.ts +13 -0
- package/dist/src/dataflow/state-store/index.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/index.js +13 -0
- package/dist/src/dataflow/state-store/index.js.map +1 -0
- package/dist/src/dataflow/state-store/interfaces.d.ts +159 -0
- package/dist/src/dataflow/state-store/interfaces.d.ts.map +1 -0
- package/dist/src/dataflow/state-store/interfaces.js +6 -0
- package/dist/src/dataflow/state-store/interfaces.js.map +1 -0
- package/dist/src/dataflow/steps.d.ts +222 -0
- package/dist/src/dataflow/steps.d.ts.map +1 -0
- package/dist/src/dataflow/steps.js +707 -0
- package/dist/src/dataflow/steps.js.map +1 -0
- package/dist/src/dataflow/steps.spec.d.ts +6 -0
- package/dist/src/dataflow/steps.spec.d.ts.map +1 -0
- package/dist/src/dataflow/steps.spec.js +343 -0
- package/dist/src/dataflow/steps.spec.js.map +1 -0
- package/dist/src/dataflow/types.d.ts +127 -0
- package/dist/src/dataflow/types.d.ts.map +1 -0
- package/dist/src/dataflow/types.js +7 -0
- package/dist/src/dataflow/types.js.map +1 -0
- package/dist/src/dataflow-orchestration.spec.d.ts +6 -0
- package/dist/src/dataflow-orchestration.spec.d.ts.map +1 -0
- package/dist/src/dataflow-orchestration.spec.js +1025 -0
- package/dist/src/dataflow-orchestration.spec.js.map +1 -0
- package/dist/src/dataflow.d.ts +113 -38
- package/dist/src/dataflow.d.ts.map +1 -1
- package/dist/src/dataflow.js +269 -416
- package/dist/src/dataflow.js.map +1 -1
- package/dist/src/dataflow.spec.d.ts +6 -0
- package/dist/src/dataflow.spec.d.ts.map +1 -0
- package/dist/src/dataflow.spec.js +663 -0
- package/dist/src/dataflow.spec.js.map +1 -0
- package/dist/src/dataset-refs.d.ts +124 -0
- package/dist/src/dataset-refs.d.ts.map +1 -0
- package/dist/src/dataset-refs.js +319 -0
- package/dist/src/dataset-refs.js.map +1 -0
- package/dist/src/errors.d.ts +39 -9
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +51 -8
- package/dist/src/errors.js.map +1 -1
- package/dist/src/errors.spec.d.ts +6 -0
- package/dist/src/errors.spec.d.ts.map +1 -0
- package/dist/src/errors.spec.js +276 -0
- package/dist/src/errors.spec.js.map +1 -0
- package/dist/src/execution/LocalTaskRunner.d.ts +73 -0
- package/dist/src/execution/LocalTaskRunner.d.ts.map +1 -0
- package/dist/src/execution/LocalTaskRunner.js +399 -0
- package/dist/src/execution/LocalTaskRunner.js.map +1 -0
- package/dist/src/execution/MockTaskRunner.d.ts +49 -0
- package/dist/src/execution/MockTaskRunner.d.ts.map +1 -0
- package/dist/src/execution/MockTaskRunner.js +54 -0
- package/dist/src/execution/MockTaskRunner.js.map +1 -0
- package/dist/src/execution/index.d.ts +16 -0
- package/dist/src/execution/index.d.ts.map +1 -0
- package/dist/src/execution/index.js +8 -0
- package/dist/src/execution/index.js.map +1 -0
- package/dist/src/execution/interfaces.d.ts +246 -0
- package/dist/src/execution/interfaces.d.ts.map +1 -0
- package/dist/src/execution/interfaces.js +6 -0
- package/dist/src/execution/interfaces.js.map +1 -0
- package/dist/src/execution/processHelpers.d.ts +20 -0
- package/dist/src/execution/processHelpers.d.ts.map +1 -0
- package/dist/src/execution/processHelpers.js +62 -0
- package/dist/src/execution/processHelpers.js.map +1 -0
- package/dist/src/executions.d.ts +71 -104
- package/dist/src/executions.d.ts.map +1 -1
- package/dist/src/executions.js +113 -481
- package/dist/src/executions.js.map +1 -1
- package/dist/src/executions.spec.d.ts +6 -0
- package/dist/src/executions.spec.d.ts.map +1 -0
- package/dist/src/executions.spec.js +387 -0
- package/dist/src/executions.spec.js.map +1 -0
- package/dist/src/formats.d.ts +18 -2
- package/dist/src/formats.d.ts.map +1 -1
- package/dist/src/formats.js +34 -2
- package/dist/src/formats.js.map +1 -1
- package/dist/src/gc.spec.d.ts +6 -0
- package/dist/src/gc.spec.d.ts.map +1 -0
- package/dist/src/gc.spec.js +512 -0
- package/dist/src/gc.spec.js.map +1 -0
- package/dist/src/index.d.ts +20 -10
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +48 -18
- package/dist/src/index.js.map +1 -1
- package/dist/src/objects.d.ts +7 -53
- package/dist/src/objects.d.ts.map +1 -1
- package/dist/src/objects.js +13 -232
- package/dist/src/objects.js.map +1 -1
- package/dist/src/objects.spec.d.ts +6 -0
- package/dist/src/objects.spec.d.ts.map +1 -0
- package/dist/src/objects.spec.js +247 -0
- package/dist/src/objects.spec.js.map +1 -0
- package/dist/src/packages.d.ts +41 -14
- package/dist/src/packages.d.ts.map +1 -1
- package/dist/src/packages.js +151 -89
- package/dist/src/packages.js.map +1 -1
- package/dist/src/packages.spec.d.ts +6 -0
- package/dist/src/packages.spec.d.ts.map +1 -0
- package/dist/src/packages.spec.js +324 -0
- package/dist/src/packages.spec.js.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts +35 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.js +107 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.js.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts +6 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js +187 -0
- package/dist/src/storage/in-memory/InMemoryRepoStore.spec.js.map +1 -0
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts +139 -0
- package/dist/src/storage/in-memory/InMemoryStorage.d.ts.map +1 -0
- package/dist/src/storage/in-memory/InMemoryStorage.js +439 -0
- package/dist/src/storage/in-memory/InMemoryStorage.js.map +1 -0
- package/dist/src/storage/in-memory/index.d.ts +12 -0
- package/dist/src/storage/in-memory/index.d.ts.map +1 -0
- package/dist/src/storage/in-memory/index.js +12 -0
- package/dist/src/storage/in-memory/index.js.map +1 -0
- package/dist/src/storage/index.d.ts +18 -0
- package/dist/src/storage/index.d.ts.map +1 -0
- package/dist/src/storage/index.js +10 -0
- package/dist/src/storage/index.js.map +1 -0
- package/dist/src/storage/interfaces.d.ts +581 -0
- package/dist/src/storage/interfaces.d.ts.map +1 -0
- package/dist/src/storage/interfaces.js +6 -0
- package/dist/src/storage/interfaces.js.map +1 -0
- package/dist/src/storage/local/LocalBackend.d.ts +56 -0
- package/dist/src/storage/local/LocalBackend.d.ts.map +1 -0
- package/dist/src/storage/local/LocalBackend.js +145 -0
- package/dist/src/storage/local/LocalBackend.js.map +1 -0
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts +22 -0
- package/dist/src/storage/local/LocalDatasetRefStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalDatasetRefStore.js +118 -0
- package/dist/src/storage/local/LocalDatasetRefStore.js.map +1 -0
- package/dist/src/storage/local/LocalLockService.d.ts +111 -0
- package/dist/src/storage/local/LocalLockService.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLockService.js +364 -0
- package/dist/src/storage/local/LocalLockService.js.map +1 -0
- package/dist/src/storage/local/LocalLockService.spec.d.ts +6 -0
- package/dist/src/storage/local/LocalLockService.spec.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLockService.spec.js +148 -0
- package/dist/src/storage/local/LocalLockService.spec.js.map +1 -0
- package/dist/src/storage/local/LocalLogStore.d.ts +23 -0
- package/dist/src/storage/local/LocalLogStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalLogStore.js +66 -0
- package/dist/src/storage/local/LocalLogStore.js.map +1 -0
- package/dist/src/storage/local/LocalObjectStore.d.ts +55 -0
- package/dist/src/storage/local/LocalObjectStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalObjectStore.js +300 -0
- package/dist/src/storage/local/LocalObjectStore.js.map +1 -0
- package/dist/src/storage/local/LocalRefStore.d.ts +50 -0
- package/dist/src/storage/local/LocalRefStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRefStore.js +337 -0
- package/dist/src/storage/local/LocalRefStore.js.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.d.ts +55 -0
- package/dist/src/storage/local/LocalRepoStore.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.js +365 -0
- package/dist/src/storage/local/LocalRepoStore.js.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.spec.d.ts +6 -0
- package/dist/src/storage/local/LocalRepoStore.spec.d.ts.map +1 -0
- package/dist/src/storage/local/LocalRepoStore.spec.js +255 -0
- package/dist/src/storage/local/LocalRepoStore.spec.js.map +1 -0
- package/dist/src/storage/local/gc.d.ts +92 -0
- package/dist/src/storage/local/gc.d.ts.map +1 -0
- package/dist/src/storage/local/gc.js +377 -0
- package/dist/src/storage/local/gc.js.map +1 -0
- package/dist/src/storage/local/index.d.ts +18 -0
- package/dist/src/storage/local/index.d.ts.map +1 -0
- package/dist/src/storage/local/index.js +18 -0
- package/dist/src/storage/local/index.js.map +1 -0
- package/dist/src/storage/local/localHelpers.d.ts +25 -0
- package/dist/src/storage/local/localHelpers.d.ts.map +1 -0
- package/dist/src/storage/local/localHelpers.js +69 -0
- package/dist/src/storage/local/localHelpers.js.map +1 -0
- package/dist/src/{repository.d.ts → storage/local/repository.d.ts} +8 -4
- package/dist/src/storage/local/repository.d.ts.map +1 -0
- package/dist/src/{repository.js → storage/local/repository.js} +31 -29
- package/dist/src/storage/local/repository.js.map +1 -0
- package/dist/src/storage/local/repository.spec.d.ts +6 -0
- package/dist/src/storage/local/repository.spec.d.ts.map +1 -0
- package/dist/src/storage/local/repository.spec.js +186 -0
- package/dist/src/storage/local/repository.spec.js.map +1 -0
- package/dist/src/tasks.d.ts +16 -10
- package/dist/src/tasks.d.ts.map +1 -1
- package/dist/src/tasks.js +35 -41
- package/dist/src/tasks.js.map +1 -1
- package/dist/src/tasks.spec.d.ts +6 -0
- package/dist/src/tasks.spec.d.ts.map +1 -0
- package/dist/src/tasks.spec.js +105 -0
- package/dist/src/tasks.spec.js.map +1 -0
- package/dist/src/test-helpers.d.ts +5 -4
- package/dist/src/test-helpers.d.ts.map +1 -1
- package/dist/src/test-helpers.js +9 -21
- package/dist/src/test-helpers.js.map +1 -1
- package/dist/src/transfer/InMemoryTransferBackend.d.ts +75 -0
- package/dist/src/transfer/InMemoryTransferBackend.d.ts.map +1 -0
- package/dist/src/transfer/InMemoryTransferBackend.js +211 -0
- package/dist/src/transfer/InMemoryTransferBackend.js.map +1 -0
- package/dist/src/transfer/index.d.ts +9 -0
- package/dist/src/transfer/index.d.ts.map +1 -0
- package/dist/src/transfer/index.js +11 -0
- package/dist/src/transfer/index.js.map +1 -0
- package/dist/src/transfer/interfaces.d.ts +103 -0
- package/dist/src/transfer/interfaces.d.ts.map +1 -0
- package/dist/src/transfer/interfaces.js +6 -0
- package/dist/src/transfer/interfaces.js.map +1 -0
- package/dist/src/transfer/process.d.ts +55 -0
- package/dist/src/transfer/process.d.ts.map +1 -0
- package/dist/src/transfer/process.js +144 -0
- package/dist/src/transfer/process.js.map +1 -0
- package/dist/src/transfer/types.d.ts +106 -0
- package/dist/src/transfer/types.d.ts.map +1 -0
- package/dist/src/transfer/types.js +61 -0
- package/dist/src/transfer/types.js.map +1 -0
- package/dist/src/trees.d.ts +102 -63
- package/dist/src/trees.d.ts.map +1 -1
- package/dist/src/trees.js +319 -479
- package/dist/src/trees.js.map +1 -1
- package/dist/src/trees.spec.d.ts +6 -0
- package/dist/src/trees.spec.d.ts.map +1 -0
- package/dist/src/trees.spec.js +635 -0
- package/dist/src/trees.spec.js.map +1 -0
- package/dist/src/uuid.d.ts +26 -0
- package/dist/src/uuid.d.ts.map +1 -0
- package/dist/src/uuid.js +80 -0
- package/dist/src/uuid.js.map +1 -0
- package/dist/src/workspaceStatus.d.ts +6 -4
- package/dist/src/workspaceStatus.d.ts.map +1 -1
- package/dist/src/workspaceStatus.js +46 -60
- package/dist/src/workspaceStatus.js.map +1 -1
- package/dist/src/workspaces.d.ts +46 -47
- package/dist/src/workspaces.d.ts.map +1 -1
- package/dist/src/workspaces.js +281 -221
- package/dist/src/workspaces.js.map +1 -1
- package/dist/src/workspaces.spec.d.ts +6 -0
- package/dist/src/workspaces.spec.d.ts.map +1 -0
- package/dist/src/workspaces.spec.js +273 -0
- package/dist/src/workspaces.spec.js.map +1 -0
- package/package.json +15 -15
- package/dist/src/gc.d.ts +0 -54
- package/dist/src/gc.d.ts.map +0 -1
- package/dist/src/gc.js +0 -233
- package/dist/src/gc.js.map +0 -1
- package/dist/src/repository.d.ts.map +0 -1
- package/dist/src/repository.js.map +0 -1
- package/dist/src/workspaceLock.d.ts +0 -67
- package/dist/src/workspaceLock.d.ts.map +0 -1
- package/dist/src/workspaceLock.js +0 -217
- 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
|