import { Context } from 'cordis' import { describe, expect, it, vi } from 'vitest' import type { Agent } from '@deepseek-ai/dsh-agent' import type { Component, OverlayHandle, } from '@earendil-works/pi-tui' import type { TuiComponent, TuiOverlayHost, TuiOverlayOptions, TuiOverlaySession, TuiTheme, } from '../src/extension.ts' import { TuiExtensionServiceImpl, TuiOverlayManager, type TuiOverlayDriver, } from '../src/overlay-manager.ts' const theme: TuiTheme = Object.freeze({ text: (value: string) => `text:${value}`, muted: (value: string) => `muted:${value}`, dim: (value: string) => `dim:${value}`, accent: (value: string) => `accent:${value}`, success: (value: string) => `success:${value}`, warning: (value: string) => `warning:${value}`, error: (value: string) => `error:${value}`, bold: (value: string) => `bold:${value}`, }) interface ShownOverlay { component: Component options: TuiOverlayOptions | undefined hidden: boolean focused: boolean } interface DriverFixture { driver: TuiOverlayDriver shown: ShownOverlay[] errors: unknown[] invalidations: number showError?: unknown onShow?: (component: Component) => void } function driverFixture(): DriverFixture { const fixture: DriverFixture = { shown: [], errors: [], invalidations: 0, driver: undefined as never, } fixture.driver = { viewport: () => ({ columns: 96, rows: 32 }), theme: () => theme, display: value => `safe:${value}`, show(component, options) { if (fixture.showError !== undefined) throw fixture.showError const shown: ShownOverlay = { component, options, hidden: false, focused: true, } fixture.shown.push(shown) const handle: OverlayHandle = { hide() { shown.hidden = true shown.focused = false }, setHidden(hidden) { shown.hidden = hidden }, isHidden: () => shown.hidden, focus() { shown.focused = true }, unfocus() { shown.focused = false }, isFocused: () => shown.focused, } fixture.onShow?.(component) return handle }, invalidate() { fixture.invalidations += 1 }, reportError(error) { fixture.errors.push(error) }, } return fixture } function component(lines = ['overlay']): TuiComponent { return { render: () => lines, invalidate() {}, } } async function microtask(): Promise { await Promise.resolve() await Promise.resolve() } describe('TuiOverlayManager', () => { it('serializes overlays, exposes the constrained host, and settles normal close once', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) let firstHost: TuiOverlayHost | undefined const firstComponent = { focused: false, wantsKeyRelease: true, inputs: [] as string[], invalidated: 0, render: (width: number) => [`first:${String(width)}`], handleInput(data: string) { this.inputs.push(data) }, invalidate() { this.invalidated += 1 }, } const first = manager.open({ create(host) { firstHost = host return firstComponent }, options: { width: '75%', minWidth: 24, maxHeight: 20, anchor: 'center', margin: { bottom: 1 } }, }) const secondOptions: TuiOverlayOptions = { width: 40, margin: { bottom: 2 } } const second = manager.open({ create: () => component(['second']), options: secondOptions, }) ;(secondOptions as { width: number }).width = 80 ;(secondOptions.margin as { bottom: number }).bottom = 4 expect(manager.hasActiveOverlay()).toBe(true) expect(first.state).toBe('active') expect(second.state).toBe('queued') expect(fixture.shown).toHaveLength(1) expect(fixture.shown[0]?.options).toEqual({ width: '75%', minWidth: 24, maxHeight: 20, anchor: 'center', margin: { bottom: 1 }, }) expect(firstHost?.viewport).toEqual({ columns: 96, rows: 32 }) expect(Object.isFrozen(firstHost?.viewport)).toBe(true) expect(firstHost?.theme.accent('x')).toBe('accent:x') expect(firstHost?.display('\u001b')).toBe('safe:\u001b') firstHost?.invalidate() expect(firstComponent.invalidated).toBe(1) expect(fixture.shown[0]?.component.render(40)).toEqual(['first:40']) fixture.shown[0]!.component.handleInput?.('x') fixture.shown[0]!.component.invalidate() expect(firstComponent.inputs).toEqual(['x']) expect(firstComponent.invalidated).toBe(2) expect(fixture.shown[0]?.component.wantsKeyRelease).toBe(true) ;(fixture.shown[0]?.component as Component & { focused: boolean }).focused = true expect(firstComponent.focused).toBe(true) expect((fixture.shown[0]?.component as Component & { focused: boolean }).focused).toBe(true) const firstOutcome = await first.close() expect(firstOutcome).toEqual({ reason: 'closed' }) expect(await first.close()).toBe(firstOutcome) expect(firstHost?.signal.aborted).toBe(true) const beforeClosedInvalidation = fixture.invalidations firstHost?.invalidate() expect(fixture.invalidations).toBe(beforeClosedInvalidation) await microtask() expect(first.state).toBe('closed') expect(second.state).toBe('active') expect(fixture.shown[0]?.hidden).toBe(true) expect(fixture.shown[1]?.options).toEqual({ width: 40, margin: { bottom: 2 } }) expect(Object.isFrozen(fixture.shown[1]?.options)).toBe(true) expect(Object.isFrozen(fixture.shown[1]?.options?.margin)).toBe(true) expect(fixture.shown[1]?.component.wantsKeyRelease).toBe(false) expect((fixture.shown[1]?.component as Component & { focused: boolean }).focused).toBe(false) ;(fixture.shown[1]?.component as Component & { focused: boolean }).focused = true fixture.shown[1]!.component.handleInput?.('ignored') await second.close() await microtask() const numericMargin = manager.open({ create: () => component(['numeric margin']), options: { margin: 1 }, }) expect(fixture.shown[2]?.options).toEqual({ margin: 1 }) await numericMargin.close() await microtask() const emptyOptions = manager.open({ create: () => component(['empty options']), options: {}, }) expect(fixture.shown[3]?.options).toEqual({}) await emptyOptions.close() await microtask() expect(manager.hasActiveOverlay()).toBe(false) await manager.dispose() await manager.dispose() }) it('removes pre-aborted, active, and queued requests without activating cancelled work', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const preAborted = new AbortController() preAborted.abort() const pre = manager.open({ signal: preAborted.signal, create: () => component(['never']), }) expect(await pre.closed).toEqual({ reason: 'aborted' }) expect(fixture.shown).toHaveLength(0) const activeAbort = new AbortController() let activeHost: TuiOverlayHost | undefined const active = manager.open({ signal: activeAbort.signal, create(host) { activeHost = host return component(['active']) }, }) const queuedAbort = new AbortController() const queued = manager.open({ signal: queuedAbort.signal, create: () => component(['queued']), }) queuedAbort.abort() expect(await queued.closed).toEqual({ reason: 'aborted' }) expect(queued.state).toBe('closed') activeAbort.abort() expect(await active.closed).toEqual({ reason: 'aborted' }) expect(activeHost?.signal.aborted).toBe(true) await microtask() expect(fixture.shown).toHaveLength(1) expect(manager.hasActiveOverlay()).toBe(false) }) it('does not mount entries closed or aborted during component construction', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const closed = manager.open({ create(host) { host.invalidate() host.close() return component(['closed during construction']) }, }) await expect(closed.closed).resolves.toEqual({ reason: 'closed' }) const controller = new AbortController() const aborted = manager.open({ signal: controller.signal, create() { controller.abort() return component(['aborted during construction']) }, }) await expect(aborted.closed).resolves.toEqual({ reason: 'aborted' }) const after = manager.open({ create: () => component(['after construction closes']) }) expect(fixture.shown).toHaveLength(1) expect(fixture.shown[0]?.component.render(40)).toEqual(['after construction closes']) await after.close() }) it('hides a handle returned after reentrant closure during mounting', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) fixture.onShow = (shown) => { ;(shown as Component & { focused: boolean }).focused = true } const closed = manager.open({ create(host) { return { get focused(): boolean { return false }, set focused(_value: boolean) { host.close() }, render: () => ['closed during mount'], invalidate() {}, } }, }) await expect(closed.closed).resolves.toEqual({ reason: 'closed' }) expect(fixture.shown[0]?.hidden).toBe(true) expect(manager.hasActiveOverlay()).toBe(false) delete fixture.onShow const after = manager.open({ create: () => component(['after mount close']) }) expect(fixture.shown[1]?.hidden).toBe(false) expect(fixture.shown[1]?.component.render(40)).toEqual(['after mount close']) await after.close() }) it('stops admission and disposes active and queued overlays with the TUI', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const active = manager.open({ create: () => component(['active']) }) const queued = manager.open({ create: () => component(['queued']) }) manager.beginShutdown() expect(() => manager.open({ create: () => component() })).toThrow('TUI is shutting down') await manager.dispose() expect(await active.closed).toEqual({ reason: 'tui-disposed' }) expect(await queued.closed).toEqual({ reason: 'tui-disposed' }) expect(fixture.shown).toHaveLength(1) expect(fixture.shown[0]?.hidden).toBe(true) await manager.dispose() }) it('contains factory, mount, render, input, and invalidation failures', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const factoryError = new Error('factory failed') const factory = manager.open({ create() { throw factoryError }, }) const afterFactory = manager.open({ create: () => component(['after factory']) }) expect(await factory.closed).toEqual({ reason: 'error', error: factoryError }) await microtask() expect(afterFactory.state).toBe('active') await afterFactory.close() await microtask() const showError = new Error('show failed') fixture.showError = showError const show = manager.open({ create: () => component(['show']) }) expect(await show.closed).toEqual({ reason: 'error', error: showError }) delete fixture.showError await microtask() const renderError = new Error('render failed') const rendering = manager.open({ create: () => ({ render() { throw renderError }, invalidate() { throw new Error('must be suppressed after the first failure') }, }), }) const renderComponent = fixture.shown.at(-1)!.component expect(renderComponent.render(20)).toEqual([]) renderComponent.invalidate() expect(fixture.errors.filter(error => error === renderError)).toHaveLength(1) expect(await rendering.closed).toEqual({ reason: 'error', error: renderError }) await microtask() const inputError = new Error('input failed') const input = manager.open({ create: () => ({ render: () => ['input'], handleInput() { throw inputError }, invalidate() {}, }), }) fixture.shown.at(-1)!.component.handleInput?.('x') expect(await input.closed).toEqual({ reason: 'error', error: inputError }) await microtask() const invalidateError = new Error('invalidate failed') let invalidatingHost: TuiOverlayHost | undefined const invalidating = manager.open({ create(host) { invalidatingHost = host return { render: () => ['invalidate'], invalidate() { throw invalidateError }, } }, }) const invalidationsBeforeFailure = fixture.invalidations invalidatingHost?.invalidate() invalidatingHost?.invalidate() expect(fixture.invalidations).toBe(invalidationsBeforeFailure) expect(await invalidating.closed).toEqual({ reason: 'error', error: invalidateError }) await microtask() const focusError = new Error('focus failed') const focus = manager.open({ create: () => ({ get focused(): boolean { throw focusError }, set focused(_value: boolean) { throw new Error('focus assignment failed') }, get wantsKeyRelease(): boolean { throw new Error('key-release query failed') }, render: () => ['focus'], invalidate() {}, }), }) const guarded = fixture.shown.at(-1)!.component as Component & { focused: boolean } expect(guarded.focused).toBe(false) guarded.focused = true expect(guarded.wantsKeyRelease).toBe(false) expect(await focus.closed).toEqual({ reason: 'error', error: focusError }) expect(fixture.errors).toEqual([ factoryError, showError, renderError, inputError, invalidateError, focusError, ]) }) it('contains host redraw, overlay removal, and error-reporter failures', async () => { const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) let host: TuiOverlayHost | undefined const invalidationError = new Error('redraw failed') let redrawFails = false fixture.driver.invalidate = () => { if (redrawFails) throw invalidationError } fixture.driver.reportError = () => { throw new Error('report failed') } const invalidating = manager.open({ create(value) { host = value return component() }, }) redrawFails = true host?.invalidate() expect(await invalidating.closed).toEqual({ reason: 'error', error: invalidationError }) await microtask() redrawFails = false fixture.driver.invalidate = () => {} const hideError = new Error('hide failed') fixture.driver.show = () => ({ hide() { throw hideError }, setHidden() {}, isHidden: () => false, focus() {}, unfocus() {}, isFocused: () => true, }) const hiding = manager.open({ create(value) { host = value return component() }, }) host?.close() expect(await hiding.closed).toEqual({ reason: 'closed' }) }) }) describe('TuiExtensionService', () => { it('binds an open overlay to the calling plugin fiber', async () => { const ctx = new Context() const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const agent = {} as Agent const provider = ctx.plugin((providerCtx) => { new TuiExtensionServiceImpl(providerCtx, agent, manager) }) await provider let session: TuiOverlaySession | undefined let host: TuiOverlayHost | undefined const consumer = ctx.inject(['tui'], (consumerCtx) => { expect(consumerCtx.tui.agent).toBe(agent) session = consumerCtx.tui.openOverlay({ create(value) { host = value return component(['plugin']) }, }) }) await consumer expect(session?.state).toBe('active') await consumer.dispose() expect(await session?.closed).toEqual({ reason: 'owner-disposed' }) expect(host?.signal.aborted).toBe(true) await provider.dispose() await manager.dispose() await ctx.fiber.dispose() }) it('unloads and reloads dependent plugins with the mounted TUI service', async () => { const ctx = new Context() const agent = {} as Agent const sessions: TuiOverlaySession[] = [] let starts = 0 const consumer = ctx.inject(['tui'], (consumerCtx) => { starts += 1 sessions.push(consumerCtx.tui.openOverlay({ create: () => component([`start:${String(starts)}`]) })) }) const firstFixture = driverFixture() const firstManager = new TuiOverlayManager(firstFixture.driver) const firstProvider = ctx.plugin((providerCtx) => { new TuiExtensionServiceImpl(providerCtx, agent, firstManager) }) await firstProvider await consumer expect(starts).toBe(1) await firstProvider.dispose() expect(await sessions[0]?.closed).toEqual({ reason: 'owner-disposed' }) const secondFixture = driverFixture() const secondManager = new TuiOverlayManager(secondFixture.driver) const secondProvider = ctx.plugin((providerCtx) => { new TuiExtensionServiceImpl(providerCtx, agent, secondManager) }) await secondProvider await vi.waitFor(() => { expect(starts).toBe(2) }) await sessions[1]?.close() await consumer.dispose() await secondProvider.dispose() await firstManager.dispose() await secondManager.dispose() await ctx.fiber.dispose() }) it('rejects new service work after terminal shutdown begins', async () => { const ctx = new Context() const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const provider = ctx.plugin((providerCtx) => { new TuiExtensionServiceImpl(providerCtx, {} as Agent, manager) }) await provider manager.beginShutdown() const consumer = ctx.inject(['tui'], (consumerCtx) => { expect(() => consumerCtx.tui.openOverlay({ create: () => component() })) .toThrow('TUI is shutting down') }) await consumer await consumer.dispose() await provider.dispose() await manager.dispose() await ctx.fiber.dispose() }) it('does not admit an overlay when called from an unloading plugin', async () => { const ctx = new Context() const fixture = driverFixture() const manager = new TuiOverlayManager(fixture.driver) const provider = ctx.plugin((providerCtx) => { new TuiExtensionServiceImpl(providerCtx, {} as Agent, manager) }) await provider let error: unknown const consumer = ctx.inject(['tui'], (consumerCtx) => { consumerCtx.effect(() => () => { try { consumerCtx.tui.openOverlay({ create: () => component() }) } catch (value) { error = value } }) }) await consumer await consumer.dispose() expect(error).toMatchObject({ code: 'INACTIVE_EFFECT' }) expect(fixture.shown).toHaveLength(0) await provider.dispose() await manager.dispose() await ctx.fiber.dispose() }) })