mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 16:02:03 +00:00
test(agent-status): pin both half-migration structured-row filters
Neither the `agentStatus:getSnapshot` filter nor the main-window listener's had a single assertion, so deleting either — the first step of PR 2 — was green everywhere. Also covers the perf skip and the drop's lack of a renderer clear.
This commit is contained in:
@@ -128,6 +128,64 @@ describe('AgentHookServer ingestStructuredStatus', () => {
|
||||
expect(server.getStatusSnapshot()).toEqual([])
|
||||
})
|
||||
|
||||
// The resume-identity remnant a dismissed PTY pane keeps exists so the agent can be resumed in
|
||||
// that pane. A structured session has no pane, and the record store owns its resume identity —
|
||||
// so a remnant here would be an unclearable row that every null-status publish re-minted.
|
||||
it('leaves no resume-identity remnant behind, even carrying a provider session', () => {
|
||||
const server = new AgentHookServer()
|
||||
const withProviderSession = summary({
|
||||
providerSession: { key: 'session_id', id: 'codex-thread-1' }
|
||||
})
|
||||
server.ingestStructuredStatus(withProviderSession)
|
||||
expect(server.getStatusSnapshot()[0]?.providerSession).toEqual({
|
||||
key: 'session_id',
|
||||
id: 'codex-thread-1'
|
||||
})
|
||||
|
||||
server.dropStructuredStatus(SESSION)
|
||||
expect(server.getStatusSnapshot()).toEqual([])
|
||||
})
|
||||
|
||||
// Structured rows are never serialized, so persisting one could only rewrite the file already
|
||||
// on disk — once per debounce window for the whole of every streaming chat.
|
||||
// "Exactly one writer per pane key" has to hold for deletes too: the renderer's feed bridge owns
|
||||
// this pane, so a pane-status-clear would be main reaching into a row it does not write.
|
||||
it('drops the row without sending the renderer a clear for a pane it does not write', () => {
|
||||
const server = new AgentHookServer()
|
||||
const cleared: unknown[] = []
|
||||
const dropped: string[] = []
|
||||
server.setPaneStatusClearListener((clear) => cleared.push(clear))
|
||||
server.subscribeStatusDrop((paneKey) => dropped.push(paneKey))
|
||||
|
||||
server.ingestStructuredStatus(summary())
|
||||
server.dropStructuredStatus(SESSION)
|
||||
|
||||
expect(server.getStatusSnapshot()).toEqual([])
|
||||
expect(cleared).toEqual([])
|
||||
expect(dropped).toEqual([STRUCTURED_PANE])
|
||||
})
|
||||
|
||||
it('schedules no persist for a structured row, while a hook row still does', () => {
|
||||
const server = new AgentHookServer()
|
||||
const persists: number[] = []
|
||||
const scheduled = server as unknown as { scheduleStatusPersist: () => void }
|
||||
const original = scheduled.scheduleStatusPersist.bind(server)
|
||||
scheduled.scheduleStatusPersist = () => {
|
||||
persists.push(1)
|
||||
original()
|
||||
}
|
||||
|
||||
server.ingestStructuredStatus(summary())
|
||||
expect(persists).toHaveLength(0)
|
||||
|
||||
server.ingestTerminalStatus({
|
||||
paneKey: PANE,
|
||||
connectionId: null,
|
||||
payload: { state: 'working', prompt: 'watch the build', agentType: 'claude' }
|
||||
})
|
||||
expect(persists).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('leaves a hook-reported pane alone', () => {
|
||||
const server = new AgentHookServer()
|
||||
server.ingestTerminalStatus({
|
||||
|
||||
@@ -199,7 +199,11 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
|
||||
this.runtimeObservedStatusPaneKeys.add(enriched.paneKey)
|
||||
}
|
||||
this.state.lastStatusByPaneKey.set(enriched.paneKey, enriched)
|
||||
this.scheduleStatusPersist()
|
||||
// Why skipped for structured rows: the serializer drops them, so the whole walk and stringify
|
||||
// can only ever reproduce the last file — once per debounce window for a streaming chat.
|
||||
if (!enriched.structuredHost) {
|
||||
this.scheduleStatusPersist()
|
||||
}
|
||||
this.notifyStatusChangeListeners()
|
||||
this.emitEnrichedStatus(enriched)
|
||||
return enriched
|
||||
|
||||
@@ -152,6 +152,37 @@ describe('agentStatus:getSnapshot IPC', () => {
|
||||
expect(handler!({})).toEqual(snapshot)
|
||||
})
|
||||
|
||||
// The half-migration seam: until PR 2 retires the renderer's own feed bridge, main must not
|
||||
// publish structured rows to the renderer at all — one pane key, one writer.
|
||||
it('omits structured rows the renderer feed bridge still owns', async () => {
|
||||
getStatusSnapshot.mockReturnValue([
|
||||
{
|
||||
paneKey: PANE_KEY,
|
||||
state: 'done',
|
||||
prompt: 'hook row',
|
||||
agentType: 'claude',
|
||||
connectionId: null,
|
||||
receivedAt: 1_700_000_000_000,
|
||||
stateStartedAt: 1_699_999_999_000
|
||||
},
|
||||
{
|
||||
paneKey: CHILD_PANE_KEY,
|
||||
state: 'working',
|
||||
prompt: 'native chat row',
|
||||
agentType: 'codex',
|
||||
connectionId: null,
|
||||
structuredHost: 'owned',
|
||||
receivedAt: 1_700_000_001_000,
|
||||
stateStartedAt: 1_700_000_000_500
|
||||
}
|
||||
])
|
||||
const { registerAgentHookHandlers } = await import('./agent-hooks')
|
||||
registerAgentHookHandlers()
|
||||
|
||||
const rows = handleHandlers.get('agentStatus:getSnapshot')!({}) as { paneKey: string }[]
|
||||
expect(rows.map((row) => row.paneKey)).toEqual([PANE_KEY])
|
||||
})
|
||||
|
||||
it('enriches the hook cache snapshot with runtime lineage metadata', async () => {
|
||||
const snapshot = [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// The renderer half of the half-migration seam.
|
||||
//
|
||||
// Until PR 2 retires `StructuredAgentSessionStatusBridge`, the renderer writes structured rows
|
||||
// itself. Main forwarding them too would give one pane key two writers, so the window listener
|
||||
// drops them — a filter nothing else asserts, which makes deleting it green everywhere.
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { EnrichedAgentHookEventPayload } from '../agent-hooks/server'
|
||||
|
||||
const hooks = vi.hoisted(() => ({
|
||||
listener: null as ((payload: EnrichedAgentHookEventPayload) => void) | null
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => '', on: vi.fn(), isReady: () => true }
|
||||
}))
|
||||
vi.mock('../agent-hooks/server', () => ({
|
||||
agentHookServer: {
|
||||
setListener: (listener: ((payload: EnrichedAgentHookEventPayload) => void) | null) => {
|
||||
hooks.listener = listener
|
||||
},
|
||||
setPaneStatusClearListener: vi.fn()
|
||||
}
|
||||
}))
|
||||
vi.mock('../agent-hooks/migration-unsupported-pty-state', () => ({
|
||||
setMigrationUnsupportedPtyListener: vi.fn()
|
||||
}))
|
||||
vi.mock('../window/dashboard-popout-window', () => ({
|
||||
getDashboardPopoutWindow: () => null
|
||||
}))
|
||||
vi.mock('./synthetic-title-runtime', () => ({
|
||||
driveSyntheticTitleFromHook: vi.fn(),
|
||||
shouldSuppressCodexAutoApprovalSyntheticTitleFromHook: () => false,
|
||||
stopAllSyntheticTitleSpinners: vi.fn()
|
||||
}))
|
||||
|
||||
import { installMainWindowAgentStatusListeners } from './main-window-agent-status'
|
||||
import { mainProcessState } from './main-process-state'
|
||||
|
||||
const sent: { channel: string; event: { paneKey: string } }[] = []
|
||||
|
||||
function statusPayload(
|
||||
over: Partial<EnrichedAgentHookEventPayload>
|
||||
): EnrichedAgentHookEventPayload {
|
||||
return {
|
||||
paneKey: 'pane-1',
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'repo::/wt',
|
||||
connectionId: null,
|
||||
receivedAt: 1,
|
||||
stateStartedAt: 1,
|
||||
payload: { state: 'working', prompt: 'ship it', agentType: 'codex' },
|
||||
...over
|
||||
} as EnrichedAgentHookEventPayload
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
sent.length = 0
|
||||
hooks.listener = null
|
||||
mainProcessState.runtime = null
|
||||
mainProcessState.mainWindow = {
|
||||
isDestroyed: () => false,
|
||||
webContents: {
|
||||
send: (channel: string, event: { paneKey: string }) => sent.push({ channel, event })
|
||||
}
|
||||
} as unknown as typeof mainProcessState.mainWindow
|
||||
installMainWindowAgentStatusListeners({
|
||||
window: mainProcessState.mainWindow!,
|
||||
maybeAutoRenameBranchOnFirstWork: vi.fn(),
|
||||
onRecordAgentState: vi.fn()
|
||||
})
|
||||
})
|
||||
|
||||
describe('the main-window agent-status listener', () => {
|
||||
it('forwards a hook row but never a structured one', () => {
|
||||
expect(hooks.listener).not.toBeNull()
|
||||
|
||||
hooks.listener!(statusPayload({ paneKey: 'hook-pane' }))
|
||||
hooks.listener!(
|
||||
statusPayload({
|
||||
paneKey: 'structured-agent-session-s1:leaf',
|
||||
structuredHost: 'owned'
|
||||
})
|
||||
)
|
||||
|
||||
expect(sent.map((entry) => `${entry.channel}:${entry.event.paneKey}`)).toEqual([
|
||||
'agentStatus:set:hook-pane'
|
||||
])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user