Просмотр исходного кода

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.
Tianyi Cui 2 месяцев назад
Родитель
Сommit
306dd2b1fe
2 измененных файлов с 38 добавлено и 2 удалено
  1. 17 2
      packages/examples/cli-demo/src/cli.ts
  2. 21 0
      packages/examples/cli-demo/tests/cli.spec.ts

+ 17 - 2
packages/examples/cli-demo/src/cli.ts

@@ -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)
 }
 
 /**

+ 21 - 0
packages/examples/cli-demo/tests/cli.spec.ts

@@ -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()