From 4e3ee00eda8b485fcbc993b32efdf76508429a07 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Tue, 3 Nov 2026 03:24:09 -0800 Subject: [PATCH] test: remove 167 low-value shared agent tests (#37791) --- config/scripts/ci-shard-timings.json | 9 - .../agent-terminal-status-equivalence.test.ts | 31 --- .../agent-title-agy-gemini-collision.test.ts | 27 +- src/shared/agent-title-decoration.test.ts | 40 --- src/shared/agent-title-evidence.test.ts | 241 +----------------- ...nt-title-identity-characterization.test.ts | 118 --------- src/shared/agent-transitions.test.ts | 64 ----- src/shared/agent-tui-input-clear.test.ts | 81 +----- src/shared/ai-vault-resume-command.test.ts | 203 +-------------- .../ai-vault-resume-preparation.test.ts | 30 --- src/shared/ai-vault-resume-target.test.ts | 42 --- .../ai-vault-scan-error-message.test.ts | 89 ------- src/shared/ai-vault-scope-paths.test.ts | 66 +---- .../ai-vault-search-agent-history.test.ts | 59 ----- src/shared/ai-vault-search-contract.test.ts | 76 +----- .../ai-vault-search-query-operators.test.ts | 107 -------- .../ai-vault-search-scope-contract.test.ts | 48 +--- src/shared/ai-vault-search-settings.test.ts | 80 ------ .../ai-vault-search-status-fallback.test.ts | 20 +- .../ai-vault-session-cwd-candidates.test.ts | 21 -- src/shared/ai-vault-session-depth.test.ts | 88 +------ src/shared/ai-vault-session-filters.test.ts | 213 +--------------- src/shared/ai-vault-ssh-resume-target.test.ts | 55 ---- src/shared/ai-vault-types.test.ts | 93 ------- 24 files changed, 15 insertions(+), 1886 deletions(-) delete mode 100644 src/shared/agent-terminal-status-equivalence.test.ts delete mode 100644 src/shared/agent-title-decoration.test.ts delete mode 100644 src/shared/agent-title-identity-characterization.test.ts delete mode 100644 src/shared/ai-vault-resume-preparation.test.ts delete mode 100644 src/shared/ai-vault-scan-error-message.test.ts delete mode 100644 src/shared/ai-vault-search-query-operators.test.ts delete mode 100644 src/shared/ai-vault-search-settings.test.ts delete mode 100644 src/shared/ai-vault-session-cwd-candidates.test.ts delete mode 100644 src/shared/ai-vault-types.test.ts diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 509f2bb5310..8bcf672468e 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -11711,30 +11711,21 @@ "src/shared/agent-status-transport-envelope.test.ts": 192, "src/shared/agent-status-types.test.ts": 132, "src/shared/agent-status-worktree-attribution.test.ts": 198, - "src/shared/agent-terminal-status-equivalence.test.ts": 46, "src/shared/agent-title-agy-gemini-collision.test.ts": 83, - "src/shared/agent-title-decoration.test.ts": 43, "src/shared/agent-title-evidence.test.ts": 166, - "src/shared/agent-title-identity-characterization.test.ts": 77, "src/shared/agent-transcript-poll-scheduler.test.ts": 113, "src/shared/agent-transitions.test.ts": 105, "src/shared/agent-tui-input-clear.test.ts": 57, "src/shared/ai-vault-resume-command.test.ts": 70, - "src/shared/ai-vault-resume-preparation.test.ts": 39, "src/shared/ai-vault-resume-target.test.ts": 85, - "src/shared/ai-vault-scan-error-message.test.ts": 54, "src/shared/ai-vault-scope-paths.test.ts": 73, "src/shared/ai-vault-search-agent-history.test.ts": 168, "src/shared/ai-vault-search-contract.test.ts": 148, - "src/shared/ai-vault-search-query-operators.test.ts": 82, "src/shared/ai-vault-search-scope-contract.test.ts": 130, - "src/shared/ai-vault-search-settings.test.ts": 86, "src/shared/ai-vault-search-status-fallback.test.ts": 175, - "src/shared/ai-vault-session-cwd-candidates.test.ts": 113, "src/shared/ai-vault-session-depth.test.ts": 78, "src/shared/ai-vault-session-filters.test.ts": 176, "src/shared/ai-vault-ssh-resume-target.test.ts": 84, - "src/shared/ai-vault-types.test.ts": 44, "src/shared/ansi-escape-sequences-consumers.test.ts": 140, "src/shared/ansi-escape-sequences.test.ts": 289, "src/shared/antigravity-generation-plan.test.ts": 122, diff --git a/src/shared/agent-terminal-status-equivalence.test.ts b/src/shared/agent-terminal-status-equivalence.test.ts deleted file mode 100644 index dd3affba347..00000000000 --- a/src/shared/agent-terminal-status-equivalence.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { terminalStatusPayloadMatchesHook } from './agent-terminal-status-equivalence' - -describe('terminalStatusPayloadMatchesHook', () => { - const hook = { - state: 'working' as const, - workingMode: 'monitoring' as const, - prompt: 'watch tests', - agentType: 'claude' - } - - it('accepts an otherwise equivalent terminal payload that omits hook-owned mode', () => { - expect( - terminalStatusPayloadMatchesHook(hook, { - state: 'working', - prompt: 'watch tests', - agentType: 'claude' - }) - ).toBe(true) - }) - - it('rejects a terminal payload from a different turn', () => { - expect( - terminalStatusPayloadMatchesHook(hook, { - state: 'working', - prompt: 'fix tests', - agentType: 'claude' - }) - ).toBe(false) - }) -}) diff --git a/src/shared/agent-title-agy-gemini-collision.test.ts b/src/shared/agent-title-agy-gemini-collision.test.ts index ea57c812d44..afcd889656e 100644 --- a/src/shared/agent-title-agy-gemini-collision.test.ts +++ b/src/shared/agent-title-agy-gemini-collision.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' -import { getAgentLabel } from './agent-title-identity' +import './agent-title-identity' import { resolveExplicitTerminalTitleAgentType } from './terminal-title-agent-type' -import { GEMINI_IDLE, GEMINI_WORKING } from './agent-title-core' +import './agent-title-core' // agy model names include Gemini; the earlier Gemini token detector once mistook model metadata for pane identity. describe('agy titles carrying a Gemini model name', () => { @@ -11,30 +11,7 @@ describe('agy titles carrying a Gemini model name', () => { 'agy · Gemini 3.5 Flash (High)' ] - it.each(AGY_TITLES)('labels %s as Antigravity', (title) => { - expect(getAgentLabel(title)).toBe('Antigravity') - }) - - // Exercise both sidebar and tab detector chains so fixing one cannot leave the surfaces disagreeing. it.each(AGY_TITLES)('resolves %s as antigravity on the tab path too', (title) => { expect(resolveExplicitTerminalTitleAgentType(title)).toBe('antigravity') }) }) - -describe('titles that must keep their existing label', () => { - // Recorded Grok task text mentions Antigravity; narrowing the Gemini token avoids changing this ordered-chain result. - it('keeps a grok pane whose task text names Antigravity', () => { - expect(getAgentLabel('STA-4011 Linux Antigravity Commit Messages - grok')).toBe('Grok') - }) - - // Why: the four Gemini OSC glyphs are decisive and stay so; agy emits none of them. - it('still labels a glyph-bearing Gemini CLI title', () => { - expect(getAgentLabel(`${GEMINI_WORKING} Gemini CLI`)).toBe('Gemini CLI') - expect(getAgentLabel(`${GEMINI_IDLE} Gemini CLI`)).toBe('Gemini CLI') - }) - - // Why: a Gemini CLI title with no agy token must be unaffected by the narrowing. - it('still labels a bare gemini token title', () => { - expect(getAgentLabel('gemini ready')).toBe('Gemini CLI') - }) -}) diff --git a/src/shared/agent-title-decoration.test.ts b/src/shared/agent-title-decoration.test.ts deleted file mode 100644 index 5a5666696e0..00000000000 --- a/src/shared/agent-title-decoration.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - stripLeadingAgentTitleDecoration, - stripLeadingAgentTitleDecorationOrEmpty -} from './agent-title-decoration' - -describe('stripLeadingAgentTitleDecoration', () => { - it('strips Qoder permission decoration without stripping triangles inside a title', () => { - expect(stripLeadingAgentTitleDecoration('▲ Qoder CLI')).toBe('Qoder CLI') - expect(stripLeadingAgentTitleDecoration('deploy ▲ complete')).toBe('deploy ▲ complete') - }) - - it("strips Claude's ✳ idle glyph", () => { - expect(stripLeadingAgentTitleDecoration('✳ Claude Code')).toBe('Claude Code') - }) - - it("strips Claude's working/idle text prefixes", () => { - expect(stripLeadingAgentTitleDecoration('. working on the fix')).toBe('working on the fix') - expect(stripLeadingAgentTitleDecoration('* Claude Code')).toBe('Claude Code') - }) - - it('strips a leading braille spinner', () => { - expect(stripLeadingAgentTitleDecoration('⠋ Pi')).toBe('Pi') - }) - - it('leaves an undecorated title untouched', () => { - expect(stripLeadingAgentTitleDecoration('Dolphin-2')).toBe('Dolphin-2') - expect(stripLeadingAgentTitleDecoration('npm run dev')).toBe('npm run dev') - }) - - it('keeps the original when the title is only a status glyph', () => { - expect(stripLeadingAgentTitleDecoration('✳')).toBe('✳') - expect(stripLeadingAgentTitleDecoration('✳ ')).toBe('✳ ') - }) - - it('can strip to empty when the caller supplies its own fallback label', () => { - expect(stripLeadingAgentTitleDecorationOrEmpty('✳')).toBe('') - expect(stripLeadingAgentTitleDecorationOrEmpty('✳ ')).toBe('') - }) -}) diff --git a/src/shared/agent-title-evidence.test.ts b/src/shared/agent-title-evidence.test.ts index 829a53b8047..8f4324b2c4a 100644 --- a/src/shared/agent-title-evidence.test.ts +++ b/src/shared/agent-title-evidence.test.ts @@ -1,10 +1,5 @@ import { describe, expect, it } from 'vitest' -import { - GEMINI_IDLE, - GEMINI_PERMISSION, - GEMINI_SILENT_WORKING, - GEMINI_WORKING -} from './agent-title-core' +import './agent-title-core' import { collectAgentTitleEvidence, collectExplicitAgentTitleEvidence @@ -13,7 +8,6 @@ import { resolveCanonicalPaneAgentIdentity } from './pane-agent-identity-adapter import { resolvePublishedPaneAgentIdentity } from './published-pane-agent-identity' const agentFor = (title: string) => collectAgentTitleEvidence(title).agent -const reasonFor = (title: string) => collectAgentTitleEvidence(title).reason describe('collectAgentTitleEvidence', () => { it.each([ @@ -49,19 +43,6 @@ describe('collectAgentTitleEvidence', () => { ])('resolves %j to the suffix owner', (title) => { expect(agentFor(title)).toBe('grok') }) - - it('does not read a hyphenated worktree name as an owner suffix', () => { - // Require whitespace before the owner-suffix dash so directory names such as review-14600-codex do not claim ownership. - expect(agentFor('review-14600-codex')).toBeNull() - expect(agentFor('codex-split-core')).toBeNull() - }) - - it.each(['pi', 'omp', 'claude-agent-teams', 'qwen-code'] as const)( - 'recognizes the reserved owner id %s', - (agent) => { - expect(agentFor(`Review another agent… - ${agent}`)).toBe(agent) - } - ) }) describe('order independence', () => { @@ -81,113 +62,18 @@ describe('collectAgentTitleEvidence', () => { }) }) - it.each([ - ['claude', 'claude'], - ['openclaude', 'openclaude'], - ['codex', 'codex'], - ['copilot', 'copilot'], - ['cursor', 'cursor'], - ['gemini', 'gemini'], - ['antigravity', 'antigravity'], - ['opencode', 'opencode'], - ['mimo', 'mimo-code'], - ['openclaw', 'openclaw'], - ['aider', 'aider'], - ['grok', 'grok'], - ['devin', 'devin'] - ] as const)('collects the free-text token %s without claiming identity', (token, agent) => { - expect(collectAgentTitleEvidence(`review the ${token} integration`)).toMatchObject({ - agent: null, - reason: 'free-text-only', - freeTextNames: [agent] - }) - }) - - describe('a vendor marker is evidence the agent emitted, not text a human typed', () => { - it('keeps a Claude pane Claude when its task text names another agent', () => { - // 13 recorded titles have this shape. The sigil is emitted by Claude; the name is typed. - expect(agentFor('✳ Fix Codex false attention notifications on Windows')).toBe('claude') - expect(reasonFor('✳ Consolidate Codex subagent sidebar rows')).toBe('vendor-marker') - }) - - it('keeps the Claude marker when the remaining bare name is unanchored', () => { - expect(agentFor('✳ agy')).toBe('claude') - expect(reasonFor('✳ agy')).toBe('vendor-marker') - expect(agentFor('✳ codex')).toBe('claude') - expect(reasonFor('✳ codex')).toBe('vendor-marker') - }) - - it('keeps an OpenCode envelope OpenCode when its session text names another agent', () => { - expect(agentFor('OC | QA PR #14582 Cursor sidecar SSH arms')).toBe('opencode') - }) - }) - describe('Antigravity model names are metadata, not identity', () => { it('reads an identity segment plus a model name as Antigravity', () => { expect(agentFor('agy · Gemini 3.7 Flash')).toBe('antigravity') expect(agentFor('Antigravity — Gemini 3.7 Flash')).toBe('antigravity') }) - it('declines a bare model name rather than guessing Gemini CLI', () => { - // Without identity grammar/vendor evidence, free-text agy/Gemini names are ambiguous. - expect(agentFor('Gemini 3.7 Flash · high')).toBeNull() - }) - - it.each([GEMINI_WORKING, GEMINI_SILENT_WORKING, GEMINI_IDLE, GEMINI_PERMISSION])( - 'still resolves the real Gemini marker %s', - (marker) => { - expect(agentFor(`${marker} Refactor the parser`)).toBe('gemini') - } - ) - it('does not promote model names in task text', () => { expect(agentFor('Compare Antigravity with Gemini 3.7 Flash')).toBeNull() expect(agentFor('Compare Antigravity with Gemini 3.7 Flash… - grok')).toBe('grok') }) - - it('does not treat a Gemini glyph inside task text as a vendor marker', () => { - const evidence = collectAgentTitleEvidence('Explain the ✦ marker… - grok') - expect(evidence.agent).toBe('grok') - expect(evidence.vendorMarkers).toEqual([]) - }) }) - describe('a name in free text alone is never identity', () => { - it.each([ - '◐ DaemonConnectionLostError with 70 Codex agents', - 'Fix the grok hook', - 'Debug the cursor sidecar', - 'grok', - '⠋ grok', - '⠋ codex' - ])('declines %j', (title) => { - expect(agentFor(title)).toBeNull() - expect(reasonFor(title)).toBe('free-text-only') - }) - }) - - it.each([ - ['Claude Code', 'claude'], - ['Gemini CLI', 'gemini'], - ['Claude Agent Teams', 'claude-agent-teams'], - ['MiMo Code', 'mimo-code'], - ['Prime Agent', 'prime-agent'], - ['Command Code', 'command-code'], - ['GitHub Copilot', 'copilot'], - ['Agent Teams', 'claude-agent-teams'] - ] as const)('recognizes the emitted whole-title alias %s', (title, agent) => { - expect(agentFor(title)).toBe(agent) - expect(reasonFor(title)).toBe('anchored') - }) - - it.each(['Continue', 'Charm', 'Goose', 'Amp'])( - 'does not treat the UI-only display label %s as identity', - (title) => { - expect(agentFor(title)).toBeNull() - expect(reasonFor(title)).toBe('no-evidence') - } - ) - it.each([ '~/codex', '/grok', @@ -200,74 +86,11 @@ describe('collectAgentTitleEvidence', () => { expect(agentFor(title)).toBeNull() }) - it('does not duplicate an anchored token as free text', () => { - expect(collectAgentTitleEvidence('codex.exe').freeTextNames).toEqual([]) - expect(collectAgentTitleEvidence('Claude Agent Teams').freeTextNames).toEqual([]) - }) - - it.each([ - ['Codex ready', 'codex'], - ['Codex - action required', 'codex'], - ['Cursor ready', 'cursor'], - ['Droid - action required', 'droid'], - ['Hermes ready', 'hermes'], - ['Devin - action required', 'devin'], - ['Pi ready', 'pi'], - ['OMP - action required', 'omp'] - ] as const)('recognizes Orca-controlled synthetic title %s', (title, agent) => { - expect(agentFor(title)).toBe(agent) - expect(reasonFor(title)).toBe('anchored') - }) - - it.each(['Droid', 'Hermes', 'Devin'])( - 'does not treat a bare working label as synthetic identity: %s', - (title) => { - expect(agentFor(title)).toBeNull() - expect(reasonFor(title)).toBe('free-text-only') - } - ) - - it.each([ - 'Claude Code ready', - 'Claude thinking', - '. Claude Code working', - 'zsh | ⠋ Claude Code - action required' - ])('recognizes the explicit Claude identity frame %s', (title) => { - expect(agentFor(title)).toBe('claude') - expect(reasonFor(title)).toBe('anchored') - }) - it('does not promote Claude status words in task text', () => { expect(agentFor('Fix the Claude Code ready-state parser')).toBeNull() expect(agentFor('Fix Claude Code ready behavior… - grok')).toBe('grok') }) - it.each(['. Review the parser', '* Waiting for input'])( - 'recognizes the established Claude status prefix in %s', - (title) => { - expect(agentFor(title)).toBe('claude') - expect(reasonFor(title)).toBe('vendor-marker') - } - ) - - it.each([ - ['⠋ Cursor Agent', 'cursor'], - ['⠋ Pi idle', 'pi'], - ['⠋ OMP done', 'omp'], - ['⠋ Droid', 'droid'], - ['⠋ Hermes', 'hermes'], - ['⠋ Devin', 'devin'] - ] as const)('recognizes the decorated identity frame %s', (title, agent) => { - expect(agentFor(title)).toBe(agent) - expect(reasonFor(title)).toBe('anchored') - }) - - it('does not invent synthetic titles for an opted-out profile', () => { - expect(agentFor('OpenCode ready')).toBeNull() - expect(agentFor('⠋ OpenCode')).toBeNull() - expect(reasonFor('OpenCode ready')).toBe('free-text-only') - }) - it('reads identity from the innermost wrapper segment', () => { expect(agentFor('zsh | ⠋ Claude Code')).toBe('claude') expect(agentFor('ssh | tmux | Cursor Agent')).toBe('cursor') @@ -281,63 +104,6 @@ describe('collectAgentTitleEvidence', () => { expect(collectExplicitAgentTitleEvidence(title).agent).toBe('opencode') }) - it('bounds wrapper inspection while preserving innermost identity', () => { - const wrappers = Array.from({ length: 200 }, (_, index) => `wrapper-${index}`).join(' | ') - expect(agentFor(`${wrappers} | ⠋ Cursor Agent`)).toBe('cursor') - expect(agentFor(`${wrappers} | OC | review the parser`)).toBe('opencode') - expect(agentFor(`outer-a | outer-b | OC | ${wrappers} | Cursor Agent`)).toBe('cursor') - }) - - it.each([ - ['codex.exe', 'codex'], - ['openclaude.cmd', 'openclaude'], - ['gemini.ps1', 'gemini'], - ['droid.cmd', 'droid'], - ['hermes.exe', 'hermes'], - ['agy.bat', 'antigravity'], - ['CODEX.EXE', 'codex'], - ['DROID.CMD', 'droid'] - ] as const)('recognizes the bare Windows launcher %s', (title, agent) => { - expect(agentFor(title)).toBe(agent) - expect(reasonFor(title)).toBe('anchored') - }) - - it('produces no name evidence for an agent outside the token set', () => { - // Keep the evidence token set narrow: short names such as omp would classify ordinary shell text. - expect(reasonFor('Review PR for OMP transcript rendering')).toBe('no-evidence') - }) - - describe('activity is not identity', () => { - it.each(['◐ Rebase PR #14624 onto main', '⠂ Fix SSH fallback', '⠋ Thinking'])( - 'declines the spinner-only title %j', - (title) => { - // Shared braille/quarter-circle spinners prove activity only; identity requires other evidence. - expect(agentFor(title)).toBeNull() - expect(reasonFor(title)).toBe('no-evidence') - } - ) - }) - - it('does not treat an embedded Claude sigil as a vendor marker', () => { - expect(collectAgentTitleEvidence('task text ✳ decoration')).toEqual({ - vendorMarkers: [], - anchoredNames: [], - freeTextNames: [], - agent: null, - reason: 'no-evidence' - }) - }) - - it('recognizes a bare Claude sigil as a vendor marker', () => { - expect(collectAgentTitleEvidence('✳')).toEqual({ - vendorMarkers: ['claude'], - anchoredNames: [], - freeTextNames: [], - agent: 'claude', - reason: 'vendor-marker' - }) - }) - describe('conflicting evidence of the same class resolves to nothing', () => { it('declines two anchored names', () => { const evidence = collectAgentTitleEvidence('OC | something… - grok') @@ -372,11 +138,6 @@ describe('collectAgentTitleEvidence', () => { }) }) - it('requires whitespace before the owner suffix dash', () => { - expect(agentFor('task- codex')).toBeNull() - expect(reasonFor('task- codex')).toBe('free-text-only') - }) - it('terminates when a wrapper title starts with a separator', () => { expect(agentFor(' | ')).toBeNull() }) diff --git a/src/shared/agent-title-identity-characterization.test.ts b/src/shared/agent-title-identity-characterization.test.ts deleted file mode 100644 index 3f4c231dd6b..00000000000 --- a/src/shared/agent-title-identity-characterization.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { getAgentLabel } from './agent-title-identity' - -/** - * Characterization of `getAgentLabel` before the identity refactor. - * - * `getAgentLabel` is an ordered first-match-wins scan of title predicates. Detector precedence - * and targeted exceptions — not a unified evidence model — decide identity. These tests pin - * the current answers, including the wrong ones, so the resolver change shows up as a - * reviewable diff of assertions rather than as silent behavior drift. - * - * Cases marked DEFECT are minimized from real recorded pane titles - * (`terminal-history//checkpoint.json` -> `lastTitle`). At the time of writing the live - * corpus held 2,846 checkpoints / 1,084 populated titles / 747 distinct, of which 5 real titles - * resolved to the wrong agent. Task text is minimized here; the corpus stays local. - */ - -/** Orca's own owner suffix: the agent that owns the pane is named after the final `- `. */ -const ownerSuffix = (task: string, agent: string): string => `${task}… - ${agent}` - -describe('getAgentLabel — characterization (pre-refactor)', () => { - describe('the owner suffix loses to a foreign name in task text', () => { - // DEFECT. In each case the pane owner is Grok, named by Orca's own `- grok` suffix - // grammar, while the competing agent appears only inside free-form task text. Codex is - // checked before Grok, so the weaker evidence wins. - it.each([ - ['Switch Claude and Codex off the load balancer', 'Codex'], - ['Codex structured chat revalidation', 'Codex'], - ['Swap Codex off the load balancer', 'Codex'] - ])('reads %j as %s instead of Grok', (task, current) => { - expect(getAgentLabel(ownerSuffix(task, 'grok'))).toBe(current) - }) - - it('reads a spinner-prefixed Grok pane as Codex', () => { - // DEFECT. Real shape: a status spinner and phase precede the task text. - expect(getAgentLabel(`⠸ - Thinking - ${ownerSuffix('Codex native-chat work', 'grok')}`)).toBe( - 'Codex' - ) - }) - - it('resolves a Grok pane naming two other agents, but only by a targeted exception', () => { - // Fixed by #15535, which teaches the Gemini token branch to decline when an Antigravity - // name is present. Correct here only because that exception happens to clear the path to - // Grok — drop the word "Antigravity" and it reads as Gemini CLI again (next case). - expect( - getAgentLabel(ownerSuffix('Electron QA: Antigravity tab vs Gemini label', 'grok')) - ).toBe('Grok') - }) - - it('still reads a Grok pane as Gemini CLI when no exception covers the pair', () => { - // DEFECT, and the general form of the case above: one narrowing fixes one collision. - expect(getAgentLabel(ownerSuffix('Electron QA: check the Gemini label', 'grok'))).toBe( - 'Gemini CLI' - ) - }) - - it('resolves the owner suffix correctly only when no earlier agent is named', () => { - // Why this passes today: nothing earlier in the chain matches, so position never comes up. - expect(getAgentLabel(ownerSuffix('Fix the sidebar row', 'grok'))).toBe('Grok') - }) - }) - - describe('a hyphenated worktree name is correctly not identity', () => { - // Not a defect — pinned so the resolver does not start claiming these. - it.each(['review-14600-codex', 'sta4779-review-codex', 'codex-split-core'])( - 'declines %j', - (title) => { - expect(getAgentLabel(title)).toBeNull() - } - ) - }) - - describe('title order does not decide between two names', () => { - // Both orderings of each pair agree: detector precedence and its targeted exceptions, - // rather than name order within the title, choose the label. - it.each([ - ['codex', 'grok', 'Codex'], - ['grok', 'codex', 'Codex'], - ['gemini', 'antigravity', 'Antigravity'], - ['antigravity', 'gemini', 'Antigravity'], - ['copilot', 'devin', 'GitHub Copilot'], - ['devin', 'copilot', 'GitHub Copilot'] - ])('%s + %s both resolve to %s', (first, second, winner) => { - expect(getAgentLabel(`${first} and ${second}`)).toBe(winner) - }) - }) - - describe('a vendor glyph is claimed by whichever check sits earliest', () => { - it('claims a Claude glyph even when the task text names another agent', () => { - // Correct today (the pane really is Claude) but for the wrong reason: the glyph is not - // consulted as vendor evidence, the `✳ ` prefix branch simply sits first. - expect(getAgentLabel('✳ Fix Codex false attention notifications on Windows')).toBe( - 'Claude Code' - ) - }) - - it('gives a bare agent name no way to outrank an earlier glyph check', () => { - // DEFECT. `agy` is the entire undecorated remainder — the strongest possible name - // evidence — and still loses to Claude's prefix glyph. - expect(getAgentLabel('✳ agy')).toBe('Claude Code') - }) - }) - - describe('Antigravity model names', () => { - it('reads a bare Antigravity model title as Gemini CLI', () => { - // DEFECT. Antigravity models are named `Gemini `, so an agy pane's own title - // carries a whole `gemini` token, and Gemini CLI is checked first. - expect(getAgentLabel('Gemini 3.7 Flash · high')).toBe('Gemini CLI') - }) - - it('resolves once the Antigravity name is also present', () => { - // Fixed by #15535. The resolver reaches the same answer structurally — the agy segment is - // the anchored identity and `Gemini ` is model metadata — rather than by - // teaching one detector about one competitor. - expect(getAgentLabel('agy · Gemini 3.7 Flash')).toBe('Antigravity') - }) - }) -}) diff --git a/src/shared/agent-transitions.test.ts b/src/shared/agent-transitions.test.ts index 247c2ee51dd..04ec822b001 100644 --- a/src/shared/agent-transitions.test.ts +++ b/src/shared/agent-transitions.test.ts @@ -1,77 +1,13 @@ import { describe, expect, it } from 'vitest' import { - agentPhaseTransition, hookRowAgentPhase, hookRowTransition, nextAgentPhase, - titleAgentPhase, type AgentPhase } from './agent-transitions' import { AGENT_TRANSITION_SCENARIOS } from './agent-transitions.test-fixture' -const PHASES: readonly AgentPhase[] = ['rest', 'working', 'needs-you', 'done'] - -describe('hookRowAgentPhase', () => { - it('maps hook states to phases, and a stopped turn to Rest', () => { - expect(hookRowAgentPhase({ state: 'working' })).toBe('working') - expect(hookRowAgentPhase({ state: 'blocked' })).toBe('needs-you') - expect(hookRowAgentPhase({ state: 'waiting' })).toBe('needs-you') - expect(hookRowAgentPhase({ state: 'done' })).toBe('done') - expect(hookRowAgentPhase({ state: 'done', interrupted: true })).toBe('rest') - expect( - hookRowAgentPhase({ state: 'done', mainAgent: { state: 'done', outcome: 'cancellation' } }) - ).toBe('rest') - // An error is news: it ranks as Done, like a finish. - expect( - hookRowAgentPhase({ state: 'done', mainAgent: { state: 'done', outcome: 'failure' } }) - ).toBe('done') - }) -}) - -describe('titleAgentPhase', () => { - it('reads the title heuristic Smart uses', () => { - expect(titleAgentPhase('⠋ Codex working')).toBe('working') - expect(titleAgentPhase('zsh')).toBe('rest') - }) -}) - describe('agentPhaseTransition', () => { - it('names the news in every phase change', () => { - const table = Object.fromEntries( - PHASES.flatMap((from) => - PHASES.map((to) => [`${from} -> ${to}`, agentPhaseTransition(from, to)]) - ) - ) - expect(table).toEqual({ - 'rest -> rest': null, - 'rest -> working': 'prompt-submitted', - 'rest -> needs-you': 'needs-input', - 'rest -> done': 'turn-finished', - 'working -> rest': null, - 'working -> working': null, - 'working -> needs-you': 'needs-input', - 'working -> done': 'turn-finished', - 'needs-you -> rest': null, - 'needs-you -> working': null, - 'needs-you -> needs-you': null, - 'needs-you -> done': 'turn-finished', - 'done -> rest': null, - 'done -> working': 'prompt-submitted', - 'done -> needs-you': 'needs-input', - 'done -> done': null - }) - }) - - it('tells a failed turn from a finish only by its row', () => { - const failed = { - state: 'done' as const, - mainAgent: { state: 'done' as const, outcome: 'failure' as const } - } - expect(hookRowTransition('working', 'done', failed)).toBe('errored') - expect(hookRowTransition('working', 'done', { state: 'done' })).toBe('turn-finished') - expect(hookRowTransition('done', 'done', failed)).toBeNull() - }) - it('names no news for a session boundary, whose phase still reads done', () => { const boundary = { state: 'done' as const, sessionBoundary: true } expect(hookRowAgentPhase(boundary)).toBe('done') diff --git a/src/shared/agent-tui-input-clear.test.ts b/src/shared/agent-tui-input-clear.test.ts index 36be37dfdf4..5d1342373b7 100644 --- a/src/shared/agent-tui-input-clear.test.ts +++ b/src/shared/agent-tui-input-clear.test.ts @@ -1,88 +1,9 @@ import { describe, expect, it } from 'vitest' -import { - AGENT_TUI_CLEAR_INPUT_FORWARD, - AGENT_TUI_CLEAR_INPUT_LINE, - AGENT_TUI_CLEAR_INPUT_MAX, - AGENT_TUI_CLEAR_LINE_SLACK, - AGENT_TUI_CLEAR_MAX_LINES, - buildAgentTuiClearInput, - buildAgentTuiClearInputForText, - countAgentTuiInputLines -} from './agent-tui-input-clear' - -const countCtrlU = (bytes: string): number => - bytes.split('').filter((char) => char === AGENT_TUI_CLEAR_INPUT_LINE).length -const countCtrlK = (bytes: string): number => - bytes.split('').filter((char) => char === AGENT_TUI_CLEAR_INPUT_FORWARD).length +import { AGENT_TUI_CLEAR_INPUT_MAX } from './agent-tui-input-clear' describe('buildAgentTuiClearInput', () => { - // Pin N kills + N-1 joins for every small N; constant bursts under-clear multiline drafts and glue residue onto the next message. - it.each([ - [1, 1], - [2, 3], - [3, 5], - [4, 7], - [10, 19] - ])('clears %i logical lines with %i Ctrl+U', (lines, expected) => { - const clearInput = buildAgentTuiClearInput(lines) - expect(countCtrlU(clearInput)).toBe(expected) - expect(countCtrlK(clearInput)).toBe(expected) - }) - - it('clears before the cursor before clearing the suffix after it', () => { - expect(buildAgentTuiClearInput(4)).toBe('\x15'.repeat(7) + '\x0b'.repeat(7)) - }) - - it('still clears one line for a zero or negative count', () => { - expect(countCtrlU(buildAgentTuiClearInput(0))).toBe(1) - expect(countCtrlU(buildAgentTuiClearInput(-5))).toBe(1) - }) - - it('caps the burst so a pathological draft cannot emit an unbounded write', () => { - expect(countCtrlU(buildAgentTuiClearInput(10_000))).toBe(2 * AGENT_TUI_CLEAR_MAX_LINES - 1) - expect(AGENT_TUI_CLEAR_INPUT_MAX).toBe(buildAgentTuiClearInput(AGENT_TUI_CLEAR_MAX_LINES)) - }) - it('keeps the widest burst under the 64-byte chunk Claude Code reads as a paste', () => { // Claude Code 2.1.314 dropped a 64-byte burst and kept a 63-byte one. expect(AGENT_TUI_CLEAR_INPUT_MAX.length).toBeLessThan(64) }) }) - -describe('countAgentTuiInputLines', () => { - it.each([ - ['one line', 1], - ['a\nb', 2], - ['a\r\nb\r\nc', 3], - ['a\rb', 2], - ['trailing\n', 2] - ])('counts %j as %i logical lines', (text, expected) => { - expect(countAgentTuiInputLines(text)).toBe(expected) - }) - - it('ignores visual wrapping — only logical newlines cost a Ctrl+U', () => { - expect(countAgentTuiInputLines('x'.repeat(5_000))).toBe(1) - }) -}) - -describe('buildAgentTuiClearInputForText', () => { - it('sizes the burst from the text plus slack for TUI-side edits', () => { - // The injected text is a LOWER bound: the user can type into the TUI line too. - expect(countCtrlU(buildAgentTuiClearInputForText('a\nb'))).toBe( - 2 * (2 + AGENT_TUI_CLEAR_LINE_SLACK) - 1 - ) - }) - - it('clears strictly more than the draft needs, never less', () => { - const draft = 'Linked Linear issue: ABC-123\nhttps://linear.app/x/issue/ABC-123\n' - expect(countCtrlU(buildAgentTuiClearInputForText(draft))).toBeGreaterThan( - 2 * countAgentTuiInputLines(draft) - 1 - ) - }) - - it('a long wrapped single line does not inflate the burst', () => { - expect(buildAgentTuiClearInputForText('y'.repeat(5_000))).toBe( - buildAgentTuiClearInputForText('y') - ) - }) -}) diff --git a/src/shared/ai-vault-resume-command.test.ts b/src/shared/ai-vault-resume-command.test.ts index a2f57257b19..57fee9c349e 100644 --- a/src/shared/ai-vault-resume-command.test.ts +++ b/src/shared/ai-vault-resume-command.test.ts @@ -1,201 +1,6 @@ import { describe, expect, it } from 'vitest' -import { - buildAiVaultResumeCommand, - buildAiVaultResumeShellCommand -} from './ai-vault-resume-command' - -describe('buildAiVaultResumeCommand', () => { - it('uses Antigravity conversation ids instead of Gemini resume flags', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'antigravity', - sessionId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee', - cwd: '/repo/app', - platform: 'darwin' - }) - ).toBe("cd '/repo/app' && agy --conversation 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'") - }) - - it('builds a self-contained cmd wrapper when no live shell is known', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'codex', - sessionId: 'session-1', - cwd: 'C:\\Users\\Ada Lovelace\\repo', - platform: 'win32' - }) - ).toBe('cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && codex resume ""session-1"""') - }) - - it('resumes CodeBuddy sessions with the Claude-compatible --resume flag', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'codebuddy', - sessionId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee', - cwd: '/repo/app', - platform: 'darwin' - }) - ).toBe("cd '/repo/app' && codebuddy --resume 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee'") - }) - - it('builds a direct queued command for a live cmd shell', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'omp', - sessionId: 'session-one', - resumeFilePath: 'C:\\Users\\Ada Lovelace\\.omp\\sessions\\A&B session one.jsonl', - cwd: 'C:\\Users\\Ada Lovelace\\A&B repo', - platform: 'win32', - shell: 'cmd' - }) - ).toBe( - 'cd /d "C:\\Users\\Ada Lovelace\\A&B repo" && omp --resume "C:\\Users\\Ada Lovelace\\.omp\\sessions\\A&B session one.jsonl"' - ) - }) - - it('emits no CODEX_HOME stamp for real-home canonical sessions', () => { - // Backfills dedupe to real-home rows: resume from ~/.codex, never a frozen managed home whose auth stops refreshing. - const command = buildAiVaultResumeCommand({ - agent: 'codex', - sessionId: 'session-1', - cwd: '/repo/app', - platform: 'darwin', - codexHome: null - }) - expect(command).toBe("cd '/repo/app' && codex resume 'session-1'") - expect(command).not.toContain('CODEX_HOME') - }) - - it('carries non-default Codex homes in copied resume commands', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'codex', - sessionId: 'session-1', - cwd: '/repo/app', - platform: 'darwin', - codexHome: '/Users/ada/Library/Application Support/Orca/codex-runtime-home/home' - }) - ).toBe( - "cd '/repo/app' && CODEX_HOME='/Users/ada/Library/Application Support/Orca/codex-runtime-home/home' codex resume 'session-1'" - ) - - expect( - buildAiVaultResumeCommand({ - agent: 'codex', - sessionId: 'session-1', - cwd: 'C:\\Users\\Ada Lovelace\\repo', - platform: 'win32', - codexHome: 'C:\\Users\\Ada\\AppData\\Roaming\\Orca\\codex-runtime-home\\home' - }) - ).toBe( - 'cmd /d /s /c "cd /d ""C:\\Users\\Ada Lovelace\\repo"" && set ""CODEX_HOME=C:\\Users\\Ada\\AppData\\Roaming\\Orca\\codex-runtime-home\\home"" && codex resume ""session-1"""' - ) - }) - - it('resumes OMP by absolute transcript path so it resolves across session-dir roots', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'omp', - sessionId: '019f27cd-4268-7000-96e7-62f42a55c144', - resumeFilePath: - '/Users/ada/.omp/agent/sessions/repo/2026-07-03T11-30-29-357Z_019f27be/OmpScannerTests.jsonl', - cwd: '/Users/ada/repo', - platform: 'darwin' - }) - ).toBe( - "cd '/Users/ada/repo' && omp --resume '/Users/ada/.omp/agent/sessions/repo/2026-07-03T11-30-29-357Z_019f27be/OmpScannerTests.jsonl'" - ) - }) - - it('quotes queued OMP resume paths for the provided Windows shell', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'omp', - sessionId: '019f27cd-4268-7000-96e7-62f42a55c144', - resumeFilePath: 'C:\\Users\\Ada Lovelace\\.omp\\agent\\sessions\\repo\\sess.jsonl', - cwd: 'C:\\Users\\Ada Lovelace\\repo', - platform: 'win32', - shell: 'powershell' - }) - ).toBe( - "Set-Location -LiteralPath 'C:\\Users\\Ada Lovelace\\repo'; omp --resume 'C:\\Users\\Ada Lovelace\\.omp\\agent\\sessions\\repo\\sess.jsonl'" - ) - }) - - it('falls back to the session id when no OMP transcript path is known', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'omp', - sessionId: '019f27cd-4268-7000-96e7-62f42a55c144', - resumeFilePath: null, - cwd: '/Users/ada/repo', - platform: 'darwin' - }) - ).toBe("cd '/Users/ada/repo' && omp --resume '019f27cd-4268-7000-96e7-62f42a55c144'") - }) - - it('resumes Prime Agent by absolute transcript path like OMP', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'prime-agent', - sessionId: 'dddddddd-eeee-4fff-8aaa-111111111111', - resumeFilePath: - '/Users/ada/.prime/agent/sessions/dddddddd-eeee-4fff-8aaa-111111111111.jsonl', - cwd: '/Users/ada/repo', - platform: 'darwin' - }) - ).toBe( - "cd '/Users/ada/repo' && prime-agent --resume '/Users/ada/.prime/agent/sessions/dddddddd-eeee-4fff-8aaa-111111111111.jsonl'" - ) - }) - - it('falls back to the session id when no Prime Agent transcript path is known', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'prime-agent', - sessionId: 'dddddddd-eeee-4fff-8aaa-111111111111', - resumeFilePath: null, - cwd: '/Users/ada/repo', - platform: 'darwin' - }) - ).toBe("cd '/Users/ada/repo' && prime-agent --resume 'dddddddd-eeee-4fff-8aaa-111111111111'") - }) - - it('resumes Rovo Dev through its acli launch command', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'rovo', - sessionId: 'rovo-session', - cwd: '/Users/ada/repo', - platform: 'darwin' - }) - ).toBe("cd '/Users/ada/repo' && acli rovodev run --restore 'rovo-session'") - }) - - it('appends the Rovo Dev restore flag to a full-command settings override', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'rovo', - sessionId: 'rovo-session', - cwd: null, - platform: 'darwin', - commandOverride: '/opt/acli/acli rovodev run' - }) - ).toBe("/opt/acli/acli rovodev run --restore 'rovo-session'") - }) - - it('resumes Muse by session id in the session cwd', () => { - expect( - buildAiVaultResumeCommand({ - agent: 'muse', - sessionId: 'eeeeeeee-ffff-4000-baaa-222222222222', - cwd: '/Users/ada/repo', - platform: 'darwin' - }) - ).toBe("cd '/Users/ada/repo' && muse resume 'eeeeeeee-ffff-4000-baaa-222222222222'") - }) -}) +import { buildAiVaultResumeShellCommand } from './ai-vault-resume-command' describe('buildAiVaultResumeShellCommand env removal', () => { const base = { @@ -204,12 +9,6 @@ describe('buildAiVaultResumeShellCommand env removal', () => { clearEnvNames: ['CODEX_HOME', 'ORCA_CODEX_HOME'] } - it('carries the removal on the agent under a POSIX shell', () => { - expect(buildAiVaultResumeShellCommand({ ...base, platform: 'darwin' })).toBe( - "cd '/repo' && env -u CODEX_HOME -u ORCA_CODEX_HOME codex 'resume' 'sid'" - ) - }) - // Filter assigned variables from env -u or the pinned home silently becomes real-home resume. it('keeps a pinned CODEX_HOME authoritative instead of stripping it', () => { const command = buildAiVaultResumeShellCommand({ diff --git a/src/shared/ai-vault-resume-preparation.test.ts b/src/shared/ai-vault-resume-preparation.test.ts deleted file mode 100644 index 3a50806987b..00000000000 --- a/src/shared/ai-vault-resume-preparation.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { isAiVaultPrepareSessionResumeUnavailableError } from './ai-vault-resume-preparation' - -describe('isAiVaultPrepareSessionResumeUnavailableError', () => { - it('recognizes deterministic old-host responses', () => { - expect( - isAiVaultPrepareSessionResumeUnavailableError({ - code: 'forbidden', - message: "Method 'aiVault.prepareSessionResume' is not available to mobile clients" - }) - ).toBe(true) - expect( - isAiVaultPrepareSessionResumeUnavailableError({ - code: 'method_not_found', - message: 'Unknown method: aiVault.prepareSessionResume' - }) - ).toBe(true) - }) - - it.each([ - { - code: 'forbidden', - message: "Method 'aiVault.listSessions' is not available to mobile clients" - }, - { code: 'forbidden', message: 'Session resume is forbidden' }, - { code: 'internal_error', message: 'Method not found while preparing session' } - ])('rejects a non-capability error: $code / $message', (error) => { - expect(isAiVaultPrepareSessionResumeUnavailableError(error)).toBe(false) - }) -}) diff --git a/src/shared/ai-vault-resume-target.test.ts b/src/shared/ai-vault-resume-target.test.ts index fbf4f7f0259..15efb6aa929 100644 --- a/src/shared/ai-vault-resume-target.test.ts +++ b/src/shared/ai-vault-resume-target.test.ts @@ -2,36 +2,11 @@ import { describe, expect, it } from 'vitest' import { canResumeAiVaultSessionOnTarget } from './ai-vault-resume-target' describe('ai vault session storage compatibility', () => { - const hostSessionFile = '/Users/ada/.claude/projects/-Users-ada-repo/session-1.jsonl' const windowsHostSessionFile = 'C:\\Users\\ada\\.claude\\projects\\C--repo\\session-1.jsonl' const wslSessionFile = '\\\\wsl$\\Ubuntu\\home\\ada\\.claude\\projects\\-home-ada-repo\\s-1.jsonl' const wslLocalhostSessionFile = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\.claude\\projects\\-home-ada-repo\\s-1.jsonl' - it('allows host-stored sessions on local targets only', () => { - expect( - canResumeAiVaultSessionOnTarget({ sessionFilePath: hostSessionFile, targetStatus: 'local' }) - ).toBe(true) - expect( - canResumeAiVaultSessionOnTarget({ sessionFilePath: hostSessionFile, targetStatus: 'ssh' }) - ).toBe(false) - expect( - canResumeAiVaultSessionOnTarget({ - sessionFilePath: windowsHostSessionFile, - targetStatus: 'ssh' - }) - ).toBe(false) - }) - - it('allows WSL-stored sessions on local and SSH targets', () => { - expect( - canResumeAiVaultSessionOnTarget({ sessionFilePath: wslSessionFile, targetStatus: 'local' }) - ).toBe(true) - expect( - canResumeAiVaultSessionOnTarget({ sessionFilePath: wslSessionFile, targetStatus: 'ssh' }) - ).toBe(true) - }) - // Native Windows and WSL are both local, but only the matching namespace contains the agent/session (#24408). it('resumes a local session only in the namespace that stores it', () => { const local = { sessionExecutionHostId: 'local', targetExecutionHostId: 'local' } as const @@ -87,17 +62,6 @@ describe('ai vault session storage compatibility', () => { ).toBe(false) }) - it('allows WSL-stored local sessions on any SSH target even with explicit host ids', () => { - expect( - canResumeAiVaultSessionOnTarget({ - sessionFilePath: wslSessionFile, - sessionExecutionHostId: 'local', - targetStatus: 'ssh', - targetExecutionHostId: 'ssh:dev-box' - }) - ).toBe(true) - }) - it('allows runtime sessions only on the matching runtime target', () => { expect( canResumeAiVaultSessionOnTarget({ @@ -123,10 +87,4 @@ describe('ai vault session storage compatibility', () => { }) ).toBe(false) }) - - it('never allows unknown targets', () => { - expect( - canResumeAiVaultSessionOnTarget({ sessionFilePath: hostSessionFile, targetStatus: 'unknown' }) - ).toBe(false) - }) }) diff --git a/src/shared/ai-vault-scan-error-message.test.ts b/src/shared/ai-vault-scan-error-message.test.ts deleted file mode 100644 index 45e3a3e8bf7..00000000000 --- a/src/shared/ai-vault-scan-error-message.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { describeAiVaultScanError } from './ai-vault-scan-error-message' - -describe('describeAiVaultScanError', () => { - it('replaces the restart-circuit wording with an actionable line', () => { - expect(describeAiVaultScanError('AI Vault service restart circuit is open.')).toBe( - 'Session scanning paused after repeated failures. Refresh to try again.' - ) - }) - - it('renders the scan timeout in seconds', () => { - expect(describeAiVaultScanError('AI Vault service timed out after 130000ms.')).toBe( - 'Session scan timed out after 130s — the machine may be busy. Refresh to try again.' - ) - }) - - it('keeps the SSH host in the timeout copy', () => { - expect( - describeAiVaultScanError( - 'Agent Session History scan timed out after 130000ms on this SSH host.' - ) - ).toBe('Session scan timed out after 130s on this SSH host. Refresh to try again.') - }) - - it('humanizes the relay-hosted variant of the same fault', () => { - expect(describeAiVaultScanError('Relay AI Vault service restart circuit is open.')).toBe( - 'Session scanning paused after repeated failures. Refresh to try again.' - ) - }) - - it('strips the Electron invoke wrapper before matching', () => { - expect( - describeAiVaultScanError( - "Error invoking remote method 'aiVault:listSessions': Error: AI Vault service did not become ready." - ) - ).toBe('The session scanner stopped unexpectedly. Refresh to try again.') - }) - - it.each([ - ['AI Vault service disconnected.', 'The session scanner stopped unexpectedly.'], - ['AI Vault service exited (null).', 'The session scanner stopped unexpectedly.'], - ['AI Vault scanner worker exited with code 1.', 'The session scanner stopped unexpectedly.'], - ['AI Vault service sent a malformed message.', 'The session scanner stopped unexpectedly.'] - ])('covers the supervision fault %s', (raw, expected) => { - expect(describeAiVaultScanError(raw)).toBe(`${expected} Refresh to try again.`) - }) - - it('routes a missing scanner entry to install guidance, not a retry', () => { - const described = describeAiVaultScanError('AI Vault service entry not found: /a/b.js') - expect(described).toBe( - 'The session scanner is missing from this Orca install. Reinstalling Orca restores it.' - ) - expect(described).not.toContain('Refresh') - }) - - it('never leaks internal vocabulary a user cannot act on', () => { - for (const raw of [ - 'AI Vault service restart circuit is open.', - 'AI Vault service timed out after 130000ms.', - 'AI Vault service queue is full.', - 'AI Vault service cache invalidation timed out.', - 'AI Vault service did not cancel within 2000ms.', - 'AI Vault service client was disposed.' - ]) { - const described = describeAiVaultScanError(raw) - expect(described).not.toMatch(/circuit|ms\.|AI Vault service|scanner worker/) - } - }) - - it('passes scanner-authored messages through untouched', () => { - const authored = 'Could not read /Users/x/.codex/sessions: EACCES' - expect(describeAiVaultScanError(authored)).toBe(authored) - expect(describeAiVaultScanError('SSH relay is not ready')).toBe('SSH relay is not ready') - }) - - it('removes transport wrappers before passing through an unknown message', () => { - expect( - describeAiVaultScanError( - "Error invoking remote method 'aiVault:listSessions': Error: SSH relay is not ready" - ) - ).toBe('SSH relay is not ready') - expect(describeAiVaultScanError('Relay AI Vault scanner is not ready')).toBe( - 'AI Vault scanner is not ready' - ) - expect(describeAiVaultScanError('Relay client is not connected')).toBe( - 'Relay client is not connected' - ) - }) -}) diff --git a/src/shared/ai-vault-scope-paths.test.ts b/src/shared/ai-vault-scope-paths.test.ts index c04d0d0b671..e3e82e4c02d 100644 --- a/src/shared/ai-vault-scope-paths.test.ts +++ b/src/shared/ai-vault-scope-paths.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from 'vitest' import type { Worktree } from './worktree/types' -import { - deriveAiVaultScopeSessionPaths, - deriveAiVaultWorkspaceScopePaths -} from './ai-vault-scope-paths' +import { deriveAiVaultWorkspaceScopePaths } from './ai-vault-scope-paths' function makeWorktree(overrides: Partial = {}): Worktree { return { @@ -29,19 +26,6 @@ function makeWorktree(overrides: Partial = {}): Worktree { } describe('deriveAiVaultWorkspaceScopePaths', () => { - it('includes prior paths so renamed workspaces keep their transcripts', () => { - const active = makeWorktree({ - id: 'repo-1::/repo/orca-renamed', - path: '/repo/orca-renamed', - priorWorktreeIds: ['repo-1::/repo/orca'] - }) - - expect(deriveAiVaultWorkspaceScopePaths(active, [active])).toEqual([ - '/repo/orca-renamed', - '/repo/orca' - ]) - }) - it('drops a claimed prior path regardless of where the claimant sits in the list', () => { // Exclude the active workspace before indexing owners so duplicates cannot hide later claimants. const active = makeWorktree({ @@ -64,52 +48,4 @@ describe('deriveAiVaultWorkspaceScopePaths', () => { ]) } }) - - it('ignores prior ids belonging to another repo', () => { - const active = makeWorktree({ priorWorktreeIds: ['repo-2::/repo/other'] }) - expect(deriveAiVaultWorkspaceScopePaths(active, [active])).toEqual(['/repo/orca']) - }) - - it('skips relative and blank paths', () => { - const active = makeWorktree({ path: 'relative/path' }) - expect(deriveAiVaultWorkspaceScopePaths(active, [active])).toEqual([]) - expect(deriveAiVaultWorkspaceScopePaths(makeWorktree({ path: ' ' }), [])).toEqual([]) - }) -}) - -describe('deriveAiVaultScopeSessionPaths', () => { - it('covers the active workspace plus the rest of its repo', () => { - const active = makeWorktree() - const sibling = makeWorktree({ id: 'repo-1::/repo/feature', path: '/repo/feature' }) - const otherRepo = makeWorktree({ - id: 'repo-2::/repo/other', - repoId: 'repo-2', - path: '/repo/other' - }) - - expect(deriveAiVaultScopeSessionPaths(active, [active, sibling, otherRepo])).toEqual([ - '/repo/orca', - '/repo/feature' - ]) - }) - - it('deduplicates paths that differ only by separators or trailing slash', () => { - // The dedupe compares normalized keys; the first spelling is what ships. - const active = makeWorktree() - const trailing = makeWorktree({ id: 'repo-1::a', path: '/repo/orca/' }) - const doubled = makeWorktree({ id: 'repo-1::b', path: '/repo//orca' }) - - expect(deriveAiVaultScopeSessionPaths(active, [active, trailing, doubled])).toEqual([ - '/repo/orca' - ]) - }) - - it('treats NFD and NFC spellings of one path as the same scope entry', () => { - // macOS yields NFD on disk while agents record NFC cwds (#10832). - const nfc = '/repo/프로젝트' - const active = makeWorktree({ id: `repo-1::${nfc}`, path: nfc }) - const nfd = makeWorktree({ id: 'repo-1::nfd', path: nfc.normalize('NFD') }) - - expect(deriveAiVaultScopeSessionPaths(active, [active, nfd])).toEqual([nfc]) - }) }) diff --git a/src/shared/ai-vault-search-agent-history.test.ts b/src/shared/ai-vault-search-agent-history.test.ts index 96ac110ed16..5a520f4f678 100644 --- a/src/shared/ai-vault-search-agent-history.test.ts +++ b/src/shared/ai-vault-search-agent-history.test.ts @@ -6,11 +6,6 @@ import { AI_VAULT_AGENTS } from './ai-vault-types' describe.each(['qoder', 'jcode'] as const)('%s search negotiation', (agent) => { const supportField = agent === 'qoder' ? 'supportsQoderHistory' : 'supportsJcodeHistory' - it('keeps the frozen old parser closed to the requested agent', () => { - expect( - LegacyRequestSchema.safeParse({ query: 'q', filters: { agents: [agent] } }).success - ).toBe(false) - }) it.each(['runtime', 'relay'] as const)( 'sends no search request for an unsupported agent over %s', @@ -97,43 +92,6 @@ describe.each(['qoder', 'jcode'] as const)('%s search negotiation', (agent) => { } ) - it('does not infer support from the other agent capability', async () => { - const otherField = agent === 'qoder' ? 'supportsJcodeHistory' : 'supportsQoderHistory' - const call = vi.fn(async (method: string) => - method === 'aiVault.searchStatus' - ? { ...unavailableSessionSearchStatus(), [otherField]: true } - : searchResults() - ) - await createSessionSearchClient(call, 'relay').searchSessions({ - query: 'q', - filters: { agents: [agent, 'codex'] } - }) - expect(call).toHaveBeenLastCalledWith('aiVault.searchSessions', { - query: 'q', - limit: 20, - filters: { agents: ['codex'] }, - supportedAgents: [...AI_VAULT_AGENTS], - supportsQoderHistory: true, - supportsJcodeHistory: true - }) - }) - - it('does not gate local IPC before the per-host aggregator negotiates', async () => { - const call = vi.fn(async () => searchResults()) - await createSessionSearchClient(call, 'ipc').searchSessions({ - query: 'q', - filters: { agents: [agent] } - }) - expect(call).toHaveBeenCalledExactlyOnceWith('aiVault.searchSessions', { - query: 'q', - limit: 20, - filters: { agents: [agent] }, - supportedAgents: [...AI_VAULT_AGENTS], - supportsQoderHistory: true, - supportsJcodeHistory: true - }) - }) - it('does not dispatch a search when the capability probe loses host contact', async () => { const call = vi.fn(async () => { throw new Error('host disconnected') @@ -149,23 +107,6 @@ describe.each(['qoder', 'jcode'] as const)('%s search negotiation', (agent) => { }) describe('explicit search host vocabulary', () => { - it.each(['runtime', 'relay'] as const)( - 'does not send sole Jcode to a host that only attests Qoder over %s', - async (transport) => { - const call = vi.fn(async () => ({ - ...unavailableSessionSearchStatus(), - supportsQoderHistory: true - })) - expect( - await createSessionSearchClient(call, transport).searchSessions({ - query: 'q', - filters: { agents: ['jcode'] } - }) - ).toEqual({ kind: 'unavailable', reason: 'unsupported-agent' }) - expect(call).toHaveBeenCalledExactlyOnceWith('aiVault.searchStatus', {}) - } - ) - it.each(['runtime', 'relay'] as const)( 'retains current Jcode support even with no indexed sessions over %s', async (transport) => { diff --git a/src/shared/ai-vault-search-contract.test.ts b/src/shared/ai-vault-search-contract.test.ts index cf5d6694de3..36772d2733d 100644 --- a/src/shared/ai-vault-search-contract.test.ts +++ b/src/shared/ai-vault-search-contract.test.ts @@ -1,84 +1,10 @@ import { describe, expect, it } from 'vitest' -import { - AiVaultSearchRequestSchema, - AiVaultSearchResponseSchema, - AiVaultSearchStatusSchema -} from './ai-vault-search-contract' +import { AiVaultSearchResponseSchema, AiVaultSearchStatusSchema } from './ai-vault-search-contract' import { searchHit, searchResults } from './ai-vault-search-test-fixture' import { redactForTransport, redactStatusForTransport } from './ai-vault-search-transport' import { unavailableSessionSearchStatus } from './ai-vault-search-client' describe('session search public contract', () => { - it('drops legacy fields without letting them override scope or freshness', () => { - expect( - AiVaultSearchRequestSchema.parse({ query: 'needle', tier: 'conversation', refresh: true }) - ).toEqual({ query: 'needle', limit: 20 }) - }) - it.each([ - [0, 1], - [-4, 1], - [200, 100], - [2.5, 20], - [undefined, 20] - ])('clamps %s to %s', (limit, expected) => { - expect(AiVaultSearchRequestSchema.parse({ query: 'needle', limit }).limit).toBe(expected) - }) - it.each(['2026-10-12T00:00:00Z', '2026-10-12T08:00:00+08:00', '2026-10-11T16:00:00-08:00'])( - 'reads since %s as the stored UTC instant', - (since) => { - expect(AiVaultSearchRequestSchema.parse({ query: 'q', filters: { since } }).filters).toEqual({ - since: '2026-10-12T00:00:00.000Z' - }) - } - ) - it('preserves long query text in the request schema', () => { - const query = 'needle '.repeat(100) - expect(AiVaultSearchRequestSchema.parse({ query }).query).toBe(query) - }) - it('validates every response variant and separate status', () => { - for (const response of [ - searchResults(), - { kind: 'malformed-cursor' }, - { kind: 'stale-cursor', generation: 2, expectedGeneration: 1 }, - ...['disabled', 'not-ready', 'no-service'].map((reason) => ({ kind: 'unavailable', reason })) - ]) { - expect(AiVaultSearchResponseSchema.parse(response)).toEqual(response) - } - expect(AiVaultSearchStatusSchema.parse(unavailableSessionSearchStatus())).toEqual( - unavailableSessionSearchStatus() - ) - expect(AiVaultSearchResponseSchema.safeParse({ kind: 'results', hits: [] }).success).toBe(false) - }) - it('keeps host attribution optional in both wire directions', () => { - const legacy = searchResults() - expect(AiVaultSearchResponseSchema.parse(legacy)).toEqual(legacy) - expect(legacy.hits[0]).not.toHaveProperty('executionHostId') - const attributed = { - ...searchResults(), - hits: [{ ...searchHit(), executionHostId: 'runtime:env-1' }] - } - expect(AiVaultSearchResponseSchema.parse(attributed)).toEqual(attributed) - expect( - AiVaultSearchResponseSchema.safeParse({ - ...searchResults(), - hits: [{ ...searchHit(), executionHostId: '' }] - }).success - ).toBe(false) - }) - it('accepts future agent names only in optional capability lists', () => { - const supportedAgents = ['codex', 'jcode', 'future-agent'] - expect( - AiVaultSearchRequestSchema.parse({ query: 'q', supportedAgents }).supportedAgents - ).toEqual(supportedAgents) - expect( - AiVaultSearchStatusSchema.parse({ ...unavailableSessionSearchStatus(), supportedAgents }) - .supportedAgents - ).toEqual(supportedAgents) - expect( - AiVaultSearchRequestSchema.safeParse({ query: 'q', filters: { agents: ['future-agent'] } }) - .success - ).toBe(false) - }) it('never accepts resume commands for an unverified or missing source', () => { for (const presence of ['unverifiable', 'missing'] as const) { const response = searchResults() diff --git a/src/shared/ai-vault-search-query-operators.test.ts b/src/shared/ai-vault-search-query-operators.test.ts deleted file mode 100644 index 26b282296d2..00000000000 --- a/src/shared/ai-vault-search-query-operators.test.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { parseVaultQuery } from './ai-vault-session-filters' -import { - hasAiVaultSearchQueryOperators, - splitAiVaultSearchQuery -} from './ai-vault-search-query-operators' - -describe('what counts as an operator', () => { - it('splits repo: and path: out of the free text', () => { - const split = splitAiVaultSearchQuery('relay capacity repo:orca path:/work/app') - expect(split.text).toBe('relay capacity') - expect(split.terms).toEqual(['relay', 'capacity']) - expect(split.repoTerms).toEqual(['orca']) - expect(split.pathTerms).toEqual(['/work/app']) - expect(hasAiVaultSearchQueryOperators(split)).toBe(true) - }) - - it('keeps a value that only looks like an operator as ordinary text', () => { - const split = splitAiVaultSearchQuery('myrepo:x https://host/path:y') - expect(split.repoTerms).toEqual([]) - expect(split.pathTerms).toEqual([]) - expect(split.text).toBe('myrepo:x https://host/path:y') - }) - - it('reads a quoted operator value whole, including its spaces', () => { - expect(splitAiVaultSearchQuery('path:"/Users/ada/My Project" needle').pathTerms).toEqual([ - '/Users/ada/My Project' - ]) - }) - - it('does not let an apostrophe in prose swallow the operator between quotes', () => { - const split = splitAiVaultSearchQuery("it's a repo:orca thing's") - expect(split.repoTerms).toEqual(['orca']) - }) - - it('preserves operator case, which the panel folds and the index must not', () => { - // Keep execution-host case in cwd_key so distinct POSIX directories remain searchable. - expect(splitAiVaultSearchQuery('path:/Work/App').pathTerms).toEqual(['/Work/App']) - expect(parseVaultQuery('path:/Work/App').pathTerms).toEqual(['/work/app']) - }) - - it('has no operators when the query is plain text', () => { - expect(hasAiVaultSearchQueryOperators(splitAiVaultSearchQuery('relay capacity'))).toBe(false) - }) -}) - -// Pin intentional differences from the former panel tokenizer; the shared parser now guarantees panel/index agreement. -describe('the shapes where the panel parser used to answer differently', () => { - it.each([ - ['repo:"" x', 'repoTerms'], - ['path:"" x', 'pathTerms'] - ] as const)('drops the empty operator value in %s instead of filtering on `""`', (query, key) => { - // Drop empty operators; filtering on a quoted empty label once silently emptied the list. - expect(splitAiVaultSearchQuery(query)[key]).toEqual([]) - expect(parseVaultQuery(query)[key]).toEqual([]) - }) - - it.each([ - ['repo:" " x', 'repoTerms'], - ['path:" " x', 'pathTerms'] - ] as const)('drops the whitespace-only operator value in %s too', (query, key) => { - // Whitespace-only operator values also narrow nothing; retaining spaces once emptied results. - expect(splitAiVaultSearchQuery(query)[key]).toEqual([]) - expect(parseVaultQuery(query)[key]).toEqual([]) - }) - - it('trims a quoted operator value rather than searching for the spaces', () => { - expect(splitAiVaultSearchQuery('repo:" session-search "').repoTerms).toEqual(['session-search']) - }) - - it.each(['"" empty', "'' empty", '" " empty'])( - 'reads the empty quotes in %s as an empty term', - (query) => { - // Empty free-text terms match everything instead of searching literal quotes. - expect(parseVaultQuery(query).terms).toEqual(['', 'empty']) - } - ) - - it.each([ - ['"foo"bar', { terms: ['foo', 'bar'], repoTerms: [], pathTerms: [] }], - ['"a b"c', { terms: ['a b', 'c'], repoTerms: [], pathTerms: [] }], - ['repo:"a"b', { terms: ['b'], repoTerms: ['a'], pathTerms: [] }], - ['path:"a"b', { terms: ['b'], repoTerms: [], pathTerms: ['a'] }], - ['repo:"a b"c d', { terms: ['c', 'd'], repoTerms: ['a b'], pathTerms: [] }] - ])('reads %s exactly as the panel always has', (query, expected) => { - // Closing quotes may end mid-word; token-start rules protect apostrophes without retaining literal quotes in search terms. - expect(parseVaultQuery(query)).toEqual(expected) - }) -}) - -describe('agrees with the sessions panel parser on operator recognition', () => { - it.each([ - 'relay capacity', - 'repo:orca needle', - 'path:/work/app needle', - 'myrepo:x', - 'needle repo:orca path:/work/app', - 'path:"/Users/ada/My Project"', - 'https://host/path:y' - ])('reads the same operators out of %s', (query) => { - const split = splitAiVaultSearchQuery(query) - const parsed = parseVaultQuery(query) - const fold = (values: readonly string[]): string[] => values.map((v) => v.toLowerCase()).sort() - expect(fold(split.repoTerms)).toEqual(fold(parsed.repoTerms)) - expect(fold(split.pathTerms)).toEqual(fold(parsed.pathTerms)) - }) -}) diff --git a/src/shared/ai-vault-search-scope-contract.test.ts b/src/shared/ai-vault-search-scope-contract.test.ts index e2d5d9ddf02..c6a25c3d009 100644 --- a/src/shared/ai-vault-search-scope-contract.test.ts +++ b/src/shared/ai-vault-search-scope-contract.test.ts @@ -1,28 +1,8 @@ import { describe, expect, it } from 'vitest' -import { AiVaultSearchRequestSchema, AiVaultSearchResponseSchema } from './ai-vault-search-contract' -import { searchResults } from './ai-vault-search-test-fixture' +import { AiVaultSearchRequestSchema } from './ai-vault-search-contract' +import './ai-vault-search-test-fixture' describe('scope identity on the search request', () => { - it('accepts a workspace identity', () => { - expect( - AiVaultSearchRequestSchema.parse({ - query: 'needle', - within: { kind: 'workspace', worktreeId: 'repo-1::/work/app' } - }).within - ).toEqual({ kind: 'workspace', worktreeId: 'repo-1::/work/app' }) - }) - - it('accepts a project identity under either key spelling', () => { - for (const projectKey of ['repo:repo-1', 'project:proj-1']) { - expect( - AiVaultSearchRequestSchema.parse({ - query: 'needle', - within: { kind: 'project', projectKey } - }).within - ).toEqual({ kind: 'project', projectKey }) - } - }) - it('refuses an identity sent alongside explicit scope paths', () => { expect(() => AiVaultSearchRequestSchema.parse({ @@ -33,33 +13,9 @@ describe('scope identity on the search request', () => { ).toThrow(/either a scope identity or explicit scope paths/) }) - it('still accepts explicit scope paths with no identity, which is what --path sends', () => { - expect( - AiVaultSearchRequestSchema.parse({ query: 'needle', filters: { scopePaths: ['/work/app'] } }) - .filters - ).toEqual({ scopePaths: ['/work/app'] }) - }) - - it('still accepts the old shape that carries neither', () => { - expect(AiVaultSearchRequestSchema.parse({ query: 'needle' })).toEqual({ - query: 'needle', - limit: 20 - }) - }) - it('refuses an unknown identity kind rather than dropping the narrowing', () => { expect(() => AiVaultSearchRequestSchema.parse({ query: 'needle', within: { kind: 'repo', repoId: 'r' } }) ).toThrow() }) - - it('carries the scope-unknown answer and per-host outcome on the response', () => { - expect( - AiVaultSearchResponseSchema.parse({ kind: 'unavailable', reason: 'scope-unknown' }) - ).toEqual({ kind: 'unavailable', reason: 'scope-unknown' }) - const hosts = [{ executionHostId: 'local', outcome: 'scope-unknown' }] - expect(AiVaultSearchResponseSchema.parse({ ...searchResults(), hosts })).toMatchObject({ - hosts - }) - }) }) diff --git a/src/shared/ai-vault-search-settings.test.ts b/src/shared/ai-vault-search-settings.test.ts deleted file mode 100644 index ba29cbf929b..00000000000 --- a/src/shared/ai-vault-search-settings.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { expect, it } from 'vitest' -import { - AiVaultSearchSettingsSchema, - resolveAiVaultSearchSettings, - sameAiVaultSearchSettings -} from './ai-vault-search-settings' - -// Default off: indexing reads all machine transcripts and requires recorded opt-in. -it('reads anything that is not an explicit opt-in as off', () => { - expect(resolveAiVaultSearchSettings(undefined)).toEqual({ enabled: false, historyDays: null }) - expect(resolveAiVaultSearchSettings({})).toEqual({ enabled: false, historyDays: null }) - expect(resolveAiVaultSearchSettings({ aiVaultSearch: null })).toEqual({ - enabled: false, - historyDays: null - }) - expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: 'yes' } })).toEqual({ - enabled: false, - historyDays: null - }) - expect(resolveAiVaultSearchSettings({ aiVaultSearch: 'on' })).toEqual({ - enabled: false, - historyDays: null - }) -}) - -it('normalizes a history bound and drops anything that is not one', () => { - expect( - resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: true, historyDays: 30.7 } }) - ).toEqual({ enabled: true, historyDays: 30 }) - // Fractional days floor to zero, otherwise disagreeing as all-history versus cutoff-now. - for (const historyDays of [0.4, 0, -30, Number.NaN] as const) { - expect(resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: true, historyDays } })).toEqual( - { enabled: true, historyDays: null } - ) - } - expect( - resolveAiVaultSearchSettings({ aiVaultSearch: { enabled: true, historyDays: 999_999 } }) - ).toEqual({ enabled: true, historyDays: 3_650 }) -}) - -// No paused lifecycle: immutable indexers would otherwise need a second store/queue/sweep lifetime. -it('keeps only the two fields the indexer is constructed from', () => { - expect( - resolveAiVaultSearchSettings({ - aiVaultSearch: { enabled: true, historyDays: 90, paused: true } - }) - ).toEqual({ enabled: true, historyDays: 90 }) -}) - -it('accepts what it produces and refuses what it does not', () => { - expect(AiVaultSearchSettingsSchema.parse({ enabled: true, historyDays: 90 })).toEqual({ - enabled: true, - historyDays: 90 - }) - expect(AiVaultSearchSettingsSchema.safeParse({ enabled: true, historyDays: 0 }).success).toBe( - false - ) - expect(AiVaultSearchSettingsSchema.safeParse({ historyDays: null }).success).toBe(false) -}) - -it('compares both enabled state and history bounds', () => { - expect( - sameAiVaultSearchSettings( - { enabled: true, historyDays: 30 }, - { enabled: true, historyDays: 30 } - ) - ).toBe(true) - expect( - sameAiVaultSearchSettings( - { enabled: true, historyDays: 30 }, - { enabled: true, historyDays: 90 } - ) - ).toBe(false) - expect( - sameAiVaultSearchSettings( - { enabled: true, historyDays: null }, - { enabled: false, historyDays: null } - ) - ).toBe(false) -}) diff --git a/src/shared/ai-vault-search-status-fallback.test.ts b/src/shared/ai-vault-search-status-fallback.test.ts index e01d7405e2e..00cbcecbce4 100644 --- a/src/shared/ai-vault-search-status-fallback.test.ts +++ b/src/shared/ai-vault-search-status-fallback.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from 'vitest' -import { createSessionSearchClient, unavailableSessionSearchStatus } from './ai-vault-search-client' +import { createSessionSearchClient } from './ai-vault-search-client' import { AiVaultSearchRequestSchema as LegacyRequestSchema } from './__fixtures__/pre-qoder-search-request' import { searchResults } from './ai-vault-search-test-fixture' @@ -71,24 +71,6 @@ describe.each(['runtime', 'relay'] as const)('missing search status over %s', (t expect(call).toHaveBeenCalledExactlyOnceWith('aiVault.searchStatus', {}) }) - it.each([{ supportedAgents: [] }, { supportedAgents: ['future-agent'] }])( - 'honors explicit narrow host vocabulary $supportedAgents', - async ({ supportedAgents }) => { - const call = vi.fn(async (method: string) => - method === 'aiVault.searchStatus' - ? { ...unavailableSessionSearchStatus(), supportedAgents } - : searchResults() - ) - expect( - await createSessionSearchClient(call, transport).searchSessions({ - query: 'needle', - filters: { agents: ['claude'] } - }) - ).toEqual({ kind: 'unavailable', reason: 'unsupported-agent' }) - expect(call).toHaveBeenCalledExactlyOnceWith('aiVault.searchStatus', {}) - } - ) - it.each([ { code: 'unauthorized', message: 'Authentication required' }, { code: 'connection_closed', message: 'Host disconnected' }, diff --git a/src/shared/ai-vault-session-cwd-candidates.test.ts b/src/shared/ai-vault-session-cwd-candidates.test.ts deleted file mode 100644 index 152cf49de9f..00000000000 --- a/src/shared/ai-vault-session-cwd-candidates.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { expect, it, vi } from 'vitest' -import { matchAiVaultSessionCwdCandidates } from './ai-vault-session-cwd-candidates' -import { createNormalizedPathInsideOrEqualMatcher } from './cross-platform-path' - -it('keeps alias matching linear in candidate roots at the maximum host alias count', () => { - const session = { - filePath: '/session.jsonl', - cwdAliases: Array.from({ length: 64 }, (_, i) => `/alias-${i}/child`) - } - for (const size of [100, 1000]) { - const owns = vi.fn(createNormalizedPathInsideOrEqualMatcher('/alias-63')) - const candidates = Array.from({ length: size }, () => ({ - normalizedPath: '/alias-63', - ownsNormalizedCwd: owns - })) - expect( - matchAiVaultSessionCwdCandidates(session, '/physical/child', candidates, () => 0) - ).toHaveLength(size) - expect(owns).toHaveBeenCalledTimes(size * 65) - } -}) diff --git a/src/shared/ai-vault-session-depth.test.ts b/src/shared/ai-vault-session-depth.test.ts index fa57e758bef..de58088fe03 100644 --- a/src/shared/ai-vault-session-depth.test.ts +++ b/src/shared/ai-vault-session-depth.test.ts @@ -1,11 +1,6 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import type { AiVaultListResult, AiVaultSession } from './ai-vault-types' -import { - aiVaultScanLimit, - aiVaultSessionDepthCovers, - requestedAiVaultSessionDepth, - truncateAiVaultListResult -} from './ai-vault-session-depth' +import { truncateAiVaultListResult } from './ai-vault-session-depth' function session(id: string, cwd: string, index: number): AiVaultSession { const timestamp = new Date(Date.UTC(2026, 7, 2, 0, 0, index)).toISOString() @@ -38,32 +33,6 @@ function result(sessions: AiVaultSession[]): AiVaultListResult { } describe('Agent Session History depth', () => { - it('recognizes which loaded depths cover a request', () => { - expect(aiVaultSessionDepthCovers(250, 250)).toBe(true) - expect(aiVaultSessionDepthCovers(1000, 250)).toBe(true) - expect(aiVaultSessionDepthCovers(250, 500)).toBe(false) - expect(aiVaultSessionDepthCovers(1000, 'unlimited')).toBe(false) - expect(aiVaultSessionDepthCovers('unlimited', 1000)).toBe(true) - expect(aiVaultSessionDepthCovers('unlimited', 'unlimited')).toBe(true) - }) - - it('normalizes default, finite, and unlimited requests', () => { - expect(requestedAiVaultSessionDepth()).toBe(1000) - expect(requestedAiVaultSessionDepth({ limit: 500 })).toBe(500) - expect(requestedAiVaultSessionDepth({ limit: 500.75 })).toBe(500) - expect(requestedAiVaultSessionDepth({ limit: 0 })).toBe(1000) - expect(requestedAiVaultSessionDepth({ limit: -1 })).toBe(1000) - expect(requestedAiVaultSessionDepth({ limit: Number.NaN })).toBe(1000) - expect(requestedAiVaultSessionDepth({ limit: Number.POSITIVE_INFINITY })).toBe(1000) - expect(requestedAiVaultSessionDepth({ limit: 500, unlimited: true })).toBe('unlimited') - }) - - it('resolves scan limits to a numeric bound', () => { - expect(aiVaultScanLimit({ limit: 500 })).toBe(500) - expect(aiVaultScanLimit({ limit: Number.POSITIVE_INFINITY })).toBe(1000) - expect(aiVaultScanLimit({ unlimited: true })).toBe(Number.POSITIVE_INFINITY) - }) - it('keeps every scoped session and the newest out-of-scope ones when truncating', () => { const loaded = result([ session('global-1', '/other', 6), @@ -87,59 +56,6 @@ describe('Agent Session History depth', () => { ]) expect(truncateAiVaultListResult(loaded, 'unlimited')).toBe(loaded) }) - - // Repeat opens served from a cache used to drop to `depth` scoped rows (341 -> 250 in QA). - it('returns a scan result with more scoped rows than the depth unchanged', () => { - const scanned = result([ - ...Array.from({ length: 3 }, (_, i) => session(`global-${i}`, '/other', 100 - i)), - ...Array.from({ length: 5 }, (_, i) => session(`codex-${i}`, '/repo', 50 - i)), - ...Array.from({ length: 4 }, (_, i) => session(`claude-${i}`, '/repo/app', 40 - i)) - ]) - - const cachedHit = truncateAiVaultListResult(scanned, 3, ['/repo']) - expect(cachedHit.sessions).toEqual(scanned.sessions) - expect(truncateAiVaultListResult(cachedHit, 3, ['/repo']).sessions).toEqual(scanned.sessions) - }) -}) - -describe('Agent Session History scope truncation', () => { - it('normalizes scope roots and candidate cwd once per truncation pass', () => { - const loaded = result( - Array.from({ length: 1000 }, (_, i) => session(`id-${i}`, `/other/${i}`, i)) - ) - const scopes = Array.from({ length: 100 }, (_, i) => `/repo/${i}`) - const normalize = vi.spyOn(String.prototype, 'normalize') - let selected: AiVaultListResult - try { - selected = truncateAiVaultListResult(loaded, 10, scopes) - expect(normalize.mock.calls.length).toBeLessThanOrEqual(1100) - } finally { - normalize.mockRestore() - } - expect(selected.sessions).toEqual(loaded.sessions.slice(0, 10)) - }) - - it('selects scoped sessions across host path spellings', () => { - const cases: [scope: string, cwd: string, kept: boolean][] = [ - ['C:\\Users\\Ada\\repo', 'c:/users/ada/repo/app', true], - ['C:/Users/Ada/repo/', 'C:\\Users\\Ada\\repo', true], - ['C:\\Users\\Ada\\repo', 'C:\\Users\\Ada\\repo-other', false], - ['//wsl.localhost/Ubuntu/home/Ada/Repo', '//wsl$/Ubuntu/home/Ada/Repo/App', true], - ['//wsl.localhost/Ubuntu/home/ada/repo', '//wsl.localhost/Debian/home/ada/repo', false], - ['/Users/ada/repo', '/Users/ada/repo//app/', true], - ['/Users/ada/repo', '/Users/ada/repository', false], - ['/', '/anywhere', true] - ] - for (const [scope, cwd, expected] of cases) { - const loaded = result([session('scoped', cwd!, 0)]) - const selected = truncateAiVaultListResult(loaded, 0, [scope!]) - expect({ scope, cwd, kept: selected.sessions.length === 1 }).toEqual({ - scope, - cwd, - kept: expected - }) - } - }) }) describe('truncateAiVaultListResult symlinked scope', () => { diff --git a/src/shared/ai-vault-session-filters.test.ts b/src/shared/ai-vault-session-filters.test.ts index 8cdf08b68de..d622385b1b9 100644 --- a/src/shared/ai-vault-session-filters.test.ts +++ b/src/shared/ai-vault-session-filters.test.ts @@ -1,14 +1,7 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import type { AiVaultSession } from './ai-vault-types' -import { - agentLabel, - filterAiVaultSessions, - folderGroupKey, - folderLabel, - groupAiVaultSessions, - parseVaultQuery -} from './ai-vault-session-filters' -import { sessionPreviewSearchText } from './ai-vault-session-display' +import { filterAiVaultSessions, groupAiVaultSessions } from './ai-vault-session-filters' +import './ai-vault-session-display' const baseSession: AiVaultSession = { id: 'claude:1', @@ -78,95 +71,6 @@ describe('/shared ai-vault-session-filters (lifted core)', () => { ).toEqual(['claude:1']) }) - it('hides empty sessions by default and keeps non-empty ones', () => { - // Keep preview/recovery evidence even without counted turns; only genuinely empty sessions disappear. - const empty: AiVaultSession = { - ...baseSession, - id: 'claude:empty', - messageCount: 0, - previewMessages: [], - queuedMessageCount: 0, - subagentTranscriptCount: 0 - } - expect( - filterAiVaultSessions([baseSession, empty], { - query: '', - agents: ['claude'], - scope: 'all', - sort: 'updated', - activeWorktreePaths: [], - hideEmptySessions: true - }).map((session) => session.id) - ).toEqual(['claude:1']) - }) - - it('groups by folder', () => { - const groups = groupAiVaultSessions([baseSession, otherSession], 'folder') - expect(groups.map((group) => group.label).sort()).toEqual(['packages/ui', 'repo/app']) - }) - - it('folds trailing-slash and repeated-slash cwd spellings into one folder group', () => { - const groups = groupAiVaultSessions( - [ - { ...baseSession, cwd: '/Users/ada/repo/app' }, - { ...baseSession, id: 'claude:2', cwd: '/Users/ada/repo/app/' }, - { ...baseSession, id: 'claude:3', cwd: '/Users/ada//repo/app//' } - ], - 'folder' - ) - expect(groups).toHaveLength(1) - expect(groups[0].sessions).toHaveLength(3) - expect(groups[0].label).toBe('repo/app') - }) - - it('folds NFD and NFC cwd spellings into one folder group with an NFC label', () => { - const groups = groupAiVaultSessions( - [ - { ...baseSession, cwd: '/Users/ada/Café/app'.normalize('NFD') }, - { ...baseSession, id: 'claude:2', cwd: '/Users/ada/Café/app'.normalize('NFC') } - ], - 'folder' - ) - expect(groups).toHaveLength(1) - expect(groups[0].sessions).toHaveLength(2) - expect(groups[0].label).toBe('Café/app'.normalize('NFC')) - }) - - it('folds separator and case variants of one Windows folder', () => { - const groups = groupAiVaultSessions( - [ - { ...baseSession, cwd: 'C:\\Users\\Ada\\repo\\app' }, - { ...baseSession, id: 'claude:2', cwd: 'c:/users/ada/repo/app/' } - ], - 'folder' - ) - expect(groups).toHaveLength(1) - expect(groups[0].sessions).toHaveLength(2) - }) - - it('folds the two WSL UNC aliases of one folder into a single group', () => { - const groups = groupAiVaultSessions( - [ - { ...baseSession, cwd: '//wsl.localhost/Ubuntu/home/ada/repo/app' }, - { ...baseSession, id: 'claude:2', cwd: '//wsl$/Ubuntu/home/ada/repo/app' } - ], - 'folder' - ) - expect(groups).toHaveLength(1) - expect(groups[0].sessions.map((session) => session.id)).toEqual(['claude:1', 'claude:2']) - }) - - it('keeps case-distinct POSIX folders in separate groups', () => { - const groups = groupAiVaultSessions( - [ - { ...baseSession, cwd: '/home/ada/Foo' }, - { ...baseSession, id: 'claude:2', cwd: '/home/ada/foo' } - ], - 'folder' - ) - expect(groups).toHaveLength(2) - }) - it('folds the project-grouping fallback onto the resolved folder project key', () => { const resolved = { ...baseSession, cwd: '/Users/ada/repo/app' } const unresolved = { ...baseSession, id: 'claude:2', cwd: '/Users/ada/repo/app/' } @@ -182,64 +86,9 @@ describe('/shared ai-vault-session-filters (lifted core)', () => { expect(groups).toHaveLength(1) expect(groups[0].sessions).toHaveLength(2) }) - - it('keys unknown cwd separately from real folders', () => { - expect(folderGroupKey(null)).toBe('unknown') - expect(folderGroupKey('/Users/ada/repo/app')).toBe('folder:/Users/ada/repo/app') - }) - - it('parses repo: and path: operators from the query', () => { - expect(parseVaultQuery('hello repo:orca path:/tmp world')).toEqual({ - terms: ['hello', 'world'], - repoTerms: ['orca'], - pathTerms: ['/tmp'] - }) - }) - - it('parses quoted repo:/path: operator values containing spaces', () => { - expect(parseVaultQuery('repo:"my repo" path:"/Users/ada/My Project"')).toEqual({ - terms: [], - repoTerms: ['my repo'], - pathTerms: ['/users/ada/my project'] - }) - }) - - it('exposes a stable agent label and folder label', () => { - expect(agentLabel('claude')).toBe('Claude') - expect(folderLabel('/Users/ada/repo/app')).toBe('repo/app') - }) - - it('builds preview search text from conversation turns', () => { - expect(sessionPreviewSearchText(baseSession)).toContain('scope tabs') - }) }) describe('/shared ai-vault-session-filters (hoisted matchers and sort keys)', () => { - it('prepares workspace path matching once for a large session filter pass', () => { - const sessions = Array.from({ length: 1000 }, (_, i) => ({ - ...baseSession, - id: String(i), - cwd: `/other/${i}` - })) - const activeWorktreePaths = Array.from({ length: 100 }, (_, i) => `/repo/${i}`) - const normalize = vi.spyOn(String.prototype, 'normalize') - try { - expect( - filterAiVaultSessions(sessions, { - query: '', - agents: ['claude'], - scope: 'workspace', - sort: 'updated', - activeWorktreePaths, - hideEmptySessions: false - }) - ).toEqual([]) - expect(normalize.mock.calls.length).toBeLessThanOrEqual(1100) - } finally { - normalize.mockRestore() - } - }) - it('does not read transcript previews for empty or field-only queries', () => { let reads = 0 const sessions = Array.from({ length: 1000 }, (_, i) => ({ @@ -276,40 +125,6 @@ describe('/shared ai-vault-session-filters (hoisted matchers and sort keys)', () expect(reads).toBeGreaterThan(0) }) - it('parses each sort timestamp once with original ordering, including invalid dates', () => { - const sessions = Array.from({ length: 2000 }, (_, i) => ({ - ...baseSession, - id: String(i), - updatedAt: - i % 131 === 0 - ? 'invalid' - : new Date(1700000000000 + ((i * 173) % 1999) * 1000).toISOString() - })) - const parse = vi.spyOn(Date, 'parse') - let actual: AiVaultSession[] - let expected: AiVaultSession[] - try { - expected = [...sessions].sort( - (a, b) => Date.parse(b.updatedAt ?? b.modifiedAt) - Date.parse(a.updatedAt ?? a.modifiedAt) - ) - expect(parse.mock.calls.length).toBeGreaterThan(10_000) - parse.mockClear() - actual = filterAiVaultSessions(sessions, { - query: '', - agents: ['claude'], - scope: 'all', - sort: 'updated', - activeWorktreePaths: [], - hideEmptySessions: false - }) - expect(parse).toHaveBeenCalledTimes(2000) - } finally { - parse.mockRestore() - } - expect(actual).toHaveLength(expected.length) - actual.forEach((session, i) => expect(session).toBe(expected[i])) - }) - it('matches workspace scope across host path spellings', () => { // Verify native containment across Windows folding, WSL aliases/mounts, trailing separators and near-miss siblings. const cases: [workspace: string, cwd: string, matched: boolean][] = [ @@ -344,28 +159,6 @@ describe('/shared ai-vault-session-filters (hoisted matchers and sort keys)', () }) } }) - - it('orders by creation time identically to the per-comparison parse', () => { - const sessions = Array.from({ length: 500 }, (_, i) => ({ - ...baseSession, - id: String(i), - createdAt: - i % 37 === 0 ? 'invalid' : new Date(1700000000000 + ((i * 91) % 499) * 1000).toISOString() - })) - const expected = [...sessions].sort( - (a, b) => Date.parse(b.createdAt ?? b.modifiedAt) - Date.parse(a.createdAt ?? a.modifiedAt) - ) - const actual = filterAiVaultSessions(sessions, { - query: '', - agents: ['claude'], - scope: 'all', - sort: 'created', - activeWorktreePaths: [], - hideEmptySessions: false - }) - expect(actual).toHaveLength(expected.length) - actual.forEach((session, i) => expect(session).toBe(expected[i])) - }) }) describe('filterAiVaultSessions symlinked workspace', () => { diff --git a/src/shared/ai-vault-ssh-resume-target.test.ts b/src/shared/ai-vault-ssh-resume-target.test.ts index 7aa97d52183..10329ebde1d 100644 --- a/src/shared/ai-vault-ssh-resume-target.test.ts +++ b/src/shared/ai-vault-ssh-resume-target.test.ts @@ -1,52 +1,7 @@ import { describe, expect, it } from 'vitest' import { canResumeAiVaultSessionOnSshHost } from './ai-vault-ssh-resume-target' -const WSL_FILE = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\.claude\\projects\\app\\s.jsonl' -const HOST_FILE = '/Users/ada/.claude/projects/app/s.jsonl' - describe('canResumeAiVaultSessionOnSshHost', () => { - it('accepts a session scanned on the target SSH host', () => { - expect( - canResumeAiVaultSessionOnSshHost({ - sessionFilePath: '/home/ada/.claude/projects/app/s.jsonl', - sessionExecutionHostId: 'ssh:builder', - targetExecutionHostId: 'ssh:builder' - }) - ).toBe(true) - }) - - it('refuses a session scanned on another SSH host', () => { - expect( - canResumeAiVaultSessionOnSshHost({ - sessionFilePath: WSL_FILE, - sessionExecutionHostId: 'ssh:other', - targetExecutionHostId: 'ssh:builder' - }) - ).toBe(false) - }) - - it('accepts a local WSL-stored transcript, with or without a stamped host', () => { - for (const sessionExecutionHostId of ['local' as const, null]) { - expect( - canResumeAiVaultSessionOnSshHost({ - sessionFilePath: WSL_FILE, - sessionExecutionHostId, - targetExecutionHostId: 'ssh:builder' - }) - ).toBe(true) - } - }) - - it('refuses a transcript stored on this machine outside WSL', () => { - expect( - canResumeAiVaultSessionOnSshHost({ - sessionFilePath: HOST_FILE, - sessionExecutionHostId: 'local', - targetExecutionHostId: 'ssh:builder' - }) - ).toBe(false) - }) - it('keeps a transcript-referencing Antigravity resume on the transcript host', () => { const wslReference = '//wsl.localhost/Ubuntu/home/ada/.gemini/antigravity-ide/brain/id/transcript_full.jsonl' @@ -74,14 +29,4 @@ describe('canResumeAiVaultSessionOnSshHost', () => { }) ).toBe(true) }) - - it('refuses a runtime-scanned session', () => { - expect( - canResumeAiVaultSessionOnSshHost({ - sessionFilePath: WSL_FILE, - sessionExecutionHostId: 'runtime:devbox', - targetExecutionHostId: 'ssh:builder' - }) - ).toBe(false) - }) }) diff --git a/src/shared/ai-vault-types.test.ts b/src/shared/ai-vault-types.test.ts deleted file mode 100644 index 567225a9947..00000000000 --- a/src/shared/ai-vault-types.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from 'vitest' -import { - aiVaultSessionRecoverableSignalCount, - isAiVaultSessionRecoverableEmpty, - isAiVaultSessionResumableContent, - type AiVaultSessionPreviewMessage -} from './ai-vault-types' - -type SignalFields = { - messageCount: number - previewMessages: AiVaultSessionPreviewMessage[] - queuedMessageCount: number - subagentTranscriptCount: number -} - -function signal(overrides: Partial = {}): SignalFields { - return { - messageCount: 0, - previewMessages: [], - queuedMessageCount: 0, - subagentTranscriptCount: 0, - ...overrides - } -} - -function preview(role: AiVaultSessionPreviewMessage['role']): AiVaultSessionPreviewMessage { - return { role, text: 'preview text', timestamp: null } -} - -describe('isAiVaultSessionResumableContent', () => { - it('is true when the transcript holds conversation turns', () => { - expect(isAiVaultSessionResumableContent(signal({ messageCount: 2 }))).toBe(true) - expect(isAiVaultSessionResumableContent(signal())).toBe(false) - }) - - it('accepts conversation previews when the turn count is zero', () => { - // Metadata turn counts may be absent while Grok/OpenCode fallback previews contain real turns. - expect(isAiVaultSessionResumableContent(signal({ previewMessages: [preview('user')] }))).toBe( - true - ) - expect( - isAiVaultSessionResumableContent(signal({ previewMessages: [preview('assistant')] })) - ).toBe(true) - }) - - it('ignores non-conversation previews', () => { - expect(isAiVaultSessionResumableContent(signal({ previewMessages: [preview('system')] }))).toBe( - false - ) - }) -}) - -describe('isAiVaultSessionRecoverableEmpty', () => { - it('is true for a zero-turn session with queued or subagent signal', () => { - expect(isAiVaultSessionRecoverableEmpty(signal({ queuedMessageCount: 3 }))).toBe(true) - expect(isAiVaultSessionRecoverableEmpty(signal({ subagentTranscriptCount: 1 }))).toBe(true) - }) - - it('is false for a plain empty session or one with resumable content', () => { - expect(isAiVaultSessionRecoverableEmpty(signal())).toBe(false) - expect( - isAiVaultSessionRecoverableEmpty(signal({ messageCount: 2, queuedMessageCount: 4 })) - ).toBe(false) - expect( - isAiVaultSessionRecoverableEmpty( - signal({ previewMessages: [preview('user')], queuedMessageCount: 4 }) - ) - ).toBe(false) - }) -}) - -describe('aiVaultSessionRecoverableSignalCount', () => { - it('sums queued messages and subagent transcripts, clamping negatives', () => { - expect( - aiVaultSessionRecoverableSignalCount({ - queuedMessageCount: 4, - subagentTranscriptCount: 2 - }) - ).toBe(6) - expect( - aiVaultSessionRecoverableSignalCount({ - queuedMessageCount: -1, - subagentTranscriptCount: 3 - }) - ).toBe(3) - expect( - aiVaultSessionRecoverableSignalCount({ - queuedMessageCount: 4, - subagentTranscriptCount: -2 - }) - ).toBe(4) - }) -})