@aion0/forge 0.10.12 → 0.10.18

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 (40) hide show
  1. package/RELEASE_NOTES.md +3 -3
  2. package/app/api/public-info/[resource]/route.ts +40 -0
  3. package/components/ProjectDetail.tsx +1 -1
  4. package/components/SettingsModal.tsx +42 -33
  5. package/components/WebTerminal.tsx +13 -5
  6. package/components/WorkspaceView.tsx +5 -3
  7. package/lib/agents/index.ts +6 -1
  8. package/lib/agents/known-models.ts +75 -0
  9. package/lib/chat/tool-dispatcher.ts +33 -0
  10. package/lib/help-docs/05-pipelines.md +9 -0
  11. package/lib/public-info/fetch.ts +116 -0
  12. package/lib/public-info/types.ts +38 -0
  13. package/lib/public-info/use-models-registry.ts +66 -0
  14. package/lib/settings.ts +9 -0
  15. package/next-env.d.ts +1 -1
  16. package/package.json +1 -1
  17. package/lib/__tests__/foreach-batch-yaml.test.ts +0 -33
  18. package/lib/__tests__/foreach-before.test.ts +0 -201
  19. package/lib/__tests__/foreach-parse.test.ts +0 -114
  20. package/lib/__tests__/foreach-snapshot.test.ts +0 -112
  21. package/lib/__tests__/foreach-source.test.ts +0 -105
  22. package/lib/__tests__/foreach-template.test.ts +0 -112
  23. package/lib/workspace/__tests__/state-machine.test.ts +0 -388
  24. package/lib/workspace/__tests__/workspace.test.ts +0 -311
  25. package/scripts/bench/README.md +0 -66
  26. package/scripts/bench/results/.gitignore +0 -2
  27. package/scripts/bench/run.ts +0 -635
  28. package/scripts/bench/tasks/01-text-utils/task.md +0 -26
  29. package/scripts/bench/tasks/01-text-utils/validator.sh +0 -46
  30. package/scripts/bench/tasks/02-pagination/setup.sh +0 -19
  31. package/scripts/bench/tasks/02-pagination/task.md +0 -48
  32. package/scripts/bench/tasks/02-pagination/validator.sh +0 -69
  33. package/scripts/bench/tasks/03-bug-fix/setup.sh +0 -82
  34. package/scripts/bench/tasks/03-bug-fix/task.md +0 -30
  35. package/scripts/bench/tasks/03-bug-fix/validator.sh +0 -29
  36. package/scripts/test-agents-migrate.ts +0 -149
  37. package/scripts/test-mantis.ts +0 -223
  38. package/scripts/test-memory-local.ts +0 -139
  39. package/scripts/test-memory-upsert.ts +0 -106
  40. package/scripts/verify-usage.ts +0 -178
package/lib/settings.ts CHANGED
@@ -112,6 +112,14 @@ export interface Settings {
112
112
  * shape as connectorsRepoUrl. Default: `aiwatching/forge-workflow`.
113
113
  */
114
114
  workflowRepoUrl: string;
115
+ /**
116
+ * Base URL for the `forge-public-info` repo. Houses model lists and
117
+ * other small JSON files that the maintainer updates between npm
118
+ * releases — push a JSON commit, all users pick it up within 24h via
119
+ * the cache. Sub-paths (e.g. `models/registry.json`) are appended by
120
+ * `lib/public-info/fetch.ts`. Default: `aiwatching/forge-public-info`.
121
+ */
122
+ publicInfoRepoUrl: string;
115
123
  /**
116
124
  * Maximum concurrent pipeline runs (running + pending). When a Job's
117
125
  * scheduler tick would push the total above this, additional items
@@ -207,6 +215,7 @@ const defaults: Settings = {
207
215
  skillsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-skills/main',
208
216
  connectorsRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-connectors/main',
209
217
  workflowRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-workflow/main',
218
+ publicInfoRepoUrl: 'https://raw.githubusercontent.com/aiwatching/forge-public-info/main',
210
219
  maxConcurrentPipelines: 5,
211
220
  displayName: 'Forge',
212
221
  displayEmail: '',
package/next-env.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.12",
3
+ "version": "0.10.18",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -1,33 +0,0 @@
1
- /**
2
- * Smoke test: fortinet-mr-review-batch v0.3.0 parses end-to-end through
3
- * parseWorkflow with the new for_each.before extension.
4
- */
5
- import { parseWorkflow } from '../pipeline';
6
- import { readFileSync } from 'fs';
7
-
8
- const txt = readFileSync('/Users/zliu/.forge/data/flows/fortinet-mr-review-batch.yaml', 'utf8');
9
- const wf = parseWorkflow(txt);
10
-
11
- let ok = true;
12
- function eq(actual: any, expected: any, label: string) {
13
- if (JSON.stringify(actual) === JSON.stringify(expected)) {
14
- console.log(` ✓ ${label}`);
15
- } else {
16
- console.log(` ✗ ${label}: got ${JSON.stringify(actual)}, want ${JSON.stringify(expected)}`);
17
- ok = false;
18
- }
19
- }
20
-
21
- console.log('fortinet-mr-review-batch v0.3.0 — parseWorkflow integration');
22
- eq(wf.name, 'fortinet-mr-review-batch', 'workflow name');
23
- eq(wf.for_each?.source, '{{nodes.list-iids.outputs.iids}}', 'for_each.source');
24
- eq(wf.for_each?.as, 'mr_iid', 'for_each.as');
25
- eq(wf.for_each?.on_failure, 'continue', 'for_each.on_failure');
26
- eq(wf.for_each?.before, ['list-iids'], 'for_each.before');
27
- eq(Object.keys(wf.nodes), ['list-iids', 'ingest', 'triage', 'fix', 'reply', 'cleanup'], 'all 6 nodes parsed');
28
- eq(wf.nodes['list-iids']?.mode, 'shell', 'list-iids mode = shell');
29
- eq(wf.nodes['list-iids']?.worktree, false, 'list-iids worktree = false');
30
- eq(wf.nodes['list-iids']?.outputs?.[0]?.name, 'iids', 'list-iids outputs.iids name');
31
- eq(wf.nodes['list-iids']?.outputs?.[0]?.extract, 'stdout', 'list-iids outputs.iids extract');
32
-
33
- process.exit(ok ? 0 : 1);
@@ -1,201 +0,0 @@
1
- /**
2
- * for_each.before: tests — covers:
3
- * - parseWorkflow validates `before:` field shape + node existence
4
- * - parseForEach passes through `before:` correctly
5
- * - resolveForEachSource accepts node outputs (`{{nodes.X.outputs.Y}}`)
6
- *
7
- * Setup-phase orchestration (startPipeline scheduling, checkPipelineCompletion
8
- * transition) needs running tasks + file system + scheduler — not unit-testable
9
- * here. Verified end-to-end in fortinet-mr-review-batch v0.3.0.
10
- *
11
- * Run: npx tsx lib/__tests__/foreach-before.test.ts
12
- */
13
-
14
- import { parseWorkflow, resolveForEachSource } from '../pipeline';
15
-
16
- let passed = 0, failed = 0;
17
- function check(name: string, fn: () => void) {
18
- try { fn(); console.log(` ✓ ${name}`); passed++; }
19
- catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
20
- }
21
- function expectThrow(fn: () => void, msgIncludes: string) {
22
- try { fn(); throw new Error(`expected throw containing "${msgIncludes}"`); }
23
- catch (e) {
24
- const m = (e as Error).message;
25
- if (!m.includes(msgIncludes)) throw new Error(`got "${m}", expected to include "${msgIncludes}"`);
26
- }
27
- }
28
- function eqArr(a: unknown[], b: unknown[], label = '') {
29
- if (a.length !== b.length || a.some((v, i) => v !== b[i])) {
30
- throw new Error(`${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
31
- }
32
- }
33
-
34
- console.log('for_each.before parsing + node-output source resolution tests');
35
-
36
- // ── parseForEach / parseWorkflow: shape + validation ──
37
-
38
- check('parseWorkflow accepts valid before: [<node>]', () => {
39
- const wf = parseWorkflow(`
40
- name: test
41
- for_each:
42
- source: "{{nodes.list-ids.outputs.ids}}"
43
- as: id
44
- before: [list-ids]
45
- nodes:
46
- list-ids:
47
- project: x
48
- prompt: "echo 1,2,3"
49
- body:
50
- project: x
51
- prompt: "consume {{id}}"
52
- depends_on: [list-ids]
53
- `);
54
- if (!wf.for_each) throw new Error('for_each missing');
55
- if (!wf.for_each.before) throw new Error('before missing');
56
- eqArr(wf.for_each.before, ['list-ids']);
57
- });
58
-
59
- check('parseWorkflow accepts multi-node before: [a, b]', () => {
60
- const wf = parseWorkflow(`
61
- name: test
62
- for_each:
63
- source: "{{nodes.b.outputs.list}}"
64
- as: x
65
- before: [a, b]
66
- nodes:
67
- a: { project: x, prompt: "1" }
68
- b: { project: x, prompt: "2", depends_on: [a] }
69
- body: { project: x, prompt: "{{x}}" }
70
- `);
71
- eqArr(wf.for_each!.before!, ['a', 'b']);
72
- });
73
-
74
- check('parseWorkflow with NO before: → before undefined (back-compat)', () => {
75
- const wf = parseWorkflow(`
76
- name: test
77
- for_each:
78
- source: "{{input.ids}}"
79
- as: id
80
- nodes:
81
- body: { project: x, prompt: "{{id}}" }
82
- `);
83
- if (wf.for_each!.before !== undefined) throw new Error(`expected undefined, got ${JSON.stringify(wf.for_each!.before)}`);
84
- });
85
-
86
- check('parseWorkflow throws on before referencing unknown node', () => {
87
- expectThrow(() => parseWorkflow(`
88
- name: test
89
- for_each:
90
- source: "{{nodes.missing.outputs.x}}"
91
- as: id
92
- before: [missing]
93
- nodes:
94
- body: { project: x, prompt: "1" }
95
- `), "references unknown node id 'missing'");
96
- });
97
-
98
- check('parseWorkflow throws on before: not-an-array', () => {
99
- expectThrow(() => parseWorkflow(`
100
- name: test
101
- for_each:
102
- source: "{{nodes.list-ids.outputs.x}}"
103
- as: id
104
- before: "list-ids"
105
- nodes:
106
- list-ids: { project: x, prompt: "1" }
107
- body: { project: x, prompt: "{{id}}" }
108
- `), 'must be an array of node id strings');
109
- });
110
-
111
- check('parseWorkflow throws on before: [empty-string]', () => {
112
- expectThrow(() => parseWorkflow(`
113
- name: test
114
- for_each:
115
- source: "{{nodes.x.outputs.x}}"
116
- as: id
117
- before: [""]
118
- nodes:
119
- x: { project: x, prompt: "1" }
120
- body: { project: x, prompt: "{{id}}" }
121
- `), 'must be an array of node id strings');
122
- });
123
-
124
- check('parseWorkflow throws on before: [non-string]', () => {
125
- expectThrow(() => parseWorkflow(`
126
- name: test
127
- for_each:
128
- source: "{{nodes.x.outputs.x}}"
129
- as: id
130
- before: [123]
131
- nodes:
132
- x: { project: x, prompt: "1" }
133
- body: { project: x, prompt: "{{id}}" }
134
- `), 'must be an array of node id strings');
135
- });
136
-
137
- // ── resolveForEachSource: ctx now includes nodes ──
138
-
139
- check('resolveForEachSource reads {{nodes.X.outputs.Y}} from pipeline.nodes', () => {
140
- const nodes = {
141
- 'list-ids': { status: 'done' as const, outputs: { ids: '7,8,9' }, iterations: 0 },
142
- };
143
- const items = resolveForEachSource(
144
- { source: '{{nodes.list-ids.outputs.ids}}' },
145
- {},
146
- {},
147
- nodes,
148
- );
149
- eqArr(items, ['7', '8', '9']);
150
- });
151
-
152
- check('resolveForEachSource trims whitespace from node-output split items', () => {
153
- const nodes = {
154
- 'list-ids': { status: 'done' as const, outputs: { ids: ' 1, 2 ,3 ' }, iterations: 0 },
155
- };
156
- const items = resolveForEachSource(
157
- { source: '{{nodes.list-ids.outputs.ids}}' },
158
- {},
159
- {},
160
- nodes,
161
- );
162
- eqArr(items, ['1', '2', '3']);
163
- });
164
-
165
- check('resolveForEachSource empty node output → empty array', () => {
166
- const nodes = {
167
- 'list-ids': { status: 'done' as const, outputs: { ids: '' }, iterations: 0 },
168
- };
169
- const items = resolveForEachSource(
170
- { source: '{{nodes.list-ids.outputs.ids}}' },
171
- {},
172
- {},
173
- nodes,
174
- );
175
- eqArr(items, []);
176
- });
177
-
178
- check('resolveForEachSource with custom split + node output', () => {
179
- const nodes = {
180
- 'list-ids': { status: 'done' as const, outputs: { ids: 'a|b|c' }, iterations: 0 },
181
- };
182
- const items = resolveForEachSource(
183
- { source: '{{nodes.list-ids.outputs.ids}}', split: '|' },
184
- {},
185
- {},
186
- nodes,
187
- );
188
- eqArr(items, ['a', 'b', 'c']);
189
- });
190
-
191
- check('resolveForEachSource without nodes param (back-compat) still works', () => {
192
- const items = resolveForEachSource(
193
- { source: '{{input.ids}}' },
194
- { ids: '1,2' },
195
- {},
196
- );
197
- eqArr(items, ['1', '2']);
198
- });
199
-
200
- console.log(`\n${passed} passed, ${failed} failed`);
201
- process.exit(failed === 0 ? 0 : 1);
@@ -1,114 +0,0 @@
1
- /**
2
- * Step-2 verification: parseWorkflow handles `for_each:` correctly.
3
- *
4
- * No test framework — just a tsx script that throws on first failure.
5
- * Run: npx tsx lib/__tests__/foreach-parse.test.ts
6
- */
7
-
8
- import { parseWorkflow } from '../pipeline';
9
-
10
- let passed = 0;
11
- let failed = 0;
12
-
13
- function check(name: string, fn: () => void) {
14
- try { fn(); console.log(` ✓ ${name}`); passed++; }
15
- catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
16
- }
17
-
18
- function expectThrow(yaml: string, expectedSubstring: string) {
19
- try {
20
- parseWorkflow(yaml);
21
- throw new Error(`expected throw containing "${expectedSubstring}", got none`);
22
- } catch (e) {
23
- const msg = (e as Error).message;
24
- if (!msg.includes(expectedSubstring)) {
25
- throw new Error(`expected throw containing "${expectedSubstring}", got: ${msg}`);
26
- }
27
- }
28
- }
29
-
30
- console.log('parseWorkflow + for_each: parser tests');
31
-
32
- check('no for_each → workflow.for_each is undefined', () => {
33
- const w = parseWorkflow('name: foo\nnodes:\n a: { project: x, prompt: "echo hi" }\n');
34
- if (w.for_each !== undefined) throw new Error(`expected undefined, got ${JSON.stringify(w.for_each)}`);
35
- });
36
-
37
- check('valid for_each with string source', () => {
38
- const w = parseWorkflow(`
39
- name: foo
40
- for_each:
41
- source: "{{input.ids}}"
42
- as: bug_id
43
- nodes:
44
- a: { project: x, prompt: "echo {{bug_id}}" }
45
- `);
46
- if (!w.for_each) throw new Error('for_each missing');
47
- if (w.for_each.source !== '{{input.ids}}') throw new Error('source wrong');
48
- if (w.for_each.as !== 'bug_id') throw new Error('as wrong');
49
- if (w.for_each.on_failure !== 'continue') throw new Error(`on_failure default should be continue, got ${w.for_each.on_failure}`);
50
- });
51
-
52
- check('default as = "item"', () => {
53
- const w = parseWorkflow('name: foo\nfor_each:\n source: "{{input.x}}"\nnodes:\n a: { project: x, prompt: "" }\n');
54
- if (w.for_each!.as !== 'item') throw new Error('default as should be item');
55
- });
56
-
57
- check('on_failure: stop accepted', () => {
58
- const w = parseWorkflow('name: foo\nfor_each:\n source: x\n on_failure: stop\nnodes:\n a: { project: x, prompt: "" }\n');
59
- if (w.for_each!.on_failure !== 'stop') throw new Error('on_failure not preserved');
60
- });
61
-
62
- check('array source literal', () => {
63
- const w = parseWorkflow('name: foo\nfor_each:\n source: [1, 2, 3]\nnodes:\n a: { project: x, prompt: "" }\n');
64
- if (!Array.isArray(w.for_each!.source)) throw new Error('array literal not preserved');
65
- if ((w.for_each!.source as any[]).length !== 3) throw new Error('array length wrong');
66
- });
67
-
68
- check('split: ";" accepted', () => {
69
- const w = parseWorkflow('name: foo\nfor_each:\n source: x\n split: ";"\nnodes:\n a: { project: x, prompt: "" }\n');
70
- if (w.for_each!.split !== ';') throw new Error('split not preserved');
71
- });
72
-
73
- console.log('\nError-path tests:');
74
-
75
- check('throws on missing source', () => {
76
- expectThrow('name: foo\nfor_each:\n as: bug\nnodes:\n a: { project: x, prompt: "" }\n', 'source is required');
77
- });
78
-
79
- check('throws on invalid as identifier', () => {
80
- expectThrow('name: foo\nfor_each:\n source: x\n as: "bad-name"\nnodes:\n a: { project: x, prompt: "" }\n', 'must be a valid identifier');
81
- });
82
-
83
- check('throws on reserved as: input', () => {
84
- expectThrow('name: foo\nfor_each:\n source: x\n as: input\nnodes:\n a: { project: x, prompt: "" }\n', 'reserved template namespace');
85
- });
86
-
87
- check('throws on reserved as: run', () => {
88
- expectThrow('name: foo\nfor_each:\n source: x\n as: run\nnodes:\n a: { project: x, prompt: "" }\n', 'reserved template namespace');
89
- });
90
-
91
- check('throws on bad on_failure value', () => {
92
- expectThrow('name: foo\nfor_each:\n source: x\n on_failure: maybe\nnodes:\n a: { project: x, prompt: "" }\n', "must be 'continue' or 'stop'");
93
- });
94
-
95
- check('throws on for_each on conversation type', () => {
96
- expectThrow(`
97
- name: foo
98
- type: conversation
99
- for_each:
100
- source: x
101
- agents:
102
- - id: a
103
- agent: claude
104
- initial_prompt: hi
105
- nodes: {}
106
- `, "only supported on type='dag'");
107
- });
108
-
109
- check('throws on for_each scalar (not object)', () => {
110
- expectThrow('name: foo\nfor_each: "{{input.ids}}"\nnodes:\n a: { project: x, prompt: "" }\n', 'must be an object');
111
- });
112
-
113
- console.log(`\n${passed} passed, ${failed} failed`);
114
- process.exit(failed === 0 ? 0 : 1);
@@ -1,112 +0,0 @@
1
- /**
2
- * Step-5 verification: snapshotIteration captures node states + correctly
3
- * advances the iterations[] history. (Full checkPipelineCompletion has
4
- * filesystem + scheduling side effects — exercised end-to-end in Step 7.)
5
- *
6
- * Run: npx tsx lib/__tests__/foreach-snapshot.test.ts
7
- */
8
-
9
- import { snapshotIteration } from '../pipeline';
10
-
11
- let passed = 0, failed = 0;
12
- function check(name: string, fn: () => void) {
13
- try { fn(); console.log(` ✓ ${name}`); passed++; }
14
- catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
15
- }
16
-
17
- function makePipeline(items: unknown[], currentIndex = 0): any {
18
- return {
19
- id: 'test123',
20
- workflowName: 'test',
21
- status: 'running',
22
- input: {},
23
- vars: {},
24
- nodes: {
25
- a: { status: 'done', outputs: { x: '1' } },
26
- b: { status: 'done', outputs: { y: '2' }, error: undefined },
27
- },
28
- nodeOrder: ['a', 'b'],
29
- createdAt: '2026-01-01T00:00:00.000Z',
30
- forEach: {
31
- items,
32
- currentIndex,
33
- total: items.length,
34
- asName: 'item',
35
- onFailure: 'continue',
36
- iterations: [],
37
- },
38
- };
39
- }
40
-
41
- console.log('snapshotIteration tests');
42
-
43
- check('snapshot captures all-done nodes with status="done"', () => {
44
- const p = makePipeline([1, 2, 3]);
45
- snapshotIteration(p, false);
46
- if (p.forEach.iterations.length !== 1) throw new Error('iterations length wrong');
47
- const iter = p.forEach.iterations[0];
48
- if (iter.status !== 'done') throw new Error(`status wrong: ${iter.status}`);
49
- if (iter.index !== 0) throw new Error('index wrong');
50
- if (iter.nodes.a.status !== 'done') throw new Error('node a status wrong');
51
- if (iter.nodes.a.outputs.x !== '1') throw new Error('node a output not preserved');
52
- });
53
-
54
- check('snapshot captures failed iter with status="failed"', () => {
55
- const p = makePipeline([1, 2]);
56
- p.nodes.b.status = 'failed';
57
- p.nodes.b.error = 'boom';
58
- snapshotIteration(p, true);
59
- const iter = p.forEach.iterations[0];
60
- if (iter.status !== 'failed') throw new Error(`status wrong: ${iter.status}`);
61
- if (iter.nodes.b.error !== 'boom') throw new Error('error not preserved');
62
- });
63
-
64
- check('startedAt = createdAt for first iter', () => {
65
- const p = makePipeline([1, 2]);
66
- snapshotIteration(p, false);
67
- if (p.forEach.iterations[0].startedAt !== p.createdAt) throw new Error('first startedAt should = createdAt');
68
- });
69
-
70
- check('startedAt = previous completedAt for subsequent iters', () => {
71
- const p = makePipeline([1, 2]);
72
- snapshotIteration(p, false);
73
- const firstCompleted = p.forEach.iterations[0].completedAt;
74
- p.forEach.currentIndex = 1;
75
- snapshotIteration(p, false);
76
- if (p.forEach.iterations[1].startedAt !== firstCompleted) {
77
- throw new Error('second iter startedAt should chain from prev completedAt');
78
- }
79
- });
80
-
81
- check('outputs are deep-copied (mutation safety)', () => {
82
- const p = makePipeline([1]);
83
- snapshotIteration(p, false);
84
- const iter = p.forEach.iterations[0];
85
- // Mutate live node — snapshot should NOT change
86
- p.nodes.a.outputs.x = 'MUTATED';
87
- if (iter.nodes.a.outputs.x !== '1') throw new Error('snapshot was mutated; need defensive copy');
88
- });
89
-
90
- check('multiple iterations accumulate', () => {
91
- const p = makePipeline([1, 2, 3]);
92
- snapshotIteration(p, false);
93
- p.forEach.currentIndex = 1;
94
- snapshotIteration(p, false);
95
- p.forEach.currentIndex = 2;
96
- snapshotIteration(p, true);
97
- if (p.forEach.iterations.length !== 3) throw new Error('expected 3 iters');
98
- if (p.forEach.iterations[2].status !== 'failed') throw new Error('iter 3 should be failed');
99
- if (p.forEach.iterations[0].index !== 0) throw new Error('iter 0 index wrong');
100
- if (p.forEach.iterations[1].index !== 1) throw new Error('iter 1 index wrong');
101
- if (p.forEach.iterations[2].index !== 2) throw new Error('iter 2 index wrong');
102
- });
103
-
104
- check('no-op when pipeline.forEach undefined', () => {
105
- const p: any = { id: 'x', nodes: {}, createdAt: '2026-01-01' };
106
- snapshotIteration(p, false);
107
- // Should not throw, should not add anything
108
- if (p.forEach !== undefined) throw new Error('should leave forEach undefined');
109
- });
110
-
111
- console.log(`\n${passed} passed, ${failed} failed`);
112
- process.exit(failed === 0 ? 0 : 1);
@@ -1,105 +0,0 @@
1
- /**
2
- * Step-4 verification: resolveForEachSource resolves + splits items.
3
- * Run: npx tsx lib/__tests__/foreach-source.test.ts
4
- */
5
-
6
- import { resolveForEachSource } from '../pipeline';
7
-
8
- let passed = 0, failed = 0;
9
- function check(name: string, fn: () => void) {
10
- try { fn(); console.log(` ✓ ${name}`); passed++; }
11
- catch (e) { console.log(` ✗ ${name}\n ${(e as Error).message}`); failed++; }
12
- }
13
- function eqArr(a: unknown[], b: unknown[], label = '') {
14
- if (a.length !== b.length || a.some((v, i) => v !== b[i])) {
15
- throw new Error(`${label}: expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`);
16
- }
17
- }
18
-
19
- console.log('resolveForEachSource tests');
20
-
21
- check('string source via {{input.x}} template, default split=","', () => {
22
- const items = resolveForEachSource(
23
- { source: '{{input.ids}}' },
24
- { ids: '111,222,333' },
25
- {},
26
- );
27
- eqArr(items, ['111', '222', '333']);
28
- });
29
-
30
- check('string source with whitespace around commas — trimmed', () => {
31
- const items = resolveForEachSource(
32
- { source: '{{input.ids}}' },
33
- { ids: ' 111 ,222 , 333 ' },
34
- {},
35
- );
36
- eqArr(items, ['111', '222', '333']);
37
- });
38
-
39
- check('string source with empty entries dropped', () => {
40
- const items = resolveForEachSource(
41
- { source: '{{input.ids}}' },
42
- { ids: '111,,222, ,333,' },
43
- {},
44
- );
45
- eqArr(items, ['111', '222', '333']);
46
- });
47
-
48
- check('empty input.ids → empty array', () => {
49
- const items = resolveForEachSource({ source: '{{input.ids}}' }, { ids: '' }, {});
50
- eqArr(items, []);
51
- });
52
-
53
- check('input missing → empty string template → empty array', () => {
54
- const items = resolveForEachSource({ source: '{{input.missing}}' }, {}, {});
55
- eqArr(items, []);
56
- });
57
-
58
- check('custom split: ";"', () => {
59
- const items = resolveForEachSource(
60
- { source: '{{input.ids}}', split: ';' },
61
- { ids: '111;222;333' },
62
- {},
63
- );
64
- eqArr(items, ['111', '222', '333']);
65
- });
66
-
67
- check('array literal source — used as-is', () => {
68
- const items = resolveForEachSource(
69
- { source: ['a', 'b', 'c'] },
70
- {},
71
- {},
72
- );
73
- eqArr(items, ['a', 'b', 'c']);
74
- });
75
-
76
- check('array literal with objects — preserved', () => {
77
- const items = resolveForEachSource(
78
- { source: [{ iid: 1 }, { iid: 2 }] },
79
- {},
80
- {},
81
- );
82
- if (items.length !== 2) throw new Error('length wrong');
83
- if ((items[0] as any).iid !== 1) throw new Error('first item wrong');
84
- });
85
-
86
- check('vars source resolves', () => {
87
- const items = resolveForEachSource(
88
- { source: '{{vars.list}}' },
89
- {},
90
- { list: 'a,b,c' },
91
- );
92
- eqArr(items, ['a', 'b', 'c']);
93
- });
94
-
95
- check('literal string with no template, just split', () => {
96
- const items = resolveForEachSource(
97
- { source: 'foo,bar' },
98
- {},
99
- {},
100
- );
101
- eqArr(items, ['foo', 'bar']);
102
- });
103
-
104
- console.log(`\n${passed} passed, ${failed} failed`);
105
- process.exit(failed === 0 ? 0 : 1);