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:
@@ -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 {
|
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 {
|
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')
|
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 () => {
|
it('interrupts Loader boot and contains every late boot outcome', async () => {
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
const lateContext = new Context()
|
const lateContext = new Context()
|
||||||
|
|||||||
Reference in New Issue
Block a user