test: remove low-value suites and copied test oracles (#33708)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-10-25 18:28:44 -07:00
committed by GitHub
co-authored by OrcaWin
parent b23e456a64
commit 5de74e632b
61 changed files with 207 additions and 1990 deletions
-1
View File
@@ -1155,7 +1155,6 @@
2 test-no-wall-clock:src/shared/fish-query-reply-child-stdin.node-pty.test.ts
1 test-no-wall-clock:src/shared/git-binary-compatibility.test.ts
1 test-no-wall-clock:src/shared/mobile-push-contract.test.ts
2 test-no-wall-clock:src/shared/native-chat-ask-fifo.test.ts
1 test-no-wall-clock:src/shared/plugins/plugin-kill-list.test.ts
2 test-no-wall-clock:src/shared/pty-reply-echo-shapes.node-pty.test.ts
5 test-no-wall-clock:src/shared/sidebar-list/smart-sort.test.ts
@@ -135,7 +135,7 @@ describe('parseAgentSessionFileCached', () => {
expect(incremental?.totalTokens).toBe(420)
})
it('parses an oversized record without quadratic carry copying', async () => {
it('parses a multi-chunk record without quadratic carry copying', async () => {
const root = await makeTempDir()
const path = join(root, 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee.jsonl')
// Span many chunks to expose quadratic copying from repeatedly joining the carried record prefix.
@@ -163,7 +163,12 @@ describe('parseAgentSessionFileCached', () => {
process.platform,
stats
)
expect(parsed).not.toBeNull()
expect(parsed?.messageCount).toBe(3)
expect(parsed?.previewMessages.map((message) => message.text)).toEqual([
'question',
expect.stringMatching(/^x+\.\.\.$/),
'tail answer'
])
} finally {
Buffer.concat = originalConcat
}
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
import { createAccumulator, finalizeSession } from './session-scanner-accumulator'
import {
cloneSessionAccumulator,
createAccumulator,
finalizeSession
} from './session-scanner-accumulator'
import { addPreviewMessage } from './session-scanner-messages'
import type { SessionAccumulator } from './session-scanner-types'
@@ -36,6 +40,8 @@ describe('AI Vault preview-window truncation signal', () => {
it('carries the flag onto snapshots taken from a cloned accumulator', () => {
const accumulator = accumulatorWithUserTurns(PREVIEW_LIMIT + 1)
expect(finalizeSession({ ...accumulator }, 'darwin')?.previewMessagesTruncated).toBe(true)
expect(
finalizeSession(cloneSessionAccumulator(accumulator), 'darwin')?.previewMessagesTruncated
).toBe(true)
})
})
@@ -397,7 +397,7 @@ describe('AiVaultScannerServiceClient', () => {
client.dispose()
})
it('faults the child on malformed output without affecting later processes', async () => {
it('rejects the active request and kills the child on malformed output', async () => {
const { child, client } = setup()
const request = client.request({ type: 'request', operation: 'titles', requests: [] })
readyAiVaultServiceChild(child)
@@ -440,6 +440,7 @@ describe('AiVaultScannerServiceClient', () => {
})
it('re-reads the init frame on every spawn so a respawn sees current consent', async () => {
vi.useFakeTimers()
const children: AiVaultServiceTestChild[] = []
let enabled = false
const client = new AiVaultScannerServiceClient({
@@ -467,7 +468,10 @@ describe('AiVaultScannerServiceClient', () => {
children[0]!.emit('error', new Error('crashed'))
await expect(first).rejects.toThrow('crashed')
void client.request({ type: 'request', operation: 'titles', requests: [] }).catch(() => {})
await vi.waitFor(() => expect(children.length).toBeGreaterThan(1))
await vi.advanceTimersByTimeAsync(249)
expect(children).toHaveLength(1)
await vi.advanceTimersByTimeAsync(1)
expect(children).toHaveLength(2)
for (const respawned of children.slice(1)) {
expect(respawned.sent[0]).toMatchObject({ sessionSearch: { settings: { enabled: true } } })
}
@@ -79,10 +79,6 @@ describe('buildAiVaultServiceEnv', () => {
expect(env.OPENCODE_DB).toBe('opencode-alt.db')
})
it('runs the forked Electron binary as plain Node', () => {
expect(buildAiVaultServiceEnv({}, 'linux').ELECTRON_RUN_AS_NODE).toBe('1')
})
it('ignores a POSIX variable that only matches an allowed name by case', () => {
expect(buildAiVaultServiceEnv({ codex_home: '/tmp/spoof' }, 'linux')).toEqual({
ELECTRON_RUN_AS_NODE: '1'
@@ -145,10 +141,6 @@ describe('buildRelayAiVaultServiceEnv', () => {
expect(env.CODEX_HOME).toBeUndefined()
expect(env.PATH).toBe('/usr/bin')
})
it('stays plain Node rather than an Electron child', () => {
expect(buildRelayAiVaultServiceEnv({}, 'linux').ELECTRON_RUN_AS_NODE).toBeUndefined()
})
})
it('carries OMP root/profile inputs only to the desktop service, retaining empty canonical profile', () => {
@@ -96,16 +96,3 @@ it('refreshes a late root without rebuilding the index', async () => {
await indexedSessions('conversation', ['early-session', 'late-session'])
expect(close).not.toHaveBeenCalled()
})
it('keeps the live indexer when an unchanged root snapshot is refreshed', async () => {
await writeMessageGraphTranscript(openclawTranscript(spawnRoot, 'early-session'), [
'a conversation in a root the spawn already knew'
])
subject.apply(init(harness.roots))
await indexedSessions('conversation', ['early-session'])
const close = vi.spyOn(SessionSearchIndexer.prototype, 'close')
currentRoots = { ...spawnRoots }
await indexedSessions('conversation', ['early-session'])
expect(close).not.toHaveBeenCalled()
})
@@ -110,12 +110,4 @@ describe('attachAiVaultScopeCwdAliases', () => {
expect(listed.sessions).toEqual(rows)
})
it('returns the scan result unchanged when no scope path is a link', async () => {
const scanned = result([session('real', realRepo)])
expect(
attachAiVaultScopeCwdAliases(scanned, await resolveAiVaultScopeRealPaths([realRepo]))
).toBe(scanned)
})
})
+15 -20
View File
@@ -17,7 +17,9 @@ const OBSERVED: Named[] = [
["'none'", 'none'],
["'unknown'", 'unknown'],
['same object', { ...META }],
['different object', { ...OTHER }]
['different object', { ...OTHER }],
['different path', { ...META, path: '/chats/b/meta.json' }],
['different size', { ...META, sizeBytes: 21 }]
]
// entry (row) x observed (column). A `true` cell is a cache hit.
@@ -27,28 +29,36 @@ const TRUTH_TABLE: Record<string, Record<string, boolean>> = {
"'none'": true,
"'unknown'": false,
'same object': false,
'different object': false
'different object': false,
'different path': false,
'different size': false
},
"'none'": {
undefined: true,
"'none'": true,
"'unknown'": false,
'same object': false,
'different object': false
'different object': false,
'different path': false,
'different size': false
},
"'unknown'": {
undefined: false,
"'none'": false,
"'unknown'": false,
'same object': false,
'different object': false
'different object': false,
'different path': false,
'different size': false
},
object: {
undefined: false,
"'none'": false,
"'unknown'": false,
'same object': true,
'different object': false
'different object': false,
'different path': false,
'different size': false
}
}
@@ -57,18 +67,3 @@ describe.each(ENTRIES)('cached %s', (entryLabel, entry) => {
expect(sidecarUnchanged(entry, observed)).toBe(TRUTH_TABLE[entryLabel][observedLabel])
})
})
it('treats a vanished sidecar as a change, not as "never had one"', () => {
expect(sidecarUnchanged({ ...META }, 'none')).toBe(false)
})
it('never concludes anything from an unreadable sidecar, in either position', () => {
expect(sidecarUnchanged('unknown', 'none')).toBe(false)
expect(sidecarUnchanged('unknown', { ...META })).toBe(false)
expect(sidecarUnchanged({ ...META }, 'unknown')).toBe(false)
})
it('keeps an agent with no sidecar at all a cache hit', () => {
expect(sidecarUnchanged(undefined, undefined)).toBe(true)
expect(sidecarUnchanged(undefined, 'none')).toBe(true)
})
@@ -152,14 +152,3 @@ it('publishes an OpenCode SQLite session through the same channel as every file
}
])
})
it('gives an OpenCode session the same three roles a file provider publishes', async () => {
const reads = await readEveryAgentVault()
const sqliteRead = reads.find(
(read) => splitOpenCodeSqliteCandidate(read.path)?.sessionId === OPENCODE_SQLITE_SESSION
)
expect(new Set(sqliteRead?.messages.map((message) => message.role))).toEqual(
new Set(['user', 'assistant', 'tool'])
)
})
@@ -1,12 +1,10 @@
import { expect, it, vi } from 'vitest'
import {
boundedText,
transcriptMessageFromText,
transcriptMessagesFromDecodedMessage,
transcriptMessagesFromContent
} from './session-transcript-message-content'
import { decodeSessionMessage } from './session-decoded-message'
import { asRecord } from './session-scanner-record-value'
const AT = '2026-05-01T10:00:00.000Z'
const MESSAGE_LIMIT = 256 * 1024
@@ -196,132 +194,50 @@ it('bounds joins for a large multi-block turn and keeps its later tool messages'
expect(largestJoinedLength).toBeLessThanOrEqual(MESSAGE_LIMIT + 1)
})
function previousToolResultText(content: unknown): string | null {
if (typeof content === 'string') {
return boundedText(content)
}
if (!Array.isArray(content)) {
return null
}
const parts: string[] = []
let length = 0
for (const item of content) {
const text = typeof item === 'string' ? item : asRecord(item)?.text
if (typeof text === 'string' && text) {
parts.push(text)
length += text.length
if (length >= MESSAGE_LIMIT) {
break
}
}
}
return boundedText(parts.join('\n'))
}
it('matches the previous tool-result collector at newline and surrogate boundaries', () => {
const cases: unknown[][] = [
['x'.repeat(MESSAGE_LIMIT - 2), '\ud800'],
['x'.repeat(MESSAGE_LIMIT - 1), '\ud800'],
[`${'x'.repeat(MESSAGE_LIMIT - 1)}\ud800`, 'later'],
['x'.repeat(MESSAGE_LIMIT - 2), '\ud800\udc00', 'later'],
['x'.repeat(MESSAGE_LIMIT), '', 'later'],
['', '', '\ud800', '\udfff', '\u0000'],
[' '.repeat(MESSAGE_LIMIT), 'visible beyond the cap'],
[null, 10, { type: 'image', text: 'still collected' }, { content: 'ignored' }]
it('bounds joined text at newline and raw surrogate boundaries', () => {
const cases = [
{
parts: ['x'.repeat(MESSAGE_LIMIT - 2), '\ud800'],
text: `${'x'.repeat(MESSAGE_LIMIT - 2)}\n\ud800`,
truncated: false
},
{
parts: ['x'.repeat(MESSAGE_LIMIT - 1), '\ud800'],
text: `${'x'.repeat(MESSAGE_LIMIT - 1)}\n`,
truncated: true
},
{
parts: [`${'x'.repeat(MESSAGE_LIMIT - 1)}\ud800`, 'later'],
text: 'x'.repeat(MESSAGE_LIMIT - 1),
truncated: true
},
{
parts: ['x'.repeat(MESSAGE_LIMIT - 2), '\ud800\udc00', 'later'],
text: `${'x'.repeat(MESSAGE_LIMIT - 2)}\n`,
truncated: true
},
{
parts: ['x'.repeat(MESSAGE_LIMIT - 3), '\ud800\udc00', 'later'],
text: `${'x'.repeat(MESSAGE_LIMIT - 3)}\n\ud800\udc00`,
truncated: true
},
{
parts: ['x'.repeat(MESSAGE_LIMIT), '', 'later'],
text: 'x'.repeat(MESSAGE_LIMIT),
truncated: true
},
{
parts: ['', '', '\ud800', '\udfff', '\u0000'],
text: '\n\n\ud800\n\udfff\n\u0000',
truncated: false
},
{ parts: [' '.repeat(MESSAGE_LIMIT), 'visible beyond the cap'], text: null, truncated: false }
]
let seed = 0x6a09e667
const random = (): number => {
seed ^= seed << 13
seed ^= seed >>> 17
seed ^= seed << 5
return seed >>> 0
}
const lengths = [0, 1, 12, 4096, MESSAGE_LIMIT - 1, MESSAGE_LIMIT, MESSAGE_LIMIT + 1]
const atoms = ['a', ' ', '\n', '\ud800', '\udfff', '\ud83d\ude80', '\u0000', '漢']
for (let sample = 0; sample < 120; sample++) {
cases.push(
Array.from({ length: 1 + (random() % 6) }, (_, index) => {
const length = lengths[random() % lengths.length]
const atom = atoms[random() % atoms.length]
const text = atom.repeat(Math.ceil(length / atom.length)).slice(0, length)
return index % 2 === 0 ? text : { type: 'text', text }
})
for (const { parts, text, truncated } of cases) {
expect(transcriptMessagesFromContent('user', parts, AT)).toEqual(
text === null
? []
: [{ role: 'user', text, timestamp: AT, ...(truncated ? { textTruncated: true } : {}) }]
)
}
for (const content of cases) {
const text = previousToolResultText(content)
expect(
transcriptMessagesFromContent(
'user',
[
{ type: 'tool_result', content },
{ type: 'tool_use', name: 'Read', input: { path: 'after' } },
{ type: 'tool_result', content: 'finished' }
],
AT
).map(({ role, text, timestamp }) => ({ role, text, timestamp }))
).toEqual([
...(text ? [{ role: 'tool', text, timestamp: AT }] : []),
{ role: 'tool', text: 'Read: after', timestamp: AT },
{ role: 'tool', text: 'finished', timestamp: AT }
])
}
})
it('matches join-then-bound at newline and raw surrogate boundaries', () => {
const partsCases = [
['x'.repeat(MESSAGE_LIMIT - 2), '\ud800'],
['x'.repeat(MESSAGE_LIMIT - 1), '\ud800'],
[`${'x'.repeat(MESSAGE_LIMIT - 1)}\ud800`, 'later'],
['x'.repeat(MESSAGE_LIMIT - 2), '\ud800\udc00', 'later'],
['x'.repeat(MESSAGE_LIMIT - 3), '\ud800\udc00', 'later'],
['x'.repeat(MESSAGE_LIMIT), '', 'later'],
['', '', '\ud800', '\udfff', '\u0000'],
[' '.repeat(MESSAGE_LIMIT), 'visible beyond the cap']
]
for (const parts of partsCases) {
const text = boundedText(parts.join('\n'))
expect(
transcriptMessagesFromContent('user', parts, AT).map(({ role, text, timestamp }) => ({
role,
text,
timestamp
}))
).toEqual(text ? [{ role: 'user', text, timestamp: AT }] : [])
}
})
it('matches the previous join-then-bound output across generated block mixtures', () => {
let seed = 0x6a09e667
const random = (): number => {
seed ^= seed << 13
seed ^= seed >>> 17
seed ^= seed << 5
return seed >>> 0
}
const lengths = [0, 1, 12, 4096, MESSAGE_LIMIT - 1, MESSAGE_LIMIT, MESSAGE_LIMIT + 1]
const atoms = ['a', ' ', '\n', '\ud800', '\udfff', '\ud83d\ude80', '\u0000', '漢']
const roles = ['user', 'assistant', 'tool', 'system', 'unknown'] as const
for (let sample = 0; sample < 120; sample++) {
const parts = Array.from({ length: 2 + (random() % 7) }, () => {
const length = lengths[random() % lengths.length]
const atom = atoms[random() % atoms.length]
return atom.repeat(Math.ceil(length / atom.length)).slice(0, length)
})
const role = roles[random() % roles.length]
const content = parts.map((text, index) => (index % 2 === 0 ? text : { type: 'Text', text }))
const text = boundedText(parts.filter((part, index) => index % 2 === 0 || part).join('\n'))
expect(
transcriptMessagesFromContent(
role,
[...content, { type: 'tool_use', name: 'Bash', input: { command: 'pwd' } }],
AT
).map(({ role, text, timestamp }) => ({ role, text, timestamp }))
).toEqual([
...(text && (role === 'user' || role === 'assistant' || role === 'tool')
? [{ role, text, timestamp: AT }]
: []),
{ role: 'tool', text: 'Bash: pwd', timestamp: AT }
])
}
})
@@ -154,7 +154,7 @@ describe('formatBrowserAnnotationsAsMarkdown', () => {
expect(account).toContain('**Feedback:** B feedback')
})
it('escapes and bounds added mixed-page context with the existing inline formatting', () => {
it('escapes added mixed-page context with the existing inline formatting', () => {
const first = makeAnnotation()
const second = makeAnnotation({ browserPageId: 'page`id\n## injected' })
second.payload.page.sanitizedUrl = 'invalid`url\n## injected'
@@ -164,23 +164,6 @@ describe('formatBrowserAnnotationsAsMarkdown', () => {
expect(markdown).not.toContain('\n## injected')
})
it('includes agent-useful selectors, source, react tree, styles, and feedback', () => {
const markdown = formatBrowserAnnotationsAsMarkdown([makeAnnotation()])
expect(markdown).toContain('## Design Feedback: /pricing')
expect(markdown).toContain('**Browser tab id:** page-1')
expect(markdown).not.toContain('Orca CLI')
expect(markdown).not.toContain('--page page-1')
expect(markdown).not.toContain('Page Feedback')
expect(markdown).toContain('**Selector:** `main.pricing > button.primary`')
expect(markdown).toContain('**Source:** src/components/PricingCta.tsx:42:8')
expect(markdown).toContain('**React:** <App> <PricingCta>')
expect(markdown).toContain('**Intent:** change')
expect(markdown).not.toContain('**Priority:**')
expect(markdown).toContain('- font-size: 16px')
expect(markdown).toContain('**Feedback:** Make this primary action more obvious.')
})
it('returns an empty string when no annotations exist', () => {
expect(formatBrowserAnnotationsAsMarkdown([])).toBe('')
})
@@ -309,7 +292,7 @@ describe('formatBrowserAnnotationsAsMarkdown', () => {
)
expect(selectedLineStart).not.toBe(-1)
expect(selectedLine).toBeDefined()
expect(selectedLine).toBe(`**Selected text:** "${'x'.repeat(2047)}"`)
expect(selectedLine).not.toContain('😀')
expect(selectedLine).not.toContain('tail')
expect(selectedLine).not.toContain('\ufffd')
@@ -101,7 +101,7 @@ function renderTray(currentUrl?: string): {
}
describe('BrowserPageAnnotationTray edit mode', () => {
it('attributes a retained note to its captured page while preserving edit and delete controls', () => {
it('attributes a retained note to its captured page while preserving deletion', () => {
const { handleDeleteBrowserAnnotation } = renderTray('https://example.com/next')
expect(screen.getByTitle('https://example.com')).toHaveTextContent('https://example.com')
fireEvent.click(screen.getByRole('button', { name: 'Delete annotation 1' }))
@@ -45,7 +45,7 @@ describe('current-document annotation marker projection', () => {
)
})
it('does not resurrect old same-URL geometry on dom-ready or return, but permits pending viewport events', () => {
it('keeps retired same-URL geometry hidden and permits pending viewport events', () => {
const { setAnnotationViewportBridge, notes } = createBridge()
const args = {
toolTargetId: 'page-1',
@@ -100,7 +100,7 @@ describe('current-document annotation marker projection', () => {
)
})
it('tolerates a destroyed guest without retaining or throwing a rejected bridge call', async () => {
it('handles a rejected bridge call without a synchronous exception', async () => {
const { setAnnotationViewportBridge, notes } = createBridge()
setAnnotationViewportBridge.mockRejectedValueOnce(new Error('Guest destroyed'))
expect(() =>
@@ -31,7 +31,7 @@ describe('markup document undo/redo', () => {
expect(canRedo(doc)).toBe(false)
})
it('commits shapes and clears the redo stack', () => {
it('commits shapes in order and enables undo', () => {
let doc = createMarkupDocument()
doc = commitShape(doc, pen('a'))
doc = commitShape(doc, pen('b'))
@@ -115,7 +115,7 @@ describe('markup geometry', () => {
])
})
it('scales rect/arrow endpoints and text position + font size', () => {
it('scales arrow endpoints and text position + font size', () => {
const arrow: ArrowShape = {
id: 'a',
kind: 'arrow',
@@ -3,8 +3,7 @@ import {
clampMarkupScale,
dataUrlByteLength,
effectiveMarkupScale,
markupCanvasSize,
MARKUP_DOWNSCALE_STEPS
markupCanvasSize
} from './markup-screenshot-compose'
describe('clampMarkupScale', () => {
@@ -62,11 +61,3 @@ describe('dataUrlByteLength', () => {
expect(dataUrlByteLength('not-a-data-url')).toBe(0)
})
})
describe('compose budget plans', () => {
it('tries full size first, then shrinks (PNG only, descending)', () => {
expect(MARKUP_DOWNSCALE_STEPS[0]).toBe(1)
expect([...MARKUP_DOWNSCALE_STEPS]).toEqual([...MARKUP_DOWNSCALE_STEPS].sort((a, b) => b - a))
expect(MARKUP_DOWNSCALE_STEPS.every((s) => s > 0 && s <= 1)).toBe(true)
})
})
@@ -61,8 +61,11 @@ describe('drawShape dispatch', () => {
]
}
drawShape(ctx, shape)
expect(methods('moveTo')).toHaveLength(1)
expect(methods('lineTo')).toHaveLength(2)
expect(methods('moveTo')).toEqual([{ method: 'moveTo', args: [0, 0] }])
expect(methods('lineTo')).toEqual([
{ method: 'lineTo', args: [5, 5] },
{ method: 'lineTo', args: [9, 1] }
])
expect(methods('stroke')).toHaveLength(1)
})
@@ -132,7 +135,7 @@ describe('drawShape dispatch', () => {
})
it('renders single-line text with a halo (stroke behind fill)', () => {
const { ctx, methods } = makeRecordingCtx()
const { ctx, calls } = makeRecordingCtx()
drawShape(ctx, {
id: 't',
kind: 'text',
@@ -141,13 +144,14 @@ describe('drawShape dispatch', () => {
text: 'fix this',
fontSize: 18
})
expect(methods('strokeText')).toHaveLength(1)
expect(methods('fillText')).toHaveLength(1)
expect(methods('fillText')[0].args[0]).toBe('fix this')
expect(calls.filter((call) => call.method.endsWith('Text'))).toEqual([
{ method: 'strokeText', args: ['fix this', 5, 5] },
{ method: 'fillText', args: ['fix this', 5, 5] }
])
})
it('save/restore brackets every shape so state never leaks', () => {
const { ctx, methods } = makeRecordingCtx()
it('brackets rectangle drawing with save and restore calls', () => {
const { ctx, calls } = makeRecordingCtx()
drawShape(ctx, {
id: 'r',
kind: 'rect',
@@ -156,8 +160,11 @@ describe('drawShape dispatch', () => {
from: { x: 0, y: 0 },
to: { x: 1, y: 1 }
})
expect(methods('save')).toHaveLength(1)
expect(methods('restore')).toHaveLength(1)
expect(calls).toEqual([
{ method: 'save', args: [] },
{ method: 'strokeRect', args: [0, 0, 1, 1] },
{ method: 'restore', args: [] }
])
})
})
@@ -519,7 +519,7 @@ describe('BrowserAddressBar autocomplete preview', () => {
expect(mocks.removeBrowserHistoryEntry).not.toHaveBeenCalled()
})
it('dismisses suggestions when focus moves into an Electron webview guest', async () => {
it('dismisses suggestions on a webview focusin event', async () => {
const onNavigate = vi.fn()
const onSubmit = vi.fn()
const webview = document.createElement('webview')
@@ -556,7 +556,7 @@ describe('BrowserAddressBar autocomplete preview', () => {
expect(onSubmit).not.toHaveBeenCalled()
})
it('lets the second press of a double-click select a word instead of the whole URL', async () => {
it('clears the whole-URL selection on the second double-click press', async () => {
const url = 'https://github.com/stablyai/orca/pull/27021'
await act(async () => {
root.render(<AddressBarHarness initialValue={url} onNavigate={vi.fn()} onSubmit={vi.fn()} />)
@@ -47,7 +47,15 @@ describe('BrowserFind session flags', () => {
it('starts a new find session when the query changes', () => {
const { findInPage } = openFindWithQuery('needle')
expect(findInPage).toHaveBeenLastCalledWith('needle', { findNext: true })
expect(findInPage).toHaveBeenCalledExactlyOnceWith('needle', { findNext: true })
findInPage.mockClear()
fireEvent.change(screen.getByPlaceholderText('Find in page...'), {
target: { value: 'replacement' }
})
act(() => vi.advanceTimersByTime(250))
expect(findInPage).toHaveBeenCalledExactlyOnceWith('replacement', { findNext: true })
})
it('advances forward as a follow-up request, not a new session', () => {
@@ -1,33 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
BROWSER_ADDRESS_BAR_MIN_INLINE_WIDTH,
isBrowserAddressBarCollapsed,
shouldOverlayBrowserAddressBar
} from './browser-address-bar-expansion'
describe('isBrowserAddressBarCollapsed', () => {
it('treats an unmeasured slot as roomy so the bar never overlays before layout', () => {
expect(isBrowserAddressBarCollapsed(null)).toBe(false)
})
it('collapses below the minimum inline width', () => {
expect(isBrowserAddressBarCollapsed(BROWSER_ADDRESS_BAR_MIN_INLINE_WIDTH - 1)).toBe(true)
expect(isBrowserAddressBarCollapsed(48)).toBe(true)
})
it('stays inline at or above the minimum width', () => {
expect(isBrowserAddressBarCollapsed(BROWSER_ADDRESS_BAR_MIN_INLINE_WIDTH)).toBe(false)
expect(isBrowserAddressBarCollapsed(640)).toBe(false)
})
})
describe('shouldOverlayBrowserAddressBar', () => {
it('overlays only while a collapsed bar is focused', () => {
expect(shouldOverlayBrowserAddressBar({ inlineWidth: 48, focused: true })).toBe(true)
expect(shouldOverlayBrowserAddressBar({ inlineWidth: 48, focused: false })).toBe(false)
})
it('never overlays a bar that already fits', () => {
expect(shouldOverlayBrowserAddressBar({ inlineWidth: 640, focused: true })).toBe(false)
})
})
@@ -209,20 +209,20 @@ describe('browser address bar suggestions', () => {
url: 'https://acme.dev/a',
normalizedUrl: 'https://acme.dev/a',
title: 'A',
lastVisitedAt: hoursAgo(23),
lastVisitedAt: hoursAgo(1),
visitCount: 50
}),
historyEntry({
url: 'https://acme.dev/b',
normalizedUrl: 'https://acme.dev/b',
title: 'B',
lastVisitedAt: hoursAgo(1),
lastVisitedAt: hoursAgo(23),
visitCount: 5000
})
]
})
expect(historyUrls(suggestions)).toEqual(['https://acme.dev/b', 'https://acme.dev/a'])
expect(historyUrls(suggestions)).toEqual(['https://acme.dev/a', 'https://acme.dev/b'])
})
it('floors the recency bonus at zero for entries older than a day', () => {
@@ -89,19 +89,17 @@ describe('BrowserChromeFoldedMenuItems', () => {
const { container } = render(
<BrowserChromeFoldedMenuItems
tools={[
{ stage: 'draw', label: 'Draw', icon: Share2, onSelect: vi.fn(), active: false },
{ stage: 'share', label: 'Share', icon: Share2, onSelect: vi.fn() }
{ stage: 'share', label: 'Share', icon: Share2, onSelect: vi.fn() },
{ stage: 'draw', label: 'Draw', icon: Share2, onSelect: vi.fn(), active: false }
]}
deferUntilClose={vi.fn()}
/>
)
// Rendered order: Draw, divider, Share, closing divider.
expect(Array.from(container.querySelectorAll('button, hr')).map((el) => el.tagName)).toEqual([
'BUTTON',
'HR',
'BUTTON',
'HR'
])
expect(
Array.from(container.querySelectorAll('button, hr')).map((el) =>
el.tagName === 'HR' ? 'divider' : el.textContent
)
).toEqual(['Draw', 'divider', 'Share', 'divider'])
})
})
@@ -1,52 +0,0 @@
import { describe, expect, it } from 'vitest'
import { formatBrowserImportSummary } from './browser-detected-browsers-summary'
const SUPPORTED_LABELS = [
'Google Chrome',
'Microsoft Edge',
'Arc',
'Brave',
'Comet',
'Helium',
'Firefox',
'Safari'
]
describe('formatBrowserImportSummary', () => {
it('uses real detected browser labels after detection completes', () => {
expect(
formatBrowserImportSummary({
detectedBrowsers: [{ label: 'Google Chrome' }, { label: 'Safari' }],
detectedBrowsersLoaded: true,
supportedImportLabels: SUPPORTED_LABELS
})
).toBe('Detected: Google Chrome, Safari.')
})
it('summarizes long detected-browser lists with a trailing count', () => {
expect(
formatBrowserImportSummary({
detectedBrowsers: [
{ label: 'Google Chrome' },
{ label: 'Safari' },
{ label: 'Microsoft Edge' },
{ label: 'Arc' },
{ label: 'Firefox' },
{ label: 'Brave' }
],
detectedBrowsersLoaded: true,
supportedImportLabels: SUPPORTED_LABELS
})
).toBe('Detected: Google Chrome, Safari, Microsoft Edge, Arc, +2 more.')
})
it('falls back to supported import sources when detection finds nothing', () => {
expect(
formatBrowserImportSummary({
detectedBrowsers: [],
detectedBrowsersLoaded: true,
supportedImportLabels: SUPPORTED_LABELS
})
).toBe('Import from: Google Chrome, Microsoft Edge, Arc, Brave, +4 more.')
})
})
@@ -1,31 +0,0 @@
import { describe, expect, it } from 'vitest'
import { shouldShowBrowserImportHint } from './browser-import-hint-visibility'
describe('shouldShowBrowserImportHint', () => {
it('shows after persisted UI loads', () => {
expect(
shouldShowBrowserImportHint({
persistedUIReady: true,
browserImportHintHidden: false
})
).toBe(true)
})
it('stays hidden until persisted UI loads', () => {
expect(
shouldShowBrowserImportHint({
persistedUIReady: false,
browserImportHintHidden: false
})
).toBe(false)
})
it('honors explicit hint dismissal', () => {
expect(
shouldShowBrowserImportHint({
persistedUIReady: true,
browserImportHintHidden: true
})
).toBe(false)
})
})
@@ -1,26 +1,6 @@
// @vitest-environment happy-dom
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { useRef } from 'react'
const mocks = vi.hoisted(() => ({
addressBar: {
current: null as { onSubmit: () => void; onNavigate: (url: string) => void } | null
}
}))
vi.mock('./BrowserAddressBar', () => ({
default: (props: { value: string; onSubmit: () => void; onNavigate: (url: string) => void }) => {
mocks.addressBar.current = props
return <input aria-label="Address" value={props.value} readOnly />
}
}))
import BrowserAddressBar from './BrowserAddressBar'
import {
BROWSER_CHROME_ADDRESS_SLOT_ATTRIBUTE,
BROWSER_CHROME_ADDRESS_SLOT_HEIGHT_CLASS
} from './browser-chrome-address-slot'
import {
BrowserNavigationControlRow,
type BrowserNavigationControls
@@ -36,48 +16,12 @@ function renderRow(overrides: Partial<BrowserNavigationControls> = {}): BrowserN
navigate: vi.fn(),
...overrides
}
function Host(): React.JSX.Element {
const inputRef = useRef<HTMLInputElement | null>(null)
return (
<BrowserNavigationControlRow
controls={controls}
addressSlot={
<BrowserAddressBar
pageId="page-1"
value="https://example.com/"
committedValue="https://example.com/"
onChange={vi.fn()}
onSubmit={vi.fn()}
onNavigate={controls.navigate}
inputRef={inputRef}
/>
}
/>
)
}
render(<Host />)
render(<BrowserNavigationControlRow controls={controls} addressSlot={null} />)
return controls
}
/** The read-only counterpart the document preview passes, standing in for any non-URL identity. */
function renderWithIdentityChip(): void {
render(
<BrowserNavigationControlRow
controls={{
canGoBack: false,
canGoForward: false,
goBack: vi.fn(),
goForward: vi.fn(),
reload: vi.fn(),
navigate: vi.fn()
}}
addressSlot={<span>docs/report.html</span>}
/>
)
}
describe('BrowserNavigationControlRow', () => {
afterEach(() => cleanup())
afterEach(cleanup)
it('drives every history action through the controls seam', () => {
const controls = renderRow()
@@ -94,43 +38,4 @@ describe('BrowserNavigationControlRow', () => {
expect(screen.getByLabelText<HTMLButtonElement>('Back').disabled).toBe(true)
expect(screen.getByLabelText<HTMLButtonElement>('Forward').disabled).toBe(true)
})
it('routes an address-bar suggestion pick into the backend navigate', () => {
const controls = renderRow()
mocks.addressBar.current?.onNavigate('https://picked.example/')
expect(controls.navigate).toHaveBeenCalledWith('https://picked.example/')
})
it('anchors the contextual tour on whichever backend renders the row', () => {
renderRow()
expect(document.querySelector('[data-contextual-tour-target="browser-toolbar"]')).not.toBeNull()
})
// A read-only document identity must retain the same history controls as an address bar.
it('renders a non-address identity widget in the same slot', () => {
renderWithIdentityChip()
expect(screen.getByText('docs/report.html')).not.toBeNull()
expect(screen.queryByLabelText('Address')).toBeNull()
expect(screen.getByLabelText('Back')).not.toBeNull()
expect(screen.getByLabelText('Reload')).not.toBeNull()
})
// The row sets height so switching between input and document text cannot resize the toolbar.
it('gives every identity widget the same slot height, and stretches it to fill', () => {
renderRow()
const addressSlot = document.querySelector(`[${BROWSER_CHROME_ADDRESS_SLOT_ATTRIBUTE}]`)
const addressSlotClass = addressSlot?.className ?? ''
const addressWidgetParent = screen.getByLabelText('Address').parentElement
cleanup()
renderWithIdentityChip()
const chipSlot = document.querySelector(`[${BROWSER_CHROME_ADDRESS_SLOT_ATTRIBUTE}]`)
expect(addressSlotClass).toContain(BROWSER_CHROME_ADDRESS_SLOT_HEIGHT_CLASS)
expect(chipSlot?.className).toBe(addressSlotClass)
expect(addressSlotClass).toContain('items-stretch')
// Both widgets are direct children, so the slot's height reaches them instead of a wrapper.
expect(addressWidgetParent).toBe(addressSlot)
expect(screen.getByText('docs/report.html').parentElement).toBe(chipSlot)
})
})
@@ -1,34 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, expect, it } from 'vitest'
import { BrowserPageZoomIndicator } from './browser-page-zoom-indicator'
afterEach(cleanup)
it('preserves zoom feedback visibility and accessible status as it updates', () => {
const view = render(
<BrowserPageZoomIndicator
state={{ ariaHidden: false, opacityClassName: 'opacity-100' }}
percent={125}
/>
)
const indicator = screen.getByRole('status')
expect(indicator.textContent).toBe('125%')
expect(indicator.getAttribute('aria-live')).toBe('polite')
expect(indicator.getAttribute('aria-hidden')).toBe('false')
expect(indicator.classList.contains('opacity-100')).toBe(true)
expect(indicator.classList.contains('pointer-events-none')).toBe(true)
view.rerender(
<BrowserPageZoomIndicator
state={{ ariaHidden: true, opacityClassName: 'opacity-0' }}
percent={100}
/>
)
expect(screen.queryByRole('status')).toBeNull()
expect(screen.getByRole('status', { hidden: true })).toBe(indicator)
expect(indicator.textContent).toBe('100%')
expect(indicator.getAttribute('aria-hidden')).toBe('true')
expect(indicator.classList.contains('opacity-0')).toBe(true)
expect(indicator.classList.contains('opacity-100')).toBe(false)
})
@@ -1,40 +0,0 @@
import { describe, expect, it } from 'vitest'
import { getBrowserPagesForWorkspace } from './browser-pane-page-selection'
import type { BrowserPage } from '../../../../../shared/browser-workspace-types'
function makeBrowserPage(id: string): BrowserPage {
return {
id,
workspaceId: 'workspace-a',
worktreeId: 'worktree-a',
url: `https://example.com/${id}`,
title: id,
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 1
}
}
describe('getBrowserPagesForWorkspace', () => {
it('returns only the owning workspace page array so unrelated page updates keep the selector stable', () => {
const pages = [makeBrowserPage('page-1')]
const browserPagesByWorkspace = {
workspaceA: pages,
workspaceB: [makeBrowserPage('page-2')]
}
expect(getBrowserPagesForWorkspace(browserPagesByWorkspace, 'workspaceA')).toBe(pages)
expect(
getBrowserPagesForWorkspace(
{ ...browserPagesByWorkspace, workspaceB: [makeBrowserPage('page-3')] },
'workspaceA'
)
).toBe(pages)
expect(getBrowserPagesForWorkspace(browserPagesByWorkspace, 'missing')).toBe(
getBrowserPagesForWorkspace({}, 'missing')
)
})
})
@@ -169,28 +169,45 @@ describe('browser workspace pane retention props', () => {
it('threads all three retention terms to the page that owns them', () => {
renderWorkspacePane()
expect(mocks.pageProps.some((props) => props.id === 'page-b')).toBe(false)
cleanup()
const token = acquireBrowserAutomationVisibility('page-b')
hydrateBrowserDrivers([
{ browserPageId: 'page-b', driver: { kind: 'mobile', clientId: 'phone-1' } }
])
hydrateBrowserRemoteViewerPages(['page-b'])
renderWorkspacePane()
const token = acquireBrowserAutomationVisibility('page-b')
renderWorkspacePane()
expect(propsFor('page-b')).toEqual({
id: 'page-b',
isAutomationVisible: true,
isMobileDriven: false,
isRemotelyViewed: false
})
cleanup()
releaseBrowserAutomationVisibility(token)
hydrateBrowserDrivers([
{ browserPageId: 'page-b', driver: { kind: 'mobile', clientId: 'phone-1' } }
])
renderWorkspacePane()
expect(propsFor('page-b')).toEqual({
id: 'page-b',
isAutomationVisible: false,
isMobileDriven: true,
isRemotelyViewed: false
})
cleanup()
hydrateBrowserDrivers([])
hydrateBrowserRemoteViewerPages(['page-b'])
renderWorkspacePane()
expect(propsFor('page-b')).toEqual({
id: 'page-b',
isAutomationVisible: false,
isMobileDriven: false,
isRemotelyViewed: true
})
// Why the sibling: each term must land on the page it belongs to, not on every page of the tab.
expect(propsFor('page-a')).toEqual({
id: 'page-a',
isAutomationVisible: false,
isMobileDriven: false,
isRemotelyViewed: false
})
releaseBrowserAutomationVisibility(token)
})
})
@@ -1,63 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { MarkdownTableOfContentsPanel } from './MarkdownTableOfContentsPanel'
import type { MarkdownTocItem } from './markdown-table-of-contents'
const sampleItems: MarkdownTocItem[] = [
{
id: 'intro',
level: 1,
title: 'Intro',
children: [
{
id: 'setup',
level: 2,
title: 'Setup',
children: [
{
id: 'install',
level: 3,
title: 'Install',
children: [
{
id: 'configure',
level: 4,
title: 'Configure',
children: [
{
id: 'options',
level: 5,
title: 'Options',
children: []
}
]
}
]
}
]
}
]
}
]
describe('MarkdownTableOfContentsPanel', () => {
it('renders level collapse controls and disclosure buttons', () => {
const html = renderToStaticMarkup(
<MarkdownTableOfContentsPanel items={sampleItems} onClose={() => {}} onNavigate={() => {}} />
)
expect(html).toContain('Collapse by level')
expect(html).toContain('Collapse to heading level 1')
expect(html).toContain('Collapse to heading level 4')
expect(html).toContain('Expand all heading levels')
expect(html).toContain('H5')
expect(html).toContain('Collapse Intro')
expect(html).toContain('Install')
expect(html).toContain('Configure')
expect(html).toContain('Options')
expect(html).toContain('Intro')
expect(html).toContain('Setup')
expect(html).toContain('data-markdown-toc-resize-handle')
expect(html).toContain('Resize table of contents')
})
})
@@ -1,22 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { RichMarkdownSlashMenu } from './RichMarkdownSlashMenu'
describe('RichMarkdownSlashMenu', () => {
it('keeps a visible search field when there are no matching blocks', () => {
const html = renderToStaticMarkup(
<RichMarkdownSlashMenu
editor={null}
slashMenu={{ query: 'zzz', from: 1, to: 5, left: 0, top: 0 }}
filteredCommands={[]}
selectedIndex={0}
onImagePick={() => {}}
onEmojiPick={() => {}}
/>
)
expect(html).toContain('aria-label="Search blocks"')
expect(html).toContain('value="zzz"')
expect(html).toContain('No blocks found')
})
})
@@ -1,150 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { CmdJPaletteFeatureTipVisual } from './CmdJPaletteFeatureTipVisual'
const prefersReducedMotionMock = vi.hoisted(() => vi.fn(() => false))
const shortcutMock = vi.hoisted(() => vi.fn(() => ({ keys: ['⌘', 'J'], doubleTap: false })))
const formatShortcutMock = vi.hoisted(() => vi.fn(() => [{ keys: ['⌘', 'J'], doubleTap: false }]))
vi.mock('@/components/feature-wall/feature-wall-modal-helpers', () => ({
usePrefersReducedMotion: prefersReducedMotionMock
}))
vi.mock('@/hooks/useShortcutLabel', () => ({
useShortcutKeyDetails: shortcutMock,
formatShortcutKeyComboDetails: formatShortcutMock
}))
async function renderVisual(): Promise<{ container: HTMLDivElement; root: Root }> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
await act(async () => {
root.render(<CmdJPaletteFeatureTipVisual />)
})
return { container, root }
}
describe('CmdJPaletteFeatureTipVisual', () => {
beforeEach(() => {
prefersReducedMotionMock.mockReturnValue(false)
shortcutMock.mockReturnValue({ keys: ['⌘', 'J'], doubleTap: false })
formatShortcutMock.mockReturnValue([{ keys: ['⌘', 'J'], doubleTap: false }])
})
afterEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
document.body.innerHTML = ''
})
it('renders the static end state without motion classes when reduced motion is preferred', () => {
prefersReducedMotionMock.mockReturnValue(true)
const html = renderToStaticMarkup(<CmdJPaletteFeatureTipVisual />)
expect(html).toContain('auth')
expect(html).toContain('auth-redirect')
expect(html).not.toContain('payments-api')
expect(html).not.toContain('animate-spin')
expect(html).not.toContain('animate-cmd-j-tip-caret')
expect(html).not.toContain('animate-cmd-j-tip-result-in')
})
it('does not schedule animation timers when reduced motion is preferred', async () => {
prefersReducedMotionMock.mockReturnValue(true)
const setTimeoutSpy = vi.spyOn(window, 'setTimeout')
const { root } = await renderVisual()
await act(async () => {
root.unmount()
})
expect(setTimeoutSpy).not.toHaveBeenCalled()
})
it('clears pending timers on unmount', async () => {
vi.useFakeTimers()
const clearTimeoutSpy = vi.spyOn(window, 'clearTimeout')
const { root } = await renderVisual()
await act(async () => {
vi.advanceTimersByTime(500)
})
await act(async () => {
root.unmount()
})
expect(clearTimeoutSpy).toHaveBeenCalled()
})
it('settles after the one-shot demo instead of looping idle timers', async () => {
vi.useFakeTimers()
const { container, root } = await renderVisual()
await act(async () => {
vi.runAllTimers()
})
expect(container.textContent).toContain('auth')
expect(container.textContent).toContain('auth-redirect')
expect(container.textContent).not.toContain('payments-api')
expect(vi.getTimerCount()).toBe(0)
await act(async () => {
root.unmount()
})
})
it('does not render infinite animation classes in the default preview', () => {
const html = renderToStaticMarkup(<CmdJPaletteFeatureTipVisual />)
expect(html).not.toContain('animate-spin')
expect(html).not.toContain('animate-cmd-j-tip-caret')
})
it('falls back to default per-key chips when the live binding is unassigned', () => {
shortcutMock.mockReturnValue({ keys: [], doubleTap: false })
formatShortcutMock.mockReturnValue([{ keys: ['Ctrl', 'Shift', 'J'], doubleTap: false }])
const html = renderToStaticMarkup(<CmdJPaletteFeatureTipVisual />)
expect(formatShortcutMock).toHaveBeenCalledWith('worktree.palette')
expect(html).toContain('Ctrl')
expect(html).toContain('Shift')
expect(html).toContain('J')
})
it('shows the create-worktree row only once the demo query is non-empty', () => {
prefersReducedMotionMock.mockReturnValue(true)
const html = renderToStaticMarkup(<CmdJPaletteFeatureTipVisual />)
expect(html).toContain('Create worktree')
expect(html).toContain('auth')
})
it('renders the live binding as separate shortcut key chips with plus separators', () => {
shortcutMock.mockReturnValue({ keys: ['⌘', 'J'], doubleTap: false })
const html = renderToStaticMarkup(<CmdJPaletteFeatureTipVisual />)
expect(html).toContain('⌘')
expect(html).toContain('J')
expect(html).toContain('+')
})
it('renders double-tap shortcut chips without plus separators', () => {
shortcutMock.mockReturnValue({ keys: ['⇧', '⇧'], doubleTap: true })
const html = renderToStaticMarkup(<CmdJPaletteFeatureTipVisual />)
expect(html).toContain('⇧')
expect(html).not.toContain('+')
})
})
@@ -1,89 +0,0 @@
import type { ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { FEATURE_TIPS, type FeatureTip } from '../../../../shared/feature-tips'
import { CmdJPaletteTipDialog } from './CmdJPaletteTipDialog'
const shortcutLabelMock = vi.hoisted(() => vi.fn(() => '⌘J'))
const formatShortcutLabelMock = vi.hoisted(() => vi.fn(() => '⌘J'))
vi.mock('@/hooks/useShortcutLabel', () => ({
useShortcutLabel: shortcutLabelMock,
formatShortcutLabel: formatShortcutLabelMock
}))
vi.mock('./CmdJPaletteFeatureTipVisual', () => ({
CmdJPaletteFeatureTipVisual: () => <div data-testid="palette-visual" />
}))
vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
DialogFooter: ({ children }: { children: ReactNode }) => <footer>{children}</footer>,
DialogHeader: ({ children }: { children: ReactNode }) => <header>{children}</header>,
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>
}))
vi.mock('./FeatureTipActions', () => ({
FeatureTipActions: () => <div data-testid="feature-tip-actions" />
}))
function getCmdJPaletteTip(): FeatureTip {
const tip = FEATURE_TIPS.find((entry) => entry.id === 'cmd-j-palette')
if (!tip) {
throw new Error('Expected cmd-j-palette feature tip fixture')
}
return { ...tip }
}
const paletteTip = getCmdJPaletteTip()
function renderDialog(): string {
return renderToStaticMarkup(
<CmdJPaletteTipDialog
open
tip={paletteTip}
primaryBusy={false}
onOpenChange={vi.fn()}
onPrimaryAction={vi.fn()}
onSkip={vi.fn()}
onRebindClick={vi.fn()}
/>
)
}
describe('CmdJPaletteTipDialog', () => {
it('shows a tip badge so the dialog reads as education, not a new feature', () => {
const html = renderDialog()
expect(html).toContain('TIP')
})
it('inlines the live shortcut label in the title', () => {
shortcutLabelMock.mockReturnValue('⌘J')
const html = renderDialog()
expect(html).toContain('Jump to a worktree with')
expect(html).toContain('⌘J')
})
it('falls back to the default shortcut label when the binding is unassigned', () => {
shortcutLabelMock.mockReturnValue('Unassigned')
formatShortcutLabelMock.mockReturnValue('Ctrl+Shift+J')
const html = renderDialog()
expect(formatShortcutLabelMock).toHaveBeenCalledWith('worktree.palette')
expect(html).toContain('Ctrl+Shift+J')
expect(html).not.toContain('Unassigned')
})
it('keeps the rebind guidance inside the dialog description', () => {
const html = renderDialog()
expect(html).toContain('Settings → Shortcuts')
expect(html).toContain('spin up a new worktree')
})
})
@@ -1,91 +0,0 @@
import type { ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { unavailableSessionSearchStatus } from '../../../../shared/ai-vault-search-client'
import type { AiVaultSearchStatus } from '../../../../shared/ai-vault-search-types'
import { FEATURE_TIPS, type FeatureTip } from '../../../../shared/feature-tips'
import { SessionSearchTipDialog } from './SessionSearchTipDialog'
import type { SessionSearchTipStage } from './use-session-search-tip-setup'
vi.mock('./SessionSearchFeatureTipVisual', () => ({
SessionSearchFeatureTipVisual: () => <div data-testid="session-search-visual" />
}))
vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogDescription: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogFooter: ({ children }: { children: ReactNode }) => <footer>{children}</footer>,
DialogHeader: ({ children }: { children: ReactNode }) => <header>{children}</header>,
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>
}))
function getTip(): FeatureTip {
const tip = FEATURE_TIPS.find((entry) => entry.id === 'agent-session-search')
if (!tip) {
throw new Error('Expected agent-session-search feature tip fixture')
}
return tip
}
function render(stage: SessionSearchTipStage, status: AiVaultSearchStatus | null): string {
return renderToStaticMarkup(
<SessionSearchTipDialog
open
tip={getTip()}
primaryBusy={false}
onOpenChange={() => {}}
onPrimaryAction={() => {}}
onSettingsClick={() => {}}
stage={stage}
status={status}
/>
)
}
describe('SessionSearchTipDialog', () => {
it('offers to turn search on and links to its settings', () => {
const html = render('offer', null)
expect(html).toContain('Search every agent session')
expect(html).toContain('Turn on session search')
expect(html).toContain('Settings → Agent Session Search')
expect(html).toContain('session-search-visual')
})
it('shows live progress and lets indexing continue in the background', () => {
const html = render('indexing', {
...unavailableSessionSearchStatus(),
enabled: true,
phase: 'indexing',
filesIndexed: 120,
filesDue: 380,
messagesIndexed: 5000
})
expect(html).toContain('Indexing your agent sessions')
expect(html).toContain('Settings → Agent Session Search')
expect(html).toContain('120 of 500 sessions')
expect(html).toContain('Continue in background')
expect(html).not.toMatch(/<button[^>]*\sdisabled=""/)
expect(html).toContain('session-search-visual')
})
it('does not call a not-yet-started indexer unavailable', () => {
const html = render('indexing', { ...unavailableSessionSearchStatus(), enabled: true })
expect(html).toContain('Finding your sessions')
expect(html).not.toContain('not available')
})
it('offers to start searching once the index is ready', () => {
const html = render('ready', {
...unavailableSessionSearchStatus(),
enabled: true,
phase: 'current',
filesIndexed: 500,
lastSweepCompletedAt: 1
})
expect(html).toContain('Session search is ready')
expect(html).toContain('Start searching')
expect(html).toContain('Settings → Agent Session Search')
expect(html).not.toMatch(/<button[^>]*\sdisabled=""/)
})
})
@@ -1,88 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { VoiceDictationFeatureTipVisual } from './VoiceDictationFeatureTipVisual'
const prefersReducedMotionMock = vi.hoisted(() => vi.fn(() => false))
const shortcutMock = vi.hoisted(() => vi.fn(() => ({ keys: ['⌘', 'E'], doubleTap: false })))
vi.mock('@/components/feature-wall/feature-wall-modal-helpers', () => ({
usePrefersReducedMotion: prefersReducedMotionMock
}))
vi.mock('@/hooks/useShortcutLabel', () => ({
useShortcutKeyDetails: shortcutMock
}))
async function renderVisual(): Promise<{ container: HTMLDivElement; root: Root }> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
await act(async () => root.render(<VoiceDictationFeatureTipVisual />))
return { container, root }
}
describe('VoiceDictationFeatureTipVisual', () => {
beforeEach(() => {
prefersReducedMotionMock.mockReturnValue(false)
shortcutMock.mockReturnValue({ keys: ['⌘', 'E'], doubleTap: false })
})
afterEach(() => {
vi.clearAllMocks()
vi.useRealTimers()
document.body.innerHTML = ''
})
it('types the dictated prompt once and settles on the complete sentence', async () => {
vi.useFakeTimers()
const { container, root } = await renderVisual()
const prompt = container.querySelector('[data-testid="dictated-prompt"]')
expect(prompt?.textContent).toBe('')
await act(async () => vi.advanceTimersByTime(650))
expect(prompt?.textContent).toBe('R')
await act(async () => vi.runAllTimers())
expect(prompt?.textContent).toBe(
'Review this diff for edge cases and add tests for anything you find.'
)
expect(vi.getTimerCount()).toBe(0)
await act(async () => root.unmount())
})
it('gives the prompt the full remaining row width', () => {
const html = renderToStaticMarkup(<VoiceDictationFeatureTipVisual />)
const promptClass = html.match(/data-testid="dictated-prompt" class="([^"]+)"/)?.[1]
expect(promptClass).toContain('min-w-0')
expect(promptClass).toContain('flex-1')
expect(promptClass).not.toContain('max-w-')
})
it('renders the completed prompt without timers for reduced motion', async () => {
prefersReducedMotionMock.mockReturnValue(true)
const setTimeoutSpy = vi.spyOn(window, 'setTimeout')
const { container, root } = await renderVisual()
expect(container.textContent).toContain(
'Review this diff for edge cases and add tests for anything you find.'
)
expect(container.innerHTML).not.toContain('animate-waveform')
expect(setTimeoutSpy).not.toHaveBeenCalled()
await act(async () => root.unmount())
})
it('does not show a shortcut that the user has unassigned', () => {
shortcutMock.mockReturnValue({ keys: [], doubleTap: false })
const html = renderToStaticMarkup(<VoiceDictationFeatureTipVisual />)
expect(html).not.toContain('Start dictation')
expect(html).not.toContain('Ctrl')
})
})
@@ -1,134 +0,0 @@
// @vitest-environment happy-dom
import { act, type ReactNode, type RefObject } from 'react'
import { createRoot } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { FEATURE_TIPS, type FeatureTip } from '../../../../shared/feature-tips'
import { VoiceDictationTipDialog } from './VoiceDictationTipDialog'
const shortcutMock = vi.hoisted(() => vi.fn(() => ({ keys: ['⌘', 'E'], doubleTap: false })))
const dialogContentPropsMock = vi.hoisted(() => vi.fn())
const featureTipActionsPropsMock = vi.hoisted(() => vi.fn())
vi.mock('@/hooks/useShortcutLabel', () => ({
useShortcutKeyDetails: shortcutMock
}))
vi.mock('./VoiceDictationFeatureTipVisual', () => ({
VoiceDictationFeatureTipVisual: () => <div data-testid="voice-visual" />
}))
vi.mock('@/components/ui/dialog', () => ({
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogContent: ({
children,
...props
}: {
children: ReactNode
onOpenAutoFocus?: (event: Event) => void
}) => {
dialogContentPropsMock(props)
return <div>{children}</div>
},
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>,
DialogFooter: ({ children }: { children: ReactNode }) => <footer>{children}</footer>,
DialogHeader: ({ children }: { children: ReactNode }) => <header>{children}</header>,
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>
}))
vi.mock('./FeatureTipActions', () => ({
FeatureTipActions: (props: { primaryButtonRef?: RefObject<HTMLButtonElement | null> }) => {
featureTipActionsPropsMock(props)
return <button ref={props.primaryButtonRef}>Primary action</button>
}
}))
function getVoiceTip(): FeatureTip {
const tip = FEATURE_TIPS.find((entry) => entry.id === 'voice-dictation')
if (!tip) {
throw new Error('Expected voice-dictation feature tip fixture')
}
return { ...tip }
}
function renderDialog(): string {
return renderToStaticMarkup(
<VoiceDictationTipDialog
open
tip={getVoiceTip()}
primaryBusy={false}
onOpenChange={vi.fn()}
onPrimaryAction={vi.fn()}
onSkip={vi.fn()}
onVoiceSettingsClick={vi.fn()}
/>
)
}
describe('VoiceDictationTipDialog', () => {
beforeEach(() => {
shortcutMock.mockReturnValue({ keys: ['⌘', 'E'], doubleTap: false })
dialogContentPropsMock.mockClear()
featureTipActionsPropsMock.mockClear()
})
it('uses the established feature-tip layout and durable copy', () => {
const html = renderDialog()
expect(html).toContain('TIP')
expect(html).toContain('Dictate into any pane')
expect(html).not.toContain('Turn speech into text wherever')
expect(html).not.toContain('is here')
})
it('shows the live dictation shortcut and voice settings path', () => {
const html = renderDialog()
expect(html).toContain('⌘')
expect(html).toContain('E')
expect(html).toContain('to start voice dictation. Press')
expect(html).toContain('again to stop.')
expect(html).toContain('Settings → Voice')
})
it('uses generic instructions instead of a shortcut the user has unassigned', () => {
shortcutMock.mockReturnValue({ keys: [], doubleTap: false })
const html = renderDialog()
expect(html).toContain(
'Assign a dictation shortcut before starting voice dictation in a focused pane.'
)
expect(html).not.toContain('to start voice dictation. Press')
expect(html).not.toContain('Ctrl')
})
it('moves initial focus to the primary action', async () => {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
await act(async () => {
root.render(
<VoiceDictationTipDialog
open
tip={getVoiceTip()}
primaryBusy={false}
onOpenChange={vi.fn()}
onPrimaryAction={vi.fn()}
onSkip={vi.fn()}
onVoiceSettingsClick={vi.fn()}
/>
)
})
const event = { preventDefault: vi.fn() } as unknown as Event
dialogContentPropsMock.mock.lastCall?.[0].onOpenAutoFocus(event)
expect(event.preventDefault).toHaveBeenCalledOnce()
expect(document.activeElement?.textContent).toBe('Primary action')
await act(async () => root.unmount())
container.remove()
})
})
@@ -1,40 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, describe, expect, it } from 'vitest'
import { BrowserAnimatedVisual } from './BrowserAnimatedVisual'
import { EditorAnimatedVisual } from './EditorAnimatedVisual'
import { ReviewPRViewAnimatedVisual } from './ReviewPRViewAnimatedVisual'
import { WorkbenchAnimatedVisual } from './WorkbenchAnimatedVisual'
const originalUserAgent = navigator.userAgent
function setUserAgent(userAgent: string): void {
Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value: userAgent
})
}
describe('feature wall shortcut labels', () => {
afterEach(() => {
setUserAgent(originalUserAgent)
})
it('renders Windows shortcut copy in workbench and browser visuals', () => {
setUserAgent('Windows NT 10.0')
const html = [
renderToStaticMarkup(<BrowserAnimatedVisual reducedMotion />),
renderToStaticMarkup(<WorkbenchAnimatedVisual reducedMotion />),
renderToStaticMarkup(<EditorAnimatedVisual reducedMotion />),
renderToStaticMarkup(<ReviewPRViewAnimatedVisual reducedMotion />)
].join('\n')
expect(html).toContain('Ctrl+Shift+B')
expect(html).toContain('Ctrl+Shift+D')
expect(html).toContain('Alt+Shift+D')
expect(html).toContain('Ctrl+B')
expect(html).toContain('Ctrl+I')
expect(html).toContain('Checks')
expect(html).not.toContain('⌘')
})
})
@@ -1,46 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { AGENT_CATALOG } from '@/lib/agent-catalog'
import { AgentStep } from './AgentStep'
import { TooltipProvider } from '@/components/ui/tooltip'
describe('AgentStep', () => {
it('shows the collapsed fallback agents summary', () => {
const html = renderToStaticMarkup(
<TooltipProvider>
<AgentStep
selectedAgent={null}
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
yoloPermissions
onYoloPermissionsChange={vi.fn()}
/>
</TooltipProvider>
)
expect(html).toContain(`Show ${AGENT_CATALOG.length - 1} more agents →`)
expect(html).toContain('data-agent-grid-scroll')
expect(html).toContain('data-slot="switch"')
expect(html).toContain('Yolo mode')
expect(html).not.toContain('role="radiogroup"')
})
it('labels the fallback agents summary as hide when expanded', () => {
const html = renderToStaticMarkup(
<TooltipProvider>
<AgentStep
selectedAgent={AGENT_CATALOG[1].id}
onSelect={vi.fn()}
detectedSet={new Set([AGENT_CATALOG[0].id])}
isDetecting={false}
yoloPermissions
onYoloPermissionsChange={vi.fn()}
/>
</TooltipProvider>
)
expect(html).toContain('Hide agents')
expect(html).not.toContain(`Show ${AGENT_CATALOG.length - 1} more agents →`)
})
})
@@ -1,65 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import { WINDOWS_GIT_BASH_SHELL } from '../../../../shared/windows-terminal-shell'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import { WindowsTerminalStep } from './WindowsTerminalStep'
function createSettings(overrides: Partial<GlobalSettings> = {}): GlobalSettings {
return {
terminalWindowsShell: 'powershell.exe',
terminalWindowsWslDistro: null,
terminalRightClickToPaste: true,
...overrides
} as GlobalSettings
}
describe('WindowsTerminalStep', () => {
it('renders default shell and right-click behavior choices', () => {
const html = renderToStaticMarkup(
<WindowsTerminalStep settings={createSettings()} updateSettings={vi.fn()} />
)
expect(html).toContain('Default Shell')
expect(html).toContain('PowerShell')
expect(html).toContain('Command Prompt')
expect(html).toContain('Right-click behavior')
expect(html).toContain('Paste on right-click')
expect(html).toContain('Open context menu')
expect(html).toContain('role="radiogroup"')
expect(html).toContain('aria-checked="true"')
expect(html).toContain('aria-pressed="true"')
expect(html).toContain('fill="#2E74B5"')
expect(html).not.toContain('lucide-terminal')
})
it('keeps the WSL distro control visible when WSL is already selected', () => {
const html = renderToStaticMarkup(
<WindowsTerminalStep
settings={createSettings({
terminalWindowsShell: 'wsl.exe',
terminalWindowsWslDistro: 'Debian'
})}
updateSettings={vi.fn()}
/>
)
expect(html).toContain('WSL')
expect(html).toContain('WSL Distribution')
})
it('renders Git Bash with the Git Bash mark instead of a text badge', () => {
const html = renderToStaticMarkup(
<WindowsTerminalStep
settings={createSettings({
terminalWindowsShell: WINDOWS_GIT_BASH_SHELL
})}
updateSettings={vi.fn()}
/>
)
expect(html).toContain('Git Bash')
expect(html).toContain('gwindows_logo.svg')
expect(html).not.toContain('&gt;Git&lt;')
expect(html).not.toContain('>Git<')
})
})
@@ -80,7 +80,7 @@ describe('ModifierDoubleTapDetector', () => {
const d = new ModifierDoubleTapDetector()
d.process(down('Shift'), 0)
// A missed keyUp makes a fresh keyDown restart the first tap.
d.process(down('Shift'), 50)
expect(d.process(down('Shift'), 50)).toBeNull()
d.process(up('Shift'), 60)
// The next press within the window still completes the gesture.
expect(d.process(down('Shift'), 200)).toEqual({ modifier: 'Shift' })
+8 -7
View File
@@ -37,9 +37,8 @@ const requested = (promptId = PROMPT_ID): string =>
const settled = (promptId = PROMPT_ID): string =>
logLine({ kind: 'user_input_prompt_settled', prompt_id: promptId, outcome: 'answered' })
function localShard(sessionId: string): string[] {
const hex = sessionId.replace(/-/g, '').slice(0, 12)
const date = new Date(Number.parseInt(hex, 16))
function sessionShard(): string[] {
const date = new Date('2026-09-22T19:41:18.839Z')
return [
String(date.getFullYear()),
String(date.getMonth() + 1).padStart(2, '0'),
@@ -53,7 +52,7 @@ describe('muse session log', () => {
beforeEach(() => {
sessionsDir = mkdtempSync(join(tmpdir(), 'muse-session-log-'))
const dir = join(sessionsDir, ...localShard(SESSION_ID), SESSION_ID)
const dir = join(sessionsDir, ...sessionShard(), SESSION_ID)
mkdirSync(dir, { recursive: true })
logPath = join(dir, 'session.jsonl')
})
@@ -69,9 +68,11 @@ describe('muse session log', () => {
it('returns undefined for a missing log or a non-v7 session id', () => {
expect(findMuseSessionLogPath(SESSION_ID, sessionsDir)).toBeUndefined()
expect(
findMuseSessionLogPath('1471f5e6-bd60-41a2-bfcf-c9086dbd4092', sessionsDir)
).toBeUndefined()
const nonV7 = '01a0caa3-0e77-4d41-bad7-46283a45633d'
const dir = join(sessionsDir, ...sessionShard(), nonV7)
mkdirSync(dir, { recursive: true })
writeFileSync(join(dir, 'session.jsonl'), '')
expect(findMuseSessionLogPath(nonV7, sessionsDir)).toBeUndefined()
})
it('reads prompts batched inside a retained_frame', () => {
@@ -1,5 +1,4 @@
import { expect, it, vi } from 'vitest'
import { normalizePromptField } from './agent-status-field-normalization'
import { selectStructuredAgentTurnActivity } from './native-chat-turn-activity'
it('does not trim every preceding activity line to select the final activity', () => {
@@ -17,59 +16,26 @@ it('does not trim every preceding activity line to select the final activity', (
})
it('preserves the last nonempty LF-delimited line before prompt normalization', () => {
const texts = [
'',
'\n',
'\n\n',
' \r\n\t',
'first\rsecond',
'\nfirst\r\nsecond\r\n',
'first\n\u00a0\u2003\n',
'first\n\u2028second\u2029',
'a\n😀',
'a\n\ud800',
'a\n\udc00',
`a\n${'x'.repeat(199)}😀`,
`first\n${' '.repeat(3000)}last`,
'first\n\0\n',
'first\n\ufeff\n'
const cases: [string, string | null][] = [
['', null],
['\n', null],
['\n\n', null],
[' \r\n\t', null],
['first\rsecond', 'first second'],
['\nfirst\r\nsecond\r\n', 'second'],
['first\n\u00a0\u2003\n', 'first'],
['first\n\u2028second\u2029', 'second'],
['a\n😀', '😀'],
['a\n\ud800', '\ud800'],
['a\n\udc00', '\udc00'],
[`a\n${'x'.repeat(199)}😀`, 'x'.repeat(199)],
[`first\n${' '.repeat(3000)}last`, 'last'],
['first\n\0\n', '\0'],
['first\n\ufeff\n', 'first']
]
const alphabet = [
'a',
' ',
'\n',
'\r',
'\t',
'\u00a0',
'\u2003',
'\u2028',
'\ufeff',
'😀',
'\ud800',
'\udc00'
]
let seed = 57
const next = () => {
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0
return seed
}
for (let index = 0; index < 1000; index++) {
let text = ''
const length = next() % 300
for (let offset = 0; offset < length; offset++) {
text += alphabet[next() % alphabet.length]
}
texts.push(text)
}
for (const text of texts) {
const line = text
.split('\n')
.map((part) => part.trim())
.findLast((part) => part.length > 0)
const normalized = line ? normalizePromptField(line) : ''
const expected = normalized ? { kind: 'description', text: normalized } : null
for (const [text, expected] of cases) {
expect(selectStructuredAgentTurnActivity([], 'turn', { turnId: 'turn', text })).toEqual(
expected
expected ? { kind: 'description', text: expected } : null
)
}
})
+7 -43
View File
@@ -8,31 +8,6 @@ import {
} from './native-chat-agent-profiles'
describe('native chat agent picker profiles', () => {
// Composer trigger stays /; skillPrefix changes only picked-skill insertion.
it('keeps Codex skills invocable as dollar tokens', () => {
expect(getNativeChatAgentProfile('codex')).toMatchObject({
skillPrefix: '$',
skillSourceOwner: 'codex'
})
})
it('writes Claude-family and Grok skills as slash tokens', () => {
expect(getNativeChatAgentProfile('claude')).toMatchObject({
skillPrefix: '/',
skillSourceOwner: 'claude'
})
expect(getNativeChatAgentProfile('openclaude')).toMatchObject({ skillSourceOwner: 'claude' })
expect(getNativeChatAgentProfile('grok')).toMatchObject({
skillPrefix: '/',
skillSourceOwner: 'grok'
})
})
// OpenCode 2.x reads the same skill folders as 1.x.
it("lists OpenCode's skills for OpenCode 2", () => {
expect(getNativeChatAgentProfile('opencode2')).toMatchObject({ skillSourceOwner: 'opencode' })
})
it('does not grant custom or unverified agents a skill grammar', () => {
expect(getNativeChatAgentProfile('custom-agent')).toBeNull()
})
@@ -72,26 +47,15 @@ describe('native chat command replies', () => {
expect(getNativeChatCommandReply('omp', 'compact')).toBeNull()
})
it('declares only /context, the one reply the desktop chat implements', () => {
for (const agent of ['claude', 'openclaude', 'codex', 'omp', 'grok', 'custom-agent']) {
const declared = getVerifiedNativeChatCommands(agent).filter(({ reply }) => reply)
expect(declared.map(({ name }) => name)).toEqual(
agent === 'omp' || agent === 'openclaude' ? ['context'] : []
)
}
})
it('leaves out of the agent-answered catalog only the commands that declare a reply', () => {
const names = (agent: string) =>
getAgentAnsweredNativeChatCommands(agent).map(({ name }) => name)
expect(names('openclaude')).toEqual(names('claude'))
expect(names('omp')).toEqual(
getVerifiedNativeChatCommands('omp')
.map(({ name }) => name)
.filter((name) => name !== 'context')
)
expect(getAgentAnsweredNativeChatCommands('codex')).toEqual(
getVerifiedNativeChatCommands('codex')
)
expect(names('openclaude')).toContain('compact')
expect(names('openclaude')).not.toContain('context')
expect(names('omp')).toContain('model')
expect(names('omp')).toContain('usage')
expect(names('omp')).not.toContain('context')
expect(names('codex')).toContain('permissions')
expect(names('grok')).toEqual([])
})
})
+1 -141
View File
@@ -1,158 +1,18 @@
import { expect, it } from 'vitest'
import { extractPendingAsk } from './native-chat-ask'
import { isInterruptedStatusMessage, type NativeChatMessage } from './native-chat-types'
import type { AskPrompt } from './native-chat-ask-types'
/** Verbatim pre-cursor shift() FIFO oracle; canonical question input permits inline parsing. */
function referenceExtractPendingAsk(messages: readonly NativeChatMessage[]): AskPrompt | null {
let pending: AskPrompt | null = null
const outstanding: (AskPrompt | null)[] = []
for (const message of messages) {
if (message.role === 'user' || isInterruptedStatusMessage(message)) {
outstanding.length = 0
pending = null
}
for (const block of message.blocks) {
if (block.type === 'tool-call') {
const input = block.input as { questions?: unknown[] } | undefined
const parsed =
Array.isArray(input?.questions) && input.questions.length > 0
? ({ questions: input.questions } as unknown as AskPrompt)
: null
if (parsed) {
pending = parsed
}
outstanding.push(parsed)
} else if (block.type === 'tool-result' && outstanding.length > 0) {
const resolved = outstanding.shift()
if (resolved && resolved === pending) {
pending = null
}
}
}
}
return pending
}
import type { NativeChatMessage } from './native-chat-types'
type Block = NativeChatMessage['blocks'][number]
function questionTexts(prompt: AskPrompt | null): string[] | null {
return prompt ? (prompt.questions as { question?: string }[]).map((q) => q.question ?? '') : null
}
function ask(text: string): Block {
return { type: 'tool-call', name: 'AskUserQuestion', input: { questions: [{ question: text }] } }
}
function plainCall(): Block {
return { type: 'tool-call', name: 'Bash', input: { command: 'ls' } }
}
function toolResult(): Block {
return { type: 'tool-result', output: 'ok' }
}
function assistant(id: string, blocks: Block[]): NativeChatMessage {
return { id, role: 'assistant', timestamp: 1, source: 'transcript', blocks }
}
function userTurn(id: string): NativeChatMessage {
return { id, role: 'user', timestamp: 1, source: 'transcript', blocks: [] }
}
function interrupted(id: string): NativeChatMessage {
return {
id,
role: 'system',
timestamp: 1,
source: 'transcript',
blocks: [{ type: 'text', text: 'Interrupted by user' }]
} as NativeChatMessage
}
function makeRandom(seed: number): () => number {
let state = seed >>> 0
return () => {
state = (state * 1664525 + 1013904223) >>> 0
return state / 0x100000000
}
}
function randomTranscript(random: () => number, messageCount: number): NativeChatMessage[] {
const messages: NativeChatMessage[] = []
let askSeq = 0
for (let m = 0; m < messageCount; m += 1) {
const roll = random()
if (roll < 0.12) {
messages.push(userTurn(`u${m}`))
continue
}
if (roll < 0.2) {
messages.push(interrupted(`i${m}`))
continue
}
const blocks: Block[] = []
const blockCount = 1 + Math.floor(random() * 6)
for (let b = 0; b < blockCount; b += 1) {
const kind = random()
if (kind < 0.3) {
blocks.push(ask(`q${(askSeq += 1)}`))
} else if (kind < 0.6) {
blocks.push(plainCall())
} else if (kind < 0.9) {
blocks.push(toolResult())
} else {
blocks.push({ type: 'text', text: 'chatter' })
}
}
messages.push(assistant(`a${m}`, blocks))
}
return messages
}
it('matches the shift-based FIFO on randomized add/resolve/cancel sequences', () => {
let pendingCases = 0
let clearedCases = 0
for (let seed = 1; seed <= 4000; seed += 1) {
const random = makeRandom(seed)
const messages = randomTranscript(random, 1 + Math.floor(random() * 12))
const actual = extractPendingAsk(messages)
// Compare by content: the oracle keeps raw question objects, production re-parses them.
expect(questionTexts(actual), `seed ${seed}`).toEqual(
questionTexts(referenceExtractPendingAsk(messages))
)
if (actual) {
pendingCases += 1
} else {
clearedCases += 1
}
}
// Both verdicts must be well represented or the sweep proves nothing.
expect(pendingCases).toBeGreaterThan(500)
expect(clearedCases).toBeGreaterThan(500)
})
it('returns the same ask when a reconnect replays the transcript incrementally', () => {
for (let seed = 1; seed <= 500; seed += 1) {
const random = makeRandom(seed)
const messages = randomTranscript(random, 1 + Math.floor(random() * 10))
// Reconnect extraction must match at every streamed prefix, avoiding dropped or duplicate asks.
for (let end = 1; end <= messages.length; end += 1) {
const prefix = messages.slice(0, end)
expect(questionTexts(extractPendingAsk(prefix)), `seed ${seed} prefix ${end}`).toEqual(
questionTexts(referenceExtractPendingAsk(prefix))
)
}
// Extraction is pure: replaying the identical transcript yields the identical verdict.
expect(extractPendingAsk(messages)).toEqual(extractPendingAsk(messages.slice()))
}
})
it('stays linear over a large call/result batch', () => {
const blocks: Block[] = [
...Array.from({ length: 100_000 }, (_, i) => ask(`q${i}`)),
...Array.from({ length: 100_000 }, () => toolResult())
]
const started = Date.now()
expect(extractPendingAsk([assistant('m', blocks)])).toBeNull()
// The removed shift() FIFO was quadratic here (~5e9 element moves) and took minutes.
expect(Date.now() - started).toBeLessThan(3000)
})
it('keeps the newest ask pending when an older duplicate-content ask resolves first', () => {
const pending = extractPendingAsk([assistant('m', [ask('same'), ask('same'), toolResult()])])
+1 -1
View File
@@ -80,7 +80,7 @@ describe('extractPendingAsk', () => {
expect(pending?.questions[0]?.question).toBe('Deploy?')
})
it("clears the ask when its own result arrives, keeping the newest ask's identity", () => {
it('keeps the newest ask when an older call receives its result', () => {
const first = { questions: [{ question: 'First?', options: [] }] }
const pending = extractPendingAsk([
message('m1', [
+1 -18
View File
@@ -1,20 +1,12 @@
import { describe, expect, it } from 'vitest'
import {
buildNativeChatImagePasteBytes,
buildNativeChatPasteBytes,
isMultilineDraft,
NATIVE_CHAT_SUBMIT
buildNativeChatPasteBytes
} from './native-chat-paste-bytes'
const BEGIN = '\x1b[200~'
const END = '\x1b[201~'
describe('NATIVE_CHAT_SUBMIT', () => {
it('is a bare carriage return so the Enter write is unambiguous', () => {
expect(NATIVE_CHAT_SUBMIT).toBe('\r')
})
})
describe('buildNativeChatPasteBytes', () => {
it('single-line text has no trailing submit (Enter is written separately)', () => {
expect(buildNativeChatPasteBytes('hello world')).toBe('hello world')
@@ -52,12 +44,3 @@ describe('buildNativeChatImagePasteBytes', () => {
)
})
})
describe('isMultilineDraft', () => {
it('is false for single-line', () => {
expect(isMultilineDraft('one line')).toBe(false)
})
it('is true when a newline is present', () => {
expect(isMultilineDraft('a\nb')).toBe(true)
})
})
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
nativeChatReasoningDisclosureKey,
nativeChatReasoningHeadline,
nativeChatReasoningHeadlineText,
selectNativeChatLiveReasoning
@@ -67,10 +66,6 @@ describe('the open block the live line discloses', () => {
expect(select([prompt, child])).toBeNull()
expect(select([prompt, open, child])).toBe(open)
})
it('keys one block the same for the line and the row, apart from tool runs', () => {
expect(nativeChatReasoningDisclosureKey('r-1')).toBe('reasoning:r-1')
})
})
describe('the reasoning headline', () => {
@@ -122,13 +122,17 @@ describe('recordNativeChatSessionOptionCommand', () => {
})
it('clears tracked truth for a bare picker command and reports the agent picker', () => {
const claude = claudeRecord('sonnet')
claude.valuesByModel.sonnet = { effort: { value: 'low', source: 'dispatched' } }
const claudeResult = recordNativeChatSessionOptionCommand({
catalog: CLAUDE_SESSION_OPTION_CATALOG,
models: CLAUDE_SESSION_OPTION_CATALOG.models,
record: claudeRecord('sonnet'),
record: claude,
command: '/model'
})
expect(claudeResult.opensAgentPicker).toBe(true)
expect(claudeResult).toEqual({ changed: true, opensAgentPicker: true })
expect(claude.model).toBeUndefined()
expect(claude.valuesByModel.sonnet).toBeUndefined()
const codexRecord = createNativeChatSessionOptionRecord('codex')
codexRecord.model = { value: 'gpt-5.5', source: 'dispatched' }
const codexResult = recordNativeChatSessionOptionCommand({
@@ -356,6 +360,7 @@ describe('recordNativeChatSessionOptionCommand for grok', () => {
it('still resets tracked state when the typed model is a real switch', () => {
const record = grokRecord()
record.valuesByModel['grok-build'] = { effort: { value: 'high', source: 'reported' } }
recordGrok(record, '/effort low')
recordNativeChatSessionOptionCommand({
catalog: GROK_SESSION_OPTION_CATALOG,
@@ -367,6 +372,7 @@ describe('recordNativeChatSessionOptionCommand for grok', () => {
command: '/model grok-build'
})
expect(record.model).toEqual({ value: 'grok-build', source: 'dispatched' })
expect(record.valuesByModel['grok-build']).toBeUndefined()
expect(record.valuesByModel['grok-4.7']?.effort).toEqual({
value: 'low',
source: 'dispatched'
@@ -238,7 +238,7 @@ describe('buildNativeChatSessionOptionSnapshot', () => {
})
})
it('routes Codex model changes through its typed TUI picker', () => {
it('describes Codex model changes as agent-picker actions', () => {
const snapshot = buildNativeChatSessionOptionSnapshot({
catalog: CODEX_SESSION_OPTION_CATALOG,
models: CODEX_SESSION_OPTION_CATALOG.models,
@@ -368,7 +368,7 @@ describe('defaults on load', () => {
})
const effort = snapshot.find((descriptor) => descriptor.id === 'effort')
expect(effort).toMatchObject({ valueSource: 'default' })
expect(effort?.kind.type === 'select' ? effort.kind.currentValue : null).toBeDefined()
expect(effort?.kind.type === 'select' ? effort.kind.currentValue : null).toBe('high')
})
})
@@ -427,7 +427,7 @@ describe('a boolean option always carries a value to render', () => {
expect(effort).toMatchObject({ valueSource: 'unknown' })
})
it('does not let the resolved display value reach the composed --model argument', () => {
it('includes composed launch defaults only when requested', () => {
// Composed model uses caller picks, never display descriptors; resolving a switch cannot alter dispatch.
expect(resolveAgentSessionOptionLaunch('cursor', { model: 'claude-opus-4-8' })).toEqual({
args: ['--model', 'claude-opus-4-8-thinking-high'],
@@ -39,12 +39,6 @@ describe('applyNativeChatReportedSessionOptions', () => {
expect(applyNativeChatReportedSessionOptions(record, { model: 'sonnet' })).toBe(false)
expect(record.valuesByModel.sonnet?.effort).toEqual({ value: 'high', source: 'dispatched' })
})
it('is a no-op when the report matches tracked state', () => {
const record = claudeRecord()
record.model = { value: 'sonnet', source: 'reported' }
expect(applyNativeChatReportedSessionOptions(record, { model: 'sonnet' })).toBe(false)
})
})
describe('matchNativeChatCatalogModelId', () => {
@@ -1,8 +1,4 @@
import { describe, expect, it } from 'vitest'
import {
getNativeChatAgentProfile,
getVerifiedNativeChatCommands
} from './native-chat-agent-profiles'
import {
applySlashSuggestion,
classifyNativeChatSend,
@@ -13,19 +9,6 @@ import {
} from './native-chat-slash-commands'
describe('OMP terminal command catalog', () => {
it('offers the verified model, planning and context commands without generic help', () => {
const commands = getAgentSlashCommands('omp')
const names = commands.map((command) => command.name)
expect(names).toEqual(
expect.arrayContaining(['model', 'switch', 'plan', 'compact', 'context', 'usage'])
)
expect(names).not.toContain('help')
expect(getVerifiedNativeChatCommands('omp')).toEqual(commands)
expect(getNativeChatAgentProfile('omp')).toBeNull()
expect(new Set(names).size).toBe(names.length)
expect(getAgentSlashCommands('pi').map((command) => command.name)).toEqual(['clear', 'help'])
})
it('completes arguments separately from picker command dispatch', () => {
const matches = filterSlashCommands(getAgentSlashCommands('omp'), 'MOD')
expect(matches).toHaveLength(1)
@@ -10,27 +10,6 @@ import {
} from './native-chat-slash-commands'
describe('getAgentSlashCommands', () => {
it('returns Codex-specific commands (e.g. /model, /resume) for codex', () => {
const names = getAgentSlashCommands('codex').map((c) => c.name)
expect(names).toContain('model')
expect(names).toContain('resume')
expect(names).toContain('diff')
})
it('returns Claude commands for claude (no Codex-only /model)', () => {
const names = getAgentSlashCommands('claude').map((c) => c.name)
expect(names).toContain('clear')
expect(names).toContain('compact')
expect(names).not.toContain('model')
// Claude's terminal writes no /context reply the chat could show.
expect(names).not.toContain('context')
})
it('offers OpenClaude /context, whose report lands in its transcript', () => {
const names = getAgentSlashCommands('openclaude').map((c) => c.name)
expect(names).toEqual([...getAgentSlashCommands('claude').map((c) => c.name), 'context'])
})
it('falls back to a small common set for an unknown agent (never empty)', () => {
const names = getAgentSlashCommands('some-other-agent').map((c) => c.name)
expect(names).toEqual(['clear', 'help'])
@@ -39,7 +39,7 @@ describe('nativeChatSubagentGroupHeader', () => {
expect(header).toMatchObject({ alertState: 'failed', alert: '1 failed' })
})
it('settles to the worst verdict, the run length and the tokens', () => {
it('summarizes completed siblings with the run length and tokens', () => {
const header = nativeChatSubagentGroupHeader(
[
child('a', 'completed', { startedAt: 0, settledAt: 60_000, tokens: 9_000 }),
+1 -1
View File
@@ -59,7 +59,7 @@ describe('normalizeNativeChatTaskList', () => {
).toEqual(list(task('One'), task('Two')))
})
it.each([undefined, null, 42, [], '{', '{}', { todos: null }, { todos: [{}] }])(
it.each([[undefined], [null], [42], [[]], ['{'], ['{}'], [{ todos: null }], [{ todos: [{}] }]])(
'returns null for malformed input %j',
(input) => {
expect(normalizeNativeChatTaskList('TodoWrite', input)).toBeNull()
@@ -31,43 +31,6 @@ describe('selectStructuredAgentTurnActivity', () => {
expect(activity).toEqual({ kind: 'description', text: 'Preparing the answer' })
})
it.each([
[
'a provider retry warning',
{
kind: 'status',
tone: 'warning',
text: 'Claude hit a temporary problem and is retrying.'
}
],
[
'a compaction marker',
{ kind: 'status', text: 'Context compacted', presentation: 'compaction' }
],
['a cancellation note', { kind: 'status', text: 'Cancellation requested.' }],
[
'a suppressed-notification summary',
{ kind: 'status', text: '3 more provider notifications not shown for this turn' }
]
] satisfies [string, AgentJournalItemBody][])(
'never turns %s from earlier in the turn into the live line',
(_name, body) => {
const later = item(3, {
kind: 'tool-call',
name: 'shell',
input: { command: 'pnpm test' },
state: 'running'
})
expect(
selectStructuredAgentTurnActivity([turnStart, item(2, body), later], 'turn-1')
).toBeNull()
expect(
selectStructuredAgentTurnActivity([turnStart, later, item(4, body)], 'turn-1')
).toBeNull()
}
)
it("never puts the model's reasoning on the indicator line", () => {
const reasoning = item(2, {
kind: 'message',
@@ -86,24 +49,6 @@ describe('selectStructuredAgentTurnActivity', () => {
).toEqual({ kind: 'description', text: 'Running a command' })
})
it('skips reasoning behind a typed turn item too', () => {
const typedTurnStart = item(1, { kind: 'turn', turnId: 'turn-1', state: 'running' })
expect(
selectStructuredAgentTurnActivity(
[
typedTurnStart,
item(2, {
kind: 'message',
role: 'reasoning',
blocks: [{ type: 'text', text: 'Weighing two approaches' }]
})
],
'turn-1'
)
).toBeNull()
})
it('shows matching ephemeral provider activity, never an older journal status', () => {
const activity = selectStructuredAgentTurnActivity(
[turnStart, item(2, { kind: 'status', text: 'Older journal status' })],
@@ -147,67 +92,6 @@ describe('selectStructuredAgentTurnActivity', () => {
expect(activity).toBeNull()
})
it('does not read a journal status that repeats a recent tool label', () => {
const activity = selectStructuredAgentTurnActivity(
[
turnStart,
item(2, {
kind: 'tool-call',
name: 'shell',
input: { command: 'pnpm lint' },
state: 'completed'
}),
item(3, { kind: 'status', text: 'Running pnpm lint' })
],
'turn-1'
)
expect(activity).toBeNull()
})
it('ignores active and settled tools so the tail can use a broad fallback', () => {
const activity = selectStructuredAgentTurnActivity(
[
turnStart,
item(2, {
kind: 'tool-call',
name: 'shell',
input: { command: 'pnpm test' },
state: 'running'
}),
item(3, {
kind: 'tool-call',
name: 'shell',
input: { command: 'pnpm lint' },
state: 'completed'
})
],
'turn-1'
)
expect(activity).toBeNull()
})
it('ignores diagnostic provider frames and returns nothing after the turn settles', () => {
const diagnostic = item(2, {
kind: 'status',
text: 'codex · notification:new/event',
providerFrame: {
provider: 'codex',
kind: 'notification:new/event',
payload: {
head: '{}',
byteLength: 2,
digest: 'a'.repeat(64),
truncated: false
}
}
})
expect(selectStructuredAgentTurnActivity([turnStart, diagnostic], 'turn-1')).toBeNull()
expect(selectStructuredAgentTurnActivity([turnStart, diagnostic], null)).toBeNull()
})
})
describe('selectStructuredAgentTurnActivity — which agent it answers for', () => {
-9
View File
@@ -74,15 +74,6 @@ describe('nativeChatTurnFold', () => {
expect([...foldedRows].sort()).toEqual([1, 2, 3])
})
it("never folds the reader's own message, which anchors the turn", () => {
const { foldedRows } = nativeChatTurnFold({
rows: TURN,
settledTurnKeys: SETTLED,
expandedTurnKeys: NONE
})
expect(foldedRows.has(0)).toBe(false)
})
// A stored-only provider event draws nothing: a disclosure over it would open onto nothing.
it('offers no disclosure for a turn whose only other row draws nothing', () => {
const { foldedRows, foldableTurnKeys } = nativeChatTurnFold({
@@ -77,11 +77,6 @@ describe('nativeChatTurnBarRows', () => {
])
)
})
it('puts a turn opened by a rendered message under that message', () => {
const messages = [message('A', 'user'), message('t1')]
expect(nativeChatTurnBarRows(messages, ['A', 'A']).get('A')).toEqual({ index: 0, above: false })
})
})
describe('nativeChatTurnDrawOrder', () => {
@@ -103,15 +103,6 @@ describe('buildNativeChatVisualDocument', () => {
expect(new URL('#overview', 'about:srcdoc').href).toBe('about:srcdoc#overview')
expect(() => new URL('page-2.html', 'about:srcdoc')).toThrow()
})
it('checks the theme message comes from the host window', () => {
expect(document).toContain('event.source !== host')
})
it('answers a ping from the host with the ping id', () => {
expect(document).toContain(`"ping":"${NATIVE_CHAT_VISUAL_PING_TYPE}"`)
expect(document).toContain('send({ type: C.pong, id: data.id })')
})
})
describe('nativeChatVisualThemeCss', () => {
@@ -144,10 +144,6 @@ describe('a run of provider retry rows', () => {
])
})
it("draws an older host's single revised row as it is", () => {
expect(drawn([retry(4)])).toEqual(['Codex is retrying: Reconnecting... 4/5.'])
})
it('hands back the same list when nothing retried', () => {
const messages: NativeChatMessage[] = [
{ id: 'a', role: 'assistant', blocks: [], timestamp: 1, source: 'transcript' }
@@ -106,16 +106,6 @@ describe('deriveNativeChatStreamingText', () => {
).toBe('Partial answer that is now much longer than before')
})
it('keeps showing while the preview still leads (grows past the last turn)', () => {
// The transcript hasn't flushed the new content yet; preview is longer.
expect(
deriveNativeChatStreamingText({
messages: [assistant('Partial')],
previewText: 'Partial answer that is now much longer than before',
working: true
})
).toBe('Partial answer that is now much longer than before')
})
it('hides the sentence it streamed while a tool runs, but not a new reply after the tool', () => {
// A Claude transcript writes the sentence and the tool call it led into as separate rows.
const sentence = 'Checking the notes file now, then I will compare it with the settings.'
@@ -1,53 +1,13 @@
import { describe, expect, it } from 'vitest'
import {
isShellActivityToolCall,
NATIVE_CHAT_TOOL_ICON_NAMES,
nativeChatToolCategory,
nativeChatToolIconName,
nativeChatToolRunCategory,
nativeChatToolRunIconName,
type NativeChatToolCategory
nativeChatToolRunIconName
} from './native-chat-tool-icon'
const ALL_CATEGORIES: NativeChatToolCategory[] = [
'read',
'search',
'listFiles',
'unknown',
'fileChange',
'webSearch',
'browser',
'mcpToolCall',
'subAgentActivity',
'todoList',
'other'
]
describe('native chat tool icons', () => {
it('names a glyph for every category in the vocabulary', () => {
expect(NATIVE_CHAT_TOOL_ICON_NAMES).toEqual({
read: 'eye',
search: 'search',
listFiles: 'folder',
unknown: 'square-terminal',
fileChange: 'pencil',
webSearch: 'globe',
browser: 'globe',
mcpToolCall: 'plug',
subAgentActivity: 'bot',
todoList: 'list-checks',
other: 'wrench'
})
expect(Object.keys(NATIVE_CHAT_TOOL_ICON_NAMES).sort()).toEqual([...ALL_CATEGORIES].sort())
})
it('gives each category a distinct glyph, except the browser sharing the web globe', () => {
const glyphs = ALL_CATEGORIES.filter((category) => category !== 'browser').map(
(category) => NATIVE_CHAT_TOOL_ICON_NAMES[category]
)
expect(new Set(glyphs).size).toBe(glyphs.length)
})
it('maps the row words the Codex lane renders to their category', () => {
expect(nativeChatToolCategory('read')).toBe('read')
expect(nativeChatToolCategory('search')).toBe('search')
@@ -271,9 +231,6 @@ describe('native chat tool icons', () => {
})
describe('qualified tool identity icons', () => {
it.each(['mcp__linear__list_issues'])('uses the MCP glyph for %s', (name) =>
expect(nativeChatToolIconName(name)).toBe('plug')
)
it.each([
'setup.py',
'src/read',
@@ -1,36 +1,7 @@
import { describe, expect, it } from 'vitest'
import { pairToolBlocks, type NativeChatToolPair } from './native-chat-tool-pairs'
import { pairToolBlocks } from './native-chat-tool-pairs'
import type { NativeChatBlock } from '../../native-chat-types'
function original(blocks: readonly NativeChatBlock[], limit: number): NativeChatToolPair[] {
const pairs: NativeChatToolPair[] = []
const slots: (number | null)[] = []
let ordinal = 0
for (const block of blocks) {
if (block.type === 'tool-call') {
if (pairs.length < limit) {
slots.push(pairs.length)
pairs.push({ call: block })
} else {
slots.push(null)
}
} else if (block.type === 'tool-result') {
const slot = slots[ordinal]
if (slot === undefined) {
if (pairs.length < limit) {
pairs.push({ result: block })
}
} else {
ordinal++
if (slot !== null) {
pairs[slot].result = block
}
}
}
}
return pairs
}
const call: NativeChatBlock = { type: 'tool-call', name: 'read', input: {} }
const result: NativeChatBlock = { type: 'tool-result', output: 'done' }
@@ -49,23 +20,6 @@ describe('tool pair limits', () => {
expect(reads).toBe(0)
})
it('preserves FIFO and stray-result behavior across finite and unlimited limits', () => {
for (let seed = 0; seed < 128; seed++) {
const blocks = Array.from({ length: 30 }, (_, i) =>
(seed * 13 + i * 17) % 7 < 3
? call
: (seed + i) % 3
? result
: { type: 'text' as const, text: 'hi' }
)
for (const limit of [0, 1, 2, 5, 0.5, -1, Infinity, Number.NaN]) {
expect(pairToolBlocks(blocks, limit)).toEqual(original(blocks, limit))
}
}
expect(pairToolBlocks([call, call, result, result], 1)).toEqual(
original([call, call, result, result], 1)
)
})
it('still answers every retained call when the results trail far behind', () => {
// Retain scanning until every retained call is answered, or mobile would spin on completed tools.
const blocks = [call, call, ...Array.from({ length: 5000 }, () => result)]
@@ -73,7 +27,6 @@ describe('tool pair limits', () => {
{ call, result },
{ call, result }
])
expect(pairToolBlocks(blocks, 2)).toEqual(original(blocks, 2))
})
it('gives a result that names its call to that call, past one that finished with no output', () => {
@@ -115,7 +68,6 @@ describe('tool pair limits', () => {
it('keeps a leading stray result and then stops at the limit', () => {
const blocks = [result, call, result, call, result]
expect(pairToolBlocks(blocks, 1)).toEqual(original(blocks, 1))
expect(pairToolBlocks(blocks, 1)).toEqual([{ result }])
})
})
@@ -14,39 +14,6 @@ function result(callId?: string): NativeChatToolResultBlock {
return { type: 'tool-result', output: 'done', ...(callId !== undefined ? { callId } : {}) }
}
// The original scan is an independent oracle for named and positional matching.
function scanPairs(blocks: readonly NativeChatBlock[], limit = Infinity): NativeChatToolPair[] {
const pairs: NativeChatToolPair[] = []
const pending: number[] = []
for (const block of blocks) {
if (pairs.length >= limit && pending.length === 0) {
break
}
if (block.type === 'tool-call') {
if (pairs.length < limit) {
pending.push(pairs.length)
pairs.push({ call: block })
}
} else if (block.type === 'tool-result') {
const pendingIndex =
block.callId === undefined
? pending.length > 0
? 0
: -1
: pending.findIndex((slot) => pairs[slot]?.call?.callId === block.callId)
if (pendingIndex === -1) {
if (pairs.length < limit) {
pairs.push({ result: block })
}
} else {
const [slot] = pending.splice(pendingIndex, 1)
pairs[slot]!.result = block
}
}
}
return pairs
}
function expectSameReferences(
actual: readonly NativeChatToolPair[],
expected: readonly NativeChatToolPair[]
@@ -75,10 +42,7 @@ function countedCalls(count: number): {
return { calls, reads: () => reads }
}
function countQueueMovement(
blocks: readonly NativeChatBlock[],
pair = pairToolBlocks
): {
function countQueueMovement(blocks: readonly NativeChatBlock[]): {
pairs: NativeChatToolPair[]
movedSlots: number
} {
@@ -113,7 +77,7 @@ function countQueueMovement(
return originalShift.call(this)
}
try {
return { pairs: pair(blocks), movedSlots }
return { pairs: pairToolBlocks(blocks), movedSlots }
} finally {
Array.prototype.shift = originalShift
Array.prototype.splice = originalSplice
@@ -131,7 +95,6 @@ describe('tool pairing lookup budget', () => {
const results = Array.from({ length: count }, (_, index) => result(idOf(index)))
const blocks = [...calls, ...results]
const measured = countQueueMovement(blocks)
expect(countQueueMovement(blocks, scanPairs).movedSlots).toBe((count * (count - 1)) / 2)
expectSameReferences(
measured.pairs,
calls.map((pending, index) => ({ call: pending, result: results[index] }))
@@ -213,22 +176,6 @@ describe('tool pairing identity and ordering', () => {
expect(reads).toBeLessThanOrEqual(count * 3)
})
it('keeps duplicate and positional matching correct across drained and refilled queues', () => {
const blocks: NativeChatBlock[] = []
for (let epoch = 0; epoch < 512; epoch += 1) {
blocks.push(
call('same'),
call('other'),
result('other'),
call('same'),
result(),
result('same'),
result('missing')
)
}
expectSameReferences(pairToolBlocks(blocks), scanPairs(blocks))
})
it('matches repeated and empty IDs FIFO while positional results consume the oldest call', () => {
const calls = [call('same'), call(''), call('same'), call(), call('same')]
const results = [result('same'), result(), result('same'), result(), result('same')]
@@ -264,37 +211,4 @@ describe('tool pairing identity and ordering', () => {
]
)
})
it('matches the original semantics for mixed identities, interleavings, and all limits', () => {
const ids: readonly (string | undefined)[] = [
undefined,
'',
'same',
'same',
'__proto__',
'constructor',
'東京',
'i\u0307',
'💡'
]
const limits = [0, -1, 0.5, 1, 2, 5, 30, Infinity, Number.NaN]
for (let seed = 0; seed < 256; seed += 1) {
let random = seed + 1
const next = (): number => {
random = (Math.imul(random, 1664525) + 1013904223) >>> 0
return random
}
const blocks: NativeChatBlock[] = Array.from({ length: 48 }, () => {
const kind = next() % 5
const id = ids[next() % ids.length]
return Object.freeze(
kind < 2 ? call(id) : kind < 4 ? result(id) : { type: 'text', text: 'context' }
)
})
Object.freeze(blocks)
for (const limit of limits) {
expectSameReferences(pairToolBlocks(blocks, limit), scanPairs(blocks, limit))
}
}
})
})