fix(cli-demo): make failure rendering total

Contain arbitrary plugin and runtime failures even when a thrown Proxy traps instanceof checks or its string coercion throws. Fall back to a stable diagnostic instead of letting executeCli reject outside its exit-code contract.

Route abort reasons through the same total renderer so cancellation cannot escape containment through an exotic reason value.

Add a focused regression that exercises both hostile inspection paths and verifies stdout remains empty, stderr remains labelled, and the CLI resolves with exit code 1.
This commit is contained in:
Tianyi Cui
2026-07-19 14:41:55 +08:00
parent 1698f0baa6
commit 306dd2b1fe
2 changed files with 38 additions and 2 deletions
+17 -2
View File
@@ -91,12 +91,27 @@ class CliInterruptedError extends Error {
}
}
/** Render an arbitrary value without trusting its type traps or string coercion. */
function renderUnknown(value: unknown): string {
try {
return String(value)
} catch {
return '[unrenderable thrown value]'
}
}
/** Normalize an arbitrary thrown value without letting inspection escape containment. */
function toError(error: unknown): Error {
return error instanceof Error ? error : new Error(String(error))
try {
if (error instanceof Error) return error
} catch {
// A hostile proxy may throw during instanceof; use the total renderer below.
}
return new Error(renderUnknown(error))
}
function interruptionReason(signal: AbortSignal): string {
return signal.reason === undefined ? 'interrupted' : String(signal.reason)
return signal.reason === undefined ? 'interrupted' : renderUnknown(signal.reason)
}
/**
@@ -199,6 +199,27 @@ describe('runOneShot and executeCli', () => {
expect(stderr).toContain('boot exploded')
})
it('contains a thrown value whose inspection and coercion both fail', async () => {
const hostile = new Proxy({}, {
getPrototypeOf: () => { throw new Error('prototype trap escaped') },
get: (target, key, receiver) => {
if (key === Symbol.toPrimitive) throw new Error('coercion escaped')
return Reflect.get(target, key, receiver) as unknown
},
})
let stdout = ''
let stderr = ''
const code = await executeCli(['task'], {
boot: async () => { throw hostile },
loadEnv: () => {},
writeStdout: (chunk) => { stdout += chunk },
writeStderr: (chunk) => { stderr += chunk },
})
expect(code).toBe(1)
expect(stdout).toBe('')
expect(stderr).toBe('dsh-cli-demo: [unrenderable thrown value]\n')
})
it('interrupts Loader boot and contains every late boot outcome', async () => {
const abort = new AbortController()
const lateContext = new Context()