test: remove 52 low-value plugin, checks, and runtime tests (#38909)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-06 00:17:38 -08:00
committed by GitHub
co-authored by OrcaWin
parent 9cbef961cc
commit 687b0ee252
17 changed files with 0 additions and 2109 deletions
@@ -104,84 +104,4 @@ describe('Claude plugin UI frames', () => {
)
).toEqual([[{ type: 'text', text: 'PROBE_OK' }]])
})
it('keeps the other plugin UI subtypes off the transcript too', () => {
const state = sinkState()
const translator = createClaudeJournalTranslator({ sink: state.sink })
for (const [index, subtype] of [
'ui_invalidate',
'ui_log',
'ui_panes',
'ui_placed',
'ui_scroll',
'ui_focus',
'ui_future',
'future_notice'
].entries()) {
translator.handle(pluginUi(subtype, `ui-${index}`, {}))
}
translator.handle(
pluginUi('ui_panes', 'historical-focus', {
panes: [{ id: 'blast-radius', plugin: 'blast-radius' }],
focus_requested_id: 'blast-radius'
})
)
expect(providerFrameKinds(state.items)).toEqual([])
})
it.each(['ui_status', 'future_notice', undefined])(
'does not flush streamed text for N or 10N %s diagnostics',
(subtype) => {
const writes = (count: number) => {
const state = sinkState()
const translator = createClaudeJournalTranslator({
sink: state.sink,
schedule: () => () => {}
})
translator.handle(userTurn('user-stream', 'Reply'))
let sequence = 0
const stream = (event: Record<string, unknown>) =>
translator.handle({
type: 'message',
sessionId: 'orca-session',
message: {
type: 'stream_event',
uuid: `stream-${sequence++}`,
session_id: 'claude-session',
event
}
})
stream({
type: 'message_start',
message: { id: 'msg-stream', role: 'assistant', content: [] }
})
stream({ type: 'content_block_start', index: 0, content_block: { type: 'text', text: '' } })
for (let i = 0; i < count; i++) {
stream({
type: 'content_block_delta',
index: 0,
delta: { type: 'text_delta', text: 'a' }
})
const frame = pluginUi(subtype, `status-${i}`, {
text: 'PR 6 · 진행 13 · 대기 6 /board'
})
translator.handle(frame)
}
const assistantWrites = () =>
state.items.filter(({ body }) => body.kind === 'message' && body.role === 'assistant')
expect(assistantWrites()).toHaveLength(0)
expect(providerFrameKinds(state.items)).toEqual([])
translator.flush()
expect(assistantWrites()).toHaveLength(1)
expect(assistantWrites()[0].body).toMatchObject({
blocks: [{ type: 'text', text: 'a'.repeat(count) }]
})
translator.dispose()
return assistantWrites().length
}
expect(writes(20)).toBe(writes(200))
}
)
})
@@ -1,421 +0,0 @@
import { describe, expect, it } from 'vitest'
import capture from './__fixtures__/claude-plugin-recovery.json'
import { asRecord } from '../ai-vault/session-scanner-values'
import { createClaudeJournalTranslator } from './structured/journal/claude-structured-journal-translation'
import { agentSessionFailureFact } from '../../shared/agent-session/failures/agent-session-failure'
import { agentJournalItemKey } from '../../shared/agent-session/journal/agent-session-journal-item-key'
import { resolveClaudeReplayTurn } from './claude-replay-turn-resolution'
import { dispatchClaudeTurn } from './structured/claude-structured-dispatch'
import {
sessionFor,
userMessage,
userReplayFrame
} from './structured/claude-structured-dispatch-test-support'
import {
SESSION,
SID,
at,
chat,
chatTurn,
prompt,
reply
} from './claude-transcript-catch-up-test-support'
// Claude 2.1.316: background wake and two resumed, interrupted tool calls.
const rows: Record<string, unknown>[] = capture.map((row, index) => ({
...row,
sessionId: SID,
...(index === 0 ? { parentUuid: 'a1' } : {})
}))
const frame = (row: Record<string, unknown>) => ({
...row,
session_id: SID,
parent_tool_use_id: null
})
const synthetic = rows[3]!
const task = rows[0]!
const message = (row: Record<string, unknown>) => asRecord(row.message)!
const contentParts = (row: Record<string, unknown>) => {
const content = message(row).content
if (!Array.isArray(content)) {
throw new Error('recorded content parts missing')
}
return content
}
it('imports recorded repair results without inventing outside work or duplicate failed turns', async () => {
const c = await chat()
await c.live()
const translator = createClaudeJournalTranslator({ sink: c.deferred.sink })
const live = (row: Record<string, unknown>, startsTurn = false) =>
translator.handle({
type: 'message',
sessionId: SESSION,
message: frame(row),
...(startsTurn ? { startsTurn: true } : {}),
observedAt: 4_000
})
live({
type: 'system',
subtype: 'task_started',
task_id: 'bobbnxies',
task_type: 'local_bash',
description: 'background work'
})
live({ type: 'system', subtype: 'task_notification', task_id: 'bobbnxies', status: 'completed' })
live(task)
live(rows[1]!)
const end = () =>
translator.handle({
type: 'ended',
sessionId: SESSION,
reason: 'crash',
failure: agentSessionFailureFact('pluginInputRequired'),
observedAt: 5_000
})
end()
const localPrompt = rows[4]!
await c.journal.appendSubmission({
clientMessageId: 'retry',
body: userMessage([{ type: 'text', text: 'retry' }]),
payloadFingerprint: 'retry',
fence: 1
})
await c.journal.resolveDispatch({
clientMessageId: 'retry',
state: 'accepted',
providerIdentity: { provider: 'claude', sessionId: SID, uuid: String(localPrompt.uuid) },
fence: 1
})
live(localPrompt, true)
live(rows[5]!)
end()
translator.dispose()
await c.deferred.drained()
const before = c.journal.snapshot()
const turns = before.items.filter((item) => item.body.kind === 'turn')
await c.write([...chatTurn, ...rows])
expect(await c.open('a1')).toBe(true)
const after = c.journal.snapshot()
expect(after.items.filter((item) => item.body.kind === 'turn')).toEqual(turns)
for (const row of [task, synthetic, rows[7]!]) {
expect(
c.journal.item(
agentJournalItemKey({ provider: 'claude', sessionId: SID, uuid: String(row.uuid) })
)
).toBeNull()
}
const tools = after.items.filter((item) => item.body.kind === 'tool-call')
expect(tools).toHaveLength(2)
for (const tool of tools) {
expect(tool.body).toMatchObject({ name: 'Bash', state: 'failed' })
const original = before.items.find((item) => item.itemId === tool.itemId)!
expect(tool.turnScope).toEqual(original.turnScope)
expect(tool.body.kind === 'tool-call' && tool.body.input).toEqual(
original.body.kind === 'tool-call' && original.body.input
)
}
expect(c.rows().some((row) => row.startsWith('(continued-outside'))).toBe(false)
expect(await c.open('a1')).toBe(true)
expect(c.journal.snapshot()).toEqual(after)
expect((await c.replay()).snapshot()).toEqual(after)
const outsideTask = { ...task, uuid: 'outside-task', parentUuid: rows[7]!.uuid }
await c.write([
outsideTask,
reply(
'outside-answer',
'outside-task',
'msg_outside',
{ type: 'text', text: 'real outside answer' },
10
)
])
expect(await c.open('a1')).toBe(true)
expect(c.rows()).toContain('assistant:real outside answer')
expect(c.rows().filter((row) => row === '(continued-outside)')).toHaveLength(1)
const outside = c.journal
.snapshot()
.items.find(
(item) =>
item.body.kind === 'message' &&
item.body.role === 'assistant' &&
item.body.blocks.some(
(block) => block.type === 'text' && block.text === 'real outside answer'
)
)!
const outsideScope = outside.turnScope
expect(outsideScope?.kind).toBe('turn')
if (outsideScope?.kind !== 'turn') {
throw new Error('outside turn missing')
}
expect(c.journal.item(outsideScope.turnItemId)?.body).toMatchObject({
kind: 'turn',
state: 'completed'
})
expect(turns.some((turn) => turn.itemId === outsideScope.turnItemId)).toBe(false)
expect(
c.journal.snapshot().items.filter((item) => turns.some((turn) => turn.itemId === item.itemId))
).toEqual(turns)
const caughtUp = c.journal.snapshot()
expect(await c.open('a1')).toBe(true)
expect(c.journal.snapshot()).toEqual(caughtUp)
expect((await c.replay()).snapshot()).toEqual(caughtUp)
})
it.each([false, true])('ignores quiet caveat time with a later prompt: %s', async (laterPrompt) => {
const c = await chat()
await c.live()
await c.write([
...chatTurn,
prompt('outside', 'a1', 'outside work', 10),
reply('answer', 'outside', 'msg_answer', { type: 'text', text: 'done' }, 11),
{
...prompt('caveat', 'answer', '<local-command-caveat>internal</local-command-caveat>', 60),
isMeta: true
},
...(laterPrompt ? [prompt('next', 'caveat', 'next request', 70)] : [])
])
expect(await c.open('a1')).toBe(true)
const turn = c.journal
.snapshot()
.items.find((item) => item.body.kind === 'turn' && item.body.turnId === 'outside')
expect(turn?.body).toMatchObject({
startedAt: Date.parse(at(10)),
completedAt: Date.parse(at(11))
})
})
it('preserves a task boundary without inventing a successor until real output arrives', async () => {
const c = await chat()
await c.live()
const wake = { ...task, ...prompt('wake', 'answer-again', String(message(task).content), 60) }
await c.write([
...chatTurn,
prompt('outside', 'a1', 'outside work', 10),
reply('answer', 'outside', 'msg_answer', { type: 'text', text: 'first answer' }, 11),
{ ...prompt('quiet', 'answer', 'quiet metadata', 12), isMeta: true },
reply('answer-again', 'quiet', 'msg_again', { type: 'text', text: 'continued answer' }, 13),
wake
])
expect(await c.open('a1')).toBe(true)
const before = c.journal.snapshot()
const turn = before.items.find(
(item) => item.body.kind === 'turn' && item.body.turnId === 'outside'
)!
expect(turn.body).toMatchObject({
state: 'completed',
startedAt: Date.parse(at(10)),
completedAt: Date.parse(at(13))
})
expect(before.items.filter((item) => item.body.kind === 'turn')).toHaveLength(2)
expect(c.rows().filter((row) => row === '(continued-outside)')).toHaveLength(1)
const continued = before.items.find(
(item) =>
item.body.kind === 'message' &&
item.body.role === 'assistant' &&
item.body.blocks.some((block) => block.type === 'text' && block.text === 'continued answer')
)!
expect(continued.turnScope).toEqual({ kind: 'turn', turnItemId: turn.itemId })
expect(await c.open('a1')).toBe(true)
expect(c.journal.snapshot()).toEqual(before)
await c.write([
reply('wake-answer', 'wake', 'msg_wake', { type: 'text', text: 'wake answer' }, 61)
])
expect(await c.open('a1')).toBe(true)
const after = c.journal.snapshot()
const answer = after.items.find(
(item) =>
item.body.kind === 'message' &&
item.body.role === 'assistant' &&
item.body.blocks.some((block) => block.type === 'text' && block.text === 'wake answer')
)!
const scope = answer.turnScope
expect(scope?.kind).toBe('turn')
if (scope?.kind !== 'turn') {
throw new Error('wake turn missing')
}
expect(scope.turnItemId).not.toBe(turn.itemId)
expect(c.journal.item(scope.turnItemId)?.body).toMatchObject({ kind: 'turn', state: 'completed' })
expect(c.journal.item(turn.itemId)).toEqual(turn)
expect(after.items.filter((item) => item.body.kind === 'turn')).toHaveLength(3)
// Each separate catch-up appends one genuine outside stretch.
expect(c.rows().filter((row) => row === '(continued-outside)')).toHaveLength(2)
expect(await c.open('a1')).toBe(true)
expect(c.journal.snapshot()).toEqual(after)
expect((await c.replay()).snapshot()).toEqual(after)
})
it('keeps mixed task results with their saved caller before an independent reply', async () => {
const c = await chat()
await c.live({ tool: true, interrupted: true })
const saved = c.journal.snapshot()
const tool = saved.items.find((item) => item.body.kind === 'tool-call')!
const failed = saved.items.filter((item) => item.body.kind === 'turn')
await c.write([
...chatTurn,
{
...task,
...prompt('mixed-task', 'a1', '', 60),
message: {
role: 'user',
content: [
{ type: 'text', text: String(message(task).content) },
{ type: 'tool_result', tool_use_id: 'toolu_1', content: 'real result' }
]
}
},
reply(
'mixed-answer',
'mixed-task',
'msg_mixed',
{ type: 'text', text: 'independent answer' },
61
)
])
expect(await c.open('a1')).toBe(true)
expect(c.journal.item(tool.itemId)).toMatchObject({
turnScope: tool.turnScope,
body: { name: 'Read', input: { path: 'original' }, state: 'completed' }
})
expect(
c.journal.snapshot().items.filter((item) => failed.some((old) => old.itemId === item.itemId))
).toEqual(failed)
const answer = c.journal
.snapshot()
.items.find(
(item) =>
item.body.kind === 'message' &&
item.body.role === 'assistant' &&
item.body.blocks.some(
(block) => block.type === 'text' && block.text === 'independent answer'
)
)!
expect(answer.turnScope).not.toEqual(tool.turnScope)
const scope = answer.turnScope
if (scope?.kind !== 'turn') {
throw new Error('independent turn missing')
}
expect(c.journal.item(scope.turnItemId)?.body).toMatchObject({ kind: 'turn', state: 'completed' })
})
describe('internal content keeps live settlement and semantic siblings', () => {
it('does not open a turn or acknowledge a pending send, then accepts the real echo/result', async () => {
const c = await chat()
const translator = createClaudeJournalTranslator({ sink: c.deferred.sink })
const live = (row: Record<string, unknown>, startsTurn = false) =>
translator.handle({
type: 'message',
sessionId: SESSION,
message: frame(row),
...(startsTurn ? { startsTurn: true } : {})
})
live(synthetic)
live(task, true)
expect(translator.currentTurnId).toBeNull()
const session = sessionFor()
session.providerSessionId = SID
const sent = await dispatchClaudeTurn(session, {
sessionId: SESSION,
clientMessageId: 'real-send',
body: userMessage([{ type: 'text', text: String(message(task).content) }])
})
const waiter = session.dispatchWaiters[0]!
const internalEcho = { ...frame(task), uuid: waiter.sentUuid }
expect(resolveClaudeReplayTurn(session, internalEcho)).toBeNull()
expect(session.dispatchWaiters).toHaveLength(1)
const realEcho = {
...userReplayFrame(waiter.sentUuid, String(message(task).content)),
session_id: SID
}
expect(resolveClaudeReplayTurn(session, realEcho)).toMatchObject({
clientMessageId: 'real-send'
})
expect(sent).toEqual({ state: 'admitted' })
live(realEcho, true)
live(synthetic)
expect(translator.currentTurnId).toBe(waiter.sentUuid)
const result = { type: 'result', subtype: 'success', session_id: SID, uuid: 'real-result' }
live(result)
expect(session.dispatchWaiters).toHaveLength(0)
expect(translator.currentTurnId).toBeNull()
translator.dispose()
await c.deferred.drained()
expect(c.rows()).not.toContain('assistant:No response requested.')
})
it('keeps mixed injected tool results and synthetic tool siblings', async () => {
const c = await chat()
const translator = createClaudeJournalTranslator({ sink: c.deferred.sink })
const live = (row: Record<string, unknown>) =>
translator.handle({ type: 'message', sessionId: SESSION, message: frame(row) })
const call = rows[1]!
live({
...synthetic,
message: {
...message(synthetic),
content: [...contentParts(synthetic), ...contentParts(call)]
}
})
const result = rows[2]!
live({
...task,
message: {
...message(task),
content: [{ type: 'text', text: String(message(task).content) }, ...contentParts(result)]
}
})
live({ type: 'system', subtype: 'session_state_changed', state: 'idle' })
translator.dispose()
await c.deferred.drained()
expect(c.rows()).toContain('tool:Bash:failed')
expect(c.rows()).not.toContain('assistant:No response requested.')
})
})
it.each(['real-model', 'missing-usage', 'nonzero-usage', 'error', 'empty-error'] as const)(
'retains %s assistant semantics',
async (kind) => {
const c = await chat()
await c.live()
const nested = message(synthetic)
const variant: Record<string, unknown> = {
...synthetic,
parentUuid: 'a1',
...(kind === 'error' ? { isApiErrorMessage: true } : {}),
...(kind === 'empty-error' ? { error: 'authentication_failed' } : {}),
message: {
...nested,
...(kind === 'empty-error' ? { content: [] } : {}),
...(kind === 'real-model' ? { model: 'claude-opus-5-5' } : {}),
...(kind === 'missing-usage' ? { usage: undefined } : {}),
...(kind === 'nonzero-usage'
? { usage: { ...asRecord(nested.usage), output_tokens: 1 } }
: {})
}
}
await c.write([...chatTurn, variant])
expect(await c.open('a1')).toBe(true)
expect(
c.journal.item(
agentJournalItemKey({ provider: 'claude', sessionId: SID, uuid: String(variant.uuid) })
)
).not.toBeNull()
}
)
it.each([undefined, { kind: 'human' }, { kind: 'future-origin' }])(
'retains canonical human text with origin %j',
async (origin) => {
const c = await chat()
await c.live()
const human = { ...prompt('external-human', 'a1', String(message(task).content), 10), origin }
await c.write([...chatTurn, human])
expect(await c.open('a1')).toBe(true)
expect(
c.journal.item(
agentJournalItemKey({ provider: 'claude', sessionId: SID, uuid: 'external-human' })
)?.body
).toMatchObject({ kind: 'message', role: 'user' })
}
)
@@ -1,155 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { readAgentJournalTurn } from '../../../shared/agent-session/lifecycle/agent-session-turn-record'
import type { AgentJournalItemBody } from '../../../shared/agent-session/journal/agent-session-journal-types'
import type { ClaudeStructuredSessionEvent } from './claude-structured-session-state'
import {
adapterFor,
claudeFrame,
fakeClaude,
identityFor,
recordingJournalSink,
tick,
USER_MESSAGE
} from './claude-structured-session-test-support'
import { claudeFocusedPlugin } from '../claude-plugin-ui'
// Claude Code 2.1.303 recorded shape; UUID and session ID are test identities.
const FOCUSED_PANE = {
type: 'system',
subtype: 'ui_panes',
panes: [{ id: 'blast-radius', title: 'Blast Radius', plugin: 'blast-radius', rows: 10 }],
shown_id: 'blast-radius',
focused_id: null,
focus_requested_id: 'blast-radius',
uuid: '00000000-0000-4000-8000-000000000795'
}
async function setup() {
const claude = fakeClaude()
const events: ClaudeStructuredSessionEvent[] = []
const persisted: unknown[] = []
const items: AgentJournalItemBody[] = []
const sink = {
...recordingJournalSink(),
appendItem: (_identity: unknown, body: AgentJournalItemBody) => {
items.push(body)
},
observeExit: vi.fn()
}
const adapter = adapterFor(claude, {}, events, persisted)
const acquire = (fence = 7) =>
adapter.acquire({
identity: identityFor(),
fence,
spawnToken: `spawn-${fence}`,
events: sink
})
const dispatch = (id = 'client-1', fence = 7) =>
adapter.dispatch({
sessionId: 'session-1',
clientMessageId: id,
body: USER_MESSAGE,
fence
})
await acquire()
return { claude, events, persisted, sink, items, adapter, acquire, dispatch }
}
describe('focused Claude plugin pane recovery', () => {
it.each([null, 'child-tool'])(
'closes the held child once without interrupt for parent %s',
async (parent) => {
const state = await setup()
const first = state.claude.connections[0]
await state.dispatch()
claudeFrame(first, { ...FOCUSED_PANE, parent_tool_use_id: parent })
claudeFrame(first, FOCUSED_PANE)
await vi.waitFor(() =>
expect(state.events.filter((event) => event.type === 'ended')).toHaveLength(1)
)
expect(first.closeCount).toBe(1)
expect(first.calls.some((call) => call.subtype === 'interrupt')).toBe(false)
expect(state.events.find((event) => event.type === 'ended')).toMatchObject({
failure: {
kind: 'pluginInputRequired',
detail: { text: 'blast-radius', audience: 'person' }
},
reason:
'Claude stopped because the "blast-radius" plugin is waiting for input in a terminal pane. Disable the plugin for chat sessions, or run the task in a Claude terminal.'
})
expect(state.sink.observeExit).toHaveBeenCalledOnce()
expect(state.persisted).toHaveLength(1)
expect(state.items.flatMap((body) => readAgentJournalTurn(body) ?? []).at(-1)).toMatchObject({
state: 'interrupted',
outcome: 'failure'
})
await state.adapter.closeAll()
}
)
it('keeps passive panes running and ignores foreign, idle and settled focus frames', async () => {
const state = await setup()
const first = state.claude.connections[0]
claudeFrame(first, FOCUSED_PANE)
await state.dispatch()
claudeFrame(first, { ...FOCUSED_PANE, focus_requested_id: null })
claudeFrame(first, { type: 'system', subtype: 'ui_invalidate' })
claudeFrame(first, { type: 'system', subtype: 'ui_status', text: null })
claudeFrame(first, { ...FOCUSED_PANE, session_id: 'foreign-session' })
await tick()
expect(first.closeCount).toBe(0)
claudeFrame(first, { type: 'result', subtype: 'success', is_error: false, result: 'OK' })
claudeFrame(first, FOCUSED_PANE)
await tick()
expect(first.closeCount).toBe(0)
expect(state.events.some((event) => event.type === 'ended')).toBe(false)
await state.adapter.closeAll()
})
it('waits for close then sends on a fresh connection, ignoring late frames from the old child', async () => {
const state = await setup()
const first = state.claude.connections[0]
const closed = Promise.withResolvers<boolean>()
const close = first.close
first.close = async () => {
await close()
return closed.promise
}
await state.dispatch()
claudeFrame(first, FOCUSED_PANE)
const next = state.acquire(8)
await tick()
expect(state.claude.connections).toHaveLength(1)
closed.resolve(true)
await next
await state.dispatch('client-next', 8)
const second = state.claude.connections[1]
claudeFrame(first, FOCUSED_PANE)
expect(second.closeCount).toBe(0)
claudeFrame(second, {
type: 'assistant',
uuid: 'fresh-reply',
message: { role: 'assistant', content: [{ type: 'text', text: 'FOLLOW_UP_OK' }] }
})
claudeFrame(second, {
type: 'result',
subtype: 'success',
is_error: false,
result: 'FOLLOW_UP_OK'
})
expect(first.sent.filter((message) => message.type === 'user')).toHaveLength(1)
expect(second.sent.filter((message) => message.type === 'user')).toHaveLength(1)
expect(state.events.filter((event) => event.type === 'ended')).toHaveLength(1)
await state.adapter.closeAll()
})
it('uses the focused pane plugin name, falling back to its trimmed ID', () => {
expect(claudeFocusedPlugin({ ...FOCUSED_PANE, focus_requested_id: ' blast-radius ' })).toBe(
'blast-radius'
)
expect(
claudeFocusedPlugin({ ...FOCUSED_PANE, panes: [], focus_requested_id: ' pane-2 ' })
).toBe('pane-2')
expect(claudeFocusedPlugin({ ...FOCUSED_PANE, focus_requested_id: ' ' })).toBeNull()
})
})
@@ -1,96 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitHubApiRepositoryModule from '../../github-api-repository'
const { ghExecFileAsyncMock } = vi.hoisted(() => ({ ghExecFileAsyncMock: vi.fn() }))
vi.mock('../../gh-utils', () => ({
ghExecFileAsync: ghExecFileAsyncMock,
acquire: vi.fn(async () => undefined),
release: vi.fn()
}))
vi.mock('../../github-repository-identity', () => ({
withGitHubRepoExecution: (_r: string, _c: unknown, _l: unknown, run: () => unknown) => run(),
githubRepoContext: (repoPath: string) => ({ repoPath }),
ghRepoExecOptions: (context: { repoPath: string }) => ({ cwd: context.repoPath })
}))
vi.mock('../../github-read-execution-scope', () => ({ githubReadExecutionScope: () => 'local' }))
vi.mock('../../github-api-repository', async (importOriginal) => ({
...(await importOriginal<typeof GitHubApiRepositoryModule>()),
resolveGitHubRepoExecution: async () => ({
ownerRepo: { owner: 'acme', repo: 'widgets' },
ghOptions: { cwd: '/repo' }
})
}))
vi.mock('../../rate-limit', () => ({ noteRepositoryRateLimitSpend: vi.fn() }))
vi.mock('../lookup/pr-lookup-rate-limit', () => ({ assertRateLimitBudget: vi.fn() }))
import { getPRChecks } from './get-pr-checks'
const HEAD = 'a'.repeat(40)
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
})
describe('getPRChecks run times', () => {
it('reads the rollup without gh’s disk cache, which ignores the PR head', async () => {
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
data: {
repository: {
pullRequest: {
headRefOid: HEAD,
commits: { nodes: [{ commit: { oid: HEAD, statusCheckRollup: null } }] }
}
}
}
})
})
await getPRChecks('/repo', 1, HEAD)
expect(ghExecFileAsyncMock.mock.calls[0]?.[0]).not.toContain('--cache')
})
it('keeps REST check-run times through the fallback’s schema', async () => {
ghExecFileAsyncMock
.mockRejectedValueOnce(new Error('GraphQL rollup failed'))
.mockResolvedValueOnce({
stdout: JSON.stringify({
total_count: 1,
check_runs: [
{
id: 88,
name: 'build',
status: 'completed',
conclusion: 'failure',
html_url: 'https://github.com/acme/widgets/runs/88',
details_url: null,
started_at: '2026-11-05T10:00:00Z',
completed_at: '2026-11-05T10:03:12Z'
}
]
})
})
.mockResolvedValueOnce({ stdout: JSON.stringify({ total_count: 0, statuses: [] }) })
.mockResolvedValueOnce({ stdout: JSON.stringify({ total_count: 0, check_suites: [] }) })
const [check] = await getPRChecks('/repo', 2, HEAD)
expect(check).toMatchObject({
startedAt: '2026-11-05T10:00:00Z',
completedAt: '2026-11-05T10:03:12Z'
})
})
it('asks an older gh without run-time fields for the fields it knows', async () => {
ghExecFileAsyncMock
.mockRejectedValueOnce(new Error('GraphQL rollup failed'))
.mockRejectedValueOnce(new Error('REST details failed'))
.mockRejectedValueOnce(
Object.assign(new Error('Command failed'), { stderr: 'Unknown JSON field: "startedAt"' })
)
.mockResolvedValueOnce({
stdout: JSON.stringify([{ name: 'lint', state: 'PASS', link: '' }])
})
const checks = await getPRChecks('/repo', 3, HEAD)
expect(ghExecFileAsyncMock.mock.calls[3]?.[0]).toContain('name,state,link')
expect(checks.map((check) => check.name)).toEqual(['lint'])
})
})
@@ -1,99 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
mapGraphQLPRChecksResponse,
mapRestCheckRun,
mapRestCommitStatus
} from './pr-checks-response-mapping'
import type { GraphQLPRChecksResponse, GraphQLStatusCheckContext } from './pr-checks-graphql-query'
const repo = { owner: 'acme', repo: 'widgets' }
function rollup(contexts: GraphQLStatusCheckContext[]): GraphQLPRChecksResponse {
return {
data: {
repository: {
pullRequest: {
headRefOid: 'a'.repeat(40),
commits: {
nodes: [
{
commit: {
oid: 'a'.repeat(40),
statusCheckRollup: { contexts: { nodes: contexts } }
}
}
]
}
}
}
}
}
}
describe('check run times', () => {
it('carries when each GraphQL check ran, dropping GitHub’s unset zero time', () => {
const checks = mapGraphQLPRChecksResponse(
repo,
rollup([
{
__typename: 'CheckRun',
name: 'build',
status: 'COMPLETED',
conclusion: 'FAILURE',
startedAt: '2026-11-05T10:00:00Z',
completedAt: '2026-11-05T10:03:12Z'
},
{
__typename: 'CheckRun',
name: 'lint',
status: 'QUEUED',
conclusion: null,
startedAt: '0001-01-01T00:00:00Z',
completedAt: null
},
{
__typename: 'StatusContext',
context: 'jenkins',
state: 'FAILURE',
createdAt: '2026-11-05T09:00:00Z'
},
{
__typename: 'StatusContext',
context: 'deploy',
state: 'PENDING',
createdAt: '2026-11-05T09:30:00Z'
}
])
)
expect(
checks?.map(({ name, startedAt, completedAt }) => ({ name, startedAt, completedAt }))
).toEqual([
{ name: 'build', startedAt: '2026-11-05T10:00:00Z', completedAt: '2026-11-05T10:03:12Z' },
{ name: 'lint', startedAt: undefined, completedAt: undefined },
// A commit status's one time is when its result was posted; a pending one has no run yet.
{ name: 'jenkins', startedAt: undefined, completedAt: '2026-11-05T09:00:00Z' },
{ name: 'deploy', startedAt: undefined, completedAt: undefined }
])
})
it('carries REST check-run and commit-status times', () => {
expect(
mapRestCheckRun({
name: 'build',
status: 'in_progress',
conclusion: null,
html_url: '',
details_url: null,
started_at: '2026-11-05T10:00:00Z',
completed_at: null
})
).toMatchObject({ startedAt: '2026-11-05T10:00:00Z' })
expect(
mapRestCommitStatus({
context: 'jenkins',
state: 'success',
created_at: '2026-11-05T09:00:00Z'
})
).toMatchObject({ completedAt: '2026-11-05T09:00:00Z' })
})
})
@@ -1,328 +0,0 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session/lifecycle/agent-session-mutation-envelope'
import { ClaudeStructuredSessionAdapter } from '../../claude/structured/claude-structured-session-adapter'
import {
claudeFrame,
fakeClaude,
PROVIDER_SESSION_ID,
type FakeConnection
} from '../../claude/structured/claude-structured-session-test-support'
import { openTestAgentSessionRecordStore } from '../../runtime/agent-session/records/agent-session-record-store-test-harness'
import { structuredClaudeLifecycleEvent } from '../../runtime/agent-session/installation/structured-claude-runtime-adapter'
import {
closeTestJournalHostDatabases,
openTestJournalHostDatabase
} from '../agent-session-journal/journal-host-database-test-support'
import { NO_STRUCTURED_AGENTS } from './structured-agent-session-adapter-router-test-support'
import { StructuredAgentSessionHost } from './structured-agent-session-host'
import {
HOST_TEST_NOW,
HOST_TEST_SESSION as SESSION,
hostTestAttachParams,
hostTestMessage,
hostTestOperationId,
resetHostTestOperationIds
} from './structured-agent-session-host-test-data'
import { createStructuredAgentSessionLogger } from './structured-agent-session-logger'
// Recorded pane content; ordering before the user echo is a constructed protocol contract.
const FOCUS = {
type: 'system',
subtype: 'ui_panes',
panes: [{ id: 'blast-radius', title: 'Blast Radius', plugin: 'blast-radius', rows: 10 }],
shown_id: 'blast-radius',
focused_id: null,
focus_requested_id: 'blast-radius'
}
const CALLER = { callerKey: 'client-1' }
let root: string
let host: StructuredAgentSessionHost
let adapter: ClaudeStructuredSessionAdapter
let claude: ReturnType<typeof fakeClaude>
let observedAt: number
let restoreConnections: (() => void)[]
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-claude-plugin-input-'))
resetHostTestOperationIds()
observedAt = HOST_TEST_NOW
restoreConnections = []
claude = fakeClaude({ replayUuid: null })
adapter = new ClaudeStructuredSessionAdapter({
resolveLaunch: async () => ({
pathToClaudeCodeExecutable: 'claude',
options: {},
cwd: root,
claudeConfigDir: join(root, 'claude-home'),
providerSessionId: PROVIDER_SESSION_ID,
resumeLeafUuid: null,
resumesTranscript: false,
cliFlag: async () => 'supported' as const,
continuesChain: claude.connections.length > 0
}),
onEvent: (event) => {
const mapped = structuredClaudeLifecycleEvent(event)
if (mapped) {
void host.handleAdapterEvent(mapped)
}
},
onDispatchSettledLate: (event) => void host.settleLateDispatch(event),
openConnection: claude.openConnection,
readProcessStartTime: async () => 1_700_000_000_000,
now: () => ++observedAt
})
host = new StructuredAgentSessionHost({
agents: NO_STRUCTURED_AGENTS,
logger: createStructuredAgentSessionLogger(),
store: await openTestAgentSessionRecordStore(root),
adapter: Object.assign(adapter, { supportsCreate: () => true }),
journalDatabase: openTestJournalHostDatabase(root),
claimKeyId: 'key-1',
mintSpawnToken: () => hostTestOperationId(),
now: () => ++observedAt
})
expect(
await host.attach(
CALLER,
hostTestAttachParams(null, {
provider: 'claude',
agent: 'claude',
accountHome: { variable: 'CLAUDE_CONFIG_DIR', path: join(root, 'claude-home') },
providerHandle: { kind: 'claude', sessionId: PROVIDER_SESSION_ID, leafUuid: null }
})
)
).toMatchObject({ ok: true })
await vi.waitFor(() => expect(child()?.phase).toBe('ready'))
})
afterEach(async () => {
for (const restore of restoreConnections) {
restore()
}
await adapter.closeAll()
await host.flushAllStreamedEvents()
await closeTestJournalHostDatabases()
claude.dispose()
await rm(root, { recursive: true, force: true })
})
function child() {
return host.collaboratorsForTests().sessions.get(SESSION)?.child
}
function envelope(method: string, fields: Record<string, unknown>, id = hostTestOperationId()) {
return {
sessionId: SESSION,
clientOperationId: id,
expectedRuntimeFence: null,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method,
sessionId: SESSION,
fields
})
}
}
async function send(text: string, queued = false) {
const fields = {
body: hostTestMessage(text),
...(queued ? { delivery: 'queue-if-active' as const } : {})
}
const request = envelope('agentSession.send', fields)
const result = await host.send(CALLER, { envelope: request, ...fields, userSend: true })
expect(result).toMatchObject({ ok: true })
return { id: request.clientOperationId, result }
}
async function snapshot() {
await host.flushStreamedEvents(SESSION)
return host.journalSnapshot(SESSION)
}
async function notices() {
return (await snapshot()).items.filter(
(item) => item.body.kind === 'status' && item.body.failure?.kind === 'pluginInputRequired'
)
}
async function expectReply(connection: FakeConnection, text: string) {
claudeFrame(connection, { ...connection.sent.at(-1)!, uuid: `echo-${text}` })
claudeFrame(connection, {
type: 'assistant',
uuid: `reply-${text}`,
message: { role: 'assistant', content: [{ type: 'text', text }] }
})
claudeFrame(connection, { type: 'result', subtype: 'success', is_error: false, result: text })
await vi.waitFor(async () => expect(JSON.stringify((await snapshot()).items)).toContain(text))
expect(
(await snapshot()).items.findLast((item) => item.body.kind === 'turn')?.body
).toMatchObject({ state: 'completed', outcome: 'success' })
}
async function startHeld() {
const first = claude.connections[0]!
const initial = await send('plugin probe')
await vi.waitFor(() => expect(first.sent).toHaveLength(1))
return { first, initial }
}
function holdClose(first: FakeConnection) {
const proof = Promise.withResolvers<boolean>()
const close = first.close
first.close = async () => {
await close()
return proof.promise
}
restoreConnections.push(() => {
proof.resolve(true)
first.close = close
})
return { proof, close }
}
it('fails an unechoed action before close proof; the quick next send joins close and gets a fresh reply', async () => {
const { first, initial } = await startHeld()
const { proof } = holdClose(first)
const originalChild = child()
claudeFrame(first, FOCUS)
expect(child()).toBe(originalChild)
expect(child()?.close?.failure?.kind).toBe('pluginInputRequired')
await vi.waitFor(async () => expect(await notices()).toHaveLength(1))
const failed = await snapshot()
expect(failed.items.findLast((item) => item.body.kind === 'turn')?.body).toMatchObject({
state: 'interrupted',
outcome: 'failure'
})
expect(failed.submissions.find((s) => s.clientMessageId === initial.id)?.dispatchState).not.toBe(
'accepted'
)
expect(host.readStatusSummary(SESSION)?.status).not.toBe('working')
expect(host.collaboratorsForTests().sessions.get(SESSION)?.lastEndedChild).toBeUndefined()
claudeFrame(first, { ...first.sent[0]!, uuid: 'late-failed-echo' })
claudeFrame(first, {
type: 'result',
subtype: 'success',
is_error: false,
result: 'late failure result'
})
expect(
(await snapshot()).submissions.find((s) => s.clientMessageId === initial.id)?.dispatchState
).not.toBe('accepted')
await send('fresh follow-up')
await new Promise((resolve) => setImmediate(resolve))
expect(claude.connections).toHaveLength(1)
expect(first.sent).toHaveLength(1)
proof.resolve(true)
await vi.waitFor(() => expect(claude.connections[1]?.sent).toHaveLength(1))
const second = claude.connections[1]!
claudeFrame(first, {
type: 'assistant',
message: { content: [{ type: 'text', text: 'LATE_OLD_OUTPUT' }] }
})
claudeFrame(first, { type: 'result', subtype: 'success', is_error: false, result: 'late result' })
claudeFrame(first, FOCUS)
await expectReply(second, 'FRESH_REPLY')
expect(await notices()).toHaveLength(1)
expect(JSON.stringify((await snapshot()).items)).not.toContain('LATE_OLD_OUTPUT')
expect(second.closeCount).toBe(0)
expect(first.calls.some((call) => call.subtype === 'interrupt')).toBe(false)
})
it('retains unproven ownership, then a legitimate next-send wake retries proof and completes delivery', async () => {
const { first } = await startHeld()
const { proof, close } = holdClose(first)
claudeFrame(first, FOCUS)
await vi.waitFor(async () => expect(await notices()).toHaveLength(1))
proof.resolve(false)
await new Promise((resolve) => setImmediate(resolve))
expect(child()?.close?.failure?.kind).toBe('pluginInputRequired')
expect(claude.connections).toHaveLength(1)
first.close = close
await send('retry after proof failure')
await vi.waitFor(() => expect(claude.connections[1]?.sent).toHaveLength(1))
await expectReply(claude.connections[1]!, 'RETRY_REPLY')
expect(first.closeCount).toBe(2)
expect(await notices()).toHaveLength(1)
})
it('holds queued cards through semantic and physical failure until explicit Resume dispatches and replies', async () => {
const { first } = await startHeld()
const queued = await send('queued follow-up', true)
expect(queued.result).toMatchObject({
ok: true,
value: { queued: { messageId: expect.any(String) } }
})
claudeFrame(first, FOCUS)
await vi.waitFor(() => expect(child()).toBeNull())
const journal = host.collaboratorsForTests().sessions.get(SESSION)!.journal
expect(journal.queuedMessages.pauses()).toContainEqual(
expect.objectContaining({ reason: 'failed' })
)
await host.flushStreamedEvents(SESSION)
await new Promise((resolve) => setImmediate(resolve))
expect(claude.connections).toHaveLength(1)
expect(first.sent).toHaveLength(1)
expect(
await host.queuedMessagesResume(CALLER, {
envelope: envelope('agentSession.queuedMessagesResume', {})
})
).toMatchObject({ ok: true, value: { resumed: true } })
await vi.waitFor(() => expect(claude.connections[1]?.sent).toHaveLength(1))
await expectReply(claude.connections[1]!, 'QUEUED_REPLY')
expect(await notices()).toHaveLength(1)
})
it.each([false, true])(
'keeps personal Stop precedence when focus arrives during interrupt (echoed=%s)',
async (echoed) => {
const { first } = await startHeld()
if (echoed) {
claudeFrame(first, { ...first.sent[0]!, uuid: 'stopped-user-echo' })
await vi.waitFor(async () =>
expect(
(await snapshot()).items.some(
(item) => item.body.kind === 'turn' && item.body.state === 'running'
)
).toBe(true)
)
}
claude.routes.interrupt = () => {
claudeFrame(first, FOCUS)
}
expect(
await host.cancel(CALLER, {
envelope: envelope('agentSession.cancel', {})
})
).toMatchObject({ ok: true })
await vi.waitFor(() => expect(child()).toBeNull())
expect(await notices()).toHaveLength(0)
expect(
(await snapshot()).items.findLast((item) => item.body.kind === 'turn')?.body
).toMatchObject({ state: 'interrupted', outcome: 'cancellation' })
}
)
it('finishes a retained unproven close when its exact process later reports exit', async () => {
const { first } = await startHeld()
const close = first.close
restoreConnections.push(() => {
first.close = close
})
first.close = async () => {
await close()
return false
}
claudeFrame(first, FOCUS)
await vi.waitFor(async () => expect(await notices()).toHaveLength(1))
expect(child()?.close).toBeDefined()
first.exitVerdict = { root: 'exited', tree: 'unverifiable' }
first.handlers.onExit?.(new Error('late real exit'), { expected: true })
await vi.waitFor(() => expect(child()).toBeNull())
expect(await notices()).toHaveLength(1)
await send('after late exit')
await vi.waitFor(() => expect(claude.connections[1]?.sent).toHaveLength(1))
await expectReply(claude.connections[1]!, 'LATE_EXIT_REPLY')
})
@@ -1,54 +0,0 @@
// #38434: clients judge an agent row's ownership by worktree, so a restored pane's status must name it.
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { partialRuntimeStore } from './orca-runtime-tests/runtime-store-partial-fixture'
// Preserve side-effect order: module registration, runtime, lifecycle, fixtures.
await import('./orca-runtime-tests/runtime-test-module-registration.spec')
const { OrcaRuntimeService } = await import('./orca-runtime')
await import('./orca-runtime-tests/orca-runtime-test-lifecycle.spec')
const {
HEADLESS_LEAF_ID,
TEST_WORKTREE_ID,
makeRuntimeStoreWithWorkspaceSession,
makeWorkspaceSessionWithHeadlessTerminal
} = await import('./orca-runtime-tests/orca-runtime-test-fixtures.spec')
// The previous run's hook time; restored rows keep it.
const RESTORED_AT = 1_700_000_000_000
// A minute after the restored row's hook; only the clock is fixed.
beforeEach(() => vi.useFakeTimers({ now: RESTORED_AT + 60_000, toFake: ['Date'] }))
afterEach(() => vi.useRealTimers())
it('names the worktree on a restored pane agent whose hook row carries none', async () => {
const { runtimeStore } = makeRuntimeStoreWithWorkspaceSession(
makeWorkspaceSessionWithHeadlessTerminal()
)
// Hydrated from the previous run's cache; the hook that reported it named no worktree.
const rows = [
{
paneKey: `host-tab:${HEADLESS_LEAF_ID}`,
agentType: 'codex' as const,
providerSession: { key: 'session_id' as const, id: 'codex-session' },
prompt: 'ship it',
state: 'working' as const,
receivedAt: RESTORED_AT,
stateStartedAt: RESTORED_AT,
connectionId: null,
restoredUnconfirmed: true
}
]
const runtime = new OrcaRuntimeService(partialRuntimeStore(runtimeStore), undefined, {
getAgentProviderSessionSnapshot: () => rows,
getAgentProviderSessionRowsForPane: (paneKey: string) =>
rows.filter((row) => row.paneKey === paneKey)
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
const [tab] = (await runtime.listMobileSessionTabs(`id:${TEST_WORKTREE_ID}`)).tabs
expect(tab).toMatchObject({
status: 'pending-handle',
agentStatus: { restoredUnconfirmed: true, worktreeId: TEST_WORKTREE_ID }
})
})
@@ -1,171 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import { getDefaultWorkspaceSession } from '../../shared/constants'
import { parsePtySessionId } from '../../shared/terminal/pty/lifecycle/pty-session-id-format'
import { withBoundPane } from './orca-runtime-tests/bind-first-pane.test-fixture'
// Preserve side-effect order: mocks, lifecycle, fixtures.
await import('./orca-runtime-tests/runtime-test-module-registration.spec')
const { OrcaRuntimeService } = await import('./orca-runtime')
await import('./orca-runtime-tests/orca-runtime-test-lifecycle.spec')
const { createFolderWorkspaceRuntimeStore, makeFolderProjectGroup, makeFolderWorkspace } =
await import('./orca-runtime-tests/orca-runtime-test-fixtures.spec')
const FOLDER = process.cwd()
const H = '10000000-0000-4000-8000-000000000001'
const B = '10000000-0000-4000-8000-000000000002'
// Same-path folder workspaces on a headless serve host (#36716).
function makeServeHost(seed: { ptyId: string; worktreeId?: string }[] = []) {
const folders = [H, B].map((id) => makeFolderWorkspace({ id, folderPath: FOLDER }))
let session = getDefaultWorkspaceSession()
for (const [index, { ptyId, worktreeId }] of seed.entries()) {
if (worktreeId) {
session = withBoundPane(session, {
worktreeId,
tabId: `tab-${index}`,
leafId: `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
ptyId
})
}
}
const store = {
...createFolderWorkspaceRuntimeStore(
folders[0],
makeFolderProjectGroup({ parentPath: FOLDER })
),
getFolderWorkspaces: () => folders,
getWorkspaceSession: () => session,
setWorkspaceSession: (next: typeof session) => {
session = next
},
flushOrThrow: () => {}
}
const live = new Set(seed.map(({ ptyId }) => ptyId))
const runtime = new OrcaRuntimeService(store)
const spawn = vi.fn(
async (args: {
sessionId?: string
worktreeId?: string
tabId?: string
leafId?: string
persistHostSessionBinding?: boolean
}) => {
if (!args.worktreeId || !args.tabId || !args.leafId) {
throw new Error('missing identity')
}
const id = args.sessionId ?? `${args.worktreeId}@@control`
live.add(id)
if (args.persistHostSessionBinding) {
session = withBoundPane(session, {
worktreeId: args.worktreeId,
tabId: args.tabId,
leafId: args.leafId,
ptyId: id
})
}
return { id }
}
)
const stopAndWait = vi.fn(async (id: string) => {
live.delete(id)
runtime.onPtyExit(id, 0)
return true
})
runtime.setPtyController({
spawn,
stopAndWait,
write: () => true,
kill: () => true,
hasPty: (id: string) => live.has(id),
getForegroundProcess: async () => null,
// Like the daemon, report only the workspace a session id names; every shell shares one cwd.
listProcesses: async () =>
[...live].map((id) => {
const { worktreeId } = parsePtySessionId(id)
return {
id,
incarnationId: `${id}-incarnation`,
terminalHandle: `term_${id}`,
wslDistro: null,
cwd: FOLDER,
title: 'shell',
...(worktreeId ? { worktreeId } : {})
}
})
})
runtime.syncWindowGraph(0, { tabs: [], leaves: [] })
const ownerOf = async (ptyId: string) =>
(await runtime.listTerminals()).terminals.find((entry) => entry.ptyId === ptyId)?.worktreeId
return { runtime, spawn, stopAndWait, live, ownerOf }
}
describe('serve host terminal workspace ownership', () => {
it('keeps a paired client terminal in its workspace when a same-path sibling refreshes', async () => {
const { runtime, spawn, stopAndWait, live, ownerOf } = makeServeHost()
await runtime.createMobileSessionTerminal(`id:folder:${H}`, { activate: false })
const spawned = await spawn.mock.results.at(-1)?.value
const id = spawned?.id ?? ''
expect(id).toMatch(new RegExp(`^folder:${H}@@serve-`))
await runtime.listMobileSessionTabs(`id:folder:${B}`)
expect((await runtime.listTerminals(`id:folder:${B}`)).terminals).toEqual([])
expect(await ownerOf(id)).toBe(`folder:${H}`)
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
expect(stopAndWait).not.toHaveBeenCalled()
expect(live.has(id)).toBe(true)
})
it('keeps a restored workspace-less serve terminal with its persisted binding', async () => {
const legacy = 'serve-00000000-0000-4000-8000-0000000000aa'
const { runtime, stopAndWait, live, ownerOf } = makeServeHost([
{ ptyId: legacy, worktreeId: `folder:${H}` }
])
await runtime.listMobileSessionTabs(`id:folder:${B}`)
expect((await runtime.listTerminals(`id:folder:${B}`)).terminals).toEqual([])
expect(await ownerOf(legacy)).toBe(`folder:${H}`)
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
expect(stopAndWait).not.toHaveBeenCalled()
expect(live.has(legacy)).toBe(true)
})
it('never stops an unbound terminal that a workspace only claims by cwd', async () => {
const unbound = 'unbound-shell'
const { runtime, stopAndWait, live } = makeServeHost([{ ptyId: unbound }])
await runtime.listMobileSessionTabs(`id:folder:${B}`)
const listed = await runtime.listTerminals(`id:folder:${B}`)
expect(listed.terminals.map((entry) => entry.ptyId)).toEqual([unbound])
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
await runtime.sleepTerminalsForWorktree(`id:folder:${B}`)
expect(stopAndWait).not.toHaveBeenCalled()
expect(live.has(unbound)).toBe(true)
})
it('stops a cwd-filed terminal with its workspace once a pane adopts it', async () => {
const orphan = 'unbound-shell'
const { runtime, stopAndWait, live } = makeServeHost([{ ptyId: orphan }])
const before = await runtime.listTerminals(`id:folder:${B}`)
await runtime.adoptTerminalOrphans({
worktree: `id:folder:${B}`,
expectedTopologyRevision: before.topologyRevisions?.[`folder:${B}`] ?? 0,
claims: [
{
terminal: `term_${orphan}`,
ptyId: orphan,
incarnationId: `${orphan}-incarnation`,
tabId: 'adopted-tab',
leafId: '00000000-0000-4000-8000-0000000000ad'
}
]
})
await runtime.stopTerminalsForWorktree(`id:folder:${B}`)
expect(stopAndWait).toHaveBeenCalledWith(orphan)
expect(live.has(orphan)).toBe(false)
})
})
@@ -1,6 +1,5 @@
// A recipe VM's suspend and resume never overlap, and one reopening runs its resume once.
import { describe, expect, it } from 'vitest'
import type { EphemeralVmRuntimeRecord } from '../../../../shared/ephemeral-vm-runtimes'
import { WorkspaceSleepCoordinator, type WorkspaceSleepRuntime } from './workspace-sleep-runtime'
import type { WorkspaceSleepPlatform } from './workspace-sleep-platform'
import type { WorkspaceLifecycleQueue } from '../lifecycle/workspace-lifecycle-queue'
@@ -15,20 +14,6 @@ function gate(): Gate {
return { opened, open }
}
/** A lifecycle queue that holds every command until the test releases it. */
function heldLifecycleQueue() {
const held: (() => void)[] = []
const hold = (_key: unknown, command: { run: () => Promise<unknown> }) =>
new Promise((resolve, reject) => held.push(() => void command.run().then(resolve, reject)))
const queue = { submit: hold, submitResolving: hold, pausedKeys: () => [] }
return {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the sleep and wake paths only submit commands.
queue: queue as unknown as WorkspaceLifecycleQueue,
held,
releaseAll: () => held.splice(0).forEach((release) => release())
}
}
function coordinator(
vm: Pick<WorkspaceSleepPlatform, 'suspendVm' | 'resumeVm'>,
workspaceLifecycle: WorkspaceLifecycleQueue | null = null
@@ -94,63 +79,6 @@ describe('WorkspaceSleepCoordinator VM wake', () => {
expect(order).toEqual(['suspend:start', 'suspend:end', 'resume'])
})
it("runs the recipe's resume once for two quick clicks, the second waiting for the first", async () => {
const resuming = gate()
let suspended = true
let resumes = 0
// Like the recipe resume: it only runs on a suspended VM.
const resumeVm = async (): Promise<EphemeralVmRuntimeRecord | null> => {
if (!suspended) {
return null
}
resumes += 1
await resuming.opened
suspended = false
return null
}
const workspaceSleep = coordinator({ suspendVm: async () => {}, resumeVm })
const first = workspaceSleep.wakeVm(WORKTREE_ID)
const second = workspaceSleep.wakeVm(WORKTREE_ID)
resuming.open()
await Promise.all([first, second])
expect(resumes).toBe(1)
})
it("resumes the VM while the workspace's lifecycle queue waits on the host it runs", async () => {
const lifecycle = heldLifecycleQueue()
const resumeVm = async (): Promise<EphemeralVmRuntimeRecord | null> => null
const workspaceSleep = coordinator({ suspendVm: async () => {}, resumeVm }, lifecycle.queue)
await expect(workspaceSleep.wakeVm(WORKTREE_ID)).resolves.toBeNull()
expect(lifecycle.held).toHaveLength(0)
})
it('keeps the VM up when the workspace is opened while its sleep waits its turn', async () => {
const lifecycle = heldLifecycleQueue()
const order: string[] = []
const workspaceSleep = coordinator(
{
suspendVm: async () => {
order.push('suspend')
},
resumeVm: async () => {
order.push('resume')
return null
}
},
lifecycle.queue
)
const slept = sleep(workspaceSleep)
await workspaceSleep.wakeVm(WORKTREE_ID)
lifecycle.releaseAll()
await slept
expect(order).toEqual(['resume'])
})
it('keeps the VM up when the workspace is opened while its suspend waits behind a wake', async () => {
const order: string[] = []
const resuming = gate()
@@ -1,47 +0,0 @@
// @vitest-environment happy-dom
import '@testing-library/jest-dom/vitest'
import { cleanup, render, screen } from '@testing-library/react'
import { afterEach, expect, it } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { i18n } from '@/i18n/i18n'
import { agentSessionFailureFact } from '../../../../shared/agent-session/failures/agent-session-failure'
import { NativeChatNoticeRow } from './NativeChatNoticeRow'
function renderFailure(failure: ReturnType<typeof agentSessionFailureFact>, text: string) {
render(
<TooltipProvider>
<NativeChatNoticeRow
agentName="Claude"
block={{ type: 'text', tone: 'error', text, failure }}
/>
</TooltipProvider>
)
}
afterEach(async () => {
cleanup()
await i18n.changeLanguage('en')
})
it('renders a plugin input failure with its recovery in English and French', async () => {
const failure = agentSessionFailureFact('pluginInputRequired', {
detail: { text: 'blast-radius', audience: 'person' }
})
const text =
'Claude stopped because the "blast-radius" plugin is waiting for input in a terminal pane. Disable the plugin for chat sessions, or run the task in a Claude terminal.'
renderFailure(failure, text)
expect(screen.getByText(text)).toBeInTheDocument()
expect(screen.queryByText('Details')).not.toBeInTheDocument()
cleanup()
await i18n.changeLanguage('fr')
renderFailure(failure, text)
expect(
screen.getByText(
'Claude s’est arrêté car le plugin « blast-radius » attend une saisie dans un volet de terminal. Désactivez le plugin pour les sessions de chat ou exécutez la tâche dans un terminal Claude.'
)
).toBeInTheDocument()
expect(screen.queryByText('Détails')).not.toBeInTheDocument()
cleanup()
renderFailure(failure, 'Custom host recovery advice.')
expect(screen.getByText('Custom host recovery advice.')).toBeInTheDocument()
})
@@ -70,32 +70,6 @@ describe('useNativeChatSteerChordPreview', () => {
return { props, view, field, button, hold, preview: () => button.dataset.preview }
}
it('previews steering the head card while Cmd is held alone on an empty composer', () => {
const { props, field, button, hold, preview } = setup()
hold()
expect(preview()).toBe('steer-queued')
fireEvent.click(button)
expect(props.steerHead).toHaveBeenCalledTimes(1)
fireEvent.keyUp(field, { key: 'Meta' })
expect(preview()).toBe('')
})
it('names what Cmd+Enter does to a typed draft, and a click sends it that way', () => {
const { props, button, hold, preview } = setup({ alternateSend: 'queue', hasDraft: true })
hold()
expect(preview()).toBe('queue')
fireEvent.click(button)
expect(props.send).toHaveBeenCalledWith({ alternate: true })
expect(props.steerHead).not.toHaveBeenCalled()
})
it('shows nothing when Shift is already down, since Cmd+Shift+Enter is another chord', () => {
const { field, preview } = setup({ hasDraft: true })
fireEvent.keyDown(field, { key: 'Meta', shiftKey: true })
act(() => vi.advanceTimersByTime(NATIVE_CHAT_STEER_CHORD_HOLD_MS))
expect(preview()).toBe('')
})
it('never flashes for a quick chord such as Cmd+C', () => {
const { field, preview } = setup()
fireEvent.keyDown(field, { key: 'Meta' })
@@ -119,14 +93,4 @@ describe('useNativeChatSteerChordPreview', () => {
act(() => vi.advanceTimersByTime(NATIVE_CHAT_STEER_CHORD_HOLD_MS))
expect(preview()).toBe('')
})
it.each([
['the chord queues', { alternateSend: 'queue' as const }],
['there is no card to steer', { steerHead: undefined }],
['a picker takes Enter', { pickerOpen: true }]
])('shows nothing on an empty composer where %s', (_case, overrides) => {
const { hold, preview } = setup(overrides)
hold()
expect(preview()).toBe('')
})
})
@@ -1,88 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import type { PRCheckDetail } from '../../../../../../shared/github/check-types'
globalThis.IS_REACT_ACT_ENVIRONMENT = true
const formatCalls = vi.hoisted(() => ({ count: 0 }))
// Counts how often labels rebuild their exact times, the per-row text that never depends on the clock.
vi.mock('@/components/github/pr-check-presentation', () => ({
formatCheckTimestamp: (input: string | null | undefined) => {
formatCalls.count += 1
return input ?? null
}
}))
import { CheckRunTime } from './check-run-time'
let container: HTMLDivElement
let root: Root
function rows(timed: number, untimed: number): PRCheckDetail[] {
const base = { status: 'completed', conclusion: 'success', url: null } as const
return [
...Array.from({ length: timed }, (_, i) => ({
...base,
name: `timed-${i}`,
startedAt: '2026-11-05T09:57:00Z',
completedAt: '2026-11-05T10:00:12Z'
})),
...Array.from({ length: untimed }, (_, i) => ({ ...base, name: `untimed-${i}` }))
]
}
function formatsPerTick(timed: number, untimed: number): number {
act(() => {
root.render(
<TooltipProvider>
{rows(timed, untimed).map((check) => (
<CheckRunTime key={check.name} check={check} />
))}
</TooltipProvider>
)
})
formatCalls.count = 0
act(() => {
vi.advanceTimersByTime(30_000)
})
return formatCalls.count
}
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-11-05T12:00:00Z'))
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
formatCalls.count = 0
})
afterEach(() => {
act(() => root.unmount())
container.remove()
vi.useRealTimers()
})
describe('CheckRunTime clock cost', () => {
it('rebuilds no fixed text on a clock tick, at N and 10N rows', () => {
// A tick can only move ages; exact times, durations and copy depend on data and language alone.
expect(formatsPerTick(10, 10)).toBe(0)
act(() => root.unmount())
root = createRoot(container)
expect(formatsPerTick(100, 100)).toBe(0)
})
it('still moves each visible age on a tick', () => {
formatsPerTick(3, 0)
const before = container.textContent
act(() => {
vi.advanceTimersByTime(60 * 60_000)
})
expect(container.textContent).not.toBe(before)
})
})
@@ -1,51 +0,0 @@
import { describe, expect, it } from 'vitest'
import { formatCheckRunAge, getCheckRunTimeFacts } from './check-run-time'
import type { PRCheckDetail } from '../../../../../../shared/github/check-types'
const NOW = Date.parse('2026-11-05T12:00:00Z')
function facts(overrides: Partial<PRCheckDetail>) {
return getCheckRunTimeFacts({ status: 'completed', ...overrides })
}
function age(overrides: Partial<PRCheckDetail>): string | null {
const result = facts(overrides)
return result ? formatCheckRunAge(result, NOW) : null
}
describe('check run time facts', () => {
it('dates a finished check by its finish and shows how long it took', () => {
const finished = facts({
startedAt: '2026-11-05T09:57:00Z',
completedAt: '2026-11-05T10:00:12Z'
})
expect(finished?.whenMs).toBe(Date.parse('2026-11-05T10:00:12Z'))
expect(finished?.fixedText).not.toBe('')
})
it('dates a running check by its start, with no duration yet', () => {
const running = facts({ status: 'in_progress', startedAt: '2026-11-05T11:55:00Z' })
expect(running?.whenMs).toBe(Date.parse('2026-11-05T11:55:00Z'))
expect(running?.running).toBe(true)
expect(running?.fixedText).toBe('')
})
it('reads a provider clock ahead of ours as now and drops sub-second runs', () => {
const ahead = facts({ startedAt: '2026-11-05T12:00:20Z', completedAt: '2026-11-05T12:00:20Z' })
expect(ahead?.fixedText).toBe('')
expect(age({ completedAt: '2026-11-05T12:00:20Z' })).toBe(
age({ completedAt: '2026-11-05T12:00:00Z' })
)
})
it('shows nothing for a finished check without a finish time', () => {
expect(facts({ startedAt: '2026-11-05T09:57:00Z' })).toBeNull()
expect(facts({})).toBeNull()
})
it('keeps an earlier-commit note even when the run has no time', () => {
const note = facts({ notLatestCommitSha: 'a'.repeat(40) })
expect(note?.fixedText).not.toBe('')
expect(age({ notLatestCommitSha: 'a'.repeat(40) })).toBeNull()
})
})
@@ -1,154 +0,0 @@
// @vitest-environment happy-dom
import { renderHook, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo } from '../../../../../shared/repo-types'
import { GITLAB_MR_INVOLVEMENT_RUNTIME_CAPABILITY } from '../../../../../shared/source-control/runtime/source-control-runtime-capabilities'
import { clearRuntimeCompatibilityCacheForTests } from '@/runtime/runtime-rpc-client'
import { replaceRuntimeEnvironmentRevisions } from '@/runtime/runtime-environment-revision'
import { isRuntimeEnvironmentPairingChangedError } from '@/runtime/runtime-environment-pairing-refresh'
import { createCompatibleRuntimeStatusResponse } from '@/runtime/runtime-compatibility-test-fixture'
import { useTaskPageGitLabLoading } from './use-task-page-gitlab-loading'
vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn() } }))
const environmentCall = vi.fn()
function statusResponse(supportsFilter: boolean) {
const response = createCompatibleRuntimeStatusResponse('runtime-1')
return {
...response,
result: {
...response.result,
capabilities: (response.result.capabilities ?? []).filter(
(capability) => supportsFilter || capability !== GITLAB_MR_INVOLVEMENT_RUNTIME_CAPABILITY
)
}
}
}
function callsTo(method: string) {
return environmentCall.mock.calls.filter(([request]) => request.method === method)
}
function renderMineOnServer(repos: Repo[]) {
const setGitlabLoading = vi.fn()
const setGitlabError = vi.fn()
const view = renderHook(
({ nonce }) =>
useTaskPageGitLabLoading({
selectedRepos: repos,
selectedReposKey: `server-${repos.length}`,
primaryRepo: repos[0],
taskSource: 'gitlab',
setGitlabItems: vi.fn(),
setGitlabLoading,
setGitlabError,
gitlabRefreshNonce: nonce,
gitlabView: 'mrs',
setGitlabTodos: vi.fn(),
setGitlabTodosLoading: vi.fn(),
activeGitlabFilter: 'opened',
gitlabMRInvolvement: 'authored',
gitlabItems: [],
gitlabError: null,
gitlabLoading: false,
displayedGitLabItems: [],
gitlabListQueryKey: null,
setGitlabListQueryKey: vi.fn()
}),
{ initialProps: { nonce: 0 } }
)
const settled = async () => {
await waitFor(() => expect(setGitlabLoading).toHaveBeenLastCalledWith(false))
setGitlabLoading.mockClear()
}
return { ...view, settled, setGitlabError }
}
function serverRepos(count: number): Repo[] {
return Array.from({ length: count }, (_, index) => ({
id: `server-repo-${index}`,
path: `/srv/app-${index}`,
displayName: `app-${index}`,
badgeColor: '',
addedAt: 1,
executionHostId: 'runtime:build-box'
}))
}
beforeEach(() => {
replaceRuntimeEnvironmentRevisions([{ id: 'build-box', createdAt: 1 }])
clearRuntimeCompatibilityCacheForTests()
environmentCall.mockReset()
Object.assign(window, { api: { runtimeEnvironments: { call: environmentCall } } })
})
describe('GitLab MR filter capability checks', () => {
it.each([12, 120])(
'asks an older server for its status once per refresh across %s projects',
async (projectCount) => {
environmentCall.mockImplementation(async (request: { method: string }) =>
request.method === 'status.get'
? statusResponse(false)
: { id: 'list', ok: true, result: { items: [] } }
)
const { rerender, settled, setGitlabError } = renderMineOnServer(serverRepos(projectCount))
await settled()
expect(setGitlabError).toHaveBeenLastCalledWith(expect.any(String))
// The compatibility check and the capability read share one status probe on a cold client.
expect(callsTo('status.get')).toHaveLength(1)
expect(callsTo('gitlab.listMRs')).toHaveLength(0)
rerender({ nonce: 1 })
await settled()
expect(callsTo('status.get')).toHaveLength(2)
expect(callsTo('gitlab.listMRs')).toHaveLength(0)
}
)
it('shows a failed check as a failure and lists on the checked runtime once it recovers', async () => {
let statusFails = true
environmentCall.mockImplementation(async (request: { method: string }) => {
if (request.method === 'status.get') {
if (statusFails) {
throw new Error('Connection lost')
}
return statusResponse(true)
}
return { id: 'list', ok: true, result: { items: [], page: 1, perPage: 50 } }
})
const { rerender, settled, setGitlabError } = renderMineOnServer(serverRepos(2))
await settled()
expect(setGitlabError).toHaveBeenLastCalledWith(expect.stringContaining('Connection lost'))
expect(callsTo('gitlab.listMRs')).toHaveLength(0)
statusFails = false
rerender({ nonce: 1 })
await settled()
expect(callsTo('gitlab.listMRs')).toHaveLength(2)
for (const [request] of callsTo('gitlab.listMRs')) {
expect(request).toMatchObject({
params: expect.objectContaining({ involvement: 'authored' }),
expectedEnvironmentRuntimeId: 'runtime-1'
})
}
})
it('ignores an unsupported answer from a pairing replaced while it was checked', async () => {
environmentCall.mockImplementation(async (request: { method: string }) => {
if (request.method === 'status.get') {
replaceRuntimeEnvironmentRevisions([{ id: 'build-box', createdAt: 2 }])
return statusResponse(false)
}
return { id: 'list', ok: true, result: { items: [] } }
})
const { settled, setGitlabError } = renderMineOnServer(serverRepos(1))
await settled()
expect(isRuntimeEnvironmentPairingChangedError(setGitlabError.mock.lastCall?.[0])).toBe(true)
expect(callsTo('gitlab.listMRs')).toHaveLength(0)
})
})
@@ -1,203 +0,0 @@
/** A server that restarted publishes its restored agents without their worktree, and restored panes
* wait for a stream handle. When that server closes such a tab, a paired desktop must drop the tab
* and its agent row like any other retraction, not keep a greyed retained row for a pane that is gone. */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
import { makePaneKey } from '../../../shared/workspace-layout/stable-pane-id'
import { toWebTerminalSurfaceTabId } from '../../../shared/workspace-layout/terminal-surface-id'
import { getDefaultSettings } from '../../../shared/constants'
import type { AppState } from '../store/types'
import { createTestStore, makeWorktree, seedStore } from '../store/slices/store-test-helpers'
import { applyFreshWebSessionTabsSnapshot } from './web-session-tabs-sync/snapshot-api'
import { resetWebSessionTabsSnapshotFreshnessForTests } from './web-session-tabs-sync/tracking-lifecycle'
import {
buildRetainedAgentsSyncSnapshot,
collectRetainedAgentsOnDisappear
} from '../components/dashboard/useRetainedAgents'
import { buildWorktreeAgentRows } from '../components/sidebar/workspace-card/activity/worktree-agent-rows'
import {
selectLiveAgentStatusEntriesForWorktree,
selectRetainedAgentEntriesForWorktree
} from '../components/sidebar/workspace-card/activity/worktree-agent-row-selectors'
import {
selectLivePtyIdsForWorktree,
selectRuntimePaneTitlesForWorktree
} from '../components/sidebar/workspace-card/activity/worktree-card-status-inputs'
// Mirror imports app store singleton; redirect it to this createTestStore harness.
vi.mock('../store', () => ({
useAppStore: {
setState: vi.fn(),
getState: vi.fn(() => ({})),
subscribe: vi.fn(() => () => {})
}
}))
const WT = 'repo1::/path/wt1'
const ENV = 'web-env-1'
const T0 = 1_700_000_000_000
const KEEP_TAB = 'host-tab-live'
const CLOSED_TAB = 'host-tab-finished-worker'
const LEAF: Record<string, string> = {
[KEEP_TAB]: '11111111-1111-4111-8111-111111111111',
[CLOSED_TAB]: '22222222-2222-4222-8222-222222222222'
}
const paneKeyOf = (hostTabId: string) =>
makePaneKey(toWebTerminalSurfaceTabId(hostTabId), LEAF[hostTabId]!)
const KEEP_PANE_KEY = paneKeyOf(KEEP_TAB)
const CLOSED_PANE_KEY = paneKeyOf(CLOSED_TAB)
type TestStore = ReturnType<typeof createTestStore>
type RetainedSyncAgents = ReturnType<typeof buildRetainedAgentsSyncSnapshot>['currentAgents']
/** A restarted server's frame: restored panes wait for a handle and their agents name no worktree. */
function restoredHostSnapshot(
snapshotVersion: number,
hostTabIds: readonly string[]
): RuntimeMobileSessionTabsResult {
return {
worktree: WT,
publicationEpoch: 'headless-hydrated:restarted',
snapshotVersion,
activeGroupId: 'host-group-1',
activeTabId: hostTabIds[0] ? `${hostTabIds[0]}::${LEAF[hostTabIds[0]]}` : null,
activeTabType: hostTabIds.length > 0 ? 'terminal' : null,
tabs: hostTabIds.map((hostTabId, index) => ({
type: 'terminal' as const,
id: `${hostTabId}::${LEAF[hostTabId]}`,
title: hostTabId,
parentTabId: hostTabId,
leafId: LEAF[hostTabId]!,
isActive: index === 0,
ptyId: `${WT}@@pty-${index + 1}`,
status: 'pending-handle' as const,
terminal: null,
agentStatus: {
state: 'done' as const,
prompt: `work in ${hostTabId}`,
updatedAt: T0 - 60_000,
stateStartedAt: T0 - 120_000,
agentType: 'codex',
paneKey: makePaneKey(hostTabId, LEAF[hostTabId]!),
tabId: hostTabId,
restoredUnconfirmed: true,
stateHistory: []
}
}))
}
}
function applyHostSnapshot(
store: TestStore,
snapshot: RuntimeMobileSessionTabsResult,
now: number
) {
vi.setSystemTime(now)
const state = store.getState()
const patch = applyFreshWebSessionTabsSnapshot(state, snapshot, ENV, now)
expect(patch, 'host snapshot must pass the freshness gate').not.toBe(state)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the gate returned a store patch for this state (checked above); the test applies it as the store would.
store.setState(patch as Partial<AppState>)
}
/** useRetainedAgentsSync's effect body, minus React. */
function replayRetainedAgentsSync(
store: TestStore,
previousAgents: RetainedSyncAgents,
now: number
): RetainedSyncAgents {
const state = store.getState()
const { currentAgents, existingWorktreeIds, tabIndex } = buildRetainedAgentsSyncSnapshot({
repos: state.repos,
worktreesByRepo: state.worktreesByRepo,
folderWorkspaces: state.folderWorkspaces,
tabsByWorktree: state.tabsByWorktree,
agentStatusByPaneKey: state.agentStatusByPaneKey,
now
})
const { toRetain, consumedSuppressedPaneKeys } = collectRetainedAgentsOnDisappear({
previousAgents,
currentAgents,
retainedAgentsByPaneKey: state.retainedAgentsByPaneKey,
retentionSuppressedPaneKeys: state.retentionSuppressedPaneKeys,
recentlyClosedAgentStatusTabIds: state.recentlyClosedAgentStatusTabIds,
recentlyRetiredAgentStatusPaneKeys: state.recentlyRetiredAgentStatusPaneKeys,
tabIndex
})
store.getState().retainAgents(toRetain)
store.getState().pruneRetainedAgents(existingWorktreeIds)
if (consumedSuppressedPaneKeys.length > 0) {
store.getState().clearRetentionSuppressedPaneKeys(consumedSuppressedPaneKeys)
}
return currentAgents
}
/** The sidebar card's agent rows and the tab strip, as useWorktreeAgentRows builds them. */
function observeDesktop(store: TestStore, now: number) {
const state = store.getState()
const tabs = state.tabsByWorktree[WT] ?? []
const rows = buildWorktreeAgentRows({
tabs,
entries: selectLiveAgentStatusEntriesForWorktree(state, WT),
retained: selectRetainedAgentEntriesForWorktree(state, WT),
runtimePaneTitlesByTabId: selectRuntimePaneTitlesForWorktree(state, WT),
ptyIdsByTabId: selectLivePtyIdsForWorktree(state, WT),
terminalLayoutsByTabId: Object.fromEntries(
tabs.map((tab) => [tab.id, state.terminalLayoutsByTabId[tab.id]])
),
now
})
return {
tabIds: tabs.map((tab) => tab.id),
rowPaneKeys: rows.filter((row) => row.rowSource !== 'subagent').map((row) => row.paneKey),
retainedPaneKeys: Object.keys(state.retainedAgentsByPaneKey)
}
}
function seedPairedDesktop(): TestStore {
const store = createTestStore()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial seed; the session-tab sync reads only these fields.
seedStore(store, {
settings: getDefaultSettings('/tmp'),
worktreesByRepo: { repo1: [makeWorktree({ id: WT, repoId: 'repo1', path: '/path/wt1' })] },
activeWorktreeId: WT
} as Partial<AppState>)
return store
}
describe('a paired desktop following a restarted server', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(T0)
resetWebSessionTabsSnapshotFreshnessForTests()
})
afterEach(() => {
vi.useRealTimers()
})
it('drops restored terminals the server closes, with their agent rows', () => {
const store = seedPairedDesktop()
let previousAgents: RetainedSyncAgents = new Map()
applyHostSnapshot(store, restoredHostSnapshot(1, [KEEP_TAB, CLOSED_TAB]), T0)
previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 500)
expect(observeDesktop(store, T0 + 500).rowPaneKeys).toContain(CLOSED_PANE_KEY)
applyHostSnapshot(store, restoredHostSnapshot(2, [KEEP_TAB]), T0 + 2_000)
previousAgents = replayRetainedAgentsSync(store, previousAgents, T0 + 2_500)
expect(observeDesktop(store, T0 + 3_000)).toEqual({
tabIds: [toWebTerminalSurfaceTabId(KEEP_TAB)],
rowPaneKeys: [KEEP_PANE_KEY],
retainedPaneKeys: []
})
// The last tab's retraction publishes an empty worktree.
applyHostSnapshot(store, restoredHostSnapshot(3, []), T0 + 4_000)
replayRetainedAgentsSync(store, previousAgents, T0 + 4_500)
expect(observeDesktop(store, T0 + 5_000)).toEqual({
tabIds: [],
rowPaneKeys: [],
retainedPaneKeys: []
})
})
})
@@ -1,38 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
isPtySessionIdOwnedByWorktree,
isServeMintedPtySessionId,
mintServePtySessionId,
parsePtySessionId
} from './pty-session-id-format'
const UUID = '10000000-0000-4000-8000-0000000000aa'
// The parsers are unchanged since v1.4.243, so released clients read serve ids the same way (#36716).
describe('serve-minted pty session ids', () => {
it.each([
['folder', 'folder:aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa'],
['POSIX worktree', 'repo-1::/Users/me/src/app:v2/wt'],
['SSH worktree', 'repo-ssh::/home/dev/a@b/wt'],
['Windows worktree', 'repo-2::C:\\Users\\me\\wt'],
['WSL worktree', 'repo-3::\\\\wsl.localhost\\Ubuntu\\home\\me\\wt']
])('round-trips a %s workspace id', (_kind, worktreeId) => {
const id = mintServePtySessionId(worktreeId, UUID)
expect(parsePtySessionId(id)).toEqual({ worktreeId })
expect(isServeMintedPtySessionId(id)).toBe(true)
expect(isPtySessionIdOwnedByWorktree(id, worktreeId)).toBe(true)
expect(isPtySessionIdOwnedByWorktree(id, `${worktreeId}-sibling`)).toBe(false)
})
it('still recognizes the workspace-less ids older hosts minted', () => {
const legacy = `serve-${UUID}`
expect(isServeMintedPtySessionId(legacy)).toBe(true)
expect(parsePtySessionId(legacy)).toEqual({ worktreeId: null })
})
it('does not treat a workspace path containing serve- as a serve id', () => {
expect(isServeMintedPtySessionId('repo::/srv/serve-app@@deadbeef')).toBe(false)
})
})
@@ -7,22 +7,6 @@ import { SUPPORTED, openStubChat, test } from './helpers/structured-claude-stub'
test.describe('native chat plugin UI frames', () => {
test.skip(!SUPPORTED, 'the Claude stub wrapper is POSIX-only')
test('a turn with plugin status and toast frames shows only the reply', async ({
orcaPage: page,
claudeStub
}) => {
const { reply } = await openStubChat(page, claudeStub.command)
await page.keyboard.type('plugin-ui probe')
await page.keyboard.press('Enter')
await expect(reply).toBeVisible({ timeout: 30_000 })
const chat = page.locator('[data-native-chat-root="true"]')
// Unknown frames land inside the turn's work section, so open it before looking.
await chat.getByText(/Worked for/).click()
await expect(chat.getByText('PR 6 · 진행 13 · 대기 6', { exact: false })).toHaveCount(0)
await expect(chat.getByText('Board synced')).toHaveCount(0)
await expect(reply).toBeVisible()
})
test('a focused plugin pane fails once and a follow-up completes on a fresh process', async ({
orcaPage: page,
claudeStub