diff --git a/tests/fixtures/process_stdout_resize_child.cjs b/tests/fixtures/process_stdout_resize_child.cjs new file mode 100644 index 0000000..f52c72b --- /dev/null +++ b/tests/fixtures/process_stdout_resize_child.cjs @@ -0,0 +1,16 @@ +let resizeCount = 0; + +process.stdout.on('resize', () => { + resizeCount++; + console.log(`RESIZE ${process.stdout.rows} ${process.stdout.columns}`); + setTimeout(() => process.exit(0), 25); +}); + +console.log('READY'); + +setTimeout(() => { + if (resizeCount === 0) { + console.error('resize event did not fire'); + process.exit(2); + } +}, 3000); diff --git a/tests/test_debug_error_trace.cjs b/tests/test_debug_error_trace.cjs new file mode 100644 index 0000000..e8018f7 --- /dev/null +++ b/tests/test_debug_error_trace.cjs @@ -0,0 +1,74 @@ +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +function assert(condition, message) { + if (!condition) throw new Error(message); +} + +function runCase(bin, dir, file, source) { + const scriptPath = path.join(dir, file); + fs.writeFileSync(scriptPath, source); + + const result = spawnSync(bin, [scriptPath], { + env: { ...process.env, ANT_DEBUG: 'dump/errors:trace' }, + encoding: 'utf8', + }); + + if (result.error) throw result.error; + return { scriptPath, ...result }; +} + +const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ant-debug-errors-')); +const bin = path.resolve(__dirname, '..', 'build', 'ant'); + +const thrown = runCase( + bin, + tmpDir, + 'throw-project-path.cjs', + "throw new Error('Project path is required');\n" +); +assert(thrown.status !== 0, 'throw case should fail'); +assert( + thrown.stderr.includes('[ant-debug:error] throw Error: Project path is required'), + `expected throw trace in stderr\n${thrown.stderr}` +); +assert( + thrown.stderr.includes(`site: ${thrown.scriptPath}:1:`), + `expected throw site in stderr\n${thrown.stderr}` +); + +const missing = runCase( + bin, + tmpDir, + 'missing-bailing.cjs', + "console.log(bailing);\n" +); +assert(missing.status !== 0, 'missing identifier case should fail'); +assert( + missing.stderr.includes('[ant-debug:error] create ReferenceError'), + `expected ReferenceError trace in stderr\n${missing.stderr}` +); +assert( + missing.stderr.includes('bailing'), + `expected missing identifier name in stderr\n${missing.stderr}` +); + +const constAssign = runCase( + bin, + tmpDir, + 'const-assign.cjs', + "const value = 1;\nvalue = 2;\n" +); +assert(constAssign.status !== 0, 'const assignment case should fail'); +assert( + constAssign.stderr.includes('[ant-debug:error] create TypeError: Assignment to constant variable'), + `expected const assignment trace in stderr\n${constAssign.stderr}` +); +assert( + constAssign.stderr.includes(`site: ${constAssign.scriptPath}:2:`), + `expected const assignment site in stderr\n${constAssign.stderr}` +); + +console.log('debug error trace test passed'); diff --git a/tests/test_inspect_custom.cjs b/tests/test_inspect_custom.cjs index ce0c62d..38a29fc 100644 --- a/tests/test_inspect_custom.cjs +++ b/tests/test_inspect_custom.cjs @@ -32,14 +32,40 @@ class FallbackConnection { const blob = new Blob(['hi'], { type: 'text/plain' }); const file = new File(['hi'], 'note.txt', { type: 'text/plain', lastModified: 42 }); +const headers = new Headers({ 'content-type': 'text/plain' }); +const request = new Request('https://google.com'); +const response = new Response('ok', { headers }); const timeout = setTimeout(() => {}, 1); const interval = setInterval(() => {}, 5); +const headersInspect = inspect(headers); const timeoutInspect = inspect(timeout); const intervalInspect = inspect(interval); +const requestInspect = inspect(request); +const responseInspect = inspect(response); + +assert(typeof Headers.prototype[Symbol.inspect] === 'function', 'expected Headers.prototype[Symbol.inspect] to exist'); +assert(typeof Request.prototype[Symbol.inspect] === 'function', 'expected Request.prototype[Symbol.inspect] to exist'); +assert(typeof Response.prototype[Symbol.inspect] === 'function', 'expected Response.prototype[Symbol.inspect] to exist'); assert(inspect(new Connection('localhost', 3000, 'open')) === 'Connection { localhost:3000 (open) }', 'expected custom inspect result'); assert(inspect(blob) === "Blob { size: 2, type: 'text/plain' }", 'expected Blob custom inspect output'); assert(inspect(file) === "File { size: 2, type: 'text/plain', name: 'note.txt', lastModified: 42 }", 'expected File custom inspect output'); +assert( + Headers.prototype[Symbol.inspect].call(headers) === headersInspect, + `expected Headers Symbol.inspect output, got: ${Headers.prototype[Symbol.inspect].call(headers)}` +); +assert( + requestInspect === "Request {\n method: 'GET',\n url: 'https://google.com/',\n headers: Headers {},\n destination: '',\n referrer: 'about:client',\n referrerPolicy: '',\n mode: 'cors',\n credentials: 'same-origin',\n cache: 'default',\n redirect: 'follow',\n integrity: '',\n keepalive: false,\n isReloadNavigation: false,\n isHistoryNavigation: false,\n signal: AbortSignal { aborted: false }\n}", + `expected Request inspect output, got: ${requestInspect}` +); +assert( + Request.prototype[Symbol.inspect].call(request) === requestInspect, + `expected Request Symbol.inspect output, got: ${Request.prototype[Symbol.inspect].call(request)}` +); +assert( + Response.prototype[Symbol.inspect].call(response) === responseInspect, + `expected Response Symbol.inspect output, got: ${Response.prototype[Symbol.inspect].call(response)}` +); assert( timeoutInspect === 'Timeout (1) {\n delay: 1,\n repeat: null,\n [Symbol(Symbol.toPrimitive)]: [native code]\n}', `expected legacy Timeout inspect output, got: ${timeoutInspect}` diff --git a/tests/test_object_fromentries_iterables.cjs b/tests/test_object_fromentries_iterables.cjs new file mode 100644 index 0000000..cd883ac --- /dev/null +++ b/tests/test_object_fromentries_iterables.cjs @@ -0,0 +1,63 @@ +const assert = require("node:assert"); + +assert.deepStrictEqual( + Object.fromEntries(new Map([ + ["a", 1], + ["b", 2], + ])), + { a: 1, b: 2 }, +); + +function* pairGenerator() { + yield ["x", 10]; + yield ["y", 20]; +} + +assert.deepStrictEqual( + Object.fromEntries(pairGenerator()), + { x: 10, y: 20 }, +); + +const iterable = { + [Symbol.iterator]: function* () { + yield ["left", "right"]; + yield ["up", "down"]; + }, +}; + +assert.deepStrictEqual( + Object.fromEntries(iterable), + { left: "right", up: "down" }, +); + +class RequestContextLike { + constructor() { + this.registry = new Map(); + } + + set(key, value) { + this.registry.set(key, value); + } + + entries() { + return this.registry.entries(); + } +} + +const ctx = new RequestContextLike(); +ctx.set("harness", { + state: { projectPath: "/tmp/project" }, + getState() { + return this.state; + }, +}); + +const snapshot = Object.fromEntries(ctx.entries()); +assert.strictEqual(snapshot.harness.getState().projectPath, "/tmp/project"); + +assert.throws( + () => Object.fromEntries((function* () { yield 1; })()), + /entry objects/, +); + +console.log("Object.fromEntries consumes Map iterators, generators, and custom iterables"); diff --git a/tests/test_process_stdout_resize_signal.cjs b/tests/test_process_stdout_resize_signal.cjs new file mode 100644 index 0000000..f72b3f0 --- /dev/null +++ b/tests/test_process_stdout_resize_signal.cjs @@ -0,0 +1,108 @@ +const { spawnSync } = require('child_process'); +const path = require('path'); + +function fail(message) { + throw new Error(message); +} + +function runInPty() { + const helper = path.join(__dirname, 'fixtures', 'process_stdout_resize_child.cjs'); + const script = ` +import fcntl, os, select, signal, struct, sys, termios, time + +exec_path, helper = sys.argv[1], sys.argv[2] +pid, master = os.forkpty() + +if pid == 0: + os.execv(exec_path, [exec_path, helper]) + +buf = bytearray() +sent = False +exit_code = None +deadline = time.time() + 7.0 + +while time.time() < deadline: + done, status = os.waitpid(pid, os.WNOHANG) + if done == pid: + exit_code = os.waitstatus_to_exitcode(status) + break + + r, _, _ = select.select([master], [], [], 0.1) + if master not in r: + continue + + try: + chunk = os.read(master, 4096) + except OSError: + break + + if not chunk: + break + + buf.extend(chunk) + if (not sent) and b'READY' in buf: + winsz = struct.pack('HHHH', 40, 120, 0, 0) + fcntl.ioctl(master, termios.TIOCSWINSZ, winsz) + os.kill(pid, signal.SIGWINCH) + sent = True + +if exit_code is None: + os.kill(pid, signal.SIGKILL) + _, status = os.waitpid(pid, 0) + exit_code = os.waitstatus_to_exitcode(status) + +while True: + r, _, _ = select.select([master], [], [], 0.05) + if master not in r: + break + try: + chunk = os.read(master, 4096) + except OSError: + break + if not chunk: + break + buf.extend(chunk) + +sys.stdout.buffer.write(bytes(buf)) +sys.exit(exit_code) +`; + + if (process.platform === 'win32') { + console.log('skipping stdout resize signal test on win32'); + process.exit(0); + } + + return spawnSync('python3', ['-c', script, process.execPath, helper], { + encoding: 'utf8', + timeout: 9000, + }); +} + +const result = runInPty(); + +if (result.error && result.error.code === 'ENOENT') { + console.log('skipping stdout resize signal test because `python3` is unavailable'); + process.exit(0); +} + +if (result.error) throw result.error; + +const output = `${result.stdout || ''}${result.stderr || ''}`; + +if (result.status !== 0) { + fail(`child exited ${result.status}\n${output}`); +} + +if (!output.includes('READY')) { + fail(`expected READY banner\n${output}`); +} + +if (!output.includes('RESIZE 40 120')) { + fail(`expected resize output with updated tty dimensions\n${output}`); +} + +if (output.includes('assignment to constant')) { + fail(`resize handling regressed with assignment-to-constant error\n${output}`); +} + +console.log('process.stdout resize signal updates dimensions without crashing'); diff --git a/tests/test_proxy_object_entries.cjs b/tests/test_proxy_object_entries.cjs new file mode 100644 index 0000000..3b3393a --- /dev/null +++ b/tests/test_proxy_object_entries.cjs @@ -0,0 +1,37 @@ +const assert = require("node:assert"); + +let getCalls = 0; + +const proxy = new Proxy({}, { + ownKeys() { + return ["kimi-for-coding", "hidden", Symbol("skip")]; + }, + getOwnPropertyDescriptor(_target, prop) { + if (prop === "kimi-for-coding") { + return { enumerable: true, configurable: true }; + } + if (prop === "hidden") { + return { enumerable: false, configurable: true }; + } + if (typeof prop === "symbol") { + return { enumerable: true, configurable: true }; + } + }, + get(_target, prop) { + getCalls++; + if (prop === "kimi-for-coding") { + return { apiKeyEnvVar: "KIMI_API_KEY" }; + } + if (prop === "hidden") { + return { apiKeyEnvVar: "HIDDEN_API_KEY" }; + } + return { apiKeyEnvVar: "SYMBOL_API_KEY" }; + } +}); + +assert.deepStrictEqual(Object.keys(proxy), ["kimi-for-coding"]); +assert.deepStrictEqual(Object.values(proxy), [{ apiKeyEnvVar: "KIMI_API_KEY" }]); +assert.deepStrictEqual(Object.entries(proxy), [["kimi-for-coding", { apiKeyEnvVar: "KIMI_API_KEY" }]]); +assert.strictEqual(getCalls, 2); + +console.log("proxy Object.keys/Object.values/Object.entries respect proxy traps"); diff --git a/tests/test_regexp_exec_fast_paths.cjs b/tests/test_regexp_exec_fast_paths.cjs index 1c1e452..015d47f 100644 --- a/tests/test_regexp_exec_fast_paths.cjs +++ b/tests/test_regexp_exec_fast_paths.cjs @@ -59,4 +59,50 @@ assert( 'custom exec truthiness lowering should preserve getter/arg/call order' ); +const testGets = []; +const testProxy = new Proxy( + { + exec() { + return null; + } + }, + { + get(target, key) { + testGets.push(key); + return target[key]; + } + } +); + +assert(RegExp.prototype.test.call(testProxy) === false, 'proxy-backed test should return false'); +assert(testGets.join(',') === 'exec', 'RegExp.prototype.test should only Get("exec") once'); + +const flagKeys = []; +const flagProxy = new Proxy( + {}, + { + get(target, key) { + flagKeys.push(key); + return target[key]; + } + } +); + +Object.getOwnPropertyDescriptor(RegExp.prototype, 'flags').get.call(flagProxy); + +const expectedFlagKeys = []; +if ('hasIndices' in RegExp.prototype) expectedFlagKeys.push('hasIndices'); +if ('global' in RegExp.prototype) expectedFlagKeys.push('global'); +if ('ignoreCase' in RegExp.prototype) expectedFlagKeys.push('ignoreCase'); +if ('multiline' in RegExp.prototype) expectedFlagKeys.push('multiline'); +if ('dotAll' in RegExp.prototype) expectedFlagKeys.push('dotAll'); +if ('unicode' in RegExp.prototype) expectedFlagKeys.push('unicode'); +if ('unicodeSets' in RegExp.prototype) expectedFlagKeys.push('unicodeSets'); +if ('sticky' in RegExp.prototype) expectedFlagKeys.push('sticky'); + +assert( + flagKeys.join(',') === expectedFlagKeys.join(','), + 'RegExp.prototype.flags should read observable flag properties in spec order' +); + console.log('regex exec fast path semantics ok');