Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
438 lines
15 KiB
TypeScript
438 lines
15 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { join } from 'node:path'
|
|
import {
|
|
readFileSyncMock,
|
|
recordCodexPaneAccountMock,
|
|
forgetCodexPaneAccountMock
|
|
} from './pty-ipc-mock-registry'
|
|
import { TEST_CODEX_HOME, TEST_CODEX_AUTH_JSON, TEST_MANAGED_ROOT } from './pty-ipc-test-constants'
|
|
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
|
|
import { registerPtyHandlers, setLocalPtyProvider } from './pty'
|
|
|
|
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()))
|
|
vi.mock('node:child_process', async (importOriginal) =>
|
|
(await import('./pty-ipc-mock-registry')).childProcessModuleMock(await importOriginal())
|
|
)
|
|
vi.mock('../opencode/hook-service', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.openCodeHookServiceModuleMock())
|
|
)
|
|
vi.mock('../mimo/hook-service', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.mimoHookServiceModuleMock())
|
|
)
|
|
vi.mock('../agent-hooks/server', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.agentHookServerModuleMock())
|
|
)
|
|
vi.mock('../pi/titlebar-extension-service', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.piTitlebarExtensionModuleMock())
|
|
)
|
|
vi.mock('../pwsh', () => import('./pty-ipc-mock-registry').then((m) => m.pwshModuleMock()))
|
|
vi.mock('../wsl', async (importOriginal) =>
|
|
(await import('./pty-ipc-mock-registry')).wslModuleMock(await importOriginal())
|
|
)
|
|
vi.mock('../telemetry/client', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.telemetryClientModuleMock())
|
|
)
|
|
vi.mock('../telemetry/classify-error', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.classifyErrorModuleMock())
|
|
)
|
|
vi.mock('../cli/linux-terminal-orca-cli-shim', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.linuxCliShimModuleMock())
|
|
)
|
|
vi.mock('../memory/pty-registry', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.ptyRegistryModuleMock())
|
|
)
|
|
vi.mock('../agent-hooks/migration-unsupported-pty-state', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.migrationUnsupportedPtyModuleMock())
|
|
)
|
|
vi.mock('../codex/codex-pane-account-registry', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.codexPaneAccountRegistryModuleMock())
|
|
)
|
|
vi.mock('../codex/codex-state-db-backfill-recovery', () =>
|
|
import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
|
|
)
|
|
|
|
const MANAGED_ORIGIN_HOME = join(TEST_MANAGED_ROOT, 'origin', 'home')
|
|
const MANAGED_CURRENT_HOME = join(TEST_MANAGED_ROOT, 'current', 'home')
|
|
const MANAGED_SHARED_HOME = join(TEST_MANAGED_ROOT, 'shared-mirror', 'home')
|
|
const ORIGIN_ROLLOUT = join(MANAGED_ORIGIN_HOME, 'sessions', '2026', '07', '20', 'rollout-a.jsonl')
|
|
const SHARED_ROLLOUT = join(MANAGED_SHARED_HOME, 'sessions', '2026', '07', '20', 'rollout-a.jsonl')
|
|
|
|
describe('registerPtyHandlers', () => {
|
|
const { handlers, mainWindow } = setupPtyIpcSuite()
|
|
|
|
it('records route provenance for a process-wide CODEX_HOME', async () => {
|
|
const previousCodexHome = process.env.CODEX_HOME
|
|
process.env.CODEX_HOME = '/process/custom-codex-home'
|
|
try {
|
|
setLocalPtyProvider({
|
|
spawn: vi.fn(async () => ({ id: 'pty-process-home' })),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
const getSettings = vi.fn().mockReturnValue({ activeCodexManagedAccountId: null })
|
|
registerPtyHandlers(
|
|
mainWindow as never,
|
|
undefined,
|
|
() => TEST_CODEX_HOME,
|
|
getSettings as never
|
|
)
|
|
|
|
await handlers.get('pty:spawn')!(null, {
|
|
cols: 80,
|
|
rows: 24,
|
|
env: { CODEX_HOME: '/process/custom-codex-home' }
|
|
})
|
|
|
|
expect(recordCodexPaneAccountMock).toHaveBeenCalledWith('pty-process-home', {
|
|
selectionKey: 'host',
|
|
accountId: null,
|
|
homeRoute: 'shared-home',
|
|
environmentHomeOverride: { codexHome: '/process/custom-codex-home' }
|
|
})
|
|
} finally {
|
|
if (previousCodexHome === undefined) {
|
|
delete process.env.CODEX_HOME
|
|
} else {
|
|
process.env.CODEX_HOME = previousCodexHome
|
|
}
|
|
}
|
|
})
|
|
it('does not guess route provenance for a pane-local environment CODEX_HOME', async () => {
|
|
setLocalPtyProvider({
|
|
spawn: vi.fn(async () => ({ id: 'pty-pane-env-home' })),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
const getSettings = vi.fn().mockReturnValue({ activeCodexManagedAccountId: null })
|
|
registerPtyHandlers(mainWindow as never, undefined, () => TEST_CODEX_HOME, getSettings as never)
|
|
|
|
await handlers.get('pty:spawn')!(null, {
|
|
cols: 80,
|
|
rows: 24,
|
|
env: { CODEX_HOME: '/pane/custom-codex-home' }
|
|
})
|
|
|
|
expect(recordCodexPaneAccountMock).toHaveBeenCalledWith('pty-pane-env-home', {
|
|
selectionKey: 'host',
|
|
accountId: null,
|
|
homeRoute: 'custom-home'
|
|
})
|
|
})
|
|
it('does not resume under another account when the origin auth stays unavailable', async () => {
|
|
vi.useFakeTimers()
|
|
const spawn = vi.fn(async () => ({ id: 'pty-must-not-spawn' }))
|
|
setLocalPtyProvider({
|
|
spawn,
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
readFileSyncMock.mockImplementation((filePath: string) => {
|
|
if (filePath.endsWith('auth.json')) {
|
|
throw Object.assign(new Error('missing auth'), { code: 'ENOENT' })
|
|
}
|
|
return ''
|
|
})
|
|
const resolveHome = vi.fn(() => MANAGED_CURRENT_HOME)
|
|
registerPtyHandlers(
|
|
mainWindow as never,
|
|
undefined,
|
|
resolveHome,
|
|
(() => ({
|
|
codexManagedAccounts: [
|
|
{ id: 'account-a', managedHomePath: MANAGED_ORIGIN_HOME },
|
|
{ id: 'account-b', managedHomePath: MANAGED_CURRENT_HOME }
|
|
]
|
|
})) as never,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
prepareCodexSessionResume: async () => ({
|
|
outcome: 'resume' as const,
|
|
codexHomePath: MANAGED_ORIGIN_HOME
|
|
})
|
|
}
|
|
)
|
|
|
|
const launch = handlers.get('pty:spawn')!(null, {
|
|
cols: 80,
|
|
rows: 24,
|
|
command: 'codex resume session-a',
|
|
envToDelete: ['CODEX_HOME', 'ORCA_CODEX_HOME'],
|
|
launchAgent: 'codex',
|
|
resumeProviderSession: {
|
|
key: 'session_id',
|
|
id: 'session-a',
|
|
transcriptPath: ORIGIN_ROLLOUT
|
|
}
|
|
})
|
|
const rejection = expect(launch).rejects.toThrow(
|
|
'The Codex account credentials for this session are temporarily unavailable. Try opening the terminal again.'
|
|
)
|
|
await vi.advanceTimersByTimeAsync(2_000)
|
|
await rejection
|
|
|
|
expect(resolveHome).not.toHaveBeenCalled()
|
|
expect(spawn).not.toHaveBeenCalled()
|
|
expect(recordCodexPaneAccountMock).not.toHaveBeenCalled()
|
|
})
|
|
it('records the origin account a resumed Codex pane is pinned to', async () => {
|
|
setLocalPtyProvider({
|
|
spawn: vi.fn(async () => ({ id: 'pty-resumed' })),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
const getSettings = vi.fn().mockReturnValue({
|
|
activeCodexManagedAccountId: 'account-b',
|
|
codexManagedAccounts: [
|
|
{ id: 'account-a', managedHomePath: MANAGED_ORIGIN_HOME },
|
|
{ id: 'account-b', managedHomePath: MANAGED_CURRENT_HOME }
|
|
]
|
|
})
|
|
registerPtyHandlers(
|
|
mainWindow as never,
|
|
undefined,
|
|
vi.fn(() => MANAGED_CURRENT_HOME),
|
|
getSettings as never,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
prepareCodexSessionResume: async () => ({
|
|
outcome: 'resume' as const,
|
|
codexHomePath: MANAGED_ORIGIN_HOME
|
|
})
|
|
}
|
|
)
|
|
readFileSyncMock.mockReturnValue(TEST_CODEX_AUTH_JSON)
|
|
|
|
await handlers.get('pty:spawn')!(null, {
|
|
cols: 80,
|
|
rows: 24,
|
|
command: 'codex resume session-a',
|
|
launchAgent: 'codex',
|
|
resumeProviderSession: {
|
|
key: 'session_id',
|
|
id: 'session-a',
|
|
transcriptPath: ORIGIN_ROLLOUT
|
|
}
|
|
})
|
|
|
|
// Why: the resume deliberately overrides the selection, so the pane really
|
|
// is on account-a. Recording that is what makes the restart prompt appear.
|
|
expect(recordCodexPaneAccountMock.mock.calls).toEqual([
|
|
['pty-resumed', { selectionKey: 'host', accountId: 'account-a', homeRoute: 'account-home' }]
|
|
])
|
|
expect(readFileSyncMock).toHaveBeenCalledWith(join(MANAGED_ORIGIN_HOME, 'auth.json'), 'utf8')
|
|
expect(forgetCodexPaneAccountMock).not.toHaveBeenCalled()
|
|
})
|
|
it('leaves a resumed Codex pane unattributed when no account owns its home', async () => {
|
|
setLocalPtyProvider({
|
|
spawn: vi.fn(async () => ({ id: 'pty-resumed' })),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
const getSettings = vi.fn().mockReturnValue({
|
|
activeCodexManagedAccountId: 'account-b',
|
|
codexManagedAccounts: [{ id: 'account-b', managedHomePath: MANAGED_CURRENT_HOME }]
|
|
})
|
|
registerPtyHandlers(
|
|
mainWindow as never,
|
|
undefined,
|
|
vi.fn(() => MANAGED_CURRENT_HOME),
|
|
getSettings as never,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
prepareCodexSessionResume: async () => ({
|
|
outcome: 'resume' as const,
|
|
codexHomePath: MANAGED_SHARED_HOME
|
|
})
|
|
}
|
|
)
|
|
|
|
await handlers.get('pty:spawn')!(null, {
|
|
cols: 80,
|
|
rows: 24,
|
|
command: 'codex resume session-a',
|
|
launchAgent: 'codex',
|
|
resumeProviderSession: {
|
|
key: 'session_id',
|
|
id: 'session-a',
|
|
transcriptPath: SHARED_ROLLOUT
|
|
}
|
|
})
|
|
|
|
// Why: an unowned home cannot be named, so guessing here would raise a
|
|
// restart notice that blocks a correctly-signed-in pane's input.
|
|
expect(recordCodexPaneAccountMock).not.toHaveBeenCalled()
|
|
expect(forgetCodexPaneAccountMock).toHaveBeenCalledWith('pty-resumed')
|
|
})
|
|
// Why: the runtime controller is the CLI/relay resume path, and it repeats the
|
|
// same recording call the ipc handler makes. Without its own coverage a revert
|
|
// there is invisible.
|
|
it('records the origin account for a resumed Codex pane spawned by the runtime controller', async () => {
|
|
type RuntimeSpawnController = {
|
|
spawn(args: Record<string, unknown>): Promise<{ id: string }>
|
|
}
|
|
setLocalPtyProvider({
|
|
spawn: vi.fn(async () => ({ id: 'pty-runtime-resumed' })),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
const runtime = {
|
|
setPtyController: vi.fn(),
|
|
registerPty: vi.fn(),
|
|
noteTerminalSpawnCommand: vi.fn(),
|
|
onPtySpawned: vi.fn(),
|
|
onPtyExit: vi.fn(),
|
|
onPtyData: vi.fn()
|
|
}
|
|
const getSettings = vi.fn().mockReturnValue({
|
|
activeCodexManagedAccountId: 'account-b',
|
|
codexManagedAccounts: [
|
|
{ id: 'account-a', managedHomePath: MANAGED_ORIGIN_HOME },
|
|
{ id: 'account-b', managedHomePath: MANAGED_CURRENT_HOME }
|
|
]
|
|
})
|
|
handlers.clear()
|
|
registerPtyHandlers(
|
|
mainWindow as never,
|
|
runtime as never,
|
|
vi.fn(() => MANAGED_CURRENT_HOME),
|
|
getSettings as never,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
prepareCodexSessionResume: async () => ({
|
|
outcome: 'resume' as const,
|
|
codexHomePath: MANAGED_ORIGIN_HOME
|
|
})
|
|
}
|
|
)
|
|
const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController
|
|
readFileSyncMock.mockReturnValue(TEST_CODEX_AUTH_JSON)
|
|
|
|
await controller.spawn({
|
|
cols: 80,
|
|
rows: 24,
|
|
worktreeId: 'wt-runtime',
|
|
command: 'codex resume session-a',
|
|
launchAgent: 'codex',
|
|
resumeProviderSession: {
|
|
key: 'session_id',
|
|
id: 'session-a',
|
|
transcriptPath: ORIGIN_ROLLOUT
|
|
}
|
|
})
|
|
|
|
expect(recordCodexPaneAccountMock.mock.calls).toEqual([
|
|
[
|
|
'pty-runtime-resumed',
|
|
{ selectionKey: 'host', accountId: 'account-a', homeRoute: 'account-home' }
|
|
]
|
|
])
|
|
expect(readFileSyncMock).toHaveBeenCalledWith(join(MANAGED_ORIGIN_HOME, 'auth.json'), 'utf8')
|
|
expect(forgetCodexPaneAccountMock).not.toHaveBeenCalled()
|
|
})
|
|
it('leaves a runtime-controller resumed Codex pane unattributed when no account owns its home', async () => {
|
|
type RuntimeSpawnController = {
|
|
spawn(args: Record<string, unknown>): Promise<{ id: string }>
|
|
}
|
|
setLocalPtyProvider({
|
|
spawn: vi.fn(async () => ({ id: 'pty-runtime-resumed' })),
|
|
write: vi.fn(),
|
|
resize: vi.fn(),
|
|
kill: vi.fn(),
|
|
shutdown: vi.fn(),
|
|
onData: vi.fn(() => vi.fn()),
|
|
onExit: vi.fn(() => vi.fn()),
|
|
listProcesses: vi.fn(async () => []),
|
|
getForegroundProcess: vi.fn(async () => null)
|
|
} as never)
|
|
const runtime = {
|
|
setPtyController: vi.fn(),
|
|
registerPty: vi.fn(),
|
|
noteTerminalSpawnCommand: vi.fn(),
|
|
onPtySpawned: vi.fn(),
|
|
onPtyExit: vi.fn(),
|
|
onPtyData: vi.fn()
|
|
}
|
|
const getSettings = vi.fn().mockReturnValue({
|
|
activeCodexManagedAccountId: 'account-b',
|
|
codexManagedAccounts: [{ id: 'account-b', managedHomePath: MANAGED_CURRENT_HOME }]
|
|
})
|
|
handlers.clear()
|
|
const resolveHome = vi.fn(() => MANAGED_SHARED_HOME)
|
|
registerPtyHandlers(
|
|
mainWindow as never,
|
|
runtime as never,
|
|
resolveHome,
|
|
getSettings as never,
|
|
undefined,
|
|
undefined,
|
|
{
|
|
prepareCodexSessionResume: async () => ({
|
|
outcome: 'resume' as const,
|
|
codexHomePath: MANAGED_SHARED_HOME,
|
|
reconcileSharedRuntimeAuth: true
|
|
})
|
|
}
|
|
)
|
|
const controller = runtime.setPtyController.mock.calls[0]?.[0] as RuntimeSpawnController
|
|
|
|
await controller.spawn({
|
|
cols: 80,
|
|
rows: 24,
|
|
worktreeId: 'wt-runtime',
|
|
command: 'codex resume session-a',
|
|
launchAgent: 'codex',
|
|
resumeProviderSession: {
|
|
key: 'session_id',
|
|
id: 'session-a',
|
|
transcriptPath: SHARED_ROLLOUT
|
|
}
|
|
})
|
|
|
|
expect(resolveHome).toHaveBeenCalledTimes(1)
|
|
expect(recordCodexPaneAccountMock).not.toHaveBeenCalled()
|
|
expect(forgetCodexPaneAccountMock).toHaveBeenCalledWith('pty-runtime-resumed')
|
|
})
|
|
})
|