From 7a60c944d1a8357fccc2a1eaae70f5b4c8cc7669 Mon Sep 17 00:00:00 2001 From: Wassim SAMAD Date: Wed, 9 Sep 2026 08:28:27 -0400 Subject: [PATCH] test(nodes): run source-system probes from a package-local file, not bun -e Fix private-editor CI's Lint, Typecheck & Test / Unit tests failure on Bun 1.3.0 Linux: eval probes started at the editor submodule root could not resolve @pascal-app/core from dependencies hoisted to the private root. Write isolated probes under ignored package-local .turbo directories, resolve source imports and mocks from import.meta.dir, and remove probes in finally. Apply the same fix to the core parser test that imports zod from an eval probe. Preserve all cases and assertions. Verified both dependency layouts, package and private-root test invocations, eval failure and file success from /tmp with automatic installs disabled, randomized nodes tests (seed 1), core parser tests, Biome, and no-emit typechecks. --- .../src/schema/compiled-node-parsers.test.ts | 31 ++++++--- .../shared/node-batch/source-systems.test.ts | 68 +++++++++++-------- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/packages/core/src/schema/compiled-node-parsers.test.ts b/packages/core/src/schema/compiled-node-parsers.test.ts index 798d406204..7156657313 100644 --- a/packages/core/src/schema/compiled-node-parsers.test.ts +++ b/packages/core/src/schema/compiled-node-parsers.test.ts @@ -1,4 +1,6 @@ import { afterAll, beforeAll, describe, expect, test } from 'bun:test' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' import { z } from 'zod' import { authoredNodeSchemas, NODE_KINDS, nodeFixtures } from './__fixtures__/node-fixtures' import { @@ -287,16 +289,25 @@ describe('compiled node parsers — jitless (CSP) profile', () => { })) ` - const proc = Bun.spawnSync(['bun', '-e', source], { cwd: dir }) - const stdout = proc.stdout.toString().trim() - expect(proc.exitCode, proc.stderr.toString()).toBe(0) - - const result = JSON.parse(stdout.slice(stdout.lastIndexOf('{'))) - expect(result.allowsEval).toBe(false) - expect(result.enabled).toBe(true) - expect(result.kinds).toBe(NODE_KINDS.length) - expect(result.compiledAnyway).toEqual([]) - expect(result.parseFailures).toEqual([]) + const cacheDir = join(dir, '.turbo') + mkdirSync(cacheDir, { recursive: true }) + const probeDir = mkdtempSync(join(cacheDir, 'source-test-')) + try { + const probePath = join(probeDir, 'probe.ts') + writeFileSync(probePath, source) + const proc = Bun.spawnSync([process.execPath, probePath], { cwd: dir }) + const stdout = proc.stdout.toString().trim() + expect(proc.exitCode, proc.stderr.toString()).toBe(0) + + const result = JSON.parse(stdout.slice(stdout.lastIndexOf('{'))) + expect(result.allowsEval).toBe(false) + expect(result.enabled).toBe(true) + expect(result.kinds).toBe(NODE_KINDS.length) + expect(result.compiledAnyway).toEqual([]) + expect(result.parseFailures).toEqual([]) + } finally { + rmSync(probeDir, { recursive: true, force: true }) + } }, 60_000) }) diff --git a/packages/nodes/src/shared/node-batch/source-systems.test.ts b/packages/nodes/src/shared/node-batch/source-systems.test.ts index 806774b108..591863f720 100644 --- a/packages/nodes/src/shared/node-batch/source-systems.test.ts +++ b/packages/nodes/src/shared/node-batch/source-systems.test.ts @@ -1,13 +1,22 @@ import { expect, test } from 'bun:test' -import { resolve } from 'node:path' +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' + +function sourcePath(path: string) { + return JSON.stringify(resolve(import.meta.dir, '../../../../..', path)) +} // Isolate module wiring: exercise changed viewer sources without rebuilding live dists // or leaking Bun's process-global module mocks into the randomized nodes suite. function runSourceTest(body: string) { - const result = Bun.spawnSync( - [ - process.execPath, - '-e', + const cacheDir = join(import.meta.dir, '.turbo') + mkdirSync(cacheDir, { recursive: true }) + // A package-local file resolves hoisted dependencies in private-editor's submodule layout. + const probeDir = mkdtempSync(join(cacheDir, 'source-test-')) + try { + const probePath = join(probeDir, 'probe.ts') + writeFileSync( + probePath, ` import assert from 'node:assert/strict' import { mock } from 'bun:test' @@ -16,22 +25,27 @@ function runSourceTest(body: string) { import { Group, Mesh, MeshBasicMaterial } from 'three' ${body} `, - ], - { cwd: resolve(import.meta.dir, '../../../../..'), stdout: 'pipe', stderr: 'pipe' }, - ) - expect({ code: result.exitCode, stderr: result.stderr.toString() }).toEqual({ - code: 0, - stderr: '', - }) + ) + const result = Bun.spawnSync([process.execPath, probePath], { + stdout: 'pipe', + stderr: 'pipe', + }) + expect({ code: result.exitCode, stderr: result.stderr.toString() }).toEqual({ + code: 0, + stderr: '', + }) + } finally { + rmSync(probeDir, { recursive: true, force: true }) + } } test('real slab top/side/skirt collection, shared defaults, transparent overrides and cache ownership', () => { runSourceTest(` - const sourceMaterials = await import('./packages/viewer/src/lib/materials.ts') + const sourceMaterials = await import(${sourcePath('packages/viewer/src/lib/materials.ts')}) mock.module('@pascal-app/viewer', () => ({ ...viewer, ...sourceMaterials })) - const { buildSlabGeometry } = await import('./packages/nodes/src/slab/geometry.ts') - const { collectBatchCandidate } = await import('./packages/nodes/src/shared/node-batch/candidates.ts') - const { disposeObject3DResources } = await import('./packages/viewer/src/lib/dispose-object3d.ts') + const { buildSlabGeometry } = await import(${sourcePath('packages/nodes/src/slab/geometry.ts')}) + const { collectBatchCandidate } = await import(${sourcePath('packages/nodes/src/shared/node-batch/candidates.ts')}) + const { disposeObject3DResources } = await import(${sourcePath('packages/viewer/src/lib/dispose-object3d.ts')}) const site = core.SiteNode.parse({ id: 'site_test', children: ['building_test'] }) const building = core.BuildingNode.parse({ id: 'building_test', parentId: site.id, children: ['level_test'] }) const level = core.LevelNode.parse({ id: 'level_test', parentId: building.id, level: 0, height: 2.5 }) @@ -103,8 +117,8 @@ test('priority-1 dirty snapshot sees the priority-2 ceiling rebuild and batches const react = await import('react') const refs = [] mock.module('react', () => ({ ...react, useEffect: () => {}, useRef: (value) => { const ref = { current: value }; refs.push(ref); return ref } })) - const { CeilingSystem, generateCeilingGeometry } = await import('./packages/viewer/src/systems/ceiling/ceiling-system.tsx') - const { NodeBatchSystem, runBatchFrame, resetNodeBatchState } = await import('./packages/nodes/src/shared/node-batch/system.tsx') + const { CeilingSystem, generateCeilingGeometry } = await import(${sourcePath('packages/viewer/src/systems/ceiling/ceiling-system.tsx')}) + const { NodeBatchSystem, runBatchFrame, resetNodeBatchState } = await import(${sourcePath('packages/nodes/src/shared/node-batch/system.tsx')}) let now = 0 performance.now = () => now const root = new Group() @@ -137,8 +151,8 @@ test('priority-1 dirty snapshot sees the priority-2 ceiling rebuild and batches NodeBatchSystem().type() assert.deepEqual(callbacks.map((pass) => pass.priority), [2, 1, 5]) const viewerStore = viewer.useViewer - mock.module('./packages/viewer/src/store/use-viewer.ts', () => ({ default: Object.assign((selector) => selector(viewerStore.getState()), viewerStore) })) - const { GeometrySystem } = await import('./packages/viewer/src/systems/geometry/geometry-system.tsx') + mock.module(${sourcePath('packages/viewer/src/store/use-viewer.ts')}, () => ({ default: Object.assign((selector) => selector(viewerStore.getState()), viewerStore) })) + const { GeometrySystem } = await import(${sourcePath('packages/viewer/src/systems/geometry/geometry-system.tsx')}) GeometrySystem() assert.equal(callbacks[3].priority, 2) const pipeline = callbacks.sort((a,b) => a.priority - b.priority) @@ -158,14 +172,14 @@ test('priority-1 dirty snapshot sees the priority-2 ceiling rebuild and batches }) const slabCacheFixture = ` - const sourceMaterials = await import('./packages/viewer/src/lib/materials.ts') + const sourceMaterials = await import(${sourcePath('packages/viewer/src/lib/materials.ts')}) const scene = core.useScene const viewerStore = viewer.useViewer viewerStore.setState({ bumpGeometryRevision: () => viewerStore.setState({ geometryRevision: viewerStore.getState().geometryRevision + 1 }) }) const selectorHook = (store) => Object.assign((selector) => selector(store.getState()), store) mock.module('@pascal-app/core', () => ({ ...core, useScene: selectorHook(scene), useRegistryVersion: () => 0 })) mock.module('@pascal-app/viewer', () => ({ ...viewer, ...sourceMaterials, useViewer: selectorHook(viewerStore) })) - mock.module('./packages/viewer/src/store/use-viewer.ts', () => ({ default: selectorHook(viewerStore) })) + mock.module(${sourcePath('packages/viewer/src/store/use-viewer.ts')}, () => ({ default: selectorHook(viewerStore) })) const fiber = await import('@react-three/fiber') const frames = [] mock.module('@react-three/fiber', () => ({ ...fiber, useThree: (selector) => selector({ gl: { domElement: {} }, invalidate: () => {} }), useFrame: (callback, priority) => frames.push({ callback, priority }) })) @@ -175,9 +189,9 @@ const slabCacheFixture = ` let refIndex = 0 const hooks = { useEffect: (effect) => effects.push(effect), useCallback: (callback) => callback, useRef: (value) => refs[refIndex++] ??= { current: value }, useSyncExternalStore: (_, snapshot) => snapshot(), useDebugValue: () => {} } mock.module('react', () => ({ ...react, ...hooks, default: { ...react.default, ...hooks } })) - const { buildSlabGeometry } = await import('./packages/nodes/src/slab/geometry.ts') - const { GeometrySystem } = await import('./packages/viewer/src/systems/geometry/geometry-system.tsx') - const { captureChangedNodes, runBatchFrame, subscribeBatchInteractions, resetNodeBatchState } = await import('./packages/nodes/src/shared/node-batch/system.tsx') + const { buildSlabGeometry } = await import(${sourcePath('packages/nodes/src/slab/geometry.ts')}) + const { GeometrySystem } = await import(${sourcePath('packages/viewer/src/systems/geometry/geometry-system.tsx')}) + const { captureChangedNodes, runBatchFrame, subscribeBatchInteractions, resetNodeBatchState } = await import(${sourcePath('packages/nodes/src/shared/node-batch/system.tsx')}) const preset = { ...core.MATERIAL_CATALOG[0], id: 'slab-cache-fixture', preset: { ...core.MATERIAL_CATALOG[0].preset, maps: {} } } core.registerLibraryMaterials([preset]) core.registerNode({ kind: 'slab', schemaVersion: 1, schema: core.SlabNode, geometry: buildSlabGeometry, capabilities: {} }) @@ -265,7 +279,7 @@ test('selected legacy slab cache clear invalidates saved originals before dispos runSourceTest( slabCacheFixture + ` - const { SelectionManager } = await import('./packages/editor/src/components/editor/selection-manager.tsx') + const { SelectionManager } = await import(${sourcePath('packages/editor/src/components/editor/selection-manager.tsx')}) const SelectionMaterialSync = SelectionManager().props.children[1].type effects = []; refs = []; refIndex = 0 viewerStore.setState({ selection: { ...viewerStore.getState().selection, selectedIds: ['slab_0'] } }) @@ -306,7 +320,7 @@ test('paint cancellation after cache clear never restores a disposed legacy slab runSourceTest( slabCacheFixture + ` - const { slabPaint } = await import('./packages/nodes/src/slab/paint.ts') + const { slabPaint } = await import(${sourcePath('packages/nodes/src/slab/paint.ts')}) const oldMesh = slabs[0].children[0] const original = oldMesh.material let disposed = false