From 68ccf50a77f34288de50d5008eb636851cd48e18 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Thu, 30 Jul 2026 02:04:21 +0800 Subject: [PATCH] refactor(code-runtime): generate shared subprocess runner --- package.json | 2 + .../code-runtime-worker/src/bootstrap.ts | 19 +- .../code-runtime-worker/src/runtime-host.ts | 26 +- .../tests/bootstrap.spec.ts | 27 ++ .../src/runner-source.generated.ts | 6 + packages/util/atomic-write/src/runner.ts | 390 ++++++++++++++++++ scripts/gen-code-runtime-runner.ts | 70 ++++ scripts/run-gates.ts | 1 + vitest.config.ts | 96 +---- 9 files changed, 555 insertions(+), 82 deletions(-) create mode 100644 packages/util/atomic-write/src/runner-source.generated.ts create mode 100644 packages/util/atomic-write/src/runner.ts create mode 100644 scripts/gen-code-runtime-runner.ts diff --git a/package.json b/package.json index 423cbf813c..2b4971a6b7 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,8 @@ "gen-module-graph": "tsx scripts/gen-module-graph.ts", "gen-scoped-events": "tsx scripts/gen-scoped-events.ts", "verify-scoped-events": "tsx scripts/gen-scoped-events.ts --check", + "gen-code-runtime-runner": "tsx scripts/gen-code-runtime-runner.ts", + "verify-code-runtime-runner": "tsx scripts/gen-code-runtime-runner.ts --check", "verify-module-graph": "tsx scripts/gen-module-graph.ts --check", "constraints": "tsx scripts/check-workspace-constraints.ts", "doc-sync": "tsx scripts/run-gates.ts doc-sync", diff --git a/packages/code-runtime/code-runtime-worker/src/bootstrap.ts b/packages/code-runtime/code-runtime-worker/src/bootstrap.ts index cbe0d0ac55..2207389c97 100644 --- a/packages/code-runtime/code-runtime-worker/src/bootstrap.ts +++ b/packages/code-runtime/code-runtime-worker/src/bootstrap.ts @@ -6,6 +6,7 @@ */ import { inspect } from 'node:util' +import type { CodeJsonValue } from '@deepseek-ai/dsh-code-runtime' import type { DoneMessage, ReplyMessage, WorkerBootData, WorkerToHost } from './protocol.ts' import { jsonStringBytesUpTo, jsonValueBytesUpTo, truncateJsonStringBytes } from './output-json.ts' import { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts' @@ -310,6 +311,7 @@ export function wireReplies(port: BootstrapPort, pending: Map, nextId: { value: number }, errorClasses: Map = makeBindingErrorClasses(data), + maxFrameBytes?: number, ): Record[] { return data.namespaces.map(({ global, names }) => { const errorClass = errorClasses.get(global) @@ -335,6 +338,11 @@ export function makeNamespaces( if (detached === undefined) { return Promise.reject(bindingFailure(errorClass, name, 'binding arguments must be lossless JSON')) } + const call = { type: 'call' as const, id: nextId.value, global, name, args: encodeWorkerJson(detached) } + if (maxFrameBytes !== undefined + && jsonValueBytesUpTo(call as unknown as CodeJsonValue, maxFrameBytes) === undefined) { + return Promise.reject(bindingFailure(errorClass, name, 'binding arguments exceed maxFrameBytes')) + } return new Promise((resolve, reject) => { const id = nextId.value++ pending.set(id, { @@ -344,7 +352,7 @@ export function makeNamespaces( }, }) try { - port.postMessage({ type: 'call', id, global, name, args: encodeWorkerJson(detached) }) + port.postMessage(call) } catch (error: unknown) { pending.delete(id) const message = `binding arguments must be structured-cloneable: ${error instanceof CapturedError ? error.message : String(error)}` @@ -364,12 +372,14 @@ export function makeNamespaces( * @param port - host message port or test double. * @param data - the boot payload the host sent. * @param streams - stdout/stderr objects captured as program logs. + * @param maxFrameBytes - optional serialized transport cap checked before posting. * @returns after posting the done message. */ export async function runWorkerMain( port: BootstrapPort, data: WorkerBootData, streams: { stdout: PatchableStream; stderr: PatchableStream }, + maxFrameBytes?: number, ): Promise { const logs = new LogBuffer( data.maxOutputBytes, @@ -384,7 +394,7 @@ export async function runWorkerMain( const nextId = { value: 1 } const errorClasses = makeBindingErrorClasses(data) - const namespaces = makeNamespaces(data, port, pending, nextId, errorClasses) + const namespaces = makeNamespaces(data, port, pending, nextId, errorClasses, maxFrameBytes) const errorClassParameters: string[] = [] const errorClassValues: BindingErrorConstructor[] = [] for (const namespace of data.namespaces) { @@ -420,5 +430,8 @@ export async function runWorkerMain( ...prepareException(error, logs.remainingOutputBytes(), data.maxOutputBytes), } } - port.postMessage(done) + port.postMessage(maxFrameBytes !== undefined + && jsonValueBytesUpTo(done as unknown as CodeJsonValue, maxFrameBytes) === undefined + ? { type: 'output-limit' } + : done) } diff --git a/packages/code-runtime/code-runtime-worker/src/runtime-host.ts b/packages/code-runtime/code-runtime-worker/src/runtime-host.ts index d2bb1082b8..47bfacfe91 100644 --- a/packages/code-runtime/code-runtime-worker/src/runtime-host.ts +++ b/packages/code-runtime/code-runtime-worker/src/runtime-host.ts @@ -1,6 +1,7 @@ /** Shared host mechanics for local and subprocess-hosted TypeScript worker runtimes. */ import { stripTypeScriptTypes } from 'node:module' +import type { Readable } from 'node:stream' import type { CodeBindingNamespace, CodeJsonValue, @@ -15,6 +16,28 @@ import type { WorkerJsonWire } from './worker-json.ts' /** Smallest cap that can represent an empty log array and failure message. */ export const MIN_RUNTIME_OUTPUT_BYTES = 4 +/** + * Resolve after a worker pipe emits queued data or closes during termination. + * @param stream - captured worker or child-process pipe. + * @returns after no more queued bytes can arrive. + */ +export function waitForRuntimePipeDrain(stream: Readable): Promise { + if (stream.readableEnded || stream.destroyed) return Promise.resolve() + return new Promise((resolve) => { + const done = (): void => { + stream.off('end', done) + stream.off('close', done) + stream.off('error', done) + resolve() + } + stream.once('end', done) + stream.once('close', done) + stream.once('error', done) + /* v8 ignore next -- termination can win the adjacent listener-registration race. */ + if (stream.readableEnded || stream.destroyed) done() + }) +} + const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/ const RESERVED_WORDS = new Set([ 'await', 'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do', @@ -223,5 +246,6 @@ export class RuntimeOutputLedger { } export { decodeWorkerJson, encodeWorkerJson, snapshotCodeJsonValue } from './worker-json.ts' -export { jsonValueBytesUpTo } from './output-json.ts' +export { jsonStringBytesUpTo, jsonValueBytesUpTo } from './output-json.ts' +export { runWorkerMain } from './bootstrap.ts' export type { WorkerJsonWire } from './worker-json.ts' diff --git a/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts b/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts index a2aac6d9c2..247501df29 100644 --- a/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts +++ b/packages/code-runtime/code-runtime-worker/tests/bootstrap.spec.ts @@ -286,6 +286,23 @@ describe('makeNamespaces', () => { expect(nextId.value).toBe(1) }) + it('rejects an oversized transport frame before posting or allocating a call id', async () => { + const port = new FakePort() + const pending = new Map() + const nextId = { value: 1 } + const data = { namespaces: [toolNamespace(['x'])] } + const [tools] = makeNamespaces( + data, port, pending, nextId, makeBindingErrorClasses(data), 64, + ) as [Record Promise>] + + await expect(tools.x?.({ text: 'x'.repeat(64) })).rejects.toMatchObject({ + name: 'ToolCallError', toolName: 'x', message: 'binding arguments exceed maxFrameBytes', + }) + expect(port.sent).toEqual([]) + expect(pending.size).toBe(0) + expect(nextId.value).toBe(1) + }) + it('uses ordinary Error for non-tools namespace failures', async () => { const deniedPort = new FakePort() deniedPort.respond = message => message.type === 'call' @@ -343,6 +360,16 @@ describe('runWorkerMain', () => { }) }) + it('reports output-limit before posting a completion that expands past the transport cap', async () => { + const port = new FakePort() + await runWorkerMain(port, { + maxOutputBytes: 1_000, + code: 'return Array.from({ length: 100 }, () => [])', + namespaces: [], + }, fakeStreams(), 100) + expect(port.sent.at(-1)).toEqual({ type: 'output-limit' }) + }) + it('reports a thrown program error on the done message', async () => { const port = new FakePort() await runWorkerMain(port, { ...BOOT, code: 'throw new Error("boom")', namespaces: [] }, fakeStreams()) diff --git a/packages/util/atomic-write/src/runner-source.generated.ts b/packages/util/atomic-write/src/runner-source.generated.ts new file mode 100644 index 0000000000..d40c77783c --- /dev/null +++ b/packages/util/atomic-write/src/runner-source.generated.ts @@ -0,0 +1,6 @@ +/** + * Generated dependency-free execution-world runner. + * Do not edit by hand; run `pnpm run gen-code-runtime-runner`. + */ + +export const CODE_RUNNER_SOURCE = "import{Buffer as e}from\"node:buffer\";import{fork as t}from\"node:child_process\";import{createInterface as n}from\"node:readline\";import{inspect as r}from\"node:util\";import{fileURLToPath as i}from\"node:url\";import{Worker as a,isMainThread as o,parentPort as s,workerData as c}from\"node:worker_threads\";const l=Reflect.apply,u=Array.isArray,d=Buffer,f=Reflect.get(Buffer,`byteLength`),ee=Object.create,p=Object.defineProperty,te=Object.keys,ne=String,re=Reflect.get(String.prototype,`charCodeAt`),ie=Reflect.get(String.prototype,`codePointAt`),m=Reflect.get(String.prototype,`slice`);function h(e){let t=ee(null);return t.value=e,t}function ae(e,t,n){let r=h(n);r.enumerable=!0,r.configurable=!0,r.writable=!0,p(e,t,r)}function g(e){return l(f,d,[e,`utf8`])}function _(e,t){ae(e,e.length,t)}function oe(e){if(e.length===0)return;let t=e.length-1,n=e[t];return p(e,`length`,h(t)),n}function se(e,t){return l(m,e,[t,t+(l(ie,e,[t])>65535?2:1)])}function ce(e){if(e.length===2)return 4;if(e===`\"`||e===`\\\\`)return 2;let t=l(re,e,[0]);return t>=55296&&t<=57343?6:t<32?t===8||t===9||t===10||t===12||t===13?2:6:g(e)}function v(e,t){if(t<2)return;let n=2;for(let r=0;rt)return;r+=i.length}return n}function y(e,t){let n=0,r=e=>(n+=e,n<=t),i=[{kind:`value`,value:e}];for(let e=oe(i);e!==void 0;e=oe(i)){if(e.kind===`value`){let a=e.value;if(a===null){if(!r(4))return}else if(typeof a==`string`){let e=v(a,t-n);if(e===void 0)return;n+=e}else if(typeof a==`number`){if(!r(g(ne(a))))return}else if(typeof a==`boolean`){if(!r(a?4:5))return}else if(u(a)){if(!r(2))return;a.length>0&&_(i,{kind:`array`,value:a,index:0})}else{if(!r(2))return;let e=te(a);e.length>0&&_(i,{kind:`object`,value:a,keys:e,index:0})}continue}if(e.index>0&&!r(1))return;if(e.kind===`array`){let t=e.value[e.index];if(t===void 0)return;e.index+1t)break;n+=o,r+=a.length,i+=a.length}return r===e.length?e:l(m,e,[0,r])}const ue=Reflect.get(Function.prototype,`toString`),b=Reflect.get(Reflect,`apply`),x=Error,S=Set,C=Array.isArray,de=Array.prototype,w=Number.isFinite,fe=Number.isSafeInteger,pe=Object.create,me=Object.defineProperty,he=Object.getOwnPropertyDescriptor,T=Object.getPrototypeOf,ge=Object.hasOwn,E=Object.is,_e=Object.keys,D=Object.prototype,ve=Reflect.get(D,`propertyIsEnumerable`),O=Reflect.ownKeys,ye=Reflect.get(Set.prototype,`add`),be=Reflect.get(Set.prototype,`delete`),xe=Reflect.get(Set.prototype,`has`);function k(e){let t=pe(null);return t.value=e,t}function A(e,t,n){let r=k(n);r.enumerable=!0,r.configurable=!0,r.writable=!0,me(e,t,r)}function j(e,t){A(e,e.length,t)}function M(e){if(e.length===0)return;let t=e.length-1,n=e[t];return me(e,`length`,k(t)),n}function N(e,t){return b(xe,e,[t])}function P(e,t){b(ye,e,[t])}function Se(e,t){b(be,e,[t])}function F(e,t){let n=he(e,`constructor`)?.value;if(typeof n!=`function`)return!1;try{return n.name===t&&n.prototype===e&&b(ue,n,[])===`function ${t}() { [native code] }`}catch{return!1}}function I(e){return T(e)===null&&F(e,`Object`)}function L(e){let t=T(e);if(t===de)return!0;if(!C(t)||!F(t,`Array`))return!1;let n=T(t);return typeof n==`object`&&!!n&&I(n)}function R(e){let t=T(e);return t===null||t===D||typeof t==`object`&&I(t)}function z(e){let t=O(e);for(let n=0;n{e.kind===`root`?n=t:e.kind===`array`?A(e.target,e.index,t):A(e.target,e.key,t)},i=[{kind:`visit`,value:e,destination:{kind:`root`}}];for(let e=M(i);e!==void 0;e=M(i)){if(e.kind===`leave`){Se(t,e.source);continue}if(e.kind===`array-item`){if(!ge(e.source,e.index))return;j(i,{kind:`visit`,value:e.source[e.index],destination:{kind:`array`,target:e.target,index:e.index}});continue}if(e.kind===`object-property`){j(i,{kind:`visit`,value:e.source[e.key],destination:{kind:`object`,target:e.target,key:e.key}});continue}let n=e.value;if(n===null){r(e.destination,null);continue}if(typeof n==`boolean`||typeof n==`string`){r(e.destination,n);continue}if(typeof n==`number`){if(!w(n)||E(n,-0))return;r(e.destination,n);continue}if(typeof n!=`object`||N(t,n))return;if(C(n)){if(!L(n))return;let a=n.length;if(O(n).length!==a+1)return;let o=[];r(e.destination,o),P(t,n),j(i,{kind:`leave`,source:n});for(let e=a-1;e>=0;e--)j(i,{kind:`array-item`,source:n,index:e,target:o});continue}if(!R(n))return;let a=z(n);if(a===void 0)return;let o={};r(e.destination,o),P(t,n),j(i,{kind:`leave`,source:n});for(let e=a.length-1;e>=0;e--){let t=a[e];if(t===void 0)return;j(i,{kind:`object-property`,source:n,key:t,target:o})}}return n}function V(e){let t=[],n=[e];for(let e=M(n);e!==void 0;e=M(n)){if(e===null||typeof e==`boolean`||typeof e==`number`||typeof e==`string`){j(t,e);continue}if(C(e)){j(t,{kind:`array`,length:e.length});for(let t=e.length-1;t>=0;t--){let r=e[t];if(r===void 0)throw new x(`cannot encode a sparse JSON array`);j(n,r)}continue}let r=_e(e);j(t,{kind:`object`,keys:r});for(let t=r.length-1;t>=0;t--){let i=r[t];if(i===void 0)throw new x(`cannot encode a missing JSON object key`);let a=e[i];if(a===void 0)throw new x(`cannot encode an undefined JSON object property`);j(n,a)}}return t}function H(e){if(!L(e)||O(e).length!==e.length+1)return!1;for(let t=0;t=0?{kind:`array`,length:e}:void 0}if(n.kind===`object`){if(t.length!==2||!U(t,`kind`)||!U(t,`keys`))return;let e=n.keys;if(!C(e)||!H(e))return;let r=new S,i=[],a=e;for(let e=0;e{let t=n[n.length-1];if(!t)return i?!1:(r=e,i=!0,!0);if(t.index>=(t.kind===`array`?t.length:t.keys.length))return!1;if(t.kind===`array`)j(t.target,e);else{let n=t.keys[t.index];if(n===void 0)return!1;A(t.target,n,e)}return t.index+=1,!0};for(let e=0;ea)return;let e=[];i=e,n.length>0&&(o={kind:`array`,target:e,length:n.length,index:0})}else{if(n.keys.length>a)return;let e={};i=e,n.keys.length>0&&(o={kind:`object`,target:e,keys:n.keys,index:0})}}if(!a(i))return;for(o&&j(n,o);n.length>0;){let e=n[n.length-1];if(e===void 0||e.index<(e.kind===`array`?e.length:e.keys.length))break;M(n)}}return n.length===0?r:void 0}catch{return}}const G=Error,we=Object.create,Te=Object.defineProperty;function K(e,t,n){let r=we(null);r.enumerable=!0,r.value=n,Te(e,t,r)}var Ee=class{bytes=2;entries=0;truncated=!1;sink;onLimit;maxBytes;constructor(e,t,n=()=>{}){this.maxBytes=e,this.sink=t,this.onLimit=n}push(e){if(this.truncated)return;let t=+(this.entries>0),n=this.maxBytes-this.bytes-t,r=v(e,n);if(r===void 0){this.truncated=!0;let r=le(e,n);if(r.length>0){let e=v(r,n);if(e===void 0)throw new G(`worker output ledger produced an oversized log prefix`);this.bytes+=e+t,this.entries+=1,this.sink(r)}this.onLimit();return}this.bytes+=r+t,this.entries+=1,this.sink(e)}remainingOutputBytes(){return this.maxBytes-this.bytes}};const De=[`log`,`info`,`warn`,`error`,`debug`];function Oe(e){let t=e=>e.map(e=>typeof e==`string`?e:r(e,Ae)).join(` `),n=Object.create(null);for(let r of De)n[r]=(...n)=>{e.push(t(n))};return n}function ke(e,t){let n=t.write;return t.write=(t,...n)=>{e.push(typeof t==`string`?t:String(t));let r=[n[0],n[1]].find(e=>typeof e==`function`);return r&&queueMicrotask(()=>{r(null)}),!0},()=>{t.write=n}}const Ae={depth:4,maxArrayLength:100,maxStringLength:1e4};function je(e,t,n=t){if(e===void 0)return{};let r;try{r=B(e)}catch{r=void 0}return r===void 0?Ne(`invalid-output`,`program completion must be lossless JSON`,t,n):y(r,t)===void 0?Me(n):{value:V(r)}}function Me(e){return{error:{kind:`output-limit`,message:`outer output exceeded ${e} bytes`}}}function Ne(e,t,n,r){return v(t,n)===void 0?Me(r):{error:{kind:e,message:t}}}function Pe(e,t,n=t){let r;try{let t=e instanceof G?e.stack??e.message:e;r=typeof t==`string`?t:String(t)}catch{r=`program threw an unrenderable value`}return Ne(`exception`,r,t,n)}function Fe(e){return class extends G{constructor(t,n){super(n),K(this,`name`,e.name),K(this,e.memberNameProperty,t)}}}function q(e,t,n){return e?new e(t,n):new G(n)}function Ie(e){let t=new Map;for(let n of e.namespaces)n.errorClass&&t.set(n.global,Fe(n.errorClass));return t}function Le(e,t){e.on(`message`,e=>{let n=t.get(e.id);if(n)if(t.delete(e.id),e.ok){let t=W(e.value);t===void 0?n.reject(new G(`binding resolution must be lossless JSON`)):n.resolve(t)}else n.reject(new G(e.message))})}function Re(e,t,n,r,i=Ie(e),a){return e.namespaces.map(({global:e,names:o})=>{let s=i.get(e),c=Object.create(null);for(let i of o)Object.defineProperty(c,i,{enumerable:!0,value:o=>{let c;try{c=B(o)}catch{c=void 0}if(c===void 0)return Promise.reject(q(s,i,`binding arguments must be lossless JSON`));let l={type:`call`,id:r.value,global:e,name:i,args:V(c)};return a!==void 0&&y(l,a)===void 0?Promise.reject(q(s,i,`binding arguments exceed maxFrameBytes`)):new Promise((e,a)=>{let o=r.value++;n.set(o,{resolve:e,reject:e=>{a(q(s,i,e.message))}});try{t.postMessage(l)}catch(e){n.delete(o),a(q(s,i,`binding arguments must be structured-cloneable: ${e instanceof G?e.message:String(e)}`))}})}});return c})}async function ze(e,t,n,r){let i=new Ee(t.maxOutputBytes,t=>{e.postMessage({type:`log`,text:t})},()=>{e.postMessage({type:`output-limit`})});ke(i,n.stdout),ke(i,n.stderr);let a=new Map;Le(e,a);let o={value:1},s=Ie(t),c=Re(t,e,a,o,s,r),l=[],u=[];for(let e of t.namespaces){if(!e.errorClass)continue;l.push(e.errorClass.name);let t=s.get(e.global);if(!t)throw new G(`missing binding error class for ${e.global}`);u.push(t)}let d=Oe(i),f;try{let e=(async()=>{}).constructor;f={type:`done`,...je(await new e(...t.namespaces.map(e=>e.global),...l,`console`,`'use strict';\\n${t.code}`)(...c,...u,d),i.remainingOutputBytes(),t.maxOutputBytes)}}catch(e){f={type:`done`,...Pe(e,i.remainingOutputBytes(),t.maxOutputBytes)}}e.postMessage(r!==void 0&&y(f,r)===void 0?{type:`output-limit`}:f)}function J(e){return e.readableEnded||e.destroyed?Promise.resolve():new Promise(t=>{let n=()=>{e.off(`end`,n),e.off(`close`,n),e.off(`error`,n),t()};e.once(`end`,n),e.once(`close`,n),e.once(`error`,n),(e.readableEnded||e.destroyed)&&n()})}const Be=new Set([`exception`,`timeout`,`abort`,`worker-exit`,`invalid-output`,`output-limit`]);let Y=0;function X(e){return typeof e==`object`&&e?e:void 0}function Z(t,n){try{let r=JSON.stringify(t);return typeof r==`string`&&e.byteLength(r)<=n?r:void 0}catch{return}}function Ve(e){process.stdout.write(e),process.stdout.write(`\n`)}function He(e){let t=Z(e,Y);return t===void 0?!1:(Ve(t),!0)}function Ue(e){let t=X(e);return t!==void 0&&typeof t.kind==`string`&&Be.has(t.kind)&&typeof t.message==`string`}function We(e){return Ue(e)&&(e.kind===`exception`||e.kind===`invalid-output`||e.kind===`output-limit`)}function Ge(e){let t=X(e);if(!(t===void 0||t.type!==`boot`||typeof t.code!=`string`||!Array.isArray(t.namespaces)||!Number.isSafeInteger(t.maxOutputBytes)||t.maxOutputBytes<4||!Number.isSafeInteger(t.maxFrameBytes)||t.maxFrameBytes{e.once(`exit`,()=>{t()})})}function $(){return{type:`done`,error:{kind:`worker-exit`,message:`code runtime bridge frame exceeded maxFrameBytes`}}}function Je(){let e=n({input:process.stdin,crlfDelay:1/0}),r,a=0,o=2,s=0,c=!1,l=t=>{if(c)return;let n=Z(t,Y)??Z($(),Y);c=!0,n!==void 0&&Ve(n);let i=r;r=void 0,(i===void 0?Promise.resolve():new Promise(e=>{setImmediate(e)}).then(async()=>{let e=J(i.stdout),t=J(i.stderr),n=qe(i);i.kill(`SIGKILL`),await Promise.all([n,e,t])})).catch(e=>{process.stderr.write(`dsh-code-runtime-subprocess controller cleanup error: ${String(e)}\\n`)}).then(()=>{e.close(),process.stdin.destroy()})},u=e=>{if(c)return;let t=+(s>0),n=v(e,a-o-t);if(n===void 0){l({type:`output-limit`});return}o+=n+t,s+=1,He({type:`log`,text:e})||l($())},d=e=>{a=e.maxOutputBytes,Y=e.maxFrameBytes,r=t(i(import.meta.url),[],{env:{DSH_CODE_RUNTIME_CONTROLLER:`1`},detached:!1,execArgv:[],stdio:[`ignore`,`pipe`,`pipe`,`ipc`]});let n=r;n.stdout.on(`data`,e=>{u(e.toString(`utf8`))}),n.stderr.on(`data`,e=>{u(e.toString(`utf8`))}),n.on(`message`,e=>{let t=X(e);if(t!==void 0){if(t.type===`log`&&typeof t.text==`string`){u(t.text);return}c||(t.type===`call`&&typeof t.id==`number`&&typeof t.global==`string`&&typeof t.name==`string`?He({type:`call`,id:t.id,global:t.global,name:t.name,args:Q(t.args)})||l($()):t.type===`output-limit`?l({type:`output-limit`}):t.type===`done`&&(t.error===void 0?l({type:`done`,...t.value===void 0?{}:{value:Q(t.value)}}):Ue(t.error)&&l({type:`done`,error:t.error})))}}),n.on(`error`,e=>{l({type:`done`,error:{kind:`worker-exit`,message:`remote controller error: ${e.message}`}})}),n.on(`exit`,e=>{c||l({type:`done`,error:{kind:`worker-exit`,message:`remote controller exited with code ${e} before completing`}})}),n.send(e,e=>{e!==null&&l({type:`done`,error:{kind:`worker-exit`,message:`remote controller boot failed: ${e.message}`}})})};e.on(`line`,e=>{let t;try{t=JSON.parse(e)}catch(e){process.stderr.write(`dsh-code-runtime-subprocess frame error: ${String(e)}\\n`),l({type:`done`,error:{kind:`worker-exit`,message:`remote runner received a malformed frame`}});return}if(r===void 0){let e=Ge(t);if(e===void 0){l({type:`done`,error:{kind:`worker-exit`,message:`remote runner received an invalid boot frame`}});return}d(e);return}let n=Ke(t);n!==void 0&&r.send(n,e=>{e!==null&&l({type:`done`,error:{kind:`worker-exit`,message:`remote controller reply failed: ${e.message}`}})})}),e.on(`close`,()=>{r!==void 0&&!c&&l({type:`done`,error:{kind:`abort`,message:`remote runner input closed`}})})}function Ye(){let e,t=!1,n,i=0,o=e=>process.send===void 0||i>0&&Z(e,i)===void 0?!1:(process.send(e),!0),s=r=>{if(t)return;t=!0,clearInterval(n);let a=i>0&&Z(r,i)===void 0?$():r,s=e;e=void 0,(s===void 0?Promise.resolve():new Promise(e=>{setImmediate(e)}).then(async()=>{let e=J(s.stdout),t=J(s.stderr);await Promise.all([s.terminate(),e,t])})).catch(e=>{o({type:`log`,text:`dsh-code-runtime-subprocess worker cleanup error: ${String(e)}\\n`})}).then(()=>{if(process.send===void 0){process.exitCode=1;return}process.send(a,()=>{process.connected&&process.disconnect()})})};process.on(`message`,c=>{if(e===void 0){let l=Ge(c);if(l===void 0){s({type:`done`,error:{kind:`worker-exit`,message:`remote controller received an invalid boot frame`}});return}i=l.maxFrameBytes,e=new a(new URL(import.meta.url),{workerData:l,env:{},execArgv:[],stdout:!0,stderr:!0,resourceLimits:{maxOldGenerationSizeMb:l.maxOldGenerationSizeMb}});let u=e;u.stdout.on(`data`,e=>{o({type:`log`,text:e.toString(`utf8`)})||s($())}),u.stderr.on(`data`,e=>{o({type:`log`,text:e.toString(`utf8`)})||s($())}),u.on(`message`,e=>{let t=X(e);t!==void 0&&(t.type===`call`&&typeof t.id==`number`&&typeof t.global==`string`&&typeof t.name==`string`?o({type:`call`,id:t.id,global:t.global,name:t.name,args:Q(t.args)})||s($()):t.type===`log`&&typeof t.text==`string`?o({type:`log`,text:t.text})||s($()):t.type===`output-limit`?s({type:`output-limit`}):t.type===`done`&&(t.error===void 0?s({type:`done`,...t.value===void 0?{}:{value:Q(t.value)}}):We(t.error)&&s({type:`done`,error:t.error})))}),u.on(`error`,e=>{s({type:`done`,error:{kind:`worker-exit`,message:`worker error: ${e.stack||e.message||r(e)}`}})}),u.on(`exit`,e=>{t||s({type:`done`,error:{kind:`worker-exit`,message:`worker exited with code ${e} before completing`}})}),n=setInterval(()=>{e!==void 0&&e.performance.eventLoopUtilization().active>l.computeMs&&s({type:`done`,error:{kind:`timeout`,message:`compute budget exhausted (${l.computeMs}ms busy)`}})},25);return}let l=Ke(c);l!==void 0&&e.postMessage(l)}),process.on(`disconnect`,()=>{e!==void 0&&!t&&e.terminate()})}if(o)process.env.DSH_CODE_RUNTIME_CONTROLLER===`1`?Ye():Je();else{if(s===null)throw Error(`remote worker requires parentPort`);let e=c;ze(s,e,{stdout:process.stdout,stderr:process.stderr},e.maxFrameBytes)}export{};" diff --git a/packages/util/atomic-write/src/runner.ts b/packages/util/atomic-write/src/runner.ts new file mode 100644 index 0000000000..7a0b3eb678 --- /dev/null +++ b/packages/util/atomic-write/src/runner.ts @@ -0,0 +1,390 @@ +/** Typed source for the dependency-free execution-world runner bundle. */ + +import { Buffer } from 'node:buffer' +import { fork } from 'node:child_process' +import type { ChildProcess } from 'node:child_process' +import { createInterface } from 'node:readline' +import type { Readable } from 'node:stream' +import { inspect } from 'node:util' +import { fileURLToPath } from 'node:url' +import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads' +import { + decodeWorkerJson, + encodeWorkerJson, + jsonStringBytesUpTo, + runWorkerMain, + waitForRuntimePipeDrain, +} from '@deepseek-ai/dsh-code-runtime-worker/runtime-host' +import type { WorkerJsonWire } from '@deepseek-ai/dsh-code-runtime-worker/runtime-host' + +type WorkerBootData = Parameters[1] +type Controller = ChildProcess & { stdout: Readable; stderr: Readable } +type FailureKind = 'exception' | 'timeout' | 'abort' | 'worker-exit' | 'invalid-output' | 'output-limit' + +interface RuntimeBootData extends WorkerBootData { + type: 'boot' + maxFrameBytes: number + maxOldGenerationSizeMb: number + computeMs: number +} + +interface RuntimeFailure { + kind: FailureKind + message: string +} + +interface RuntimeCall { + type: 'call' + id: number + global: string + name: string + args: WorkerJsonWire | null +} + +type RuntimeReply = + | { type: 'reply'; id: number; ok: true; value: unknown } + | { type: 'reply'; id: number; ok: false; message: string } + +type RuntimeMessage = RuntimeCall + | { type: 'log'; text: string } + | { type: 'output-limit' } + | { type: 'done'; value?: WorkerJsonWire | null; error?: RuntimeFailure } + +const failureKinds = new Set([ + 'exception', 'timeout', 'abort', 'worker-exit', 'invalid-output', 'output-limit', +]) + +let maxFrameBytes = 0 + +function recordOf(value: unknown): Record | undefined { + return typeof value === 'object' && value !== null ? value as Record : undefined +} + +function encodeJsonBounded(value: unknown, maxBytes: number): string | undefined { + try { + const json: unknown = JSON.stringify(value) + return typeof json === 'string' && Buffer.byteLength(json) <= maxBytes ? json : undefined + } catch { + return undefined + } +} + +function emitJson(json: string): void { + process.stdout.write(json) + process.stdout.write('\n') +} + +function emitFrame(message: RuntimeMessage): boolean { + const json = encodeJsonBounded(message, maxFrameBytes) + if (json === undefined) return false + emitJson(json) + return true +} + +function validFailure(value: unknown): value is RuntimeFailure { + const record = recordOf(value) + return record !== undefined + && typeof record.kind === 'string' + && failureKinds.has(record.kind as FailureKind) + && typeof record.message === 'string' +} + +function validWorkerFailure(value: unknown): value is RuntimeFailure { + return validFailure(value) + && (value.kind === 'exception' || value.kind === 'invalid-output' || value.kind === 'output-limit') +} + +function runtimeBoot(value: unknown): RuntimeBootData | undefined { + const record = recordOf(value) + if (record === undefined + || record.type !== 'boot' + || typeof record.code !== 'string' + || !Array.isArray(record.namespaces) + || !Number.isSafeInteger(record.maxOutputBytes) + || (record.maxOutputBytes as number) < 4 + || !Number.isSafeInteger(record.maxFrameBytes) + || (record.maxFrameBytes as number) < (record.maxOutputBytes as number) + || typeof record.computeMs !== 'number' + || !Number.isFinite(record.computeMs) + || (record.computeMs) <= 0 + || typeof record.maxOldGenerationSizeMb !== 'number' + || !Number.isFinite(record.maxOldGenerationSizeMb) + || (record.maxOldGenerationSizeMb) <= 0) return undefined + return record as unknown as RuntimeBootData +} + +function runtimeReply(value: unknown): RuntimeReply | undefined { + const record = recordOf(value) + if (record === undefined || record.type !== 'reply' || typeof record.id !== 'number' || typeof record.ok !== 'boolean') return undefined + return record.ok + ? { type: 'reply', id: record.id, ok: true, value: record.value } + : { type: 'reply', id: record.id, ok: false, message: String(record.message) } +} + +function transportWireOrNull(input: unknown): WorkerJsonWire | null { + const value = decodeWorkerJson(input) + return value === undefined ? null : encodeWorkerJson(value) +} + +function waitForChildExit(child: ChildProcess): Promise { + if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve() + return new Promise((resolve) => { child.once('exit', () => { resolve() }) }) +} + +function frameLimitFailure(): RuntimeMessage { + return { + type: 'done', + error: { kind: 'worker-exit', message: 'code runtime bridge frame exceeded maxFrameBytes' }, + } +} + +function runLauncher(): void { + const input = createInterface({ input: process.stdin, crlfDelay: Infinity }) + let controller: Controller | undefined + let maxOutputBytes = 0 + let logBytes = 2 + let logEntries = 0 + let settling = false + + const finish = (message: RuntimeMessage): void => { + if (settling) return + const encoded = encodeJsonBounded(message, maxFrameBytes) + ?? encodeJsonBounded(frameLimitFailure(), maxFrameBytes) + settling = true + if (encoded !== undefined) emitJson(encoded) + const current = controller + controller = undefined + const drain = current === undefined + ? Promise.resolve() + : new Promise((resolve) => { setImmediate(resolve) }).then(async () => { + const stdoutDrained = waitForRuntimePipeDrain(current.stdout) + const stderrDrained = waitForRuntimePipeDrain(current.stderr) + const exited = waitForChildExit(current) + current.kill('SIGKILL') + await Promise.all([exited, stdoutDrained, stderrDrained]) + }) + void drain.catch((error: unknown) => { + process.stderr.write(`dsh-code-runtime-subprocess controller cleanup error: ${String(error)}\n`) + }).then(() => { + input.close() + process.stdin.destroy() + }) + } + + const forwardLog = (text: string): void => { + if (settling) return + const separator = logEntries > 0 ? 1 : 0 + const cost = jsonStringBytesUpTo(text, maxOutputBytes - logBytes - separator) + if (cost === undefined) { + finish({ type: 'output-limit' }) + return + } + logBytes += cost + separator + logEntries += 1 + if (!emitFrame({ type: 'log', text })) finish(frameLimitFailure()) + } + + const startController = (boot: RuntimeBootData): void => { + maxOutputBytes = boot.maxOutputBytes + maxFrameBytes = boot.maxFrameBytes + controller = fork(fileURLToPath(import.meta.url), [], { + env: { DSH_CODE_RUNTIME_CONTROLLER: '1' }, + detached: false, + execArgv: [], + stdio: ['ignore', 'pipe', 'pipe', 'ipc'], + }) as Controller + const current = controller + current.stdout.on('data', (data: Buffer) => { forwardLog(data.toString('utf8')) }) + current.stderr.on('data', (data: Buffer) => { forwardLog(data.toString('utf8')) }) + current.on('message', (raw: unknown) => { + const message = recordOf(raw) + if (message === undefined) return + if (message.type === 'log' && typeof message.text === 'string') { + forwardLog(message.text) + return + } + if (settling) return + if (message.type === 'call' + && typeof message.id === 'number' + && typeof message.global === 'string' + && typeof message.name === 'string') { + if (!emitFrame({ + type: 'call', id: message.id, global: message.global, name: message.name, args: transportWireOrNull(message.args), + })) finish(frameLimitFailure()) + } else if (message.type === 'output-limit') { + finish({ type: 'output-limit' }) + } else if (message.type === 'done') { + if (message.error !== undefined) { + if (validFailure(message.error)) finish({ type: 'done', error: message.error }) + } else { + finish({ type: 'done', ...message.value === undefined ? {} : { value: transportWireOrNull(message.value) } }) + } + } + }) + current.on('error', (error: Error) => { + finish({ type: 'done', error: { kind: 'worker-exit', message: `remote controller error: ${error.message}` } }) + }) + current.on('exit', (code: number | null) => { + if (!settling) { + finish({ type: 'done', error: { kind: 'worker-exit', message: `remote controller exited with code ${code} before completing` } }) + } + }) + current.send(boot, (error: Error | null) => { + if (error !== null) { + finish({ type: 'done', error: { kind: 'worker-exit', message: `remote controller boot failed: ${error.message}` } }) + } + }) + } + + input.on('line', (line: string) => { + let raw: unknown + try { + raw = JSON.parse(line) as unknown + } catch (error: unknown) { + process.stderr.write(`dsh-code-runtime-subprocess frame error: ${String(error)}\n`) + finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote runner received a malformed frame' } }) + return + } + if (controller === undefined) { + const boot = runtimeBoot(raw) + if (boot === undefined) { + finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote runner received an invalid boot frame' } }) + return + } + startController(boot) + return + } + const reply = runtimeReply(raw) + if (reply !== undefined) { + controller.send(reply, (error: Error | null) => { + if (error !== null) { + finish({ type: 'done', error: { kind: 'worker-exit', message: `remote controller reply failed: ${error.message}` } }) + } + }) + } + }) + input.on('close', () => { + if (controller !== undefined && !settling) { + finish({ type: 'done', error: { kind: 'abort', message: 'remote runner input closed' } }) + } + }) +} + +function runController(): void { + let worker: Worker | undefined + let finished = false + let computeTimer: NodeJS.Timeout | undefined + let controllerMaxFrameBytes = 0 + + const send = (message: RuntimeMessage): boolean => { + if (process.send === undefined) return false + if (controllerMaxFrameBytes > 0 && encodeJsonBounded(message, controllerMaxFrameBytes) === undefined) return false + process.send(message) + return true + } + + const finish = (message: RuntimeMessage): void => { + if (finished) return + finished = true + clearInterval(computeTimer) + const bounded = controllerMaxFrameBytes > 0 && encodeJsonBounded(message, controllerMaxFrameBytes) === undefined + ? frameLimitFailure() + : message + const current = worker + worker = undefined + const drain = current === undefined + ? Promise.resolve() + : new Promise((resolve) => { setImmediate(resolve) }).then(async () => { + const stdoutDrained = waitForRuntimePipeDrain(current.stdout) + const stderrDrained = waitForRuntimePipeDrain(current.stderr) + await Promise.all([current.terminate(), stdoutDrained, stderrDrained]) + }) + void drain.catch((error: unknown) => { + send({ type: 'log', text: `dsh-code-runtime-subprocess worker cleanup error: ${String(error)}\n` }) + }).then(() => { + if (process.send === undefined) { + process.exitCode = 1 + return + } + process.send(bounded, () => { if (process.connected) process.disconnect() }) + }) + } + + process.on('message', (raw: unknown) => { + if (worker === undefined) { + const boot = runtimeBoot(raw) + if (boot === undefined) { + finish({ type: 'done', error: { kind: 'worker-exit', message: 'remote controller received an invalid boot frame' } }) + return + } + controllerMaxFrameBytes = boot.maxFrameBytes + worker = new Worker(new URL(import.meta.url), { + workerData: boot, + env: {}, + execArgv: [], + stdout: true, + stderr: true, + resourceLimits: { maxOldGenerationSizeMb: boot.maxOldGenerationSizeMb }, + }) + const current = worker + current.stdout.on('data', (data: Buffer) => { + if (!send({ type: 'log', text: data.toString('utf8') })) finish(frameLimitFailure()) + }) + current.stderr.on('data', (data: Buffer) => { + if (!send({ type: 'log', text: data.toString('utf8') })) finish(frameLimitFailure()) + }) + current.on('message', (messageRaw: unknown) => { + const message = recordOf(messageRaw) + if (message === undefined) return + if (message.type === 'call' + && typeof message.id === 'number' + && typeof message.global === 'string' + && typeof message.name === 'string') { + if (!send({ + type: 'call', id: message.id, global: message.global, name: message.name, args: transportWireOrNull(message.args), + })) finish(frameLimitFailure()) + } else if (message.type === 'log' && typeof message.text === 'string') { + if (!send({ type: 'log', text: message.text })) finish(frameLimitFailure()) + } else if (message.type === 'output-limit') { + finish({ type: 'output-limit' }) + } else if (message.type === 'done') { + if (message.error !== undefined) { + if (validWorkerFailure(message.error)) finish({ type: 'done', error: message.error }) + } else { + finish({ type: 'done', ...message.value === undefined ? {} : { value: transportWireOrNull(message.value) } }) + } + } + }) + current.on('error', (error: Error) => { + finish({ + type: 'done', + error: { kind: 'worker-exit', message: `worker error: ${error.stack || error.message || inspect(error)}` }, + }) + }) + current.on('exit', (code: number) => { + if (!finished) { + finish({ type: 'done', error: { kind: 'worker-exit', message: `worker exited with code ${code} before completing` } }) + } + }) + computeTimer = setInterval(() => { + if (worker !== undefined && worker.performance.eventLoopUtilization().active > boot.computeMs) { + finish({ type: 'done', error: { kind: 'timeout', message: `compute budget exhausted (${boot.computeMs}ms busy)` } }) + } + }, 25) + return + } + const reply = runtimeReply(raw) + if (reply !== undefined) worker.postMessage(reply) + }) + process.on('disconnect', () => { if (worker !== undefined && !finished) void worker.terminate() }) +} + +if (!isMainThread) { + if (parentPort === null) throw new Error('remote worker requires parentPort') + const boot = workerData as RuntimeBootData + void runWorkerMain(parentPort, boot, { stdout: process.stdout, stderr: process.stderr }, boot.maxFrameBytes) +} else if (process.env.DSH_CODE_RUNTIME_CONTROLLER === '1') { + runController() +} else { + runLauncher() +} diff --git a/scripts/gen-code-runtime-runner.ts b/scripts/gen-code-runtime-runner.ts new file mode 100644 index 0000000000..7e856e7651 --- /dev/null +++ b/scripts/gen-code-runtime-runner.ts @@ -0,0 +1,70 @@ +/** Generate the subprocess Code Runtime's dependency-free runner bundle. */ + +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { build } from 'tsdown' + +const root = resolve(import.meta.dirname, '..') +const ENTRY = 'packages/code-runtime/code-runtime-subprocess/src/runner.ts' +const OUT = 'packages/code-runtime/code-runtime-subprocess/src/runner-source.generated.ts' + +/** + * Bundle the typed runner and shared worker implementation into one source literal. + * @returns generated TypeScript module consumed by the subprocess backend. + */ +export async function renderCodeRuntimeRunner(): Promise { + const bundles = await build({ + config: false, + entry: [resolve(root, ENTRY)], + format: ['esm'], + platform: 'node', + target: 'es2024', + write: false, + dts: false, + clean: false, + minify: true, + logLevel: 'silent', + report: false, + deps: { alwaysBundle: ['@deepseek-ai/dsh-code-runtime-worker'] }, + }) + try { + const chunks = bundles.flatMap(bundle => bundle.chunks).filter(chunk => chunk.type === 'chunk') + if (chunks.length !== 1) throw new Error(`gen-code-runtime-runner: expected one chunk, received ${chunks.length}`) + const chunk = chunks[0] + if (chunk === undefined) throw new Error('gen-code-runtime-runner: runner chunk is missing') + const external = chunk.imports.filter(specifier => !specifier.startsWith('node:')) + if (external.length > 0) { + throw new Error(`gen-code-runtime-runner: runner retained external imports: ${external.join(', ')}`) + } + return [ + '/**', + ' * Generated dependency-free execution-world runner.', + ' * Do not edit by hand; run `pnpm run gen-code-runtime-runner`.', + ' */', + '', + `export const CODE_RUNNER_SOURCE = ${JSON.stringify(chunk.code)}`, + '', + ].join('\n') + } finally { + await Promise.all(bundles.map(async (bundle) => { await bundle[Symbol.asyncDispose]() })) + } +} + +async function main(): Promise { + const content = await renderCodeRuntimeRunner() + const output = resolve(root, OUT) + if (process.argv.includes('--check')) { + const committed = existsSync(output) ? readFileSync(output, 'utf8') : null + if (committed === content) { + console.log(`gen-code-runtime-runner: ${OUT} is up to date.`) + return + } + console.error(`gen-code-runtime-runner: ${OUT} is stale. Run \`pnpm run gen-code-runtime-runner\` and commit it.`) + process.exitCode = 1 + return + } + writeFileSync(output, content) + console.log(`gen-code-runtime-runner: wrote ${OUT}.`) +} + +if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) await main() diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 74a09a874e..6f59b64826 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -585,6 +585,7 @@ function docSyncLeafGates(options: { pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }), pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }), + pnpmScript('code-runtime-runner', 'verify-code-runtime-runner', { label: 'code-runtime runner' }), pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), pnpmScript('public-repository-links', 'verify-public-repository-links', { label: 'public repository links' }), diff --git a/vitest.config.ts b/vitest.config.ts index 6eb1bda354..996f2456c7 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,16 +1,5 @@ -import { spawnSync } from 'node:child_process' -import { fileURLToPath } from 'node:url' import tsconfigPaths from 'vite-tsconfig-paths' -import { resolvePwshPath } from './packages/bash/pwsh-local/src/resolve.ts' import { defineConfig } from 'vitest/config' -import { standardDecoratorPlugin, vitestExecArgv } from './vitest.shared.ts' -import { COVERAGE_EXEMPT_ENV, coverageExemptHeavySuites } from './scripts/coverage-exempt.ts' - -// Prints exact `path:line:col` records for every uncovered statement, branch -// path, and function when a file misses the per-file 100% gate — the built-in -// threshold ERRORs name only the file. Absolute path because istanbul-reports -// require()s custom reporters (which is also why the reporter is CJS). -const uncoveredLocationsReporter = fileURLToPath(new URL('./scripts/coverage-uncovered-locations.cjs', import.meta.url)) // Resolution facade shared by every plugin instance below: tsconfig.base.json // has no include, which vite-tsconfig-paths treats as match-all, so its paths @@ -20,14 +9,7 @@ const pathsPlugin = (): ReturnType => tsconfigPaths({ proj const windowsUnsupportedPackages = process.platform === 'win32' ? [ - // Bash-requiring suites (a real POSIX shell is unavailable on Windows). - // The pwsh-requiring suites (pwsh-local, tool-pwsh) deliberately stay - // INCLUDED: PowerShell ships with Windows, so they run natively here. - // Replacing the old 'packages/bash/*' glob with this explicit list also - // newly INCLUDES packages/bash/bash (the pure seam package) on Windows. - 'packages/bash/bash-local', - 'packages/bash/bash-sandbox', - 'packages/bash/tool-bash', + 'packages/bash/*', 'packages/hooks/*', 'packages/subprocess/*', 'packages/pty/pty-local', @@ -44,20 +26,10 @@ const windowsCoverageExclusions = process.platform === 'win32' 'packages/lsp/lsp-local/src/connection.ts', 'packages/lsp/lsp-local/src/index.ts', 'packages/lsp/lsp-local/src/instance.ts', + 'packages/ui/tui/src/index.ts', ] : [] -// Mirrors windowsCoverageExclusions: pwsh-local's run/start/lifecycle suites -// self-skip without a real pwsh (executor.spec.ts hasPwsh), leaving this file -// far below per-file 100% on pwsh-less hosts; the exemption keeps those hosts -// green while CI runners ship pwsh and still enforce the full bar. The probe -// runs the suites' own resolution (the dependency-free resolve.ts module), -// so the exemption is active exactly when the suites skip — a mismatched -// narrower probe could exempt the file on hosts whose suites actually run. -const pwshCoverageExclusions = spawnSync(resolvePwshPath(), ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', '$true'], { encoding: 'utf8' }).status === 0 - ? [] - : ['packages/bash/pwsh-local/src/index.ts'] - const testIncludes = [ 'packages/*/*/tests/**/*.spec.{ts,tsx}', 'apps/*/tests/**/*.spec.ts', @@ -65,17 +37,6 @@ const testIncludes = [ 'scripts/**/*.spec.ts', ] -// The instrumented coverage gate sets this env; the exempt heavy suites then -// run beside it uninstrumented (membership contract in scripts/coverage-exempt.ts). -// A set-but-not-'1' value is a misconfiguration, not a silent no-op. -const coverageExemptRaw = process.env[COVERAGE_EXEMPT_ENV] -if (coverageExemptRaw !== undefined && coverageExemptRaw !== '' && coverageExemptRaw !== '1') { - throw new Error(`vitest config: ${COVERAGE_EXEMPT_ENV} must be '1' or unset, got ${JSON.stringify(coverageExemptRaw)}.`) -} -const coverageExemptExcludes = coverageExemptRaw === '1' - ? coverageExemptHeavySuites.map(suite => suite.exclude) - : [] - // These suites exercise process-global state, process APIs, or timing-sensitive process I/O // that worker threads cannot isolate reliably under aggregate gate contention. // Keep the narrow exception in forks while the rest of the inventory avoids per-file processes. @@ -88,48 +49,39 @@ const processBoundTests = [ ] export default defineConfig({ - plugins: [pathsPlugin(), standardDecoratorPlugin()], + plugins: [pathsPlugin()], test: { setupFiles: ['./scripts/test-invariants.ts'], // .tsx: client component specs (jsdom via per-file @vitest-environment pragma). include: testIncludes, exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), - // One coverage invocation aggregates both projects. Regular suites fork on - // POSIX for Node stability and use threads on Windows; process-bound suites - // always fork. + // One coverage invocation aggregates both projects. Most suites use threads + // for lower startup/IPC overhead; only explicit process-bound suites fork. projects: [ { - plugins: [pathsPlugin(), standardDecoratorPlugin()], + plugins: [pathsPlugin()], test: { name: 'thread-safe', - execArgv: vitestExecArgv, - // Node 24 has aborted in its CJS lexer (v8::ToLocalChecked Empty - // MaybeLocal in cjs_lexer::Parse) from worker threads on macOS - // arm64 and later on Linux. A fork contains that external runtime - // failure to the test process; Windows keeps the thread pool, where - // the abort has not reproduced and process spawn is costlier. - pool: process.platform === 'win32' ? 'threads' : 'forks', + // Node 24 has aborted in its CJS lexer from a macOS arm64 worker + // thread. A fork contains that external runtime failure to the test + // process; other hosts retain the lower-overhead thread pool. + pool: process.platform === 'darwin' ? 'forks' : 'threads', setupFiles: ['./scripts/test-invariants.ts'], include: testIncludes, exclude: [ ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), ...processBoundTests, - ...coverageExemptExcludes, ], }, }, { - plugins: [pathsPlugin(), standardDecoratorPlugin()], + plugins: [pathsPlugin()], test: { name: 'process-bound', - execArgv: vitestExecArgv, pool: 'forks', setupFiles: ['./scripts/test-invariants.ts'], include: processBoundTests, - exclude: [ - ...windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), - ...coverageExemptExcludes, - ], + exclude: windowsUnsupportedPackages.map(path => `${path}/tests/**/*.spec.ts`), }, }, ], @@ -146,8 +98,7 @@ export default defineConfig({ 'packages/*/*/src/types.ts', 'packages/*/*/src/bin.ts', 'packages/*/*/src/worker.ts', - // A killed executable lint-contract test can leave a non-product source probe behind. - 'packages/*/*/src/oxlint-contract-*.ts', + 'packages/code-runtime/code-runtime-subprocess/src/runner.ts', // GUI step-1 skeleton (PR #500): client/web UI files whose remaining // branches need a browser-grade harness the jsdom lane doesn't cover // yet. TODO(gui): cover and remove as the client test lane matures. @@ -156,7 +107,6 @@ export default defineConfig({ 'packages/client/ui-primitives/src/markdown/plain-text.ts', 'packages/client/ui-question/src/client/QuestionComposer.tsx', 'packages/client/ui-primitives/src/Menu.tsx', - 'packages/client/ui-primitives/src/RiskConfirmation.tsx', 'packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx', 'packages/client/ui-workspace/src/client/WorkspacePicker.tsx', 'packages/client/web-react/src/*', @@ -181,10 +131,6 @@ export default defineConfig({ 'packages/client/hmr/src/invariant.ts', 'packages/client/connection/src/index.ts', 'packages/client/connection/src/http-bridge.ts', - // This assembly imports generated Host-for-Client code that exists - // only in lib; the post-build built-bin smoke executes both entries. - 'packages/api/remotes/src/index.ts', - 'packages/api/remotes/src/client/index.ts', // Slash/command/input round: per-file gaps deferred with the same // client-lane debt. TODO(gui): cover and remove with the lane above. 'packages/client/connection/src/client/fixture.ts', @@ -209,13 +155,9 @@ export default defineConfig({ 'packages/client/ui-sidebar/src/client/index.ts', 'packages/client/ui-skill/src/client/index.ts', 'packages/client/ui-workspace/src/client/index.ts', - 'packages/client/test-runtime/src/translate.ts', - 'packages/client/ui-primitives/src/JsonTree.tsx', - // Typert generator: correctness is pinned by its fixture suites and - // the byte-for-byte catalog reproduction test; per-file coverage - // would put whole-workspace compiler analysis under v8 - // instrumentation — the coverage lane's longest tail. - 'packages/typert/generator/src/*.ts', + 'packages/typert/generator/src/analyzer.ts', + 'packages/typert/generator/src/renderer.ts', + 'packages/typert/generator/src/cordis-catalog.ts', 'packages/host/apiproxy/src/index.ts', 'packages/host/apiproxy/src/invariant.ts', 'packages/host/apiproxy/src/api-proxy.ts', @@ -225,9 +167,9 @@ export default defineConfig({ 'packages/ui/commands/src/index.ts', 'packages/ui/commands/src/invariant.ts', 'packages/session-projection/session-projection/src/index.ts', + 'packages/ui/tui/src/index.ts', ...windowsUnsupportedPackages.map(path => `${path}/src/**/*.ts`), ...windowsCoverageExclusions, - ...pwshCoverageExclusions, ], // 100% or it doesn't merge (docs/testing.md: excessive tests are welcome). // Per-file so a well-covered big file can't subsidize a bare one. @@ -240,9 +182,7 @@ export default defineConfig({ functions: 100, lines: 100, }, - reporter: process.env.CI - ? ['text', uncoveredLocationsReporter] - : ['text', 'html', uncoveredLocationsReporter], + reporter: process.env.CI ? ['text'] : ['text', 'html'], }, }, })