mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
+1
-2
@@ -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(
|
||||
|
||||
+8
-4
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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]])
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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.')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user