mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
test(agent-status): inventory legacy pane identity surfaces (#16575)
* feat(agent-status): measure identity evidence before migrating any consumer PR 1 of the identity migration. It changes no displayed or routed identity — it only measures. Why measure first: the hierarchy shipped in #16148/#16157 has zero consumers, while ~31 sites still derive identity independently. Every migration decision after this is currently a guess, including the one that matters most — how often a real pane has no evidence at all. A live P0 reports "No Claude status shown", and this design trades toward showing nothing when uncertain, so the blank rate has to be a number before any surface moves. - `pane-agent-identity-evidence.ts` — one assembler that gathers a pane's evidence, so consumers stop each inventing their own ladder. - `pane-agent-identity-census.ts` — shadow-only counters keyed by host kind (native / wsl-host / wsl-distro / ssh / relay) and launch mode (typed / orca-launch / resume). Records a bitmask of which sources were present and whether the resolver returned null or ambiguous. No titles, prompts, paths, handles, or agent text. - `pane-agent-identity-inventory.test.ts` — a ratchet that fails when a legacy identity helper gains a new production caller, so the surface cannot grow while the migration runs. Three review findings are encoded rather than deferred: launch stays above run-key-less completed hooks (promoting the hook lets a stale record hijack a pane); OMP/Pi evidence is owner-normalized before assembly, since OMP emits Pi-compatible frames and a wrapper's hook would otherwise be read as the agent it wraps; and Windows-side `wsl.exe` is rejected as process evidence, because the host observes the distro wrapper rather than the agent inside it. The census cannot be completed from a worktree. It needs representative native, SSH, WSL and relay cohorts collected from real use, and that review is the gate on PR 3 — not this PR. * test(agent-status): keep identity migration inventory-only * test(agent-status): reuse reliable source scanner * test(agent-status): bound inventory scan work * test(agent-status): avoid inventory path false negatives * test(agent-status): refresh identity inventory after base repair * test(agent-status): correct inventory classifications * test(agent-status): correct action boundary inventory * test(agent-status): pin inventory occurrence counts * test(agent-status): fail closed on scanner desync
This commit is contained in:
@@ -115,8 +115,3 @@ export function resolvePaneAgentActivity(
|
||||
livePtyRequired: false
|
||||
}
|
||||
}
|
||||
|
||||
// Deliberately absent: a resolvePaneAgentOwner precedence resolver. The only
|
||||
// Phase 2 identity consumer (native-chat toggle) reads hook identity without a
|
||||
// freshness gate, so a gated owner resolver would change its behavior; the
|
||||
// owner resolver lands with its first real consumer in a later slice.
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { glob } from 'tinyglobby'
|
||||
import {
|
||||
blankStringContents,
|
||||
blankStringContentsDesynced,
|
||||
isTestFile,
|
||||
stripComments
|
||||
} from './source-scan/source-tree-scan'
|
||||
|
||||
const HELPERS = [
|
||||
'getAgentLabel',
|
||||
'isClaudeAgent',
|
||||
'titleHasAgentName',
|
||||
'buildAgentNameRe',
|
||||
'resolveTerminalTitleAgentType',
|
||||
'resolveExplicitTerminalTitleAgentType',
|
||||
'resolveCommittedTitleAgentType',
|
||||
'resolvePaneAgentOwner',
|
||||
'resolveCompatibleAgentTypeForOwner'
|
||||
] as const
|
||||
|
||||
const TEST_SUPPORT_PATHS = new Set([
|
||||
'src/renderer/src/components/terminal-pane/pty-connection-test-environment.ts'
|
||||
])
|
||||
|
||||
type Helper = (typeof HELPERS)[number]
|
||||
type InventoryPath = string | readonly [path: string, occurrences: number]
|
||||
type Classification =
|
||||
| 'parser-implementation'
|
||||
| 'activity-only'
|
||||
| 'enum-formatter'
|
||||
| 'evidence-producer'
|
||||
| 'identity-consumer'
|
||||
| 'action-consumer'
|
||||
|
||||
type InventoryGroup = {
|
||||
helper: Helper
|
||||
classification: Classification
|
||||
paths: readonly InventoryPath[]
|
||||
}
|
||||
|
||||
const INVENTORY: readonly InventoryGroup[] = [
|
||||
{
|
||||
helper: 'getAgentLabel',
|
||||
classification: 'enum-formatter',
|
||||
paths: [
|
||||
[
|
||||
'src/renderer/src/components/agent-session-continuation/AgentSessionContinuationDialog.tsx',
|
||||
2
|
||||
],
|
||||
['src/renderer/src/components/automations/AutomationListLocalRows.tsx', 2],
|
||||
'src/renderer/src/components/automations/automation-draft-model.ts',
|
||||
['src/renderer/src/components/automations/automation-list-search-rows.ts', 2],
|
||||
['src/renderer/src/components/dashboard-popout/AgentMapSnapshotWorkspaceMenu.tsx', 2],
|
||||
['src/renderer/src/components/dashboard-popout/AgentMapWorktreeRingNode.tsx', 2],
|
||||
['src/renderer/src/components/settings/QuickCommandsList.tsx', 2],
|
||||
['src/renderer/src/components/tab-bar/TabBarQuickCommandItem.tsx', 2],
|
||||
['src/renderer/src/components/tab-bar/TabBarQuickCommandsMenu.tsx', 2],
|
||||
'src/renderer/src/lib/agent-catalog.tsx',
|
||||
['src/renderer/src/lib/launch-agent-session-continuation.ts', 3],
|
||||
['src/renderer/src/lib/orchestration-skill-coverage.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'getAgentLabel',
|
||||
classification: 'activity-only',
|
||||
paths: [['src/renderer/src/lib/pane-agent-evidence.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'getAgentLabel',
|
||||
classification: 'parser-implementation',
|
||||
paths: [
|
||||
'src/renderer/src/lib/agent-status.ts',
|
||||
'src/shared/agent-detection.ts',
|
||||
'src/shared/agent-title-identity.ts',
|
||||
['src/shared/agent-title-owner.ts', 2],
|
||||
['src/shared/terminal-title-agent-type.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'isClaudeAgent',
|
||||
classification: 'action-consumer',
|
||||
paths: [
|
||||
['src/renderer/src/components/terminal-pane/cache-timer-seeding.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/parked-terminal-byte-watcher.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.ts', 2],
|
||||
['src/renderer/src/store/terminals/terminal-ephemeral-state.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'isClaudeAgent',
|
||||
classification: 'action-consumer',
|
||||
paths: [
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/command-inferred-pane-agent.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'isClaudeAgent',
|
||||
classification: 'parser-implementation',
|
||||
paths: [
|
||||
'src/renderer/src/lib/agent-status.ts',
|
||||
'src/shared/agent-detection.ts',
|
||||
['src/shared/agent-title-identity.ts', 2],
|
||||
['src/shared/terminal-title-agent-type.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'titleHasAgentName',
|
||||
classification: 'parser-implementation',
|
||||
paths: [
|
||||
'src/shared/agent-detection.ts',
|
||||
'src/shared/agent-name-token-match.ts',
|
||||
['src/shared/agent-title-core.ts', 4],
|
||||
['src/shared/agent-title-evidence.ts', 2],
|
||||
['src/shared/agent-title-identity.ts', 11],
|
||||
['src/shared/terminal-title-agent-type.ts', 14]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'titleHasAgentName',
|
||||
classification: 'evidence-producer',
|
||||
paths: [['src/renderer/src/hooks/ipc-events/agent-status-routing.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'buildAgentNameRe',
|
||||
classification: 'action-consumer',
|
||||
paths: [['src/main/runtime/orchestration/groups.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'buildAgentNameRe',
|
||||
classification: 'parser-implementation',
|
||||
paths: [['src/shared/agent-name-token-match.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'resolveTerminalTitleAgentType',
|
||||
classification: 'identity-consumer',
|
||||
paths: [['src/renderer/src/lib/notes-send-agent-targets.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'resolveTerminalTitleAgentType',
|
||||
classification: 'parser-implementation',
|
||||
paths: [['src/shared/terminal-title-agent-type.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'resolveExplicitTerminalTitleAgentType',
|
||||
classification: 'identity-consumer',
|
||||
paths: [
|
||||
['mobile/src/session/mobile-terminal-tab-agent.ts', 2],
|
||||
['src/renderer/src/lib/open-tab-occupant-agent.ts', 2],
|
||||
['src/renderer/src/lib/use-tab-agent.ts', 3]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveExplicitTerminalTitleAgentType',
|
||||
classification: 'parser-implementation',
|
||||
paths: [
|
||||
['src/renderer/src/lib/pane-agent-evidence.ts', 2],
|
||||
'src/shared/terminal-title-agent-type.ts'
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCommittedTitleAgentType',
|
||||
classification: 'action-consumer',
|
||||
paths: [
|
||||
['src/renderer/src/components/native-chat/use-native-chat-toggle-shortcut.ts', 3],
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/connect-pane-pty.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/terminal-ctrl-enter.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/terminal-windows-shift-enter.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/use-notification-dispatch.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCommittedTitleAgentType',
|
||||
classification: 'identity-consumer',
|
||||
paths: [
|
||||
['src/renderer/src/components/tab-bar/tab-bar-item-surface.tsx', 3],
|
||||
['src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts', 4],
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCommittedTitleAgentType',
|
||||
classification: 'parser-implementation',
|
||||
paths: ['src/renderer/src/lib/pane-agent-evidence.ts']
|
||||
},
|
||||
{
|
||||
helper: 'resolveCommittedTitleAgentType',
|
||||
classification: 'action-consumer',
|
||||
paths: [
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/command-inferred-pane-agent.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolvePaneAgentOwner',
|
||||
classification: 'parser-implementation',
|
||||
paths: ['src/shared/pane-agent-owner.ts']
|
||||
},
|
||||
{
|
||||
helper: 'resolvePaneAgentOwner',
|
||||
classification: 'evidence-producer',
|
||||
paths: [['src/renderer/src/components/terminal-pane/parked-terminal-command-status.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'resolvePaneAgentOwner',
|
||||
classification: 'identity-consumer',
|
||||
paths: [
|
||||
['src/main/runtime/orca-runtime.ts', 3],
|
||||
['src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/shell-command-inference.ts', 2],
|
||||
['src/renderer/src/lib/use-tab-agent.ts', 2],
|
||||
['src/renderer/src/runtime/web-session-tabs-sync.ts', 3]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCompatibleAgentTypeForOwner',
|
||||
classification: 'parser-implementation',
|
||||
paths: [['src/shared/agent-title-owner.ts', 2]]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCompatibleAgentTypeForOwner',
|
||||
classification: 'identity-consumer',
|
||||
paths: [
|
||||
['src/main/runtime/orca-runtime.ts', 3],
|
||||
['src/renderer/src/components/sidebar/worktree-agent-rows.ts', 2],
|
||||
['src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts', 2],
|
||||
['src/renderer/src/lib/use-tab-agent.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCompatibleAgentTypeForOwner',
|
||||
classification: 'action-consumer',
|
||||
paths: [
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.ts', 2],
|
||||
[
|
||||
'src/renderer/src/components/terminal-pane/pty-connection/command-inferred-pane-agent.ts',
|
||||
3
|
||||
],
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/terminal-keydown-fit.ts', 3],
|
||||
['src/renderer/src/components/terminal-pane/use-notification-dispatch.ts', 2]
|
||||
]
|
||||
},
|
||||
{
|
||||
helper: 'resolveCompatibleAgentTypeForOwner',
|
||||
classification: 'evidence-producer',
|
||||
paths: [
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts', 2],
|
||||
['src/renderer/src/components/terminal-pane/pty-connection/title-spawn-bell.ts', 2]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const DIRECT_SINGLE_SOURCE_SURFACES: readonly {
|
||||
path: string
|
||||
classification: Classification
|
||||
marker: string
|
||||
}[] = [
|
||||
{
|
||||
path: 'src/renderer/src/components/terminal-pane/terminal-renderer-policy.ts',
|
||||
classification: 'identity-consumer',
|
||||
marker: 'resolveGeminiCompatFallback'
|
||||
},
|
||||
{
|
||||
path: 'src/renderer/src/components/terminal-pane/terminal-title-evidence.ts',
|
||||
classification: 'identity-consumer',
|
||||
marker: 'resolvePaneTitleDecision'
|
||||
},
|
||||
{
|
||||
path: 'src/renderer/src/components/terminal/terminal-close-copy-kind.ts',
|
||||
classification: 'identity-consumer',
|
||||
marker: 'resolveLeafCloseCopyKind'
|
||||
},
|
||||
{
|
||||
path: 'src/main/runtime/orchestration/mailbox-pointer-delivery.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'isCursorAgentTitle'
|
||||
},
|
||||
{
|
||||
path: 'src/main/providers/local-pty-provider.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'launchAgent'
|
||||
},
|
||||
{
|
||||
path: 'src/renderer/src/components/terminal-pane/pty-connection/pane-serializer-settle.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'sendStartupDraftPaste'
|
||||
},
|
||||
{
|
||||
path: 'src/renderer/src/lib/active-agent-note-send.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'sendNotesToActiveAgentSession'
|
||||
},
|
||||
{
|
||||
path: 'src/renderer/src/components/native-chat/native-chat-runtime-send.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'sendNativeChatMessage'
|
||||
},
|
||||
{
|
||||
path: 'mobile/src/session/mobile-native-chat-send.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'sendMobileNativeChatMessageWithOutcome'
|
||||
},
|
||||
{
|
||||
path: 'mobile/src/session/mobile-native-chat-image-send.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'pasteMobileNativeChatImagePaths'
|
||||
},
|
||||
{
|
||||
path: 'mobile/src/session/pr-ai-triage-launch.ts',
|
||||
classification: 'action-consumer',
|
||||
marker: 'createTerminalAndSendPrompt'
|
||||
}
|
||||
]
|
||||
|
||||
describe('pane agent identity inventory ratchet', () => {
|
||||
it('classifies every legacy helper definition, import, and callsite in src and mobile/src', async () => {
|
||||
const files = await glob(['src/**/*.{ts,tsx}', 'mobile/src/**/*.{ts,tsx}'], {
|
||||
ignore: ['**/*.test.*', '**/*.spec.*']
|
||||
})
|
||||
const actual: { helper: Helper; path: string; occurrences: number }[] = []
|
||||
for (const path of files) {
|
||||
if (isTestFile(path) || TEST_SUPPORT_PATHS.has(path)) {
|
||||
continue
|
||||
}
|
||||
const rawSource = readFileSync(join(process.cwd(), path), 'utf8')
|
||||
if (!HELPERS.some((helper) => rawSource.includes(helper))) {
|
||||
continue
|
||||
}
|
||||
const decommentedSource = stripComments(rawSource)
|
||||
if (blankStringContentsDesynced(decommentedSource)) {
|
||||
throw new Error(`String scanner desynchronized while inventorying ${path}`)
|
||||
}
|
||||
const source = blankStringContents(decommentedSource)
|
||||
for (const helper of HELPERS) {
|
||||
const occurrences = source.match(new RegExp(`\\b${helper}\\b`, 'g'))?.length ?? 0
|
||||
if (occurrences > 0) {
|
||||
actual.push({ helper, path, occurrences })
|
||||
}
|
||||
}
|
||||
}
|
||||
const expected = INVENTORY.flatMap(({ helper, paths }) =>
|
||||
paths.map((site) => {
|
||||
const [path, occurrences] = typeof site === 'string' ? [site, 1] : site
|
||||
return { helper, path, occurrences }
|
||||
})
|
||||
)
|
||||
const byHelperAndPath = (left: (typeof actual)[number], right: (typeof actual)[number]) =>
|
||||
left.helper.localeCompare(right.helper) || left.path.localeCompare(right.path)
|
||||
expect(actual.sort(byHelperAndPath)).toEqual(expected.sort(byHelperAndPath))
|
||||
})
|
||||
|
||||
it('pins direct single-source identity and action branches outside named helpers', () => {
|
||||
for (const site of DIRECT_SINGLE_SOURCE_SURFACES) {
|
||||
const source = stripComments(readFileSync(join(process.cwd(), site.path), 'utf8'))
|
||||
expect({
|
||||
path: site.path,
|
||||
classification: site.classification,
|
||||
hasMarker: source.includes(site.marker)
|
||||
}).toEqual({ path: site.path, classification: site.classification, hasMarker: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -85,6 +85,14 @@ describe('resolvePaneAgentIdentity', () => {
|
||||
})
|
||||
|
||||
describe('mixed-version peers', () => {
|
||||
it('keeps a run-key-less completed row from hijacking launch evidence', () => {
|
||||
const result = resolve([
|
||||
{ source: 'completed-hook', agent: 'claude' },
|
||||
{ source: 'launch', agent: 'codex' }
|
||||
])
|
||||
expect(result).toMatchObject({ agent: 'codex', source: 'launch' })
|
||||
})
|
||||
|
||||
it('treats evidence with no run id as eligible', () => {
|
||||
// An old host publishes no run ids. Treating unknown as stale would blank every row.
|
||||
const result = resolvePaneAgentIdentity({
|
||||
|
||||
Reference in New Issue
Block a user