From a74171feca232f604dff3c71c2f814aca00e958c Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 12 Sep 2026 22:38:09 -0700 Subject: [PATCH] test: replace daemon fallback type assertions --- .../daemon/daemon-init-dependency-mocks.ts | 5 ++- .../daemon-init-endpoint-adoption.test.ts | 5 +-- src/main/daemon/daemon-init-mock-types.ts | 5 ++- .../daemon-init-provider-installation.test.ts | 5 ++- ...daemon-severed-attribution-degrade.test.ts | 34 ++++++++------- ...egraded-daemon-fresh-spawn-routing.test.ts | 8 ++-- .../pty-degraded-fallback-lifecycle.test.ts | 42 +++++++++++++------ .../ipc/pty/provider/local-configure.test.ts | 9 ++-- .../providers/pty-provider-test-double.ts | 33 +++++++++++++++ 9 files changed, 102 insertions(+), 44 deletions(-) create mode 100644 src/main/providers/pty-provider-test-double.ts diff --git a/src/main/daemon/daemon-init-dependency-mocks.ts b/src/main/daemon/daemon-init-dependency-mocks.ts index 9c9ae422111..f76377c04a1 100644 --- a/src/main/daemon/daemon-init-dependency-mocks.ts +++ b/src/main/daemon/daemon-init-dependency-mocks.ts @@ -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 diff --git a/src/main/daemon/daemon-init-endpoint-adoption.test.ts b/src/main/daemon/daemon-init-endpoint-adoption.test.ts index 8775fc7c9de..b2361f60cb6 100644 --- a/src/main/daemon/daemon-init-endpoint-adoption.test.ts +++ b/src/main/daemon/daemon-init-endpoint-adoption.test.ts @@ -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 }> + const launcher = spawnerInstances[0].launcher getMacDaemonTccAttributionHealthMock.mockResolvedValueOnce('severed') getDaemonLaunchIdentityMock.mockReturnValueOnce(identity) // Why: live sessions must veto replacement — the Settings surface owns the remedy instead. diff --git a/src/main/daemon/daemon-init-mock-types.ts b/src/main/daemon/daemon-init-mock-types.ts index a4a9dcccfe5..876a6efe165 100644 --- a/src/main/daemon/daemon-init-mock-types.ts +++ b/src/main/daemon/daemon-init-mock-types.ts @@ -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 diff --git a/src/main/daemon/daemon-init-provider-installation.test.ts b/src/main/daemon/daemon-init-provider-installation.test.ts index be28cc022e6..92850d2a227 100644 --- a/src/main/daemon/daemon-init-provider-installation.test.ts +++ b/src/main/daemon/daemon-init-provider-installation.test.ts @@ -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 + 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) diff --git a/src/main/daemon/daemon-severed-attribution-degrade.test.ts b/src/main/daemon/daemon-severed-attribution-degrade.test.ts index 479b1012dff..cd39510aa98 100644 --- a/src/main/daemon/daemon-severed-attribution-degrade.test.ts +++ b/src/main/daemon/daemon-severed-attribution-degrade.test.ts @@ -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 + discover: ReturnType + dispose: ReturnType + }[] = [] return { inProcessProvider, - installedProvider, - degradedInstances: [] as { - opts: Record - discover: ReturnType - dispose: ReturnType - }[], + 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) diff --git a/src/main/daemon/degraded-daemon-fresh-spawn-routing.test.ts b/src/main/daemon/degraded-daemon-fresh-spawn-routing.test.ts index bbf2194eab1..251756bc382 100644 --- a/src/main/daemon/degraded-daemon-fresh-spawn-routing.test.ts +++ b/src/main/daemon/degraded-daemon-fresh-spawn-routing.test.ts @@ -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) { - 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, diff --git a/src/main/ipc/pty-degraded-fallback-lifecycle.test.ts b/src/main/ipc/pty-degraded-fallback-lifecycle.test.ts index c4a82dc6e0c..9b3ec6c0f54 100644 --- a/src/main/ipc/pty-degraded-fallback-lifecycle.test.ts +++ b/src/main/ipc/pty-degraded-fallback-lifecycle.test.ts @@ -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 +): 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() } } } diff --git a/src/main/ipc/pty/provider/local-configure.test.ts b/src/main/ipc/pty/provider/local-configure.test.ts index 8e440ab2be3..56f56dee43c 100644 --- a/src/main/ipc/pty/provider/local-configure.test.ts +++ b/src/main/ipc/pty/provider/local-configure.test.ts @@ -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() }) diff --git a/src/main/providers/pty-provider-test-double.ts b/src/main/providers/pty-provider-test-double.ts new file mode 100644 index 00000000000..b508e8310fa --- /dev/null +++ b/src/main/providers/pty-provider-test-double.ts @@ -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> +} + +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(() => () => {}) + } +}