@outputai/core 0.1.0

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