import assert from 'node:assert/strict'; import { readFile, mkdtemp, writeFile, rm } from 'node:fs/promises'; import { test } from 'node:test'; import vm from 'node:vm'; import { pathToFileURL } from 'node:url'; import ts from 'typescript'; const gpu = { device: 'webgpu', dtype: 'fp32' }; const wasm = { device: 'wasm', dtype: 'q8' }; async function providerHarness(capture) { const sourceUrl = new URL('../src/depth/AiDepthProvider.ts', import.meta.url); const directory = await mkdtemp(new URL('../.ai-depth-provider-test-', import.meta.url)); const source = (await readFile(sourceUrl, 'utf8')).replace("'./DepthProcessor'", JSON.stringify(new URL('../src/depth/DepthProcessor.ts', import.meta.url).href)); const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.ESNext } }); await writeFile(`${directory}/provider.mjs`, outputText); const originalWorker = globalThis.Worker; const originalCapture = globalThis.createImageBitmap; const workers = []; const bitmaps = []; globalThis.Worker = class { constructor() { this.messages = []; this.terminated = false; workers.push(this); } postMessage(message) { this.messages.push(message); } terminate() { this.terminated = true; } }; globalThis.createImageBitmap = capture ?? (async () => { const bitmap = { width: 10, height: 10, closed: false, close() { this.closed = true; } }; bitmaps.push(bitmap); return bitmap; }); const { AiDepthProvider } = await import(pathToFileURL(`${directory}/provider.mjs`).href); const statuses = []; const provider = new AiDepthProvider((status) => statuses.push(status)); return { provider, workers, statuses, bitmaps, async cleanup() { provider.cancel(); globalThis.Worker = originalWorker; globalThis.createImageBitmap = originalCapture; await rm(directory, { recursive: true, force: true }); } }; } const photo = { naturalWidth: 100, naturalHeight: 50 }; const tick = async () => { await Promise.resolve(); await Promise.resolve(); }; function replyWithResult(worker, run) { const id = worker.messages.at(-1).id; worker.onmessage({ data: { type: 'result', id, width: 1, height: 1, values: Uint8Array.of(255).buffer, normalizedValues: Float32Array.of(1).buffer, run } }); } test('provider sends a fixed-model request and preserves measured execution metadata', async () => { const h = await providerHarness(); try { const pending = h.provider.getDepthMap(photo); await tick(); const worker = h.workers[0]; const requestKeys = Object.keys(worker.messages[0]).sort(); replyWithResult(worker, { model: 'small', ...gpu, preparationMs: 5, inferenceMs: 7, totalMs: 12 }); const result = await pending; assert.deepEqual(requestKeys, ['id', 'image', 'type']); assert.equal(result.userData.aiDepthRun.model, 'small'); assert.equal(result.userData.aiDepthRun.device, 'webgpu'); assert.equal(result.userData.aiDepthRun.dtype, 'fp32'); assert.equal(result.userData.aiDepthRun.preparationMs, 5); assert.equal(result.userData.aiDepthRun.inferenceMs, 7); assert.ok(result.userData.aiDepthRun.totalMs >= 0); assert.equal(result.userData.normalizedDepth[0], 1); assert.equal(worker.terminated, true, 'finished inference must release its runtime before scene rendering resumes'); result.dispose(); } finally { await h.cleanup(); } }); test('provider cancellation is an AbortError and late captures are closed without spawning workers', async () => { let resolveCapture; const bitmap = { width: 10, height: 5, closed: false, close() { this.closed = true; } }; const h = await providerHarness(() => new Promise((resolve) => { resolveCapture = resolve; })); try { const pending = h.provider.getDepthMap(photo); const rejected = assert.rejects(pending, { name: 'AbortError' }); h.provider.cancel(); resolveCapture(bitmap); await rejected; await tick(); assert.equal(bitmap.closed, true); assert.equal(h.workers.length, 0); } finally { await h.cleanup(); } }); test('orphaned worker errors and messages cannot reject or settle a replacement request', async () => { const h = await providerHarness(); try { const first = h.provider.getDepthMap(photo); const firstRejected = assert.rejects(first); await tick(); const oldWorker = h.workers[0]; h.provider.cancel(); await firstRejected; const second = h.provider.getDepthMap(photo); // Attach immediately so a failing implementation does not create an unhandled rejection. const settled = second.then((value) => ({ value }), (error) => ({ error })); await tick(); const worker = h.workers[1]; oldWorker.onerror({ message: 'late old worker failure' }); oldWorker.onmessageerror({}); oldWorker.onmessage({ data: { type: 'status', id: worker.messages[0].id, message: 'orphaned status' } }); replyWithResult(worker, { model: 'small', ...wasm, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); const result = await settled; assert.equal(result.error, undefined, 'orphaned callbacks must be ignored'); assert.equal(h.statuses.includes('orphaned status'), false); assert.equal(worker.terminated, true, 'the valid completed worker is released'); result.value.dispose(); } finally { await h.cleanup(); } }); test('completed inference releases only its own worker and late replies cannot terminate a newer request', async () => { const h = await providerHarness(); try { const first = h.provider.getDepthMap(photo); await tick(); const old = h.workers[0]; replyWithResult(old, { model: 'small', ...gpu, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); const firstTexture = await first; assert.equal(old.terminated, true); const next = h.provider.getDepthMap(photo); await tick(); const current = h.workers[1]; replyWithResult(old, { model: 'small', ...gpu, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); old.onerror({ message: 'late old failure' }); assert.equal(current.terminated, false); replyWithResult(current, { model: 'small', ...wasm, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); const texture = await next; assert.equal(current.terminated, true); assert.equal(texture.userData.aiDepthRun.device, 'wasm'); firstTexture.dispose(); texture.dispose(); } finally { await h.cleanup(); } }); async function workerHarness({ hasGpu = true, adapter = {}, adapterError = false, missingAdapterMethod = false, failLoad = () => false, failInference = () => false, beforeLoad = async () => {} } = {}) { const source = await readFile(new URL('../src/depth/aiDepth.worker.ts', import.meta.url), 'utf8'); const { outputText } = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS } }); const constants = await import(new URL('../src/constants/depth.ts', import.meta.url)); let now = 0; let listener; let liveSessions = 0; let adapterProbes = 0; let maximumSessions = 0; const loads = []; const sessions = []; const messages = []; const terminals = new Map(); const pipeline = async (_task, model, options) => { loads.push({ model, device: options.device, dtype: options.dtype }); now += 5; options.progress_callback({ status: 'initiate' }); options.progress_callback({ status: 'progress', progress: 50 }); await beforeLoad(options); if (failLoad(model, options)) throw new Error('load failure'); liveSessions++; maximumSessions = Math.max(maximumSessions, liveSessions); const session = async () => { now += 7; if (failInference(model, options)) throw new Error('inference failure'); const output = { dims: [1, 1, 2], data: Float32Array.of(1, 3), dispose() { session.tensorDisposals++; } }; return { predicted_depth: output }; }; session.processor = { image_processor: { size: { width: 518, height: 518 } } }; session.disposals = 0; session.tensorDisposals = 0; session.dispose = async () => { now += 2; session.disposals++; liveSessions--; }; sessions.push(session); return session; }; vm.runInNewContext(outputText, { exports: {}, Error, require(name) { if (name === '@huggingface/transformers') return { pipeline }; if (name === '../constants/depth') return constants; throw new Error(`unexpected worker dependency ${name}`); }, performance: { now: () => now }, navigator: hasGpu ? { gpu: missingAdapterMethod ? {} : { async requestAdapter() { adapterProbes++; if (adapterError) throw new Error('adapter unavailable'); return adapter; } } } : {}, OffscreenCanvas: class { getContext() { return { drawImage() {} }; } }, self: { addEventListener(_type, callback) { listener = callback; }, postMessage(message) { messages.push(message); if (message.type !== 'status') terminals.get(message.id)?.(message); }, }, }); let requestId = 0; return { loads, sessions, messages, get maximumSessions() { return maximumSessions; }, get adapterProbes() { return adapterProbes; }, request(legacyFields = {}) { const id = ++requestId; const bitmap = { width: 10, height: 5, closed: 0, close() { this.closed++; } }; return new Promise((resolve) => { terminals.set(id, (message) => { terminals.delete(id); resolve({ message, bitmap }); }); listener({ data: { type: 'infer', id, image: bitmap, ...legacyFields } }); }); }, }; } test('worker uses Small 770 with WASM/q8 when WebGPU is absent', async () => { const h = await workerHarness({ hasGpu: false }); const { message, bitmap } = await h.request(); assert.equal(message.type, 'result'); assert.deepEqual(h.loads, [{ model: 'onnx-community/depth-anything-v2-small-ONNX', ...wasm }]); assert.deepEqual({ ...message.run }, { model: 'small', ...wasm, preparationMs: 5, inferenceMs: 7, totalMs: 12 }); assert.deepEqual({ ...h.sessions[0].processor.image_processor.size }, { width: 770, height: 770 }); assert.deepEqual([...new Uint8Array(message.values)], [0, 255]); assert.deepEqual([...new Float32Array(message.normalizedValues)], [0, 1]); assert.equal(h.sessions[0].tensorDisposals, 1); assert.equal(bitmap.closed, 1); }); test('a stale model-selection field cannot load another model or override execution', async () => { const h = await workerHarness(); const { message } = await h.request({ model: 'base', preset: wasm }); assert.equal(message.type, 'result'); assert.deepEqual(h.loads, [{ model: 'onnx-community/depth-anything-v2-small-ONNX', ...gpu }]); assert.equal(message.run.model, 'small'); }); test('worker reuses the fixed Small session on repeated requests', async () => { const h = await workerHarness(); await h.request(); const reused = await h.request(); assert.equal(h.loads.length, 1); assert.equal(h.adapterProbes, 1, 'cached session does not repeatedly probe GPU'); assert.equal(h.maximumSessions, 1); assert.equal(h.sessions[0].disposals, 0); assert.equal(reused.message.run.preparationMs, 0); assert.equal(reused.message.run.inferenceMs, 7); }); test('GPU inference failure disposes and requests an isolated WASM retry', async () => { const h = await workerHarness({ failInference: (_model, preset) => preset.device === 'webgpu' }); const { message, bitmap } = await h.request(); assert.equal(message.type, 'fallback'); assert.deepEqual(h.loads, [{ model: 'onnx-community/depth-anything-v2-small-ONNX', ...gpu }]); assert.equal(h.sessions[0].disposals, 1); assert.equal(message.preparationMs, 7); assert.equal(message.inferenceMs, 7); assert.equal(bitmap.closed, 1); }); test('GPU loading failure requests fresh worker and retains failed preparation time', async () => { const h = await workerHarness({ failLoad: (_model, preset) => preset.device === 'webgpu' }); const { message } = await h.request(); assert.equal(message.type, 'fallback'); assert.equal(message.preparationMs, 5); assert.equal(message.inferenceMs, 0); assert.equal(h.loads.length, 1, 'must not retry inside a poisoned worker'); }); test('a present GPU API with null or rejected adapter selects WASM directly', async () => { for (const options of [{ adapter: null }, { adapterError: true }, { missingAdapterMethod: true }]) { const h = await workerHarness(options); const { message } = await h.request(); assert.equal(message.type, 'result'); assert.equal(message.run.device, 'wasm'); assert.deepEqual(h.loads, [{ model: 'onnx-community/depth-anything-v2-small-ONNX', ...wasm }]); } }); test('forced fallback worker bypasses GPU and retains Small 770', async () => { const h = await workerHarness(); const { message } = await h.request({ forceWasm: true }); assert.equal(message.type, 'result'); assert.equal(message.run.device, 'wasm'); assert.deepEqual(h.loads, [{ model: 'onnx-community/depth-anything-v2-small-ONNX', ...wasm }]); assert.equal(h.sessions[0].processor.image_processor.size.width, 770); assert.equal(h.adapterProbes, 0, 'forced CPU retry cannot touch GPU again'); }); test('terminal WASM failure closes the input and returns an error without more retries', async () => { const h = await workerHarness({ hasGpu: false, failInference: () => true }); const { message, bitmap } = await h.request(); assert.equal(message.type, 'error'); assert.equal(message.message, 'inference failure'); assert.equal(h.loads.length, 1); assert.equal(bitmap.closed, 1); }); test('worker labels file progress without implying a cold download', async () => { const h = await workerHarness(); await h.request(); const statuses = h.messages.filter(({ type }) => type === 'status').map(({ message }) => message); assert.ok(statuses.includes('출발 준비 중 · 50%')); assert.ok(statuses.includes('내비게이션으로 길을 찾는 중…')); assert.ok(statuses.every((message) => !/AI|깊이|모델|WebGPU|CPU|계산/i.test(message))); assert.ok(statuses.every((message) => !/다운로드|download/i.test(message))); }); test('provider retries once in a fresh worker and ignores orphan messages', async () => { const h = await providerHarness(); try { const pending = h.provider.getDepthMap(photo); const settled = pending.then(value => ({ value }), error => ({ error })); await tick(); const old = h.workers[0], id = old.messages[0].id; old.onmessage({ data: { type: 'fallback', id, preparationMs: 5, inferenceMs: 7 } }); await tick(); assert.equal(h.statuses.at(-1), '다른 길을 찾고 있어요. 조금만 더 기다려 주세요…'); assert.equal(h.workers.length, 2, 'fallback requires a new JS realm'); const fallback = h.workers[1]; assert.equal(old.terminated, true); assert.equal(fallback.messages[0].forceWasm, true); old.onmessage({ data: { type: 'error', id, message: 'late GPU failure' } }); old.onerror({ message: 'late crash' }); replyWithResult(fallback, { model: 'small', ...wasm, preparationMs: 10, inferenceMs: 20, totalMs: 30 }); const result = await settled; assert.equal(result.error, undefined); assert.equal(result.value.userData.aiDepthRun.preparationMs, 15); assert.equal(result.value.userData.aiDepthRun.inferenceMs, 27); assert.equal(h.workers.length, 2); result.value.dispose(); } finally { await h.cleanup(); } }); test('fallback cannot loop and terminal failures discard the poisoned runtime', async () => { const h = await providerHarness(); try { const pending = h.provider.getDepthMap(photo); const settled = pending.then(value => ({ value }), error => ({ error })); await tick(); const old = h.workers[0], id = old.messages[0].id; old.onmessage({ data: { type: 'fallback', id, preparationMs: 5, inferenceMs: 0 } }); await tick(); assert.equal(h.workers.length, 2); const fallback = h.workers[1]; fallback.onmessage({ data: { type: 'fallback', id, preparationMs: 5, inferenceMs: 0 } }); const result = await settled; assert.ok(result.error); assert.equal(h.workers.length, 2); assert.equal(fallback.terminated, true); } finally { await h.cleanup(); } }); test('photo replacement during fallback capture closes stale bitmap without replacing new worker', async () => { let captures = 0, release; const h = await providerHarness(async () => { captures++; if (captures === 2) return new Promise(resolve => { release = resolve; }); return { width: 10, height: 5, close() {} }; }); try { const pending = h.provider.getDepthMap(photo); const rejected = assert.rejects(pending, { name: 'AbortError' }); await tick(); const old = h.workers[0], id = old.messages[0].id; old.onmessage({ data: { type: 'fallback', id, preparationMs: 5, inferenceMs: 0 } }); await tick(); assert.equal(typeof release, 'function', 'fallback recaptures the requested photo'); h.provider.cancel(); await rejected; const replacement = h.provider.getDepthMap({ naturalWidth: 50, naturalHeight: 100 }); await tick(); assert.equal(h.workers.length, 2); const current = h.workers[1]; const stale = { width: 10, height: 5, closed: 0, close() { this.closed++; } }; release(stale); await tick(); assert.equal(stale.closed, 1); assert.equal(h.workers.length, 2); assert.equal(old.terminated, true); replyWithResult(current, { model: 'small', ...wasm, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); (await replacement).dispose(); } finally { await h.cleanup(); } }); test('serialized terminal error also discards runtime so manual retry starts fresh', async () => { const h = await providerHarness(); try { const pending = h.provider.getDepthMap(photo); const rejected = assert.rejects(pending, /WASM failed/); await tick(); const old = h.workers[0], id = old.messages[0].id; old.onmessage({ data: { type: 'error', id, message: 'WASM failed' } }); await rejected; assert.equal(old.terminated, true); const retry = h.provider.getDepthMap(photo); await tick(); assert.equal(h.workers.length, 2); replyWithResult(h.workers[1], { model: 'small', ...wasm, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); (await retry).dispose(); } finally { await h.cleanup(); } }); // Execute the installed dependency's actual initialization chain, replacing only ORT's // slow hardware boundary. A rejected GPU load prevents same-realm WASM from running. async function dependencyRuntime() { const source = await readFile(new URL('../node_modules/@huggingface/transformers/src/backends/onnx.js', import.meta.url), 'utf8'); const start = source.indexOf('export async function createInferenceSession('); assert.ok(start >= 0); const body = source.slice(start, source.indexOf('\n/**', start)).replace('export ', ''); const calls = []; const context = { ensureWasmLoaded: async () => {}, getOnnxLogSeverityLevel: () => 2, env: {}, LogLevel: { WARNING: 2 }, apis: { IS_WEB_ENV: true }, InferenceSession: { async create(_buffer, options) { const device = options.executionProviders[0]; calls.push(device); if (device === 'webgpu') throw new Error('Failed to get GPU adapter'); return {}; } }, }; vm.createContext(context); vm.runInContext('let webInitChain = Promise.resolve();' + body + ';this.create = createInferenceSession;', context); return { calls, create: preset => context.create(new Uint8Array(), { executionProviders: [preset.device] }, {}) }; } test('real dependency rejected chain is isolated before WASM fallback', async () => { const poisoned = await dependencyRuntime(); const gpuWorker = await workerHarness({ beforeLoad: poisoned.create }); const failed = await gpuWorker.request(); assert.equal(failed.message.type, 'fallback'); assert.deepEqual(poisoned.calls, ['webgpu']); await assert.rejects(poisoned.create(wasm), /Failed to get GPU adapter/); assert.deepEqual(poisoned.calls, ['webgpu'], 'same-realm fallback is skipped by the dependency, matching the reported mechanism'); const clean = await dependencyRuntime(); const cpuWorker = await workerHarness({ beforeLoad: clean.create }); const recovered = await cpuWorker.request({ forceWasm: true }); assert.equal(recovered.message.type, 'result'); assert.equal(recovered.message.run.device, 'wasm'); assert.deepEqual(clean.calls, ['wasm']); await cpuWorker.request(); assert.deepEqual(clean.calls, ['wasm'], 'successful WASM session is reused'); }); test('duplicate and stale fallback replies cannot replace the active CPU worker', async () => { const h = await providerHarness(); try { const pending = h.provider.getDepthMap(photo); await tick(); const old = h.workers[0], id = old.messages[0].id; const fallbackReply = { data: { type: 'fallback', id, preparationMs: 5, inferenceMs: 0 } }; old.onmessage(fallbackReply); old.onmessage(fallbackReply); await tick(); assert.equal(h.workers.length, 2); const current = h.workers[1]; current.onmessage({ data: { type: 'fallback', id: id - 1, preparationMs: 0, inferenceMs: 0 } }); assert.equal(current.terminated, false); assert.equal(h.workers.length, 2); replyWithResult(current, { model: 'small', ...wasm, preparationMs: 1, inferenceMs: 2, totalMs: 3 }); (await pending).dispose(); } finally { await h.cleanup(); } }); test('Worker constructor failure closes the captured bitmap', async () => { const h = await providerHarness(); try { globalThis.Worker = class { constructor() { throw new Error('worker unavailable'); } }; await assert.rejects(h.provider.getDepthMap(photo), /worker unavailable/); assert.equal(h.bitmaps[0].closed, true); } finally { await h.cleanup(); } });