[CI] Refresh unit timing weights and reduce repeated test setup (#27315)

This commit is contained in:
Neil
2026-10-10 15:15:03 -07:00
committed by GitHub
parent 42be4c50d4
commit 8ba353a1a7
12 changed files with 13427 additions and 11137 deletions
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -16,7 +16,7 @@ const EXTENSIONS = [
'/index.js'
]
const INDIRECT_INPUT =
/\b(?:readFile\w*|readdir\w*|glob\w*|spawn\w*|execFile\w*|execSync|runProcess\w*|fork|Worker)\b|\bimport\s*\(\s*[^'"\s]|\brequire\s*\(\s*[^'"\s]|\bnew\s+URL\s*\(/
/\b(?:readFile\w*|readdir\w*|glob\w*|spawn\w*|execFile\w*|execSync|runProcess\w*|fork|Worker|createRequire|getBuiltinModule)\b|\bimport\s*\(\s*[^'"\s]|\brequire\s*\(\s*[^'"\s]|\bnew\s+URL\s*\(/
function localPath(file, specifier) {
if (specifier.startsWith('.')) {
@@ -45,6 +45,9 @@ export function buildUnitDependencyGraph(sources, workspace = { resolve: () => n
opaque.add(file)
}
for (const imported of ts.preProcessFile(source, true, true).importedFiles) {
if (imported.fileName === 'module' || imported.fileName === 'node:module') {
opaque.add(file)
}
const workspaceSource = workspace.resolve(imported.fileName)
const path = workspaceSource?.file ?? localPath(file, imported.fileName)
if (path === null) {
@@ -86,3 +86,58 @@ it.each(['@orca/unknown', '@orca/process-host/private'])(
).toThrow('No public orca-source export')
}
)
it.each([
[
'an assigned native loader',
'src/main/windows-native-registry.ts',
"import { createRequire } from 'node:module'; const requireFromMain = createRequire(__filename); export const loadWindowsNativeRegistry = () => requireFromMain('@orca/windows-registry')",
'src/main/windows-registry-addon.test.ts',
"import { loadWindowsNativeRegistry } from './windows-native-registry'"
],
[
'a direct loader with a runtime serializer path',
'src/main/daemon/serialize-grid-roundtrip.ts',
"import { createRequire } from 'node:module'; export const loadOldSerializer = (path) => createRequire(import.meta.url)(path)",
'src/main/daemon/serialize-grid.differential.fuzz.test.ts',
"import { loadOldSerializer } from './serialize-grid-roundtrip'"
],
[
'a module namespace whose computed loader escapes through an object',
'src/main/stored-native-loader.ts',
"import * as hostModule from 'node:module'; const native = hostModule; const load = native['create' + 'Require'](__filename); const api = { load }; export const open = (path) => api.load(path)",
'src/main/stored-native-loader.test.ts',
"import { open } from './stored-native-loader'"
],
[
'an optional builtin loader',
'src/main/sqlite/sync-database.ts',
"export const isSqliteAvailable = () => process.getBuiltinModule?.('node:sqlite')",
'src/main/sqlite/sync-database-portability.test.ts',
"import { isSqliteAvailable } from './sync-database'"
],
[
'an assigned builtin loader',
'src/main/sqlite/assigned-reader.ts',
"const getBuiltin = process.getBuiltinModule; export const sqlite = () => getBuiltin('node:sqlite')",
'src/main/sqlite/assigned-reader.test.ts',
"import { sqlite } from './assigned-reader'"
]
])(
'keeps real test consumers of %s when another covered source changes',
(_label, owner, source, consumer, testSource) => {
const graph = collectUnitDependencyGraph(
fixture({
[owner]: source,
[consumer]: testSource,
'src/selected-input.ts': 'export const value = 1',
'src/selected-input.test.ts': "import { value } from './selected-input'"
})
)
const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort()
expect(selectUnitFiles(tests, ['src/selected-input.ts'], graph)).toMatchObject({
full: false,
files: [consumer, 'src/selected-input.test.ts'].sort()
})
}
)
@@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { formatOrcaSessionAddress } from '../../../shared/orca-session-address'
import { testOrcaSessionId } from '../../../shared/orca-session-address-test-fixture'
import { ORCHESTRATION_SESSION_CALLER_ERROR_CODES as CODES } from '../../../shared/orchestration-session-caller-codes'
import { ORCHESTRATION_TARGET_PARAM } from '../orchestration/orchestration-party'
import {
mintStructuredWorkerHandle,
mintStructuredWorkerPaneKey,
@@ -100,20 +99,6 @@ function assignTerminal(runId: string): void {
}
describe('every target param resolves both spellings of a party to one canonical address', () => {
it('covers exactly the target params the contract lists', () => {
expect(Object.keys(ORCHESTRATION_TARGET_PARAM).sort()).toEqual(
[
'orchestration.send',
'orchestration.ask',
'orchestration.dispatch',
'orchestration.inbox',
'orchestration.sessionAddress',
'orchestration.partyLocation',
'orchestration.workerStart'
].sort()
)
})
it.each(WORKER_SPELLINGS)('send: a worker at %s gets its Dispatch mailbox', async (_l, to) => {
const dispatchId = assignWorker(await chatRun())
const { message } = await as(undefined, 'orchestration.send', {
-64
View File
@@ -77,34 +77,6 @@ describe('resolveRelayPushTarget', () => {
])
})
it('does not combine remote.pushDefault with a base-branch merge target', async () => {
const git = gitForConfig({
branch: 'feature/fix',
pushRemote: new Error('missing pushRemote'),
pushDefault: 'fork',
branchRemote: 'origin',
merge: 'refs/heads/main',
base: 'refs/remotes/origin/main'
})
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toBeNull()
})
it('keeps a fork head target when the contributor branch matches the base branch name', async () => {
const git = gitForConfig({
branch: 'review/pr-1',
pushRemote: 'fork',
branchRemote: 'fork',
merge: 'refs/heads/main',
base: 'refs/remotes/origin/main'
})
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
remote: 'fork',
refspec: 'HEAD:main'
})
})
it('uses remote.pushDefault when branch pushRemote is missing', async () => {
const git = gitForConfig({
pushRemote: new Error('missing pushRemote'),
@@ -118,42 +90,6 @@ describe('resolveRelayPushTarget', () => {
})
})
it('normalizes a URL-valued branch remote to a matching named remote', async () => {
const forkUrl = 'https://github.com/contributor/orca.git'
const git = gitForConfig({
pushRemote: new Error('missing pushRemote'),
pushDefault: new Error('missing pushDefault'),
branchRemote: forkUrl,
remotes: ['origin', 'pr-contributor-orca'],
remoteUrls: {
origin: 'https://github.com/stablyai/orca.git',
'pr-contributor-orca': forkUrl
}
})
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
remote: 'pr-contributor-orca',
refspec: 'HEAD:feature/fix'
})
})
it('keeps a URL-valued pushRemote when no named remote matches it', async () => {
const forkUrl = 'git@github.com:contributor/orca.git'
const git = gitForConfig({
pushRemote: forkUrl,
branchRemote: forkUrl,
remotes: ['origin'],
remoteUrls: {
origin: 'git@github.com:stablyai/orca.git'
}
})
await expect(resolveRelayPushTarget(git, '/repo', undefined)).resolves.toEqual({
remote: forkUrl,
refspec: 'HEAD:feature/fix'
})
})
it('uses an explicit push target without reading branch config', async () => {
const git = vi.fn(async () => ({ stdout: '', stderr: '' }))
@@ -53,11 +53,6 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => {
expect(clearInput).not.toBe(NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT)
})
it('still defaults to a single Ctrl+U when no draft is parked', () => {
sendNativeChatMessage(PTY, 'plain')
expect(writes()[0]).toBe(NATIVE_CHAT_CLEAR_UNSUBMITTED_INPUT)
})
it('holds the body until the clear is confirmed, then submits after the gap', () => {
const clearInput = buildAgentTuiClearInputForText(DRAFT)
sendNativeChatMessage(PTY, 'edited', {
@@ -117,18 +112,6 @@ describe('sendNativeChatMessage with a parked multi-line draft', () => {
])
})
it('re-clears before the body, never after it', () => {
sendNativeChatMessage(PTY, 'edited', {
clearInput: buildAgentTuiClearInputForText(DRAFT),
confirmCleared: () => false
})
vi.advanceTimersByTime(NATIVE_CHAT_CLEAR_CONFIRM_MS + NATIVE_CHAT_SUBMIT_DELAY_MS)
const order = writes()
expect(order.indexOf(AGENT_TUI_CLEAR_INPUT_MAX)).toBeLessThan(
order.indexOf(buildNativeChatPasteBytes('edited'))
)
})
it('charges the confirm gap to the handle so the send card outlives the Enter', () => {
const withConfirm = sendNativeChatMessage(PTY, 'a', {
clearInput: '\x15',
-2
View File
@@ -90,8 +90,6 @@ describe('standalone runtime artifacts', () => {
it('keeps target-specific files out of the shared template set', () => {
const common = orcadTemplateCommonFilenames()
for (const target of SERVER_TARGETS) {
const specific = orcadArtifactFilenames(target).filter((file) => !common.includes(file))
expect(specific.every((file) => !common.includes(file))).toBe(true)
expect(common.every((file) => orcadArtifactFilenames(target).includes(file))).toBe(true)
}
})
@@ -1,222 +0,0 @@
// @vitest-environment happy-dom
// Register preload/store seams before importing their consumers.
import {
fixture,
transport,
WORKSPACE,
SUBJECT,
SCOPE,
NOTIFICATION_SETTINGS,
addPrompt,
AttentionPolicy,
dismissIds
} from './structured-attention-read-retirement.test-fixture'
import { Fragment, createElement } from 'react'
import { act, render, waitFor } from '@testing-library/react'
import { expect, it } from 'vitest'
import type { AgentJournalRenderItem } from '../../src/shared/agent-session-journal-types'
import {
agentSessionAttentionSubjectPrefix,
agentSessionPromptAttentionKey
} from '../../src/shared/agent-session-attention'
import { SESSION } from '../../src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture'
import {
projectStructuredAgentSessionStatusState,
structuredAgentSessionPaneKey
} from '../../src/shared/structured-agent-session-projection'
import { StructuredAgentSessionTurnCompletionFeed } from '../../src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed'
import { createStructuredAttentionMobileDelivery } from '../../src/main/runtime/structured-agent-session-mobile-attention'
import { makeUnifiedTab } from '@/store/slices/store-test-helpers'
import { useAppStore } from '@/store'
import { StructuredAgentSessionAttentionBridge } from '@/components/native-chat/StructuredAgentSessionAttentionBridge'
import { useStructuredAgentSessionRead } from '@/components/native-chat/use-structured-agent-session-read'
const TARGET = { kind: 'local' } as const
function Transcript({ visible }: { visible: boolean }): null {
useStructuredAgentSessionRead({
sessionId: SESSION,
target: TARGET,
isVisible: visible,
isViewed: visible
})
return null
}
function bridge(visible?: boolean) {
return createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
...(visible === undefined ? [] : [createElement(Transcript, { visible })])
)
}
function settleTurn(): void {
fixture.items = fixture.items.map((item): AgentJournalRenderItem =>
item.body.kind === 'turn'
? {
...item,
revision: item.revision + 1,
body: { ...item.body, state: 'completed', outcome: 'success' }
}
: item
)
fixture.sequence += 1
fixture.hostFeed.observe(SESSION)
}
async function flush(): Promise<void> {
for (let i = 0; i < 5; i += 1) {
await act(async () => {})
}
}
const promptKey = (id: string) => agentSessionPromptAttentionKey(SCOPE, SESSION, id)
it('Mark read on a chat never opened withdraws the prompt alert it surfaced', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
render(bridge())
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit'))
await flush()
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
expect(dismissIds()).toEqual([promptKey('A')])
})
it('Mark read on a chat never opened withdraws its completion alert, as before prompt alerts', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
render(bridge())
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => settleTurn())
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit'))
await flush()
expect(dismissIds()).toEqual([`${agentSessionAttentionSubjectPrefix(SCOPE, SESSION)}turn:turn-1`])
})
it('Mark read on a chat hidden since an earlier view covers the newer prompt it surfaced', async () => {
const screen = render(bridge(true))
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(dismissIds()).toHaveLength(1))
act(() => useAppStore.setState({ activeWorktreeId: 'elsewhere' }))
screen.rerender(bridge(false))
await flush()
transport.dispatch.mockClear()
act(() => addPrompt('B'))
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit'))
await flush()
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
expect(dismissIds()).toEqual([promptKey('A'), promptKey('B')])
})
it('Mark read never covers a prompt raised after the click', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
render(bridge())
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => {
useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit')
addPrompt('B')
})
await flush()
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(2))
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBe('agent-completion')
expect(dismissIds()).toEqual([promptKey('A')])
})
it('Mark all read covers every chat it marks, viewed earlier or never opened', async () => {
const OTHER = 'session-beta'
const otherSubject = structuredAgentSessionPaneKey('chat-2', OTHER)
useAppStore.setState({
unifiedTabsByWorktree: {
[WORKSPACE]: [
...(useAppStore.getState().unifiedTabsByWorktree[WORKSPACE] ?? []),
makeUnifiedTab({
id: 'chat-2',
worktreeId: WORKSPACE,
groupId: 'group',
contentType: 'agent-session',
entityId: OTHER,
agentSessionAgent: 'claude'
})
]
}
})
// The second chat's host side: its own journal, the same phone delivery and renderer stream.
let otherItems = fixture.items.map((item) => ({ ...item }))
let otherSequence = fixture.sequence
const otherFeed = new StructuredAgentSessionTurnCompletionFeed({
sessions: new Map([
[
OTHER,
{
journal: { cursor: () => ({ epoch: 'journal-b', sequence: otherSequence }) },
params: { location: SCOPE }
}
]
]),
readStatusState: () => projectStructuredAgentSessionStatusState(otherItems),
now: () => 42
})
const delivery = createStructuredAttentionMobileDelivery({
readNotificationSettings: () => ({ ...NOTIFICATION_SETTINGS, suppressWhenFocused: false }),
readWorkspaceLabels: () => ({}),
dispatch: (event) => fixture.controller.dispatch(event),
reconcile: (state) => fixture.controller.reconcileStructuredPromptAttention(state),
now: () => 42
})
otherFeed.subscribe({
id: 'other',
includePrompts: true,
emit: (event) => {
if (event.type !== 'end') {
delivery.deliver(event, undefined)
fixture.completion?.({
id: 'completion',
ok: true,
_meta: { runtimeId: 'h' },
result: event
})
}
}
})
otherFeed.observe(OTHER)
const screen = render(bridge(true))
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(dismissIds()).toHaveLength(1))
act(() => useAppStore.setState({ activeWorktreeId: 'elsewhere' }))
screen.rerender(bridge(false))
await flush()
transport.dispatch.mockClear()
act(() => {
addPrompt('B')
const promptB = fixture.items.find((item) => item.itemId === 'B')
if (!promptB) {
throw new Error('prompt B missing')
}
otherItems = [...otherItems, { ...promptB, itemId: 'C', sequence: ++otherSequence }]
otherFeed.observe(OTHER)
})
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(2))
act(() =>
useAppStore.getState().acknowledgeAgents([SUBJECT, otherSubject], undefined, 'explicit')
)
await flush()
expect(dismissIds()).toEqual([
promptKey('A'),
promptKey('B'),
agentSessionPromptAttentionKey(SCOPE, OTHER, 'C')
])
})
File diff suppressed because it is too large Load Diff
@@ -1,129 +0,0 @@
// @vitest-environment happy-dom
// Register preload/store seams before importing their consumers.
import {
fixture,
WORKSPACE,
SUBJECT,
SCOPE,
addPrompt,
publishView,
AttentionPolicy,
readCalls,
dismissIds
} from './structured-attention-read-retirement.test-fixture'
import { Fragment, StrictMode, createElement } from 'react'
import { act, render, waitFor } from '@testing-library/react'
import { expect, it } from 'vitest'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import { agentSessionPromptAttentionKey } from '../../src/shared/agent-session-attention'
import { SESSION } from '../../src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture'
import { useAppStore } from '@/store'
import { StructuredAgentSessionAttentionBridge } from '@/components/native-chat/StructuredAgentSessionAttentionBridge'
import { findStructuredAgentSessionReadOwner } from '@/components/native-chat/structured-agent-session-read-owner'
import { useStructuredAgentSessionRead } from '@/components/native-chat/use-structured-agent-session-read'
const PROMPT_A = agentSessionPromptAttentionKey(SCOPE, SESSION, 'A')
// Memoized like the pane's own target, so a rerender keeps the owner it already holds.
const LOCAL: RuntimeClientTarget = { kind: 'local' }
/** The chat pane's read: shown means visible and viewed, as the transport derives both. */
function Pane({ shown, target }: { shown: boolean; target: RuntimeClientTarget }): null {
useStructuredAgentSessionRead({ sessionId: SESSION, target, isVisible: shown, isViewed: shown })
return null
}
function tree(options: {
shown: boolean
paneKey?: string
strict?: boolean
target?: RuntimeClientTarget
}) {
const inner = createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(Pane, {
key: options.paneKey ?? 'pane',
shown: options.shown,
target: options.target ?? LOCAL
})
)
return options.strict ? createElement(StrictMode, null, inner) : inner
}
async function settle(): Promise<void> {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 30))
})
}
/** A prompt raised while the user is elsewhere and the pane is hidden. */
async function promptWhileAway(options: { strict?: boolean } = {}) {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
const screen = render(tree({ shown: false, strict: options.strict }))
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await settle()
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBe('agent-completion')
return screen
}
/** The user opens the chat: the pane reads its history and views it. */
async function show(rerender: () => void): Promise<void> {
fixture.hydrate = undefined
act(() => useAppStore.getState().setActiveWorktree(WORKSPACE))
rerender()
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await settle()
}
function expectViewReadWithdraws(): void {
expect(findStructuredAgentSessionReadOwner(SESSION, LOCAL)).toBeDefined()
expect(readCalls()).toBeGreaterThan(0)
expect(dismissIds()).toContain(PROMPT_A)
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
}
it('a view read under StrictMode still finds its owner and withdraws the alert', async () => {
const screen = await promptWhileAway({ strict: true })
await show(() => screen.rerender(tree({ shown: true, strict: true })))
expectViewReadWithdraws()
})
it('a pane hidden by a worktree switch and shown again withdraws on view', async () => {
const screen = await promptWhileAway()
await show(() => screen.rerender(tree({ shown: true })))
expect(dismissIds()).toContain(PROMPT_A)
act(() => useAppStore.setState({ activeWorktreeId: 'elsewhere' }))
screen.rerender(tree({ shown: false }))
await settle()
act(() => addPrompt('B'))
await settle()
// An owner that already holds history resumes its journal stream instead of re-reading it.
act(() => useAppStore.getState().setActiveWorktree(WORKSPACE))
screen.rerender(tree({ shown: true }))
await settle()
act(() => publishView())
await settle()
expectViewReadWithdraws()
expect(dismissIds()).toContain(agentSessionPromptAttentionKey(SCOPE, SESSION, 'B'))
})
it('a pane remounted in one commit (a tab move) keeps its owner findable', async () => {
const screen = await promptWhileAway()
// The new instance reads the still-held owner before the old one's cleanup releases it.
screen.rerender(tree({ shown: false, paneKey: 'moved' }))
await settle()
await show(() => screen.rerender(tree({ shown: true, paneKey: 'moved' })))
expectViewReadWithdraws()
})
it('a new target object for the same host keeps the owner findable', async () => {
const screen = await promptWhileAway()
screen.rerender(tree({ shown: false, target: { kind: 'local' } }))
await settle()
await show(() => screen.rerender(tree({ shown: true, target: { kind: 'local' } })))
expectViewReadWithdraws()
})
@@ -4,250 +4,423 @@ import {
fixture,
transport,
WORKSPACE,
TAB,
SUBJECT,
SCOPE,
NOTIFICATION_SETTINGS,
addPrompt,
publishView,
ReadSurface,
AttentionPolicy,
dismissIds,
publishView,
readCalls,
dismissIds
TAB,
ReadSurface
} from './structured-attention-read-retirement.test-fixture'
import { Fragment, createElement } from 'react'
import { Fragment, createElement, StrictMode } from 'react'
import { act, render, waitFor } from '@testing-library/react'
import { expect, it } from 'vitest'
import { expect, it, describe } from 'vitest'
import type { AgentJournalRenderItem } from '../../src/shared/agent-session-journal-types'
import {
agentSessionAttentionSubjectPrefix,
agentSessionPromptAttentionKey
} from '../../src/shared/agent-session-attention'
import { SESSION } from '../../src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture'
import {
projectStructuredAgentSessionStatusState,
structuredAgentSessionPaneKey
} from '../../src/shared/structured-agent-session-projection'
import { StructuredAgentSessionTurnCompletionFeed } from '../../src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed'
import { createStructuredAttentionMobileDelivery } from '../../src/main/runtime/structured-agent-session-mobile-attention'
import { makeUnifiedTab } from '@/store/slices/store-test-helpers'
import { useAppStore } from '@/store'
import { StructuredAgentSessionAttentionBridge } from '@/components/native-chat/StructuredAgentSessionAttentionBridge'
import { useStructuredAgentSessionRead } from '@/components/native-chat/use-structured-agent-session-read'
import type { RuntimeClientTarget } from '@/runtime/runtime-client-target'
import { findStructuredAgentSessionReadOwner } from '@/components/native-chat/structured-agent-session-read-owner'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import type { StructuredNotificationRead } from '../../src/shared/notification-settings-types'
import type { AgentJournalRenderItem } from '../../src/shared/agent-session-journal-types'
import { agentSessionPromptAttentionKey } from '../../src/shared/agent-session-attention'
import { RuntimeMobileNotificationController } from '../../src/main/runtime/runtime-mobile-notification-controller'
import { SESSION } from '../../src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture'
import { useAppStore } from '@/store'
import { StructuredAgentSessionAttentionBridge } from '@/components/native-chat/StructuredAgentSessionAttentionBridge'
it('reads the first accepted history even after local unread and clock targets were cleared', async () => {
addPrompt('A')
render(
createElement(
describe('Explicit desktop read acknowledgements', () => {
const TARGET = { kind: 'local' } as const
function Transcript({ visible }: { visible: boolean }): null {
useStructuredAgentSessionRead({
sessionId: SESSION,
target: TARGET,
isVisible: visible,
isViewed: visible
})
return null
}
function bridge(visible?: boolean) {
return createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
...(visible === undefined ? [] : [createElement(Transcript, { visible })])
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
expect(readCalls()).toBe(0)
expect(useAppStore.getState().unreadAgentCompletionPanes).toEqual({})
await act(async () => fixture.hydrate?.())
await waitFor(() =>
expect(dismissIds()).toEqual([agentSessionPromptAttentionKey(SCOPE, SESSION, 'A')])
)
expect(readCalls()).toBe(1)
})
}
it('reads B on return while A keeps the already-read row clock unchanged', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
useAppStore
.getState()
.setAgentStatus(
SUBJECT,
{ state: 'blocked', prompt: 'Work', agentType: 'claude' },
'Chat',
{ updatedAt: 1000, stateStartedAt: 1000 },
{ tabId: TAB, worktreeId: WORKSPACE }
function settleTurn(): void {
fixture.items = fixture.items.map((item): AgentJournalRenderItem =>
item.body.kind === 'turn'
? {
...item,
revision: item.revision + 1,
body: { ...item.body, state: 'completed', outcome: 'success' }
}
: item
)
const screen = render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: false })
)
)
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(fixture.journal).toBeTypeOf('function'))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT]))
await waitFor(() => expect(dismissIds()).toHaveLength(1))
const stamp = useAppStore.getState().acknowledgedAgentsByPaneKey[SUBJECT]
act(() => {
addPrompt('B')
publishView()
})
expect(dismissIds()).toHaveLength(1)
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBe('agent-completion')
act(() => useAppStore.setState({ activeWorktreeId: WORKSPACE }))
screen.rerender(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() =>
expect(dismissIds()).toEqual(
['A', 'B'].map((id) => agentSessionPromptAttentionKey(SCOPE, SESSION, id))
)
)
expect(useAppStore.getState().acknowledgedAgentsByPaneKey[SUBJECT]).toBe(stamp)
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
expect(readCalls()).toBe(2)
})
fixture.sequence += 1
fixture.hostFeed.observe(SESSION)
}
it('reads a newly accepted visible prompt without sending an RPC for text-only updates', async () => {
addPrompt('A')
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(readCalls()).toBe(1))
act(() => {
addPrompt('B')
publishView()
})
await waitFor(() => expect(dismissIds()).toHaveLength(2))
const calls = readCalls()
act(() => {
fixture.items = [
...fixture.items,
{
itemId: 'text',
revision: 1,
sequence: ++fixture.sequence,
observedAt: fixture.sequence,
body: {
kind: 'message',
role: 'assistant',
blocks: [{ type: 'text', text: 'More output' }]
}
}
]
publishView()
})
await act(async () => {})
expect(readCalls()).toBe(calls)
})
it('keeps hydration unread while away and retries the read on presence return', async () => {
transport.away.mockResolvedValue(true)
addPrompt('A')
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(readCalls()).toBe(0)
transport.away.mockResolvedValue(false)
await act(async () => window.dispatchEvent(new Event('focus')))
await waitFor(() => expect(dismissIds()).toHaveLength(1))
})
it.each(['transport-error', 'false-result'] as const)(
'retries a remote %s on a later read with the existing presence gate',
async (failure) => {
const target = { kind: 'environment', environmentId: 'retry-host' } as const
const tab = useAppStore.getState().unifiedTabsByWorktree[WORKSPACE]?.[0]
if (!tab) {
throw new Error('chat tab missing')
async function flush(): Promise<void> {
for (let i = 0; i < 5; i += 1) {
await act(async () => {})
}
}
const promptKey = (id: string) => agentSessionPromptAttentionKey(SCOPE, SESSION, id)
it('Mark read on a chat never opened withdraws the prompt alert it surfaced', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
render(bridge())
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit'))
await flush()
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
expect(dismissIds()).toEqual([promptKey('A')])
})
it('Mark read on a chat never opened withdraws its completion alert, as before prompt alerts', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
render(bridge())
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => settleTurn())
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit'))
await flush()
expect(dismissIds()).toEqual([
`${agentSessionAttentionSubjectPrefix(SCOPE, SESSION)}turn:turn-1`
])
})
it('Mark read on a chat hidden since an earlier view covers the newer prompt it surfaced', async () => {
const screen = render(bridge(true))
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(dismissIds()).toHaveLength(1))
act(() => useAppStore.setState({ activeWorktreeId: 'elsewhere' }))
screen.rerender(bridge(false))
await flush()
transport.dispatch.mockClear()
act(() => addPrompt('B'))
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit'))
await flush()
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
expect(dismissIds()).toEqual([promptKey('A'), promptKey('B')])
})
it('Mark read never covers a prompt raised after the click', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
render(bridge())
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(1))
act(() => {
useAppStore.getState().acknowledgeAgents([SUBJECT], undefined, 'explicit')
addPrompt('B')
})
await flush()
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(2))
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBe('agent-completion')
expect(dismissIds()).toEqual([promptKey('A')])
})
it('Mark all read covers every chat it marks, viewed earlier or never opened', async () => {
const OTHER = 'session-beta'
const otherSubject = structuredAgentSessionPaneKey('chat-2', OTHER)
useAppStore.setState({
unifiedTabsByWorktree: { [WORKSPACE]: [{ ...tab, executionHostId: 'runtime:retry-host' }] }
})
addPrompt('A')
const original = transport.call.getMockImplementation()
let failed = false
transport.call.mockImplementation(async (...args) => {
if (args[1] === 'agentSession.acknowledgeAttention' && !failed) {
failed = true
if (failure === 'transport-error') {
throw new Error('scripted transient transport failure')
}
return { acknowledged: false }
unifiedTabsByWorktree: {
[WORKSPACE]: [
...(useAppStore.getState().unifiedTabsByWorktree[WORKSPACE] ?? []),
makeUnifiedTab({
id: 'chat-2',
worktreeId: WORKSPACE,
groupId: 'group',
contentType: 'agent-session',
entityId: OTHER,
agentSessionAgent: 'claude'
})
]
}
return original?.(...args)
})
// The second chat's host side: its own journal, the same phone delivery and renderer stream.
let otherItems = fixture.items.map((item) => ({ ...item }))
let otherSequence = fixture.sequence
const otherFeed = new StructuredAgentSessionTurnCompletionFeed({
sessions: new Map([
[
OTHER,
{
journal: { cursor: () => ({ epoch: 'journal-b', sequence: otherSequence }) },
params: { location: SCOPE }
}
]
]),
readStatusState: () => projectStructuredAgentSessionStatusState(otherItems),
now: () => 42
})
const delivery = createStructuredAttentionMobileDelivery({
readNotificationSettings: () => ({ ...NOTIFICATION_SETTINGS, suppressWhenFocused: false }),
readWorkspaceLabels: () => ({}),
dispatch: (event) => fixture.controller.dispatch(event),
reconcile: (state) => fixture.controller.reconcileStructuredPromptAttention(state),
now: () => 42
})
otherFeed.subscribe({
id: 'other',
includePrompts: true,
emit: (event) => {
if (event.type !== 'end') {
delivery.deliver(event, undefined)
fixture.completion?.({
id: 'completion',
ok: true,
_meta: { runtimeId: 'h' },
result: event
})
}
}
})
otherFeed.observe(OTHER)
const screen = render(bridge(true))
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(dismissIds()).toHaveLength(1))
act(() => useAppStore.setState({ activeWorktreeId: 'elsewhere' }))
screen.rerender(bridge(false))
await flush()
transport.dispatch.mockClear()
act(() => {
addPrompt('B')
const promptB = fixture.items.find((item) => item.itemId === 'B')
if (!promptB) {
throw new Error('prompt B missing')
}
otherItems = [...otherItems, { ...promptB, itemId: 'C', sequence: ++otherSequence }]
otherFeed.observe(OTHER)
})
await waitFor(() => expect(transport.dispatch).toHaveBeenCalledTimes(2))
act(() =>
useAppStore.getState().acknowledgeAgents([SUBJECT, otherSubject], undefined, 'explicit')
)
await flush()
expect(dismissIds()).toEqual([
promptKey('A'),
promptKey('B'),
agentSessionPromptAttentionKey(SCOPE, OTHER, 'C')
])
})
})
describe('Viewed structured session owner lifecycle', () => {
const PROMPT_A = agentSessionPromptAttentionKey(SCOPE, SESSION, 'A')
// Memoized like the pane's own target, so a rerender keeps the owner it already holds.
const LOCAL: RuntimeClientTarget = { kind: 'local' }
/** The chat pane's read: shown means visible and viewed, as the transport derives both. */
function Pane({ shown, target }: { shown: boolean; target: RuntimeClientTarget }): null {
useStructuredAgentSessionRead({ sessionId: SESSION, target, isVisible: shown, isViewed: shown })
return null
}
function tree(options: {
shown: boolean
paneKey?: string
strict?: boolean
target?: RuntimeClientTarget
}) {
const inner = createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(Pane, {
key: options.paneKey ?? 'pane',
shown: options.shown,
target: options.target ?? LOCAL
})
)
return options.strict ? createElement(StrictMode, null, inner) : inner
}
async function settle(): Promise<void> {
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 30))
})
}
/** A prompt raised while the user is elsewhere and the pane is hidden. */
async function promptWhileAway(options: { strict?: boolean } = {}) {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
const screen = render(tree({ shown: false, strict: options.strict }))
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await settle()
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBe('agent-completion')
return screen
}
/** The user opens the chat: the pane reads its history and views it. */
async function show(rerender: () => void): Promise<void> {
fixture.hydrate = undefined
act(() => useAppStore.getState().setActiveWorktree(WORKSPACE))
rerender()
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
await settle()
}
function expectViewReadWithdraws(): void {
expect(findStructuredAgentSessionReadOwner(SESSION, LOCAL)).toBeDefined()
expect(readCalls()).toBeGreaterThan(0)
expect(dismissIds()).toContain(PROMPT_A)
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
}
it('a view read under StrictMode still finds its owner and withdraws the alert', async () => {
const screen = await promptWhileAway({ strict: true })
await show(() => screen.rerender(tree({ shown: true, strict: true })))
expectViewReadWithdraws()
})
it('a pane hidden by a worktree switch and shown again withdraws on view', async () => {
const screen = await promptWhileAway()
await show(() => screen.rerender(tree({ shown: true })))
expect(dismissIds()).toContain(PROMPT_A)
act(() => useAppStore.setState({ activeWorktreeId: 'elsewhere' }))
screen.rerender(tree({ shown: false }))
await settle()
act(() => addPrompt('B'))
await settle()
// An owner that already holds history resumes its journal stream instead of re-reading it.
act(() => useAppStore.getState().setActiveWorktree(WORKSPACE))
screen.rerender(tree({ shown: true }))
await settle()
act(() => publishView())
await settle()
expectViewReadWithdraws()
expect(dismissIds()).toContain(agentSessionPromptAttentionKey(SCOPE, SESSION, 'B'))
})
it('a pane remounted in one commit (a tab move) keeps its owner findable', async () => {
const screen = await promptWhileAway()
// The new instance reads the still-held owner before the old one's cleanup releases it.
screen.rerender(tree({ shown: false, paneKey: 'moved' }))
await settle()
await show(() => screen.rerender(tree({ shown: true, paneKey: 'moved' })))
expectViewReadWithdraws()
})
it('a new target object for the same host keeps the owner findable', async () => {
const screen = await promptWhileAway()
screen.rerender(tree({ shown: false, target: { kind: 'local' } }))
await settle()
await show(() => screen.rerender(tree({ shown: true, target: { kind: 'local' } })))
expectViewReadWithdraws()
})
})
describe('Accepted structured history read retirement', () => {
it('reads the first accepted history even after local unread and clock targets were cleared', async () => {
addPrompt('A')
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true, target: target })
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
expect(readCalls()).toBe(0)
expect(useAppStore.getState().unreadAgentCompletionPanes).toEqual({})
await act(async () => fixture.hydrate?.())
expect(readCalls()).toBe(1)
expect(dismissIds()).toEqual([])
await act(async () => {})
expect(readCalls()).toBe(1)
if (failure === 'transport-error') {
await act(async () => useAppStore.getState().acknowledgeAgents([SUBJECT]))
} else {
transport.away.mockResolvedValue(true)
await act(async () => window.dispatchEvent(new Event('focus')))
expect(readCalls()).toBe(1)
transport.away.mockResolvedValue(false)
await act(async () => window.dispatchEvent(new Event('focus')))
}
await waitFor(() =>
expect(dismissIds()).toEqual([agentSessionPromptAttentionKey(SCOPE, SESSION, 'A')])
)
expect(readCalls()).toBe(2)
expect(
transport.call.mock.calls
.filter(([, method]) => method === 'agentSession.acknowledgeAttention')
.map(([owner]) => owner)
).toEqual([target, target])
}
)
expect(readCalls()).toBe(1)
})
it('retries a failed local desktop relay withdrawal on a later explicit read', async () => {
const relayDirectory = mkdtempSync(join(tmpdir(), 'orca-relay-read-retry-'))
try {
addPrompt('A')
const sent = fixture.events.find((event) => event.type === 'notification')
if (!sent?.notificationId || sent.type !== 'notification') {
throw new Error('prompt not sent')
}
const relay = new RuntimeMobileNotificationController()
relay.configureDismissalStore(relayDirectory)
relay.dispatch(sent)
const withdrawals: string[] = []
relay.onDispatched((event) => {
if (event.type === 'dismiss') {
withdrawals.push(event.notificationId)
}
it('reads B on return while A keeps the already-read row clock unchanged', async () => {
useAppStore.setState({ activeWorktreeId: 'elsewhere' })
useAppStore
.getState()
.setAgentStatus(
SUBJECT,
{ state: 'blocked', prompt: 'Work', agentType: 'claude' },
'Chat',
{ updatedAt: 1000, stateStartedAt: 1000 },
{ tabId: TAB, worktreeId: WORKSPACE }
)
const screen = render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: false })
)
)
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
act(() => addPrompt('A'))
await act(async () => fixture.hydrate?.())
await waitFor(() => expect(fixture.journal).toBeTypeOf('function'))
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT]))
await waitFor(() => expect(dismissIds()).toHaveLength(1))
const stamp = useAppStore.getState().acknowledgedAgentsByPaneKey[SUBJECT]
act(() => {
addPrompt('B')
publishView()
})
transport.dismiss
.mockImplementation(async (_ids, _panes, reads?: StructuredNotificationRead[]) => {
for (const read of reads ?? []) {
relay.retireStructuredAttention(read)
}
return { dismissed: 0 }
})
.mockRejectedValueOnce(new Error('scripted local retirement failure'))
expect(dismissIds()).toHaveLength(1)
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBe('agent-completion')
act(() => useAppStore.setState({ activeWorktreeId: WORKSPACE }))
screen.rerender(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() =>
expect(dismissIds()).toEqual(
['A', 'B'].map((id) => agentSessionPromptAttentionKey(SCOPE, SESSION, id))
)
)
expect(useAppStore.getState().acknowledgedAgentsByPaneKey[SUBJECT]).toBe(stamp)
expect(useAppStore.getState().unreadAgentCompletionPanes[SUBJECT]).toBeUndefined()
expect(readCalls()).toBe(2)
})
it('reads a newly accepted visible prompt without sending an RPC for text-only updates', async () => {
addPrompt('A')
render(
createElement(
Fragment,
@@ -259,69 +432,214 @@ it('retries a failed local desktop relay withdrawal on a later explicit read', a
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(dismissIds()).toHaveLength(1)
expect(withdrawals).toEqual([])
await act(async () => useAppStore.getState().acknowledgeAgents([SUBJECT]))
await waitFor(() => expect(withdrawals).toEqual([sent.notificationId]))
expect(transport.dismiss.mock.calls.filter(([, , reads]) => Array.isArray(reads))).toHaveLength(
2
)
} finally {
rmSync(relayDirectory, { recursive: true, force: true })
}
})
it('an older failed attempt cannot erase a newer success when the observation returns to A', async () => {
addPrompt('A')
const original = transport.call.getMockImplementation()
let release: (() => void) | undefined
let first = true
transport.call.mockImplementation(async (...args) => {
if (args[1] === 'agentSession.acknowledgeAttention' && first) {
first = false
return await new Promise((resolve) => {
release = () => resolve({ acknowledged: false })
})
}
return original?.(...args)
})
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(readCalls()).toBe(1)
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT]))
expect(readCalls()).toBe(1)
act(() => {
addPrompt('B')
publishView()
})
await waitFor(() => expect(readCalls()).toBe(2))
await act(async () => {})
act(() => {
fixture.items = fixture.items.map((item): AgentJournalRenderItem =>
item.itemId === 'B' && item.body.kind === 'approval'
? {
...item,
revision: item.revision + 1,
body: { ...item.body, resolution: { ...item.body.resolution, state: 'resolved' } }
await waitFor(() => expect(readCalls()).toBe(1))
act(() => {
addPrompt('B')
publishView()
})
await waitFor(() => expect(dismissIds()).toHaveLength(2))
const calls = readCalls()
act(() => {
fixture.items = [
...fixture.items,
{
itemId: 'text',
revision: 1,
sequence: ++fixture.sequence,
observedAt: fixture.sequence,
body: {
kind: 'message',
role: 'assistant',
blocks: [{ type: 'text', text: 'More output' }]
}
: item
}
]
publishView()
})
await act(async () => {})
expect(readCalls()).toBe(calls)
})
it('keeps hydration unread while away and retries the read on presence return', async () => {
transport.away.mockResolvedValue(true)
addPrompt('A')
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
fixture.sequence += 1
fixture.hostFeed.observe(SESSION)
publishView()
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(readCalls()).toBe(0)
transport.away.mockResolvedValue(false)
await act(async () => window.dispatchEvent(new Event('focus')))
await waitFor(() => expect(dismissIds()).toHaveLength(1))
})
it.each(['transport-error', 'false-result'] as const)(
'retries a remote %s on a later read with the existing presence gate',
async (failure) => {
const target = { kind: 'environment', environmentId: 'retry-host' } as const
const tab = useAppStore.getState().unifiedTabsByWorktree[WORKSPACE]?.[0]
if (!tab) {
throw new Error('chat tab missing')
}
useAppStore.setState({
unifiedTabsByWorktree: { [WORKSPACE]: [{ ...tab, executionHostId: 'runtime:retry-host' }] }
})
addPrompt('A')
const original = transport.call.getMockImplementation()
let failed = false
transport.call.mockImplementation(async (...args) => {
if (args[1] === 'agentSession.acknowledgeAttention' && !failed) {
failed = true
if (failure === 'transport-error') {
throw new Error('scripted transient transport failure')
}
return { acknowledged: false }
}
return original?.(...args)
})
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true, target: target })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(readCalls()).toBe(1)
expect(dismissIds()).toEqual([])
await act(async () => {})
expect(readCalls()).toBe(1)
if (failure === 'transport-error') {
await act(async () => useAppStore.getState().acknowledgeAgents([SUBJECT]))
} else {
transport.away.mockResolvedValue(true)
await act(async () => window.dispatchEvent(new Event('focus')))
expect(readCalls()).toBe(1)
transport.away.mockResolvedValue(false)
await act(async () => window.dispatchEvent(new Event('focus')))
}
await waitFor(() =>
expect(dismissIds()).toEqual([agentSessionPromptAttentionKey(SCOPE, SESSION, 'A')])
)
expect(readCalls()).toBe(2)
expect(
transport.call.mock.calls
.filter(([, method]) => method === 'agentSession.acknowledgeAttention')
.map(([owner]) => owner)
).toEqual([target, target])
}
)
it('retries a failed local desktop relay withdrawal on a later explicit read', async () => {
const relayDirectory = mkdtempSync(join(tmpdir(), 'orca-relay-read-retry-'))
try {
addPrompt('A')
const sent = fixture.events.find((event) => event.type === 'notification')
if (!sent?.notificationId || sent.type !== 'notification') {
throw new Error('prompt not sent')
}
const relay = new RuntimeMobileNotificationController()
relay.configureDismissalStore(relayDirectory)
relay.dispatch(sent)
const withdrawals: string[] = []
relay.onDispatched((event) => {
if (event.type === 'dismiss') {
withdrawals.push(event.notificationId)
}
})
transport.dismiss
.mockImplementation(async (_ids, _panes, reads?: StructuredNotificationRead[]) => {
for (const read of reads ?? []) {
relay.retireStructuredAttention(read)
}
return { dismissed: 0 }
})
.mockRejectedValueOnce(new Error('scripted local retirement failure'))
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(dismissIds()).toHaveLength(1)
expect(withdrawals).toEqual([])
await act(async () => useAppStore.getState().acknowledgeAgents([SUBJECT]))
await waitFor(() => expect(withdrawals).toEqual([sent.notificationId]))
expect(
transport.dismiss.mock.calls.filter(([, , reads]) => Array.isArray(reads))
).toHaveLength(2)
} finally {
rmSync(relayDirectory, { recursive: true, force: true })
}
})
it('an older failed attempt cannot erase a newer success when the observation returns to A', async () => {
addPrompt('A')
const original = transport.call.getMockImplementation()
let release: (() => void) | undefined
let first = true
transport.call.mockImplementation(async (...args) => {
if (args[1] === 'agentSession.acknowledgeAttention' && first) {
first = false
return await new Promise((resolve) => {
release = () => resolve({ acknowledged: false })
})
}
return original?.(...args)
})
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: true })
)
)
await waitFor(() => expect(fixture.hydrate).toBeTypeOf('function'))
await act(async () => fixture.hydrate?.())
expect(readCalls()).toBe(1)
act(() => useAppStore.getState().acknowledgeAgents([SUBJECT]))
expect(readCalls()).toBe(1)
act(() => {
addPrompt('B')
publishView()
})
await waitFor(() => expect(readCalls()).toBe(2))
await act(async () => {})
act(() => {
fixture.items = fixture.items.map((item): AgentJournalRenderItem =>
item.itemId === 'B' && item.body.kind === 'approval'
? {
...item,
revision: item.revision + 1,
body: { ...item.body, resolution: { ...item.body.resolution, state: 'resolved' } }
}
: item
)
fixture.sequence += 1
fixture.hostFeed.observe(SESSION)
publishView()
})
await waitFor(() => expect(readCalls()).toBe(3))
await act(async () => release?.())
await act(async () => useAppStore.getState().acknowledgeAgents([SUBJECT]))
expect(readCalls()).toBe(3)
expect(dismissIds()).toHaveLength(2)
})
await waitFor(() => expect(readCalls()).toBe(3))
await act(async () => release?.())
await act(async () => useAppStore.getState().acknowledgeAgents([SUBJECT]))
expect(readCalls()).toBe(3)
expect(dismissIds()).toHaveLength(2)
})
@@ -1,354 +0,0 @@
// @vitest-environment happy-dom
// Register preload/store seams before importing their consumers.
import {
fixture,
REMOTE_TARGET,
NOTIFICATION_SETTINGS,
transport,
WORKSPACE,
TAB,
addPrompt,
publishStatus,
publishView,
ReadSurface,
AttentionPolicy,
readCalls
} from './structured-attention-read-retirement.test-fixture'
import { Fragment, createElement } from 'react'
import { act, render, waitFor } from '@testing-library/react'
import { expect, it } from 'vitest'
import { join } from 'node:path'
import type { NotificationDispatchRequest } from '../../src/shared/notification-settings-types'
import type { AgentJournalRenderItem } from '../../src/shared/agent-session-journal-types'
import type { AgentSessionExecutionLocation } from '../../src/shared/agent-session-record'
import { RuntimeMobileNotificationController } from '../../src/main/runtime/runtime-mobile-notification-controller'
import { MobileNotificationDismissalStore } from '../../src/main/runtime/mobile-notification-dismissal-store'
import { createNotificationDeliveryService } from '../../src/main/notifications/notification-delivery-service'
import { SESSION } from '../../src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture'
import { makeUnifiedTab, makeWorktree } from '@/store/slices/store-test-helpers'
import { useAppStore } from '@/store'
import { StructuredAgentSessionAttentionBridge } from '@/components/native-chat/StructuredAgentSessionAttentionBridge'
/** A remote-host chat this desktop relays to its own phones while the user is away. */
async function mountRemoteRelay(options?: { promptStatus?: false; raise?: false }) {
const relayDirectory = join(fixture.directory, 'relay')
const relay = new RuntimeMobileNotificationController()
relay.configureDismissalStore(relayDirectory)
const delivery = createNotificationDeliveryService({
readNotificationSettings: () => NOTIFICATION_SETTINGS,
findActiveWindow: () => null,
isWindowVisible: () => false,
setTrayAttention: () => {},
isNotificationSupported: () => false,
dispatchMobileNotification: relay.dispatch.bind(relay),
readAuthorizationStatus: async () => 'authorized',
recordDeliveryOutcome: () => {},
deliverNative: () => ({ delivered: true }),
platform: 'linux',
now: () => 20_000
})
transport.dispatch.mockImplementation(async (request: NotificationDispatchRequest) =>
delivery.dispatch(request)
)
// What the notifications:settleStructuredPrompts handler does with it.
transport.settle.mockImplementation(async (scope: AgentSessionExecutionLocation, id: string) =>
relay.reconcileStructuredPromptAttention({ scope, sessionId: id, pendingPromptIds: [] })
)
transport.away.mockResolvedValue(true)
useAppStore.setState({
activeWorktreeId: 'elsewhere',
activeWorkspaceExecutionHostId: 'runtime:relay-host',
worktreesByRepo: {
repo1: [
makeWorktree({
id: WORKSPACE,
repoId: 'repo1',
hostId: 'runtime:relay-host',
runtimeOwnerEnvironmentId: 'relay-host'
})
]
},
unifiedTabsByWorktree: {
[WORKSPACE]: [
makeUnifiedTab({
id: TAB,
worktreeId: WORKSPACE,
groupId: 'group',
contentType: 'agent-session',
entityId: SESSION,
agentSessionAgent: 'claude',
executionHostId: 'runtime:relay-host'
})
]
},
runtimeEnvironments: [
{
id: 'relay-host',
name: 'Remote',
createdAt: 1,
updatedAt: 1,
lastUsedAt: null,
runtimeId: null,
endpoints: [],
preferredEndpointId: 'endpoint'
}
]
})
render(
createElement(
Fragment,
null,
createElement(StructuredAgentSessionAttentionBridge),
createElement(AttentionPolicy),
createElement(ReadSurface, { viewed: false, target: REMOTE_TARGET })
)
)
await waitFor(() => expect(fixture.completion).toBeTypeOf('function'))
await waitFor(() => expect(fixture.status).toBeTypeOf('function'))
act(() => publishStatus())
const live = (kind: 'prompt' | 'completion') =>
new MobileNotificationDismissalStore(relayDirectory)
.liveDeliveries()
.filter((entry) => entry.structuredOrigin?.cause.kind === kind)
if (options?.raise === false) {
return { live }
}
act(() => {
addPrompt('A')
if (options?.promptStatus !== false) {
publishStatus()
}
publishView()
})
await waitFor(() => expect(live('prompt')).toHaveLength(1))
return { live }
}
function revise(update: (item: AgentJournalRenderItem) => AgentJournalRenderItem): void {
fixture.sequence += 1
fixture.items = fixture.items.map((item) => {
const next = update(item)
return next === item
? item
: { ...next, revision: item.revision + 1, sequence: fixture.sequence }
})
}
const resolvePrompt =
(id: string, state: 'resolved' | 'cancelled') =>
(item: AgentJournalRenderItem): AgentJournalRenderItem =>
item.itemId === id && item.body.kind === 'approval'
? {
...item,
body: {
...item.body,
resolution: {
state,
selectedOptionId: state === 'resolved' ? 'yes' : null,
resolvedBy: state === 'resolved' ? 'second-client' : null,
resolvedAt: fixture.sequence
}
}
}
: item
const resolveA = (state: 'resolved' | 'cancelled') => resolvePrompt('A', state)
const interrupt = (item: AgentJournalRenderItem): AgentJournalRenderItem =>
item.body.kind === 'turn'
? { ...item, body: { ...item.body, state: 'interrupted', outcome: 'cancellation' } }
: item
/** A frame the status socket queued before A's commit: another session's row. */
function otherSessionStatus(): void {
fixture.status?.({
id: 'status',
ok: true,
_meta: { runtimeId: 'h' },
result: {
type: 'status',
session: {
sessionId: 'other-session',
workspaceId: WORKSPACE,
agent: 'claude',
status: 'working',
latestPrompt: '',
updatedAt: 1
}
}
})
}
function endStatusStream(): void {
fixture.status?.({ id: 'status', ok: true, _meta: { runtimeId: 'h' }, result: { type: 'end' } })
}
it.each([
['answered from another client', () => revise(resolveA('resolved'))],
['turn cancelled', () => revise((item) => interrupt(resolveA('cancelled')(item)))]
] as const)(
'withdraws the relayed prompt alert once the remote host reports none pending: %s',
async (_mode, resolve) => {
const relay = await mountRemoteRelay()
act(() => {
resolve()
fixture.hostFeed.observe(SESSION)
publishStatus()
})
await waitFor(() => expect(relay.live('prompt')).toEqual([]))
expect(readCalls()).toBe(0)
expect(transport.settle).toHaveBeenCalledOnce()
// Prompt settlement never touches a completion alert.
const relayedCompletions = transport.dispatch.mock.calls.filter(
([request]: [NotificationDispatchRequest]) =>
request.structuredOrigin?.cause.kind === 'completion'
)
expect(relay.live('completion')).toHaveLength(relayedCompletions.length)
}
)
it('withdraws it when a restarted remote host comes back with the prompt cancelled', async () => {
const relay = await mountRemoteRelay()
const before = fixture.status
act(() => endStatusStream())
revise((item) => interrupt(resolveA('cancelled')(item)))
await waitFor(() => expect(fixture.status).not.toBe(before))
act(() => publishStatus())
await waitFor(() => expect(relay.live('prompt')).toEqual([]))
})
it('keeps the relayed prompt alert while the host still reports a prompt pending', async () => {
const relay = await mountRemoteRelay()
act(() => {
revise((item) => item)
publishStatus()
})
await act(async () => {})
expect(transport.settle).not.toHaveBeenCalled()
expect(relay.live('prompt')).toHaveLength(1)
})
it('never settles from a status the desktop lost contact with', async () => {
// The mirror still holds the row from before A, which reads no prompt; it goes unverifiable.
const relay = await mountRemoteRelay({ promptStatus: false })
act(() => endStatusStream())
await act(async () => {})
expect(transport.settle).not.toHaveBeenCalled()
expect(relay.live('prompt')).toHaveLength(1)
})
it('settles once per relayed prompt, not on every later status update', async () => {
await mountRemoteRelay()
act(() => {
revise(resolveA('resolved'))
fixture.hostFeed.observe(SESSION)
publishStatus()
})
await waitFor(() => expect(transport.settle).toHaveBeenCalledOnce())
act(() => {
revise((item) => item)
publishStatus()
})
await act(async () => {})
expect(transport.settle).toHaveBeenCalledOnce()
})
// Status and edges ride separate sockets: the pre-prompt row can still be draining after the edge.
it('keeps the pending prompt alert when the edge outruns its status and another frame lands', async () => {
const relay = await mountRemoteRelay({ promptStatus: false })
act(() => otherSessionStatus())
await act(async () => {})
act(() => publishStatus())
await act(async () => {})
expect(transport.settle).not.toHaveBeenCalled()
expect(relay.live('prompt')).toHaveLength(1)
})
it('keeps it when a queued frame lands in the same task as the edge', async () => {
const relay = await mountRemoteRelay({ promptStatus: false })
act(() => {
addPrompt('B')
otherSessionStatus()
})
await act(async () => {})
expect(transport.settle).not.toHaveBeenCalled()
expect(relay.live('prompt')).toHaveLength(2)
})
it('retries a settlement that failed on the next qualifying row', async () => {
const relay = await mountRemoteRelay()
const settle = transport.settle.getMockImplementation()
transport.settle.mockRejectedValueOnce(new Error('main unavailable'))
act(() => {
revise(resolveA('resolved'))
fixture.hostFeed.observe(SESSION)
publishStatus()
})
await waitFor(() => expect(transport.settle).toHaveBeenCalledOnce())
expect(relay.live('prompt')).toHaveLength(1)
transport.settle.mockImplementation(settle ?? (async () => {}))
act(() => {
revise((item) => item)
publishStatus()
})
await waitFor(() => expect(relay.live('prompt')).toEqual([]))
expect(transport.settle).toHaveBeenCalledTimes(2)
})
it('settles a prompt answered while the previous settle call was still out', async () => {
const relay = await mountRemoteRelay()
const reconcile = transport.settle.getMockImplementation()
const replies: (() => void)[] = []
// Main reconciles on arrival; only the reply is late.
transport.settle.mockImplementation((scope: AgentSessionExecutionLocation, id: string) => {
void reconcile?.(scope, id)
return new Promise<void>((resolve) => replies.push(resolve))
})
act(() => {
revise(resolveA('resolved'))
fixture.hostFeed.observe(SESSION)
publishStatus()
})
await waitFor(() => expect(replies).toHaveLength(1))
act(() => {
addPrompt('B')
publishStatus()
})
await waitFor(() => expect(relay.live('prompt')).toHaveLength(1))
act(() => {
revise(resolvePrompt('B', 'resolved'))
fixture.hostFeed.observe(SESSION)
publishStatus()
})
await act(async () => {})
expect(replies).toHaveLength(1)
// The host goes quiet; the reply alone must re-judge the mirror.
await act(async () => replies[0]?.())
await waitFor(() => expect(relay.live('prompt')).toEqual([]))
})
it('settles a prompt whose edge arrives after its own resolution row', async () => {
const relay = await mountRemoteRelay({ raise: false })
const deliver = fixture.completion
const held: Parameters<NonNullable<typeof deliver>>[0][] = []
fixture.completion = (response) => held.push(response)
act(() => {
addPrompt('A')
publishStatus()
revise(resolveA('resolved'))
fixture.hostFeed.observe(SESSION)
publishStatus()
})
fixture.completion = deliver
act(() => held.forEach((response) => deliver?.(response)))
await waitFor(() =>
expect(
transport.dispatch.mock.calls.some(
([request]: [NotificationDispatchRequest]) =>
request.structuredOrigin?.cause.kind === 'prompt'
)
).toBe(true)
)
await waitFor(() => expect(relay.live('prompt')).toEqual([]))
expect(transport.settle).toHaveBeenCalledOnce()
})