Files
orca/src/main/runtime/runtime-worktree-agent-rows-structured.test.ts
T
Brennan Benson 291b4ddd6f feat(agent-status): route structured sessions through canonical ownership (#20718)
* feat(agent-status): route structured status through canonical ownership and fence child lifetimes

Restacked onto the canonical store and child-work contract. Completing that
restack drops the `reopenStructuredParent` mutation flag this change had
carried, along with its contract field, its codec branch, and its single
call site in structured ingest, which passed a hardcoded `true`.

The flag was a narrow escape hatch from the absolute `tombstones.has(...)`
rule that governed parent upserts in this branch's original base. The
canonical store replaces that rule with a revision envelope, because a
bounded store compacts tombstones away and a presence-based guard silently
stops fencing once one is evicted. With the envelope deciding the outcome,
the escape hatch has nothing left to escape from, so removing it changes no
production behaviour.

`agent-status-store-reopen.test.ts` is rewritten against the envelope: the
reopen case now pins that an unflagged republication succeeds while replay
from before the reopen stays fenced even after the parent tombstone is
compacted away, and the second case pins where the guard genuinely bites —
a republication inside the removing mutation itself, for every subject kind.

* fix(agent-status): re-admit unchanged structured owners after teardown

* fix(agent-status): clear anti-slop object-param and Reflect.apply findings

- agent-status-store-byte-budget.ts: type the byte-budget helper's
  record parameter as the union of what its call sites actually pass
  (the snapshot header plus each store entity record) instead of the
  broad `object`.
- server-structured-canonical-status.test.ts: replace `Reflect.apply`
  with a typed, explicitly-bound call that models a caller at an
  untyped boundary omitting the trusted owner subject.

* docs(agent-status): drop the 2A progress doc from docs/reference

docs/reference/ holds implementation detail, not rollout progress. The
canonical-boundary notes move to the effort's working directory; the
agent-status-store status section keeps the boundary statement and loses
the now-dangling link.

* fix(agent-status): mint the canonical epoch on first use, not at construction

The hook server's canonical store was built in an instance-member initializer, so
constructing AgentHookServer — which happens at import time for the module
singleton — demanded a live randomUUID. Any importer that stubs node:crypto threw
'Invalid agent status store epoch' before a single test ran.

The store is now created on first canonical access and reset by dropping it, so
construction owes nothing to a crypto implementation and the epoch still rotates
per authority incarnation.

* fix(agent-status): drop the orphaned snapshot budget and a duplicated pane guard

Two leftovers from the canonical-store routing change:

agent-status-store-snapshot-budget.ts lost its only caller when the store state
switched to agentStatusStoreFitsByteBudget. Nothing in the repo imports it now,
so the module goes with the caller it existed for. The replacement is not a
straight copy: it only memoises a record's measured size once the record is
frozen, so a still-mutable record can no longer return a stale byte count.

persistedStructuredWorkerPaneKeyIsValid repeated its public-pane-key rejection
verbatim three lines below the first one. The tests covering that rejection pass
on the first occurrence alone, so the second decided nothing and only obscured
which predicate was load-bearing.

* fix(agent-status): stop a failed structured publish from latching as owned

Three defects found reviewing the structured routing path.

combinedStatusEntries defaulted a missing listing order to 0, but the counter it
compares against starts at 1, so any unordered row sorted above every ordered
one. Unknown order now sorts last.

The owner map recorded a session as owned before the sink ran. A publish that
threw therefore left matchesLocation reporting an owned location for a row that
was never written, and the unchanged-projection path — the only thing that would
re-offer it — stopped. The address still has to survive a throw so teardown can
forget a row that did land, so the two facts are now separate: the address is
recorded up front, and only a publish that returned marks the row as landed.

The reopen test claimed the revision envelope rather than the tombstone fences a
stale replay. It cannot tell: transport consecutiveness, the parent-revision
validator and the tombstone guard each refuse that replay alone, and ablating any
two leaves the test green. It now asserts the outcome and says so.
2026-09-16 01:20:58 -07:00

147 lines
5.8 KiB
TypeScript

import { makeStructuredAgentStatusSubject } from '../../shared/agent-status-subject'
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'
const SUBJECT = makeStructuredAgentStatusSubject(
{
executionHostId: 'local',
wslDistro: null,
workspaceId: WORKTREE_ID,
workspaceKind: 'git-worktree'
},
SESSION
)
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, SUBJECT)
}
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')
})
})