@chainlink/cre-sdk 1.1.4 → 1.2.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.
@@ -0,0 +1,433 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import path from 'node:path'
5
+ import {
6
+ assertWorkflowRuntimeCompatibility,
7
+ WorkflowRuntimeCompatibilityError,
8
+ } from './validate-workflow-runtime-compat'
9
+
10
+ let tempDir: string
11
+
12
+ beforeEach(() => {
13
+ tempDir = mkdtempSync(path.join(tmpdir(), 'cre-validate-test-'))
14
+ })
15
+
16
+ afterEach(() => {
17
+ rmSync(tempDir, { recursive: true, force: true })
18
+ })
19
+
20
+ /** Write a file in the temp directory and return its absolute path. */
21
+ const writeTemp = (filename: string, content: string): string => {
22
+ const filePath = path.join(tempDir, filename)
23
+ const dir = path.dirname(filePath)
24
+ mkdirSync(dir, { recursive: true })
25
+ writeFileSync(filePath, content, 'utf-8')
26
+ return filePath
27
+ }
28
+
29
+ /** Assert that the validator throws with violations matching the given patterns. */
30
+ const expectViolations = (entryPath: string, expectedPatterns: (string | RegExp)[]) => {
31
+ try {
32
+ assertWorkflowRuntimeCompatibility(entryPath)
33
+ throw new Error('Expected WorkflowRuntimeCompatibilityError but none was thrown')
34
+ } catch (error) {
35
+ expect(error).toBeInstanceOf(WorkflowRuntimeCompatibilityError)
36
+ const message = (error as Error).message
37
+ for (const pattern of expectedPatterns) {
38
+ if (typeof pattern === 'string') {
39
+ expect(message).toContain(pattern)
40
+ } else {
41
+ expect(message).toMatch(pattern)
42
+ }
43
+ }
44
+ }
45
+ }
46
+
47
+ /** Assert that the validator does NOT throw. */
48
+ const expectNoViolations = (entryPath: string) => {
49
+ expect(() => assertWorkflowRuntimeCompatibility(entryPath)).not.toThrow()
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Pass 1: Module import analysis
54
+ // ---------------------------------------------------------------------------
55
+
56
+ describe('module import analysis', () => {
57
+ test("detects import ... from 'node:fs'", () => {
58
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`)
59
+ expectViolations(entry, ["'node:fs' is not available"])
60
+ })
61
+
62
+ test("detects bare module specifier 'fs' (without node: prefix)", () => {
63
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'fs';\n`)
64
+ expectViolations(entry, ["'fs' is not available"])
65
+ })
66
+
67
+ test('detects export ... from restricted module', () => {
68
+ const entry = writeTemp('workflow.ts', `export { createHash } from 'node:crypto';\n`)
69
+ expectViolations(entry, ["'node:crypto' is not available"])
70
+ })
71
+
72
+ test('detects import = require() syntax', () => {
73
+ const entry = writeTemp('workflow.ts', `import fs = require('node:fs');\n`)
74
+ expectViolations(entry, ["'node:fs' is not available"])
75
+ })
76
+
77
+ test('detects require() call', () => {
78
+ const entry = writeTemp('workflow.js', `const fs = require('node:fs');\n`)
79
+ expectViolations(entry, ["'node:fs' is not available"])
80
+ })
81
+
82
+ test('detects dynamic import()', () => {
83
+ const entry = writeTemp('workflow.ts', `const fs = await import('node:fs');\n`)
84
+ expectViolations(entry, ["'node:fs' is not available"])
85
+ })
86
+
87
+ test('detects all restricted modules in a single file', () => {
88
+ const modules = [
89
+ 'crypto',
90
+ 'node:crypto',
91
+ 'fs',
92
+ 'node:fs',
93
+ 'fs/promises',
94
+ 'node:fs/promises',
95
+ 'net',
96
+ 'node:net',
97
+ 'http',
98
+ 'node:http',
99
+ 'https',
100
+ 'node:https',
101
+ 'child_process',
102
+ 'node:child_process',
103
+ 'os',
104
+ 'node:os',
105
+ 'stream',
106
+ 'node:stream',
107
+ 'worker_threads',
108
+ 'node:worker_threads',
109
+ 'dns',
110
+ 'node:dns',
111
+ 'zlib',
112
+ 'node:zlib',
113
+ ]
114
+
115
+ const imports = modules.map((mod, i) => `import m${i} from '${mod}';`).join('\n')
116
+ const entry = writeTemp('workflow.ts', `${imports}\n`)
117
+ expectViolations(
118
+ entry,
119
+ modules.map((mod) => `'${mod}' is not available`),
120
+ )
121
+ })
122
+
123
+ test('does NOT flag allowed third-party modules', () => {
124
+ const entry = writeTemp(
125
+ 'workflow.ts',
126
+ `import { something } from '@chainlink/cre-sdk';\nimport lodash from 'lodash';\n`,
127
+ )
128
+ expectNoViolations(entry)
129
+ })
130
+
131
+ test('does NOT flag relative imports themselves', () => {
132
+ const helper = writeTemp('helper.ts', `export const add = (a: number, b: number) => a + b;\n`)
133
+ const entry = writeTemp(
134
+ 'workflow.ts',
135
+ `import { add } from './helper';\nconsole.log(add(1, 2));\n`,
136
+ )
137
+ expectNoViolations(entry)
138
+ })
139
+
140
+ test('follows relative imports transitively and detects violations in them', () => {
141
+ writeTemp('deep.ts', `import { readFileSync } from 'node:fs';\nexport const x = 1;\n`)
142
+ writeTemp('middle.ts', `import { x } from './deep';\nexport const y = x;\n`)
143
+ const entry = writeTemp('workflow.ts', `import { y } from './middle';\nconsole.log(y);\n`)
144
+ expectViolations(entry, ["'node:fs' is not available"])
145
+ })
146
+
147
+ test('handles circular relative imports without infinite loop', () => {
148
+ writeTemp('a.ts', `import { b } from './b';\nexport const a = 'a';\n`)
149
+ writeTemp('b.ts', `import { a } from './a';\nexport const b = 'b';\n`)
150
+ const entry = writeTemp('workflow.ts', `import { a } from './a';\n`)
151
+ expectNoViolations(entry)
152
+ })
153
+
154
+ test('resolves imports without file extension', () => {
155
+ writeTemp('utils.ts', `import { cpus } from 'node:os';\nexport const x = 1;\n`)
156
+ const entry = writeTemp('workflow.ts', `import { x } from './utils';\nconsole.log(x);\n`)
157
+ expectViolations(entry, ["'node:os' is not available"])
158
+ })
159
+
160
+ test('resolves index file imports', () => {
161
+ writeTemp('lib/index.ts', `import { hostname } from 'node:os';\nexport const name = 'test';\n`)
162
+ const entry = writeTemp('workflow.ts', `import { name } from './lib';\nconsole.log(name);\n`)
163
+ expectViolations(entry, ["'node:os' is not available"])
164
+ })
165
+
166
+ test('reports multiple violations from multiple files', () => {
167
+ writeTemp('helper.ts', `import { exec } from 'node:child_process';\nexport const run = exec;\n`)
168
+ const entry = writeTemp(
169
+ 'workflow.ts',
170
+ `import { run } from './helper';\nimport { readFileSync } from 'node:fs';\n`,
171
+ )
172
+ expectViolations(entry, ["'node:child_process' is not available", "'node:fs' is not available"])
173
+ })
174
+ })
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Pass 2: Global API analysis
178
+ // ---------------------------------------------------------------------------
179
+
180
+ describe('global API analysis', () => {
181
+ test('detects bare fetch() usage', () => {
182
+ const entry = writeTemp('workflow.ts', `const res = fetch('https://example.com');\n`)
183
+ expectViolations(entry, ["'fetch' is not available"])
184
+ })
185
+
186
+ test('detects setTimeout usage', () => {
187
+ const entry = writeTemp('workflow.ts', `setTimeout(() => {}, 1000);\n`)
188
+ expectViolations(entry, ["'setTimeout' is not available"])
189
+ })
190
+
191
+ test('detects setInterval usage', () => {
192
+ const entry = writeTemp('workflow.ts', `setInterval(() => {}, 1000);\n`)
193
+ expectViolations(entry, ["'setInterval' is not available"])
194
+ })
195
+
196
+ test('detects window reference', () => {
197
+ const entry = writeTemp('workflow.ts', `const w = window;\n`)
198
+ expectViolations(entry, ["'window' is not available"])
199
+ })
200
+
201
+ test('detects document reference', () => {
202
+ const entry = writeTemp('workflow.ts', `const el = document.getElementById('app');\n`)
203
+ expectViolations(entry, ["'document' is not available"])
204
+ })
205
+
206
+ test('detects XMLHttpRequest usage', () => {
207
+ const entry = writeTemp('workflow.ts', `const xhr = new XMLHttpRequest();\n`)
208
+ expectViolations(entry, ["'XMLHttpRequest' is not available"])
209
+ })
210
+
211
+ test('detects localStorage usage', () => {
212
+ const entry = writeTemp('workflow.ts', `localStorage.setItem('key', 'value');\n`)
213
+ expectViolations(entry, ["'localStorage' is not available"])
214
+ })
215
+
216
+ test('detects sessionStorage usage', () => {
217
+ const entry = writeTemp('workflow.ts', `sessionStorage.getItem('key');\n`)
218
+ expectViolations(entry, ["'sessionStorage' is not available"])
219
+ })
220
+
221
+ test('detects globalThis.fetch access', () => {
222
+ const entry = writeTemp('workflow.ts', `const res = globalThis.fetch('https://example.com');\n`)
223
+ expectViolations(entry, ["'globalThis.fetch' is not available"])
224
+ })
225
+
226
+ test('detects globalThis.setTimeout access', () => {
227
+ const entry = writeTemp('workflow.ts', `globalThis.setTimeout(() => {}, 100);\n`)
228
+ expectViolations(entry, ["'globalThis.setTimeout' is not available"])
229
+ })
230
+
231
+ test('does NOT flag user-defined variable named fetch', () => {
232
+ const entry = writeTemp(
233
+ 'workflow.ts',
234
+ `export const fetch = (url: string) => url;\nconst result = fetch('test');\n`,
235
+ )
236
+ expectNoViolations(entry)
237
+ })
238
+
239
+ test('does NOT flag user-defined function named fetch', () => {
240
+ const entry = writeTemp(
241
+ 'workflow.ts',
242
+ `export function fetch(url: string) { return url; }\nconst result = fetch('test');\n`,
243
+ )
244
+ expectNoViolations(entry)
245
+ })
246
+
247
+ test('does NOT flag function parameter named fetch', () => {
248
+ const entry = writeTemp(
249
+ 'workflow.ts',
250
+ `export function doRequest(fetch: (url: string) => void) { fetch('test'); }\n`,
251
+ )
252
+ expectNoViolations(entry)
253
+ })
254
+
255
+ test('does NOT flag property access obj.fetch', () => {
256
+ const entry = writeTemp('workflow.ts', `const obj = { fetch: () => {} };\nobj.fetch();\n`)
257
+ expectNoViolations(entry)
258
+ })
259
+
260
+ test('does NOT flag interface property named fetch', () => {
261
+ const entry = writeTemp('workflow.ts', `interface Client { fetch: (url: string) => void; }\n`)
262
+ expectNoViolations(entry)
263
+ })
264
+
265
+ test('does NOT flag destructured property named fetch from local object', () => {
266
+ const entry = writeTemp(
267
+ 'workflow.ts',
268
+ `const capabilities = { fetch: (url: string) => url };\nconst { fetch } = capabilities;\nexport const result = fetch('test');\n`,
269
+ )
270
+ expectNoViolations(entry)
271
+ })
272
+
273
+ test('does NOT flag class method named fetch', () => {
274
+ const entry = writeTemp(
275
+ 'workflow.ts',
276
+ `class HttpClient {\n fetch(url: string) { return url; }\n}\nnew HttpClient().fetch('test');\n`,
277
+ )
278
+ expectNoViolations(entry)
279
+ })
280
+
281
+ test('detects global APIs in transitively imported files', () => {
282
+ writeTemp('helper.ts', `export const doFetch = () => fetch('https://example.com');\n`)
283
+ const entry = writeTemp('workflow.ts', `import { doFetch } from './helper';\ndoFetch();\n`)
284
+ expectViolations(entry, ["'fetch' is not available"])
285
+ })
286
+
287
+ test('handles a nearby tsconfig with sparse libs and no ambient types', () => {
288
+ writeTemp(
289
+ 'tsconfig.json',
290
+ JSON.stringify(
291
+ {
292
+ compilerOptions: {
293
+ lib: ['ESNext'],
294
+ module: 'ESNext',
295
+ moduleResolution: 'Bundler',
296
+ skipLibCheck: true,
297
+ types: [],
298
+ },
299
+ },
300
+ null,
301
+ 2,
302
+ ),
303
+ )
304
+ const entry = writeTemp('workflow.ts', `fetch('https://example.com');\n`)
305
+ expectViolations(entry, ["'fetch' is not available"])
306
+ })
307
+ })
308
+
309
+ // ---------------------------------------------------------------------------
310
+ // Combined / integration tests
311
+ // ---------------------------------------------------------------------------
312
+
313
+ describe('integration', () => {
314
+ test('clean workflow passes validation', () => {
315
+ const entry = writeTemp(
316
+ 'workflow.ts',
317
+ `
318
+ import { Runner, cre } from '@chainlink/cre-sdk';
319
+
320
+ export async function main() {
321
+ const runner = await Runner.newRunner();
322
+ console.log('Hello from CRE');
323
+ }
324
+ `,
325
+ )
326
+ expectNoViolations(entry)
327
+ })
328
+
329
+ test('detects both module and global API violations in same file', () => {
330
+ const entry = writeTemp(
331
+ 'workflow.ts',
332
+ `
333
+ import { readFileSync } from 'node:fs';
334
+ const data = readFileSync('/tmp/data.json', 'utf-8');
335
+ const res = fetch('https://api.example.com');
336
+ `,
337
+ )
338
+ expectViolations(entry, ["'node:fs' is not available", "'fetch' is not available"])
339
+ })
340
+
341
+ test('error message includes file path and line/column info', () => {
342
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`)
343
+ try {
344
+ assertWorkflowRuntimeCompatibility(entry)
345
+ throw new Error('Expected error')
346
+ } catch (error) {
347
+ expect(error).toBeInstanceOf(WorkflowRuntimeCompatibilityError)
348
+ const msg = (error as Error).message
349
+ // Should contain relative or absolute path to the file
350
+ expect(msg).toContain('workflow.ts')
351
+ // Should contain line:column format
352
+ expect(msg).toMatch(/:\d+:\d+/)
353
+ }
354
+ })
355
+
356
+ test('error message includes docs link', () => {
357
+ const entry = writeTemp('workflow.ts', `import { readFileSync } from 'node:fs';\n`)
358
+ try {
359
+ assertWorkflowRuntimeCompatibility(entry)
360
+ throw new Error('Expected error')
361
+ } catch (error) {
362
+ const msg = (error as Error).message
363
+ expect(msg).toContain('https://docs.chain.link/cre/concepts/typescript-wasm-runtime')
364
+ }
365
+ })
366
+
367
+ test('handles .js files', () => {
368
+ const entry = writeTemp('workflow.js', `const fs = require('node:fs');\n`)
369
+ expectViolations(entry, ["'node:fs' is not available"])
370
+ })
371
+
372
+ test('handles .mjs files', () => {
373
+ const entry = writeTemp('workflow.mjs', `import { readFileSync } from 'node:fs';\n`)
374
+ expectViolations(entry, ["'node:fs' is not available"])
375
+ })
376
+
377
+ test('handles .cjs files', () => {
378
+ const entry = writeTemp('workflow.cjs', `const fs = require('node:fs');\n`)
379
+ expectViolations(entry, ["'node:fs' is not available"])
380
+ })
381
+
382
+ test('violations are sorted by file path, then line, then column', () => {
383
+ writeTemp(
384
+ 'b-helper.ts',
385
+ `import { exec } from 'node:child_process';\nexport const run = exec;\n`,
386
+ )
387
+ const entry = writeTemp(
388
+ 'a-workflow.ts',
389
+ `import { run } from './b-helper';\nimport { readFileSync } from 'node:fs';\nimport { cpus } from 'node:os';\n`,
390
+ )
391
+ try {
392
+ assertWorkflowRuntimeCompatibility(entry)
393
+ throw new Error('Expected error')
394
+ } catch (error) {
395
+ const msg = (error as Error).message
396
+ const violationLines = msg.split('\n').filter((line) => line.startsWith('- '))
397
+
398
+ // Should have 3 violations minimum
399
+ expect(violationLines.length).toBeGreaterThanOrEqual(3)
400
+
401
+ // Extract file paths from violation lines
402
+ const filePaths = violationLines.map((line) => line.split(':')[0].replace('- ', ''))
403
+
404
+ // Verify sorted order: a-workflow.ts violations before b-helper.ts
405
+ const aIndexes = filePaths
406
+ .map((f, i) => (f.includes('a-workflow') ? i : -1))
407
+ .filter((i) => i >= 0)
408
+ const bIndexes = filePaths
409
+ .map((f, i) => (f.includes('b-helper') ? i : -1))
410
+ .filter((i) => i >= 0)
411
+
412
+ if (aIndexes.length > 0 && bIndexes.length > 0) {
413
+ expect(Math.max(...aIndexes)).toBeLessThan(Math.min(...bIndexes))
414
+ }
415
+ }
416
+ })
417
+
418
+ test('empty file passes validation', () => {
419
+ const entry = writeTemp('workflow.ts', '')
420
+ expectNoViolations(entry)
421
+ })
422
+
423
+ test('file with only comments passes validation', () => {
424
+ const entry = writeTemp('workflow.ts', `// This is a comment\n/* Block comment */\n`)
425
+ expectNoViolations(entry)
426
+ })
427
+
428
+ test('non-existent entry file does not throw', () => {
429
+ const nonExistent = path.join(tempDir, 'does-not-exist.ts')
430
+ // Should not throw since the file doesn't exist - it just won't find violations
431
+ expectNoViolations(nonExistent)
432
+ })
433
+ })