tjs-lang 0.8.1 → 0.8.2

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 (61) hide show
  1. package/CLAUDE.md +9 -3
  2. package/demo/examples.test.ts +6 -2
  3. package/demo/src/playground-shared.ts +48 -16
  4. package/demo/src/playground-test-results.test.ts +112 -0
  5. package/demo/src/tjs-playground.ts +11 -5
  6. package/dist/examples/modules/dist/main.d.ts +34 -0
  7. package/dist/examples/modules/dist/math.d.ts +120 -0
  8. package/dist/index.js +115 -112
  9. package/dist/index.js.map +4 -4
  10. package/dist/src/lang/core.d.ts +1 -1
  11. package/dist/src/lang/dialect.d.ts +35 -0
  12. package/dist/src/lang/emitters/ast.d.ts +1 -1
  13. package/dist/src/lang/emitters/js-tests.d.ts +7 -0
  14. package/dist/src/lang/emitters/js.d.ts +12 -0
  15. package/dist/src/lang/index.d.ts +2 -1
  16. package/dist/src/lang/parser-types.d.ts +17 -0
  17. package/dist/src/lang/parser.d.ts +18 -0
  18. package/dist/src/lang/transpiler.d.ts +1 -0
  19. package/dist/src/lang/types.d.ts +18 -0
  20. package/dist/src/vm/runtime.d.ts +18 -0
  21. package/dist/src/vm/vm.d.ts +15 -1
  22. package/dist/tjs-batteries.js +2 -2
  23. package/dist/tjs-batteries.js.map +2 -2
  24. package/dist/tjs-eval.js +43 -43
  25. package/dist/tjs-eval.js.map +3 -3
  26. package/dist/tjs-from-ts.js +1 -1
  27. package/dist/tjs-from-ts.js.map +2 -2
  28. package/dist/tjs-lang.js +76 -73
  29. package/dist/tjs-lang.js.map +4 -4
  30. package/dist/tjs-vm.js +49 -49
  31. package/dist/tjs-vm.js.map +3 -3
  32. package/llms.txt +1 -0
  33. package/package.json +20 -1
  34. package/src/batteries/audit.ts +3 -2
  35. package/src/cli/commands/check.ts +3 -2
  36. package/src/cli/commands/emit.ts +4 -2
  37. package/src/cli/commands/run.ts +6 -2
  38. package/src/cli/commands/types.ts +2 -2
  39. package/src/lang/codegen.test.ts +4 -1
  40. package/src/lang/core.ts +6 -4
  41. package/src/lang/dialect.test.ts +63 -0
  42. package/src/lang/dialect.ts +50 -0
  43. package/src/lang/emitters/ast.ts +145 -2
  44. package/src/lang/emitters/js-tests.ts +46 -37
  45. package/src/lang/emitters/js.ts +19 -2
  46. package/src/lang/features.test.ts +6 -5
  47. package/src/lang/index.ts +18 -5
  48. package/src/lang/parser-types.ts +17 -0
  49. package/src/lang/parser.test.ts +12 -6
  50. package/src/lang/parser.ts +113 -3
  51. package/src/lang/subset-invariant.test.ts +90 -0
  52. package/src/lang/transpiler.ts +8 -0
  53. package/src/lang/types.ts +18 -0
  54. package/src/lang/wasm.test.ts +8 -2
  55. package/src/linalg/linalg.test.ts +8 -2
  56. package/src/use-cases/batteries.test.ts +4 -0
  57. package/src/use-cases/local-helpers.test.ts +219 -0
  58. package/src/use-cases/timeout-overrides.test.ts +169 -0
  59. package/src/vm/atoms/batteries.ts +17 -3
  60. package/src/vm/runtime.ts +89 -4
  61. package/src/vm/vm.ts +47 -9
@@ -81,6 +81,18 @@ export interface TJSTranspileOptions {
81
81
  sourceMap?: boolean
82
82
  /** Mode: 'dev' | 'strict' | 'production' */
83
83
  mode?: 'dev' | 'strict' | 'production'
84
+ /**
85
+ * Source dialect — what kind of source this string is:
86
+ * - `'tjs'` (default for a bare string): native TJS, footgun-removal modes ON
87
+ * (structural `==`, `TjsStandard`, etc.).
88
+ * - `'js'`: plain JavaScript — modes OFF, `safety: 'none'`; the source's own
89
+ * semantics are preserved (no `==`→`Eq`, no truthiness rewrite, no input
90
+ * validation). Use this when feeding tjs() a vanilla `.js` string so it
91
+ * transpiles without changing meaning. See PRINCIPLES.md (TJS ⊇ JS).
92
+ *
93
+ * For TypeScript, use the `fromTS` entry point (TS → TJS → JS).
94
+ */
95
+ dialect?: 'js' | 'tjs'
84
96
  /**
85
97
  * Test execution mode:
86
98
  * - true (default): run tests at transpile time, throw on failure
@@ -640,6 +652,7 @@ export function transpileToJS(
640
652
  } = parse(cleanSource, {
641
653
  filename,
642
654
  colonShorthand: true,
655
+ dialect: options.dialect,
643
656
  moduleLoader: options.moduleLoader,
644
657
  })
645
658
 
@@ -650,6 +663,7 @@ export function transpileToJS(
650
663
  // Pass through the moduleLoader so Phase 3 cross-file wasm composition
651
664
  // sees imported `wasm function` declarations.
652
665
  const preprocessed = preprocess(cleanSource, {
666
+ dialect: options.dialect,
653
667
  moduleLoader: options.moduleLoader,
654
668
  filename,
655
669
  })
@@ -1119,8 +1133,11 @@ export function transpileToJS(
1119
1133
  )
1120
1134
 
1121
1135
  // Check for failures and throw only if runTests === true (strict mode)
1122
- // 'only' and 'report' modes return results without throwing
1123
- const failures = testResults.filter((r) => !r.passed)
1136
+ // 'only' and 'report' modes return results without throwing.
1137
+ // Inconclusive results (a test that couldn't *run* — undefined refs like AJS
1138
+ // atoms, or a module the harness can't execute) never block transpilation:
1139
+ // that would turn subset-legal code illegal. See PRINCIPLES.md.
1140
+ const failures = testResults.filter((r) => !r.passed && !r.inconclusive)
1124
1141
  if (failures.length > 0 && runTests === true) {
1125
1142
  const errorLines = failures.map((f) => {
1126
1143
  if (f.isSignatureTest) {
@@ -1097,14 +1097,15 @@ describe('signature tests (transpile-time)', () => {
1097
1097
  `,
1098
1098
  { runTests: 'report' }
1099
1099
  )
1100
- // Should have a signature test result that's skipped (passed), not failed
1100
+ // The test can't run (unresolved import) → inconclusive, not failed.
1101
+ // Inconclusive never blocks transpilation (see PRINCIPLES.md).
1101
1102
  expect(result.testResults).toBeDefined()
1102
1103
  const sigTest = result.testResults!.find((t: any) =>
1103
1104
  t.description.includes('validateUser')
1104
1105
  )
1105
1106
  expect(sigTest).toBeDefined()
1106
- expect(sigTest?.passed).toBe(true)
1107
- expect(sigTest?.error).toBeUndefined()
1107
+ expect(sigTest?.passed).toBe(false)
1108
+ expect(sigTest?.inconclusive).toBe(true)
1108
1109
  })
1109
1110
 
1110
1111
  it('should skip tests gracefully when imports are unresolved (function-level)', () => {
@@ -1124,8 +1125,8 @@ describe('signature tests (transpile-time)', () => {
1124
1125
  t.description.includes('formatDate')
1125
1126
  )
1126
1127
  expect(sigTest).toBeDefined()
1127
- expect(sigTest?.passed).toBe(true)
1128
- expect(sigTest?.error).toBeUndefined()
1128
+ expect(sigTest?.passed).toBe(false)
1129
+ expect(sigTest?.inconclusive).toBe(true)
1129
1130
  })
1130
1131
 
1131
1132
  it('should test sync functions alongside async functions', () => {
package/src/lang/index.ts CHANGED
@@ -27,11 +27,23 @@ import type {
27
27
  TranspileResult,
28
28
  FunctionSignature,
29
29
  } from './types'
30
- import { parse, validateSingleFunction } from './parser'
30
+ import { parse, extractFunctions } from './parser'
31
31
  import { transformFunction } from './emitters/ast'
32
32
 
33
33
  export * from './types'
34
- export { parse, preprocess, extractTDoc } from './parser'
34
+ export {
35
+ parse,
36
+ preprocess,
37
+ extractTDoc,
38
+ validateSingleFunction,
39
+ extractFunctions,
40
+ } from './parser'
41
+ export {
42
+ dialectForFilename,
43
+ sourceKindForFilename,
44
+ type Dialect,
45
+ type SourceKind,
46
+ } from './dialect'
35
47
  export { transformFunction } from './emitters/ast'
36
48
  export {
37
49
  transpileToJS,
@@ -177,15 +189,16 @@ export function transpile(
177
189
  })
178
190
 
179
191
  // Validate structure
180
- const func = validateSingleFunction(program, options.filename)
192
+ const { entry, helpers } = extractFunctions(program, options.filename)
181
193
 
182
194
  // Transform to Agent99 AST
183
195
  const { ast, signature, warnings } = transformFunction(
184
- func,
196
+ entry,
185
197
  originalSource,
186
198
  returnType,
187
199
  options,
188
- requiredParams
200
+ requiredParams,
201
+ helpers.size > 0 ? helpers : undefined
189
202
  )
190
203
 
191
204
  return {
@@ -15,6 +15,17 @@ export interface ParseOptions {
15
15
  * When true, skips == to Is() transformation since the VM handles == correctly.
16
16
  */
17
17
  vmTarget?: boolean
18
+ /**
19
+ * Source dialect — tells the transpiler what kind of source this string is:
20
+ * - `'tjs'`: native TJS, all footgun-removal modes ON (structural `==`,
21
+ * `TjsStandard`, etc.). This is the default for a bare string.
22
+ * - `'js'`: plain JavaScript — modes OFF and `safety: 'none'`, so the source's
23
+ * own semantics are preserved (no `==`→`Eq`, no truthiness rewrite, etc.).
24
+ *
25
+ * Authoritative when set; otherwise the dialect is inferred (the fromTS
26
+ * annotation and `vmTarget` ⇒ JS-compatible). See PRINCIPLES.md (TJS ⊇ JS).
27
+ */
28
+ dialect?: 'js' | 'tjs'
18
29
  /**
19
30
  * Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
20
31
  * When provided, imports are resolved at transpile time and matching wasm
@@ -109,6 +120,12 @@ export interface PreprocessOptions {
109
120
  * Default: false (transform == to Is() for TJS code running in regular JS)
110
121
  */
111
122
  vmTarget?: boolean
123
+ /**
124
+ * Source dialect: `'js'` ⇒ modes OFF / `safety: 'none'` (preserve plain-JS
125
+ * semantics); `'tjs'` ⇒ native modes ON. Authoritative when set; otherwise
126
+ * inferred from the fromTS annotation / `vmTarget`. See ParseOptions.dialect.
127
+ */
128
+ dialect?: 'js' | 'tjs'
112
129
  /**
113
130
  * Optional ModuleLoader for cross-file `wasm function` composition (Phase 3).
114
131
  * See ParseOptions.moduleLoader for details.
@@ -814,13 +814,19 @@ test 'always fails' { throw new Error('intentional') }
814
814
  })
815
815
 
816
816
  describe('Error handling', () => {
817
- it('should reject multiple functions', () => {
818
- expect(() =>
819
- transpile(`
820
- function a() {}
821
- function b() {}
817
+ it('accepts multiple functions as local helpers (last is the entry)', () => {
818
+ // Multiple top-level functions are now the local-helpers feature: the
819
+ // last declaration is the entry point, the rest are helpers called by
820
+ // name via callLocal. See use-cases/local-helpers.test.ts for semantics.
821
+ const { ast } = transpile(`
822
+ function helper(x: 0): 0 { return x }
823
+ function main(n: 0): 0 {
824
+ const r = helper(n)
825
+ return r
826
+ }
822
827
  `)
823
- ).toThrow('Only a single function')
828
+ // Unused/used helpers are collected onto the root node by name.
829
+ expect((ast as any).helpers?.helper).toBeDefined()
824
830
  })
825
831
 
826
832
  it('should require a function when only a class is provided', () => {
@@ -135,9 +135,17 @@ export function preprocess(
135
135
  // The /* tjs <- filename */ annotation is the signal
136
136
  const isFromTS = /\/\*\s*tjs\s*<-\s*\S+\s*\*\//.test(source)
137
137
 
138
- // Native TJS: all modes ON by default (TJS is its own language)
139
- // TS-originated or VM target (AJS): all modes OFF, safety none (JS-compatible)
140
- const isCompat = isFromTS || options.vmTarget
138
+ // Native TJS: all modes ON by default (TJS is its own language).
139
+ // Plain JS (dialect: 'js'), TS-originated, or VM target: all modes OFF +
140
+ // safety none, so the source's own semantics are preserved (JS-compatible).
141
+ // An explicit `dialect` is authoritative; otherwise infer from the fromTS
142
+ // annotation / vmTarget. See PRINCIPLES.md (TJS ⊇ JS).
143
+ const isCompat =
144
+ options.dialect === 'js'
145
+ ? true
146
+ : options.dialect === 'tjs'
147
+ ? false
148
+ : isFromTS || options.vmTarget
141
149
  const tjsModes: TjsModes = isCompat
142
150
  ? {
143
151
  tjsEquals: false,
@@ -452,6 +460,7 @@ export function parse(
452
460
  filename = '<source>',
453
461
  colonShorthand = true,
454
462
  vmTarget = false,
463
+ dialect,
455
464
  } = options
456
465
 
457
466
  // Preprocess for custom syntax
@@ -472,6 +481,7 @@ export function parse(
472
481
  } = colonShorthand
473
482
  ? preprocess(source, {
474
483
  vmTarget,
484
+ dialect,
475
485
  moduleLoader: options.moduleLoader,
476
486
  filename: options.filename,
477
487
  })
@@ -602,6 +612,106 @@ export function validateSingleFunction(
602
612
  return functions[0]
603
613
  }
604
614
 
615
+ /**
616
+ * Extract top-level function declarations from the parsed program.
617
+ *
618
+ * Returns `{ entry, helpers }` where:
619
+ * - `entry` is the last function declaration (the agent's entry point)
620
+ * - `helpers` are all preceding function declarations, looked up by name
621
+ *
622
+ * This matches the natural "helpers first, agent last" pattern, including
623
+ * the TOOL_LIBRARY use case where helper async wrappers are prepended
624
+ * before the user-supplied agent function.
625
+ *
626
+ * Same top-level construct restrictions as `validateSingleFunction`:
627
+ * imports, exports, and classes are rejected.
628
+ */
629
+ export function extractFunctions(
630
+ ast: Program,
631
+ filename?: string
632
+ ): { entry: FunctionDeclaration; helpers: Map<string, FunctionDeclaration> } {
633
+ // Top-level construct checks (same as validateSingleFunction)
634
+ for (const node of ast.body) {
635
+ if (node.type === 'ImportDeclaration') {
636
+ throw new SyntaxError(
637
+ 'Imports are not supported. All atoms must be registered with the VM.',
638
+ node.loc?.start || { line: 1, column: 0 },
639
+ undefined,
640
+ filename
641
+ )
642
+ }
643
+
644
+ if (
645
+ node.type === 'ExportNamedDeclaration' ||
646
+ node.type === 'ExportDefaultDeclaration'
647
+ ) {
648
+ throw new SyntaxError(
649
+ 'Exports are not supported. The function is automatically exported.',
650
+ node.loc?.start || { line: 1, column: 0 },
651
+ undefined,
652
+ filename
653
+ )
654
+ }
655
+
656
+ if (node.type === 'ClassDeclaration') {
657
+ throw new SyntaxError(
658
+ 'Classes are not supported. Agent99 uses functional composition.',
659
+ node.loc?.start || { line: 1, column: 0 },
660
+ undefined,
661
+ filename
662
+ )
663
+ }
664
+ }
665
+
666
+ const functions = ast.body.filter(
667
+ (node): node is FunctionDeclaration => node.type === 'FunctionDeclaration'
668
+ )
669
+
670
+ if (functions.length === 0) {
671
+ throw new SyntaxError(
672
+ 'Source must contain a function declaration',
673
+ { line: 1, column: 0 },
674
+ undefined,
675
+ filename
676
+ )
677
+ }
678
+
679
+ const entry = functions[functions.length - 1]
680
+ const helpers = new Map<string, FunctionDeclaration>()
681
+
682
+ for (let i = 0; i < functions.length - 1; i++) {
683
+ const fn = functions[i]
684
+ const name = fn.id?.name
685
+ if (!name) {
686
+ throw new SyntaxError(
687
+ 'Helper function must have a name',
688
+ fn.loc?.start || { line: 1, column: 0 },
689
+ undefined,
690
+ filename
691
+ )
692
+ }
693
+ if (helpers.has(name)) {
694
+ throw new SyntaxError(
695
+ `Duplicate helper function name: ${name}`,
696
+ fn.loc?.start || { line: 1, column: 0 },
697
+ undefined,
698
+ filename
699
+ )
700
+ }
701
+ if (name === entry.id?.name) {
702
+ throw new SyntaxError(
703
+ `Helper function cannot share a name with the entry function: ${name}`,
704
+ fn.loc?.start || { line: 1, column: 0 },
705
+ undefined,
706
+ filename
707
+ )
708
+ }
709
+ helpers.set(name, fn)
710
+ }
711
+
712
+ return { entry, helpers }
713
+ }
714
+
605
715
  /**
606
716
  * Extract TDoc comment from before a function
607
717
  *
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { tjs, transpile } from './index'
3
+
4
+ /**
5
+ * Guards the language subset invariants engraved in PRINCIPLES.md:
6
+ *
7
+ * AJS ⊆ TJS — every legal AJS source is legal TJS source
8
+ * JS ⊆ TJS (no modes) — every legal JS program is legal TJS
9
+ *
10
+ * TJS may do MORE with the same source (enforce contracts, run signature
11
+ * tests) but must never REJECT source the subset accepts. The classic way this
12
+ * breaks: a build-time signature test that can't *run* (it calls an AJS atom
13
+ * that doesn't exist at build time, etc.) gets escalated into a transpile
14
+ * error. Such tests must be *inconclusive*, never failing.
15
+ */
16
+ describe('Language subset invariants (PRINCIPLES.md)', () => {
17
+ // Representative AJS-shaped sources. Each must be valid AJS (transpile to a
18
+ // VM AST) AND valid TJS (tjs() must not throw). Several carry return types
19
+ // and call atoms — the exact shape that used to be illegal TJS.
20
+ const ajsSnippets: Array<[string, string]> = [
21
+ [
22
+ 'agent returning an object (no types)',
23
+ `function main(n: 0) {\n return { doubled: n * 2 }\n}`,
24
+ ],
25
+ [
26
+ 'atom call + return type',
27
+ `function main(url: ''): { x: '' } {\n const x = httpFetch({ url })\n return { x }\n}`,
28
+ ],
29
+ [
30
+ 'helper with a typed signature',
31
+ `function double(x: 0): 0 {\n return x * 2\n}\nfunction main(n: 0) {\n const d = double(n)\n return { d }\n}`,
32
+ ],
33
+ [
34
+ 'helper that calls an atom + return type',
35
+ `function fetchIt(u: ''): '' {\n const r = httpFetch({ url: u })\n return r\n}\nfunction main(url: '') {\n const x = fetchIt(url)\n return { x }\n}`,
36
+ ],
37
+ [
38
+ 'consistent signature example still validates',
39
+ `function add(a: 2, b: 3): 5 {\n return a + b\n}\nfunction main(x: 0, y: 0) {\n const s = add(x, y)\n return { s }\n}`,
40
+ ],
41
+ ]
42
+
43
+ describe('TJS ⊇ AJS', () => {
44
+ for (const [label, src] of ajsSnippets) {
45
+ it(`valid as both AJS and TJS: ${label}`, () => {
46
+ // Valid AJS (produces a VM AST)…
47
+ expect(() => transpile(src, { vmTarget: true })).not.toThrow()
48
+ // …and therefore must be valid TJS (never rejected).
49
+ expect(() => tjs(src)).not.toThrow()
50
+ })
51
+ }
52
+ })
53
+
54
+ it('un-runnable signature tests are inconclusive, not failures', () => {
55
+ const r = tjs(
56
+ `function main(url: ''): { x: '' } {\n const x = httpFetch({ url })\n return { x }\n}`
57
+ )
58
+ const sig = r.testResults?.find((t: any) => t.isSignatureTest)
59
+ expect(sig).toBeDefined()
60
+ expect(sig?.passed).toBe(false)
61
+ expect(sig?.inconclusive).toBe(true)
62
+ })
63
+
64
+ it('still REJECTS a genuinely inconsistent signature example (validation intact)', () => {
65
+ // 2 + 3 = 5, not 99 — the test runs cleanly and mismatches → hard failure.
66
+ expect(() =>
67
+ tjs(`function add(a: 2, b: 3): 99 {\n return a + b\n}`)
68
+ ).toThrow(/inconsistent/)
69
+ })
70
+
71
+ describe('TJS (no modes) ⊇ JS', () => {
72
+ // Plain JavaScript under options-off TJS (TjsCompat disables all modes).
73
+ const jsSnippets: Array<[string, string]> = [
74
+ ['arithmetic fn', `function f(x) { return x + 1 }`],
75
+ [
76
+ 'control flow + array methods',
77
+ `function f(xs) {\n let total = 0\n for (const x of xs) { total += x }\n return xs.map(v => v * 2).filter(v => v > total)\n}`,
78
+ ],
79
+ [
80
+ 'object + destructuring',
81
+ `function f(o) {\n const { a, b } = o\n return { ...o, sum: a + b }\n}`,
82
+ ],
83
+ ]
84
+ for (const [label, src] of jsSnippets) {
85
+ it(`accepts plain JS: ${label}`, () => {
86
+ expect(() => tjs(`TjsCompat\n${src}`)).not.toThrow()
87
+ })
88
+ }
89
+ })
90
+ })
@@ -16,6 +16,14 @@ export { transpile, ajs, tjs, createAgent, getToolDefinitions } from './core'
16
16
  // Parser
17
17
  export { parse, preprocess, extractTDoc } from './parser'
18
18
 
19
+ // Dialect resolution for file-based tooling (extension → dialect)
20
+ export {
21
+ dialectForFilename,
22
+ sourceKindForFilename,
23
+ type Dialect,
24
+ type SourceKind,
25
+ } from './dialect'
26
+
19
27
  // AST emitter
20
28
  export { transformFunction } from './emitters/ast'
21
29
 
package/src/lang/types.ts CHANGED
@@ -225,6 +225,21 @@ export interface TransformContext {
225
225
  filename: string
226
226
  /** Options */
227
227
  options: TranspileOptions
228
+ /**
229
+ * Helper functions (top-level functions declared before the entry function).
230
+ * Calls to these names emit `callLocal` instead of treating them as atom calls.
231
+ */
232
+ helpers?: Map<string, any /* FunctionDeclaration */>
233
+ /**
234
+ * Cache of transformed helper bodies. Lazily populated as helpers are
235
+ * referenced from the entry function (or from other helpers).
236
+ */
237
+ helperSteps?: Map<string, { steps: any[]; paramNames: string[] }>
238
+ /**
239
+ * Names of helpers currently being transformed. Used to detect direct or
240
+ * transitive recursion.
241
+ */
242
+ helperTransforming?: Set<string>
228
243
  }
229
244
 
230
245
  /** Create a child context for nested scopes */
@@ -239,6 +254,9 @@ export function createChildContext(parent: TransformContext): TransformContext {
239
254
  source: parent.source,
240
255
  filename: parent.filename,
241
256
  options: parent.options,
257
+ helpers: parent.helpers,
258
+ helperSteps: parent.helperSteps,
259
+ helperTransforming: parent.helperTransforming,
242
260
  }
243
261
  }
244
262
 
@@ -2317,14 +2317,20 @@ describe('boundary distribution form (Phase 4)', () => {
2317
2317
  async function dynamicImportLibrary(transpiled: string): Promise<any> {
2318
2318
  const { tmpdir } = await import('node:os')
2319
2319
  const { join } = await import('node:path')
2320
- const { writeFileSync, unlinkSync } = await import('node:fs')
2320
+ const { writeFileSync, unlinkSync, realpathSync } = await import('node:fs')
2321
+ const { pathToFileURL } = await import('node:url')
2321
2322
  const path = join(
2322
2323
  tmpdir(),
2323
2324
  `tjs-lib-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
2324
2325
  )
2325
2326
  writeFileSync(path, transpiled)
2327
+ // Resolve macOS /var → /private/var symlink and use file:// URL — bun's
2328
+ // resolver in subsequent test cases (after a `new Function(emittedCode)`
2329
+ // execution earlier in the same file) fails on bare absolute paths with
2330
+ // "Cannot find module ... from ''". This form is robust to that state.
2331
+ const url = pathToFileURL(realpathSync(path)).href
2326
2332
  try {
2327
- const mod = await import(path)
2333
+ const mod = await import(url)
2328
2334
  // Wait for the async wasm bootstrap inside the module to finish.
2329
2335
  // The bootstrap runs as a top-level IIFE; instantiation is async.
2330
2336
  await new Promise((r) => setTimeout(r, 100))
@@ -14,9 +14,10 @@
14
14
  */
15
15
 
16
16
  import { describe, it, expect } from 'bun:test'
17
- import { readFileSync } from 'node:fs'
17
+ import { readFileSync, realpathSync } from 'node:fs'
18
18
  import { join } from 'node:path'
19
19
  import { tmpdir } from 'node:os'
20
+ import { pathToFileURL } from 'node:url'
20
21
  import { writeFileSync, unlinkSync } from 'node:fs'
21
22
 
22
23
  const LINALG_PATH = join(import.meta.dir, 'index.tjs')
@@ -41,8 +42,13 @@ async function dynamicImportLibrary(transpiled: string): Promise<any> {
41
42
  `linalg-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.mjs`
42
43
  )
43
44
  writeFileSync(path, transpiled)
45
+ // Resolve macOS /var → /private/var symlink and use file:// URL — bun's
46
+ // resolver in subsequent test cases (after a `new Function(emittedCode)`
47
+ // execution earlier in the same file) fails on bare absolute paths with
48
+ // "Cannot find module ... from ''". This form is robust to that state.
49
+ const url = pathToFileURL(realpathSync(path)).href
44
50
  try {
45
- const mod = await import(path)
51
+ const mod = await import(url)
46
52
  // Wait for the async wasm bootstrap inside the module to finish
47
53
  await new Promise((r) => setTimeout(r, 100))
48
54
  return mod
@@ -252,4 +252,8 @@ describe('Batteries Included', () => {
252
252
  expect(msg.tool_calls[0].function.name).toBe('get_weather')
253
253
  expect(msg.tool_calls[0].function.arguments).toBe('{"city":"Paris"}')
254
254
  })
255
+
256
+ it('LLM battery atoms have IO-friendly timeouts', () => {
257
+ expect(llmPredictBattery.timeoutMs).toBeGreaterThanOrEqual(60000)
258
+ })
255
259
  })