fix(runtime): close portable backend boundary gaps
This commit is contained in:
@@ -477,6 +477,21 @@ export function runWorkerCodeRuntimeContract(
|
|||||||
expect(result.logs).toEqual([])
|
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 () => {
|
it('fails forged log floods and forged done values through the same outer cap', async () => {
|
||||||
const { runtime } = await setup({ maxOutputBytes: 200 })
|
const { runtime } = await setup({ maxOutputBytes: 200 })
|
||||||
const result = await runtime.run({
|
const result = await runtime.run({
|
||||||
|
|||||||
@@ -369,14 +369,6 @@ export async function readWholeText(target: LocalTarget, signal?: AbortSignal):
|
|||||||
return decodeUtf8(raw, 'read', target.displayPath)
|
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
|
* Stream a whole regular UTF-8 text file as decoded text chunks. Same text
|
||||||
* semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,
|
* semantics as {@link readWholeText} (regular-file check, binary/NUL rejection,
|
||||||
|
|||||||
@@ -415,7 +415,7 @@ export class LocalPtySession implements PtyBackendSession {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const foreground = await this.terminal.inspectForeground()
|
const foreground = await this.terminal.inspectForeground()
|
||||||
if (this.active !== operation) return
|
if (this.active !== operation || this.closing) return
|
||||||
const idleFor = Date.now() - this.lastOutputAt
|
const idleFor = Date.now() - this.lastOutputAt
|
||||||
if (this.promptSeen && foreground !== undefined && this.shellPgid === undefined) {
|
if (this.promptSeen && foreground !== undefined && this.shellPgid === undefined) {
|
||||||
this.shellPgid = foreground.processGroupId
|
this.shellPgid = foreground.processGroupId
|
||||||
|
|||||||
@@ -1096,4 +1096,41 @@ describe('LocalPtySession bounds, signals, and teardown', () => {
|
|||||||
expect(terminal.writes).toEqual([])
|
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')
|
||||||
|
})
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,9 +59,10 @@ export class LocalSubprocessService extends SubprocessService {
|
|||||||
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
|
pending.push(handle.done.catch(() => {}).then(() => handle.waitForExit()))
|
||||||
}
|
}
|
||||||
for (const terminal of this.terminals) {
|
for (const terminal of this.terminals) {
|
||||||
pending.push(terminal.terminate().then(() => { this.terminals.delete(terminal) }))
|
pending.push(terminal.terminate())
|
||||||
}
|
}
|
||||||
this.live.clear()
|
this.live.clear()
|
||||||
|
this.terminals.clear()
|
||||||
const outcomes = [
|
const outcomes = [
|
||||||
...await Promise.allSettled(pending),
|
...await Promise.allSettled(pending),
|
||||||
...await Promise.allSettled([rm(this.runtimeRoot, { recursive: true, force: true })]),
|
...await Promise.allSettled([rm(this.runtimeRoot, { recursive: true, force: true })]),
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ describe('LocalSubprocessService', () => {
|
|||||||
expect(terminals.size).toBe(0)
|
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 ctx = new Context()
|
||||||
const fiber = await ctx.plugin(LocalSubprocessService)
|
const fiber = await ctx.plugin(LocalSubprocessService)
|
||||||
const service = ctx.subprocess
|
const service = ctx.subprocess
|
||||||
@@ -154,7 +154,7 @@ describe('LocalSubprocessService', () => {
|
|||||||
expect(disposed).toBe(false)
|
expect(disposed).toBe(false)
|
||||||
finishCleanup()
|
finishCleanup()
|
||||||
await disposing
|
await disposing
|
||||||
expect(terminals).toEqual(new Set([failedTerminal, secondFailedTerminal]))
|
expect(terminals.size).toBe(0)
|
||||||
await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' })
|
await expect(stat(runtimeRoot)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||||
expect(disposalErrors).toHaveLength(1)
|
expect(disposalErrors).toHaveLength(1)
|
||||||
expect(disposalErrors[0]).toMatchObject({
|
expect(disposalErrors[0]).toMatchObject({
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-subprocess/README.md
|
||||||
README.md: eff5a6e648eb13c3411c19bca4d05f0898d3ad05
|
README.md: 152921a1ed595676781aad170e3396f7ce613161
|
||||||
README.zh.md: 16cf0e0ea6d487e255fc520a8fa1f41f97ee5d30
|
README.zh.md: 3e8223c33c7ce85f342c9a36ab5a24c4d2a6cb94
|
||||||
|
|||||||
Reference in New Issue
Block a user