mirror of
https://github.com/zhu1090093659/dsh-web.git
synced 2026-09-28 14:24:03 +08:00
The DSH loader mounts every patch row as one transactional group: a single plugin that fails to import or start rolls back the whole family and aborts 'dsh web'. Redefine the fault unit to one plugin. - scripts/aggregate.mjs emits each family insert row with the aggregate's own never-failing shell module as the row name and the real plugin package in the row config (config.plugin, original config under config.config); dsh-i18n and all external npm rows keep mounting directly - @linxin666/dsh-web-all main entry is the shell: it imports the real module at start time, contains import/shape/activation failures to that entry (logged + recorded), and mounts the real plugin as a nested plugin so cordis service scoping, lifecycle, and retraction semantics stay intact - loopback-only GET /api/dsh-web-all/degraded reports the degradation ledger for doctor/monitoring consumption - shared/host/run-guarded.ts (+ synced copies in the four packages with in-process HTTP/poll faces) converts fire-and-forget rejections into logged errors against the host's installFailLoud whole-process exit - tests: shell-isolation.spec.ts runs the real installed host boot through start-failure / import-failure / no-webServer scenarios plus the control case proving today's direct-mount shape still kills the boot Evidence: pnpm typecheck, pnpm test (22 packages), pnpm docs:check, pnpm i18n:check, pnpm aggregate:check, pnpm test:scripts (234), and full pnpm build all pass. Bundle-layer change: needs a dsh web restart.
62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { guardedHandler, runGuarded } from '../host/run-guarded.js'
|
|
|
|
describe('runGuarded', () => {
|
|
it('consumes rejections of fire-and-forget promises', async () => {
|
|
const log = vi.fn()
|
|
const boom = Promise.reject(new Error('boom'))
|
|
const returned = runGuarded(boom, 'site', log)
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
expect(log).toHaveBeenCalledTimes(1)
|
|
expect(String(log.mock.calls[0][0])).toContain('[site] unhandled async failure: Error: boom')
|
|
expect(returned).toBe(boom)
|
|
})
|
|
|
|
it('leaves resolved values untouched', async () => {
|
|
const log = vi.fn()
|
|
const value = await runGuarded(Promise.resolve(42), 'site', log)
|
|
expect(value).toBe(42)
|
|
expect(log).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('guardedHandler', () => {
|
|
it('guards sync throws per invocation', () => {
|
|
const log = vi.fn()
|
|
const handler = guardedHandler('site', () => {
|
|
throw new Error('sync boom')
|
|
}, log)
|
|
expect(handler()).toBeUndefined()
|
|
expect(log).toHaveBeenCalledTimes(1)
|
|
expect(String(log.mock.calls[0][0])).toContain('sync failure')
|
|
})
|
|
|
|
it('guards async rejections per invocation', async () => {
|
|
const log = vi.fn()
|
|
const handler = guardedHandler('site', async () => {
|
|
throw new Error('async boom')
|
|
}, log)
|
|
expect(handler()).toBeUndefined()
|
|
await new Promise(resolve => setTimeout(resolve, 0))
|
|
expect(log).toHaveBeenCalledTimes(1)
|
|
expect(String(log.mock.calls[0][0])).toContain('async boom')
|
|
})
|
|
|
|
it('passes sync return values through', () => {
|
|
const handler = guardedHandler('site', (a: number) => a * 2, vi.fn())
|
|
expect(handler(21)).toBe(42)
|
|
})
|
|
|
|
it('guards each invocation independently', async () => {
|
|
const log = vi.fn()
|
|
const handler = guardedHandler('site', (fail: boolean) => {
|
|
if (fail) throw new Error('no')
|
|
return 'ok'
|
|
}, log)
|
|
expect(handler(false)).toBe('ok')
|
|
expect(handler(true)).toBeUndefined()
|
|
expect(handler(false)).toBe('ok')
|
|
expect(log).toHaveBeenCalledTimes(1)
|
|
expect(log.mock.calls[0]).toBeTruthy()
|
|
})
|
|
}) |