@output.ai/core 0.2.2 → 0.3.0-dev.pr263-a59dd0e

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.
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
2
+ import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  import validatorLoader from './index.mjs';
@@ -17,16 +17,14 @@ function runLoader( filename, source ) {
17
17
  }
18
18
 
19
19
  describe( 'workflow_validator loader', () => {
20
- it( 'workflow.js: allows imports from steps/shared_steps/evaluators/workflow', async () => {
20
+ it( 'workflow.js: allows imports from steps/evaluators/workflow', async () => {
21
21
  const dir = mkdtempSync( join( tmpdir(), 'wf-allow-' ) );
22
22
  writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
23
- writeFileSync( join( dir, 'shared_steps.js' ), 'export const SS = step({ name: "ss" })\n' );
24
23
  writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
25
24
  writeFileSync( join( dir, 'workflow.js' ), 'export const W = workflow({ name: "w" })\n' );
26
25
 
27
26
  const src = [
28
27
  'import { S } from "./steps.js";',
29
- 'import { SS } from "./shared_steps.js";',
30
28
  'import { E } from "./evaluators.js";',
31
29
  'import { W } from "./workflow.js";',
32
30
  'const x = 1;'
@@ -36,10 +34,10 @@ describe( 'workflow_validator loader', () => {
36
34
  rmSync( dir, { recursive: true, force: true } );
37
35
  } );
38
36
 
39
- it( 'workflow.js: rejects external dependencies', async () => {
40
- const dir = mkdtempSync( join( tmpdir(), 'wf-reject-' ) );
37
+ it( 'workflow.js: allows imports from any non-component file', async () => {
38
+ const dir = mkdtempSync( join( tmpdir(), 'wf-any-allow-' ) );
41
39
  const src = 'import x from "./foo.js";';
42
- await expect( runLoader( join( dir, 'workflow.js' ), src ) ).rejects.toThrow( /Invalid (import|dependency) in workflow\.js/ );
40
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
43
41
  rmSync( dir, { recursive: true, force: true } );
44
42
  } );
45
43
 
@@ -54,7 +52,7 @@ describe( 'workflow_validator loader', () => {
54
52
  rmSync( dir, { recursive: true, force: true } );
55
53
  } );
56
54
 
57
- it( 'steps.js: rejects importing steps/shared_steps/evaluators/workflow', async () => {
55
+ it( 'steps.js: rejects importing steps/evaluators/workflow', async () => {
58
56
  const dir = mkdtempSync( join( tmpdir(), 'steps-reject-' ) );
59
57
  const src = 'import { S } from "./steps.js";';
60
58
  await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
@@ -68,44 +66,34 @@ describe( 'workflow_validator loader', () => {
68
66
  rmSync( dir, { recursive: true, force: true } );
69
67
  } );
70
68
 
71
- it( 'evaluators.js: rejects importing evaluators/steps/shared_steps/workflow', async () => {
69
+ it( 'evaluators.js: rejects importing evaluators/steps/workflow', async () => {
72
70
  const dir = mkdtempSync( join( tmpdir(), 'evals-reject-' ) );
73
71
  const src = 'import { E } from "./evaluators.js";';
74
72
  await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
75
73
  rmSync( dir, { recursive: true, force: true } );
76
74
  } );
77
75
 
78
- it( 'steps.js: rejects calling another step/evaluator/workflow inside fn', async () => {
76
+ it( 'steps.js: rejects calling another step inside fn', async () => {
79
77
  const dir = mkdtempSync( join( tmpdir(), 'steps-call-reject-' ) );
78
+ // Can only test same-type components since cross-type declarations are now blocked by instantiation validation
80
79
  const src = [
81
80
  'const A = step({ name: "a" });',
82
81
  'const B = step({ name: "b" });',
83
82
  'const obj = { fn: function() { B(); } };'
84
83
  ].join( '\n' );
85
84
  await expect( runLoader( join( dir, 'steps.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
86
-
87
- const src2 = [
88
- 'const E = evaluator({ name: "e" });',
89
- 'const obj = { fn: () => { E(); } };'
90
- ].join( '\n' );
91
- await expect( runLoader( join( dir, 'steps.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
92
85
  rmSync( dir, { recursive: true, force: true } );
93
86
  } );
94
87
 
95
- it( 'evaluators.js: rejects calling another evaluator/step/workflow inside fn', async () => {
88
+ it( 'evaluators.js: rejects calling another evaluator inside fn', async () => {
96
89
  const dir = mkdtempSync( join( tmpdir(), 'evals-call-reject-' ) );
90
+ // Can only test same-type components since cross-type declarations are now blocked by instantiation validation
97
91
  const src = [
98
92
  'const E1 = evaluator({ name: "e1" });',
99
93
  'const E2 = evaluator({ name: "e2" });',
100
94
  'const obj = { fn: function() { E2(); } };'
101
95
  ].join( '\n' );
102
96
  await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
103
-
104
- const src2 = [
105
- 'const S = step({ name: "s" });',
106
- 'const obj = { fn: () => { S(); } };'
107
- ].join( '\n' );
108
- await expect( runLoader( join( dir, 'evaluators.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
109
97
  rmSync( dir, { recursive: true, force: true } );
110
98
  } );
111
99
 
@@ -125,28 +113,25 @@ describe( 'workflow_validator loader', () => {
125
113
  rmSync( dir, { recursive: true, force: true } );
126
114
  } );
127
115
 
128
- it( 'workflow.js: allows require from steps/shared_steps/evaluators/workflow; rejects others', async () => {
116
+ it( 'workflow.js: allows require from steps/evaluators/workflow; allows other require', async () => {
129
117
  const dir = mkdtempSync( join( tmpdir(), 'wf-req-' ) );
130
118
  writeFileSync( join( dir, 'steps.js' ), 'export const S = step({ name: "s" })\n' );
131
- writeFileSync( join( dir, 'shared_steps.js' ), 'export const SS = step({ name: "ss" })\n' );
132
119
  writeFileSync( join( dir, 'evaluators.js' ), 'export const E = evaluator({ name: "e" })\n' );
133
120
  writeFileSync( join( dir, 'workflow.js' ), 'export default workflow({ name: "w" })\n' );
134
121
  const ok = [
135
122
  'const { S } = require("./steps.js");',
136
- 'const { SS } = require("./shared_steps.js");',
137
123
  'const { E } = require("./evaluators.js");',
138
124
  'const W = require("./workflow.js");'
139
125
  ].join( '\n' );
140
126
  await expect( runLoader( join( dir, 'workflow.js' ), ok ) ).resolves.toBeTruthy();
141
- const bad = 'const X = require("./util.js");';
142
- await expect( runLoader( join( dir, 'workflow.js' ), bad ) ).rejects.toThrow( /Invalid (require|dependency) in workflow\.js/ );
127
+ // Also allow random files (not rejected anymore)
128
+ const ok2 = 'const X = require("./random_file.js");';
129
+ await expect( runLoader( join( dir, 'workflow.js' ), ok2 ) ).resolves.toBeTruthy();
143
130
  rmSync( dir, { recursive: true, force: true } );
144
131
  } );
145
132
 
146
- it( 'steps.js: rejects importing shared_steps/evaluators/workflow variants', async () => {
133
+ it( 'steps.js: rejects importing evaluators/workflow variants', async () => {
147
134
  const dir = mkdtempSync( join( tmpdir(), 'steps-reject2-' ) );
148
- await expect( runLoader( join( dir, 'steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
149
- .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
150
135
  await expect( runLoader( join( dir, 'steps.js' ), 'import { E } from "./evaluators.js";' ) )
151
136
  .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
152
137
  await expect( runLoader( join( dir, 'steps.js' ), 'import WF from "./workflow.js";' ) )
@@ -154,12 +139,10 @@ describe( 'workflow_validator loader', () => {
154
139
  rmSync( dir, { recursive: true, force: true } );
155
140
  } );
156
141
 
157
- it( 'evaluators.js: rejects importing steps/shared_steps/workflow variants', async () => {
142
+ it( 'evaluators.js: rejects importing steps/workflow variants', async () => {
158
143
  const dir = mkdtempSync( join( tmpdir(), 'evals-reject2-' ) );
159
144
  await expect( runLoader( join( dir, 'evaluators.js' ), 'import { S } from "./steps.js";' ) )
160
145
  .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
161
- await expect( runLoader( join( dir, 'evaluators.js' ), 'import { SS } from "./shared_steps.js";' ) )
162
- .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
163
146
  await expect( runLoader( join( dir, 'evaluators.js' ), 'import WF from "./workflow.js";' ) )
164
147
  .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
165
148
  rmSync( dir, { recursive: true, force: true } );
@@ -174,42 +157,6 @@ describe( 'workflow_validator loader', () => {
174
157
  rmSync( dir, { recursive: true, force: true } );
175
158
  } );
176
159
 
177
- it( 'shared_steps.js: rejects importing steps/shared_steps/evaluators/workflow', async () => {
178
- const dir = mkdtempSync( join( tmpdir(), 'shared-imp-reject-' ) );
179
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { S } from "./steps.js";' ) )
180
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
181
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { SS } from "./shared_steps.js";' ) )
182
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
183
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import { E } from "./evaluators.js";' ) )
184
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
185
- await expect( runLoader( join( dir, 'shared_steps.js' ), 'import WF from "./workflow.js";' ) )
186
- .rejects.toThrow( /Invalid (import|imports|dependency) in shared_steps\.js/ );
187
- rmSync( dir, { recursive: true, force: true } );
188
- } );
189
-
190
- it( 'shared_steps.js: rejects calling step/evaluator/workflow inside fn', async () => {
191
- const dir = mkdtempSync( join( tmpdir(), 'shared-call-reject-' ) );
192
- const src1 = [
193
- 'const A = step({ name: "a" });',
194
- 'const obj = { fn: function() { A(); } };'
195
- ].join( '\n' );
196
- await expect( runLoader( join( dir, 'shared_steps.js' ), src1 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
197
-
198
- const src2 = [
199
- 'const E = evaluator({ name: "e" });',
200
- 'const obj = { fn: () => { E(); } };'
201
- ].join( '\n' );
202
- await expect( runLoader( join( dir, 'shared_steps.js' ), src2 ) ).rejects.toThrow( /Invalid call in .*\.js fn/ );
203
- rmSync( dir, { recursive: true, force: true } );
204
- } );
205
-
206
- it( 'shared_steps.js: top-level calls outside fn are allowed', async () => {
207
- const dir = mkdtempSync( join( tmpdir(), 'shared-toplevel-allowed-' ) );
208
- const src = [ 'const A = step({ name: "a" });', 'A();' ].join( '\n' );
209
- await expect( runLoader( join( dir, 'shared_steps.js' ), src ) ).resolves.toBeTruthy();
210
- rmSync( dir, { recursive: true, force: true } );
211
- } );
212
-
213
160
  it( 'workflow.js: allows importing ./types.js and bare types', async () => {
214
161
  const dir = mkdtempSync( join( tmpdir(), 'wf-types-allow-' ) );
215
162
  writeFileSync( join( dir, 'types.js' ), 'export const T = {}\n' );
@@ -218,9 +165,9 @@ describe( 'workflow_validator loader', () => {
218
165
  rmSync( dir, { recursive: true, force: true } );
219
166
  } );
220
167
 
221
- it( 'workflow.js: allows importing extraneous files (consts/constants/vars/variables)', async () => {
168
+ it( 'workflow.js: allows importing any file (consts/constants/vars/variables/random)', async () => {
222
169
  const dir = mkdtempSync( join( tmpdir(), 'wf-extra-allow-' ) );
223
- const bases = [ 'consts', 'constants', 'vars', 'variables' ];
170
+ const bases = [ 'consts', 'constants', 'vars', 'variables', 'random', 'anything' ];
224
171
  for ( const base of bases ) {
225
172
  writeFileSync( join( dir, `${base}.js` ), 'export const X = 1\n' );
226
173
  const src = `import x from "./${base}.js";`;
@@ -229,12 +176,10 @@ describe( 'workflow_validator loader', () => {
229
176
  rmSync( dir, { recursive: true, force: true } );
230
177
  } );
231
178
 
232
- it( 'steps.js: rejects require of steps/shared_steps/evaluators/workflow; allows other require', async () => {
179
+ it( 'steps.js: rejects require of steps/evaluators/workflow; allows other require', async () => {
233
180
  const dir = mkdtempSync( join( tmpdir(), 'steps-require-' ) );
234
181
  await expect( runLoader( join( dir, 'steps.js' ), 'const { S } = require("./steps.js");' ) )
235
182
  .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
236
- await expect( runLoader( join( dir, 'steps.js' ), 'const { SS } = require("./shared_steps.js");' ) )
237
- .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
238
183
  await expect( runLoader( join( dir, 'steps.js' ), 'const { E } = require("./evaluators.js");' ) )
239
184
  .rejects.toThrow( /Invalid (require|dependency) in steps\.js/ );
240
185
  await expect( runLoader( join( dir, 'steps.js' ), 'const W = require("./workflow.js");' ) )
@@ -244,16 +189,394 @@ describe( 'workflow_validator loader', () => {
244
189
  rmSync( dir, { recursive: true, force: true } );
245
190
  } );
246
191
 
247
- it( 'evaluators.js: rejects require of steps/shared_steps/workflow; allows other require', async () => {
192
+ it( 'evaluators.js: rejects require of steps/workflow; allows other require', async () => {
248
193
  const dir = mkdtempSync( join( tmpdir(), 'evals-require-' ) );
249
194
  await expect( runLoader( join( dir, 'evaluators.js' ), 'const { S } = require("./steps.js");' ) )
250
195
  .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
251
- await expect( runLoader( join( dir, 'evaluators.js' ), 'const { SS } = require("./shared_steps.js");' ) )
252
- .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
253
196
  await expect( runLoader( join( dir, 'evaluators.js' ), 'const W = require("./workflow.js");' ) )
254
197
  .rejects.toThrow( /Invalid (require|dependency) in evaluators\.js/ );
255
198
  const ok = 'const util = require("./util.js");';
256
199
  await expect( runLoader( join( dir, 'evaluators.js' ), ok ) ).resolves.toBeTruthy();
257
200
  rmSync( dir, { recursive: true, force: true } );
258
201
  } );
202
+
203
+ // =====================================================
204
+ // Folder-based utilities and shared directory
205
+ // =====================================================
206
+
207
+ describe( 'folder-based utilities', () => {
208
+ it( 'workflow.js: allows imports from ./utils/helper.js (folder-based utils)', async () => {
209
+ const dir = mkdtempSync( join( tmpdir(), 'wf-folder-utils-' ) );
210
+ mkdirSync( join( dir, 'utils' ) );
211
+ writeFileSync( join( dir, 'utils', 'helper.js' ), 'export const helper = () => 1;\n' );
212
+ const src = 'import { helper } from "./utils/helper.js";';
213
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
214
+ rmSync( dir, { recursive: true, force: true } );
215
+ } );
216
+
217
+ it( 'workflow.js: allows imports from ./clients/redis.js (folder-based clients)', async () => {
218
+ const dir = mkdtempSync( join( tmpdir(), 'wf-folder-clients-' ) );
219
+ mkdirSync( join( dir, 'clients' ) );
220
+ writeFileSync( join( dir, 'clients', 'redis.js' ), 'export const client = {};\n' );
221
+ const src = 'import { client } from "./clients/redis.js";';
222
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
223
+ rmSync( dir, { recursive: true, force: true } );
224
+ } );
225
+
226
+ it( 'steps/fetch_data.js: allows imports from ../utils/http.js', async () => {
227
+ const dir = mkdtempSync( join( tmpdir(), 'steps-folder-utils-' ) );
228
+ mkdirSync( join( dir, 'steps' ) );
229
+ mkdirSync( join( dir, 'utils' ) );
230
+ writeFileSync( join( dir, 'utils', 'http.js' ), 'export const get = () => {};\n' );
231
+ const src = 'import { get } from "../utils/http.js";';
232
+ await expect( runLoader( join( dir, 'steps', 'fetch_data.js' ), src ) ).resolves.toBeTruthy();
233
+ rmSync( dir, { recursive: true, force: true } );
234
+ } );
235
+
236
+ it( 'evaluators/quality.js: allows imports from ../utils/metrics.js', async () => {
237
+ const dir = mkdtempSync( join( tmpdir(), 'evals-folder-utils-' ) );
238
+ mkdirSync( join( dir, 'evaluators' ) );
239
+ mkdirSync( join( dir, 'utils' ) );
240
+ writeFileSync( join( dir, 'utils', 'metrics.js' ), 'export const compute = () => {};\n' );
241
+ const src = 'import { compute } from "../utils/metrics.js";';
242
+ await expect( runLoader( join( dir, 'evaluators', 'quality.js' ), src ) ).resolves.toBeTruthy();
243
+ rmSync( dir, { recursive: true, force: true } );
244
+ } );
245
+ } );
246
+
247
+ describe( 'shared directory imports', () => {
248
+ it( 'workflow.js: allows imports from ../../shared/utils/keys.js', async () => {
249
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-utils-' ) );
250
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
251
+ mkdirSync( join( dir, 'shared', 'utils' ), { recursive: true } );
252
+ writeFileSync( join( dir, 'shared', 'utils', 'keys.js' ), 'export const KEY = "test";\n' );
253
+ const src = 'import { KEY } from "../../shared/utils/keys.js";';
254
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
255
+ rmSync( dir, { recursive: true, force: true } );
256
+ } );
257
+
258
+ it( 'workflow.js: allows imports from ../../shared/steps/common.js', async () => {
259
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-steps-' ) );
260
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
261
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
262
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
263
+ const src = 'import { commonStep } from "../../shared/steps/common.js";';
264
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
265
+ rmSync( dir, { recursive: true, force: true } );
266
+ } );
267
+
268
+ it( 'workflow.js: allows imports from ../../shared/evaluators/quality.js', async () => {
269
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-evals-' ) );
270
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
271
+ mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
272
+ writeFileSync( join( dir, 'shared', 'evaluators', 'quality.js' ), 'export const qualityEval = evaluator({ name: "quality" });\n' );
273
+ const src = 'import { qualityEval } from "../../shared/evaluators/quality.js";';
274
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
275
+ rmSync( dir, { recursive: true, force: true } );
276
+ } );
277
+
278
+ it( 'workflow.js: allows imports from ../../shared/clients/pokeapi.js', async () => {
279
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-clients-' ) );
280
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
281
+ mkdirSync( join( dir, 'shared', 'clients' ), { recursive: true } );
282
+ writeFileSync( join( dir, 'shared', 'clients', 'pokeapi.js' ), 'export const getPokemon = () => {};\n' );
283
+ const src = 'import { getPokemon } from "../../shared/clients/pokeapi.js";';
284
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
285
+ rmSync( dir, { recursive: true, force: true } );
286
+ } );
287
+
288
+ it( 'steps.ts: allows imports from ../../shared/utils/keys.js', async () => {
289
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-utils-' ) );
290
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
291
+ mkdirSync( join( dir, 'shared', 'utils' ), { recursive: true } );
292
+ writeFileSync( join( dir, 'shared', 'utils', 'keys.js' ), 'export const KEY = "test";\n' );
293
+ const src = 'import { KEY } from "../../shared/utils/keys.js";';
294
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) ).resolves.toBeTruthy();
295
+ rmSync( dir, { recursive: true, force: true } );
296
+ } );
297
+
298
+ it( 'steps.ts: allows imports from ../../shared/clients/redis.js', async () => {
299
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-clients-' ) );
300
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
301
+ mkdirSync( join( dir, 'shared', 'clients' ), { recursive: true } );
302
+ writeFileSync( join( dir, 'shared', 'clients', 'redis.js' ), 'export const client = {};\n' );
303
+ const src = 'import { client } from "../../shared/clients/redis.js";';
304
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) ).resolves.toBeTruthy();
305
+ rmSync( dir, { recursive: true, force: true } );
306
+ } );
307
+
308
+ it( 'evaluators.ts: allows imports from ../../shared/utils/helpers.js', async () => {
309
+ const dir = mkdtempSync( join( tmpdir(), 'evals-shared-utils-' ) );
310
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
311
+ mkdirSync( join( dir, 'shared', 'utils' ), { recursive: true } );
312
+ writeFileSync( join( dir, 'shared', 'utils', 'helpers.js' ), 'export const helper = () => 1;\n' );
313
+ const src = 'import { helper } from "../../shared/utils/helpers.js";';
314
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) ).resolves.toBeTruthy();
315
+ rmSync( dir, { recursive: true, force: true } );
316
+ } );
317
+ } );
318
+
319
+ describe( 'activity isolation - shared imports', () => {
320
+ it( 'steps.ts: rejects imports from ../../shared/steps/common.js (activity isolation)', async () => {
321
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-steps-reject-' ) );
322
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
323
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
324
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
325
+ const src = 'import { commonStep } from "../../shared/steps/common.js";';
326
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) )
327
+ .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
328
+ rmSync( dir, { recursive: true, force: true } );
329
+ } );
330
+
331
+ it( 'steps.ts: rejects imports from ../../shared/evaluators/quality.js (activity isolation)', async () => {
332
+ const dir = mkdtempSync( join( tmpdir(), 'steps-shared-evals-reject-' ) );
333
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
334
+ mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
335
+ writeFileSync( join( dir, 'shared', 'evaluators', 'quality.js' ), 'export const qualityEval = evaluator({ name: "quality" });\n' );
336
+ const src = 'import { qualityEval } from "../../shared/evaluators/quality.js";';
337
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'steps.js' ), src ) )
338
+ .rejects.toThrow( /Invalid (import|imports|dependency) in steps\.js/ );
339
+ rmSync( dir, { recursive: true, force: true } );
340
+ } );
341
+
342
+ it( 'evaluators.ts: rejects imports from ../../shared/steps/common.js (activity isolation)', async () => {
343
+ const dir = mkdtempSync( join( tmpdir(), 'evals-shared-steps-reject-' ) );
344
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
345
+ mkdirSync( join( dir, 'shared', 'steps' ), { recursive: true } );
346
+ writeFileSync( join( dir, 'shared', 'steps', 'common.js' ), 'export const commonStep = step({ name: "common" });\n' );
347
+ const src = 'import { commonStep } from "../../shared/steps/common.js";';
348
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) )
349
+ .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
350
+ rmSync( dir, { recursive: true, force: true } );
351
+ } );
352
+
353
+ it( 'evaluators.ts: rejects imports from ../../shared/evaluators/other.js (activity isolation)', async () => {
354
+ const dir = mkdtempSync( join( tmpdir(), 'evals-shared-evals-reject-' ) );
355
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
356
+ mkdirSync( join( dir, 'shared', 'evaluators' ), { recursive: true } );
357
+ writeFileSync( join( dir, 'shared', 'evaluators', 'other.js' ), 'export const otherEval = evaluator({ name: "other" });\n' );
358
+ const src = 'import { otherEval } from "../../shared/evaluators/other.js";';
359
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'evaluators.js' ), src ) )
360
+ .rejects.toThrow( /Invalid (import|imports|dependency) in evaluators\.js/ );
361
+ rmSync( dir, { recursive: true, force: true } );
362
+ } );
363
+ } );
364
+
365
+ describe( 'workflow import scope', () => {
366
+ it( 'workflow.js: allows imports from other workflow directories (cross-workflow)', async () => {
367
+ const dir = mkdtempSync( join( tmpdir(), 'wf-cross-allow-' ) );
368
+ mkdirSync( join( dir, 'workflows', 'workflow_a' ), { recursive: true } );
369
+ mkdirSync( join( dir, 'workflows', 'workflow_b' ), { recursive: true } );
370
+ writeFileSync( join( dir, 'workflows', 'workflow_b', 'steps.js' ), 'export const S = step({ name: "s" });\n' );
371
+ // Cross-workflow imports are allowed for workflows (they can import steps from other workflows)
372
+ const src = 'import { S } from "../workflow_b/steps.js";';
373
+ await expect( runLoader( join( dir, 'workflows', 'workflow_a', 'workflow.js' ), src ) ).resolves.toBeTruthy();
374
+ rmSync( dir, { recursive: true, force: true } );
375
+ } );
376
+
377
+ it( 'workflow.js: allows imports from utils with relative parent paths', async () => {
378
+ // The validator does not enforce strict scope boundaries, it validates by file type patterns
379
+ const dir = mkdtempSync( join( tmpdir(), 'wf-parent-utils-' ) );
380
+ mkdirSync( join( dir, 'src', 'workflows', 'my_workflow' ), { recursive: true } );
381
+ mkdirSync( join( dir, 'utils' ), { recursive: true } );
382
+ writeFileSync( join( dir, 'utils', 'helpers.js' ), 'export const helper = () => 1;\n' );
383
+ const src = 'import { helper } from "../../../utils/helpers.js";';
384
+ // This should pass because it's not a component file
385
+ await expect( runLoader( join( dir, 'src', 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
386
+ rmSync( dir, { recursive: true, force: true } );
387
+ } );
388
+ } );
389
+
390
+ // =====================================================
391
+ // Arbitrary path imports (proving any folder name works)
392
+ // =====================================================
393
+
394
+ describe( 'arbitrary path imports', () => {
395
+ it( 'workflow.js: allows imports from ./foobar.js', async () => {
396
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-1-' ) );
397
+ const src = 'import { foo } from "./foobar.js";';
398
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
399
+ rmSync( dir, { recursive: true, force: true } );
400
+ } );
401
+
402
+ it( 'workflow.js: allows imports from ./foobar/baz.js', async () => {
403
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-2-' ) );
404
+ mkdirSync( join( dir, 'foobar' ) );
405
+ writeFileSync( join( dir, 'foobar', 'baz.js' ), 'export const baz = 1;\n' );
406
+ const src = 'import { baz } from "./foobar/baz.js";';
407
+ await expect( runLoader( join( dir, 'workflow.js' ), src ) ).resolves.toBeTruthy();
408
+ rmSync( dir, { recursive: true, force: true } );
409
+ } );
410
+
411
+ it( 'workflow.js: allows imports from ../../shared/foobar.js', async () => {
412
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-3-' ) );
413
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
414
+ mkdirSync( join( dir, 'shared' ), { recursive: true } );
415
+ writeFileSync( join( dir, 'shared', 'foobar.js' ), 'export const foobar = 1;\n' );
416
+ const src = 'import { foobar } from "../../shared/foobar.js";';
417
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
418
+ rmSync( dir, { recursive: true, force: true } );
419
+ } );
420
+
421
+ it( 'workflow.js: allows imports from ../../shared/foobar/baz.js', async () => {
422
+ const dir = mkdtempSync( join( tmpdir(), 'wf-arbitrary-4-' ) );
423
+ mkdirSync( join( dir, 'workflows', 'my_workflow' ), { recursive: true } );
424
+ mkdirSync( join( dir, 'shared', 'foobar' ), { recursive: true } );
425
+ writeFileSync( join( dir, 'shared', 'foobar', 'baz.js' ), 'export const baz = 1;\n' );
426
+ const src = 'import { baz } from "../../shared/foobar/baz.js";';
427
+ await expect( runLoader( join( dir, 'workflows', 'my_workflow', 'workflow.js' ), src ) ).resolves.toBeTruthy();
428
+ rmSync( dir, { recursive: true, force: true } );
429
+ } );
430
+
431
+ it( 'steps.js: allows imports from ./helpers/anything.js', async () => {
432
+ const dir = mkdtempSync( join( tmpdir(), 'steps-arbitrary-1-' ) );
433
+ mkdirSync( join( dir, 'helpers' ) );
434
+ writeFileSync( join( dir, 'helpers', 'anything.js' ), 'export const anything = 1;\n' );
435
+ const src = 'import { anything } from "./helpers/anything.js";';
436
+ await expect( runLoader( join( dir, 'steps.js' ), src ) ).resolves.toBeTruthy();
437
+ rmSync( dir, { recursive: true, force: true } );
438
+ } );
439
+
440
+ it( 'evaluators.js: allows imports from ./my_custom_lib/tools.js', async () => {
441
+ const dir = mkdtempSync( join( tmpdir(), 'evals-arbitrary-1-' ) );
442
+ mkdirSync( join( dir, 'my_custom_lib' ) );
443
+ writeFileSync( join( dir, 'my_custom_lib', 'tools.js' ), 'export const tools = {};\n' );
444
+ const src = 'import { tools } from "./my_custom_lib/tools.js";';
445
+ await expect( runLoader( join( dir, 'evaluators.js' ), src ) ).resolves.toBeTruthy();
446
+ rmSync( dir, { recursive: true, force: true } );
447
+ } );
448
+ } );
449
+
450
+ // =====================================================
451
+ // Instantiation location validation
452
+ // =====================================================
453
+
454
+ describe( 'instantiation location tests - correct locations', () => {
455
+ it( 'step() called in steps/fetch_data.js is allowed', async () => {
456
+ const dir = mkdtempSync( join( tmpdir(), 'step-folder-allowed-' ) );
457
+ mkdirSync( join( dir, 'steps' ) );
458
+ const src = [
459
+ 'import { step } from "@output.ai/core";',
460
+ 'export const fetchData = step({ name: "fetch_data", fn: async () => ({}) });'
461
+ ].join( '\n' );
462
+ await expect( runLoader( join( dir, 'steps', 'fetch_data.js' ), src ) ).resolves.toBeTruthy();
463
+ rmSync( dir, { recursive: true, force: true } );
464
+ } );
465
+
466
+ it( 'step() called in src/shared/steps/common.js is allowed', async () => {
467
+ const dir = mkdtempSync( join( tmpdir(), 'step-shared-allowed-' ) );
468
+ mkdirSync( join( dir, 'src', 'shared', 'steps' ), { recursive: true } );
469
+ const src = [
470
+ 'import { step } from "@output.ai/core";',
471
+ 'export const commonStep = step({ name: "common_step", fn: async () => ({}) });'
472
+ ].join( '\n' );
473
+ await expect( runLoader( join( dir, 'src', 'shared', 'steps', 'common.js' ), src ) ).resolves.toBeTruthy();
474
+ rmSync( dir, { recursive: true, force: true } );
475
+ } );
476
+
477
+ it( 'evaluator() called in evaluators/quality.js is allowed', async () => {
478
+ const dir = mkdtempSync( join( tmpdir(), 'eval-folder-allowed-' ) );
479
+ mkdirSync( join( dir, 'evaluators' ) );
480
+ const src = [
481
+ 'import { evaluator } from "@output.ai/core";',
482
+ 'export const quality = evaluator({ name: "quality", fn: async () => ({ value: 1 }) });'
483
+ ].join( '\n' );
484
+ await expect( runLoader( join( dir, 'evaluators', 'quality.js' ), src ) ).resolves.toBeTruthy();
485
+ rmSync( dir, { recursive: true, force: true } );
486
+ } );
487
+
488
+ it( 'evaluator() called in src/shared/evaluators/metrics.js is allowed', async () => {
489
+ const dir = mkdtempSync( join( tmpdir(), 'eval-shared-allowed-' ) );
490
+ mkdirSync( join( dir, 'src', 'shared', 'evaluators' ), { recursive: true } );
491
+ const src = [
492
+ 'import { evaluator } from "@output.ai/core";',
493
+ 'export const metrics = evaluator({ name: "metrics", fn: async () => ({ value: 1 }) });'
494
+ ].join( '\n' );
495
+ await expect( runLoader( join( dir, 'src', 'shared', 'evaluators', 'metrics.js' ), src ) ).resolves.toBeTruthy();
496
+ rmSync( dir, { recursive: true, force: true } );
497
+ } );
498
+ } );
499
+
500
+ describe( 'instantiation location tests - wrong locations', () => {
501
+ it( 'step() called in utils.js is blocked by validator', async () => {
502
+ const dir = mkdtempSync( join( tmpdir(), 'step-utils-fail-' ) );
503
+ const src = [
504
+ 'import { step } from "@output.ai/core";',
505
+ 'export const badStep = step({ name: "bad", fn: async () => ({}) });'
506
+ ].join( '\n' );
507
+ await expect( runLoader( join( dir, 'utils.js' ), src ) )
508
+ .rejects.toThrow( /Invalid instantiation location.*step\(\).*steps/ );
509
+ rmSync( dir, { recursive: true, force: true } );
510
+ } );
511
+
512
+ it( 'step() called in clients/api.js is blocked by validator', async () => {
513
+ const dir = mkdtempSync( join( tmpdir(), 'step-clients-fail-' ) );
514
+ mkdirSync( join( dir, 'clients' ) );
515
+ const src = [
516
+ 'import { step } from "@output.ai/core";',
517
+ 'export const badStep = step({ name: "bad", fn: async () => ({}) });'
518
+ ].join( '\n' );
519
+ await expect( runLoader( join( dir, 'clients', 'api.js' ), src ) )
520
+ .rejects.toThrow( /Invalid instantiation location.*step\(\).*steps/ );
521
+ rmSync( dir, { recursive: true, force: true } );
522
+ } );
523
+
524
+ it( 'evaluator() called in utils.js is blocked by validator', async () => {
525
+ const dir = mkdtempSync( join( tmpdir(), 'eval-utils-fail-' ) );
526
+ const src = [
527
+ 'import { evaluator } from "@output.ai/core";',
528
+ 'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
529
+ ].join( '\n' );
530
+ await expect( runLoader( join( dir, 'utils.js' ), src ) )
531
+ .rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
532
+ rmSync( dir, { recursive: true, force: true } );
533
+ } );
534
+
535
+ it( 'evaluator() called in clients/api.js is blocked by validator', async () => {
536
+ const dir = mkdtempSync( join( tmpdir(), 'eval-clients-fail-' ) );
537
+ mkdirSync( join( dir, 'clients' ) );
538
+ const src = [
539
+ 'import { evaluator } from "@output.ai/core";',
540
+ 'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
541
+ ].join( '\n' );
542
+ await expect( runLoader( join( dir, 'clients', 'api.js' ), src ) )
543
+ .rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
544
+ rmSync( dir, { recursive: true, force: true } );
545
+ } );
546
+
547
+ it( 'evaluator() called in helpers.js is blocked by validator', async () => {
548
+ const dir = mkdtempSync( join( tmpdir(), 'eval-helpers-fail-' ) );
549
+ const src = [
550
+ 'import { evaluator } from "@output.ai/core";',
551
+ 'export const badEval = evaluator({ name: "bad", fn: async () => ({ value: 1 }) });'
552
+ ].join( '\n' );
553
+ await expect( runLoader( join( dir, 'helpers.js' ), src ) )
554
+ .rejects.toThrow( /Invalid instantiation location.*evaluator\(\).*evaluators/ );
555
+ rmSync( dir, { recursive: true, force: true } );
556
+ } );
557
+
558
+ it( 'workflow() called in shared/common.js is blocked by validator', async () => {
559
+ const dir = mkdtempSync( join( tmpdir(), 'wf-shared-fail-' ) );
560
+ mkdirSync( join( dir, 'shared' ) );
561
+ const src = [
562
+ 'import { workflow } from "@output.ai/core";',
563
+ 'export const badWf = workflow({ name: "bad", fn: async () => ({}) });'
564
+ ].join( '\n' );
565
+ await expect( runLoader( join( dir, 'shared', 'common.js' ), src ) )
566
+ .rejects.toThrow( /Invalid instantiation location.*workflow\(\).*workflow/ );
567
+ rmSync( dir, { recursive: true, force: true } );
568
+ } );
569
+
570
+ it( 'workflow() called in utils/index.js is blocked by validator', async () => {
571
+ const dir = mkdtempSync( join( tmpdir(), 'wf-utils-fail-' ) );
572
+ mkdirSync( join( dir, 'utils' ) );
573
+ const src = [
574
+ 'import { workflow } from "@output.ai/core";',
575
+ 'export const badWf = workflow({ name: "bad", fn: async () => ({}) });'
576
+ ].join( '\n' );
577
+ await expect( runLoader( join( dir, 'utils', 'index.js' ), src ) )
578
+ .rejects.toThrow( /Invalid instantiation location.*workflow\(\).*workflow/ );
579
+ rmSync( dir, { recursive: true, force: true } );
580
+ } );
581
+ } );
259
582
  } );