158 lines
8.3 KiB
TypeScript
158 lines
8.3 KiB
TypeScript
import type { PtcProgramFormat } from '@deepseek-ai/dsh-ptc-runtime'
|
|
import { expect, it, onTestFinished, vi } from 'vitest'
|
|
import { Parser } from 'acorn'
|
|
import { createProgramEvaluator } from '../src/evaluators/index.ts'
|
|
import type { ProgramEvaluator } from '../src/evaluators/types.ts'
|
|
import { processTopLevelAwait } from '../src/evaluators/vm-script-await.ts'
|
|
|
|
type EvaluationResult = Parameters<Parameters<ProgramEvaluator['evaluate']>[1]>[0]
|
|
|
|
function evaluator(format: PtcProgramFormat = 'script') {
|
|
const instance = createProgramEvaluator(format, () => {})
|
|
onTestFinished(() => { instance.close() })
|
|
const run = (code: string) => new Promise<EvaluationResult>((resolve) => {
|
|
try { instance.evaluate(code, resolve) } catch (error: unknown) { resolve({ ok: false, error }) }
|
|
})
|
|
const value = async (code: string): Promise<unknown> => {
|
|
const result = await run(code)
|
|
if (!result.ok) throw result.error
|
|
return result.value
|
|
}
|
|
return { ...instance, run, value }
|
|
}
|
|
|
|
it.each<[string, unknown]>([
|
|
['1 + 2', 3],
|
|
['Promise.resolve(4)', 4],
|
|
['await(5)', 5],
|
|
['await\nPromise.resolve(6)', 6],
|
|
['(await Promise.resolve(7));;;', 7],
|
|
['await Promise.resolve(); Promise.resolve(8)', 8],
|
|
['await Promise.resolve(); let done = true;', undefined],
|
|
['await Promise.resolve(); if (true) { 9 }', undefined],
|
|
['#!script\nawait Promise.resolve(10) // trailing comment', 10],
|
|
['let sum = 0; for await (const item of [1, 2]) sum += item; sum', 3],
|
|
['for (var name in {a: 1}) { await Promise.resolve() }; name', 'a'],
|
|
['for (var item of [11]) { await Promise.resolve() }; item', 11],
|
|
['for (var{x} of [{x: 1}]) { await 0 }; x', 1],
|
|
['for (var[x] of [[2]]) { await 0 }; x', 2],
|
|
['for (var/* comment */x of [3]) { await 0 }; x', 3],
|
|
['for (var/* comment */key in {a: 1}) { await 0 }; key', 'a'],
|
|
['let first, second = await Promise.resolve(12); [typeof first, second]', ['undefined', 12]],
|
|
['const {a = 13, ...rest} = await Promise.resolve({b: 14}); const [, x, ...tail] = [0, 15, 16]; [a, rest.b, x, tail[0]]', [13, 14, 15, 16]],
|
|
['{ var local = await Promise.resolve(17) }; local', 17],
|
|
['function before() { return 18 }; await Promise.resolve(); before()', 18],
|
|
['class Box { [await Promise.resolve("value")]() { return 19 } }; new Box().value()', 19],
|
|
['class Box { static { this.value = 20 }; method() { return 1 } }; await Promise.resolve(); Box.value', 20],
|
|
['{ class Local {} }; await Promise.resolve(); typeof Local', 'undefined'],
|
|
['const fn = async function () { return await Promise.resolve(21) }; fn()', 21],
|
|
['const fn = async () => await Promise.resolve(22); fn()', 22],
|
|
['const obj = { [await Promise.resolve("value")]: 23 }; obj.value', 23],
|
|
['const {basename} = await import("node:path"); basename("/a/b")', 'b'],
|
|
['global === globalThis && typeof Buffer === "function" && typeof setTimeout === "function"', true],
|
|
])('evaluates Script completion: %s', async (code, expected) => {
|
|
expect(await evaluator().value(code)).toEqual(expected)
|
|
})
|
|
|
|
it('retains native and transformed declarations, closures, and class identity', async () => {
|
|
const e = evaluator()
|
|
await e.value('let count = 1; const box = {count}; function next() { return ++count }; class Native {}')
|
|
await e.value('const imported = await import("node:path"); class Awaited extends Native {}; function read() { return box };')
|
|
expect(await e.value('[next(), read() === box, new Awaited() instanceof Native, imported.basename("/a/b")]')).toEqual([2, true, true, 'b'])
|
|
expect(await e.run('let count = 9')).toHaveProperty('error.message', expect.stringContaining('already been declared'))
|
|
})
|
|
|
|
it('leaves a failed native lexical initializer uninitialized until its context is replaced', async () => {
|
|
const e = evaluator()
|
|
expect(await e.run('const poisoned = (() => { throw new Error("initializer failed") })()')).toMatchObject({ ok: false })
|
|
for (const code of ['poisoned', 'poisoned = 2', 'const poisoned = 3']) expect(await e.run(code)).toMatchObject({ ok: false })
|
|
expect(await e.value('const replacement = 4; replacement')).toBe(4)
|
|
expect(await evaluator().value('const poisoned = 5; poisoned')).toBe(5)
|
|
})
|
|
|
|
it.each([
|
|
'return 1', 'await Promise.resolve(); return 1', 'await Promise.resolve(); const = 1',
|
|
'await Promise.resolve();\nconst = 1', 'await Promise.resolve(); "unterminated',
|
|
'await Promise.resolve(); using resource = null;', 'await using resource = null;',
|
|
])('reports a complete-submission syntax error without transformer frames: %s', async (code) => {
|
|
const result = await evaluator().run(code)
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) {
|
|
expect(String(result.error)).toContain('SyntaxError')
|
|
expect(result.error).toHaveProperty('stack', expect.stringContaining('SyntaxError'))
|
|
expect(result.error).toHaveProperty('stack', expect.not.stringContaining('vm-script'))
|
|
expect(result.error).toHaveProperty('stack', expect.not.stringContaining('node:vm'))
|
|
}
|
|
})
|
|
|
|
it('keeps the native syntax location and caret without compiler stack frames', async () => {
|
|
const result = await evaluator().run('const =')
|
|
expect(result).toHaveProperty('error.stack', expect.stringMatching(/ptc-program\.js:1\nconst =\n\s*\^\n\nSyntaxError:/))
|
|
expect(result).toHaveProperty('error.stack', expect.not.stringContaining('vm-script'))
|
|
expect(result).toHaveProperty('error.stack', expect.not.stringContaining('node:vm'))
|
|
})
|
|
|
|
it('reports the syntax heading when the host stack formatter omits the stack', async () => {
|
|
const original = Object.getOwnPropertyDescriptor(Error, 'prepareStackTrace')
|
|
Object.defineProperty(Error, 'prepareStackTrace', { configurable: true, value: () => undefined })
|
|
try {
|
|
expect(await evaluator().run('const =')).toHaveProperty('error.stack', "SyntaxError: Unexpected token '='")
|
|
} finally {
|
|
if (original) Object.defineProperty(Error, 'prepareStackTrace', original)
|
|
else Reflect.deleteProperty(Error, 'prepareStackTrace')
|
|
}
|
|
})
|
|
|
|
it('keeps block-scoped disposable bindings local to the awaited evaluation', async () => {
|
|
const e = evaluator()
|
|
expect(await e.value('let disposed = false; { using item = { [Symbol.dispose]() { disposed = true } }; await Promise.resolve(); }; disposed')).toBe(true)
|
|
})
|
|
|
|
it('preserves thrown primitives and the original cross-realm error', async () => {
|
|
const e = evaluator()
|
|
expect(await e.run('throw "failure"')).toEqual({ ok: false, error: 'failure' })
|
|
const original = await e.value('const error = new Error("hidden"); Object.defineProperty(error, "stack", { get() { throw 1 } }); error')
|
|
const result = await e.run('throw error')
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.error).toBe(original)
|
|
})
|
|
|
|
it.each(['const invalid = ; await 1\n', 'await\\a\n', 'await = 1\n'])('leaves earlier or ambiguous syntax to native compilation: %s', (code) => {
|
|
expect(processTopLevelAwait(code)).toBeNull()
|
|
})
|
|
|
|
it('does not transform nested await or a top-level return', () => {
|
|
expect(processTopLevelAwait('async function nested() { await 1 }\n')).toBeNull()
|
|
expect(processTopLevelAwait('return await 1\n')).toBeNull()
|
|
expect(processTopLevelAwait('\n')).toBeNull()
|
|
})
|
|
|
|
it('runs function-body parameters and Promise rejection through the evaluator entry', async () => {
|
|
const e = evaluator('function-body')
|
|
e.globals.answer = 42
|
|
expect(await e.value('return answer')).toBe(42)
|
|
expect(await e.run('throw new Error("rejected")')).toMatchObject({ ok: false, error: { message: 'rejected' } })
|
|
expect(await e.run('const =')).toHaveProperty('error', expect.any(SyntaxError))
|
|
})
|
|
|
|
it('falls back to native errors for input escaping the async wrapper', () => {
|
|
expect(processTopLevelAwait('}); (async () => {\n')).toBeNull()
|
|
expect(processTopLevelAwait('}).call(null); (async () => {\n')).toBeNull()
|
|
})
|
|
|
|
it('reports an unfinished expression at the end of submitted input', () => {
|
|
expect(() => processTopLevelAwait('await null; (\n')).toThrow(SyntaxError)
|
|
})
|
|
|
|
it('preserves parser infrastructure failures without rewriting them as syntax errors', async () => {
|
|
const failure = new Error('parser unavailable')
|
|
const parse = vi.spyOn(Parser, 'parse').mockImplementation(() => { throw failure })
|
|
try {
|
|
expect(() => processTopLevelAwait('await 1\n')).toThrow(failure)
|
|
const result = await evaluator().run('await 1')
|
|
expect(result.ok).toBe(false)
|
|
if (!result.ok) expect(result.error).toBe(failure)
|
|
}
|
|
finally { parse.mockRestore() }
|
|
})
|