Files
orca/src/main/runtime/runtime-worktree-agent-rows-structured.test.ts
T
Brennan BensonandMerge Sim da5d555259 refactor(agent-status): delete the runtime's retained row store (PR 1b) (#19785)
* docs(agent-status): plan PR 1b at file level

Names the five RuntimeAgentRowStore call sites and what each becomes, why
terminalHandle has to be stamped before the store can go, and the one
intended behavior change.

* feat(agent-status): stamp the pane terminal handle on hook-server rows

The runtime's retained row store carried the pty binding two readers need. Put
that fact on the row that already owns the pane instead, resolved through the
same lookup the renderer-facing IPC boundary runs, so the two surfaces cannot
disagree about which terminal a pane is.

Carried forward when a later write resolves no handle (only main's OSC parse
can), and never persisted: a handle belongs to the runtime that issued it.

* refactor(agent-status): route the session-tabs republish off the store

`retain()` was not only a duplicate store: its boolean return was the signal
that republished `session.tabs` for a status-only transition, which no title
change covers (#7970). `hook-status-session-tabs-invalidation.ts` already
mirrors that change set plus hook restore provenance, so route the signal off
the store rather than keep a second comparator.

Adds the status-drop arm a user dismissal emits, which the pane-clear fan-out
deliberately skips — now load-bearing, because a dismissed row leaves the
listing at once.

Installed on both hosts. orcad had neither the OSC producer nor this signal, so
its runtime observed agent status and published it nowhere; deleting the
retained copy without wiring it would list no PTY agents there at all.

* refactor(agent-status): delete the runtime's duplicate retained row store

`RuntimeAgentRowStore` held the same payload the hook server already holds, so
the same pane could legitimately read differently in the sidebar, in
`worktree ps`, and on the phone. Both of its readers move onto the store's
snapshot in `runtime-hook-agent-row-selection.ts`, and
`collectRuntimeWorktreePtyAgentSources` loses the retained-versus-hook
reconciliation that only existed because two stores could disagree.

`ConnectedPtyEvidence` trades its flat pty-id set for `ptyIdByTerminalHandle`,
which is how a row still resolves the connected PTY behind it — the
working-terminal rollup's match key, and the last rescue for a row whose pane
binding a controller incarnation nulled under it.

The one intended behavior change: a row the user dismisses on the desktop
leaves `worktree ps` and mobile at once instead of lingering until the pty
exits. One store means one dismissal.

The suites written against the retained store are rewired to a real
AgentHookServer rather than deleted, so each still asserts the listing
behavior it named.

* docs(agent-status): record what PR 1b landed

Past tense, plus two corrections to the plan: `terminalHandle` is not the pty
id (they are different identifiers, and the explicit-status reader was already
comparing against a real handle), and the legacy numeric pane key is a
consequence the plan did not name.

* fix(agent-status): harden single-store lifecycle

* fix(agent-status): preserve mobile terminal rejoin

* fix(agent-status): preserve unverifiable remote rows

* fix(agent-status): own PTY row lifecycle in hook server

* fix(agent-status): preserve state and renew freshness

* fix(agent-status): ignore freshness for dismissed identity rows

* fix(agent-status): fence orcad observed identities

* fix(orcad): always release daemon adapter on cleanup

* fix(agent-status): cover remint and headless lifecycle edges

* fix agent status identity recovery gaps

* fix(agent-status): suppress duplicate child-only row mutation

* test(runtime): preserve hook store wiring in transcript harness

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-11 15:28:16 -07:00

137 lines
5.5 KiB
TypeScript

import { collectRuntimeWorktreeAgentSources } from './runtime-worktree-agent-sources'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { attachRuntimeWorktreeAgentRows } from './runtime-worktree-agent-rows'
import {
structuredAgentSessionPaneKey,
structuredAgentSessionTabId
} from '../../shared/structured-agent-session-projection'
import type { AgentSessionStatusSummary } from '../../shared/agent-session-wire'
import type { RuntimeWorktreePsSummary } from '../../shared/runtime-types'
import { AgentHookServer, _internals } from '../agent-hooks/server'
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../telemetry/cohort-classifier', () => ({
getCohortAtEmit: vi.fn(() => ({ nth_repo_added: 2 }))
}))
/**
* A structured session has no PTY and no hook script, so the host publishes its projection into
* the agent-status store itself. This walks that store into `worktree ps` rows: before it, the CLI
* reported a worktree running one as idle while the sidebar showed it working.
*/
const WORKTREE_ID = 'repo-1::/workspace/app'
const SESSION = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
function summary(over: Partial<AgentSessionStatusSummary> = {}): AgentSessionStatusSummary {
return {
sessionId: SESSION,
workspaceId: WORKTREE_ID,
agent: 'claude',
status: 'working',
latestPrompt: 'ship the thing',
updatedAt: 1_757_030_400_000,
hostExecutionOwned: true,
...over
} as AgentSessionStatusSummary
}
function attach(summaries: AgentSessionStatusSummary[]): RuntimeWorktreePsSummary {
const store = new AgentHookServer()
for (const entry of summaries) {
store.ingestStructuredStatus(entry)
}
const row = {
worktreeId: WORKTREE_ID,
status: 'inactive',
hasHostSidebarActivity: false,
agents: []
} as unknown as RuntimeWorktreePsSummary
const summariesById = new Map<string, RuntimeWorktreePsSummary>([[WORKTREE_ID, row]])
attachRuntimeWorktreeAgentRows({
summaries: summariesById,
pathIndex: { byPath: new Map(), byRealPath: new Map() } as never,
missingWorktreeIds: new Set(),
workingTerminalEvidenceByWorktreeId: new Map(),
rowSources: collectRuntimeWorktreeAgentSources({
mirroredWorktreeIdByTabId: new Map(),
connectedPtyEvidence: {
tabIds: new Set(),
paneKeys: new Set(),
ptyIdByTerminalHandle: new Map()
},
hookSnapshots: store.getStatusSnapshot()
}),
orchestrationByPaneKey: null,
getSummary: (map, _p, _m, id) => map.get(id) ?? null
})
return row
}
beforeEach(() => {
_internals.resetCachesForTests()
})
describe('worktree ps reports structured sessions', () => {
it('a busy structured session is not reported idle', () => {
const row = attach([summary()])
expect(row.agents).toHaveLength(1)
expect(row.agents[0]?.state).toBe('working')
expect(row.agents[0]?.agentType).toBe('claude')
expect(row.agents[0]?.prompt).toBe('ship the thing')
expect(row.status).toBe('working')
})
// The same projection the sidebar applies, so the two surfaces cannot disagree about one session.
it('maps attention to blocked and idle to done', () => {
expect(attach([summary({ status: 'attention' })]).agents[0]?.state).toBe('blocked')
expect(attach([summary({ status: 'idle' })]).agents[0]?.state).toBe('done')
})
it('does not turn a completed host-held session into permission', () => {
const row = attach([summary({ status: 'idle' })])
expect(row.status).toBe('inactive')
expect(row.hasHostSidebarActivity).toBe(false)
})
it('reports the DERIVED pane key, never an orchestration credential', () => {
const row = attach([summary()])
expect(row.agents[0]?.paneKey).toBe(
structuredAgentSessionPaneKey(structuredAgentSessionTabId(SESSION), SESSION)
)
})
// Null status means no turn has been persisted; the chat itself shows nothing, so neither does this.
it('omits a session with no projected status', () => {
expect(attach([summary({ status: null })]).agents).toHaveLength(0)
})
it('keeps the journal clock on the row, so a restart republish is not new activity', () => {
const row = attach([summary()])
expect(row.agents[0]?.updatedAt).toBe(1_757_030_400_000)
expect(row.agents[0]?.stateStartedAt).toBe(1_757_030_400_000)
})
})
/**
* The deliberate non-goal. Adding structured rows to `terminal list` was investigated and rejected:
* mobile mounts a terminal WebView per row that can never receive a frame, a `connected`-keyed
* refresh check goes permanently true and pins shipped clients to a fast cadence with no exit, and
* the plugin projection has no field that can carry `writable: false`. Every SAFE consumer of a
* terminal summary checks `ptyId`; the breaking ones key off `connected` or mere row presence,
* which no added field can qualify. This pins that the listing never reads the status store.
*/
describe('terminal listing is deliberately left alone', () => {
it('never reads the agent-status store that now carries structured rows', async () => {
const { readFile } = await import('node:fs/promises')
// orca-runtime-subscribe-to-terminal-resize.ts owns listTerminals.
const listing = await readFile(
new URL('./orca-runtime-subscribe-to-terminal-resize.ts', import.meta.url),
'utf8'
)
// Guard the guard: an empty read would make every assertion below vacuously true.
expect(listing).toContain('async listTerminals(')
expect(listing).not.toContain('getAgentStatusSnapshotFn')
expect(listing).not.toContain('structuredHost')
})
})