test: remove 167 low-value shared agent tests (#37791)

This commit is contained in:
OrcaWin
2026-11-03 03:24:09 -08:00
committed by GitHub
parent 572fbc8a2a
commit 4e3ee00eda
24 changed files with 15 additions and 1886 deletions
-9
View File
@@ -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,
@@ -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)
})
})
@@ -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')
})
})
-40
View File
@@ -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('')
})
})
+1 -240
View File
@@ -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()
})
@@ -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/<id>/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 <n.n> <Name>`, 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 <n.n> <Name>` is model metadata — rather than by
// teaching one detector about one competitor.
expect(getAgentLabel('agy · Gemini 3.7 Flash')).toBe('Antigravity')
})
})
})
-64
View File
@@ -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')
+1 -80
View File
@@ -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')
)
})
})
+1 -202
View File
@@ -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({
@@ -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)
})
})
-42
View File
@@ -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)
})
})
@@ -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'
)
})
})
+1 -65
View File
@@ -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> = {}): Worktree {
return {
@@ -29,19 +26,6 @@ function makeWorktree(overrides: Partial<Worktree> = {}): 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])
})
})
@@ -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) => {
+1 -75
View File
@@ -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()
@@ -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))
})
})
@@ -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
})
})
})
@@ -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)
})
@@ -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' },
@@ -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)
}
})
+2 -86
View File
@@ -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', () => {
+3 -210
View File
@@ -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', () => {
@@ -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)
})
})
-93
View File
@@ -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> = {}): 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)
})
})