test: replace daemon fallback type assertions

This commit is contained in:
Jinjing
2026-09-12 22:38:09 -07:00
parent 281e0c2206
commit a74171feca
9 changed files with 102 additions and 44 deletions
@@ -11,6 +11,7 @@ import type {
MockSpawnerHandle,
NetConnectStubs
} from './daemon-init-mock-types'
import type { DaemonLauncher } from './daemon-spawner'
export type { MockAdapter, MockSpawner } from './daemon-init-mock-types'
@@ -58,7 +59,7 @@ export function createDaemonInitModuleFactories(state: DaemonInitMockState) {
// Why: both fakes are annotated with constructor types so the exported factories widen to
// MockSpawner/MockAdapter instead of leaking their private fields into declaration emit.
const MockDaemonSpawner: MockSpawnerConstructor = class MockDaemonSpawner {
readonly launcher: unknown
readonly launcher: DaemonLauncher
readonly ensureRunning: Mock
readonly resetHandle: Mock
readonly resetRespawnWindow: Mock
@@ -66,7 +67,7 @@ export function createDaemonInitModuleFactories(state: DaemonInitMockState) {
readonly getHandle: Mock
private socketCounter: number
private handle: MockSpawnerHandle | null
constructor(opts: { runtimeDir: string; launcher: unknown }) {
constructor(opts: { runtimeDir: string; launcher: DaemonLauncher }) {
this.launcher = opts.launcher
this.socketCounter = 0
this.handle = null
@@ -216,10 +216,7 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => {
const mod = await importFresh()
await mod.initDaemonPtyProvider(undefined, { macosLoginSessionWatch: true })
const launcher = spawnerInstances[0].launcher as (
socketPath: string,
tokenPath: string
) => Promise<{ mode?: string; shutdown(): Promise<void> }>
const launcher = spawnerInstances[0].launcher
getMacDaemonTccAttributionHealthMock.mockResolvedValueOnce('severed')
getDaemonLaunchIdentityMock.mockReturnValueOnce(identity)
// Why: live sessions must veto replacement — the Settings surface owns the remedy instead.
+3 -2
View File
@@ -1,4 +1,5 @@
import type { Mock } from 'vitest'
import type { DaemonLauncher } from './daemon-spawner'
/** Fake DaemonSpawner instance every mocked `new DaemonSpawner()` records. */
export type MockSpawner = {
@@ -7,7 +8,7 @@ export type MockSpawner = {
resetRespawnWindow: Mock
shutdown: Mock
getHandle: Mock
launcher: unknown
launcher: DaemonLauncher
}
/** Fake DaemonPtyAdapter instance every mocked `new DaemonPtyAdapter()` records. */
@@ -40,7 +41,7 @@ export type MockAdapter = {
export type MockSpawnerConstructor = new (opts: {
runtimeDir: string
launcher: unknown
launcher: DaemonLauncher
}) => MockSpawner
export type MockAdapterConstructor = new (opts: MockAdapter['options']) => MockAdapter
@@ -359,8 +359,11 @@ describe('daemon-init: runRestartDaemon (7-step sequence)', () => {
getMacDaemonTccAttributionHealthMock.mockResolvedValue('severed')
const { DegradedDaemonPtyProvider } = await import('./degraded-daemon-pty-provider')
const provider = mod.getDaemonProvider() as InstanceType<typeof DegradedDaemonPtyProvider>
const provider = mod.getDaemonProvider()
expect(provider).toBeInstanceOf(DegradedDaemonPtyProvider)
if (!(provider instanceof DegradedDaemonPtyProvider)) {
throw new Error('Expected degraded daemon provider')
}
await expect(provider.recoverFreshSpawnRouting()).resolves.toBe(false)
expect(provider.routesFreshSpawnsToLocalProvider).toBe(true)
@@ -2,27 +2,27 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
inProcessProvider,
installedProvider,
degradedInstances,
isDaemonRestartInFlightMock,
getDaemonProviderMock,
getLocalPtyProviderMock,
replaceDaemonProviderMock,
rebindLocalProviderListenersMock,
checkDaemonHealthMock,
getMacDaemonTccAttributionHealthMock
} = vi.hoisted(() => {
const inProcessProvider = { kind: 'in-process' }
const installedProvider = { kind: 'installed-daemon-adapter' }
const degradedInstances: {
opts: Record<string, unknown>
discover: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
}[] = []
return {
inProcessProvider,
installedProvider,
degradedInstances: [] as {
opts: Record<string, unknown>
discover: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
}[],
degradedInstances,
isDaemonRestartInFlightMock: vi.fn(() => false),
getDaemonProviderMock: vi.fn((): unknown => installedProvider),
getDaemonProviderMock: vi.fn<() => unknown>(),
getLocalPtyProviderMock: vi.fn<() => unknown>(),
replaceDaemonProviderMock: vi.fn(),
rebindLocalProviderListenersMock: vi.fn(),
checkDaemonHealthMock: vi.fn(async () => 'healthy'),
@@ -34,7 +34,7 @@ vi.mock('../ipc/pty', () => ({
getInProcessPtyProvider: () => inProcessProvider,
// Why the installed provider here: this is what the real registry answers after install, and
// what the degraded fallback must never be.
getLocalPtyProvider: () => installedProvider,
getLocalPtyProvider: getLocalPtyProviderMock,
rebindLocalProviderListeners: rebindLocalProviderListenersMock
}))
vi.mock('./daemon-restart-state', () => ({ isDaemonRestartInFlight: isDaemonRestartInFlightMock }))
@@ -70,15 +70,19 @@ import {
createSeveredDaemonRecoveryProbe,
degradeInstalledProviderForSeveredDaemon
} from './daemon-severed-attribution-degrade'
import type { DaemonPtyAdapter } from './daemon-pty-adapter'
import { DaemonPtyAdapter } from './daemon-pty-adapter'
const installedAdapter = installedProvider as unknown as DaemonPtyAdapter
const installedAdapter = new DaemonPtyAdapter({
socketPath: '/runtime/daemon.sock',
tokenPath: '/runtime/token'
})
beforeEach(() => {
degradedInstances.length = 0
vi.clearAllMocks()
isDaemonRestartInFlightMock.mockReturnValue(false)
getDaemonProviderMock.mockReturnValue(installedProvider)
getDaemonProviderMock.mockReturnValue(installedAdapter)
getLocalPtyProviderMock.mockReturnValue(installedAdapter)
checkDaemonHealthMock.mockResolvedValue('healthy')
getMacDaemonTccAttributionHealthMock.mockResolvedValue('intact')
})
@@ -106,8 +110,8 @@ describe('degradeInstalledProviderForSeveredDaemon', () => {
expect(degradedInstances).toHaveLength(1)
const [degraded] = degradedInstances
expect(degraded.opts.fallback).toBe(inProcessProvider)
expect(degraded.opts.fallback).not.toBe(installedProvider)
expect(degraded.opts.current).toBe(installedProvider)
expect(degraded.opts.fallback).not.toBe(installedAdapter)
expect(degraded.opts.current).toBe(installedAdapter)
expect(degraded.opts.legacy).toEqual([])
expect(replaceDaemonProviderMock).toHaveBeenCalledTimes(1)
expect(rebindLocalProviderListenersMock).toHaveBeenCalledTimes(1)
@@ -1,11 +1,11 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import { DegradedDaemonFreshSpawnRouter } from './degraded-daemon-fresh-spawn-routing'
import type { IPtyProvider } from '../providers/types'
import { createPtyProviderTestDouble } from '../providers/pty-provider-test-double'
describe('degraded fresh-spawn transitions', () => {
function setup(probe: () => Promise<boolean>) {
const current = { spawn: vi.fn(async () => ({ id: 'daemon' })) } as unknown as IPtyProvider
const fallback = { spawn: vi.fn(async () => ({ id: 'local' })) } as unknown as IPtyProvider
const current = createPtyProviderTestDouble('daemon')
const fallback = createPtyProviderTestDouble('local')
return {
current,
fallback,
@@ -11,6 +11,24 @@ import {
import { DegradedDaemonPtyProvider } from '../daemon/degraded-daemon-pty-provider'
import type { DaemonPtyAdapter } from '../daemon/daemon-pty-adapter'
function isDaemonAdapter(
provider: ReturnType<typeof getLocalPtyProvider>
): provider is DaemonPtyAdapter {
return typeof provider.onWriteUnavailable === 'function'
}
function getSpawnResultId(result: unknown): string {
if (
typeof result !== 'object' ||
result === null ||
!('id' in result) ||
typeof result.id !== 'string'
) {
throw new Error('Expected PTY spawn result')
}
return result.id
}
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
vi.mock('node-pty', () => import('./pty-ipc-mock-registry').then((m) => m.nodePtyModuleMock()))
@@ -61,10 +79,14 @@ describe('configured in-process fallback lifecycle', () => {
function setup() {
const daemon = installObservableDaemonTestProvider()
getLocalPtyProvider().onWriteUnavailable = vi.fn(() => () => {})
const current = getLocalPtyProvider()
current.onWriteUnavailable = vi.fn(() => () => {})
if (!isDaemonAdapter(current)) {
throw new Error('Expected daemon adapter test double')
}
const fallback = getInProcessPtyProvider()
const provider = new DegradedDaemonPtyProvider({
current: getLocalPtyProvider() as DaemonPtyAdapter,
current,
legacy: [],
fallback
})
@@ -88,9 +110,7 @@ describe('configured in-process fallback lifecycle', () => {
it('delivers local and daemon output and natural exits exactly once', async () => {
const { daemon, provider, runtime, process } = setup()
try {
const result = (await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 })) as {
id: string
}
const id = getSpawnResultId(await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }))
process.emitData('abc')
expect(runtime.onPtyData).toHaveBeenCalledTimes(1)
process.emitExit()
@@ -101,7 +121,7 @@ describe('configured in-process fallback lifecycle', () => {
expect(runtime.onPtyExit).toHaveBeenCalledTimes(2)
expect(mainWindow.webContents.send).toHaveBeenCalledWith(
'pty:exit',
expect.objectContaining({ id: result.id })
expect.objectContaining({ id })
)
} finally {
provider.disposeProviderOnly()
@@ -111,13 +131,11 @@ describe('configured in-process fallback lifecycle', () => {
it('destroys fallback handles on app quit without stopping daemon sessions', async () => {
const { daemon, fallback, provider, process } = setup()
try {
const result = (await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 })) as {
id: string
}
const id = getSpawnResultId(await handlers.get('pty:spawn')!(null, { cols: 80, rows: 24 }))
const kill = process.proc.kill
killAllPty()
expect(kill).toHaveBeenCalledOnce()
expect(fallback.hasPty(result.id)).toBe(false)
expect(fallback.hasPty(id)).toBe(false)
expect(daemon.shutdown).not.toHaveBeenCalled()
} finally {
provider.disposeProviderOnly()
@@ -131,8 +149,8 @@ describe('configured in-process fallback lifecycle', () => {
const kill = process.proc.kill
const reload = () => {
for (const [name, callback] of mainWindow.webContents.on.mock.calls) {
if (name === 'did-finish-load') {
;(callback as () => void)()
if (name === 'did-finish-load' && typeof callback === 'function') {
callback()
}
}
}
@@ -1,5 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { LocalPtyProvider } from '../../../providers/local-pty-provider'
import { createPtyProviderTestDouble } from '../../../providers/pty-provider-test-double'
import { configureLocalPtyProvider } from './local-configure'
import { getInProcessPtyProvider, getLocalPtyProvider, setLocalPtyProvider } from './registry'
@@ -12,7 +13,7 @@ afterEach(() => {
describe('configureLocalPtyProvider', () => {
it('configures the in-process provider once when it is also the installed one', () => {
const inProcess = getInProcessPtyProvider() as LocalPtyProvider
const inProcess = getInProcessPtyProvider()
expect(getLocalPtyProvider()).toBe(inProcess)
const configure = vi.spyOn(inProcess, 'configure')
@@ -25,9 +26,9 @@ describe('configureLocalPtyProvider', () => {
// Why: the daemon lands before handlers register, so this is the production order. Degraded
// routing spawns fresh terminals on the in-process provider, which must carry hook env and
// runtime callbacks even though it is no longer what IPC routes through.
const daemonTopology = { isDegraded: false } as never
const daemonTopology = createPtyProviderTestDouble('daemon')
setLocalPtyProvider(daemonTopology)
const inProcess = getInProcessPtyProvider() as LocalPtyProvider
const inProcess = getInProcessPtyProvider()
const configure = vi.spyOn(inProcess, 'configure')
configureLocalPtyProvider({ trustedTerminalHandleEnv: new Set() })
@@ -40,7 +41,7 @@ describe('configureLocalPtyProvider', () => {
const installed = new LocalPtyProvider()
setLocalPtyProvider(installed)
const installedConfigure = vi.spyOn(installed, 'configure')
const inProcessConfigure = vi.spyOn(getInProcessPtyProvider() as LocalPtyProvider, 'configure')
const inProcessConfigure = vi.spyOn(getInProcessPtyProvider(), 'configure')
configureLocalPtyProvider({ trustedTerminalHandleEnv: new Set() })
@@ -0,0 +1,33 @@
import { vi, type Mock } from 'vitest'
import { settledWriteStub } from './settled-pty-write-stub'
import type { IPtyProvider, PtySpawnOptions, PtySpawnResult } from './types'
export type PtyProviderTestDouble = IPtyProvider & {
spawn: Mock<(opts: PtySpawnOptions) => Promise<PtySpawnResult>>
}
export function createPtyProviderTestDouble(id: string): PtyProviderTestDouble {
return {
spawn: vi.fn(async () => ({ id })),
attach: vi.fn(async () => {}),
write: vi.fn(),
writeWithSettlement: vi.fn(settledWriteStub()),
resize: vi.fn(),
shutdown: vi.fn(async () => {}),
sendSignal: vi.fn(async () => {}),
getCwd: vi.fn(async () => ''),
getInitialCwd: vi.fn(async () => ''),
clearBuffer: vi.fn(async () => {}),
acknowledgeDataEvent: vi.fn(),
hasChildProcesses: vi.fn(async () => false),
getForegroundProcess: vi.fn(async () => null),
serialize: vi.fn(async () => '{}'),
revive: vi.fn(async () => {}),
listProcesses: vi.fn(async () => []),
getDefaultShell: vi.fn(async () => '/bin/zsh'),
getProfiles: vi.fn(async () => []),
onData: vi.fn(() => () => {}),
onReplay: vi.fn(() => () => {}),
onExit: vi.fn(() => () => {})
}
}