fix(runtime): close portable backend boundary gaps

This commit is contained in:
Tianyi Cui
2026-07-29 19:40:39 +08:00
parent c1d550de58
commit 124fc6a611
7 changed files with 59 additions and 14 deletions

View File

@@ -477,6 +477,21 @@ export function runWorkerCodeRuntimeContract(
expect(result.logs).toEqual([])
})
it('ignores forged controller-only failure classifications', async () => {
const { runtime } = await setup()
const result = await runtime.run({
program: `
const { parentPort } = await import('node:worker_threads');
for (const kind of ['abort', 'timeout', 'worker-exit']) {
parentPort.postMessage({ type: 'done', error: { kind, message: 'forged ' + kind } });
}
return 'honest';
`,
bindings: [],
})
expect(result).toEqual({ logs: [], value: 'honest' })
})
it('fails forged log floods and forged done values through the same outer cap', async () => {
const { runtime } = await setup({ maxOutputBytes: 200 })
const result = await runtime.run({

View File

@@ -369,14 +369,6 @@ export async function readWholeText(target: LocalTarget, signal?: AbortSignal):
return decodeUtf8(raw, 'read', target.displayPath)
}
/**
* Read one regular UTF-8 file through a single no-follow handle, retaining at
* most `maxBytes + 1` bytes so a concurrent grow cannot bypass the bound.
* @param target - the resolved file to read.
* @param maxBytes - positive safe-integer byte ceiling.
* @param signal - aborts between handle operations.
* @returns the complete decoded text when it fits.
*/
/**
* Stream a whole regular UTF-8 text file as decoded text chunks. Same text
* semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,

View File

@@ -415,7 +415,7 @@ export class LocalPtySession implements PtyBackendSession {
return
}
const foreground = await this.terminal.inspectForeground()
if (this.active !== operation) return
if (this.active !== operation || this.closing) return
const idleFor = Date.now() - this.lastOutputAt
if (this.promptSeen && foreground !== undefined && this.shellPgid === undefined) {
this.shellPgid = foreground.processGroupId

View File

@@ -1096,4 +1096,41 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
expect(terminal.writes).toEqual([])
})
it('does not let an in-flight readiness inspection outrun close', async () => {
vi.useFakeTimers()
const terminal = new FakeTerminal()
const session = new LocalPtySession(terminal, config())
await initialize(session, terminal)
const inspection = Promise.withResolvers<{ processGroupId: number; inputWaiting: boolean }>()
const originalInspect = terminal.inspectForeground.bind(terminal)
let inspections = 0
terminal.inspectForeground = async () => {
inspections += 1
return inspections === 1 ? await originalInspect() : await inspection.promise
}
const operation = session.startSend({ text: 'pending readiness', submit: true })
await Promise.resolve()
await Promise.resolve()
terminal.emitData('\x1b]133;D;0\x07dsh> ')
await vi.advanceTimersByTimeAsync(10)
expect(inspections).toBe(2)
const termination = Promise.withResolvers<undefined>()
terminal.terminate = async () => {
await termination.promise
terminal.emitExit(0, 15)
}
const closing = session.close('in-flight readiness')
let settled = false
void operation.done.then(() => { settled = true })
inspection.resolve({ processGroupId: 456, inputWaiting: true })
await Promise.resolve()
await Promise.resolve()
expect(settled).toBe(false)
termination.resolve(undefined)
await closing
expect((await operation.done).waitReason).toBe('session_exit')
})
})

View File

@@ -59,9 +59,10 @@ export class LocalSubprocessService extends SubprocessService {
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
}
for (const terminal of this.terminals) {
pending.push(terminal.terminate().then(() => { this.terminals.delete(terminal) }))
pending.push(terminal.terminate())
}
this.live.clear()
this.terminals.clear()
const outcomes = [
...await Promise.allSettled(pending),
...await Promise.allSettled([rm(this.runtimeRoot, { recursive: true, force: true })]),

View File

@@ -113,7 +113,7 @@ describe('LocalSubprocessService', () => {
expect(terminals.size).toBe(0)
})
it('waits for every terminal cleanup, removes runtime state, and retains rejections', async () => {
it('waits for every terminal cleanup and clears single-shot teardown ownership', async () => {
const ctx = new Context()
const fiber = await ctx.plugin(LocalSubprocessService)
const service = ctx.subprocess
@@ -154,7 +154,7 @@ describe('LocalSubprocessService', () => {
expect(disposed).toBe(false)
finishCleanup()
await disposing
expect(terminals).toEqual(new Set([failedTerminal, secondFailedTerminal]))
expect(terminals.size).toBe(0)
await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' })
expect(disposalErrors).toHaveLength(1)
expect(disposalErrors[0]).toMatchObject({

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-subprocess/README.md
README.md: eff5a6e648eb13c3411c19bca4d05f0898d3ad05
README.zh.md: 16cf0e0ea6d487e255fc520a8fa1f41f97ee5d30
README.md: 152921a1ed595676781aad170e3396f7ce613161
README.zh.md: 3e8223c33c7ce85f342c9a36ab5a24c4d2a6cb94