test: repair behavioral coverage and remove export inventory (#33246)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-24 13:22:32 -07:00
committed by GitHub
co-authored by OrcaWin
parent a1246ea166
commit e802f4db7c
14 changed files with 129 additions and 63 deletions
@@ -30,12 +30,14 @@ const fruit = {
let tree: ReactTestRenderer
function render(prompt: AskPrompt, onAnswer = vi.fn(async () => true)): typeof onAnswer {
function render(
prompt: AskPrompt,
onAnswer = vi.fn(async () => true),
onCancel = vi.fn(async () => true)
): typeof onAnswer {
vi.useFakeTimers()
act(() => {
tree = create(
createElement(MobileNativeChatAsk, { prompt, onAnswer, onCancel: async () => true })
)
tree = create(createElement(MobileNativeChatAsk, { prompt, onAnswer, onCancel }))
})
return onAnswer
}
@@ -45,6 +47,15 @@ function press(label: string): void {
act(() => row.props.onPress())
}
async function submit(): Promise<void> {
const button = tree.root.find(
(node) =>
isHost(node, 'Pressable') && node.findAll((n) => n.props.children === 'Submit').length > 0
)
expect(button.props.disabled).toBe(false)
await act(async () => button.props.onPress())
}
async function passAutoAdvanceBeat(): Promise<void> {
await act(async () => {
await vi.advanceTimersByTimeAsync(NATIVE_CHAT_QUESTION_AUTO_ADVANCE_MS)
@@ -84,6 +95,12 @@ describe('MobileNativeChatAsk single-select auto-advance', () => {
await passAutoAdvanceBeat()
expect(onAnswer).not.toHaveBeenCalled()
const input = tree.root.find((node) => isHost(node, 'TextInput'))
act(() => input.props.onChangeText(' custom answer '))
await passAutoAdvanceBeat()
expect(onAnswer).not.toHaveBeenCalled()
await submit()
expect(onAnswer).toHaveBeenCalledExactlyOnceWith([{ indices: [], other: 'custom answer' }])
})
it('waits for Submit on a multi-select question', async () => {
@@ -93,6 +110,8 @@ describe('MobileNativeChatAsk single-select auto-advance', () => {
await passAutoAdvanceBeat()
expect(onAnswer).not.toHaveBeenCalled()
await submit()
expect(onAnswer).toHaveBeenCalledExactlyOnceWith([{ indices: [0] }])
})
it('holds its choices while an answer is being delivered', async () => {
@@ -113,7 +132,12 @@ describe('MobileNativeChatAsk single-select auto-advance', () => {
})
it('sends nothing when the question is cancelled before the pick moves on', async () => {
const onAnswer = render({ questions: [indent] })
const onCancel = vi.fn(async () => true)
const onAnswer = render(
{ questions: [indent] },
vi.fn(async () => true),
onCancel
)
press('Spaces')
const cancel = tree.root.find(
@@ -121,6 +145,7 @@ describe('MobileNativeChatAsk single-select auto-advance', () => {
isHost(node, 'Pressable') && node.findAll((n) => n.props.children === 'Cancel').length > 0
)
await act(async () => cancel.props.onPress())
expect(onCancel).toHaveBeenCalledExactlyOnceWith()
await passAutoAdvanceBeat()
expect(onAnswer).not.toHaveBeenCalled()
@@ -40,6 +40,7 @@ type Tick = {
structured?: boolean
activeTabId?: string | null
setTabView?: (tabId: string, view: 'terminal') => void
attachImage?: (source: 'library' | 'camera') => Promise<void>
}
function overlayElement(tick: Tick): ReturnType<typeof createElement> {
@@ -77,7 +78,14 @@ function overlayElement(tick: Tick): ReturnType<typeof createElement> {
return createElement(MobileNativeChatOverlay, {
controller,
activeSessionTabId: tick.activeTabId,
images: {} as never,
onOpenFile: vi.fn(),
images: {
attachments: [],
isAttaching: false,
attachImage: tick.attachImage ?? vi.fn(async () => {}),
removeAttachment: vi.fn(),
sendNativeChat: vi.fn(async () => true)
},
onMicPress: vi.fn(),
micActive: false,
dictationMode: 'toggle',
@@ -277,10 +285,17 @@ describe('MobileNativeChatOverlay attach button', () => {
)
it.each([true, false])('offers attach only when the agent takes images (%s)', async (accepts) => {
const attachImage = vi.fn(async () => {})
await act(async () => {
renderer = create(overlayElement({ acceptsImages: accepts }))
renderer = create(overlayElement({ acceptsImages: accepts, attachImage }))
})
const view = renderer!.root.find((node) => node.type === 'ChatView')
expect(typeof view.props.onAttachImage === 'function').toBe(accepts)
if (accepts) {
await act(async () => view.props.onAttachImage())
expect(attachImage).toHaveBeenCalledExactlyOnceWith('library')
} else {
expect(attachImage).not.toHaveBeenCalled()
}
})
})
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity'
import type { RpcResponse } from '../transport/types'
import type { RpcClient } from '../transport/rpc-client'
import { buildAskAnswerKeys, type AskPrompt } from '../../../src/shared/native-chat-ask'
import type { AskPrompt } from '../../../src/shared/native-chat-ask'
import {
visible,
handleRef,
@@ -103,7 +103,6 @@ describe('prompt cards through the production controller, send contract and view
questions: [{ question: 'Answer?', options: [{ label: 'A' }], multiSelect: false }]
}
const selections = [{ indices: [], other: 'custom answer' }]
const groups = buildAskAnswerKeys(prompt, selections)
vi.mocked(client.sendRequest).mockResolvedValue(response(true, 'legacy'))
await render({
tab: {
@@ -129,9 +128,9 @@ describe('prompt cards through the production controller, send contract and view
.mock.calls.filter(([method]) => method === 'terminal.send')
.map(([, params]) => params)
expect(writes).toEqual(
groups.map((group) =>
['2', 'custom answer', '\r'].map((text) =>
expect.objectContaining({
text: 'raw' in group ? group.raw : group.text,
text,
enter: false,
requireWriteSettlement: true
})
@@ -1,5 +1,4 @@
import { describe, expect, it } from 'vitest'
import type { AgentJournalRenderItem } from '../../../src/shared/agent-session-journal-types'
import {
projectStructuredQuestion,
type StructuredQuestionItem
@@ -11,6 +10,8 @@ function groupedPrompt(): StructuredQuestionItem {
return {
itemId: 'item-1',
revision: 1,
sequence: 1,
observedAt: 1,
body: {
kind: 'question',
question: '2 grouped questions from Claude',
@@ -31,16 +32,16 @@ function groupedPrompt(): StructuredQuestionItem {
freeTextQuestionId: 'q2'
}
],
resolution: { state: 'pending' }
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
}
} as unknown as AgentJournalRenderItem as StructuredQuestionItem
}
}
describe('structured question projection for grouped Claude prompts', () => {
it('renders an answerable question instead of the empty placeholder card', () => {
const projected = projectStructuredQuestion(groupedPrompt())
expect(projected?.question).not.toBe('2 grouped questions from Claude')
expect(projected?.question).toBe('Which database? (1 of 2)')
expect(projected?.options).toEqual(['Postgres'])
expect(projected?.optionDescriptions).toEqual(['Durable server'])
expect(projected?.optionTokens.filter(Boolean)).toHaveLength(1)
@@ -63,6 +63,7 @@ describe('useMobileStructuredNativeChatSendBridge', () => {
await expect(sendWithOutcome('/review')).resolves.toBe(outcome)
expect(sendStructured).toHaveBeenCalledWith('/review')
expect(clearDraftForSend).toHaveBeenCalledExactlyOnceWith(ORIGIN, '/review')
expect(restoreRejectedDraft).not.toHaveBeenCalled()
}
)
@@ -7,7 +7,6 @@ import {
EMPTY_STRUCTURED_AGENT_SESSION,
type StructuredAgentSessionState
} from '../../../src/shared/structured-agent-session-reducer'
import { encodeAgentSessionQuestionAnswers } from '../../../src/shared/agent-session-question-answer'
import { formatQuestionFreeTextAnswer } from './mobile-native-chat-question'
import {
projectStructuredQuestion,
@@ -352,7 +351,12 @@ describe('useMobileStructuredPromptResponses', () => {
expect(sentFields(sent)).toEqual({
itemId: 'item-g',
expectedRevision: 1,
...(supported ? { answers } : { optionId: encodeAgentSessionQuestionAnswers(answers) })
...(supported
? { answers }
: {
optionId:
'question-group:[{"questionId":"q1","optionIds":["q1:choice-1"]},{"questionId":"q2","optionIds":["q2:choice-1"]}]'
})
})
})
@@ -1,7 +1,10 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentSessionQueuedMessage } from '../../../src/shared/agent-session-wire'
import type {
AgentSessionQueuedMessage,
AgentSessionQueuedMessageEditHoldResult
} from '../../../src/shared/agent-session-wire'
import { agentSessionSendBodyFingerprint } from '../../../src/shared/structured-agent-session-send-mutation'
import { queuedMessageWithEdit } from '../../../src/shared/queued-message-edit'
import type { RpcClient } from '../transport/rpc-client'
@@ -27,6 +30,20 @@ function withText(body: AgentSessionQueuedMessage['body'], text: string) {
return edited.body
}
function editHoldReply(params: unknown): RpcResponse {
const result: AgentSessionQueuedMessageEditHoldResult =
fieldsOf(params).action === 'release'
? { status: 'released' }
: { status: 'held', fingerprint: 'fp', leaseDurationMs: 120_000, remainingMs: 120_000 }
return ok(result)
}
function readOrHoldReply(method: string, params: unknown): RpcResponse {
return method === 'agentSession.queuedMessageEditHold'
? editHoldReply(params)
: ok({ models: [], current: {} })
}
vi.mock('@react-native-async-storage/async-storage', () => ({
default: { getItem: async () => null, setItem: async () => {}, removeItem: async () => {} }
}))
@@ -89,12 +106,10 @@ describe('mobile in-place queued Edit', () => {
}
async function mount(message: AgentSessionQueuedMessage, editSupport: boolean | null = true) {
vi.clearAllMocks()
sendRequest.mockImplementation(async (method) =>
method === 'agentSession.queuedMessageEditHold'
? ok({ status: 'held', fingerprint: 'fp', leaseDurationMs: 120_000, remainingMs: 120_000 })
: method === 'agentSession.queuedMessageUpdate'
? mutationOk({ status: 'updated', messageId: message.messageId, fingerprint: 'new' })
: ok({ models: [], current: {} })
sendRequest.mockImplementation(async (method, params) =>
method === 'agentSession.queuedMessageUpdate'
? mutationOk({ status: 'updated', messageId: message.messageId, fingerprint: 'new' })
: readOrHoldReply(method, params)
)
await act(async () => {
renderer = create(createElement(Harness, { editSupport }))
@@ -125,7 +140,7 @@ describe('mobile in-place queued Edit', () => {
expect(hook!.queued.edit).toEqual(expect.any(Function))
})
it('holds and renews a card while typing, saves its text without replacing images, and cancels in place', async () => {
it('holds and renews a card while typing, sends only its edited text, and cancels in place', async () => {
const message = queuedDraft({
messageId: 'image-card',
position: 7,
@@ -169,7 +184,6 @@ describe('mobile in-place queued Edit', () => {
const body = withText(message.body, 'edited')
act(() => listener?.(batchEvent([{ ...message, body }])))
expect(hook!.queued.cards).toMatchObject([{ messageId: 'image-card', text: 'edited' }])
expect(body.blocks).toContainEqual({ type: 'image-ref', path: '/images/snapshot.png' })
expect(hook!.queued.editor).toBeUndefined()
expect(fieldsOf(holdCalls().at(-1)?.[1])).toMatchObject({
action: 'release',
@@ -191,9 +205,9 @@ describe('mobile in-place queued Edit', () => {
const message = queuedDraft({ messageId: 'draft-1' })
await mount(message)
const newer = withText(message.body, 'edited elsewhere')
sendRequest.mockImplementation(async (method) => {
sendRequest.mockImplementation(async (method, params) => {
if (method !== 'agentSession.queuedMessageUpdate') {
return ok({ status: 'held' })
return readOrHoldReply(method, params)
}
listener?.(batchEvent([{ ...message, body: newer }]))
return mutationOk({ status: 'changed', messageId: 'draft-1' })
@@ -209,12 +223,12 @@ describe('mobile in-place queued Edit', () => {
expect(hook!.queued.editor).toMatchObject({ messageId: 'draft-1', text: 'edited elsewhere' })
act(() => hook!.queued.editor!.change('saved text'))
const body = withText(message.body, 'saved text')
sendRequest.mockImplementation(async (method) => {
sendRequest.mockImplementation(async (method, params) => {
if (method === 'agentSession.queuedMessageUpdate') {
listener?.(batchEvent([{ ...message, body }]))
throw markRpcDeliveryUnknown(new Error('reply lost'))
}
return ok({ status: 'held' })
return readOrHoldReply(method, params)
})
await act(async () => {
await hook!.queued.editor!.save()
@@ -250,12 +264,12 @@ describe('mobile in-place queued Edit', () => {
const message = queuedDraft({ messageId: 'deferred-card' })
await mount(message)
let answer: ((response: RpcResponse) => void) | undefined
sendRequest.mockImplementation((method) =>
sendRequest.mockImplementation((method, params) =>
method === 'agentSession.queuedMessageUpdate'
? new Promise<RpcResponse>((resolve) => {
answer = resolve
})
: Promise.resolve(ok({ status: 'held' }))
: Promise.resolve(readOrHoldReply(method, params))
)
act(() => hook!.queued.editor!.change('edited in place'))
let save: Promise<boolean> | undefined
@@ -286,12 +300,12 @@ describe('mobile in-place queued Edit', () => {
nextMessage.messageId
])
let answer: ((response: RpcResponse) => void) | undefined
sendRequest.mockImplementation((method) =>
sendRequest.mockImplementation((method, params) =>
method === 'agentSession.queuedMessageUpdate'
? new Promise<RpcResponse>((resolve) => {
answer = resolve
})
: Promise.resolve(ok({ status: 'held' }))
: Promise.resolve(readOrHoldReply(method, params))
)
act(() => hook!.queued.editor!.change('submitted edit'))
let save: Promise<boolean> | undefined
@@ -327,13 +341,13 @@ describe('mobile in-place queued Edit', () => {
it('keeps a refused Save in the editor and closes only when the published card is gone', async () => {
const message = queuedDraft({ messageId: 'refused-card' })
await mount(message)
sendRequest.mockImplementation(async (method) =>
sendRequest.mockImplementation(async (method, params) =>
method === 'agentSession.queuedMessageUpdate'
? ok({
ok: false,
refusal: { code: 'agent_session_operation_invalid', message: 'Could not edit.' }
})
: ok({ status: 'held' })
: readOrHoldReply(method, params)
)
act(() => hook!.queued.editor!.change('keep this text'))
await act(async () => {
@@ -361,7 +375,9 @@ describe('mobile in-place queued Edit', () => {
expect(hook!.queued.editor?.text).toBe(text)
const reconnectedRequest = vi.fn<RpcClient['sendRequest']>(async (method, params) =>
method === 'agentSession.queuedMessageEditHold'
? ok({ status: fieldsOf(params).action === 'renew' ? 'expired' : 'held' })
? fieldsOf(params).action === 'renew'
? ok({ status: 'expired' })
: editHoldReply(params)
: ok({ models: [], current: {} })
)
await act(async () =>
@@ -404,7 +420,7 @@ describe('mobile in-place queued Edit', () => {
answerHold = resolve
})
: method === 'agentSession.queuedMessageEditHold'
? Promise.resolve(ok({ status: 'held' }))
? Promise.resolve(readOrHoldReply(method, params))
: Promise.resolve(
mutationOk({ status: 'updated', messageId: message.messageId, fingerprint: 'new' })
)
@@ -418,7 +434,7 @@ describe('mobile in-place queued Edit', () => {
})
expect(hook!.queued.editor).toBeUndefined()
expect(answerHold).toBeDefined()
await act(async () => answerHold?.(ok({ status: 'held' })))
await act(async () => answerHold?.(editHoldReply({ action: 'acquire' })))
expect(append).not.toHaveBeenCalled()
})
})
@@ -49,9 +49,12 @@ describe("the phone's Stop press read as Stopping", () => {
tracked = press.track(() => new Promise<boolean>((_, reject) => (fail = reject)))
})
expect(latest?.pressed).toBe(true)
const failure = new Error('socket closed')
await act(async () => {
fail(new Error('socket closed'))
await tracked.catch(() => undefined)
const rejected = expect(tracked).rejects.toBe(failure)
fail(failure)
await rejected
})
expect(latest?.pressed).toBe(false)
@@ -52,13 +52,21 @@ const bindingRecovery = {
}
describe('workspace session terminal binding replay', () => {
it('retains a restorable legacy tab binding when neither snapshot has a layout', () => {
it('retains only restorable legacy tab bindings when neither snapshot has a layout', () => {
const prior = session('runtime-pty')
const incoming = session(null)
preserveMissingWorkspaceSessionTerminalBindings(incoming, prior, bindingRecovery as never)
expect(incoming.tabsByWorktree.worktree[0]!.ptyId).toBe('runtime-pty')
const unavailable = session(null)
preserveMissingWorkspaceSessionTerminalBindings(unavailable, prior, {
...bindingRecovery,
getTerminalLayoutLeafIds: () => new Set<string>(),
isRestorablePtyBinding: () => false
})
expect(unavailable.tabsByWorktree.worktree[0]!.ptyId).toBeNull()
})
it('does not retain a tab binding whose prior leaf was removed from the layout', () => {
@@ -272,7 +272,7 @@ describe('worktree meta alias projection', () => {
* now-dangling aliases and `migrateLegacyWorktreeMetadata` re-mints the identical identity key
* from the `instanceId` the locator row still carries. Nothing is lost at any step.
*/
it('loses no row on a build that has no rebuild at all', () => {
it('preserves rows through metadata normalization before current Store reimport', () => {
const fixture = buildFixture()
const dataFile = tempDataFile()
writeFixture(dataFile, fixture.state)
@@ -1,27 +1,12 @@
import { describe, expect, it, vi } from 'vitest'
import {
classifyTabEntryQuery,
getTabEntryOptions,
openTabEntryWithOperations,
TAB_ENTRY_ABSOLUTE_PATH_REMOTE_BLOCKED_MESSAGE,
validateNewTabEntryAbsolutePath,
validateNewTabEntryRelativePath,
type TabEntryOperations
} from './tab-create-entry-action'
import * as classifier from './tab-create-entry-classifier'
const readyFiles = (files: string[]) => ({ files, loading: false, loadError: null })
it('keeps the pure classifier available through the tab action module', () => {
expect(classifyTabEntryQuery).toBe(classifier.classifyTabEntryQuery)
expect(getTabEntryOptions).toBe(classifier.getTabEntryOptions)
expect(TAB_ENTRY_ABSOLUTE_PATH_REMOTE_BLOCKED_MESSAGE).toBe(
classifier.TAB_ENTRY_ABSOLUTE_PATH_REMOTE_BLOCKED_MESSAGE
)
expect(validateNewTabEntryAbsolutePath).toBe(classifier.validateNewTabEntryAbsolutePath)
expect(validateNewTabEntryRelativePath).toBe(classifier.validateNewTabEntryRelativePath)
})
describe('openTabEntryWithOperations', () => {
function makeOperations(overrides: Partial<TabEntryOperations> = {}): TabEntryOperations {
return {
@@ -157,5 +157,13 @@ describe('tab entry options over the real runtime listing', () => {
await drainMicrotasks()
expect(blockedIds(latestState(states))).not.toContain('loading')
expect(getTabEntryOptions('packages/app/package.json', latestState(states), 4)).toContainEqual({
id: 'existing-file:packages/app/package.json',
classification: {
kind: 'existing-file',
matchKind: 'exact-path',
relativePath: 'packages/app/package.json'
}
})
})
})
+1 -1
View File
@@ -28,7 +28,7 @@ describe('clipboard image limits', () => {
).toThrow('Clipboard image is too large')
})
it('rejects invalid or oversized image dimensions before pixel allocation', () => {
it('rejects invalid or oversized image dimensions', () => {
expect(() =>
assertClipboardImageDimensionsWithinLimit({
height: 1,
+5 -4
View File
@@ -1,4 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import * as utf8ByteLimits from './utf8-byte-limits'
import {
CLIPBOARD_TEXT_MEASURE_YIELD_CODE_UNITS,
CLIPBOARD_TEXT_TOO_LARGE_ERROR,
@@ -52,7 +53,7 @@ describe('clipboard text limits', () => {
expect(yieldToEventLoop).toHaveBeenCalled()
})
it('uses the default 256k code-unit yield cadence for accepted large clipboard text', async () => {
it('yields twice across two default clipboard measurement batches', async () => {
const yieldToEventLoop = vi.fn(async () => {})
const measurement = await measureClipboardTextByteLengthWithYield(
@@ -80,7 +81,7 @@ describe('clipboard text limits', () => {
})
it('rejects text whose string length already exceeds the byte limit without scanning', () => {
const codePointAt = vi.spyOn(String.prototype, 'codePointAt')
const measureBytes = vi.spyOn(utf8ByteLimits, 'measureUtf8ByteLength')
expect(isClipboardTextByteLengthOverLimit('x'.repeat(6), 5)).toBe(true)
expect(() => assertClipboardTextWithinLimit('x'.repeat(6), { maxBytes: 5 })).toThrow(
@@ -89,7 +90,7 @@ describe('clipboard text limits', () => {
expect(() => assertClipboardTextWriteWithinLimit('x'.repeat(6), { maxBytes: 5 })).toThrow(
CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR
)
expect(codePointAt).not.toHaveBeenCalled()
expect(measureBytes).not.toHaveBeenCalled()
})
it('lets each clipboard consumer override the shared default byte limits', async () => {
@@ -129,7 +130,7 @@ describe('clipboard text limits', () => {
).rejects.toThrow(CLIPBOARD_TEXT_TOO_LARGE_ERROR)
await expect(
assertClipboardTextWriteWithinLimitWithYield('copied-secret-token-value', { maxBytes: 4 })
).rejects.toThrow(CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR)
).rejects.toEqual(new Error(CLIPBOARD_TEXT_WRITE_TOO_LARGE_ERROR))
try {
await assertClipboardTextWithinLimitWithYield('secret-token-value', { maxBytes: 4 })