test: exercise real mobile controller and repair weak assertions (#33248)

* test: exercise mobile controller and strengthen audit assertions

* test: use array syntax required by React Doctor

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-24 13:54:31 -07:00
committed by GitHub
co-authored by OrcaWin
parent 5ee74f4f83
commit c90d0f93c0
18 changed files with 232 additions and 157 deletions
@@ -1,16 +1,25 @@
import { describe, expect, it, vi } from 'vitest'
import type { RpcClient } from '../transport/rpc-client'
import type { RpcResponse } from '../transport/types'
import { sendMobileTerminalQueryReply } from './mobile-terminal-query-reply'
function createClient() {
return {
sendRequest: vi.fn().mockResolvedValue({
sendRequest: vi.fn<RpcClient['sendRequest']>().mockResolvedValue({
id: '1',
ok: true,
result: { send: { accepted: true, bytesWritten: 6 } },
_meta: { runtimeId: 'runtime-1' }
})
} as unknown as Pick<RpcClient, 'sendRequest'>
}),
subscribe: () => () => {},
updateTerminalSubscriptionViewport: () => {},
getState: () => 'connected',
getReconnectAttempt: () => 0,
getLastConnectedAt: () => null,
onStateChange: () => () => {},
notifyForeground: () => {},
close: () => {}
} satisfies RpcClient
}
describe('sendMobileTerminalQueryReply', () => {
@@ -36,6 +45,25 @@ describe('sendMobileTerminalQueryReply', () => {
inputKind: 'query-reply',
client: { id: 'mobile-1', type: 'mobile' }
})
const refused: RpcResponse = {
id: '2',
ok: true,
result: { send: { handle: 'terminal-1', accepted: false, bytesWritten: 0 } }
}
client.sendRequest.mockResolvedValueOnce(refused)
await expect(
sendMobileTerminalQueryReply({
bytes: '\x1b[3;4R',
client,
clientId: 'mobile-1',
connected: true,
handle: 'terminal-1',
hostSupportsQueryReplyInput: true,
subscribedTerminals: new Set(['terminal-1'])
})
).resolves.toBe(false)
expect(client.sendRequest).toHaveBeenCalledTimes(2)
})
it.each([
@@ -50,6 +50,7 @@ describe('mock terminal stream cancellation', () => {
expect(handle(request('new', 'terminal.subscribe'))).toBe(true)
const oldResponses = deferred.filter(({ response }) => response.id === 'old')
const newResponses = deferred.filter(({ response }) => response.id === 'new')
expect(newResponses).toHaveLength(1)
expect(oldResponses.every(({ shouldSend }) => shouldSend?.() === false)).toBe(true)
expect(newResponses.every(({ shouldSend }) => shouldSend?.() === true)).toBe(true)
+28 -21
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import type { TerminalQuickCommand } from '../../../src/shared/terminal-quick-command-types'
import type {
TerminalCommandQuickCommand,
TerminalAgentQuickCommand
} from '../../../src/shared/terminal-quick-command-types'
import {
buildMobileQuickCommandLaunch,
getQuickCommandDisplayPreview,
@@ -7,7 +10,9 @@ import {
supportsMobileQuickCommands
} from './quick-commands'
function command(overrides: Partial<TerminalQuickCommand> = {}): TerminalQuickCommand {
function command(
overrides: Partial<TerminalCommandQuickCommand> = {}
): TerminalCommandQuickCommand {
return {
id: 'command',
label: 'Command',
@@ -16,7 +21,18 @@ function command(overrides: Partial<TerminalQuickCommand> = {}): TerminalQuickCo
appendEnter: true,
scope: { type: 'global' },
...overrides
} as TerminalQuickCommand
}
}
function agentCommand(prompt: string): TerminalAgentQuickCommand {
return {
id: 'agent-command',
label: 'Command',
action: 'agent-prompt',
agent: 'codex',
prompt,
scope: { type: 'global' }
}
}
describe('mobile quick-command launch', () => {
@@ -59,29 +75,20 @@ describe('mobile quick-command launch', () => {
})
it('injects supported agent prompts into the host-built launch command', () => {
expect(
buildMobileQuickCommandLaunch(
command({
action: 'agent-prompt',
agent: 'codex',
prompt: 'Review this diff'
})
)
).toEqual({ agent: 'codex', options: { agentPrompt: 'Review this diff' } })
expect(buildMobileQuickCommandLaunch(agentCommand('Review this diff'))).toEqual({
agent: 'codex',
options: { agentPrompt: 'Review this diff' }
})
})
it('bounds native row text without truncating searchable or executable content', () => {
const longPrompt = `Review ${'x'.repeat(5993)}`
const agentCommand = command({
action: 'agent-prompt',
agent: 'codex',
prompt: longPrompt
})
const savedCommand = agentCommand(longPrompt)
expect(getQuickCommandDisplayPreview(agentCommand)).toHaveLength(240)
expect(getQuickCommandDisplayPreview(agentCommand)).toMatch(/^Codex: Review .*…$/)
expect(getQuickCommandPreview(agentCommand)).toBe(`Codex: ${longPrompt}`)
expect(buildMobileQuickCommandLaunch(agentCommand)).toEqual({
expect(getQuickCommandDisplayPreview(savedCommand)).toHaveLength(240)
expect(getQuickCommandDisplayPreview(savedCommand)).toMatch(/^Codex: Review .*…$/)
expect(getQuickCommandPreview(savedCommand)).toBe(`Codex: ${longPrompt}`)
expect(buildMobileQuickCommandLaunch(savedCommand)).toEqual({
agent: 'codex',
options: { agentPrompt: longPrompt }
})
@@ -6,15 +6,23 @@ import {
} from './terminal-auto-restore-fit-state'
describe('terminal auto restore fit state', () => {
it('reads the saved delay out of the host reply envelope, after a read and after a save', async () => {
it('reads host values after a read, a clamped save, and a refused save', async () => {
const sendRequest = vi
.fn()
.mockResolvedValueOnce({ id: '1', ok: true, result: { ms: 300_000 } })
.mockResolvedValueOnce({ id: '2', ok: true, result: { ms: 60_000 } })
.mockResolvedValueOnce({ id: '2', ok: true, result: { ms: 5_000 } })
.mockResolvedValueOnce({ id: '3', ok: false, error: { code: 'denied', message: 'Refused' } })
.mockResolvedValueOnce({ id: '4', ok: true, result: { ms: 120_000 } })
await expect(readTerminalAutoRestoreFitMs({ sendRequest })).resolves.toBe(300_000)
await expect(saveTerminalAutoRestoreFitMs({ sendRequest }, 60_000)).resolves.toBe(60_000)
expect(sendRequest).toHaveBeenLastCalledWith('terminal.setAutoRestoreFit', { ms: 60_000 })
await expect(saveTerminalAutoRestoreFitMs({ sendRequest }, 1_000)).resolves.toBe(5_000)
await expect(saveTerminalAutoRestoreFitMs({ sendRequest }, 300_000)).resolves.toBe(120_000)
expect(sendRequest.mock.calls).toEqual([
['terminal.getAutoRestoreFit'],
['terminal.setAutoRestoreFit', { ms: 1_000 }],
['terminal.setAutoRestoreFit', { ms: 300_000 }],
['terminal.getAutoRestoreFit']
])
})
it('reads a host without the setting as the default', async () => {
@@ -20,21 +20,6 @@ describe('dismissTerminalKeyboard', () => {
expect(calls).toEqual(['clear', 'live-blur', 'command-blur', 'dismiss'])
})
it('blurs both live and buffered command inputs', () => {
const liveInput = { blur: vi.fn() }
const commandInput = { blur: vi.fn() }
dismissTerminalKeyboard({
clearPendingLiveInputFocus: vi.fn(),
commandInput,
dismissKeyboard: vi.fn(),
liveInput
})
expect(liveInput.blur).toHaveBeenCalledTimes(1)
expect(commandInput.blur).toHaveBeenCalledTimes(1)
})
it('dismisses the keyboard without a live input handle', () => {
const commandInput = { blur: vi.fn() }
const dismissKeyboard = vi.fn()
@@ -36,6 +36,12 @@ describe('terminal live accessory raw send', () => {
// Why: an accessory key parked in the connect wait would fire into the PTY long after the tap.
{ failWhenDisconnected: true }
)
expect(sendRequest).toHaveBeenCalledTimes(2)
expect(sendRequest).toHaveBeenLastCalledWith(
'orchestration.workerTerminalUserInput',
{ terminal: 'terminal-a' },
{ timeoutMs: 5_000, budgetSpansConnect: true, failWhenDisconnected: true }
)
})
it('drops the bytes instead of sending while disconnected', async () => {
@@ -27,26 +27,29 @@ describe('terminal live control send order', () => {
})
it('Given a successful pending text flush When control bytes follow Then sends them afterward', async () => {
// Given
const events: string[] = []
const flushPendingText = vi.fn(async () => {
events.push('flush')
return true
})
const sendControlBytes = vi.fn(async () => {
events.push('control')
return true
})
vi.useFakeTimers()
try {
const events: string[] = []
const pendingFlush = Promise.withResolvers<boolean>()
const flushPendingText = vi.fn(() => {
events.push('flush')
return pendingFlush.promise
})
const sendControlBytes = vi.fn(async () => {
events.push('control')
return true
})
// When
const result = await sendTerminalLiveControlAfterPendingFlush(
flushPendingText,
sendControlBytes
)
// Then
expect(result).toBe(true)
expect(events).toEqual(['flush', 'control'])
const result = sendTerminalLiveControlAfterPendingFlush(flushPendingText, sendControlBytes)
await vi.advanceTimersByTimeAsync(0)
expect(events).toEqual(['flush'])
expect(sendControlBytes).not.toHaveBeenCalled()
pendingFlush.resolve(true)
await expect(result).resolves.toBe(true)
expect(events).toEqual(['flush', 'control'])
} finally {
vi.useRealTimers()
}
})
it('Given a failed control byte send When pending text flushed Then reports failure', async () => {
@@ -1,101 +1,121 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { TerminalWebViewCommand } from './terminal-webview-messages'
import { createTerminalWebViewPendingMessages } from './terminal-webview-pending-messages'
import {
createTerminalWriteCoalescer,
TERMINAL_WRITE_FLUSH_WINDOW_MS
} from './terminal-write-coalescer'
import { useTerminalWebViewController } from './use-terminal-webview-controller'
import { TERMINAL_WRITE_FLUSH_WINDOW_MS } from './terminal-write-coalescer'
// Simulates TerminalWebView's postMessage: ready → deliver, not ready → queue.
// There is no React render harness in the node environment, so the boundary
// invariants are gated here against the same pending-queue module the component uses.
function createSimulatedTerminalWebView() {
const delivered: TerminalWebViewCommand[] = []
const pendingMessages = createTerminalWebViewPendingMessages()
const readiness = { webReady: true }
const send = (msg: TerminalWebViewCommand) => {
delivered.push(msg)
vi.mock('react-native', () => ({
AppState: { currentState: 'active' },
Pressable: 'Pressable',
Text: 'Text',
View: 'View',
StyleSheet: { absoluteFillObject: {}, create: (styles: unknown) => styles }
}))
vi.mock('lucide-react-native', () => ({ RefreshCw: 'RefreshCw' }))
let activeRenderer: ReactTestRenderer | undefined
function createTerminalController() {
const delivered: (TerminalWebViewCommand & { id: number })[] = []
let controller: ReturnType<typeof useTerminalWebViewController> | undefined
const transport = {
post: (command: TerminalWebViewCommand & { id: number }) => {
delivered.push(command)
},
pingsOnForegroundRecovery: () => true
}
const postMessage = (msg: TerminalWebViewCommand) => {
if (!readiness.webReady) {
pendingMessages.queue(msg)
return
function Harness() {
controller = useTerminalWebViewController({}, transport)
return null
}
act(() => {
activeRenderer = create(createElement(Harness))
})
const mountedController = () => controller
const view = mountedController()
if (!view) {
throw new Error('Terminal controller did not mount')
}
act(() => {
view.receive({ type: 'web-ready' })
})
delivered.length = 0
const recover = () => {
const ping = delivered.find((command) => command.type === 'ping')
if (!ping) {
throw new Error('Foreground recovery did not ping the document')
}
send(msg)
act(() => {
view.receive({ type: 'pong', pingId: ping.id })
})
}
const coalescer = createTerminalWriteCoalescer((data) => postMessage({ type: 'write', data }))
return { coalescer, delivered, pendingMessages, postMessage, readiness, send }
const writesAndInits = () =>
delivered.filter((command) => command.type === 'write' || command.type === 'init')
return { delivered, view: view.handle, recover, writesAndInits }
}
describe('terminal write coalescer boundaries', () => {
afterEach(() => {
act(() => {
activeRenderer?.unmount()
})
activeRenderer = undefined
vi.useRealTimers()
})
it('drops buffered pre-snapshot writes on init (write → clear → init supersession)', () => {
vi.useFakeTimers()
const view = createSimulatedTerminalWebView()
view.coalescer.write('a')
view.coalescer.write('stale-pre-snapshot')
// init() boundary as wired in TerminalWebView: clear the coalescer, then post init.
view.coalescer.clear()
view.postMessage({ type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null })
vi.runOnlyPendingTimers()
expect(view.delivered).toEqual([
const { view, writesAndInits } = createTerminalController()
act(() => {
view.write('a')
view.write('stale-pre-snapshot')
view.init({ cols: 80, rows: 24, initialData: 'snapshot', frame: null })
vi.runOnlyPendingTimers()
})
expect(writesAndInits()).toMatchObject([
{ type: 'write', data: 'a' },
{ type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null }
])
const initIndex = view.delivered.findIndex((msg) => msg.type === 'init')
expect(initIndex).toBeGreaterThanOrEqual(0)
expect(view.delivered.slice(initIndex + 1)).toEqual([])
})
it('keeps foreground recovery safe: a late timer flush lands before init in FIFO order', () => {
vi.useFakeTimers()
const view = createSimulatedTerminalWebView()
view.coalescer.write('live')
view.coalescer.write('buffered-mid-recovery')
// prepareForForegroundRecovery(): readiness invalidated before the timer fires.
view.readiness.webReady = false
vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS)
// Recovery re-init: coalescer.clear() cancels any pending timer synchronously,
// so nothing can flush after this point; the queued init supersedes the flush.
view.coalescer.clear()
view.postMessage({ type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null })
view.readiness.webReady = true
view.pendingMessages.flush(view.send)
vi.runOnlyPendingTimers()
expect(view.delivered).toEqual([
const { view, delivered, recover, writesAndInits } = createTerminalController()
act(() => {
view.write('live')
view.write('buffered-mid-recovery')
view.prepareForForegroundRecovery()
vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS)
})
expect(delivered.map((command) => command.type)).toEqual(['write', 'ping'])
act(() => {
view.init({ cols: 80, rows: 24, initialData: 'snapshot', frame: null })
})
expect(delivered.map((command) => command.type)).toEqual(['write', 'ping'])
recover()
act(() => {
vi.runOnlyPendingTimers()
})
expect(writesAndInits()).toMatchObject([
{ type: 'write', data: 'live' },
{ type: 'write', data: 'buffered-mid-recovery' },
{ type: 'init', cols: 80, rows: 24, initialData: 'snapshot', frame: null }
])
// Invariant: no write reaches the document after the recovery init.
const initIndex = view.delivered.findIndex((msg) => msg.type === 'init')
expect(view.delivered.slice(initIndex + 1).filter((msg) => msg.type === 'write')).toEqual([])
})
it('delivers the deferred-recovery timer flush when no init follows (skipped recovery)', () => {
vi.useFakeTimers()
const view = createSimulatedTerminalWebView()
view.coalescer.write('live')
view.coalescer.write('still-current-document')
view.readiness.webReady = false
vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS)
// 'skipped' recovery: pong re-confirms readiness, no init is posted — the
// buffered bytes belong to the still-live document and must not be lost.
view.readiness.webReady = true
view.pendingMessages.flush(view.send)
expect(view.delivered).toEqual([
const { view, delivered, recover, writesAndInits } = createTerminalController()
act(() => {
view.write('live')
view.write('still-current-document')
view.prepareForForegroundRecovery()
vi.advanceTimersByTime(TERMINAL_WRITE_FLUSH_WINDOW_MS)
})
expect(delivered.map((command) => command.type)).toEqual(['write', 'ping'])
recover()
expect(writesAndInits()).toMatchObject([
{ type: 'write', data: 'live' },
{ type: 'write', data: 'still-current-document' }
])
@@ -50,7 +50,11 @@ it.each(['throw', 'rpc refusal'])(
sendRequest:
failure === 'throw'
? vi.fn().mockRejectedValue(new Error('offline'))
: vi.fn().mockResolvedValue({ id: 'report', ok: false, error: { message: 'refused' } })
: vi.fn().mockResolvedValue({
id: 'report',
ok: false,
error: { code: 'permission_denied', message: 'refused' }
})
}
reportWorkerTerminalUserInput(client, 'term-1')
await vi.advanceTimersByTimeAsync(249)
-1
View File
@@ -71,7 +71,6 @@ src/source-control/use-mobile-hosted-review-eligibility.test.ts
src/tasks/source-workspace-create.test.ts
src/tasks/task-source-search-reply-schema.test.ts
src/terminal/buffered-terminal-draft-restoration.test.ts
src/terminal/mobile-terminal-query-reply.test.ts
src/terminal/terminal-caret-rendering-oracle.test.ts
src/terminal/terminal-live-accessory-raw-send.test.ts
src/terminal/terminal-live-text-commit.test.ts
@@ -26,7 +26,7 @@ beforeEach(() => {
afterEach(() => rmSync(root, { recursive: true, force: true }))
describe('orcad migration blob staging', () => {
it('stages, resumes from disk after a restart, and accepts only exact retries', () => {
it('stages, reloads progress from disk, and accepts only exact retries', () => {
const bytes = Buffer.from('echo one\necho two 🐋\n', 'utf8')
const descriptor = blob('shell-history', `${'a'.repeat(16)}-zsh`, bytes)
const manifest = migrationManifest('resume')
@@ -108,11 +108,10 @@ describe('orcad scrollback snapshot transfer', () => {
expect(readFinal(descriptor)).toBeNull()
})
it('recognizes an already-materialized matching final file after restart', () => {
it('recognizes an already-materialized matching final file without staged bytes', () => {
const bytes = Buffer.from('already committed bytes', 'utf8')
const descriptor = snapshot('3', 'tab-3', 'leaf-3', bytes)
const manifest = migrationManifest('materialized', [descriptor])
stagedState(manifest)
writeFinal(descriptor, bytes)
expect(inspectOrcadMigrationSnapshotUploads(manifest, storage)?.[0]?.receivedBytes).toBe(
@@ -34,7 +34,7 @@ function manifest(): OrcadMigrationManifest {
}
describe('retiring client-hosted browser close intents', () => {
it('finishes a retry after the moving write already flushed', () => {
it('moves close intents to the destination and accepts an exact retry', () => {
const state = getDefaultPersistedState('/home/test')
state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment = {
old: [
@@ -43,9 +43,13 @@ describe('retiring client-hosted browser close intents', () => {
]
}
subtractOrcadMigrationClientState(state, manifest())
const moved = structuredClone(
state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment
)
const moved = {
'environment-1': [
{ browserPageId: 'page-1', worktreeId, closedAt: 42 },
{ browserPageId: 'page-2', worktreeId, closedAt: 43 }
]
}
expect(state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment).toEqual(moved)
expect(() => subtractOrcadMigrationClientState(state, manifest())).not.toThrow()
expect(state.workspaceSession.clientHostedBrowserCloseIntentsByEnvironment).toEqual(moved)
})
@@ -31,7 +31,7 @@ describe('bindTabStripContentResizeObservers', () => {
expect(onResize).toHaveBeenCalled()
disconnectObservers()
expect(disconnect).toHaveBeenCalled()
expect(disconnect).toHaveBeenCalledTimes(2)
vi.unstubAllGlobals()
})
+15 -13
View File
@@ -1,7 +1,6 @@
import { expect, it } from 'vitest'
import {
AiVaultSearchSettingsSchema,
DEFAULT_AI_VAULT_SEARCH_SETTINGS,
resolveAiVaultSearchSettings,
sameAiVaultSearchSettings
} from './ai-vault-search-settings'
@@ -9,17 +8,20 @@ import {
// Off is the only safe default: building the index reads every transcript on the
// machine, so a profile that has never answered must read as "no".
it('reads anything that is not an explicit opt-in as off', () => {
expect(resolveAiVaultSearchSettings(undefined)).toEqual(DEFAULT_AI_VAULT_SEARCH_SETTINGS)
expect(resolveAiVaultSearchSettings({})).toEqual(DEFAULT_AI_VAULT_SEARCH_SETTINGS)
expect(resolveAiVaultSearchSettings({ aiVaultSearch: null })).toEqual(
DEFAULT_AI_VAULT_SEARCH_SETTINGS
)
expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: 'yes' } })).toEqual(
DEFAULT_AI_VAULT_SEARCH_SETTINGS
)
expect(resolveAiVaultSearchSettings({ aiVaultSearch: 'on' })).toEqual(
DEFAULT_AI_VAULT_SEARCH_SETTINGS
)
expect(resolveAiVaultSearchSettings(undefined)).toEqual({ enabled: false, historyDays: null })
expect(resolveAiVaultSearchSettings({})).toEqual({ enabled: false, historyDays: null })
expect(resolveAiVaultSearchSettings({ aiVaultSearch: null })).toEqual({
enabled: false,
historyDays: null
})
expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: 'yes' } })).toEqual({
enabled: false,
historyDays: null
})
expect(resolveAiVaultSearchSettings({ aiVaultSearch: 'on' })).toEqual({
enabled: false,
historyDays: null
})
})
it('normalizes a history bound and drops anything that is not one', () => {
@@ -59,7 +61,7 @@ it('accepts what it produces and refuses what it does not', () => {
expect(AiVaultSearchSettingsSchema.safeParse({ historyDays: null }).success).toBe(false)
})
it('treats an unchanged policy as unchanged so a re-save never restarts the index', () => {
it('compares both enabled state and history bounds', () => {
expect(
sameAiVaultSearchSettings(
{ enabled: true, historyDays: 30 },
+7 -1
View File
@@ -33,7 +33,7 @@ describe('isAiVaultSessionResumableContent', () => {
expect(isAiVaultSessionResumableContent(signal())).toBe(false)
})
it('accepts conversation previews when the turn count is missing', () => {
it('accepts conversation previews when the turn count is zero', () => {
// Some parsers (Grok, OpenCode fallback schemas) derive messageCount from
// metadata that can be absent while real turns exist in previews.
expect(isAiVaultSessionResumableContent(signal({ previewMessages: [preview('user')] }))).toBe(
@@ -84,5 +84,11 @@ describe('aiVaultSessionRecoverableSignalCount', () => {
subagentTranscriptCount: 3
})
).toBe(3)
expect(
aiVaultSessionRecoverableSignalCount({
queuedMessageCount: 4,
subagentTranscriptCount: -2
})
).toBe(4)
})
})
@@ -16,10 +16,11 @@ it('rebuilds only after a canonical nonzero completed exit, at most twice', asyn
['codex', 1, canonical, false]
] as const) {
const result = { exitCode, stdout: '', stderr }
const run = vi.fn(async (_strictMcp: boolean) => result)
const retryResult = { ...result, stdout: 'retry output' }
const run = vi.fn(async (strictMcp: boolean) => (strictMcp ? result : retryResult))
expect(
await runClaudeBackgroundAttempts({ agentId, run, failedExit: (outcome) => outcome })
).toBe(result)
).toBe(eligible ? retryResult : result)
expect(run.mock.calls).toEqual(eligible ? [[true], [false]] : [[true]])
}
})
+3 -1
View File
@@ -17,7 +17,9 @@ describe('isCodexAuthError', () => {
describe('extractCodexAuthError', () => {
it('returns the first matching auth line', () => {
expect(
extractCodexAuthError('startup log\nERROR: not logged in. Please sign in again.\nmore log')
extractCodexAuthError(
'startup log\nERROR: not logged in. Please sign in again.\nplease reauthenticate\nmore log'
)
).toBe('ERROR: not logged in. Please sign in again.')
})