@tagma/sdk 0.6.12 → 0.7.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/README.md +56 -15
- package/dist/bootstrap.d.ts +6 -6
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +5 -6
- package/dist/bootstrap.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -0
- package/dist/core/dataflow.d.ts +23 -0
- package/dist/core/dataflow.d.ts.map +1 -0
- package/dist/core/dataflow.js +99 -0
- package/dist/core/dataflow.js.map +1 -0
- package/dist/core/log-prune.d.ts +16 -0
- package/dist/core/log-prune.d.ts.map +1 -0
- package/dist/core/log-prune.js +34 -0
- package/dist/core/log-prune.js.map +1 -0
- package/dist/core/preflight.d.ts +13 -0
- package/dist/core/preflight.d.ts.map +1 -0
- package/dist/core/preflight.js +61 -0
- package/dist/core/preflight.js.map +1 -0
- package/dist/core/run-context.d.ts +52 -0
- package/dist/core/run-context.d.ts.map +1 -0
- package/dist/core/run-context.js +156 -0
- package/dist/core/run-context.js.map +1 -0
- package/dist/core/run-state.d.ts +25 -0
- package/dist/core/run-state.d.ts.map +1 -0
- package/dist/core/run-state.js +93 -0
- package/dist/core/run-state.js.map +1 -0
- package/dist/core/scheduler.d.ts +13 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +35 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/task-executor.d.ts +13 -0
- package/dist/core/task-executor.d.ts.map +1 -0
- package/dist/core/task-executor.js +623 -0
- package/dist/core/task-executor.js.map +1 -0
- package/dist/core/trigger-errors.d.ts +9 -0
- package/dist/core/trigger-errors.d.ts.map +1 -0
- package/dist/core/trigger-errors.js +15 -0
- package/dist/core/trigger-errors.js.map +1 -0
- package/dist/engine.d.ts +6 -14
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +68 -1035
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline-definition.d.ts +3 -0
- package/dist/pipeline-definition.d.ts.map +1 -0
- package/dist/pipeline-definition.js +4 -0
- package/dist/pipeline-definition.js.map +1 -0
- package/dist/pipeline-runner.d.ts +2 -1
- package/dist/pipeline-runner.d.ts.map +1 -1
- package/dist/pipeline-runner.js +2 -2
- package/dist/pipeline-runner.js.map +1 -1
- package/dist/plugins.d.ts +5 -0
- package/dist/plugins.d.ts.map +1 -0
- package/dist/plugins.js +3 -0
- package/dist/plugins.js.map +1 -0
- package/dist/ports.d.ts +4 -0
- package/dist/ports.d.ts.map +1 -1
- package/dist/ports.js +27 -4
- package/dist/ports.js.map +1 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +7 -35
- package/dist/registry.js.map +1 -1
- package/dist/tagma.d.ts +24 -0
- package/dist/tagma.d.ts.map +1 -0
- package/dist/tagma.js +23 -0
- package/dist/tagma.js.map +1 -0
- package/dist/utils-api.d.ts +2 -0
- package/dist/utils-api.d.ts.map +1 -0
- package/dist/utils-api.js +2 -0
- package/dist/utils-api.js.map +1 -0
- package/dist/validate-raw.d.ts +4 -4
- package/dist/validate-raw.js +91 -132
- package/dist/validate-raw.js.map +1 -1
- package/dist/yaml.d.ts +4 -0
- package/dist/yaml.d.ts.map +1 -0
- package/dist/yaml.js +3 -0
- package/dist/yaml.js.map +1 -0
- package/package.json +53 -8
- package/src/bootstrap.ts +6 -6
- package/src/config.ts +26 -0
- package/src/core/dataflow.test.ts +166 -0
- package/src/core/dataflow.ts +161 -0
- package/src/core/log-prune.test.ts +58 -0
- package/src/core/log-prune.ts +43 -0
- package/src/core/preflight.test.ts +49 -0
- package/src/core/preflight.ts +89 -0
- package/src/core/run-context.test.ts +244 -0
- package/src/core/run-context.ts +207 -0
- package/src/core/run-state.test.ts +98 -0
- package/src/core/run-state.ts +122 -0
- package/src/core/scheduler.test.ts +83 -0
- package/src/core/scheduler.ts +42 -0
- package/src/core/task-executor.ts +769 -0
- package/src/core/trigger-errors.ts +15 -0
- package/src/engine-ports-mixed.test.ts +68 -411
- package/src/engine-ports.test.ts +37 -341
- package/src/engine.ts +80 -1248
- package/src/index.ts +28 -0
- package/src/pipeline-definition.ts +5 -0
- package/src/pipeline-runner.test.ts +5 -9
- package/src/pipeline-runner.ts +3 -2
- package/src/plugin-registry.test.ts +7 -10
- package/src/plugins.ts +18 -0
- package/src/ports.test.ts +80 -0
- package/src/ports.ts +36 -4
- package/src/registry.ts +7 -49
- package/src/schema-ports.test.ts +41 -214
- package/src/tagma.test.ts +84 -0
- package/src/tagma.ts +47 -0
- package/src/utils-api.ts +8 -0
- package/src/validate-raw-ports.test.ts +80 -393
- package/src/validate-raw.ts +93 -137
- package/src/yaml.ts +11 -0
- package/dist/sdk.d.ts +0 -32
- package/dist/sdk.d.ts.map +0 -1
- package/dist/sdk.js +0 -41
- package/dist/sdk.js.map +0 -1
- package/src/sdk.ts +0 -151
package/src/engine-ports.test.ts
CHANGED
|
@@ -2,12 +2,10 @@ import { describe, expect, test } from 'bun:test';
|
|
|
2
2
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { PluginRegistry } from './registry';
|
|
6
5
|
import { bootstrapBuiltins } from './bootstrap';
|
|
7
6
|
import { runPipeline, type RunEventPayload } from './engine';
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
7
|
+
import { PluginRegistry } from './registry';
|
|
8
|
+
import type { PipelineConfig, TaskConfig, TaskStatus } from './types';
|
|
11
9
|
|
|
12
10
|
const PERMS = { read: true, write: false, execute: false };
|
|
13
11
|
|
|
@@ -18,36 +16,21 @@ function freshRegistry(): PluginRegistry {
|
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
function makeDir(): string {
|
|
21
|
-
return mkdtempSync(join(tmpdir(), 'tagma-
|
|
19
|
+
return mkdtempSync(join(tmpdir(), 'tagma-bindings-'));
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
/**
|
|
25
|
-
* Write a small Node script to the workspace dir that emits the given
|
|
26
|
-
* payload on stdout as a single-line JSON object.
|
|
27
|
-
*
|
|
28
|
-
* Tests that rely on shell-quoted inline JSON (`echo '{"x":1}'`) are
|
|
29
|
-
* fragile across Windows cmd / PowerShell / Git Bash — quote handling
|
|
30
|
-
* differs widely. Putting the payload into a Node script instead keeps
|
|
31
|
-
* the command line a plain `node /path/to/file.js`, which survives any
|
|
32
|
-
* shell, and still exercises the engine's "last-line JSON" extraction
|
|
33
|
-
* on real child-process output.
|
|
34
|
-
*/
|
|
35
22
|
function writeEmitScript(dir: string, name: string, payload: Record<string, unknown>): string {
|
|
36
23
|
const path = join(dir, `${name}.js`);
|
|
37
|
-
|
|
38
|
-
|
|
24
|
+
writeFileSync(
|
|
25
|
+
path,
|
|
26
|
+
`process.stdout.write(${JSON.stringify(JSON.stringify(payload))});\nprocess.stdout.write('\\n');\n`,
|
|
27
|
+
);
|
|
39
28
|
return path;
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
/**
|
|
43
|
-
* Same as writeEmitScript but echoes args joined with `|`, so downstream
|
|
44
|
-
* tests can assert that upstream input values ended up on the command
|
|
45
|
-
* line post-substitution.
|
|
46
|
-
*/
|
|
47
31
|
function writeEchoArgsScript(dir: string, name: string): string {
|
|
48
32
|
const path = join(dir, `${name}.js`);
|
|
49
|
-
|
|
50
|
-
writeFileSync(path, src);
|
|
33
|
+
writeFileSync(path, `process.stdout.write(process.argv.slice(2).join('|'));\n`);
|
|
51
34
|
return path;
|
|
52
35
|
}
|
|
53
36
|
|
|
@@ -62,7 +45,7 @@ function task(overrides: Partial<TaskConfig> & { id: string }): TaskConfig {
|
|
|
62
45
|
|
|
63
46
|
function pipeline(tasks: TaskConfig[]): PipelineConfig {
|
|
64
47
|
return {
|
|
65
|
-
name: '
|
|
48
|
+
name: 'bindings-test',
|
|
66
49
|
tracks: [
|
|
67
50
|
{
|
|
68
51
|
id: 't',
|
|
@@ -76,24 +59,14 @@ function pipeline(tasks: TaskConfig[]): PipelineConfig {
|
|
|
76
59
|
};
|
|
77
60
|
}
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
events: RunEventPayload[];
|
|
81
|
-
states: ReadonlyMap<string, unknown>;
|
|
82
|
-
success: boolean;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function run(
|
|
86
|
-
config: PipelineConfig,
|
|
87
|
-
workDir: string,
|
|
88
|
-
registry = freshRegistry(),
|
|
89
|
-
): Promise<RunResult> {
|
|
62
|
+
async function run(config: PipelineConfig, workDir: string) {
|
|
90
63
|
const events: RunEventPayload[] = [];
|
|
91
64
|
const result = await runPipeline(config, workDir, {
|
|
92
|
-
registry,
|
|
65
|
+
registry: freshRegistry(),
|
|
93
66
|
skipPluginLoading: true,
|
|
94
67
|
onEvent: (e) => events.push(e),
|
|
95
68
|
});
|
|
96
|
-
return { events,
|
|
69
|
+
return { events, success: result.success };
|
|
97
70
|
}
|
|
98
71
|
|
|
99
72
|
function finalUpdateFor(events: RunEventPayload[], qid: string): RunEventPayload | undefined {
|
|
@@ -109,360 +82,83 @@ function finalStatusFrom(events: RunEventPayload[], qid: string): TaskStatus | u
|
|
|
109
82
|
return last && last.type === 'task_update' ? last.status : undefined;
|
|
110
83
|
}
|
|
111
84
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
describe('engine — ports: output extraction + input resolution', () => {
|
|
115
|
-
test('lightweight task inputs substitute command placeholders without ports', async () => {
|
|
85
|
+
describe('engine — unified inputs and outputs', () => {
|
|
86
|
+
test('typed outputs feed typed inputs and command placeholders', async () => {
|
|
116
87
|
const dir = makeDir();
|
|
117
88
|
try {
|
|
118
|
-
const
|
|
119
|
-
const config = pipeline([
|
|
120
|
-
task({
|
|
121
|
-
id: 'down',
|
|
122
|
-
command: `node "${echo}" "{{inputs.city}}" "{{inputs.mode}}"`,
|
|
123
|
-
inputs: {
|
|
124
|
-
city: { value: 'Shanghai' },
|
|
125
|
-
mode: { default: 'quick' },
|
|
126
|
-
},
|
|
127
|
-
}),
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
const { events, success } = await run(config, dir);
|
|
131
|
-
expect(success).toBe(true);
|
|
132
|
-
const downFinal = finalUpdateFor(events, 't.down');
|
|
133
|
-
if (downFinal?.type === 'task_update') {
|
|
134
|
-
expect((downFinal.stdout ?? '').trim()).toBe('Shanghai|quick');
|
|
135
|
-
expect(downFinal.inputs).toEqual({ city: 'Shanghai', mode: 'quick' });
|
|
136
|
-
}
|
|
137
|
-
} finally {
|
|
138
|
-
rmSync(dir, { recursive: true, force: true });
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test('lightweight task outputs publish named values for downstream bindings', async () => {
|
|
143
|
-
const dir = makeDir();
|
|
144
|
-
try {
|
|
145
|
-
const emit = writeEmitScript(dir, 'emit', { bundlePath: 'dist/app.js' });
|
|
146
|
-
const echo = writeEchoArgsScript(dir, 'echo');
|
|
147
|
-
const config = pipeline([
|
|
148
|
-
task({
|
|
149
|
-
id: 'build',
|
|
150
|
-
command: `node "${emit}"`,
|
|
151
|
-
outputs: {
|
|
152
|
-
bundlePath: {},
|
|
153
|
-
},
|
|
154
|
-
}),
|
|
155
|
-
task({
|
|
156
|
-
id: 'test',
|
|
157
|
-
depends_on: ['build'],
|
|
158
|
-
command: `node "${echo}" "{{inputs.bundlePath}}"`,
|
|
159
|
-
inputs: {
|
|
160
|
-
bundlePath: { from: 't.build.outputs.bundlePath', required: true },
|
|
161
|
-
},
|
|
162
|
-
}),
|
|
163
|
-
]);
|
|
164
|
-
|
|
165
|
-
const { events, success } = await run(config, dir);
|
|
166
|
-
expect(success).toBe(true);
|
|
167
|
-
const buildFinal = finalUpdateFor(events, 't.build');
|
|
168
|
-
if (buildFinal?.type === 'task_update') {
|
|
169
|
-
expect(buildFinal.outputs).toEqual({ bundlePath: 'dist/app.js' });
|
|
170
|
-
}
|
|
171
|
-
const testFinal = finalUpdateFor(events, 't.test');
|
|
172
|
-
if (testFinal?.type === 'task_update') {
|
|
173
|
-
expect((testFinal.stdout ?? '').trim()).toBe('dist/app.js');
|
|
174
|
-
expect(testFinal.inputs).toEqual({ bundlePath: 'dist/app.js' });
|
|
175
|
-
}
|
|
176
|
-
} finally {
|
|
177
|
-
rmSync(dir, { recursive: true, force: true });
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test('upstream outputs feed downstream inputs via name match', async () => {
|
|
182
|
-
const dir = makeDir();
|
|
183
|
-
try {
|
|
184
|
-
const emit = writeEmitScript(dir, 'emit', { city: 'Shanghai', id: 42 });
|
|
89
|
+
const emit = writeEmitScript(dir, 'emit', { id: '42', city: 'Shanghai' });
|
|
185
90
|
const echo = writeEchoArgsScript(dir, 'echo');
|
|
186
91
|
const config = pipeline([
|
|
187
92
|
task({
|
|
188
93
|
id: 'up',
|
|
189
94
|
command: `node "${emit}"`,
|
|
190
|
-
|
|
191
|
-
outputs: [
|
|
192
|
-
{ name: 'city', type: 'string' },
|
|
193
|
-
{ name: 'id', type: 'number' },
|
|
194
|
-
],
|
|
195
|
-
} as TaskPorts,
|
|
95
|
+
outputs: { id: { type: 'number' }, city: { type: 'string' } },
|
|
196
96
|
}),
|
|
197
97
|
task({
|
|
198
98
|
id: 'down',
|
|
199
99
|
depends_on: ['up'],
|
|
200
100
|
command: `node "${echo}" "{{inputs.city}}" "{{inputs.id}}"`,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
],
|
|
206
|
-
} as TaskPorts,
|
|
101
|
+
inputs: {
|
|
102
|
+
city: { from: 't.up.outputs.city', type: 'string', required: true },
|
|
103
|
+
id: { from: 't.up.outputs.id', type: 'number', required: true },
|
|
104
|
+
},
|
|
207
105
|
}),
|
|
208
106
|
]);
|
|
209
107
|
|
|
210
108
|
const { events, success } = await run(config, dir);
|
|
211
109
|
expect(success).toBe(true);
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const upFinal = finalUpdateFor(events, 't.up')!;
|
|
216
|
-
expect(upFinal.type).toBe('task_update');
|
|
217
|
-
if (upFinal.type !== 'task_update') return;
|
|
218
|
-
expect(upFinal.outputs).toEqual({ city: 'Shanghai', id: 42 });
|
|
219
|
-
|
|
220
|
-
// Downstream saw the values: echoed stdout is "Shanghai|42\n".
|
|
221
|
-
const downFinal = finalUpdateFor(events, 't.down')!;
|
|
222
|
-
if (downFinal.type !== 'task_update') return;
|
|
223
|
-
expect(downFinal.status).toBe('success');
|
|
224
|
-
expect((downFinal.stdout ?? '').trim()).toBe('Shanghai|42');
|
|
225
|
-
expect(downFinal.inputs).toEqual({ city: 'Shanghai', id: 42 });
|
|
110
|
+
expect(finalUpdateFor(events, 't.up')?.outputs).toEqual({ id: 42, city: 'Shanghai' });
|
|
111
|
+
expect(finalUpdateFor(events, 't.down')?.inputs).toEqual({ city: 'Shanghai', id: 42 });
|
|
112
|
+
expect(finalUpdateFor(events, 't.down')?.stdout).toContain('Shanghai|42');
|
|
226
113
|
} finally {
|
|
227
114
|
rmSync(dir, { recursive: true, force: true });
|
|
228
115
|
}
|
|
229
116
|
});
|
|
230
117
|
|
|
231
|
-
test('
|
|
118
|
+
test('missing required unified input blocks without spawning downstream', async () => {
|
|
232
119
|
const dir = makeDir();
|
|
233
120
|
try {
|
|
234
|
-
|
|
235
|
-
// the engine can't extract `city` — diagnostic on stderr, no
|
|
236
|
-
// outputs. Downstream required input unresolved → blocked.
|
|
237
|
-
const noJson = join(dir, 'no-json.js');
|
|
238
|
-
writeFileSync(noJson, "process.stdout.write('hello\\n');\n");
|
|
121
|
+
const emit = writeEmitScript(dir, 'emit', { other: 'x' });
|
|
239
122
|
const echo = writeEchoArgsScript(dir, 'echo');
|
|
240
123
|
const config = pipeline([
|
|
241
|
-
task({
|
|
242
|
-
id: 'up',
|
|
243
|
-
command: `node "${noJson}"`,
|
|
244
|
-
ports: { outputs: [{ name: 'city', type: 'string' }] } as TaskPorts,
|
|
245
|
-
}),
|
|
124
|
+
task({ id: 'up', command: `node "${emit}"`, outputs: { city: { type: 'string' } } }),
|
|
246
125
|
task({
|
|
247
126
|
id: 'down',
|
|
248
127
|
depends_on: ['up'],
|
|
249
128
|
command: `node "${echo}" "{{inputs.city}}"`,
|
|
250
|
-
|
|
251
|
-
inputs: [{ name: 'city', type: 'string', required: true }],
|
|
252
|
-
} as TaskPorts,
|
|
129
|
+
inputs: { city: { from: 't.up.outputs.city', type: 'string', required: true } },
|
|
253
130
|
}),
|
|
254
131
|
]);
|
|
255
132
|
|
|
256
133
|
const { events, success } = await run(config, dir);
|
|
257
134
|
expect(success).toBe(false);
|
|
258
135
|
expect(finalStatusFrom(events, 't.up')).toBe('success');
|
|
259
|
-
|
|
260
|
-
expect(downStatus).toBe('blocked');
|
|
261
|
-
// The blocked update carries the engine's diagnostic in stderr so
|
|
262
|
-
// the editor can display it verbatim.
|
|
263
|
-
const downFinal = finalUpdateFor(events, 't.down');
|
|
264
|
-
if (downFinal?.type === 'task_update') {
|
|
265
|
-
expect(downFinal.stderr ?? '').toMatch(/missing required input.*city/i);
|
|
266
|
-
}
|
|
136
|
+
expect(finalStatusFrom(events, 't.down')).toBe('blocked');
|
|
267
137
|
} finally {
|
|
268
138
|
rmSync(dir, { recursive: true, force: true });
|
|
269
139
|
}
|
|
270
140
|
});
|
|
271
141
|
|
|
272
|
-
test('
|
|
142
|
+
test('typed output coercion diagnostics leave missing downstream input', async () => {
|
|
273
143
|
const dir = makeDir();
|
|
274
144
|
try {
|
|
275
|
-
const
|
|
276
|
-
writeFileSync(noop, 'process.stdout.write("ok\\n");\n');
|
|
145
|
+
const emit = writeEmitScript(dir, 'emit', { id: 'not-a-number' });
|
|
277
146
|
const echo = writeEchoArgsScript(dir, 'echo');
|
|
278
147
|
const config = pipeline([
|
|
279
|
-
task({ id: 'up', command: `node "${
|
|
280
|
-
task({
|
|
281
|
-
id: 'down',
|
|
282
|
-
depends_on: ['up'],
|
|
283
|
-
command: `node "${echo}" "{{inputs.lang}}"`,
|
|
284
|
-
ports: {
|
|
285
|
-
inputs: [{ name: 'lang', type: 'string', default: 'en' }],
|
|
286
|
-
} as TaskPorts,
|
|
287
|
-
}),
|
|
288
|
-
]);
|
|
289
|
-
const { events, success } = await run(config, dir);
|
|
290
|
-
expect(success).toBe(true);
|
|
291
|
-
const downFinal = finalUpdateFor(events, 't.down');
|
|
292
|
-
if (downFinal?.type === 'task_update') {
|
|
293
|
-
expect((downFinal.stdout ?? '').trim()).toBe('en');
|
|
294
|
-
expect(downFinal.inputs).toEqual({ lang: 'en' });
|
|
295
|
-
}
|
|
296
|
-
} finally {
|
|
297
|
-
rmSync(dir, { recursive: true, force: true });
|
|
298
|
-
}
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
test('optional input without default or upstream → empty placeholder', async () => {
|
|
302
|
-
const dir = makeDir();
|
|
303
|
-
try {
|
|
304
|
-
const noop = join(dir, 'noop.js');
|
|
305
|
-
writeFileSync(noop, 'process.stdout.write("ok\\n");\n');
|
|
306
|
-
// Use a Node script that prints `|<arg>|` so the empty substitution
|
|
307
|
-
// shows as `||` — cross-platform argv handling.
|
|
308
|
-
const sentinel = join(dir, 'sentinel.js');
|
|
309
|
-
writeFileSync(sentinel, 'process.stdout.write("<" + (process.argv[2] || "") + ">\\n");\n');
|
|
310
|
-
const config = pipeline([
|
|
311
|
-
task({ id: 'up', command: `node "${noop}"` }),
|
|
148
|
+
task({ id: 'up', command: `node "${emit}"`, outputs: { id: { type: 'number' } } }),
|
|
312
149
|
task({
|
|
313
150
|
id: 'down',
|
|
314
151
|
depends_on: ['up'],
|
|
315
|
-
command: `node "${
|
|
316
|
-
|
|
317
|
-
inputs: [{ name: 'note', type: 'string' }],
|
|
318
|
-
} as TaskPorts,
|
|
152
|
+
command: `node "${echo}" "{{inputs.id}}"`,
|
|
153
|
+
inputs: { id: { from: 't.up.outputs.id', type: 'number', required: true } },
|
|
319
154
|
}),
|
|
320
155
|
]);
|
|
321
|
-
const { events, success } = await run(config, dir);
|
|
322
|
-
expect(success).toBe(true);
|
|
323
|
-
const downFinal = finalUpdateFor(events, 't.down');
|
|
324
|
-
if (downFinal?.type === 'task_update') {
|
|
325
|
-
expect((downFinal.stdout ?? '').trim()).toBe('<>');
|
|
326
|
-
}
|
|
327
|
-
} finally {
|
|
328
|
-
rmSync(dir, { recursive: true, force: true });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
156
|
|
|
332
|
-
test('tasks with no ports declared are unaffected', async () => {
|
|
333
|
-
const dir = makeDir();
|
|
334
|
-
try {
|
|
335
|
-
const config = pipeline([task({ id: 'plain', command: 'echo hello' })]);
|
|
336
157
|
const { events, success } = await run(config, dir);
|
|
337
|
-
expect(success).toBe(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
expect(final.inputs).toEqual({});
|
|
342
|
-
}
|
|
343
|
-
} finally {
|
|
344
|
-
rmSync(dir, { recursive: true, force: true });
|
|
345
|
-
}
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
test('ambiguous name-match blocks downstream unless disambiguated', async () => {
|
|
349
|
-
const dir = makeDir();
|
|
350
|
-
try {
|
|
351
|
-
const emitA = writeEmitScript(dir, 'emitA', { val: 'from-a' });
|
|
352
|
-
const emitB = writeEmitScript(dir, 'emitB', { val: 'from-b' });
|
|
353
|
-
const echo = writeEchoArgsScript(dir, 'echo');
|
|
354
|
-
// Two upstreams both export `val`; downstream auto-matches → ambiguous.
|
|
355
|
-
const ambigConfig = pipeline([
|
|
356
|
-
task({
|
|
357
|
-
id: 'a',
|
|
358
|
-
command: `node "${emitA}"`,
|
|
359
|
-
ports: { outputs: [{ name: 'val', type: 'string' }] } as TaskPorts,
|
|
360
|
-
}),
|
|
361
|
-
task({
|
|
362
|
-
id: 'b',
|
|
363
|
-
command: `node "${emitB}"`,
|
|
364
|
-
ports: { outputs: [{ name: 'val', type: 'string' }] } as TaskPorts,
|
|
365
|
-
}),
|
|
366
|
-
task({
|
|
367
|
-
id: 'down',
|
|
368
|
-
depends_on: ['a', 'b'],
|
|
369
|
-
command: `node "${echo}" "{{inputs.val}}"`,
|
|
370
|
-
ports: {
|
|
371
|
-
inputs: [{ name: 'val', type: 'string', required: true }],
|
|
372
|
-
} as TaskPorts,
|
|
373
|
-
}),
|
|
374
|
-
]);
|
|
375
|
-
const { events: evAmbig } = await run(ambigConfig, dir);
|
|
376
|
-
expect(finalStatusFrom(evAmbig, 't.down')).toBe('blocked');
|
|
377
|
-
const ambigFinal = finalUpdateFor(evAmbig, 't.down');
|
|
378
|
-
if (ambigFinal?.type === 'task_update') {
|
|
379
|
-
expect(ambigFinal.stderr ?? '').toMatch(/ambiguous|multiple upstreams/i);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Now add an explicit `from: "t.b.val"` → downstream should succeed.
|
|
383
|
-
const dir2 = makeDir();
|
|
384
|
-
try {
|
|
385
|
-
const emitA2 = writeEmitScript(dir2, 'emitA', { val: 'from-a' });
|
|
386
|
-
const emitB2 = writeEmitScript(dir2, 'emitB', { val: 'from-b' });
|
|
387
|
-
const echo2 = writeEchoArgsScript(dir2, 'echo');
|
|
388
|
-
const explicitConfig = pipeline([
|
|
389
|
-
task({
|
|
390
|
-
id: 'a',
|
|
391
|
-
command: `node "${emitA2}"`,
|
|
392
|
-
ports: { outputs: [{ name: 'val', type: 'string' }] } as TaskPorts,
|
|
393
|
-
}),
|
|
394
|
-
task({
|
|
395
|
-
id: 'b',
|
|
396
|
-
command: `node "${emitB2}"`,
|
|
397
|
-
ports: { outputs: [{ name: 'val', type: 'string' }] } as TaskPorts,
|
|
398
|
-
}),
|
|
399
|
-
task({
|
|
400
|
-
id: 'down',
|
|
401
|
-
depends_on: ['a', 'b'],
|
|
402
|
-
command: `node "${echo2}" "{{inputs.val}}"`,
|
|
403
|
-
ports: {
|
|
404
|
-
inputs: [
|
|
405
|
-
{ name: 'val', type: 'string', required: true, from: 't.b.val' },
|
|
406
|
-
],
|
|
407
|
-
} as TaskPorts,
|
|
408
|
-
}),
|
|
409
|
-
]);
|
|
410
|
-
const { events: evExplicit, success } = await run(explicitConfig, dir2);
|
|
411
|
-
expect(success).toBe(true);
|
|
412
|
-
const downFinal = finalUpdateFor(evExplicit, 't.down');
|
|
413
|
-
if (downFinal?.type === 'task_update') {
|
|
414
|
-
expect((downFinal.stdout ?? '').trim()).toBe('from-b');
|
|
415
|
-
}
|
|
416
|
-
} finally {
|
|
417
|
-
rmSync(dir2, { recursive: true, force: true });
|
|
418
|
-
}
|
|
419
|
-
} finally {
|
|
420
|
-
rmSync(dir, { recursive: true, force: true });
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
test('string → number coercion happens during input resolution', async () => {
|
|
425
|
-
const dir = makeDir();
|
|
426
|
-
try {
|
|
427
|
-
// Emit `id` as a string; downstream declares it as `number`.
|
|
428
|
-
const emit = writeEmitScript(dir, 'emit', { id: '42' });
|
|
429
|
-
const script = join(dir, 'assert-number.js');
|
|
430
|
-
writeFileSync(
|
|
431
|
-
script,
|
|
432
|
-
`const v = process.argv[2];
|
|
433
|
-
const n = Number(v);
|
|
434
|
-
if (!Number.isFinite(n)) { process.exit(2); }
|
|
435
|
-
process.stdout.write("n=" + n + "\\n");
|
|
436
|
-
`,
|
|
437
|
-
);
|
|
438
|
-
const config = pipeline([
|
|
439
|
-
task({
|
|
440
|
-
id: 'up',
|
|
441
|
-
command: `node "${emit}"`,
|
|
442
|
-
ports: {
|
|
443
|
-
// upstream declares string — matches the emitted literal.
|
|
444
|
-
outputs: [{ name: 'id', type: 'string' }],
|
|
445
|
-
} as TaskPorts,
|
|
446
|
-
}),
|
|
447
|
-
task({
|
|
448
|
-
id: 'down',
|
|
449
|
-
depends_on: ['up'],
|
|
450
|
-
command: `node "${script}" "{{inputs.id}}"`,
|
|
451
|
-
ports: {
|
|
452
|
-
// downstream demands number — resolve should coerce "42" → 42.
|
|
453
|
-
inputs: [{ name: 'id', type: 'number', required: true }],
|
|
454
|
-
} as TaskPorts,
|
|
455
|
-
}),
|
|
456
|
-
]);
|
|
457
|
-
const { events, success } = await run(config, dir);
|
|
458
|
-
expect(success).toBe(true);
|
|
459
|
-
const downFinal = finalUpdateFor(events, 't.down');
|
|
460
|
-
if (downFinal?.type === 'task_update') {
|
|
461
|
-
expect((downFinal.stdout ?? '').trim()).toBe('n=42');
|
|
462
|
-
// Value on the wire should be the coerced number, not the raw
|
|
463
|
-
// string, so the editor renders it faithfully too.
|
|
464
|
-
expect(downFinal.inputs).toEqual({ id: 42 });
|
|
465
|
-
}
|
|
158
|
+
expect(success).toBe(false);
|
|
159
|
+
expect(finalStatusFrom(events, 't.up')).toBe('success');
|
|
160
|
+
expect(finalUpdateFor(events, 't.up')?.stderr).toContain('expected number');
|
|
161
|
+
expect(finalStatusFrom(events, 't.down')).toBe('blocked');
|
|
466
162
|
} finally {
|
|
467
163
|
rmSync(dir, { recursive: true, force: true });
|
|
468
164
|
}
|