From 5de74e632be845ebe21da35a7874d92809119a9a Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Sun, 25 Oct 2026 18:28:44 -0700 Subject: [PATCH] test: remove low-value suites and copied test oracles (#33708) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/flaky-test-pattern-baseline.txt | 1 - .../session-scanner-parse-cache.test.ts | 9 +- ...-scanner-preview-window-truncation.test.ts | 10 +- .../session-scanner-service-client.test.ts | 8 +- .../session-scanner-service-env.test.ts | 8 - ...ssion-scanner-service-search-roots.test.ts | 13 -- .../ai-vault/session-scope-real-paths.test.ts | 8 - .../ai-vault/session-sidecar-stat.test.ts | 35 ++-- ...ion-transcript-every-agent-capture.test.ts | 11 -- ...session-transcript-message-content.test.ts | 170 +++++------------- .../browser-annotation-output.test.ts | 21 +-- .../browser-page-annotation-tray.test.tsx | 2 +- .../guest-annotation-viewport-bridge.test.ts | 4 +- .../annotate/markup-drawing-model.test.ts | 4 +- .../markup-screenshot-compose.test.ts | 11 +- .../annotate/markup-shape-render.test.ts | 27 +-- .../BrowserAddressBar.test.tsx | 4 +- .../assemble-chrome/BrowserFind.test.tsx | 10 +- .../browser-address-bar-expansion.test.ts | 33 ---- .../browser-address-bar-suggestions.test.ts | 6 +- .../browser-chrome-folded-tools.test.tsx | 16 +- .../browser-detected-browsers-summary.test.ts | 52 ------ .../browser-import-hint-visibility.test.ts | 31 ---- .../browser-navigation-control-row.test.tsx | 99 +--------- .../browser-page-zoom-indicator.test.tsx | 34 ---- .../browser-pane-page-selection.test.ts | 40 ----- ...er-workspace-pane.retention-props.test.tsx | 35 +++- .../MarkdownTableOfContentsPanel.test.tsx | 63 ------- .../editor/RichMarkdownSlashMenu.test.tsx | 22 --- .../CmdJPaletteFeatureTipVisual.test.tsx | 150 ---------------- .../CmdJPaletteTipDialog.test.tsx | 89 --------- .../SessionSearchTipDialog.test.tsx | 91 ---------- .../VoiceDictationFeatureTipVisual.test.tsx | 88 --------- .../VoiceDictationTipDialog.test.tsx | 134 -------------- .../feature-wall-shortcut-labels.test.tsx | 40 ----- .../components/onboarding/AgentStep.test.tsx | 46 ----- .../onboarding/WindowsTerminalStep.test.tsx | 65 ------- .../modifier-double-tap-detector.test.ts | 2 +- src/shared/muse-session-log.test.ts | 15 +- .../native-chat-activity-tail-line.test.ts | 70 ++------ src/shared/native-chat-agent-profiles.test.ts | 50 +----- src/shared/native-chat-ask-fifo.test.ts | 142 +-------------- src/shared/native-chat-ask.test.ts | 2 +- src/shared/native-chat-paste-bytes.test.ts | 19 +- src/shared/native-chat-reasoning-row.spec.ts | 5 - ...ative-chat-session-option-commands.test.ts | 10 +- ...ative-chat-session-option-snapshot.test.ts | 6 +- .../native-chat-session-option-state.test.ts | 6 - .../native-chat-slash-commands.omp.test.ts | 17 -- src/shared/native-chat-slash-commands.test.ts | 21 --- .../native-chat-subagent-group-header.spec.ts | 2 +- src/shared/native-chat-task-list.test.ts | 2 +- src/shared/native-chat-turn-activity.test.ts | 116 ------------ src/shared/native-chat-turn-fold.test.ts | 9 - src/shared/native-chat-turn-grouping.spec.ts | 5 - src/shared/native-chat-visual-shell.test.ts | 9 - .../native-chat-provider-retry-runs.test.ts | 4 - .../native-chat/native-chat-streaming.spec.ts | 10 -- .../tools/native-chat-tool-icon.test.ts | 45 +---- .../tools/native-chat-tool-pair-limit.spec.ts | 50 +----- ...tive-chat-tool-pairing.performance.test.ts | 90 +--------- 61 files changed, 207 insertions(+), 1990 deletions(-) delete mode 100644 src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts delete mode 100644 src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts delete mode 100644 src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts delete mode 100644 src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx delete mode 100644 src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts delete mode 100644 src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx delete mode 100644 src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx delete mode 100644 src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx delete mode 100644 src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx delete mode 100644 src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx delete mode 100644 src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx delete mode 100644 src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx delete mode 100644 src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx delete mode 100644 src/renderer/src/components/onboarding/AgentStep.test.tsx delete mode 100644 src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index d3038d635c3..bd044baadb6 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -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 diff --git a/src/main/ai-vault/session-scanner-parse-cache.test.ts b/src/main/ai-vault/session-scanner-parse-cache.test.ts index e3ab9ca9115..fd39f94c0ca 100644 --- a/src/main/ai-vault/session-scanner-parse-cache.test.ts +++ b/src/main/ai-vault/session-scanner-parse-cache.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 } diff --git a/src/main/ai-vault/session-scanner-preview-window-truncation.test.ts b/src/main/ai-vault/session-scanner-preview-window-truncation.test.ts index 446965e7fc1..0155855a03f 100644 --- a/src/main/ai-vault/session-scanner-preview-window-truncation.test.ts +++ b/src/main/ai-vault/session-scanner-preview-window-truncation.test.ts @@ -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) }) }) diff --git a/src/main/ai-vault/session-scanner-service-client.test.ts b/src/main/ai-vault/session-scanner-service-client.test.ts index 3b1c70d3118..817033fc0ca 100644 --- a/src/main/ai-vault/session-scanner-service-client.test.ts +++ b/src/main/ai-vault/session-scanner-service-client.test.ts @@ -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 } } }) } diff --git a/src/main/ai-vault/session-scanner-service-env.test.ts b/src/main/ai-vault/session-scanner-service-env.test.ts index 68f8af69e4f..6e1705b0570 100644 --- a/src/main/ai-vault/session-scanner-service-env.test.ts +++ b/src/main/ai-vault/session-scanner-service-env.test.ts @@ -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', () => { diff --git a/src/main/ai-vault/session-scanner-service-search-roots.test.ts b/src/main/ai-vault/session-scanner-service-search-roots.test.ts index ddcfe6a7df3..d25e6415f9d 100644 --- a/src/main/ai-vault/session-scanner-service-search-roots.test.ts +++ b/src/main/ai-vault/session-scanner-service-search-roots.test.ts @@ -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() -}) diff --git a/src/main/ai-vault/session-scope-real-paths.test.ts b/src/main/ai-vault/session-scope-real-paths.test.ts index ea91967b1e8..b8b3617455b 100644 --- a/src/main/ai-vault/session-scope-real-paths.test.ts +++ b/src/main/ai-vault/session-scope-real-paths.test.ts @@ -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) - }) }) diff --git a/src/main/ai-vault/session-sidecar-stat.test.ts b/src/main/ai-vault/session-sidecar-stat.test.ts index e8d384d6f6b..e93ca2348e4 100644 --- a/src/main/ai-vault/session-sidecar-stat.test.ts +++ b/src/main/ai-vault/session-sidecar-stat.test.ts @@ -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> = { "'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) -}) diff --git a/src/main/ai-vault/session-transcript-every-agent-capture.test.ts b/src/main/ai-vault/session-transcript-every-agent-capture.test.ts index 7282e700458..f38396dd290 100644 --- a/src/main/ai-vault/session-transcript-every-agent-capture.test.ts +++ b/src/main/ai-vault/session-transcript-every-agent-capture.test.ts @@ -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']) - ) -}) diff --git a/src/main/ai-vault/session-transcript-message-content.test.ts b/src/main/ai-vault/session-transcript-message-content.test.ts index 5611d9e15d2..03de553bea4 100644 --- a/src/main/ai-vault/session-transcript-message-content.test.ts +++ b/src/main/ai-vault/session-transcript-message-content.test.ts @@ -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 } - ]) - } }) diff --git a/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts b/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts index c6826478399..a6a1a507227 100644 --- a/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts +++ b/src/renderer/src/components/browser-pane/annotate/browser-annotation-output.test.ts @@ -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:** ') - 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') diff --git a/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx b/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx index ea127a25524..13d16337a5a 100644 --- a/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx +++ b/src/renderer/src/components/browser-pane/annotate/browser-page-annotation-tray.test.tsx @@ -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' })) diff --git a/src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts b/src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts index 144fabd91fe..7e0c97d4746 100644 --- a/src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts +++ b/src/renderer/src/components/browser-pane/annotate/guest-annotation-viewport-bridge.test.ts @@ -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(() => diff --git a/src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts b/src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts index bc26f9ee357..fe6e21b4722 100644 --- a/src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts +++ b/src/renderer/src/components/browser-pane/annotate/markup-drawing-model.test.ts @@ -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', diff --git a/src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts b/src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts index 39658bfd3f9..0f4c5869bbf 100644 --- a/src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts +++ b/src/renderer/src/components/browser-pane/annotate/markup-screenshot-compose.test.ts @@ -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) - }) -}) diff --git a/src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts b/src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts index e9ec61292e9..48026f637d8 100644 --- a/src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts +++ b/src/renderer/src/components/browser-pane/annotate/markup-shape-render.test.ts @@ -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: [] } + ]) }) }) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx index e94cf5766b3..9ab73e6877d 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.test.tsx @@ -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() diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx index c536b763df6..0f188046377 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/BrowserFind.test.tsx @@ -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', () => { diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts b/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts deleted file mode 100644 index 8d339bb75ae..00000000000 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-expansion.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts b/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts index fa51a6089ef..eb86b1d85ce 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-address-bar-suggestions.test.ts @@ -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', () => { diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx index 0247f46509b..13bed84cd99 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-chrome-folded-tools.test.tsx @@ -89,19 +89,17 @@ describe('BrowserChromeFoldedMenuItems', () => { const { container } = render( ) - // 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']) }) }) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts b/src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts deleted file mode 100644 index e3345d57558..00000000000 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-detected-browsers-summary.test.ts +++ /dev/null @@ -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.') - }) -}) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts b/src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts deleted file mode 100644 index 0425a4be253..00000000000 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-import-hint-visibility.test.ts +++ /dev/null @@ -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) - }) -}) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx index 755fc29b463..9c7ded833bc 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-navigation-control-row.test.tsx @@ -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 - } -})) - -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 = {}): BrowserN navigate: vi.fn(), ...overrides } - function Host(): React.JSX.Element { - const inputRef = useRef(null) - return ( - - } - /> - ) - } - render() + render() return controls } -/** The read-only counterpart the document preview passes, standing in for any non-URL identity. */ -function renderWithIdentityChip(): void { - render( - docs/report.html} - /> - ) -} - 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('Back').disabled).toBe(true) expect(screen.getByLabelText('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) - }) }) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx deleted file mode 100644 index 957e9729b87..00000000000 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-page-zoom-indicator.test.tsx +++ /dev/null @@ -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( - - ) - 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( - - ) - 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) -}) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts b/src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts deleted file mode 100644 index 0024a51ec41..00000000000 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-pane-page-selection.test.ts +++ /dev/null @@ -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') - ) - }) -}) diff --git a/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx b/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx index 1c4fc695519..86ab1da3588 100644 --- a/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx +++ b/src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.retention-props.test.tsx @@ -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) }) }) diff --git a/src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx b/src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx deleted file mode 100644 index de226788e7c..00000000000 --- a/src/renderer/src/components/editor/MarkdownTableOfContentsPanel.test.tsx +++ /dev/null @@ -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( - {}} 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') - }) -}) diff --git a/src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx b/src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx deleted file mode 100644 index c9ddbb20f23..00000000000 --- a/src/renderer/src/components/editor/RichMarkdownSlashMenu.test.tsx +++ /dev/null @@ -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( - {}} - onEmojiPick={() => {}} - /> - ) - - expect(html).toContain('aria-label="Search blocks"') - expect(html).toContain('value="zzz"') - expect(html).toContain('No blocks found') - }) -}) diff --git a/src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx b/src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx deleted file mode 100644 index d3083567760..00000000000 --- a/src/renderer/src/components/feature-tips/CmdJPaletteFeatureTipVisual.test.tsx +++ /dev/null @@ -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() - }) - - 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() - - 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() - - 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() - - 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() - - 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() - - 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() - - expect(html).toContain('⇧') - expect(html).not.toContain('+') - }) -}) diff --git a/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx b/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx deleted file mode 100644 index 9511df9e860..00000000000 --- a/src/renderer/src/components/feature-tips/CmdJPaletteTipDialog.test.tsx +++ /dev/null @@ -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: () =>
-})) - -vi.mock('@/components/ui/dialog', () => ({ - Dialog: ({ children }: { children: ReactNode }) =>
{children}
, - DialogContent: ({ children }: { children: ReactNode }) =>
{children}
, - DialogDescription: ({ children }: { children: ReactNode }) =>

{children}

, - DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, - DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, - DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

-})) - -vi.mock('./FeatureTipActions', () => ({ - FeatureTipActions: () =>
-})) - -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( - - ) -} - -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') - }) -}) diff --git a/src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx b/src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx deleted file mode 100644 index 7877c4fe451..00000000000 --- a/src/renderer/src/components/feature-tips/SessionSearchTipDialog.test.tsx +++ /dev/null @@ -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: () =>
-})) - -vi.mock('@/components/ui/dialog', () => ({ - Dialog: ({ children }: { children: ReactNode }) =>
{children}
, - DialogContent: ({ children }: { children: ReactNode }) =>
{children}
, - DialogDescription: ({ children }: { children: ReactNode }) =>
{children}
, - DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, - DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, - DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

-})) - -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( - {}} - 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(/]*\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(/]*\sdisabled=""/) - }) -}) diff --git a/src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx b/src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx deleted file mode 100644 index 5e73fdfe889..00000000000 --- a/src/renderer/src/components/feature-tips/VoiceDictationFeatureTipVisual.test.tsx +++ /dev/null @@ -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()) - 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() - 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() - - expect(html).not.toContain('Start dictation') - expect(html).not.toContain('Ctrl') - }) -}) diff --git a/src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx b/src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx deleted file mode 100644 index d366b056b6e..00000000000 --- a/src/renderer/src/components/feature-tips/VoiceDictationTipDialog.test.tsx +++ /dev/null @@ -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: () =>
-})) - -vi.mock('@/components/ui/dialog', () => ({ - Dialog: ({ children }: { children: ReactNode }) =>
{children}
, - DialogContent: ({ - children, - ...props - }: { - children: ReactNode - onOpenAutoFocus?: (event: Event) => void - }) => { - dialogContentPropsMock(props) - return
{children}
- }, - DialogDescription: ({ children }: { children: ReactNode }) =>

{children}

, - DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, - DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, - DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

-})) - -vi.mock('./FeatureTipActions', () => ({ - FeatureTipActions: (props: { primaryButtonRef?: RefObject }) => { - featureTipActionsPropsMock(props) - return - } -})) - -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( - - ) -} - -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( - - ) - }) - 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() - }) -}) diff --git a/src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx b/src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx deleted file mode 100644 index 34f7ffb0183..00000000000 --- a/src/renderer/src/components/feature-wall/feature-wall-shortcut-labels.test.tsx +++ /dev/null @@ -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(), - renderToStaticMarkup(), - renderToStaticMarkup(), - renderToStaticMarkup() - ].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('⌘') - }) -}) diff --git a/src/renderer/src/components/onboarding/AgentStep.test.tsx b/src/renderer/src/components/onboarding/AgentStep.test.tsx deleted file mode 100644 index 1f311968177..00000000000 --- a/src/renderer/src/components/onboarding/AgentStep.test.tsx +++ /dev/null @@ -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( - - - - ) - - 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( - - - - ) - - expect(html).toContain('Hide agents') - expect(html).not.toContain(`Show ${AGENT_CATALOG.length - 1} more agents β†’`) - }) -}) diff --git a/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx b/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx deleted file mode 100644 index aee8ce2aa2d..00000000000 --- a/src/renderer/src/components/onboarding/WindowsTerminalStep.test.tsx +++ /dev/null @@ -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 { - 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( - - ) - - 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( - - ) - - 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( - - ) - - expect(html).toContain('Git Bash') - expect(html).toContain('gwindows_logo.svg') - expect(html).not.toContain('>Git<') - expect(html).not.toContain('>Git<') - }) -}) diff --git a/src/shared/modifier-double-tap-detector.test.ts b/src/shared/modifier-double-tap-detector.test.ts index 457d47cea01..de7047b4c31 100644 --- a/src/shared/modifier-double-tap-detector.test.ts +++ b/src/shared/modifier-double-tap-detector.test.ts @@ -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' }) diff --git a/src/shared/muse-session-log.test.ts b/src/shared/muse-session-log.test.ts index dc82815cd3d..3c0139d0010 100644 --- a/src/shared/muse-session-log.test.ts +++ b/src/shared/muse-session-log.test.ts @@ -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', () => { diff --git a/src/shared/native-chat-activity-tail-line.test.ts b/src/shared/native-chat-activity-tail-line.test.ts index 7c77de32392..f05b9cb7e83 100644 --- a/src/shared/native-chat-activity-tail-line.test.ts +++ b/src/shared/native-chat-activity-tail-line.test.ts @@ -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 ) } }) diff --git a/src/shared/native-chat-agent-profiles.test.ts b/src/shared/native-chat-agent-profiles.test.ts index 27bf2564d37..790aedd3266 100644 --- a/src/shared/native-chat-agent-profiles.test.ts +++ b/src/shared/native-chat-agent-profiles.test.ts @@ -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([]) }) }) diff --git a/src/shared/native-chat-ask-fifo.test.ts b/src/shared/native-chat-ask-fifo.test.ts index eb2019252d7..221f60c9763 100644 --- a/src/shared/native-chat-ask-fifo.test.ts +++ b/src/shared/native-chat-ask-fifo.test.ts @@ -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()])]) diff --git a/src/shared/native-chat-ask.test.ts b/src/shared/native-chat-ask.test.ts index bb1c3157a0a..4400f0b6e76 100644 --- a/src/shared/native-chat-ask.test.ts +++ b/src/shared/native-chat-ask.test.ts @@ -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', [ diff --git a/src/shared/native-chat-paste-bytes.test.ts b/src/shared/native-chat-paste-bytes.test.ts index 420c95b44f1..62e13c8829b 100644 --- a/src/shared/native-chat-paste-bytes.test.ts +++ b/src/shared/native-chat-paste-bytes.test.ts @@ -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) - }) -}) diff --git a/src/shared/native-chat-reasoning-row.spec.ts b/src/shared/native-chat-reasoning-row.spec.ts index 76c2f422e15..7d7131bba68 100644 --- a/src/shared/native-chat-reasoning-row.spec.ts +++ b/src/shared/native-chat-reasoning-row.spec.ts @@ -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', () => { diff --git a/src/shared/native-chat-session-option-commands.test.ts b/src/shared/native-chat-session-option-commands.test.ts index 12e4ad1ca1c..407b8b9ce03 100644 --- a/src/shared/native-chat-session-option-commands.test.ts +++ b/src/shared/native-chat-session-option-commands.test.ts @@ -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' diff --git a/src/shared/native-chat-session-option-snapshot.test.ts b/src/shared/native-chat-session-option-snapshot.test.ts index 58676dadddb..7e32fd1ab26 100644 --- a/src/shared/native-chat-session-option-snapshot.test.ts +++ b/src/shared/native-chat-session-option-snapshot.test.ts @@ -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'], diff --git a/src/shared/native-chat-session-option-state.test.ts b/src/shared/native-chat-session-option-state.test.ts index cbe5b4bb121..a73a1305c84 100644 --- a/src/shared/native-chat-session-option-state.test.ts +++ b/src/shared/native-chat-session-option-state.test.ts @@ -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', () => { diff --git a/src/shared/native-chat-slash-commands.omp.test.ts b/src/shared/native-chat-slash-commands.omp.test.ts index 8282f9da796..d5300fea879 100644 --- a/src/shared/native-chat-slash-commands.omp.test.ts +++ b/src/shared/native-chat-slash-commands.omp.test.ts @@ -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) diff --git a/src/shared/native-chat-slash-commands.test.ts b/src/shared/native-chat-slash-commands.test.ts index 11c07f4a930..d2c74d9337d 100644 --- a/src/shared/native-chat-slash-commands.test.ts +++ b/src/shared/native-chat-slash-commands.test.ts @@ -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']) diff --git a/src/shared/native-chat-subagent-group-header.spec.ts b/src/shared/native-chat-subagent-group-header.spec.ts index e67073fa006..9916fd7e7b8 100644 --- a/src/shared/native-chat-subagent-group-header.spec.ts +++ b/src/shared/native-chat-subagent-group-header.spec.ts @@ -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 }), diff --git a/src/shared/native-chat-task-list.test.ts b/src/shared/native-chat-task-list.test.ts index 0bbb7077f3c..4583fb435db 100644 --- a/src/shared/native-chat-task-list.test.ts +++ b/src/shared/native-chat-task-list.test.ts @@ -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() diff --git a/src/shared/native-chat-turn-activity.test.ts b/src/shared/native-chat-turn-activity.test.ts index 72cee450377..f3dd80625d3 100644 --- a/src/shared/native-chat-turn-activity.test.ts +++ b/src/shared/native-chat-turn-activity.test.ts @@ -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', () => { diff --git a/src/shared/native-chat-turn-fold.test.ts b/src/shared/native-chat-turn-fold.test.ts index c652240db18..a5155cb50dc 100644 --- a/src/shared/native-chat-turn-fold.test.ts +++ b/src/shared/native-chat-turn-fold.test.ts @@ -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({ diff --git a/src/shared/native-chat-turn-grouping.spec.ts b/src/shared/native-chat-turn-grouping.spec.ts index fed223cc83f..4cf9fdbc168 100644 --- a/src/shared/native-chat-turn-grouping.spec.ts +++ b/src/shared/native-chat-turn-grouping.spec.ts @@ -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', () => { diff --git a/src/shared/native-chat-visual-shell.test.ts b/src/shared/native-chat-visual-shell.test.ts index 1cad4feb009..6bcee2ba6b2 100644 --- a/src/shared/native-chat-visual-shell.test.ts +++ b/src/shared/native-chat-visual-shell.test.ts @@ -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', () => { diff --git a/src/shared/native-chat/native-chat-provider-retry-runs.test.ts b/src/shared/native-chat/native-chat-provider-retry-runs.test.ts index d96745d47ae..e6fb18eb861 100644 --- a/src/shared/native-chat/native-chat-provider-retry-runs.test.ts +++ b/src/shared/native-chat/native-chat-provider-retry-runs.test.ts @@ -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' } diff --git a/src/shared/native-chat/native-chat-streaming.spec.ts b/src/shared/native-chat/native-chat-streaming.spec.ts index 54dbac1dadc..3b7b301f7b4 100644 --- a/src/shared/native-chat/native-chat-streaming.spec.ts +++ b/src/shared/native-chat/native-chat-streaming.spec.ts @@ -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.' diff --git a/src/shared/native-chat/tools/native-chat-tool-icon.test.ts b/src/shared/native-chat/tools/native-chat-tool-icon.test.ts index c3294779bf5..d25169640fb 100644 --- a/src/shared/native-chat/tools/native-chat-tool-icon.test.ts +++ b/src/shared/native-chat/tools/native-chat-tool-icon.test.ts @@ -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', diff --git a/src/shared/native-chat/tools/native-chat-tool-pair-limit.spec.ts b/src/shared/native-chat/tools/native-chat-tool-pair-limit.spec.ts index 018b0041d91..e45f4f207c4 100644 --- a/src/shared/native-chat/tools/native-chat-tool-pair-limit.spec.ts +++ b/src/shared/native-chat/tools/native-chat-tool-pair-limit.spec.ts @@ -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 }]) }) }) diff --git a/src/shared/native-chat/tools/native-chat-tool-pairing.performance.test.ts b/src/shared/native-chat/tools/native-chat-tool-pairing.performance.test.ts index 86e970cf2e0..8450200074a 100644 --- a/src/shared/native-chat/tools/native-chat-tool-pairing.performance.test.ts +++ b/src/shared/native-chat/tools/native-chat-tool-pairing.performance.test.ts @@ -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)) - } - } - }) })