Files
orca/src/shared/agent-detection.test.ts
T

229 lines
8.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import {
detectAgentStatusFromTitle,
extractAllOscTitles,
extractLastOscTitle,
getAgentLabel,
isCursorAgentTitle,
MAX_OSC_TITLE_CHARS,
normalizeTerminalTitle
} from './agent-detection'
import {
hasCompatibleAgentTitleIdentity,
normalizeCompatibleAgentStatusEntryForOwner,
normalizeCompatibleAgentTitleForOwner,
resolveCompatibleAgentTypeForOwner
} from './agent-title-owner'
import { SYNTHETIC_AGENT_TITLE_PROFILES } from './synthetic-agent-title'
afterEach(() => {
vi.restoreAllMocks()
})
describe('OSC title extraction', () => {
it('extracts the last OSC title from BEL-terminated PTY data', () => {
expect(extractLastOscTitle('\x1b]0;First\x07noise\x1b]2;Second\x07')).toBe('Second')
})
it('extracts all OSC titles including ST-terminated titles', () => {
expect(extractAllOscTitles('\x1b]0;First\x1b\\noise\x1b]2;Second\x07')).toEqual([
'First',
'Second'
])
})
it('ignores incomplete OSC titles until a later chunk supplies the terminator', () => {
expect(extractAllOscTitles('\x1b]0;Incomplete title')).toEqual([])
expect(extractLastOscTitle('\x1b]0;Incomplete title')).toBeNull()
})
it('recovers when an abandoned incomplete OSC title is followed by a fresh title', () => {
const data = '\x1b]0;abandoned\x1b]0;Fresh title\x07'
expect(extractLastOscTitle(data)).toBe('Fresh title')
expect(extractAllOscTitles(data)).toEqual(['Fresh title'])
})
it('scans large PTY chunks without regex match iteration', () => {
const matchAll = vi.spyOn(String.prototype, 'matchAll')
const data = `${'pasted terminal noise \x1b]x;ignored\x07 '.repeat(10_000)}\x1b]0;Agent working\x07`
expect(extractLastOscTitle(data)).toBe('Agent working')
expect(extractAllOscTitles(data).at(-1)).toBe('Agent working')
expect(matchAll).not.toHaveBeenCalled()
})
it('caps oversized OSC titles before downstream title processing', () => {
const title = `${'a'.repeat(MAX_OSC_TITLE_CHARS)}${'b'.repeat(10_000)}`
const data = `before\x1b]0;${title}\x07after`
const extracted = extractLastOscTitle(data)
expect(extracted).toHaveLength(MAX_OSC_TITLE_CHARS)
expect(extracted?.startsWith('a'.repeat(MAX_OSC_TITLE_CHARS / 2))).toBe(true)
expect(extracted?.endsWith('b'.repeat(MAX_OSC_TITLE_CHARS / 2))).toBe(true)
expect(extractAllOscTitles(data)).toEqual([extracted])
})
})
describe('MiMo title detection', () => {
it.each([
['MiMo Code', 'idle'],
['mimo ready', 'idle'],
['mimo working', 'working'],
['\u280b MiMo Code', 'working']
] as const)('classifies %s', (title, expectedStatus) => {
expect(getAgentLabel(title)).toBe('MiMo Code')
expect(detectAgentStatusFromTitle(title)).toBe(expectedStatus)
})
it.each(['~/mimo/working', 'mimo-code-fixtures ready'])(
'does not classify path or hyphen false positive %s',
(title) => {
expect(getAgentLabel(title)).toBeNull()
expect(detectAgentStatusFromTitle(title)).toBeNull()
}
)
})
describe('Pi-compatible title detection', () => {
it.each([
['\u280b OMP', 'OMP', 'working'],
['OMP ready', 'OMP', 'idle'],
['OMP', 'OMP', 'idle'],
['OMP - action required', 'OMP', 'permission'],
['\u280b Pi', 'Pi', 'working'],
['Pi ready', 'Pi', 'idle'],
// Why: normalizeTerminalTitle collapses idle π frames to bare "Pi"; re-detection
// from stored lastOscTitle must still classify idle, not neutral.
['Pi', 'Pi', 'idle'],
['Pi - action required', 'Pi', 'permission']
] as const)('classifies synthesized %s', (title, expectedLabel, expectedStatus) => {
expect(getAgentLabel(title)).toBe(expectedLabel)
expect(detectAgentStatusFromTitle(title)).toBe(expectedStatus)
})
it('re-detects status after display-title normalization for Pi idle frames', () => {
expect(normalizeTerminalTitle('π - my-project')).toBe('Pi')
expect(detectAgentStatusFromTitle(normalizeTerminalTitle('π - my-project'))).toBe('idle')
expect(detectAgentStatusFromTitle(normalizeTerminalTitle('\u280b π - my-project'))).toBe(
'working'
)
})
it.each([
['\u280b Pi', 'omp', '\u280b OMP'],
['Pi ready', 'omp', 'OMP ready'],
['Pi - action required', 'omp', 'OMP - action required'],
['π - tmp', 'omp', 'OMP ready'],
['π: tmp', 'omp', 'OMP ready'],
['\u280b π: tmp', 'omp', '\u280b OMP'],
['\u280b π - tmp', 'omp', '\u280b OMP'],
['\u280b OMP', 'pi', '\u280b Pi'],
['lucky-echidna | \u283c π - Diagnose Orca terminal title flicker - test', 'omp', '\u280b OMP'],
['lucky-echidna | Pi ready', 'omp', 'OMP ready'],
['Codex | Pi ready', 'omp', 'OMP ready'],
// Why: the wrapped whole reads as a braille Claude title, but the re-ownable
// synthetic pane suffix must still win.
['lucky-echidna | ⠋ OMP', 'omp', '⠋ OMP'],
['lucky-echidna | \u283c π - Diagnose | test', 'omp', '\u280b OMP']
] as const)('normalizes %s to the authoritative %s owner', (title, owner, expectedTitle) => {
expect(normalizeCompatibleAgentTitleForOwner(title, owner)).toBe(expectedTitle)
})
it('preserves Pi-compatible custom titles and unrelated owners', () => {
expect(normalizeCompatibleAgentTitleForOwner('Fix pi bugs', 'omp')).toBe('Fix pi bugs')
// Why: a wrapped title with no re-ownable identity in any " | " segment
// passes through untouched instead of collapsing to the owner label.
expect(normalizeCompatibleAgentTitleForOwner('lucky-echidna | Fix pi bugs', 'omp')).toBe(
'lucky-echidna | Fix pi bugs'
)
expect(normalizeCompatibleAgentTitleForOwner('xxPi ready', 'omp')).toBe('xxPi ready')
expect(normalizeCompatibleAgentTitleForOwner('\u280b Pi', 'codex')).toBe('\u280b Pi')
})
it('identifies titles whose compatible identity can be re-owned', () => {
expect(hasCompatibleAgentTitleIdentity('Pi ready')).toBe(true)
expect(hasCompatibleAgentTitleIdentity('π - tmp')).toBe(true)
expect(hasCompatibleAgentTitleIdentity('\u280b OMP')).toBe(true)
expect(hasCompatibleAgentTitleIdentity('Fix pi bugs')).toBe(false)
expect(hasCompatibleAgentTitleIdentity('\u280b Codex')).toBe(false)
})
it('normalizes Pi-compatible status identity and terminal title to the owner', () => {
const status = normalizeCompatibleAgentStatusEntryForOwner(
{
state: 'working',
prompt: '',
updatedAt: 1,
stateStartedAt: 1,
agentType: 'pi',
paneKey: 'tab-1:leaf-1',
terminalTitle: '\u280b Pi',
stateHistory: []
},
'omp'
)
expect(status.agentType).toBe('omp')
expect(status.terminalTitle).toBe('\u280b OMP')
expect(resolveCompatibleAgentTypeForOwner('codex', 'omp')).toBe('codex')
})
it.each(['~/omp/working', 'omp-harness ready', '~/pi/working', 'pi-scratch ready'])(
'does not classify path or hyphen false positive %s',
(title) => {
expect(getAgentLabel(title)).toBeNull()
expect(detectAgentStatusFromTitle(title)).toBeNull()
}
)
})
describe('Cursor agent title identity', () => {
// Why: the accepted vocabulary is the set of labels Orca actually synthesizes for Cursor.
// Pin it to that profile so renaming a label there cannot silently drop @cursor to zero
// recipients (and desync the auto-Enter suppression that shares this predicate).
it('accepts every label Orca synthesizes for Cursor', () => {
const profile = SYNTHETIC_AGENT_TITLE_PROFILES.cursor
for (const label of [
profile.workingLabel,
`⠋ ${profile.workingLabel}`,
profile.idleLabel,
profile.permissionLabel
]) {
expect(isCursorAgentTitle(label)).toBe(true)
}
})
it.each([
'Cursor Agent',
' cursor agent ',
'⠋ Cursor Agent',
'⣿ Cursor Agent',
'Cursor ready',
'Cursor - action required'
])('accepts the native or Orca-synthesized Cursor title %j', (title) => {
expect(isCursorAgentTitle(title)).toBe(true)
})
// Why: "cursor" is ordinary editor vocabulary in another agent's task-summary title,
// so a whole-token name match is not Cursor identity.
it.each([
'⠋ fix the text cursor blink',
'✳ Fix the text cursor blink',
'. fix cursor position',
'* cursor rendering done',
'Terminal Cursor and Orca slows down',
'cursor-agent',
'cursor.exe',
'~/cursor-rules',
'',
null,
undefined
])('rejects the non-Cursor title %j', (title) => {
expect(isCursorAgentTitle(title)).toBe(false)
})
})