test: remove 242 low-value tests (#39487)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-07 04:36:43 -08:00
committed by GitHub
co-authored by OrcaWin
parent cecfa9865d
commit 3ca9cb8bc2
87 changed files with 7 additions and 6877 deletions
@@ -2797,24 +2797,15 @@
"src/main/native-chat/agent-session-timeline/provider-timeline-identity.test.ts": 52,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-conversation-outline.test.ts": 375,
"src/main/native-chat/agent-session-wire/observations/testing/agent-session-current-requests.test.ts": 271,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-overflow-prefix.test.ts": 105,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer-protected.test.ts": 68,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-delta-coalescer.test.ts": 61,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/agent-session-empty-delta-retention.test.ts": 362,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-batches.test.ts": 837,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-accounting.test.ts": 1200,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-byte-limits.test.ts": 858,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-conversation-window.test.ts": 3502,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-forward-read-budget.test.ts": 2439,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-large-identities.test.ts": 1124,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page-scaling.test.ts": 57,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-history-page.test.ts": 1049,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-item-wire-form.test.ts": 2246,
"src/main/native-chat/agent-session-wire/publication/history/testing/agent-session-stop-note-paging.test.ts": 1337,
"src/main/native-chat/agent-session-wire/options/testing/at-rest-command-demand.test.ts": 43,
"src/main/native-chat/agent-session-wire/providers/testing/hook-failure-provider-frame.test.ts": 234,
"src/main/native-chat/agent-session-wire/publication/history/testing/json-string-shortening.test.ts": 47,
"src/main/native-chat/agent-session-wire/providers/testing/provider-frame-disposition.test.ts": 82,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-accept-then-deliver.test.ts": 7805,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-acquire-isolation.test.ts": 1897,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-adopted-import.test.ts": 2881,
@@ -2826,7 +2817,6 @@
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-clear-queued-command.test.ts": 2168,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-close-aborts-start.test.ts": 1806,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-codex-held-goal.test.ts": 2081,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-command-publication.test.ts": 1350,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-create-adoption-budget.test.ts": 4134,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-create-adoption.test.ts": 1670,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-current-requests.test.ts": 3325,
@@ -2844,7 +2834,6 @@
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-late-settlement-cancelled.test.ts": 2020,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-lease-renewer.test.ts": 875,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-newer-content-host.test.ts": 1997,
"src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-operation-start-refusal.test.ts": 1210,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-options-read-scaling.test.ts": 1006,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-prompt-cancel-repeat.test.ts": 1493,
"src/main/native-chat/agent-session-wire/providers/testing/structured-agent-session-provider-naming.test.ts": 297,
@@ -2866,14 +2855,11 @@
"src/main/native-chat/agent-session-wire/journal-ingress/testing/structured-agent-session-resolved-append.test.ts": 683,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-audience.test.ts": 2056,
"src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-cap.test.ts": 731,
"src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-context-clear.test.ts": 538,
"src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-continuation.test.ts": 533,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-failure-filing.test.ts": 2350,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-offer-after-reattach.test.ts": 1903,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-offer-endings.test.ts": 2091,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-offer-newer-orca.test.ts": 2824,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-ownership.test.ts": 4640,
"src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-restart-resume.test.ts": 575,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-restart-teardown.test.ts": 2163,
"src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-resumed-child-owner.test.ts": 682,
"src/main/native-chat/agent-session-wire/rewind/testing/structured-agent-session-rewind-provenance.test.ts": 626,
@@ -2885,18 +2871,11 @@
"src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-turn-verdict.test.ts": 305,
"src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-persistence.test.ts": 972,
"src/main/native-chat/agent-session-wire/termination/testing/structured-agent-session-stale-verdict-prompts.test.ts": 665,
"src/main/native-chat/agent-session-wire/startup/testing/structured-agent-session-start-flight.test.ts": 48,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-startup-attempt-reports.test.ts": 1857,
"src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-launch-directory.test.ts": 856,
"src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed-stopping.test.ts": 831,
"src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts": 1849,
"src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-reentry.test.ts": 1147,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-stop-event.test.ts": 1644,
"src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-opened-turn.test.ts": 1446,
"src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-stop-note-withdrawn-send.test.ts": 1566,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-background-tasks.test.ts": 829,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-oversized-removal.test.ts": 717,
"src/main/native-chat/agent-session-wire/publication/subscribers/testing/structured-agent-session-subscriber-publication.test.ts": 732,
"src/main/native-chat/agent-session-wire/recovery/testing/structured-agent-session-target-reconcile.test.ts": 786,
"src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts": 5574,
"src/main/native-chat/agent-session-wire/operations/testing/structured-agent-session-thread-goal.test.ts": 3029,
@@ -2908,7 +2887,6 @@
"src/main/native-chat/agent-session-wire/host/testing/structured-conversation-command-child-work.test.ts": 1810,
"src/main/native-chat/agent-session-wire/host/testing/structured-conversation-command.test.ts": 2989,
"src/main/native-chat/agent-session-wire/rewind/testing/structured-rewind-cut.test.ts": 672,
"src/main/native-chat/agent-session-wire/providers/testing/unhandled-provider-frame.test.ts": 231,
"src/main/native-chat/chat-launcher-command.test.ts": 651,
"src/main/native-chat/host-readable-transcript-path-fs-gate.test.ts": 373,
"src/main/native-chat/host-readable-transcript-path.test.ts": 150,
@@ -6533,7 +6511,6 @@
"src/renderer/src/components/pull-request-page/page/use-details.test.tsx": 745,
"src/renderer/src/components/quick-open-file-history.test.ts": 1716,
"src/renderer/src/components/quick-open-file-list-host-name-filter.react.test.tsx": 1941,
"src/renderer/src/components/quick-open-file-list-metadata.react.test.tsx": 2100,
"src/renderer/src/components/quick-open-file-list.react.test.tsx": 2215,
"src/renderer/src/components/quick-open-file-list.test.ts": 54,
"src/renderer/src/components/quick-open-history-ranking.test.ts": 112,
@@ -6616,14 +6593,12 @@
"src/renderer/src/components/right-sidebar/source-control/commit/use-commit-message-generation.cancel.test.ts": 1992,
"src/renderer/src/components/right-sidebar/source-control/listing/open-uncommitted-entry.test.ts": 900,
"src/renderer/src/components/right-sidebar/source-control/listing/use-file-projection-work.test.tsx": 703,
"src/renderer/src/components/right-sidebar/source-control/listing/use-store-actions.store-subscriptions.test.tsx": 1912,
"src/renderer/src/components/right-sidebar/source-control/panel/use-panel-view-state.test.tsx": 866,
"src/renderer/src/components/right-sidebar/source-control/review/create-pr-intent-operation-target.test.ts": 1735,
"src/renderer/src/components/right-sidebar/source-control/review/use-create-pr-intent-jira-title.test.tsx": 1956,
"src/renderer/src/components/right-sidebar/source-control/review/use-pull-request-generation.test.tsx": 2720,
"src/renderer/src/components/right-sidebar/source-control/review/use-review-push-target.test.tsx": 268,
"src/renderer/src/components/right-sidebar/source-control/sync/git-history-format.test.ts": 266,
"src/renderer/src/components/right-sidebar/source-control/sync/use-git-history-wsl-wake.test.tsx": 2095,
"src/renderer/src/components/right-sidebar/ssh-port-forward-dialog.ime-enter.test.tsx": 585,
"src/renderer/src/components/right-sidebar/ai-vault/search/use-ai-vault-search.test.tsx": 688,
"src/renderer/src/components/right-sidebar/checks-panel/refresh/use-checks-panel-terminal-worktree.test.ts": 2116,
@@ -6749,7 +6724,6 @@
"src/renderer/src/components/sidebar/CommentMarkdown.parse-validity.test.tsx": 7186,
"src/renderer/src/components/sidebar/CommentMarkdown.unknown-html.test.tsx": 3140,
"src/renderer/src/components/sidebar/CommentMarkdown.word-fade.test.tsx": 2113,
"src/renderer/src/components/sidebar/CommentMermaidBlock.system-theme.test.tsx": 389,
"src/renderer/src/components/sidebar/workspace-actions/deletion/DeleteNestedWorktreesDialog.test.tsx": 1807,
"src/renderer/src/components/sidebar/HostRemoveDialog.test.tsx": 6650,
"src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 2025,
@@ -6789,7 +6763,6 @@
"src/renderer/src/components/sidebar/worktree-list/grouping/host-section-reveal.test.tsx": 2294,
"src/renderer/src/components/sidebar/linear-agent-skill-setup-reminders.test.ts": 52,
"src/renderer/src/components/sidebar/worktree-list/listing/manual-sort-switch-toast.test.ts": 1753,
"src/renderer/src/components/sidebar/markdown-top-level-blocks.test.ts": 543,
"src/renderer/src/components/sidebar/mobile-sidebar-onboarding-badge.test.ts": 230,
"src/renderer/src/components/sidebar/worktree-list/grouping/pinned-section-worktrees.test.ts": 446,
"src/renderer/src/components/sidebar/workspace-actions/deletion/preserved-branch-batch-toast.test.tsx": 627,
@@ -8951,11 +8924,6 @@
"src/shared/host-balanced-listing-scaling.test.ts": 64,
"src/shared/host-setting-overrides.test.ts": 55,
"src/shared/host-spawn-admission.test.ts": 183,
"src/shared/source-control/hosted-review/hosted-review-create-confirmation.test.ts": 43,
"src/shared/source-control/hosted-review/hosted-review-create-seed.test.ts": 83,
"src/shared/source-control/hosted-review/hosted-review-creation-providers.test.ts": 35,
"src/shared/source-control/hosted-review/hosted-review-github.test.ts": 40,
"src/shared/source-control/hosted-review/hosted-review-push-target.test.ts": 52,
"src/shared/source-control/hosted-review/hosted-review-refs.test.ts": 57,
"src/shared/html-file-url.test.ts": 49,
"src/shared/image-data-uri.test.ts": 59,
@@ -9244,11 +9212,7 @@
"src/shared/powershell-native-argument.test.ts": 34,
"src/shared/powershell-native-argument.windows.test.ts": 67,
"src/shared/source-control/review-comments/pr-bot-author-overrides.test.ts": 76,
"src/shared/source-control/checks/pr-check-severity-order.test.ts": 51,
"src/shared/source-control/checks/pr-checks-fix-prompt.test.ts": 93,
"src/shared/source-control/review-comments/pr-comment-audience.test.ts": 51,
"src/shared/source-control/review-comments/pr-comment-groups.test.ts": 35,
"src/shared/source-control/review-comments/pr-comment-time.test.ts": 40,
"src/shared/preferred-git-remote.test.ts": 42,
"src/shared/priority-semaphore.test.ts": 43,
"src/shared/process-growth-breaker.test.ts": 47,
@@ -9432,14 +9396,8 @@
"src/shared/skill-package-manifest.test.ts": 188,
"src/shared/skill-path-containment.test.ts": 49,
"src/shared/sleeping-record-legacy-pane-key.test.ts": 59,
"src/shared/source-control/decisions/source-control-create-review-intent.test.ts": 54,
"src/shared/source-control/listing/source-control-discard-all-sequence.test.ts": 50,
"src/shared/source-control/listing/source-control-entry-actions.test.ts": 54,
"src/shared/source-control/listing/source-control-group-order.test.ts": 31,
"src/shared/source-control/decisions/source-control-next-step-decision.test.ts": 99,
"src/shared/source-control/decisions/source-control-primary-action-decision.test.ts": 67,
"src/shared/source-control/recovery/source-control-push-failure.test.ts": 51,
"src/shared/source-control/listing/source-control-section-order.test.ts": 63,
"src/shared/source-control/listing/source-control-tree.test.ts": 63,
"src/shared/source-scan/source-tree-scan.test.ts": 50,
"src/shared/source-scan/source-tree-walk.test.ts": 62,
@@ -152,7 +152,6 @@ const NODE_RUNTIME_INCLUDE = [
'src/renderer/src/components/automations/use-automation-runs-dashboard-payload.test.tsx',
'src/main/native-chat/agent-session-journal/**/*.test.ts',
'src/main/native-chat/agent-session-wire/host/testing/structured-agent-session-terminal-writer-acquire.test.ts',
'src/main/native-chat/agent-session-wire/observations/testing/structured-agent-session-status-feed.test.ts',
'src/main/runtime/agent-session/records/agent-session-conversation-name-store.test.ts',
'src/main/runtime/rpc/ws-transport.test.ts',
'src/main/runtime/rpc/dispatcher-transport-parity.test.ts',
@@ -149,29 +149,6 @@ beforeEach(() => {
held.errors = []
})
describe('a dictation the phone can hear', () => {
it('records without the computer and inserts the phone’s text', async () => {
const rpc = createFakeRpcClient()
const sent: SentRequest[] = []
const transcripts: string[] = []
mount(rpc, (text) => transcripts.push(text))
await act(async () => {
await dictation().start()
})
expect(dictation().status).toBe('recording')
speak([1, 2, 3, 4])
seam.finish = { text: ' hello phone ', failure: null }
await act(async () => {
const stopped = dictation().stop()
await pump(rpc, sent)
await stopped
})
expect(transcripts).toEqual(['hello phone'])
expect(sent).toEqual([])
expect(dictation().status).toBe('idle')
})
})
describe('a phone recognizer that fails at the finish', () => {
it('replays what was said to the computer and inserts its text', async () => {
const rpc = createFakeRpcClient()
@@ -280,19 +257,3 @@ describe('a stop while a failed phone dictation is reaching the computer', () =>
expect(dictation().status).toBe('idle')
})
})
describe('a dictation when the user chose Computer', () => {
it('goes to the computer and never arms the phone', async () => {
seam.preference = 'computer'
const rpc = createFakeRpcClient()
const sent: SentRequest[] = []
mount(rpc, () => {})
await act(async () => {
const started = dictation().start()
await pump(rpc, sent)
await started
})
expect(sent.map((request) => request.method)).toEqual(['speech.dictation.start'])
expect(seam.calls).toEqual([])
})
})
@@ -24,11 +24,6 @@ describe('a page asking for the microphone when the user chose This phone', () =
await server.serve({ action: 'cancel' })
expect(await server.mayCapture()).toBe(false)
})
it('is allowed under Automatic, where the computer is a fallback', async () => {
const server = createNativeSpeechServer(engineWith('automatic'))
expect(await server.mayCapture()).toBe(true)
})
})
describe('a recognizer another page took over', () => {
@@ -1,117 +0,0 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import type { AgentSessionExecutionLocation } from '../../../../../shared/agent-session/lifecycle/agent-session-record'
import type { AgentSessionStatusEvent } from '../../../../../shared/agent-session/agent-session-wire'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../shared/constants'
import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import type { AgentSessionRecordStore } from '../../../../runtime/agent-session/records/agent-session-record-store'
import { openTestAgentSessionRecordStore } from '../../../../runtime/agent-session/records/agent-session-record-store-test-harness'
import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger'
import { StructuredAgentSessionStatusFeed } from '../structured-agent-session-status-feed'
const SESSION = 'session-alpha'
const NOW = 1_800_000_000_000
const FLOATING: AgentSessionExecutionLocation = {
executionHostId: 'local',
wslDistro: null,
workspaceId: FLOATING_TERMINAL_WORKTREE_ID,
workspaceKind: 'folder'
}
const WORKTREE: AgentSessionExecutionLocation = {
...FLOATING,
workspaceId: 'repo-1::/repos/one',
workspaceKind: 'git-worktree'
}
let root: string
let store: AgentSessionRecordStore
const journals = createTrackedJournalOpener()
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-status-feed-workspace-path-'))
store = await openTestAgentSessionRecordStore(root)
})
afterEach(async () => {
await journals.closeAll()
await rm(root, { recursive: true, force: true })
})
async function feedFor(location: AgentSessionExecutionLocation) {
await store.reserveOwner({
sessionId: SESSION,
location,
provider: 'codex',
accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' },
expectedFence: null,
spawnToken: 'spawn-a',
claimKeyId: 'key-1',
handoffOperationId: null,
probe: { outcome: 'reservation-unused' },
operation: {
callerKey: 'client-1',
operationId: `${NOW}-${'1'.padStart(32, '0')}`,
fingerprint: 'fp-1'
},
now: NOW
})
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: location.workspaceId,
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
stateDirectory: root
})
const feed = new StructuredAgentSessionStatusFeed({
logger: createStructuredAgentSessionLogger(),
sessions: new Map([[SESSION, { journal, params: { location, provider: 'codex' } }]]),
getConversation: (sessionId) => store.getConversation(sessionId),
now: () => NOW
})
const events: AgentSessionStatusEvent[] = []
feed.subscribe({ id: 'renderer', emit: (event) => events.push(event) })
return { feed, events }
}
function lastSummary(events: readonly AgentSessionStatusEvent[]) {
const event = events.at(-1)
if (event?.type === 'status') {
return event.session
}
if (event?.type === 'snapshot') {
return event.sessions.find((session) => session.sessionId === SESSION)
}
throw new Error('no status publication')
}
describe('status feed pinned launch directory', () => {
it('publishes a floating session its pinned folder once the launch pins it', async () => {
const { feed, events } = await feedFor(FLOATING)
expect(lastSummary(events)).toMatchObject({ sessionId: SESSION })
expect(lastSummary(events)).not.toHaveProperty('launchDirectory')
await store.pinLaunchDirectory(SESSION, '/home/me/original-floating')
feed.publish(SESSION)
expect(events.at(-1)).toMatchObject({
type: 'status',
session: { sessionId: SESSION, launchDirectory: '/home/me/original-floating' }
})
})
it('omits the pin for a worktree session, whose id is where it launches', async () => {
const { feed, events } = await feedFor(WORKTREE)
await store.pinLaunchDirectory(SESSION, '/repos/one')
feed.publish(SESSION)
expect(lastSummary(events)).toMatchObject({ sessionId: SESSION })
expect(lastSummary(events)).not.toHaveProperty('launchDirectory')
})
})
@@ -1,116 +0,0 @@
import { agentSessionConversationView } from '../../../../../shared/agent-session/lifecycle/agent-session-conversation-view'
import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentSessionStatusSummary } from '../../../../../shared/agent-session/agent-session-wire'
import { agentSessionRecordFixture } from '../../../../../shared/agent-session/lifecycle/agent-session-record.test-fixture'
import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import { indexedStatusFeedSession } from './structured-agent-session-status-feed-test-session'
import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger'
import { StructuredAgentSessionStatusFeed } from '../structured-agent-session-status-feed'
const SESSION = 'status-session'
let root: string
const journals = createTrackedJournalOpener()
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-status-feed-stopping-'))
})
afterEach(async () => {
await journals.closeAll()
await rm(root, { recursive: true, force: true })
})
describe("the status feed's Stopping", () => {
it('publishes it while the stopped turn runs, and drops it when the host lets go', async () => {
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
stateDirectory: join(root, SESSION)
})
const fence = agentSessionRecordFixture().lease.runtimeFence
const published: AgentSessionStatusSummary[] = []
const session = indexedStatusFeedSession({
journal,
child: { phase: 'ready', generation: 'child-1', fence }
})
const feed = new StructuredAgentSessionStatusFeed({
logger: createStructuredAgentSessionLogger(),
sessions: new Map([[SESSION, session]]),
statusSink: () => ({ publish: (summary) => published.push(summary), forget: () => {} }),
getConversation: () => agentSessionConversationView(agentSessionRecordFixture()),
now: () => 1_000
})
await journal.appendItem(
{ provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 },
{ kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: 1_000 },
{ fence, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
feed.publish(SESSION)
expect(published.at(-1)).toMatchObject({ status: 'working' })
expect(published.at(-1)).not.toHaveProperty('stopping')
await journal.appendStopEvent({ reason: 'user-stop', turnId: 'turn-1' }, fence)
feed.publish(SESSION)
expect(published.at(-1)).toMatchObject({ status: 'working', stopping: true })
feed.revokeLive(SESSION)
expect(published.at(-1)).toMatchObject({ status: 'working' })
expect(published.at(-1)).not.toHaveProperty('stopping')
})
it('reads a turn waiting on the person as attention, never Stopping', async () => {
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
stateDirectory: join(root, SESSION)
})
const fence = agentSessionRecordFixture().lease.runtimeFence
const published: AgentSessionStatusSummary[] = []
const feed = new StructuredAgentSessionStatusFeed({
logger: createStructuredAgentSessionLogger(),
sessions: new Map([[SESSION, indexedStatusFeedSession({ journal })]]),
statusSink: () => ({ publish: (summary) => published.push(summary), forget: () => {} }),
getConversation: () => agentSessionConversationView(agentSessionRecordFixture()),
now: () => 1_000
})
const turn = { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1' } as const
await journal.appendItem(
{ ...turn, ordinal: 0 },
{ kind: 'turn', turnId: 'turn-1', state: 'running', startedAt: 1_000 },
{ fence, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
await journal.appendStopEvent({ reason: 'user-stop', turnId: 'turn-1' }, fence)
await journal.appendItem(
{ ...turn, ordinal: 1 },
{
kind: 'approval',
title: 'Run command?',
detail: null,
options: [{ id: 'yes', label: 'Allow' }],
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
},
{ fence, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
feed.publish(SESSION)
expect(published.at(-1)).toMatchObject({ status: 'attention' })
expect(published.at(-1)).not.toHaveProperty('stopping')
})
})
@@ -1,172 +0,0 @@
import { savedStructuredAgentSessionStatus } from '../structured-agent-session-saved-status'
import { releasedStructuredStatusSummary } from '../structured-agent-session-status-summary'
import { agentSessionConversationView } from '../../../../../shared/agent-session/lifecycle/agent-session-conversation-view'
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import type { AgentSessionRecord } from '../../../../../shared/agent-session/lifecycle/agent-session-record'
import { agentSessionRecordFixture } from '../../../../../shared/agent-session/lifecycle/agent-session-record.test-fixture'
import type { AgentSessionStatusEvent } from '../../../../../shared/agent-session/agent-session-wire'
import type { AgentChildWorkView } from '../../../../../shared/agent-status/child-work/agent-status-child-work-view'
import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import { indexedStatusFeedSession as indexed } from './structured-agent-session-status-feed-test-session'
import { StructuredAgentSessionStatusFeed } from '../structured-agent-session-status-feed'
import type { StructuredAgentSessionStatusFeedDeps } from '../structured-agent-session-status-feed-deps'
import type { StructuredAgentSessionStatusSink } from '../structured-agent-session-status-ownership'
import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger'
import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
const SESSION = 'status-session'
const USER_IDENTITY = {
provider: 'codex',
threadId: 'thread-1',
turnId: 'turn-1',
ordinal: 1
} as const
let root: string
const journals = createTrackedJournalOpener()
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-agent-status-feed-'))
})
afterEach(async () => {
await journals.closeAll()
await rm(root, { recursive: true, force: true })
})
async function openJournal(sessionId = SESSION, now?: () => number) {
return journals.open({
identity: {
sessionId,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
now,
stateDirectory: join(root, sessionId)
})
}
function feedFor(
sessions: Map<string, Parameters<typeof indexed>[0]>,
record: Partial<AgentSessionRecord> | null = null,
onStatusChanged?: StructuredAgentSessionStatusFeedDeps['onStatusChanged'],
readChildWork?: () => AgentChildWorkView[],
statusSink?: StructuredAgentSessionStatusSink
) {
let now = 1_000
const sink =
statusSink ??
(readChildWork ? { publish: () => {}, forget: () => {}, readChildWork } : undefined)
const feed = new StructuredAgentSessionStatusFeed({
logger: createStructuredAgentSessionLogger(),
...(onStatusChanged ? { onStatusChanged } : {}),
...(sink ? { statusSink: () => sink } : {}),
sessions: {
get: (sessionId: string) => {
const session = sessions.get(sessionId)
return session ? indexed(session) : undefined
},
[Symbol.iterator]: function* () {
for (const [sessionId, session] of sessions) {
yield [sessionId, indexed(session)] as const
}
}
} as unknown as ReadonlyMap<string, ReturnType<typeof indexed>>,
// A partial record still has a lease: the feed reads the conversation's fence off it.
getConversation: () =>
record ? agentSessionConversationView({ ...agentSessionRecordFixture(), ...record }) : null,
now: () => (now += 1)
})
const events: AgentSessionStatusEvent[] = []
const dispose = feed.subscribe({ id: 'list-1', emit: (event) => events.push(event) })
return { feed, events, dispose }
}
describe('StructuredAgentSessionStatusFeed', () => {
it('ends a closed subscriber and keeps publishing to the rest', async () => {
const journal = await openJournal()
const { feed, events, dispose } = feedFor(new Map([[SESSION, { journal }]]))
const others: AgentSessionStatusEvent[] = []
feed.subscribe({ id: 'list-2', emit: (event) => others.push(event) })
dispose()
await journal.appendItem(
USER_IDENTITY,
{ kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hello' }] },
{ fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
feed.publish(SESSION)
expect(events.at(-1)).toEqual({ type: 'end' })
expect(others.at(-1)).toEqual({
type: 'status',
session: expect.objectContaining({ status: 'idle' })
})
})
})
// Broadcast cache retains closed history; roster reads must intersect it with live sessions.
describe('the status sink sees the roster the broadcast cache deliberately lacks', () => {
it('keeps publishing to subscribers when the sink throws', async () => {
const journal = await openJournal()
const sink: StructuredAgentSessionStatusSink = {
publish: () => {
throw new Error('store down')
},
forget: () => {
throw new Error('store down')
}
}
const { feed, events } = feedFor(
new Map([[SESSION, { journal }]]),
null,
undefined,
undefined,
sink
)
await journal.appendItem(
USER_IDENTITY,
{ kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hello' }] },
{ fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
feed.publish(SESSION, journal)
expect(() => feed.forget(SESSION)).not.toThrow()
expect(events.at(-1)).toMatchObject({
type: 'status',
session: expect.objectContaining({ sessionId: SESSION, status: 'idle' })
})
})
})
it('publishes a saved mode-only change without journal activity and retains it in reconnect summaries', async () => {
const journal = await openJournal()
const sessions = new Map<string, Parameters<typeof indexed>[0]>([[SESSION, { journal }]])
const record: Partial<AgentSessionRecord> = { options: {} }
const { feed, events, dispose } = feedFor(sessions, record)
const cursor = journal.cursor()
record.options = { interactionMode: 'plan' }
feed.publish(SESSION, journal)
expect(events.at(-1)).toEqual({
type: 'status',
session: expect.objectContaining({ interactionMode: 'plan' })
})
expect(journal.cursor()).toEqual(cursor)
const saved = savedStructuredAgentSessionStatus(
agentSessionConversationView({ ...agentSessionRecordFixture(), options: record.options })
)
expect(saved.interactionMode).toBe('plan')
expect(releasedStructuredStatusSummary(saved).interactionMode).toBe('plan')
record.options = { interactionMode: 'default' }
feed.publish(SESSION, journal)
expect(events.at(-1)).toEqual({
type: 'status',
session: expect.objectContaining({ interactionMode: 'default' })
})
dispose()
})
@@ -1,104 +0,0 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { agentJournalItemKey } from '../../../../../shared/agent-session/journal/agent-session-journal-item-key'
import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentSessionJournalIdentity } from '../../../../../shared/agent-session/journal/agent-session-journal-identity'
import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import type { StructuredAgentSessionAdapter } from '../../providers/structured-agent-session-adapter'
import { NO_STRUCTURED_AGENTS } from '../../providers/testing/structured-agent-session-adapter-router-test-support'
import { structuredAgentSessionStopNoteIdentity } from '../structured-agent-session-command-turn'
import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger'
import '../structured-agent-session-turns'
import { performCancel } from '../structured-agent-session-turns-cancel'
import type { AgentSessionTurnContext } from '../structured-agent-session-turns'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
}
const TURN = { provider: 'codex' as const, threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 }
let root: string | null = null
const journals = createTrackedJournalOpener()
afterEach(async () => {
await journals.closeAll()
if (root) {
await rm(root, { recursive: true, force: true })
root = null
}
})
async function stopWhileTheTurnOpens(
outcome: Awaited<ReturnType<StructuredAgentSessionAdapter['cancelTurn']>>
) {
root = await mkdtemp(join(tmpdir(), 'orca-stop-note-opened-turn-'))
const journal = await journals.open({ identity: IDENTITY, stateDirectory: root })
const ctx: AgentSessionTurnContext = {
logger: createStructuredAgentSessionLogger(),
sessionId: 'session-1',
journal,
fence: 1,
agents: NO_STRUCTURED_AGENTS,
agent: 'codex',
adapter: {
open: async () => ({
reportedOptions: { model: '' }
}),
acquire: vi.fn(),
dispatch: vi.fn(),
closeSession: vi.fn(),
cancelTurn: vi.fn(async () => {
await journal.appendItem(
TURN,
{ kind: 'turn', turnId: 'turn-1', state: 'running' },
{ fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
return outcome
}),
answerPrompt: vi.fn(),
setOption: vi.fn()
},
persistOptions: async () => undefined,
resolvedBy: 'client-1',
publish: vi.fn(),
now: () => 1
}
await performCancel(ctx, { clientOperationId: 'stop-1', endSession: () => {} })
return journal
.snapshot()
.items.flatMap((item) =>
item.body.kind === 'status' ? [{ itemId: item.itemId, turnScope: item.turnScope }] : []
)
}
describe('a Stop that waited for a turn to open', () => {
it('keeps the request before the turn that opens during interruption', async () => {
expect(await stopWhileTheTurnOpens({ cancelled: true, turnId: 'turn-1' })).toEqual([
{
itemId: agentJournalItemKey(structuredAgentSessionStopNoteIdentity('stop-1')),
turnScope: AGENT_JOURNAL_THREAD_SCOPE
}
])
})
it("leaves a refused Stop's row with the conversation, as before", async () => {
expect(
await stopWhileTheTurnOpens({
cancelled: false,
refusal: { detail: { text: 'failed to interrupt turn', audience: 'person' } }
})
).toEqual([
{
itemId: agentJournalItemKey(structuredAgentSessionStopNoteIdentity('stop-1')),
turnScope: AGENT_JOURNAL_THREAD_SCOPE
}
])
})
})
@@ -1,131 +0,0 @@
// Accepted Stop retains its request note even when child exit takes back the send.
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { agentSessionFailureFact } from '../../../../../shared/agent-session/failures/agent-session-failure'
import { agentSessionFailureWords } from '../../../../../shared/agent-session/failures/agent-session-failure-words'
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentSessionJournalIdentity } from '../../../../../shared/agent-session/journal/agent-session-journal-identity'
import { codexProviderHandle } from '../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
import { createTrackedJournalOpener } from '../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger'
import '../structured-agent-session-turns'
import { performCancel } from '../structured-agent-session-turns-cancel'
import type { AgentSessionTurnContext } from '../structured-agent-session-turns'
import { NO_STRUCTURED_AGENTS } from '../../providers/testing/structured-agent-session-adapter-router-test-support'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
}
let root: string | null = null
const journals = createTrackedJournalOpener()
afterEach(async () => {
await journals.closeAll()
if (root) {
await rm(root, { recursive: true, force: true })
root = null
}
})
async function stopEndingTheChild(options: {
turnRunning: boolean
takesSendBack: boolean
earlierProcessSendInDoubt?: boolean
}) {
root = await mkdtemp(join(tmpdir(), 'orca-stop-note-withdrawn-'))
const journal = await journals.open({ identity: IDENTITY, stateDirectory: root })
if (options.turnRunning) {
await journal.appendItem(
{ provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 },
{ kind: 'turn', turnId: 'turn-1', state: 'running' },
{ fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
}
if (options.earlierProcessSendInDoubt) {
await journal.appendSubmission({
clientMessageId: 'send-0',
payloadFingerprint: 'send-0',
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'before the crash' }] },
fence: 1
})
await journal.resolveDispatch({
clientMessageId: 'send-0',
state: 'unknown',
reason: 'host_restarted',
fence: 1,
recovered: true
})
}
await journal.appendSubmission({
clientMessageId: 'send-1',
payloadFingerprint: 'send-1',
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'look around' }] },
fence: 1
})
const ctx: AgentSessionTurnContext = {
logger: createStructuredAgentSessionLogger(),
sessionId: 'session-1',
journal,
fence: 1,
agents: NO_STRUCTURED_AGENTS,
agent: 'codex',
adapter: {
open: async () => ({
reportedOptions: { model: '' }
}),
acquire: vi.fn(),
dispatch: vi.fn(),
closeSession: vi.fn(),
cancelTurn: vi.fn(async () => ({
cancelled: false,
refusal: { detail: { text: 'failed to interrupt turn', audience: 'person' as const } }
})),
answerPrompt: vi.fn(),
setOption: vi.fn()
},
persistOptions: async () => undefined,
resolvedBy: 'client-1',
publish: vi.fn(),
now: () => 1
}
const result = await performCancel(ctx, {
clientOperationId: 'stop-1',
stopChild: async () => {
if (options.takesSendBack) {
await journal.resolveDispatch({
clientMessageId: 'send-1',
state: 'rejected',
...agentSessionFailureWords(agentSessionFailureFact('cancelled'), {
surface: 'rejection'
}),
fence: 1,
recovered: true
})
}
}
})
const rows = journal
.snapshot()
.items.flatMap((item) => (item.body.kind === 'status' ? [item.body.text] : []))
return { cancelled: result.ok && result.value.cancelled, rows }
}
describe('a Stop that ends the child after its interrupt failed', () => {
it("keeps Stop when the only other send in doubt was an earlier process's", async () => {
expect(
await stopEndingTheChild({
turnRunning: false,
takesSendBack: true,
earlierProcessSendInDoubt: true
})
).toEqual({ cancelled: true, rows: ['Cancellation requested.'] })
})
})
@@ -1,23 +0,0 @@
import { expect, it } from 'vitest'
import {
captureStructuredExecutionOptions,
openingMessageOptions
} from '../structured-agent-session-execution-options'
import { claudeAndCodexAgents } from '../../providers/testing/structured-agent-session-adapter-router-test-support'
it('inherits a saved Plan for older phone selections without adding model intent', () => {
expect(
captureStructuredExecutionOptions(
{
agents: claudeAndCodexAgents(),
agent: 'claude',
persistedOptions: { interactionMode: 'plan' }
},
undefined,
{}
)
).toEqual({ options: { interactionMode: 'plan', fastMode: 'false' } })
expect(openingMessageOptions({ interactionMode: 'plan' })).toEqual({
options: { interactionMode: 'plan' }
})
})
@@ -1,150 +0,0 @@
import { describe, expect, it } from 'vitest'
import { projectStructuredItemToNativeChat } from '../../../../../shared/agent-session/view/structured-agent-session-projection'
import { unhandledProviderFrameJournalItem } from '../unhandled-provider-frame'
const CODEX_KIND = 'notification:hook/completed'
const CLAUDE_KIND = 'message:system:hook_response'
// Shape captured from codex-cli 0.162.0 with a SessionStart command that exits 1.
const CODEX_FAILURE = {
threadId: 'thread-1',
turnId: 'turn-1',
run: {
id: 'session-start:0:hooks.json',
eventName: 'sessionStart',
handlerType: 'command',
executionMode: 'sync',
scope: 'thread',
sourcePath: 'hooks.json',
source: 'user',
status: 'failed',
entries: [{ kind: 'error', text: 'hook exited with code 1' }]
}
}
describe('failed hook provider frames', () => {
it('projects the Codex hook name and nested failure with its original Details', () => {
const row = unhandledProviderFrameJournalItem('codex', CODEX_KIND, CODEX_FAILURE)
expect(row).toMatchObject({
classification: 'error-surface',
body: {
kind: 'status',
tone: 'error',
text: 'Codex SessionStart hook failed: hook exited with code 1'
}
})
expect(row?.body.providerFrame?.payload.head).toBe(JSON.stringify(CODEX_FAILURE))
expect(row?.body.providerFrame?.payload.truncated).toBe(false)
if (!row) {
throw new Error('Expected failed hook row')
}
expect(
projectStructuredItemToNativeChat({
itemId: 'failed-hook',
revision: 1,
sequence: 1,
observedAt: 1,
body: row.body
})
).toMatchObject({
role: 'system',
blocks: [
{ type: 'text', tone: 'error', text: row.body.text, providerFrame: row.body.providerFrame }
]
})
})
it('preserves multiple error entries without displaying hook progress as the error', () => {
const payload = {
run: {
eventName: 'preToolUse',
status: 'failed',
statusMessage: 'Running hook',
entries: [
{ kind: 'info', text: 'Running hook' },
{ kind: 'error', text: ' First failure ' },
null,
{ kind: 'error', text: 1 },
{ kind: 'error', text: 'Second failure' }
]
}
}
expect(unhandledProviderFrameJournalItem('codex', CODEX_KIND, payload)?.body.text).toBe(
'Codex PreToolUse hook failed: First failure\nSecond failure'
)
})
it('shows Claude stderr and the exit code instead of the wire event name', () => {
expect(
unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, {
hook_name: 'PostToolUse:Bash',
hook_event: 'PostToolUse',
outcome: 'error',
exit_code: 1,
stderr: ' Script not found\n',
output: 'Other output'
})?.body
).toMatchObject({
tone: 'error',
text: 'Claude PostToolUse:Bash hook failed (exit code 1): Script not found'
})
})
it.each([
{
stderr: ' ',
output: 'Hook output',
stdout: 'Other stdout',
exit_code: null,
expected: 'Hook output'
},
{ stderr: null, output: ' ', stdout: 'Hook stdout', exit_code: '1', expected: 'Hook stdout' },
{
stderr: {},
output: null,
message: 'Hook could not start',
exit_code: 0,
expected: 'Hook could not start'
}
])('uses available Claude output when stderr is empty: $expected', ({ expected, ...fields }) => {
expect(
unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, {
hook_event: 'SessionStart',
outcome: 'error',
...fields
})?.body.text
).toBe(`Claude SessionStart hook failed: ${expected}`)
})
it('keeps successful hooks hidden even when they produce diagnostic output', () => {
expect(
unhandledProviderFrameJournalItem('codex', CODEX_KIND, {
run: { status: 'completed', entries: [{ kind: 'info', text: 'Hook completed' }] }
})
).toBeNull()
expect(
unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, {
hook_name: 'SessionStart',
outcome: 'success',
exit_code: 0,
stderr: 'Diagnostic output'
})
).toBeNull()
})
it('keeps long hook error text whole and bounds only the raw Details', () => {
const row = unhandledProviderFrameJournalItem(
'codex',
CODEX_KIND,
{
run: { status: 'failed', entries: [{ kind: 'error', text: 'x'.repeat(1000) }] }
},
{ inlineHeadBytes: 64 }
)
expect(row?.body.text).toBe(`Codex hook failed: ${'x'.repeat(1000)}`)
expect(row?.body.providerFrame?.payload).toMatchObject({ truncated: true })
expect(
Buffer.byteLength(row?.body.providerFrame?.payload.head ?? '', 'utf8')
).toBeLessThanOrEqual(64)
})
})
@@ -1,79 +0,0 @@
import { describe, expect, it } from 'vitest'
import { classifyProviderFrame } from '../provider-frame-disposition'
import { unhandledProviderFrameJournalItem } from '../unhandled-provider-frame'
describe('provider frame classification catalog', () => {
it("surfaces a failed hook from the hook's own outcome field", () => {
expect(
classifyProviderFrame('codex', 'notification:hook/completed', {
run: { status: 'failed' }
})
).toBe('error-surface')
expect(
classifyProviderFrame('claude', 'message:system:hook_response', {
outcome: 'error',
stderr: 'hook failed'
})
).toBe('error-surface')
})
it('structurally diverts unknown future delta kinds from generic rows', () => {
expect(classifyProviderFrame('codex', 'notification:item/newThing/outputDelta', {})).toBe(
'stream-into-item'
)
expect(classifyProviderFrame('claude', 'message:future_delta', {})).toBe('stream-into-item')
})
it('suppresses subAgentActivity once the roster renders it, but never collabAgentToolCall', () => {
expect(
classifyProviderFrame('codex', 'item:subAgentActivity', {
id: 'a-1',
kind: 'started',
agentThreadId: 'thread-child',
agentPath: '/root/list_directory'
})
// The roster already renders this lifecycle; raw fallback would duplicate it.
).toBe('status-chrome')
expect(
classifyProviderFrame('codex', 'item:collabAgentToolCall', {
id: 'c-1',
tool: 'spawn',
status: 'inProgress',
senderThreadId: 'thread-root',
receiverThreadIds: ['thread-child'],
agentsStates: {}
})
).toBe('timeline-substantive')
})
})
describe('failure is decided by kind, never by searching the payload', () => {
it('writes no row for a Claude session start that lists MCP servers which failed to connect', () => {
// Captured Claude 2.1.289 init includes failed MCP servers, not a failed session (#25477).
const init = {
type: 'system',
subtype: 'init',
mcp_servers: [
{ name: 'MCP_DOCKER', status: 'failed', source: 'user' },
{ name: 'codegraph', status: 'connected', source: 'user' },
{ name: 'jira', status: 'failed', source: 'user' }
]
}
expect(unhandledProviderFrameJournalItem('claude', 'message:system:init', init)).toBeNull()
})
it('never turns a nested failure-looking value into an error row', () => {
const failed = { task_id: 't-1', status: 'failed', error: 'exit code 1' }
expect(
unhandledProviderFrameJournalItem('claude', 'message:system:task_notification', failed)
).toBeNull()
expect(
unhandledProviderFrameJournalItem('codex', 'notification:mcpServer/startupStatus/updated', {
name: 'github',
status: 'failed',
error: 'auth expired'
})
).toBeNull()
expect(unhandledProviderFrameJournalItem('claude', 'message:system:future', failed)).toBeNull()
})
})
@@ -1,41 +0,0 @@
import { describe, expect, it } from 'vitest'
import { unhandledProviderFrameJournalItem } from '../unhandled-provider-frame'
describe('unhandled provider frame journal fallback', () => {
it('keeps a compact label and bounds the expandable payload without dropping it', () => {
const item = unhandledProviderFrameJournalItem(
'future-provider',
'notification:new/event',
{ body: 'abcdefghij' },
{ inlineHeadBytes: 8 }
)
expect(item).not.toBeNull()
if (!item) {
throw new Error('expected substantive provider frame')
}
expect(item.body).toMatchObject({
kind: 'status',
text: 'future-provider · notification:new/event',
providerFrame: {
provider: 'future-provider',
kind: 'notification:new/event',
payload: { byteLength: 21, truncated: true }
}
})
expect(
Buffer.byteLength(item.body.providerFrame?.payload.head ?? '', 'utf8')
).toBeLessThanOrEqual(8)
})
it('turns an unserializable message-shaped payload into an explicit visible value', () => {
const cyclic: { warning?: unknown } = {}
cyclic.warning = cyclic
const item = unhandledProviderFrameJournalItem('codex', 'frame', cyclic)
expect(item?.body.text).toBe('codex · frame')
expect(item?.body.providerFrame?.payload.head).toContain('unserializable payload')
})
})
@@ -31,55 +31,6 @@ function replyBytes(value: unknown): number {
}
describe('conversation outline read', () => {
it('drops previews before it drops entries, then drops the oldest entries, never passing the budget', () => {
const items = Array.from({ length: 40 }, (_, index) => prompt(index + 1, 'x'.repeat(500)))
const full = readAgentSessionConversationOutline(snapshot(items), Number.MAX_SAFE_INTEGER)
const shortBudget = replyBytes(
full.entries.map((entry) => ({ ...entry, preview: entry.preview.slice(0, 60) }))
)
expect(shortBudget).toBeLessThan(replyBytes(full.entries))
const shortened = readAgentSessionConversationOutline(snapshot(items), shortBudget)
expect(shortened.entries).toHaveLength(40)
expect(shortened.entries.every((entry) => entry.preview.length === 60)).toBe(true)
expect(replyBytes(shortened.entries)).toBeLessThanOrEqual(shortBudget)
const bare = readAgentSessionConversationOutline(
snapshot(items),
replyBytes(full.entries.map((entry) => ({ ...entry, preview: '' })))
)
expect(bare.entries).toHaveLength(40)
expect(bare.entries.every((entry) => entry.preview === '')).toBe(true)
const budget = 10 * replyBytes(bare.entries[0])
const trimmed = readAgentSessionConversationOutline(snapshot(items), budget)
expect(trimmed.omittedEntries).toBeGreaterThan(0)
expect(trimmed.entries.at(-1)?.sequence).toBe(40)
expect(trimmed.entries[0]?.sequence).toBe(trimmed.omittedEntries + 1)
expect(replyBytes(trimmed.entries)).toBeLessThanOrEqual(budget)
})
it('carries the reply to each prompt, and drops replies before it shortens a preview', () => {
const items = Array.from({ length: 10 }, (_, index): AgentJournalRenderItem[] => [
prompt(index * 2 + 1, 'x'.repeat(100)),
{
itemId: `reply-${index}`,
revision: 1,
sequence: index * 2 + 2,
observedAt: index * 2 + 2,
body: { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text: 'Answer' }] }
}
]).flat()
const full = readAgentSessionConversationOutline(snapshot(items), Number.MAX_SAFE_INTEGER)
expect(full.entries.map((entry) => entry.reply)).toEqual(Array(10).fill('Answer'))
// Budget includes array brackets and entry separators.
const tight = readAgentSessionConversationOutline(snapshot(items), replyBytes(full.entries) - 2)
expect(tight.entries.every((entry) => entry.reply === undefined)).toBe(true)
expect(tight.entries.map((entry) => entry.preview)).toEqual(
full.entries.map((entry) => entry.preview)
)
})
it('stays inside the history page budget for a journal far past it', () => {
const items = Array.from({ length: 10_000 }, (_, index) =>
prompt(index + 1, `${index} ${'y'.repeat(1_000)}`)
@@ -1,6 +1,3 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { agentJournalSubmissionKey } from '../../../../../../shared/agent-session/journal/agent-session-journal-item-key'
import type { AgentJournalItemIdentity } from '../../../../../../shared/agent-session/journal/agent-session-journal-identity'
@@ -9,23 +6,10 @@ import {
AGENT_JOURNAL_THREAD_SCOPE
} from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
import { structuredAgentSessionPayloadFingerprint } from '../../../../../../shared/agent-session/lifecycle/structured-agent-session-mutation'
import {
closeTestJournalHostDatabase,
createTrackedJournalOpener
} from '../../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import { projectJournalBatch } from '../agent-session-journal-batch'
import { readAgentSessionHistory } from '../agent-session-history-page'
import {
fixture,
IDENTITY,
appendItems,
body,
item,
tick
} from './agent-session-history-contract-test-fixture'
const outcomeJournals = createTrackedJournalOpener()
import { fixture, IDENTITY, body, item } from './agent-session-history-contract-test-fixture'
describe('projectJournalBatch', () => {
it('publishes every nested lifecycle mutation atomically at the outer cursor', async () => {
@@ -85,81 +69,6 @@ describe('projectJournalBatch', () => {
expect(tail.page.items.map((entry) => entry.body)).toEqual([body('one'), body('two')])
})
it('accepts sparse outcome sequence groups', async () => {
// The shared fixture pins the input format; sparse groups exist only in an outcome epoch.
const root = await mkdtemp(join(tmpdir(), 'orca-history-outcomes-'))
const journal = await outcomeJournals.open({
identity: IDENTITY,
stateDirectory: root,
now: tick
})
try {
for (let ordinal = 1; ordinal <= 3; ordinal += 1) {
await journal.appendItem(item(ordinal), body(`item-${ordinal}`), {
fence: 1,
turnScope: AGENT_JOURNAL_THREAD_SCOPE
})
}
const since = journal.readSince({ epoch: journal.epoch, sequence: 0 })
if (!since.ok) {
throw new Error(`expected rows, got reset ${since.reset}`)
}
expect(since.rows.every((row) => row.kind === 'outcome')).toBe(true)
const withHole = since.rows.filter((row) => row.seq !== since.rows[1]?.seq)
expect(
projectJournalBatch({ rows: withHole, snapshot: journal.snapshot(), afterSequence: 0 })
).toMatchObject({ ok: true })
} finally {
await journal.close()
await closeTestJournalHostDatabase(root)
await rm(root, { recursive: true, force: true })
}
})
it('publishes touched items at their current reduced state, not as a delta', async () => {
await appendItems(1)
const cursor = fixture.journal.cursor()
await fixture.journal.appendItem(item(1), body('revised'), {
fence: 1,
turnScope: AGENT_JOURNAL_THREAD_SCOPE
})
const since = fixture.journal.readSince(cursor)
if (!since.ok) {
throw new Error(`expected rows, got reset ${since.reset}`)
}
const projected = projectJournalBatch({
rows: since.rows,
snapshot: fixture.journal.snapshot(),
afterSequence: cursor.sequence
})
if (!projected.ok) {
throw new Error(`expected a batch, got reset ${projected.reset}`)
}
expect(projected.batch.items).toHaveLength(1)
expect(projected.batch.items[0]).toMatchObject({ body: body('revised'), revision: 2 })
expect(projected.batch.cursor).toEqual(fixture.journal.cursor())
})
it('lists a tombstoned item as removed', async () => {
await appendItems(1)
const cursor = fixture.journal.cursor()
await fixture.journal.appendTombstone(item(1), { fence: 1 })
const since = fixture.journal.readSince(cursor)
if (!since.ok) {
throw new Error(`expected rows, got reset ${since.reset}`)
}
const projected = projectJournalBatch({
rows: since.rows,
snapshot: fixture.journal.snapshot(),
afterSequence: cursor.sequence
})
if (!projected.ok) {
throw new Error(`expected a batch, got reset ${projected.reset}`)
}
expect(projected.batch.removedItemIds).toHaveLength(1)
expect(projected.batch.items).toHaveLength(0)
})
it('publishes a mismatched provider echo under its submission slot', async () => {
const message: AgentJournalMessageItem = {
kind: 'message',
@@ -1,107 +0,0 @@
import {
AGENT_JOURNAL_THREAD_SCOPE,
type AgentJournalItemBody
} from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, expect, it } from 'vitest'
import type {
AgentJournalItemIdentity,
AgentSessionJournalIdentity
} from '../../../../../../shared/agent-session/journal/agent-session-journal-identity'
import { createTrackedJournalOpener } from '../../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import type { AgentSessionJournal } from '../../../../agent-session-journal/journal-store'
import { readAgentSessionHistory } from '../agent-session-history-page'
import { codexProviderHandle } from '../../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
const IDENTITY: AgentSessionJournalIdentity = {
sessionId: 'session-1',
workspaceId: 'ws-1',
hostId: 'host-1',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
}
const journals = createTrackedJournalOpener()
let root: string
let clock = 1_000
let epochs = 0
let journal: AgentSessionJournal
function tick(): number {
clock += 1
return clock
}
function item(ordinal: number): AgentJournalItemIdentity {
return { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal }
}
function body(text: string): AgentJournalItemBody {
return { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text }] }
}
async function appendItems(count: number, text: string): Promise<void> {
for (let ordinal = 1; ordinal <= count; ordinal += 1) {
await journal.appendItem(item(ordinal), body(`${text}-${ordinal}`), {
fence: 1,
turnScope: AGENT_JOURNAL_THREAD_SCOPE
})
}
}
beforeEach(async () => {
root = await mkdtemp(join(tmpdir(), 'orca-wire-history-'))
clock = 1_000
epochs = 0
journal = await journals.open({
identity: IDENTITY,
stateDirectory: root,
now: tick,
mintEpoch: () => {
epochs += 1
return `epoch-${epochs}`
}
})
})
afterEach(async () => {
await journals.closeAll()
await rm(root, { recursive: true, force: true })
})
it.each([1, 100, 200])('serializes each of %i unchanged forward page items once', async (count) => {
const cursor = journal.cursor()
await appendItems(count, 'x'.repeat(8_000))
const snapshot = journal.snapshot()
const stringify = JSON.stringify
// Method-shaped type: the JSON.stringify overloads split on replacer shape and reject a forwarded one.
const forwardStringify: {
stringify(value: unknown, replacer?: unknown, space?: unknown): string
}['stringify'] = stringify
let itemSerializations = 0
JSON.stringify = (value: unknown, replacer?: unknown, space?: unknown): string => {
if (value && typeof value === 'object' && 'itemId' in value && 'body' in value) {
itemSerializations++
}
return forwardStringify(value, replacer, space)
}
try {
const result = readAgentSessionHistory(
journal,
{
sessionId: 'session-1',
direction: 'after',
limit: count,
cursor
},
{ snapshot }
)
expect(result.ok).toBe(true)
expect(result.page.items).toHaveLength(count)
expect(itemSerializations).toBe(count)
} finally {
JSON.stringify = stringify
}
})
@@ -73,31 +73,6 @@ async function seedJournal(count: number) {
}
describe('forward history SQL read budget', () => {
it('keeps an exact final page final and preserves unlimited journal readers', async () => {
const journal = await seedJournal(6)
const cursor = { epoch: journal.epoch, sequence: 1 }
expect(journal.readSince(cursor)).toMatchObject({ ok: true, rows: expect.any(Array) })
const first = readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor,
limit: 3
})
expect(first).toMatchObject({ ok: true, page: { hasNewer: true } })
if (!first.ok) {
throw new Error('Expected first page')
}
const last = readAgentSessionHistory(journal, {
sessionId: identity.sessionId,
direction: 'after',
cursor: first.page.window.nextCursor,
limit: 3
})
expect(last).toMatchObject({ ok: true, page: { hasNewer: false } })
const unlimited = journal.readSince(cursor)
expect(unlimited.ok && unlimited.rows).toHaveLength(6)
})
it('reports a sequence gap when the next page reaches it', async () => {
const journal = await seedJournal(6)
const { db } = openTestJournalHostDatabase(root!)
@@ -1,30 +0,0 @@
import { expect, it } from 'vitest'
import type { AgentJournalRenderItem } from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
import {
boundHistoryItemsByBytes,
newestWholeSequenceGroups
} from '../agent-session-history-page-bounds'
function item(index: number): AgentJournalRenderItem {
return {
itemId: String(index),
revision: 1,
sequence: index,
observedAt: index,
body: { kind: 'status', text: 'ok' }
}
}
it('retains entire boundary groups and preserves oversized first-group truncation', () => {
const items = [item(1), { ...item(2), sequence: 1 }, item(3), { ...item(4), sequence: 3 }]
expect(newestWholeSequenceGroups(items, 1)).toEqual(items.slice(2))
expect(newestWholeSequenceGroups(items, 3)).toEqual(items.slice(2))
expect(boundHistoryItemsByBytes(items, 'oldest', new Map(), 1)).toMatchObject({
dropped: 2,
items: [{ itemId: '1' }, { itemId: '2' }]
})
expect(boundHistoryItemsByBytes(items, 'newest', new Map(), 1)).toMatchObject({
dropped: 2,
items: [{ itemId: '3' }, { itemId: '4' }]
})
})
@@ -1,56 +0,0 @@
import { describe, expect, it } from 'vitest'
import { readAgentSessionHistory } from '../agent-session-history-page'
import { fixture, appendItems, item } from './agent-session-history-contract-test-fixture'
describe('readAgentSessionHistory', () => {
it('carries tombstones in a forward catch-up page', async () => {
await appendItems(1)
const cursor = fixture.journal.cursor()
await fixture.journal.appendTombstone(item(1), { fence: 1 })
const page = readAgentSessionHistory(fixture.journal, {
sessionId: 'session-1',
direction: 'after',
cursor
})
if (!page.ok) {
throw new Error(`expected a page, got reset ${page.reset}`)
}
expect(page.page.items).toHaveLength(0)
expect(page.page.removedItemIds).toEqual(['codex:thread-1:turn-1:1'])
})
it('reports a forward read with no cursor as cursor_ahead rather than serving the tail', async () => {
await appendItems(1)
expect(
readAgentSessionHistory(fixture.journal, { sessionId: 'session-1', direction: 'after' })
).toMatchObject({ ok: false, reset: 'cursor_ahead' })
})
it('resets a cursor from a previous epoch', async () => {
await appendItems(1)
const stale = fixture.journal.cursor()
await fixture.journal.rollEpoch('legacy_import', 2)
for (const direction of ['before', 'after'] as const) {
expect(
readAgentSessionHistory(fixture.journal, {
sessionId: 'session-1',
direction,
cursor: stale
})
).toMatchObject({ ok: false, reset: 'epoch_changed' })
}
})
it('resets a cursor ahead of the journal', async () => {
await appendItems(1)
const ahead = { epoch: fixture.journal.epoch, sequence: fixture.journal.cursor().sequence + 10 }
expect(
readAgentSessionHistory(fixture.journal, {
sessionId: 'session-1',
direction: 'before',
cursor: ahead
})
).toMatchObject({ ok: false, reset: 'cursor_ahead' })
})
})
@@ -1,54 +0,0 @@
import { expect, it } from 'vitest'
import type { AgentJournalRenderItem } from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
import { journalPayload } from '../../../../agent-session-journal/item-lifecycle/journal-payload-bounds'
import { HISTORY_PAGE_CONTENT_BUDGET_BYTES } from '../agent-session-history-page-bounds'
import { agentJournalItemForWire, shortenedAgentJournalItem } from '../agent-session-item-wire-form'
it('preserves the full previously readable desktop message range', () => {
const item: AgentJournalRenderItem = {
itemId: 'message',
revision: 1,
sequence: 1,
observedAt: 1,
body: {
kind: 'message',
role: 'assistant',
blocks: [{ type: 'text', text: `${'x'.repeat(1_200_000)}TAIL` }]
}
}
const formed = agentJournalItemForWire(item)
expect(formed.item).toBe(item)
expect(formed.bytes).toBe(Buffer.byteLength(JSON.stringify(item), 'utf8'))
})
it.each([73_728, HISTORY_PAGE_CONTENT_BUDGET_BYTES, 4 * 1024 * 1024])(
'keeps tool identity and output when cardinality exceeds %i bytes',
(budget) => {
const item: AgentJournalRenderItem = {
itemId: 'call',
revision: 1,
sequence: 1,
observedAt: 1,
body: {
kind: 'tool-call',
callId: 'call',
name: 'query',
state: 'completed',
input: { values: Array(Math.ceil(budget / 2) + 1_000).fill(0) },
output: journalPayload('done')
}
}
const sent = shortenedAgentJournalItem(item, budget)
expect(sent).toMatchObject({
itemId: 'call',
shortened: true,
body: {
kind: 'tool-call',
name: 'query',
input: { truncated: true },
output: { head: 'done' }
}
})
expect(Buffer.byteLength(JSON.stringify(sent))).toBeLessThanOrEqual(budget)
}
)
@@ -1,23 +0,0 @@
import { expect, it } from 'vitest'
import { jsonBytes, shortenJsonStringsToFit } from '../json-string-shortening'
it('cuts long strings to fit and marks them, never splitting a surrogate pair', () => {
const value = { text: '😀'.repeat(2_000), other: 'x'.repeat(4_000) }
const fitted = shortenJsonStringsToFit(value, 2_000)
expect(fitted).not.toBeNull()
expect(jsonBytes(fitted)).toBeLessThanOrEqual(2_000)
expect(fitted?.text.endsWith('…')).toBe(true)
// A lone high surrogate would serialize as an escape and render as a broken glyph.
expect(fitted?.text).not.toMatch(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/)
})
it('leaves identifiers whole', () => {
const id = 'i'.repeat(3_000)
const fitted = shortenJsonStringsToFit({ id, itemId: id, text: 'x'.repeat(3_000) }, 7_000)
expect(fitted).toMatchObject({ id, itemId: id })
})
it('is null when even the shortest cut cannot fit', () => {
const many = Array.from({ length: 1_000 }, () => 'x'.repeat(200))
expect(shortenJsonStringsToFit(many, 1_000)).toBeNull()
})
@@ -1,101 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
AGENT_SESSION_STREAMED_TEXT_MAX_BYTES,
AGENT_SESSION_STREAMED_TEXT_TRUNCATION_MARKER as marker,
createAgentSessionDeltaCoalescer
} from '../agent-session-delta-coalescer'
describe('streamed output overflow prefix', () => {
it('encodes only the retained prefix when 64 KiB command deltas cross the default limit', () => {
const delta = 'x'.repeat(64 * 1024)
const emitted: string[] = []
const instance = createAgentSessionDeltaCoalescer({
emit: (_key, text) => emitted.push(text),
schedule: () => () => {}
})
for (let index = 0; index < 256; index += 1) {
instance.append('command', delta)
}
const from = vi.spyOn(Buffer, 'from')
const concat = vi.spyOn(Buffer, 'concat')
const allocate = vi.spyOn(Buffer, 'allocUnsafe')
try {
instance.append('command', delta)
expect(concat).not.toHaveBeenCalled()
expect(from).toHaveBeenCalledExactlyOnceWith(marker, 'utf8')
expect(allocate).toHaveBeenCalledExactlyOnceWith(
AGENT_SESSION_STREAMED_TEXT_MAX_BYTES - Buffer.byteLength(marker)
)
} finally {
from.mockRestore()
concat.mockRestore()
allocate.mockRestore()
}
const text = 'x'.repeat(AGENT_SESSION_STREAMED_TEXT_MAX_BYTES - marker.length) + marker
expect(instance.snapshot('command')).toEqual({
text,
observedBytes: 257 * delta.length,
truncated: true
})
expect(instance.flushAll()).toBe(true)
expect(emitted).toEqual([text])
instance.dispose()
})
it('leaves ordinary appends unencoded and joins split surrogates before overflow', () => {
const instance = createAgentSessionDeltaCoalescer({
emit: () => true,
schedule: () => () => {}
})
const from = vi.spyOn(Buffer, 'from')
const concat = vi.spyOn(Buffer, 'concat')
const allocate = vi.spyOn(Buffer, 'allocUnsafe')
try {
instance.append('command', '\ud800')
instance.append('command', '')
instance.append('command', '\udc00')
expect(from).not.toHaveBeenCalled()
expect(concat).not.toHaveBeenCalled()
expect(allocate).not.toHaveBeenCalled()
} finally {
from.mockRestore()
concat.mockRestore()
allocate.mockRestore()
}
expect(instance.snapshot('command')).toEqual({
text: '\ud800\udc00',
observedBytes: 6,
truncated: false
})
instance.dispose()
})
it.each([
{ cap: 0, chunks: ['abcdef'], text: '' },
{ cap: 1, chunks: ['abcdef'], text: '\n' },
{ cap: 33, chunks: ['x'.repeat(40)], text: marker.slice(0, 33) },
{ cap: 34, chunks: ['x'.repeat(40)], text: marker },
{ cap: 35, chunks: ['é', 'x'.repeat(40)], text: marker },
{ cap: 36, chunks: ['a€', 'x'.repeat(40)], text: `a${marker}` },
{ cap: 37, chunks: ['a€', 'x'.repeat(40)], text: `a${marker}` },
{ cap: 38, chunks: ['a€', 'x'.repeat(40)], text: `a€${marker}` },
{ cap: 40, chunks: ['\ud800', '', '\udc00', 'x'.repeat(40)], text: `\ufffd\ufffd${marker}` },
{ cap: 39, chunks: ['a😀', 'x'.repeat(40)], text: `a😀${marker}` },
{ cap: 38, chunks: ['a😀', 'x'.repeat(40)], text: `a${marker}` }
])('preserves chunk encoding and UTF-8 clipping at a $cap byte cap', ({ cap, chunks, text }) => {
const instance = createAgentSessionDeltaCoalescer({
maxRetainedBytes: cap,
emit: () => true,
schedule: () => () => {}
})
for (const chunk of chunks) {
instance.append('command', chunk)
}
expect(instance.snapshot('command')).toEqual({
text,
observedBytes: chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0),
truncated: true
})
instance.dispose()
})
})
@@ -26,55 +26,7 @@ function manualClock() {
}
}
function coalescer(clock: ReturnType<typeof manualClock>, windowMs?: number) {
const emitted: [string, string][] = []
const instance = createAgentSessionDeltaCoalescer({
emit: (key, text) => emitted.push([key, text]),
schedule: clock.schedule,
...(windowMs === undefined ? {} : { windowMs })
})
return { instance, emitted }
}
describe('agent-session delta coalescer', () => {
it('emits the accumulated snapshot again, not the increment, on the next window', () => {
const clock = manualClock()
const { instance, emitted } = coalescer(clock)
instance.append('item-1', 'he')
clock.fire()
instance.append('item-1', 'llo')
clock.fire()
expect(emitted).toEqual([
['item-1', 'he'],
['item-1', 'hello']
])
})
it('retains dirty state and surfaces admission failure for retry', () => {
const clock = manualClock()
let reject = true
const emitted: string[] = []
const instance = createAgentSessionDeltaCoalescer({
schedule: clock.schedule,
emit: (_key, text) => {
if (reject) {
return false
}
emitted.push(text)
return true
}
})
instance.append('item-1', 'retry me')
expect(instance.flush('item-1')).toBe(false)
expect(emitted).toEqual([])
reject = false
expect(instance.flush('item-1')).toBe(true)
expect(emitted).toEqual(['retry me'])
})
it('does not evict buffered output when flushing the oldest stream is backpressured', () => {
const clock = manualClock()
let reject = true
@@ -1,130 +0,0 @@
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
import { describe, expect, it, vi } from 'vitest'
import { createCodexStructuredItemStreams } from '../../../../../codex/structured/items/codex-structured-item-streams'
import { createAgentSessionDeltaCoalescer } from '../agent-session-delta-coalescer'
describe('empty streamed deltas', () => {
it('does not retain empty array slots through repeated Codex publications', () => {
const prefix = 'empty-delta-retention-prefix'
const streams = createCodexStructuredItemStreams({
sink: { appendItem() {}, appendTombstone() {}, publish() {} },
turnIdFor: () => 'turn',
identityFor: () => ({ provider: 'codex', threadId: 'thread', turnId: 'turn', ordinal: 0 }),
attributionFor: () => ({ turnScope: AGENT_JOURNAL_THREAD_SCOPE }),
schedule: () => () => {}
})
const append = (delta: string) =>
streams.handle('thread', 'item/agentMessage/delta', { itemId: 'item', delta })
append(prefix)
try {
for (let batch = 0; batch < 4; batch += 1) {
for (let index = 0; index < 16_384; index += 1) {
append('')
}
expect(streams.flush()).toBe(true)
}
const originalJoin = Array.prototype.join
let retainedSlots = -1
const spy = vi.spyOn(Array.prototype, 'join').mockImplementation(function (
this: unknown[],
separator
) {
// Byte counters cannot detect empty entries retained by the stream's chunk array.
if (separator === '' && this[0] === prefix) {
retainedSlots = this.length
}
return originalJoin.call(this, separator)
})
let snapshot: ReturnType<typeof streams.snapshot>
try {
snapshot = streams.snapshot('thread', 'item')
} finally {
spy.mockRestore()
}
expect(retainedSlots).toBe(1)
expect(snapshot).toEqual({
text: prefix,
observedBytes: Buffer.byteLength(prefix),
truncated: false
})
append('é')
expect(streams.snapshot('thread', 'item')?.text).toBe(`${prefix}é`)
streams.forget('thread', 'item')
expect(streams.snapshot('thread', 'item')).toBeNull()
} finally {
streams.dispose()
}
})
it('preserves empty stream snapshots, scheduled publication and explicit flushes', () => {
const pending = new Set<() => void>()
const emitted: { key: string; text: string }[] = []
const instance = createAgentSessionDeltaCoalescer({
schedule: (run) => {
pending.add(run)
return () => {
pending.delete(run)
}
},
emit: (key, text) => emitted.push({ key, text })
})
try {
expect(instance.append('empty', '')).toBe(true)
expect(instance.snapshot('empty')).toEqual({
text: '',
observedBytes: 0,
truncated: false
})
expect(pending.size).toBe(1)
expect(emitted).toEqual([])
expect(instance.flushAll()).toBe(true)
expect(pending.size).toBe(0)
expect(emitted).toEqual([{ key: 'empty', text: '' }])
instance.append('empty', 'visible')
instance.append('empty', '')
expect(pending.size).toBe(1)
expect(instance.flush('empty')).toBe(true)
expect(emitted.at(-1)).toEqual({ key: 'empty', text: 'visible' })
instance.append('empty', '')
expect(instance.flushAll()).toBe(true)
expect(emitted).toHaveLength(3)
expect(emitted.at(-1)).toEqual({ key: 'empty', text: 'visible' })
} finally {
instance.dispose()
}
expect(pending.size).toBe(0)
})
it('still refuses a new empty stream while the oldest output is backpressured', () => {
let accepting = false
const emitted: [string, string][] = []
const instance = createAgentSessionDeltaCoalescer({
maxStreams: 1,
schedule: () => () => {},
emit: (key, text) => {
if (!accepting) {
return false
}
emitted.push([key, text])
return true
}
})
try {
instance.append('first', 'preserved')
expect(instance.append('second', '')).toBe(false)
expect(instance.snapshot('second')).toBeNull()
expect(instance.snapshot('first')?.text).toBe('preserved')
accepting = true
expect(instance.append('second', '')).toBe(true)
expect(instance.snapshot('first')).toBeNull()
expect(instance.snapshot('second')?.text).toBe('')
expect(instance.flushAll()).toBe(true)
expect(emitted).toEqual([
['first', 'preserved'],
['second', '']
])
} finally {
instance.dispose()
}
})
})
@@ -1,97 +0,0 @@
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it } from 'vitest'
import type {
AgentSessionSlashCommand,
AgentSessionSubscribeEvent
} from '../../../../../../shared/agent-session/agent-session-wire'
import type { AgentSessionBackgroundTaskState } from '../../../../../../shared/agent-session/agent-session-background-task-wire'
import { createStructuredAgentSessionEventCoalescer } from '../../../../../../shared/agent-session/structured-agent-session-coalescer'
import {
EMPTY_STRUCTURED_AGENT_SESSION,
reduceStructuredAgentSession
} from '../../../../../../shared/agent-session/structured-agent-session-reducer'
import { createTrackedJournalOpener } from '../../../../agent-session-journal/store-lifecycle/journal-host-database-test-support'
import { AgentSessionSubscribers } from '../structured-agent-session-subscribers'
import { identityFor } from '../../../../../claude/structured/claude-structured-session-test-support'
it('delivers catalog changes through existing frames without resending them on ordinary output', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-command-publication-'))
const journals = createTrackedJournalOpener()
const events: AgentSessionSubscribeEvent[] = []
let state = EMPTY_STRUCTURED_AGENT_SESSION
const coalescer = createStructuredAgentSessionEventCoalescer((event) => {
events.push(event)
state = reduceStructuredAgentSession(state, { type: 'event', event })
})
try {
const journal = await journals.open({ identity: identityFor(), stateDirectory: root })
const sessionId = identityFor().sessionId
let commands: AgentSessionSlashCommand[] | undefined = [
{ name: 'loaded', kind: 'command', kindUnspecified: true }
]
let roster: AgentSessionBackgroundTaskState | null = null
const subscribers = new AgentSessionSubscribers({
readCommands: () => commands,
readBackgroundTasks: () => roster
})
const republishRoster = (): void => {
roster = roster ? null : { state: 'monitoring' }
subscribers.republishBackgroundTasks(sessionId, 7)
}
const close = subscribers.open({
id: 'one',
sessionId,
journal,
fence: 7,
emit: coalescer.push
})
expect(state.commands).toEqual(commands)
for (let i = 0; i < 25; i++) {
republishRoster()
}
coalescer.flush()
expect(events.filter((event) => 'commands' in event)).toHaveLength(1)
commands = []
subscribers.publish(sessionId, journal)
republishRoster()
coalescer.flush()
expect(state.commands).toEqual([])
expect(events.filter((event) => 'commands' in event)).toHaveLength(2)
commands = undefined
subscribers.publish(sessionId, journal)
coalescer.flush()
expect(state.commands).toBeNull()
close()
commands = [{ name: 'reconnected', kind: 'skill' }]
subscribers.open({
id: 'two',
sessionId,
journal,
cursor: journal.cursor(),
fence: 8,
emit: coalescer.push
})
coalescer.flush()
expect(state.commands).toEqual(commands)
subscribers.snapshot(sessionId, journal, 8)
expect(state.commands).toEqual(commands)
commands = undefined
subscribers.open({
id: 'three',
sessionId,
journal,
cursor: journal.cursor(),
fence: 9,
emit: coalescer.push
})
coalescer.flush()
expect(state.commands).toBeNull()
} finally {
coalescer.dispose()
await journals.closeAll()
await rm(root, { recursive: true, force: true })
}
})
@@ -1,189 +0,0 @@
import { join } from 'node:path'
import { expect, it } from 'vitest'
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../../../../shared/agent-session/journal/agent-session-journal-types'
import type {
AgentSessionStatusEvent,
AgentSessionSubscribeEvent
} from '../../../../../../shared/agent-session/agent-session-wire'
import type { AgentSessionBackgroundTaskState } from '../../../../../../shared/agent-session/agent-session-background-task-wire'
import { StructuredAgentSessionStatusFeed } from '../../../observations/structured-agent-session-status-feed'
import { AgentSessionSubscribers } from '../structured-agent-session-subscribers'
import { createStructuredAgentSessionLogger } from '../../../host/structured-agent-session-logger'
import {
claudeProviderHandle,
codexProviderHandle
} from '../../../../../../shared/agent-session/lifecycle/agent-session-provider-handle-encoding'
import {
fixture,
SESSION,
journals
} from './structured-agent-session-subscriber-contract-test-fixture'
it('settles a session nobody is reading, from running to idle', async () => {
// Never open a subscriber: status must publish independently of transcript readers.
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
stateDirectory: join(fixture.root, 'unread-journal')
})
const statusFeed = new StructuredAgentSessionStatusFeed({
logger: createStructuredAgentSessionLogger(),
sessions: new Map([
[
SESSION,
{
journal,
params: {
location: {
executionHostId: 'local',
wslDistro: null,
workspaceId: 'workspace-1',
workspaceKind: 'git-worktree'
},
provider: 'codex'
}
}
]
]),
getConversation: () => null,
now: () => 1_000
})
const subscribers = new AgentSessionSubscribers({
onJournalPublished: (sessionId, published) => statusFeed.publish(sessionId, published)
})
const statuses: AgentSessionStatusEvent[] = []
statusFeed.subscribe({ id: 'session-list', emit: (event) => statuses.push(event) })
const turn = { provider: 'codex', threadId: 'thread-1', turnId: 'turn-1', ordinal: 0 } as const
await journal.appendItem(
{ ...turn, ordinal: 1 },
{ kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'write a poem' }] },
{ fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
await journal.appendItem(
turn,
{ kind: 'status', text: 'Working', turnLifecycle: { turnId: 'turn-1', state: 'running' } },
{ fence: 1, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
subscribers.publish(SESSION, journal)
expect(statuses.at(-1)).toEqual({
type: 'status',
session: expect.objectContaining({ status: 'working', latestPrompt: 'write a poem' })
})
await journal.appendTombstone(turn, { fence: 1 })
subscribers.publish(SESSION, journal)
expect(statuses.at(-1)).toEqual({
type: 'status',
session: expect.objectContaining({ status: 'idle' })
})
})
it('publishes background lifecycle without advancing the journal and carries its fence forward', async () => {
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'claude',
providerHandle: claudeProviderHandle('provider-1', null)
},
stateDirectory: join(fixture.root, 'background-journal')
})
let roster: AgentSessionBackgroundTaskState | null = null
const subscribers = new AgentSessionSubscribers({ readBackgroundTasks: () => roster })
const events: AgentSessionSubscribeEvent[] = []
subscribers.open({
id: 'subscriber-1',
sessionId: SESSION,
journal,
fence: 1,
emit: (event) => events.push(event)
})
const cursor = journal.cursor()
const backgroundTasks: AgentSessionBackgroundTaskState = {
state: 'monitoring',
tasks: [{ id: 'task-1', kind: 'command', description: 'run the build' }]
}
roster = backgroundTasks
subscribers.republishBackgroundTasks(SESSION, 2)
expect(journal.cursor()).toEqual(cursor)
expect(events.at(-1)).toEqual({
type: 'batch',
sessionId: SESSION,
batch: { cursor, items: [], removedItemIds: [], submissions: [] },
fence: 2,
hostNow: expect.any(Number),
backgroundTasks
})
await journal.appendItem(
{ provider: 'orca', clientMessageId: 'after-background-fence' },
{ kind: 'status', text: 'After background state' },
{ fence: 2, turnScope: AGENT_JOURNAL_THREAD_SCOPE }
)
subscribers.publish(SESSION, journal)
expect(events.at(-1)).toMatchObject({ type: 'batch', fence: 2 })
})
it('publishes latest turn activity without advancing or adding journal rows', async () => {
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
stateDirectory: join(fixture.root, 'activity-journal')
})
const subscribers = new AgentSessionSubscribers()
const events: AgentSessionSubscribeEvent[] = []
subscribers.open({
id: 'subscriber-1',
sessionId: SESSION,
journal,
fence: 1,
emit: (event) => events.push(event)
})
const cursor = journal.cursor()
subscribers.publish(SESSION, journal, {
turnId: 'turn-1',
text: 'Inspecting the session wire'
})
expect(journal.cursor()).toEqual(cursor)
expect(events.at(-1)).toEqual({
type: 'batch',
sessionId: SESSION,
batch: { cursor, items: [], removedItemIds: [], submissions: [] },
fence: 1,
hostNow: expect.any(Number),
activity: { turnId: 'turn-1', text: 'Inspecting the session wire' }
})
subscribers.close(SESSION, 'subscriber-1')
subscribers.publish(SESSION, journal, null)
subscribers.open({
id: 'reconnected',
sessionId: SESSION,
journal,
fence: 1,
cursor,
emit: (event) => events.push(event)
})
expect(journal.cursor()).toEqual(cursor)
expect(events.at(-1)).toMatchObject({ activity: null })
})
@@ -113,26 +113,6 @@ function deps(
}
describe('structured session recovery resolution', () => {
it('releases an ownerless reservation: nothing it recorded can be holding it', async () => {
const store = await openStore()
await reserve(store)
await latch(store)
const result = await resolveStructuredSessionRecovery(
deps(store, () => ({ outcome: 'indeterminate', reason: 'no scan' })),
SESSION
)
expect(result).toBe('resolved')
expect(store.getRecord(SESSION)?.lease).toMatchObject({
claimStatus: 'released',
handoffStage: null,
runtimeFence: 2,
reservedSpawnToken: null,
deathEvidence: null
})
})
it('releases an owner whose identity cannot be verified, and signals nothing', async () => {
const store = await openStore()
await liveOwner(store)
@@ -14,43 +14,6 @@ type Handover = { dispatchState: string; reason?: string | null } | undefined
const LIMIT = 3
it('publishes queued chats separately from the chat holding the start slot', async () => {
const first = Promise.withResolvers<void>()
const phases = new Map<string, string>()
const action = resumeStructuredAgentSessionsFromRestart(
{
admission: new StructuredAgentSessionResumeAdmission(1),
onPhase: (sessionId, phase) => phases.set(sessionId, phase),
consumeMarker: async () => true,
resume: async (sessionId) => {
if (sessionId === 'a') {
await first.promise
}
}
},
['a', 'b'].map((sessionId) => ({
sessionId,
workspaceId: 'folder',
executionHostId: 'local',
workspaceKind: 'folder',
agent: 'codex',
work: { kind: 'turn', id: 'turn' },
trigger: 'quit',
recordedAt: 1,
latestPrompt: ''
})),
'modal'
)
try {
await vi.waitFor(() => expect(phases.get('a')).toBe('starting'))
expect(phases.get('b')).toBe('queued')
} finally {
first.resolve()
await action
}
expect(phases.get('b')).toBe('starting')
})
it('holds each slot from accept until handover or rejection, and frees it when the wait ends (P2-27)', async () => {
const handovers = new Map<string, PromiseWithResolvers<Handover>>()
let waiting = 0
@@ -1,93 +0,0 @@
import { describe, expect, it } from 'vitest'
import { structuredAgentSessionResumableSet } from '../structured-agent-session-restart-resume-set'
import {
structuredAgentSessionWorkingAtStop,
structuredAgentSessionWorkInFlight
} from '../../termination/structured-agent-session-working-at-teardown'
import {
record,
marker,
journal,
turnItem,
submission,
NOW,
SESSION,
TEARDOWN_CURRENT
} from './structured-agent-session-restart-resume-test-harness'
import type { AgentJournalRenderItem } from '../../../../../shared/agent-session/journal/agent-session-journal-types'
const boundary = { operationId: 'clear', afterFence: 1, clearedAt: NOW }
const clear: AgentJournalRenderItem = {
itemId: 'clear',
sequence: 5,
revision: 1,
observedAt: NOW,
body: { kind: 'status', text: 'Context cleared', contextClear: boundary }
}
const value = (): ReturnType<typeof record> & { providerContextBoundary: typeof boundary } => ({
...record(),
providerHandleChain: [],
providerContextBoundary: boundary
})
const set = (m = marker(), r = value()) =>
structuredAgentSessionResumableSet({
markers: [m],
getRecord: () => r,
supportsRecord: () => true,
latestPrompt: () => 'prior prompt',
movedOn: () => false,
savedByNewerOrca: () => false
})
describe('restart offers after context clear', () => {
it('retires the old-context offer before any new provider handle exists', () => {
expect(set()).toEqual({ candidates: [], superseded: [marker()] })
expect(set({ ...marker(), contextClearOperationId: 'previous-clear' })).toEqual({
candidates: [],
superseded: [{ ...marker(), contextClearOperationId: 'previous-clear' }]
})
})
it('does not treat an old unknown send as new-context work during teardown', () => {
const unknown = { ...submission('old-send', 'unknown'), acceptedSequence: 2 }
expect(
structuredAgentSessionWorkInFlight([turnItem('old', 'running'), clear], [unknown])
).toBeNull()
expect(
structuredAgentSessionWorkingAtStop({
sessionId: SESSION,
session: {
journal: journal([turnItem('old', 'running'), clear], [unknown]),
child: { fence: 2, phase: 'ready' }
},
getRecord: value,
childWorkOpen: () => false,
childWork: () => undefined,
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})
).toBeNull()
})
it('stamps a current-context offer and keeps it eligible after restart', () => {
const proved = { ...record().providerHandleChain[0], linkId: 'new', mintedAtFence: 2 }
const currentRecord = {
...value(),
providerHandleChain: [proved]
}
const turn = { ...turnItem('new-turn', 'running'), sequence: 6 }
const offer = structuredAgentSessionWorkingAtStop({
sessionId: SESSION,
session: { journal: journal([clear, turn]), child: { fence: 2, phase: 'ready' } },
getRecord: () => currentRecord,
childWorkOpen: () => false,
childWork: () => undefined,
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})!
expect(offer.contextClearOperationId).toBe('clear')
expect(set(offer, currentRecord).candidates).toHaveLength(1)
})
})
@@ -1,161 +0,0 @@
import { expect, it, vi } from 'vitest'
import { marker, SESSION } from './structured-agent-session-restart-resume-test-harness'
import {
AGENT_SESSION_RESTART_CONTINUATION_REFUSED_NOTE,
AGENT_SESSION_RESTART_CONTINUATION_UNCONFIRMED_NOTE,
AGENT_SESSION_RESTART_NOT_CONNECTED_NOTE
} from '../../../../../shared/agent-session/lifecycle/agent-session-restart-continuation'
import {
RestartContinuationSupersededError,
startStructuredAgentSessionContinuation,
type StructuredAgentSessionContinuationDeps
} from '../structured-agent-session-restart-continuation'
import { createStructuredAgentSessionLogger } from '../../host/structured-agent-session-logger'
async function continueStructuredAgentSessionAfterRestart(
...args: Parameters<typeof startStructuredAgentSessionContinuation>
) {
const started = await startStructuredAgentSessionContinuation(...args)
return 'done' in started ? started.done : started.verdict()
}
function dependencies(
settledDispatch: 'accepted' | 'pending' | 'unknown' | 'rejected',
handedOver: 'pending' | 'rejected' | undefined = 'pending'
): StructuredAgentSessionContinuationDeps & {
note: ReturnType<typeof vi.fn>
send: ReturnType<typeof vi.fn>
} {
return {
currentFence: () => 1,
send: vi.fn(async () => ({
ok: true,
value: { submission: { dispatchState: 'pending' } }
})),
awaitSettlement: vi.fn(async () => ({
dispatchState: settledDispatch,
reason: settledDispatch === 'rejected' ? 'provider_refused' : null
})),
awaitHandedOver: vi.fn(async () =>
handedOver
? {
dispatchState: handedOver,
reason: handedOver === 'rejected' ? 'Codex could not start.' : null
}
: undefined
),
note: vi.fn(async () => undefined),
logger: createStructuredAgentSessionLogger()
}
}
const UNCONFIRMED = [SESSION, AGENT_SESSION_RESTART_CONTINUATION_UNCONFIRMED_NOTE, 'warning']
const REFUSED = [SESSION, AGENT_SESSION_RESTART_CONTINUATION_REFUSED_NOTE, 'error']
const NOT_CONNECTED = [SESSION, AGENT_SESSION_RESTART_NOT_CONNECTED_NOTE, 'error']
it.each([
['pending', { sessionId: SESSION, outcome: 'pending' }, UNCONFIRMED],
['unknown', { sessionId: SESSION, outcome: 'unknown' }, UNCONFIRMED],
['rejected', { sessionId: SESSION, outcome: 'refused', reason: 'provider_refused' }, REFUSED]
] as const)(
'preserves a %s settlement and notes it in the chat instead of the success note',
async (settled, expected, note) => {
const deps = dependencies(settled)
await expect(
continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1')
).resolves.toEqual(expected)
expect(deps.note).toHaveBeenCalledExactlyOnceWith(...note)
}
)
it("writes nothing in the chat when the user's own message came first, and still refuses", async () => {
const deps = dependencies('accepted')
deps.send.mockRejectedValue(new RestartContinuationSupersededError())
await expect(
continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1')
).rejects.toBeInstanceOf(RestartContinuationSupersededError)
expect(deps.note).not.toHaveBeenCalled()
})
// Ownership refusal also blocks personal sends; do not advise sending one.
it.each([
['agent_session_conflict', NOT_CONNECTED],
['agent_session_operation_invalid', REFUSED]
] as const)('reports a %s send refusal without waiting for settlement', async (code, note) => {
const deps = dependencies('accepted')
deps.send.mockResolvedValue({ ok: false, refusal: { code } })
await expect(
continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1')
).resolves.toEqual({
sessionId: SESSION,
outcome: 'refused',
reason: code,
refusal: { code }
})
expect(deps.awaitSettlement).not.toHaveBeenCalled()
expect(deps.note).toHaveBeenCalledExactlyOnceWith(...note)
})
it('reports a newer-Orca send refusal with its reason', async () => {
const deps = dependencies('accepted')
const refusal = {
code: 'agent_session_journal_unreadable',
details: { reason: 'journalWrittenByNewerOrca' }
} as const
deps.send.mockResolvedValue({ ok: false, refusal: { ...refusal, message: 'Update Orca.' } })
await expect(
continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1')
).resolves.toEqual({
sessionId: SESSION,
outcome: 'refused',
reason: 'agent_session_journal_unreadable',
refusal
})
})
it('reports an unattached chat without sending', async () => {
const deps = dependencies('accepted')
deps.currentFence = () => null
await expect(
continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1')
).resolves.toEqual({
sessionId: SESSION,
outcome: 'refused',
reason: 'agent_session_not_attached'
})
expect(deps.send).not.toHaveBeenCalled()
})
it('reports a continuation rejected at handover without waiting for the provider', async () => {
const deps = dependencies('accepted', 'rejected')
await expect(
continueStructuredAgentSessionAfterRestart(deps, SESSION, marker(), 'operation-1')
).resolves.toEqual({ sessionId: SESSION, outcome: 'refused', reason: 'Codex could not start.' })
expect(deps.awaitSettlement).not.toHaveBeenCalled()
expect(deps.note).toHaveBeenCalledExactlyOnceWith(...REFUSED)
})
it('returns at handover and leaves the provider verdict to be awaited', async () => {
const deps = dependencies('accepted')
const started = await startStructuredAgentSessionContinuation(
deps,
SESSION,
marker(),
'operation-1'
)
expect(deps.awaitHandedOver).toHaveBeenCalledOnce()
expect(deps.awaitSettlement).not.toHaveBeenCalled()
if (!('verdict' in started)) {
throw new Error('expected a handed-over continuation')
}
await expect(started.verdict()).resolves.toEqual({ sessionId: SESSION, outcome: 'continued' })
expect(deps.note).toHaveBeenCalledOnce()
})
@@ -1,208 +0,0 @@
import { describe, expect, it } from 'vitest'
import { structuredAgentSessionResumableSet } from '../structured-agent-session-restart-resume-set'
import { structuredAgentSessionWorkingAtStop } from '../../termination/structured-agent-session-working-at-teardown'
import {
childRecord,
CLAUDE_ROOT,
claudeRecord,
journal,
TEARDOWN_CURRENT,
marker,
NOW,
record,
resumableSet,
SESSION,
submission,
turnItem
} from './structured-agent-session-restart-resume-test-harness'
type StopInput = Parameters<typeof structuredAgentSessionWorkingAtStop>[0]
function markersAtTeardown(
input: Omit<StopInput, 'sessionId' | 'session'> & {
sessions: ReadonlyMap<string, NonNullable<StopInput['session']>>
}
) {
return [...input.sessions].flatMap(([sessionId, session]) => {
const recorded = structuredAgentSessionWorkingAtStop({ ...input, sessionId, session })
return recorded ? [recorded] : []
})
}
describe('deriving what was working at teardown', () => {
// Persisted running rows prove no live execution; only this runtime’s child can justify a witness.
it('marks nothing for a stale running row this host was not executing', () => {
expect(
markersAtTeardown({
sessions: new Map([
[SESSION, { journal: journal([turnItem('turn-1', 'running')]), child: null }]
]),
getRecord: () => record(),
childWorkOpen: () => false,
childWork: () => undefined,
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})
).toEqual([])
})
// Capture lost prompts with the same work snapshot so blocked offers name what they lost.
it('marks nothing for a settled lead whose tasks have all finished', () => {
expect(
markersAtTeardown({
sessions: new Map([
[
SESSION,
{
journal: journal([turnItem('turn-1', 'completed')]),
child: { fence: 1, phase: 'ready' as const }
}
]
]),
getRecord: () => record(),
childWorkOpen: () => false,
childWork: () => [
childRecord({ id: 'task-a', kind: 'agent', state: 'done' }),
childRecord({ id: 'task-b', kind: 'command', state: 'idle' })
],
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})
).toEqual([])
})
it('marks nothing for a session that never proved a provider cursor', () => {
expect(
markersAtTeardown({
sessions: new Map([
[
SESSION,
{
journal: journal([turnItem('turn-1', 'running')]),
child: { fence: 1, phase: 'ready' as const }
}
]
]),
getRecord: () => record({ chain: [] }),
childWorkOpen: () => false,
childWork: () => undefined,
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})
).toEqual([])
})
// Claude may echo user input long after dispatch; mark the send before any turn ID exists.
it('marks nothing for a session whose only work is a message still queued', () => {
const queued = { ...submission('msg-1', 'pending'), handoverRecorded: true as const }
expect(
markersAtTeardown({
sessions: new Map([
[
SESSION,
{
journal: journal([turnItem('turn-0', 'completed')], [queued]),
child: { fence: 1, phase: 'ready' as const }
}
]
]),
getRecord: () => claudeRecord(null),
childWorkOpen: () => false,
childWork: () => undefined,
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})
).toEqual([])
})
it('marks a handed-over message over a newer queued one, and a turn a queued one waits behind', () => {
const handedOver = {
...submission('msg-1', 'pending'),
handoverRecorded: true as const,
handedOverAt: NOW
}
const queued = { ...submission('msg-2', 'pending'), handoverRecorded: true as const }
const markers = markersAtTeardown({
sessions: new Map([
[
SESSION,
{
journal: journal([turnItem('turn-0', 'completed')], [handedOver, queued]),
child: { fence: 1, phase: 'ready' as const }
}
],
[
'session-running',
{
journal: journal([turnItem('turn-1', 'running')], [queued]),
child: { fence: 1, phase: 'ready' as const }
}
]
]),
getRecord: () => claudeRecord(null),
childWorkOpen: () => false,
childWork: () => undefined,
trigger: 'quit',
teardownId: TEARDOWN_CURRENT,
now: NOW
})
expect(markers.map((entry) => entry.work)).toEqual([
{ kind: 'submission', id: 'msg-1' },
{ kind: 'turn', id: 'turn-1' }
])
})
})
describe('the resumable set', () => {
// Same-teardown close advances Claude’s leaf; comparing full keys would permanently reject continuity.
it('still offers a Claude session whose leaf advanced after the marker was written', () => {
const { candidates } = structuredAgentSessionResumableSet({
markers: [marker({ providerHandleRoot: CLAUDE_ROOT })],
getRecord: () => claudeRecord('5aed93d6-advanced-leaf'),
supportsRecord: () => true,
latestPrompt: () => '',
movedOn: () => false,
savedByNewerOrca: () => false
})
expect(candidates).toHaveLength(1)
})
it('withdraws and reports for deletion a Claude session that forked to a different root', () => {
const forked = marker({ providerHandleRoot: CLAUDE_ROOT })
const set = structuredAgentSessionResumableSet({
markers: [forked],
getRecord: () => claudeRecord(null, 'prov-session-2'),
supportsRecord: () => true,
latestPrompt: () => '',
movedOn: () => false,
savedByNewerOrca: () => false
})
expect(set.candidates).toEqual([])
expect(set.superseded).toEqual([forked])
})
it("neither offers nor spends a newer Orca's chat, and offers it once this build can open it", () => {
const offer = marker()
expect(resumableSet({ markers: [offer], savedByNewerOrca: [SESSION] })).toEqual({
candidates: [],
superseded: []
})
expect(resumableSet({ markers: [offer] }).candidates).toHaveLength(1)
})
// Structural refusal must preserve a durable offer the user still owns.
it('does not report a structurally refused marker for deletion', () => {
expect(resumableSet({ markers: [marker()], chain: [] }).superseded).toEqual([])
})
// Provider-authored restore rows cannot overturn teardown’s observed work.
})
@@ -1,100 +0,0 @@
import { foundAgentSessionRecord } from '../../../../runtime/agent-session/records/agent-session-record-founding'
import { describe, expect, it, vi } from 'vitest'
import type { AgentSessionRecord } from '../../../../../shared/agent-session/lifecycle/agent-session-record'
import type { StructuredAgentSessionAttachContext } from '../../acquisition/structured-agent-session-attach-context'
import { startStructuredAgentSessionAgentForOperation } from '../structured-agent-session-agent-start'
import { StructuredAgentSessionAcquireAborts } from '../structured-agent-session-acquire-aborts'
import { StructuredAgentSessionStartupAttempts } from '../structured-agent-session-startup-attempt'
import { StructuredAgentSessionStartFlights } from '../structured-agent-session-start-flight'
import '../../options/structured-agent-session-options-read'
import { recordStructuredAgentSessionOptionIntent } from '../../options/structured-agent-session-option-intent'
import { claudeAndCodexAgents } from '../../providers/testing/structured-agent-session-adapter-router-test-support'
import { recordingStructuredAgentSessionLogger } from '../../host/testing/structured-agent-session-logger-test-support'
const SESSION = 'session-1'
describe('an operation whose agent start throws', () => {
it('is refused as a failed restart, with the error only in the log', async () => {
const log = recordingStructuredAgentSessionLogger()
const cause = new Error('EACCES: permission denied, open /Users/me/.orca/leases.json')
const context = {
deps: {
logger: log.logger,
store: {
requireWritableRecord: () =>
foundAgentSessionRecord(
{
sessionId: SESSION,
provider: 'codex',
accountHome: { variable: 'CODEX_HOME', path: '/test/codex' },
location: {
workspaceId: 'workspace-1',
workspaceKind: 'git-worktree',
executionHostId: 'local',
wslDistro: null
}
},
{ claimKeyId: 'test-key', now: 0 }
)
}
},
sessions: new Map(),
publishStatus: vi.fn(),
serialize: (_sessionId: string, task: () => Promise<unknown>) => task(),
tasks: { trackAttach: (start: Promise<unknown>) => start },
runtimeState: {
acquireAborts: new StructuredAgentSessionAcquireAborts(),
startFlights: new StructuredAgentSessionStartFlights(),
startupAttempts: new StructuredAgentSessionStartupAttempts({ expire: () => {} })
},
reconcileLeases: () => Promise.reject(cause)
}
const refused = await startStructuredAgentSessionAgentForOperation(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: a partial context double; the store returns a complete record; the start throws at `reconcileLeases` before the remaining context is read.
context as unknown as StructuredAgentSessionAttachContext,
SESSION
)
expect(refused).toEqual({
ok: false,
failure: { kind: 'hostFault' },
refusal: {
code: 'agent_session_owner_restart_failed',
message: "The agent couldn't restart."
}
})
expect(log.entries.map((entry) => entry.fields)).toEqual([
{ scope: 'agent-start', sessionId: SESSION, error: cause }
])
})
})
describe('an option picked while the chat is at rest', () => {
it('refuses a key the provider would not accept as a rejected option', async () => {
const reviseSessionOptions = vi.fn()
const refused = await recordStructuredAgentSessionOptionIntent(
{
store: {
getRecord: () =>
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the intent reads only the record's provider and options.
({ provider: 'codex', options: {} }) as unknown as AgentSessionRecord,
replaceSessionOptions: vi.fn(),
reviseSessionOptions
},
agents: claudeAndCodexAgents()
},
{ sessionId: SESSION, fence: 1, publish: () => {}, now: () => 0 },
{ key: 'notAnOption', value: 'x' }
)
expect(refused).toMatchObject({
ok: false,
refusal: {
code: 'agent_session_operation_invalid',
details: { reason: 'optionRejected' }
}
})
expect(reviseSessionOptions).not.toHaveBeenCalled()
})
})
@@ -62,16 +62,6 @@ it("keeps a required read's final answer over the child's later death", async ()
expect(outcome).toMatchObject({ retry: 'never', cause: answer })
})
it('answers an unanswered death after a successful open with the adapter verdict', async () => {
ending = died
expect(await settleStructuredAgentSessionProviderStarted(context(), OPENED)).toBe(died)
expect(died.retry).toBe('safe')
})
it('retires only its own owner once the child was replaced', async () => {
expect(await settleStructuredAgentSessionProviderStarted(context('g-2'), OPENED)).toBe('stale')
})
it('ends the start on a reported death while its bookkeeping is still blocked', async () => {
ending = died
const blocked = context()
@@ -1,28 +0,0 @@
import { describe, expect, it } from 'vitest'
import { StructuredAgentSessionStartFlights } from '../structured-agent-session-start-flight'
describe('start flight child retention', () => {
it.each([10, 100])('retires a flight after %i duplicate publications', (count) => {
const flights = new StructuredAgentSessionStartFlights()
const flight = {
attemptId: 'start',
abort: () => {},
outcome: Promise.resolve({ ok: true as const })
}
flights.register('chat', flight)
const child = { generation: 'child', fence: 1 }
for (let n = 0; n < count; n++) {
flights.published('chat', { ...child })
}
expect(flights.get('chat')?.children).toEqual([child])
expect(flights.ownsChild('chat', child)).toBe(true)
const successor = { ...child, fence: 2 }
flights.published('chat', successor)
flights.published('chat', { ...successor })
expect(flights.get('chat')?.children).toEqual([child, successor])
flights.end('chat', flight.attemptId)
flights.published('chat', child)
expect(flights.has('chat')).toBe(false)
expect(flights.ownsChild('chat', child)).toBe(false)
})
})
@@ -1,137 +0,0 @@
import { expect, it } from 'vitest'
import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types'
import { createNativeChatConversationProjection } from './native-chat-conversation-projection'
function text(id: string): NativeChatMessage {
return {
id,
role: 'assistant',
blocks: [{ type: 'text', text: id }],
timestamp: 1,
source: 'transcript'
}
}
it('preserves task conversion and mode-dependent goal omission across revisions and structural changes', () => {
const frame = (id: string, kind: string, head: string): NativeChatMessage => ({
...text(id),
role: 'system',
blocks: [
{
type: 'text',
text: 'provider notice',
providerFrame: {
provider: 'codex',
kind,
payload: { head, byteLength: head.length, digest: id, truncated: false }
}
}
]
})
const plan = frame(
'plan',
'notification:turn/plan/updated',
'{"plan":[{"step":"Inspect","status":"completed"}]}'
)
const goal = frame('goal', 'notification:thread/goal/cleared', '{}')
const invalid = frame('invalid', 'notification:turn/plan/updated', 'invalid json')
const project = createNativeChatConversationProjection()
expect(project([plan, goal, invalid], true).map((row) => row.id)).toEqual(['plan', 'invalid'])
expect(project([plan], false)[0]?.blocks).toEqual([
{
type: 'tool-call',
name: 'update_plan',
input: '{"plan":[{"step":"Inspect","status":"completed"}]}',
state: 'completed'
}
])
const changedRole = { ...goal, role: 'assistant' as const }
const truncated = frame('truncated', 'notification:turn/plan/updated', '{"plan":[]}')
if (truncated.blocks[0]?.type === 'text' && truncated.blocks[0].providerFrame) {
truncated.blocks[0].providerFrame.payload.truncated = true
}
const planOutput = {
id: 'plan',
role: 'assistant',
blocks: [
{
type: 'tool-call',
name: 'update_plan',
input: '{"plan":[{"step":"Inspect","status":"completed"}]}',
state: 'completed'
}
]
}
const goalOutput = {
id: 'goal',
role: 'system',
blocks: [
{
type: 'text',
text: 'provider notice',
providerFrame: {
provider: 'codex',
kind: 'notification:thread/goal/cleared',
payload: { head: '{}', byteLength: 2, digest: 'goal', truncated: false }
}
}
]
}
const invalidOutput = {
id: 'invalid',
role: 'system',
blocks: [
{
type: 'text',
text: 'provider notice',
providerFrame: {
provider: 'codex',
kind: 'notification:turn/plan/updated',
payload: { head: 'invalid json', byteLength: 12, digest: 'invalid', truncated: false }
}
}
]
}
const assistantGoalOutput = { ...goalOutput, role: 'assistant' }
const truncatedOutput = {
id: 'truncated',
role: 'system',
blocks: [
{
type: 'text',
text: 'provider notice',
providerFrame: {
provider: 'codex',
kind: 'notification:turn/plan/updated',
payload: { head: '{"plan":[]}', byteLength: 11, digest: 'truncated', truncated: true }
}
}
]
}
for (const [rows, visible, hidden] of [
[
[plan, goal, invalid],
[planOutput, goalOutput, invalidOutput],
[planOutput, invalidOutput]
],
[[goal, plan], [goalOutput, planOutput], [planOutput]],
[
[changedRole, invalid],
[assistantGoalOutput, invalidOutput],
[assistantGoalOutput, invalidOutput]
],
[
[truncated, plan, plan],
[truncatedOutput, planOutput, planOutput],
[truncatedOutput, planOutput, planOutput]
],
[[], [], []]
] as const) {
expect(project([...rows], false).map(({ id, role, blocks }) => ({ id, role, blocks }))).toEqual(
visible
)
expect(project([...rows], true).map(({ id, role, blocks }) => ({ id, role, blocks }))).toEqual(
hidden
)
}
})
@@ -1,25 +0,0 @@
import { expect, it } from 'vitest'
import { nativeChatInstallSlotState } from './NativeChatInstallSlot'
it("offers Orca's install only to chats that run the managed runtime", () => {
const notInstalled = {
kind: 'active' as const,
subject: { target: { kind: 'local' as const }, runtimeId: null, agent: 'antigravity' },
removable: false,
snapshot: {
kind: 'known' as const,
state: {
agent: 'antigravity',
revision: 1,
available: true,
version: '1.4.1',
installedVersion: null,
installedCurrent: false,
source: null,
operation: null
}
}
}
expect(nativeChatInstallSlotState(notInstalled, 'strip', false).kind).toBe('install')
expect(nativeChatInstallSlotState(notInstalled, 'strip', true)).toEqual({ kind: 'none' })
})
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { act, cleanup, fireEvent, render, renderHook, waitFor } from '@testing-library/react'
import { Profiler } from 'react'
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { useNativeChatSignIn } from './native-chat-sign-in'
const { state, remoteCreate, persistOrder, startHost, notifyFailure } = vi.hoisted(() => ({
state: {
@@ -34,45 +34,12 @@ vi.mock('@/runtime/hosted-terminals/creation/web-runtime-terminal-create-failure
}))
vi.mock('@/lib/wsl-host-user-start', () => ({ startStoppedWslHostForUserAct: startHost }))
vi.mock('@/lib/claude-account-sign-in', () => ({ signInToClaudeAccount: vi.fn() }))
import { NativeChatSignInContext, useNativeChatSignIn } from './native-chat-sign-in'
import { NativeChatSignInButton } from './NativeChatSignInButton'
afterEach(() => {
cleanup()
vi.clearAllMocks()
})
it.each([
['opencode', 'opencode auth login'],
['pi', 'pi'],
['omp', 'omp']
])(
'opens %s sign-in as a plain terminal in the originating folder and pane',
async (agent, command) => {
const { result } = renderHook(() =>
useNativeChatSignIn({
agent,
target: { kind: 'local' },
worktreeId: 'folder:login-project',
groupId: 'chat-pane',
failure: null,
startFailures: []
})
)
act(() => result.current?.signIn())
await waitFor(() => expect(result.current?.signingIn).toBe(false))
expect(state.createTab).toHaveBeenCalledExactlyOnceWith(
'folder:login-project',
'chat-pane',
undefined,
{ pendingStartup: { command }, viewMode: 'terminal' }
)
expect(state.setActiveTabType).toHaveBeenCalledWith('terminal', 'folder:login-project')
expect(persistOrder).toHaveBeenCalledWith('folder:login-project', 'login-terminal')
expect(remoteCreate).not.toHaveBeenCalled()
}
)
it('runs sign-in on the paired chat host instead of making a local terminal', async () => {
const { result } = renderHook(() =>
useNativeChatSignIn({
@@ -141,62 +108,3 @@ it('leaves a failed WSL start to its existing notification owner', async () => {
expect(remoteCreate).not.toHaveBeenCalled()
expect(notifyFailure).not.toHaveBeenCalled()
})
it.each([20, 200])('shares one sign-in operation across %i row actions', async (count) => {
const pending = Promise.withResolvers<{ status: 'created' }>()
remoteCreate.mockReturnValueOnce(pending.promise)
const eligibleRender = vi.fn()
const unrelatedRender = vi.fn()
const rows = Array.from({ length: count }, (_, index) => (
<div key={index}>
<Profiler id={`auth-${index}`} onRender={eligibleRender}>
<NativeChatSignInButton failureKind="notSignedIn" />
</Profiler>
<Profiler id={`other-${index}`} onRender={unrelatedRender}>
<NativeChatSignInButton failureKind="providerRejected" />
</Profiler>
</div>
))
function Chat({ revision: _revision }: { revision: number }) {
const signIn = useNativeChatSignIn({
agent: 'pi',
target: { kind: 'environment', environmentId: 'paired-host' },
worktreeId: 'remote-project',
failure: null,
startFailures: []
})
return (
<NativeChatSignInContext.Provider value={signIn}>{rows}</NativeChatSignInContext.Provider>
)
}
const view = render(<Chat revision={0} />)
eligibleRender.mockClear()
unrelatedRender.mockClear()
await act(async () => view.rerender(<Chat revision={1} />))
expect(eligibleRender).not.toHaveBeenCalled()
expect(unrelatedRender).not.toHaveBeenCalled()
expect(remoteCreate).not.toHaveBeenCalled()
expect(state.createTab).not.toHaveBeenCalled()
const buttons = view.getAllByRole('button')
expect(buttons).toHaveLength(count)
await act(async () => {
for (const button of buttons) {
fireEvent.click(button)
}
})
expect(startHost).toHaveBeenCalledOnce()
expect(remoteCreate).toHaveBeenCalledOnce()
expect(unrelatedRender).not.toHaveBeenCalled()
expect(eligibleRender).toHaveBeenCalledTimes(count)
expect(buttons.every((button) => button instanceof HTMLButtonElement && button.disabled)).toBe(
true
)
eligibleRender.mockClear()
await act(async () => pending.resolve({ status: 'created' }))
expect(eligibleRender).toHaveBeenCalledTimes(count)
expect(unrelatedRender).not.toHaveBeenCalled()
expect(buttons.every((button) => button instanceof HTMLButtonElement && !button.disabled)).toBe(
true
)
expect(remoteCreate).toHaveBeenCalledOnce()
})
@@ -222,13 +222,3 @@ it('keeps floating chat splits local while a remote workspace is selected', asyn
expect(createRemote).not.toHaveBeenCalled()
expect(store.getState().activeWorktreeId).toBe('remote-workspace')
})
it('retains the existing terminal-backed chat split route', () => {
expect(
runNativeChatSplitTarget({ kind: 'terminal-pane', terminalTabId: 'terminal' }, 'down')
).toBe(true)
expect(requestSplit).toHaveBeenCalledExactlyOnceWith({
tabId: 'terminal',
direction: 'horizontal'
})
})
@@ -32,39 +32,3 @@ it('routes an authenticated-start refusal to sign-in instead of retrying the sam
expect(signIn).toHaveBeenCalledOnce()
expect(retry).not.toHaveBeenCalled()
})
it.each(['failed', 'visibility-unknown'] as const)(
'says the create hand-back failure once and keeps Retry for %s',
(lifecycle) => {
const retry = vi.fn()
const message = 'The message is back in the composer.'
const notices = structuredSessionNotices({
launch: { lifecycle, failure: null, retry },
agentLabel: 'Codex',
sessionError: message,
composerError: null
})
render(<NativeChatComposerNotices notices={notices} />)
expect(screen.getAllByText(message)).toHaveLength(1)
expect(screen.queryByText('Chat could not be started.')).toBeNull()
expect(screen.queryByText('Chat connection could not be confirmed.')).toBeNull()
fireEvent.click(screen.getByRole('button', { name: 'Retry' }))
expect(retry).toHaveBeenCalledOnce()
}
)
it('keeps unrelated composer errors beside the hand-back notice', () => {
render(
<NativeChatComposerNotices
notices={structuredSessionNotices({
launch: { lifecycle: 'failed', failure: null, retry: () => {} },
agentLabel: 'Codex',
sessionError: 'The message is back in the composer.',
composerError: { text: 'Attachment could not be added.', onDismiss: () => {} }
})}
/>
)
expect(screen.getByText('The message is back in the composer.')).toBeTruthy()
expect(screen.getByText('Attachment could not be added.')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Retry' })).toBeTruthy()
})
@@ -3,36 +3,6 @@ import { act, renderHook } from '@testing-library/react'
import { expect, it, vi } from 'vitest'
import { useNativeChatHistoryRetention } from './use-native-chat-history-retention'
it('holds parked history through hidden renders and releases on return, rebind and unmount', () => {
const release = vi.fn()
const retainHistory = vi.fn(() => release)
const trimHistory = vi.fn()
const view = renderHook(
(source) =>
useNativeChatHistoryRetention({ ...source, root: { current: null }, sourceKey: 'test' }),
{
initialProps: { retainHistory, trimHistory }
}
)
act(() => {
view.result.current.leaveEnd()
view.result.current.leaveEnd()
})
view.rerender({ retainHistory, trimHistory })
expect(retainHistory).toHaveBeenCalledOnce()
expect(release).not.toHaveBeenCalled()
act(() => view.result.current.returnToEnd())
expect(release).toHaveBeenCalledOnce()
act(() => view.result.current.leaveEnd())
const nextRelease = vi.fn()
const nextRetain = vi.fn(() => nextRelease)
view.rerender({ retainHistory: nextRetain, trimHistory })
expect(release).toHaveBeenCalledTimes(2)
act(() => view.result.current.leaveEnd())
view.unmount()
expect(nextRelease).toHaveBeenCalledOnce()
})
it.each([false, true])(
'preserves a mounted selection until it clears (shared source: %s)',
(shared) => {
@@ -1,21 +1,15 @@
import type * as RuntimeCapabilityProofModule from '@/runtime/rpc/compatibility/runtime-capability-proof'
// @vitest-environment happy-dom
import {
mocks,
answer,
mutateWith,
renderOptions,
REOPENED,
currentValue
} from './use-structured-agent-session-options.test-fixture'
import { replaceRuntimeEnvironmentRevisions } from '@/runtime/rpc/environment-revisions/runtime-environment-revision'
import { forgetStructuredSessionSelection } from '@/lib/agent-session/options/structured-agent-session-selection-controller'
import { resetStructuredAgentSessionStatusFeedsForTests } from '@/runtime/structured-agent-session-status-feed'
import type { AgentSessionStatusEvent } from '../../../../shared/agent-session/agent-session-wire'
import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target'
import { useStructuredAgentSessionOptionStamp } from './structured-agent-session-status-hooks'
import { act, renderHook, waitFor } from '@testing-library/react'
import { act, waitFor } from '@testing-library/react'
import { expect, it, vi } from 'vitest'
vi.mock('@/runtime/rpc/compatibility/runtime-capability-proof', async (importOriginal) => ({
@@ -23,115 +17,6 @@ vi.mock('@/runtime/rpc/compatibility/runtime-capability-proof', async (importOri
runtimeEnvironmentSupportsCapability: async () => true
}))
it.each([2, 20])('isolates mode refresh reads and renders with %s mounted chats', async (count) => {
resetStructuredAgentSessionStatusFeedsForTests()
replaceRuntimeEnvironmentRevisions([{ id: 'server-1', createdAt: 1 }])
forgetStructuredSessionSelection('session-1')
for (let i = 0; i < count - 1; i++) {
forgetStructuredSessionSelection(`other-${i}`)
}
const emitters = new Map<string, (event: AgentSessionStatusEvent) => void>()
mocks.subscribeStatus.mockImplementation(async (target, onEvent) => {
emitters.set(target.kind, onEvent)
return { unsubscribe: () => {} }
})
let localMode: 'default' | 'plan' = 'default'
let remoteMode: 'default' | 'plan' = 'default'
const read = vi.fn(async (target: RuntimeClientTarget) => ({
models: [],
current: {
interactionMode: target.kind === 'local' ? localMode : remoteMode,
acceptedInputSources: { interactionMode: 'saved' }
}
}))
answer({ options: read })
const { mutate } = mutateWith(async () => null)
const stampRenders = vi.fn()
const stamp = renderHook(() => {
stampRenders()
return useStructuredAgentSessionOptionStamp('session-1', { kind: 'local' })
})
const affectedRenders = vi.fn()
const unaffectedRenders = vi.fn()
const remoteRenders = vi.fn()
const first = renderOptions({ ...REOPENED, onRender: affectedRenders }, mutate)
const sameChat = renderOptions({ ...REOPENED, onRender: affectedRenders }, mutate)
const others = Array.from({ length: count - 1 }, (_, i) =>
renderOptions({ ...REOPENED, sessionId: `other-${i}`, onRender: unaffectedRenders }, mutate)
)
const remote = renderOptions({ ...REOPENED, paired: true, onRender: remoteRenders }, mutate)
const mounted = [first, sameChat, ...others, remote]
const summary = {
sessionId: 'session-1',
workspaceId: 'workspace-1',
agent: 'codex',
status: 'idle' as const,
latestPrompt: '',
updatedAt: 1
}
const publish = (host: string, session = summary, mode: 'default' | 'plan' = 'default') =>
act(async () =>
emitters.get(host)?.({ type: 'status', session: { ...session, interactionMode: mode } })
)
try {
for (const hook of mounted) {
await waitFor(() =>
expect(currentValue(hook.result.current.optionSnapshot, 'interactionMode')).toBe('default')
)
}
await publish('local')
await publish('environment')
for (let i = 0; i < count - 1; i++) {
await publish('local', { ...summary, sessionId: `other-${i}` })
}
await act(async () => {})
read.mockClear()
stampRenders.mockClear()
affectedRenders.mockClear()
unaffectedRenders.mockClear()
remoteRenders.mockClear()
await publish('local', { ...summary, sessionId: 'unmounted' }, 'plan')
await publish('local', { ...summary, latestPrompt: 'transcript changed' })
expect(read).not.toHaveBeenCalled()
expect(stampRenders).not.toHaveBeenCalled()
expect(unaffectedRenders).not.toHaveBeenCalled()
expect(remoteRenders).not.toHaveBeenCalled()
localMode = 'plan'
await publish('local', summary, 'plan')
for (const hook of [first, sameChat]) {
await waitFor(() =>
expect(currentValue(hook.result.current.optionSnapshot, 'interactionMode')).toBe('plan')
)
}
expect(read.mock.calls.length).toBeGreaterThan(0)
expect(read.mock.calls.length).toBeLessThanOrEqual(2)
expect(affectedRenders).toHaveBeenCalled()
expect(stampRenders).toHaveBeenCalled()
expect(unaffectedRenders).not.toHaveBeenCalled()
expect(remoteRenders).not.toHaveBeenCalled()
read.mockClear()
stampRenders.mockClear()
affectedRenders.mockClear()
remoteMode = 'plan'
await publish('environment', summary, 'plan')
await waitFor(() =>
expect(currentValue(remote.result.current.optionSnapshot, 'interactionMode')).toBe('plan')
)
expect(read).toHaveBeenCalledTimes(1)
expect(remoteRenders).toHaveBeenCalled()
expect(affectedRenders).not.toHaveBeenCalled()
expect(stampRenders).not.toHaveBeenCalled()
expect(unaffectedRenders).not.toHaveBeenCalled()
} finally {
stamp.unmount()
mounted.forEach((hook) => hook.unmount())
resetStructuredAgentSessionStatusFeedsForTests()
mocks.subscribeStatus.mockImplementation(async () => ({ unsubscribe: vi.fn() }))
}
})
it('projects transmitted mode admission to the optimistic exit control and recovers after settlement', async () => {
const sessionId = 'mode-admission-projection'
forgetStructuredSessionSelection(sessionId)
@@ -136,48 +136,6 @@ function renderActions(
}
describe('onboarding completion destination', () => {
it.each(['claude', 'codex'] as const)(
'opens the requested %s in the selected workspace after close',
async (agent) => {
const { result, openModal, update, onOnboardingChange } = renderActions(
true,
STEPS.length - 1,
agent
)
await act(() => result.current.next())
expect(update).toHaveBeenCalledWith(
expect.objectContaining({ outcome: 'completed', lastCompletedStep: 5 })
)
expect(launch).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
agent,
worktreeId: 'folder:home',
workspaceExecutionHostId: 'runtime:chosen',
launchSource: 'onboarding',
groupId: 'group',
focusIntent
})
)
expect(onOnboardingChange.mock.invocationCallOrder[0]).toBeLessThan(
launch.mock.invocationCallOrder[0]
)
launch.mock.calls[0][0].beforeSurfaceOpen()
expect(reveal).toHaveBeenCalledWith(
'folder:home',
expect.objectContaining({
agent,
providesInitialSurface: true,
executionHostId: 'runtime:chosen'
})
)
expect(openModal).not.toHaveBeenCalled()
expect(track).toHaveBeenCalledWith(
'onboarding_completed',
expect.objectContaining({ path: 'workspace' })
)
}
)
it('reports a captured workspace disappearing while completion is saved, without opening a surface', async () => {
const { result, persistCurrentStep } = renderActions(true, STEPS.length - 1, 'claude')
const saved = Promise.withResolvers<{ ok: boolean }>()
@@ -200,45 +158,6 @@ describe('onboarding completion destination', () => {
expect(toastError).toHaveBeenCalledOnce()
})
it('retains Add Project when no workspace is selected', async () => {
const { result, openModal } = renderActions(false)
await act(() => result.current.next())
expect(launch).not.toHaveBeenCalled()
expect(openModal).toHaveBeenCalledWith('add-repo')
expect(track).toHaveBeenCalledWith(
'onboarding_completed',
expect.objectContaining({ path: 'add_project_modal' })
)
})
it.each([true, false])(
'returns to the main page when setting up later, selected workspace=%s',
async (hasSelectedWorkspace) => {
const { result, openModal, update, updateSettings } = renderActions(
hasSelectedWorkspace,
STEPS.findIndex((step) => step.id === 'agent'),
'claude'
)
await act(() => result.current.setUpLater())
expect(updateSettings).toHaveBeenCalledWith({ defaultTuiAgent: 'claude' })
expect(update).toHaveBeenCalledWith(
expect.objectContaining({
outcome: 'completed',
lastCompletedStep: 5,
checklist: { dismissed: false }
})
)
expect(openModal).not.toHaveBeenCalled()
if (hasSelectedWorkspace) {
expect(launch).toHaveBeenCalledWith(
expect.objectContaining({ agent: 'claude', worktreeId: 'folder:home' })
)
} else {
expect(launch).not.toHaveBeenCalled()
}
}
)
it('keeps onboarding open for retry when setting up later cannot save completion', async () => {
const { result, update, onOnboardingChange, openModal } = renderActions(
true,
@@ -303,66 +222,6 @@ describe('onboarding completion destination', () => {
)
})
it.each(['chat', 'terminal'] as const)(
'confirms first-page %s mode and launches the saved agent instead of a hidden auto-pick',
async (surface) => {
const { result, openModal } = renderActions(true, 0, 'claude', {
surface,
defaultAgent: 'codex'
})
await act(() => result.current.setUpLater())
expect(state.settings).toEqual(
expect.objectContaining({
agentDefaultSurface: surface,
defaultTuiAgent: 'codex'
})
)
expect(launch).toHaveBeenCalledWith(
expect.objectContaining({ agent: 'codex', worktreeId: 'folder:home' })
)
expect(openModal).not.toHaveBeenCalled()
}
)
it.each(['agent_surface', 'theme'] as const)(
'preserves saved Blank on %s despite a hidden auto-pick',
async (step) => {
const { result } = renderActions(
true,
STEPS.findIndex((value) => value.id === step),
'claude',
{
surface: 'chat',
defaultAgent: 'blank'
}
)
await act(() => result.current.setUpLater())
expect(state.settings?.defaultTuiAgent).toBe('blank')
expect(launch).not.toHaveBeenCalled()
}
)
it('confirms a visible agent or Blank without substituting an installed agent', async () => {
const agentPage = STEPS.findIndex((step) => step.id === 'agent')
const chosen = renderActions(true, agentPage, 'aider', { defaultAgent: 'codex' })
await act(() => chosen.result.current.setUpLater())
expect(state.settings?.defaultTuiAgent).toBe('aider')
expect(launch).toHaveBeenCalledWith(expect.objectContaining({ agent: 'aider' }))
cleanup()
launch.mockClear()
const blank = renderActions(true, agentPage, null, { defaultAgent: 'codex' })
await act(() => blank.result.current.setUpLater())
expect(state.settings?.defaultTuiAgent).toBe('blank')
expect(launch).not.toHaveBeenCalled()
})
it('resolves ordinary Auto without replacing the saved preference', async () => {
const { result } = renderActions(true, 0, 'codex')
await act(() => result.current.setUpLater())
expect(state.settings?.defaultTuiAgent).toBeNull()
expect(launch).toHaveBeenCalledWith(expect.objectContaining({ agent: 'claude' }))
})
it('retains the clicked workspace owner through deferred save and post-close discovery', async () => {
const { result, updateSettings, onOnboardingChange } = renderActions(true, 0, 'claude', {
surface: 'chat',
@@ -6,7 +6,6 @@ import { useAppStore } from '@/store'
import { getDefaultSettings } from '../../../../shared/constants'
import { getDefaultOnboardingState } from '../../../../shared/onboarding-defaults'
import { useOnboardingFlow } from './use-onboarding-flow'
import { usePersistCurrentStep } from './use-onboarding-flow-persistence'
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState(), true)
@@ -60,56 +59,4 @@ describe('onboarding completion persistence', () => {
expect(openModal).not.toHaveBeenCalled()
expect(notify).toHaveBeenCalledOnce()
})
it.each([
['claude', 'claude'],
[null, 'blank']
] as const)(
'saves the visible %s selection when completing without visiting the agent page',
async (selectedAgent, expectedAgent) => {
const savedProgress = getDefaultOnboardingState()
const saveProgress = vi.fn().mockResolvedValue(savedProgress)
Object.defineProperty(window, 'api', {
configurable: true,
value: { onboarding: { update: saveProgress } }
})
vi.spyOn(useAppStore.getState(), 'recordFeatureInteraction').mockResolvedValue(undefined)
const updateSettings = vi.fn().mockResolvedValue(undefined)
const onOnboardingChange = vi.fn()
const { result } = renderHook(() =>
usePersistCurrentStep({
currentStepId: 'notifications',
agentSurface: 'terminal',
selectedAgent,
agentInteractedRef: { current: false },
yoloPermissions: true,
yoloPermissionsChosen: { current: false },
theme: 'dark',
settings: {
...getDefaultSettings('/tmp'),
defaultTuiAgent: 'codex',
agentDefaultSurface: 'chat'
},
updateSettings,
onboardingChecklist: savedProgress.checklist,
onOnboardingChange,
setError: vi.fn()
})
)
await act(async () => {
expect(await result.current()).toEqual({ ok: true })
})
expect(updateSettings).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ defaultTuiAgent: expectedAgent, agentDefaultSurface: 'terminal' })
)
expect(saveProgress).toHaveBeenCalledWith(
expect.objectContaining({
lastCompletedStep: 5,
checklist: expect.objectContaining({ choseAgent: selectedAgent !== null })
})
)
expect(onOnboardingChange).toHaveBeenCalledWith(savedProgress)
}
)
})
@@ -1,330 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PluginHostListEntry } from '../../../../../preload/api-types'
import {
pluginListEntryFromInstalled,
pluginListEntryFromListing
} from '../list/plugin-list-entries'
import { pluginAuthor, pluginAuthorCatalogId, pluginsBySamePublisher } from './plugin-author'
import {
pluginAuthorGithubUrl,
pluginSourceGitUrl,
pluginSourceReadmeUrl
} from './plugin-source-links'
function listed(pluginKey: string, url: string, marketplaceSourceId = 'community') {
return pluginListEntryFromListing({
pluginKey,
marketplaceSourceId,
marketplaceName: 'Community',
marketplaceOwner: 'catalog-owner',
marketplaceCommit: 'abc',
source: { kind: 'git', url, ref: 'main' },
categories: [],
official: false,
bundled: false
})
}
function installed(
source: Pick<NonNullable<PluginHostListEntry['source']>, 'kind' | 'reference'> &
Partial<NonNullable<PluginHostListEntry['source']>>,
isDev = false
) {
return pluginListEntryFromInstalled(
{
pluginKey: 'acme.notes',
name: 'Notes',
publisher: 'acme',
version: '1.0.0',
status: 'running',
consentFingerprint: null,
needsReconsent: false,
isDev,
official: false,
bundled: false,
capabilities: [],
panels: [],
commands: [],
hasWorker: false,
restarts: 0,
source: { resolvedCommit: null, contentHash: 'b'.repeat(64), ...source }
},
listed('acme.notes', 'https://github.com/acme/notes').listing
)
}
describe('plugin publisher and source provenance', () => {
it('links GitHub publishers only when the repository owner matches the publisher', () => {
expect(pluginAuthorGithubUrl(listed('acme.notes', 'https://github.com/Acme/notes.git'))).toBe(
'https://github.com/acme'
)
expect(pluginAuthorGithubUrl(listed('acme.notes', 'git@github.com:acme/notes.git'))).toBe(
'https://github.com/acme'
)
expect(
pluginAuthorGithubUrl(listed('victim.notes', 'https://github.com/mirror/notes'))
).toBeNull()
expect(pluginAuthorGithubUrl(listed('acme.notes', 'https://gitlab.com/acme/notes'))).toBeNull()
expect(
pluginAuthorGithubUrl(installed({ kind: 'local-path', reference: '/plugins/notes' }))
).toBeNull()
})
it('recommends only the same publisher label within the same catalog', () => {
const entry = listed('acme.notes', 'https://github.com/acme/notes')
const entries = {
installed: [],
available: [
listed('acme.icons', 'https://github.com/Acme/icons'),
listed('acme.other-host', 'https://gitlab.com/acme/other'),
listed('acme.other-catalog', 'https://github.com/acme/other', 'unrelated')
]
}
expect(pluginsBySamePublisher(entry, entries).map((item) => item.pluginKey)).toEqual([
'acme.icons',
'acme.other-host'
])
})
it('keeps installed mirror authors and recommendations tied to the recorded release', () => {
const entry = installed({
kind: 'marketplace',
reference: 'https://github.com/mirrors/plugins.git (plugins/notes)',
url: 'https://github.com/mirrors/plugins.git',
path: 'plugins/notes',
authorRepository: 'https://github.com/acme/notes',
marketplace: {
reference: 'https://github.com/catalog/plugins',
resolvedCommit: 'a'.repeat(40)
}
})
const other = {
...entry,
pluginKey: 'acme.icons',
installed: { ...entry.installed!, pluginKey: 'acme.icons' }
}
expect(pluginAuthor(entry)).toEqual({
publisher: 'acme',
catalog: 'Community',
provenance: 'catalog'
})
expect(pluginSourceGitUrl(entry)).toBe('https://github.com/acme/notes')
expect(
pluginsBySamePublisher(entry, { installed: [other], available: [] }).map(
(item) => item.pluginKey
)
).toEqual(['acme.icons'])
// A newer catalog record must not replace the installed release's attribution.
const withoutListing = { ...entry, listing: null, updatable: false }
expect(pluginSourceGitUrl(withoutListing)).toBe('https://github.com/acme/notes')
expect(pluginAuthorCatalogId(withoutListing)).toBeNull()
})
it('uses structured monorepo sources instead of parsing their display label', () => {
const entry = installed({
kind: 'marketplace',
reference: 'https://github.com/acme/plugins.git (plugins/notes)',
url: 'https://github.com/acme/plugins.git',
path: 'plugins/notes'
})
expect(pluginSourceGitUrl(entry)).toBe('https://github.com/acme/plugins.git')
})
it('revokes a recorded repository only when the installed catalog release marks it unavailable', () => {
const entry = installed({
kind: 'marketplace',
reference: 'https://github.com/mirrors/plugins.git',
url: 'https://github.com/mirrors/plugins.git',
resolvedCommit: 'a'.repeat(40),
authorRepository: 'https://github.com/acme/notes',
marketplace: {
reference: 'https://github.com/catalog/plugins',
resolvedCommit: 'c'.repeat(40)
}
})
entry.listing = {
...entry.listing!,
source: { kind: 'git', url: 'https://github.com/mirrors/plugins.git', ref: 'main' },
release: {
version: '1.0.0',
commit: 'a'.repeat(40),
contentHash: 'b'.repeat(64),
execution: 'data',
minimumOrca: '1.0.0',
repository: 'https://github.com/acme/notes',
sourceUnavailable: true
}
}
expect(pluginSourceGitUrl(entry)).toBe('https://github.com/mirrors/plugins.git')
expect(pluginSourceGitUrl({ ...entry, listing: null })).toBe('https://github.com/acme/notes')
expect(pluginSourceGitUrl({ ...entry, updatable: false })).toBe('https://github.com/acme/notes')
entry.listing.release!.commit = 'd'.repeat(40)
expect(pluginSourceGitUrl(entry)).toBe('https://github.com/acme/notes')
})
it('borrows a catalog repository only from a source and release matched listing', () => {
const entry = installed({
kind: 'marketplace',
reference: 'Mirror · Notes',
url: 'https://github.com/mirrors/plugins.git',
path: 'plugins/notes',
resolvedCommit: 'a'.repeat(40),
marketplace: {
reference: 'https://github.com/catalog/plugins',
resolvedCommit: 'c'.repeat(40)
}
})
entry.listing = {
...entry.listing!,
source: {
kind: 'git',
url: 'https://github.com/mirrors/plugins.git',
ref: 'main',
path: 'plugins/notes'
},
release: {
version: '1.0.0',
commit: 'a'.repeat(40),
contentHash: 'b'.repeat(64),
execution: 'data',
minimumOrca: '1.0.0',
repository: 'https://github.com/acme/notes'
}
}
for (const listing of [
{ ...entry.listing, source: { ...entry.listing.source, path: 'plugins/other' } },
{
...entry.listing,
source: { ...entry.listing.source, url: 'https://github.com/other/plugins' }
},
{ ...entry.listing, release: { ...entry.listing.release!, commit: 'd'.repeat(40) } },
{ ...entry.listing, release: { ...entry.listing.release!, contentHash: 'e'.repeat(64) } },
{ ...entry.listing, release: { ...entry.listing.release!, sourceUnavailable: true as const } }
]) {
expect(pluginSourceGitUrl({ ...entry, listing })).toBe(
'https://github.com/mirrors/plugins.git'
)
}
expect(pluginSourceGitUrl({ ...entry, updatable: false })).toBe(
'https://github.com/mirrors/plugins.git'
)
})
it('keeps a mirror catalog claim separate from an account identity', () => {
const entry = listed('victim.plugin', 'https://github.com/mirror/plugins')
entry.listing!.release = {
version: '1.0.0',
commit: 'a'.repeat(40),
contentHash: 'b'.repeat(64),
execution: 'data',
minimumOrca: '1.0.0',
repository: 'https://github.com/victim/repo'
}
expect(pluginAuthor(entry)).toEqual({
publisher: 'victim',
catalog: 'Community',
provenance: 'catalog'
})
})
it.each([
{
reference: 'https://github.com/acme/notes',
url: 'https://github.com/acme/notes',
path: undefined,
readme: 'https://github.com/acme/notes/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa#readme'
},
{
reference: 'https://github.com/acme/plugins (plugins/notes)',
url: 'https://github.com/acme/plugins',
path: 'plugins/notes',
readme:
'https://github.com/acme/plugins/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/plugins/notes#readme'
}
])(
'restores older-host links only for the recorded location $reference',
({ reference, path, url, readme }) => {
const entry = installed({
kind: 'marketplace',
reference,
resolvedCommit: 'a'.repeat(40),
marketplace: {
reference: 'https://github.com/catalog/plugins',
resolvedCommit: 'c'.repeat(40)
}
})
entry.listing!.source = { kind: 'git', url, ref: 'main', path }
expect(pluginSourceGitUrl(entry)).toBe(url)
expect(pluginSourceReadmeUrl(entry)).toBe(readme)
expect(pluginSourceGitUrl({ ...entry, updatable: false })).toBeNull()
expect(pluginSourceGitUrl({ ...entry, listing: null })).toBeNull()
entry.listing!.source.path = 'plugins/other'
expect(pluginSourceGitUrl(entry)).toBeNull()
expect(pluginSourceReadmeUrl(entry)).toBeNull()
}
)
it('links an installed mirror README at its recorded path and commit, even after catalog changes', () => {
const entry = installed({
kind: 'marketplace',
reference: 'mirror',
url: 'https://gitlab.com/mirror/plugins',
path: 'plugins/notes',
resolvedCommit: 'a'.repeat(40),
authorRepository: 'https://github.com/acme/notes'
})
expect(pluginSourceReadmeUrl(entry)).toBe(
'https://gitlab.com/mirror/plugins/-/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/plugins/notes#readme'
)
expect(pluginSourceReadmeUrl({ ...entry, listing: null })).toBe(
'https://gitlab.com/mirror/plugins/-/tree/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/plugins/notes#readme'
)
entry.installed!.source!.resolvedCommit = null
expect(pluginSourceReadmeUrl(entry)).toBeNull()
})
it('does not borrow a listing profile or repository for local and dev installs', () => {
for (const isDev of [true, false]) {
const entry = installed({ kind: 'local-path', reference: '/plugins/notes' }, isDev)
expect(pluginSourceGitUrl(entry)).toBeNull()
}
})
it('uses a Git install source even when an unrelated listing is attached', () => {
const entry = installed({ kind: 'git', reference: 'https://gitlab.com/acme/notes' })
expect(pluginSourceGitUrl(entry)).toBe('https://gitlab.com/acme/notes')
expect(pluginAuthor(entry)).toEqual({ publisher: 'acme', catalog: null, provenance: 'git' })
expect(pluginAuthor({ ...entry, listing: null })).toEqual({
publisher: 'acme',
catalog: null,
provenance: 'git'
})
})
it('attributes local and dev publisher labels to the manifest', () => {
for (const isDev of [true, false]) {
const entry = installed({ kind: 'local-path', reference: '/plugins/notes' }, isDev)
expect(pluginAuthor(entry)).toEqual({
publisher: 'acme',
catalog: null,
provenance: 'manifest'
})
}
})
it('keeps unnamed catalog and unknown install sources distinct', () => {
const entry = installed({ kind: 'marketplace', reference: 'catalog' })
entry.listing = null
expect(pluginAuthor(entry)).toEqual({
publisher: 'acme',
catalog: null,
provenance: 'catalog'
})
entry.installed!.source = undefined
expect(pluginAuthor(entry)).toEqual({
publisher: 'acme',
catalog: null,
provenance: 'unknown'
})
})
})
@@ -1,37 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string) => fallback
}))
import { pluginInstallErrorMessage } from './plugin-error-presentation'
// Why: keep presentation tests locked to pluginInstallTrustError production strings
// so a "git" substring cannot silently re-route reserved errors to installGit.
const reservedRefusal = (pluginKey: string): string =>
`${pluginKey} uses a name reserved for official Orca plugins; Orca installs it only from its official catalog`
describe('pluginInstallErrorMessage', () => {
it('explains how to rename a local fork of an official template', () => {
const message = pluginInstallErrorMessage(
new Error(reservedRefusal('stablyai.orca-portuguese'))
)
expect(message).toContain('reserved for official Orca plugins')
expect(message).toContain('orca-plugin.json')
expect(message).toContain('"orca-"')
expect(message).toContain('"publisher"')
})
it('does not mistake a reserved key containing "git" for a fetch failure', () => {
const message = pluginInstallErrorMessage(new Error(reservedRefusal('github.tools')))
expect(message).toContain('reserved for official Orca plugins')
expect(message).not.toContain("Couldn't fetch")
})
it('still maps genuine git fetch failures to the installGit message', () => {
const message = pluginInstallErrorMessage(
new Error('git fetch failed: could not clone repository from remote')
)
expect(message).toContain("Couldn't fetch this Git revision")
})
})
@@ -1,48 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
disambiguatedContentPluginNames,
disambiguatedPluginContentLabels
} from './plugin-name-disambiguation'
describe('plugin content labels', () => {
it('suffixes the community theme, never the official one, by its key publisher', () => {
const labels = disambiguatedPluginContentLabels([
{
id: 'plugin:stablyai.orca-catppuccin-theme/mocha',
pluginKey: 'stablyai.orca-catppuccin-theme',
label: 'Catppuccin Mocha'
},
{
id: 'plugin:vale-c.catppuccin/mocha',
pluginKey: 'vale-c.catppuccin',
label: 'Catppuccin Mocha'
},
{
id: 'plugin:vale-c.catppuccin/latte',
pluginKey: 'vale-c.catppuccin',
label: 'Catppuccin Latte'
}
])
expect(labels.get('plugin:stablyai.orca-catppuccin-theme/mocha')).toBe('Catppuccin Mocha')
expect(labels.get('plugin:vale-c.catppuccin/mocha')).toBe('Catppuccin Mocha (vale-c)')
expect(labels.get('plugin:vale-c.catppuccin/latte')).toBe('Catppuccin Latte')
})
it("keeps Orca's built-in names plain and suffixes a plugin entry sharing one", () => {
const labels = disambiguatedPluginContentLabels(
[{ id: 'plugin:acme.dracula/d', pluginKey: 'acme.dracula', label: 'Dracula' }],
['Dracula']
)
expect(labels.get('plugin:acme.dracula/d')).toBe('Dracula (acme)')
})
it('names clashing plugins once each', () => {
const names = disambiguatedContentPluginNames([
{ pluginKey: 'stablyai.orca-catppuccin-theme', pluginName: 'Catppuccin for Orca' },
{ pluginKey: 'vale-c.catppuccin', pluginName: 'Catppuccin for Orca' },
{ pluginKey: 'vale-c.catppuccin', pluginName: 'Catppuccin for Orca' }
])
expect(names.get('stablyai.orca-catppuccin-theme')).toBe('Catppuccin for Orca')
expect(names.get('vale-c.catppuccin')).toBe('Catppuccin for Orca (vale-c)')
})
})
@@ -1,66 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { expect, it } from 'vitest'
import { pluginListEntryFromListing, type PluginListEntry } from '../list/plugin-list-entries'
import type { PluginPagePreview } from './use-plugin-catalog'
import { usePluginPageModel } from './use-plugin-page-model'
it('pins catalog provenance with its listing until the newer entry is selected', async () => {
const original = pluginListEntryFromListing(
{
pluginKey: 'acme.notes',
marketplaceSourceId: 'first',
marketplaceName: 'Community',
marketplaceOwner: 'catalog',
marketplaceCommit: 'a',
source: { kind: 'git', url: 'https://github.com/acme/notes', ref: 'main' },
categories: [],
official: false,
bundled: false
},
['first'],
'https://github.com/first/catalog'
)
const newer = pluginListEntryFromListing(
{
...original.listing!,
marketplaceSourceId: 'second',
marketplaceCommit: 'b'
},
['second'],
'https://gitlab.com/second/catalog'
)
const fetchPreview = () => new Promise<PluginPagePreview>(() => undefined)
let model: ReturnType<typeof usePluginPageModel> | undefined
function Harness({ entry }: { entry: PluginListEntry }) {
model = usePluginPageModel({ entry, fetchPreview, refreshing: false })
return null
}
const root = createRoot(document.createElement('div'))
try {
await act(async () => root.render(<Harness entry={original} />))
expect(model?.entry.verifiedSource).toEqual({
kind: 'catalog',
catalog: 'Community',
owner: 'github.com/first'
})
await act(async () => root.render(<Harness entry={newer} />))
expect(model?.entry.verifiedSource).toEqual({
kind: 'catalog',
catalog: 'Community',
owner: 'github.com/first'
})
expect(model?.listing?.marketplaceSourceId).toBe('first')
await act(async () => model?.showNewer())
expect(model?.entry.verifiedSource).toEqual({
kind: 'catalog',
catalog: 'Community',
owner: 'gitlab.com/second'
})
expect(model?.listing?.marketplaceSourceId).toBe('second')
} finally {
act(() => root.unmount())
}
})
@@ -93,30 +93,6 @@ describe('usePluginPageNavigation', () => {
expect(document.activeElement).toBe(container.querySelector('[data-plugin-row-name]'))
})
it('clears history for catalog requests and discards removed pages', async () => {
await mountOpen()
await act(async () => nav?.open('acme.icons'))
await act(async () => nav?.leave('acme.notes'))
expect(nav?.openKey).toBe('acme.icons')
await act(async () => nav?.back())
expect(nav?.openKey).toBeNull()
await act(async () => nav?.open('acme.notes'))
await act(async () => nav?.open('acme.icons'))
await act(async () => nav?.toCatalog())
expect(nav?.openKey).toBeNull()
await act(async () => nav?.open('acme.third'))
await act(async () => nav?.back())
expect(nav?.openKey).toBeNull()
})
it('returns to the originating detail page after replacing a completed review', async () => {
await mountOpen()
await act(async () => nav?.open('acme.tasks'))
await act(async () => nav?.open('acme.third', { replace: true }))
await act(async () => nav?.back())
expect(nav?.openKey).toBe('acme.notes')
})
it('pauses A when visiting B, keeps B decisions outside the queue, and resumes A before the next review', async () => {
await mountOpen()
await act(async () => reviews?.enqueue(['acme.notes', 'acme.third']))
@@ -1,13 +1,7 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { CircleDashed } from 'lucide-react'
import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner'
import { getCheckCounts, getOverallCheckCounts, getChecksSummaryLabel } from './pr-check-counts'
import { ChecksTabLayouts } from './pull-request-page/checks/layouts'
import { ChecksCompactHeader } from './pull-request-page/checks/toolbar'
import { ChecksTabCompactHeader } from './github-item-dialog/inspect-pull-request/checks-tab-header'
import { getChecksTabSummaryPresentation } from './github-item-dialog/inspect-pull-request/checks-tab-summary'
import { PRTriageStrip } from './right-sidebar/checks-panel/presentation/triage-strip'
import { i18n } from '@/i18n/i18n'
import type { PRCheckDetail } from '../../../shared/github/check-types'
@@ -41,138 +35,6 @@ afterEach(async () => {
await i18n.changeLanguage('en')
})
it.each([ChecksCompactHeader, ChecksTabCompactHeader])(
'keeps row approval chips while only current associated rows explain an owner failure',
(Header) => {
const accepted = acceptPRCheckObservation({ ...owner, checksSummary: blocked }, null, context)!
const counts = getCheckCounts(rows)
expect(counts).toMatchObject({ passing: 0, failing: 0, needsAction: 1 })
const overall = getOverallCheckCounts(rows, accepted, accepted)
render(
<Header
{...getChecksTabSummaryPresentation(overall)}
counts={overall}
summaryLabel={getChecksSummaryLabel(rows, accepted, accepted)}
listLength={1}
refreshAction={<button>Refresh</button>}
rerunAction={null}
secondaryActions={null}
/>
)
expect(screen.getByText('1 check needs action')).toBeTruthy()
cleanup()
const newer = acceptPRCheckObservation(
{ ...owner, checksSummary: { ...blocked } },
accepted,
context
)!
const nextCounts = getOverallCheckCounts(rows, newer, accepted)
render(
<Header
{...getChecksTabSummaryPresentation(nextCounts)}
counts={nextCounts}
summaryLabel={getChecksSummaryLabel(rows, newer, accepted)}
listLength={1}
refreshAction={<button>Refresh</button>}
rerunAction={null}
secondaryActions={null}
/>
)
expect(screen.getByText('1 check failing')).toBeTruthy()
expect(
getOverallCheckCounts(
rows,
acceptPRCheckObservation(
{ ...owner, checksSummary: { ...blocked, failed: 2, total: 102 } },
null,
context
)!,
accepted
).failing
).toBe(2)
}
)
it.each(['page', 'compact'] as const)(
'does not claim zero whole checks in the empty %s renderer',
async (variant) => {
i18n.addResourceBundle(
'es',
'translation',
{
auto: {
components: {
right: { sidebar: { ChecksPanel: { '976cefd02f': 'Comprobaciones no disponibles' } } },
prCheckCounts: { noCheckDetails: 'No hay detalles de comprobaciones' }
}
}
},
true,
true
)
await i18n.changeLanguage('es')
const accepted = acceptPRCheckObservation(owner, null, context)!
const summaryLabel = getChecksSummaryLabel([], accepted)
render(
<ChecksTabLayouts
owner={accepted}
loading={false}
listLength={0}
variant={variant}
compactHeader={<div>{summaryLabel}</div>}
actions={<button>Refresh</button>}
SummaryIcon={CircleDashed}
summaryColor=""
summaryLabel={summaryLabel}
counts={getCheckCounts([])}
summaryPending={false}
sorted={[]}
renderCheckRow={() => <div />}
fixChecksAgentDialog={<span />}
/>
)
expect(screen.getByText('Comprobaciones no disponibles')).toBeTruthy()
expect(screen.getByText('No hay detalles de comprobaciones')).toBeTruthy()
expect(screen.queryByText('No checks found')).toBeNull()
}
)
it('keeps the row Fix action available when the complete owner is unavailable', () => {
const accepted = acceptPRCheckObservation(owner, null, context)!
render(
<PRTriageStrip
review={accepted}
checks={[{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }]}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
)
expect(screen.getByText('Checks unavailable')).toBeTruthy()
expect(screen.getByRole('button', { name: 'Fix' }).hasAttribute('disabled')).toBe(false)
})
it('keeps a successful whole owner presentation when independent rows still contain an old failure', () => {
const accepted = acceptPRCheckObservation(
{ ...owner, checksSummary: { ...blocked, state: 'success', total: 1, passed: 1, failed: 0 } },
null,
context
)!
render(
<PRTriageStrip
review={accepted}
checks={[{ name: 'Build', status: 'completed', conclusion: 'failure', url: null }]}
isResolvingConflictsWithAI={false}
onResolveConflictsWithAI={() => {}}
isFixingChecksWithAI={false}
onFixChecksWithAI={() => {}}
/>
)
expect(screen.getByText('No blocking PR action')).toBeTruthy()
expect(screen.queryByText('0 failing checks')).toBeNull()
})
it.each([false, true])('keeps a failed parent pipeline fixable with empty jobs: %s', (empty) => {
const fix = vi.fn()
const props = {
@@ -7,7 +7,6 @@ import { GITHUB_COMPLETE_PR_CHECK_OBSERVATION_CAPABILITY as capability } from '.
import type { GitHubWorkItem } from '../../../shared/github/work-item-types'
import type { PRCheckDetail } from '../../../shared/github/check-types'
import { TooltipProvider } from '@/components/ui/tooltip'
import { acceptPRCheckObservation } from '../../../shared/github/complete-pr-check-owner'
import {
patchCachedPRChecks,
subscribeWorkItemDetailsCache,
@@ -155,53 +154,3 @@ it.each([PageChecks, DialogChecks])(
expect(screen.getByText('1 check failing')).toBeTruthy()
}
)
it.each([PageChecks, DialogChecks])(
'keeps approval explanation across its cache echo and retires it for a newer owner',
async (Tab) => {
lookup.mockResolvedValue({ ...initial, checksSummary: summary })
rows.mockResolvedValue([
{
name: 'Approval',
status: 'completed',
conclusion: 'action_required',
url: null,
observedHeadSha: 'a'.repeat(40)
}
])
const view = render(<Harness Tab={Tab} />)
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
})
expect(screen.getByText('1 check needs action')).toBeTruthy()
const newer = acceptPRCheckObservation({ ...initial, checksSummary: { ...summary } }, null, {
scope: 'workspace:local:local',
complete: true,
current: true
})!
view.rerender(<Harness Tab={Tab} parent={newer} />)
expect(screen.getByText('1 check failing')).toBeTruthy()
}
)
it.each([PageChecks, DialogChecks])(
'keeps a mismatched approval head conservative after its cache echo',
async (Tab) => {
lookup.mockResolvedValue({ ...initial, checksSummary: summary })
rows.mockResolvedValue([
{
name: 'Approval',
status: 'completed',
conclusion: 'action_required',
url: null,
observedHeadSha: 'b'.repeat(40)
}
])
render(<Harness Tab={Tab} />)
await act(async () => {
fireEvent.click(screen.getByRole('button', { name: 'Refresh' }))
})
expect(screen.getByText('1 check failing')).toBeTruthy()
expect(screen.getByText('Approval')).toBeTruthy()
}
)
@@ -301,61 +301,6 @@ it('refreshes the saved method after reconnecting to a restarted host with the s
expect(screen.getByRole('combobox')).toHaveTextContent('Gemini Enterprise')
})
it.each(['accounts', 'provider'] as const)(
'%s preserves another client’s active and settled sign-in status',
async (surface) => {
const target = { kind: 'environment', environmentId: `other-client-${surface}` } as const
streams.initial.set(key(target), {
revision: 0,
auth: 'unknown',
flow: { phase: 'waiting', flowId: null }
})
vi.mocked(callAntigravityChatAuthConfig).mockResolvedValue(personal)
renderSurface(surface, target)
await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled())
expect(screen.getByRole('button', { name: 'Sign in' })).toBeDisabled()
expect(screen.getByRole('status')).toHaveTextContent(
'Sign-in is in progress in another client.'
)
await act(async () => {
streams.handlers
.get(key(target))
?.onEvent({ revision: 1, auth: 'unknown', flow: { phase: 'expired', flowId: null } })
})
expect(screen.getByRole('status')).toHaveTextContent('Sign-in timed out. Try again.')
}
)
it.each(['accounts', 'provider'] as const)(
'%s preserves signed-in host context and explains a changed method',
async (surface) => {
const target = { kind: 'environment', environmentId: `signed-in-${surface}` } as const
streams.initial.set(key(target), { revision: 0, auth: 'signedIn', flow: null })
let current: AntigravityChatAuthConfig = personal
vi.mocked(callAntigravityChatAuthConfig).mockImplementation(async (_owner, config) => {
if (config) {
current = config
}
return current
})
const user = userEvent.setup()
renderSurface(surface, target)
await waitFor(() => expect(screen.getByRole('combobox')).toBeEnabled())
expect(screen.getByRole('status')).toHaveTextContent('Signed in on Test host.')
expect(screen.getByRole('button', { name: 'Change account' })).toBeEnabled()
await user.click(screen.getByRole('combobox'))
await user.click(screen.getByRole('option', { name: 'Gemini Enterprise' }))
expect(screen.getByRole('status')).toHaveTextContent('Sign in again to use Gemini Enterprise.')
await user.type(screen.getByLabelText('Google Cloud project'), enterprise.gcpProject)
await user.type(screen.getByLabelText('Google Cloud location'), enterprise.gcpLocation)
await user.tab()
await act(async () => {
streams.handlers.get(key(target))?.onEvent({ revision: 1, auth: 'unknown', flow: null })
})
expect(screen.getByRole('status')).toHaveTextContent('Sign in again to use Gemini Enterprise.')
}
)
it.each(['accounts', 'provider'] as const)(
'%s uses its single Sign in action to reread a failed load before preparing Enterprise',
async (surface) => {
@@ -1,62 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, renderHook } from '@testing-library/react'
import { afterEach, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { useNativeChatMentionFiles } from './native-chat/use-native-chat-mention-files'
import {
flushEffects,
listRuntimeFilesMock,
makeFolderWorkspace
} from './quick-open-file-list-test-harness'
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
vi.mock('@/runtime/files/search/runtime-file-search-client', async () => {
const mocks = await import('./__mocks__/quick-open-runtime-file-client')
return {
listRuntimeFiles: mocks.listRuntimeFilesMock,
cancelRuntimeFileList: mocks.cancelRuntimeFileListMock,
searchRuntimeFilePaths: mocks.searchRuntimeFilePathsMock
}
})
globalThis.IS_REACT_ACT_ENVIRONMENT = true
afterEach(cleanup)
it.each([
{ hostId: 'local', path: '/local/repo', connectionId: undefined },
{ hostId: 'ssh:box', path: '/remote/repo', connectionId: 'box' }
] as const)(
'lists chat mentions from the selected $hostId twin of a folder workspace on two hosts',
async ({ hostId, path, connectionId }) => {
const twin = { id: 'fw-twin' }
useAppStore.setState({
activeWorktreeId: folderWorkspaceKey('fw-twin'),
activeWorkspaceExecutionHostId: hostId,
repos: [],
projectGroups: [],
worktreesByRepo: {},
folderWorkspaces: [
makeFolderWorkspace({ ...twin, folderPath: '/local/repo', executionHostId: 'local' }),
makeFolderWorkspace({
...twin,
folderPath: '/remote/repo',
connectionId: 'box',
executionHostId: 'ssh:box'
})
]
})
renderHook(() =>
useNativeChatMentionFiles({
// A typed query debounces the remote search; empty lists the catalog at once.
query: '',
terminalTabId: 'chat',
structuredWorktreeId: folderWorkspaceKey('fw-twin')
})
)
await flushEffects()
expect(listRuntimeFilesMock).toHaveBeenCalledWith(
expect.objectContaining({ worktreePath: path, connectionId }),
expect.anything()
)
}
)
@@ -1,79 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { Worktree } from '../../../shared/worktree/types'
import type { RuntimeFileListState } from './quick-open-file-list'
import {
cancelRuntimeFileListMock,
flushEffects,
listRuntimeFilesMock,
makeRemoteWorktree,
renderProbe,
searchRuntimeFilePathsMock,
seedRemoteWorktree
} from './quick-open-file-list-test-harness'
vi.mock('@/runtime/files/search/runtime-file-search-client', async () => {
const mocks = await import('./__mocks__/quick-open-runtime-file-client')
return {
listRuntimeFiles: mocks.listRuntimeFilesMock,
cancelRuntimeFileList: mocks.cancelRuntimeFileListMock,
searchRuntimeFilePaths: mocks.searchRuntimeFilePathsMock
}
})
async function replaceWorktrees(worktrees: Worktree[]): Promise<void> {
await act(async () => {
useAppStore.setState({ worktreesByRepo: { 'repo-remote': worktrees } })
})
await flushEffects()
}
it('keeps a settled inventory through metadata updates', async () => {
seedRemoteWorktree()
const states: RuntimeFileListState[] = []
await renderProbe({ enabled: true, states, worktreeId: 'wt-remote' })
const settledFiles = states.at(-1)?.files
const initialRequest = listRuntimeFilesMock.mock.calls[0]?.[1]
const rendersBeforeUpdates = states.length
await replaceWorktrees([{ ...makeRemoteWorktree(), head: 'commit-1' }])
await replaceWorktrees([{ ...makeRemoteWorktree(), head: 'commit-2' }])
expect(listRuntimeFilesMock).toHaveBeenCalledOnce()
expect(cancelRuntimeFileListMock).not.toHaveBeenCalled()
expect(initialRequest?.signal?.aborted).toBe(false)
expect(states.slice(rendersBeforeUpdates).every((state) => !state.loading)).toBe(true)
expect(states.at(-1)?.files).toBe(settledFiles)
})
it('keeps an in-flight query through metadata updates but replaces it for changed exclusions', async () => {
vi.useFakeTimers()
seedRemoteWorktree()
searchRuntimeFilePathsMock.mockReturnValue(new Promise(() => {}))
try {
await renderProbe({ enabled: true, states: [], query: 'target', worktreeId: 'wt-remote' })
await act(async () => vi.advanceTimersByTimeAsync(120))
const initialRequest = searchRuntimeFilePathsMock.mock.calls[0]?.[1]
await replaceWorktrees([{ ...makeRemoteWorktree(), comment: 'Updated comment' }])
await act(async () => vi.advanceTimersByTimeAsync(120))
expect(initialRequest?.signal?.aborted).toBe(false)
expect(searchRuntimeFilePathsMock).toHaveBeenCalledOnce()
const nested = { ...makeRemoteWorktree(), id: 'nested', path: '/srv/remote/nested' }
await replaceWorktrees([makeRemoteWorktree(), nested])
expect(initialRequest?.signal?.aborted).toBe(true)
await act(async () => vi.advanceTimersByTimeAsync(120))
expect(searchRuntimeFilePathsMock).toHaveBeenCalledTimes(2)
expect(searchRuntimeFilePathsMock.mock.calls[1]?.[1].excludePaths).toEqual([nested.path])
await replaceWorktrees([makeRemoteWorktree()])
await act(async () => vi.advanceTimersByTimeAsync(120))
expect(searchRuntimeFilePathsMock).toHaveBeenCalledTimes(3)
expect(searchRuntimeFilePathsMock.mock.calls[2]?.[1].excludePaths).toBeUndefined()
} finally {
vi.useRealTimers()
}
})
@@ -86,35 +86,3 @@ it('cancels the producer on close and ignores its late result after reopening',
expect(await first).toBeUndefined()
expect(mocks.list).toHaveBeenCalledTimes(2)
})
it('cancels eligibility on owner/root changes while preserving ordinary search on errors', async () => {
const cache: QuickOpenRecentCache = { current: null }
mocks.list.mockRejectedValue(new Error('Update host'))
const result = await mergeQuickOpenRecentCandidates({
cache,
key: 'root-a',
context,
options: { rootPath: '/repo' },
candidatePaths: ['recent.ts'],
result: { files: ['recent.ts', 'ordinary.ts'], truncated: true },
cancelled: () => false
})
expect(result?.files).toEqual(['recent.ts', 'ordinary.ts'])
expect(result?.recentError).toBe('Recent files could not be checked: Update host')
})
it('uses fresh complete inventory membership without a second traversal', async () => {
const result = { files: ['ordinary.ts', 'recent.ts'], truncated: false }
expect(
await mergeQuickOpenRecentCandidates({
result,
completeInventory: true,
candidatePaths: ['recent.ts', 'deleted.ts'],
cache: { current: null },
key: 'full',
context,
options: { rootPath: '/repo' },
cancelled: () => false
})
).toBe(result)
expect(mocks.list).not.toHaveBeenCalled()
})
@@ -14,10 +14,7 @@ import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import { TooltipProvider } from '@/components/ui/tooltip'
import FolderWorkspaceSourceControlPanel from './FolderWorkspaceSourceControlPanel'
import { SourceControlPanel } from './source-control/panel/panel'
import { ConfirmationDialogContext } from '@/components/confirmation-dialog-context'
import { createRunningCommitMessageGenerationRecord } from '@/store/slices/commit-message-generation'
import type { PullRequestGenerationRecord } from '@/store/slices/hosted-review/generation/pull-request-generation-state'
const probes = vi.hoisted(() => ({
status: vi.fn(),
@@ -165,31 +162,6 @@ async function scrollTo(ids: readonly string[]): Promise<void> {
}
})
}
function writeUnrelatedGeneration(): void {
const context = {
worktreeId: 'other::/elsewhere',
worktreePath: '/elsewhere',
requestId: 1,
runtimeTarget: { kind: 'local' as const }
}
const pr: PullRequestGenerationRecord = {
context: { ...context, repoId: 'other', branch: 'other' },
seed: { base: 'main', title: '', body: '', draft: false },
seedFieldRevisions: { base: 0, title: 0, body: 0, draft: 0 },
requiresPushBeforeCreate: false,
status: 'running',
result: null,
error: null,
hydrated: false
}
useAppStore
.getState()
.setCommitMessageGenerationRecord(
context.worktreeId,
createRunningCommitMessageGenerationRecord(context)
)
useAppStore.getState().setPullRequestGenerationRecord(context.worktreeId, pr)
}
const BODY_OPERATIONS = ['compare', 'history', 'submodules', 'upstream', 'eligibility'] as const
let root: Root
@@ -318,224 +290,6 @@ async function render(node: ReactNode): Promise<void> {
})
}
function bodyWork(worktrees: readonly Worktree[]) {
const idByPath = new Map(worktrees.map((row) => [row.path, row.id]))
return Object.fromEntries(
BODY_OPERATIONS.map((operation) => {
const ids = probes[operation].mock.calls.map(
([key]: unknown[]) => idByPath.get(String(key)) ?? String(key)
)
return [operation, { calls: ids.length, worktrees: [...new Set(ids)].sort() }]
})
)
}
it('runs expanded body work only for on-screen sections, at mount and over the safety interval', async () => {
const counts: unknown[] = []
for (const count of [3, 30]) {
useAppStore.setState(useAppStore.getInitialState(), true)
const worktrees = setup(count)
const visible = [worktrees[0]!.id, worktrees[1]!.id]
probes.visible.clear()
for (const id of visible) {
probes.visible.add(id)
}
for (const probe of [probes.upstreamRef, ...BODY_OPERATIONS.map((name) => probes[name])]) {
probe.mockClear()
}
await render(<FolderWorkspaceSourceControlPanel />)
const atMount = bodyWork(worktrees)
expect(atMount.upstream!.calls).toBeGreaterThan(0)
expect(atMount.compare!.worktrees).toEqual(visible.toSorted())
expect(atMount.compare!.calls).toBeGreaterThanOrEqual(visible.length)
const intervals: ReturnType<typeof bodyWork>[] = []
for (let tick = 0; tick < 2; tick++) {
for (const operation of BODY_OPERATIONS) {
probes[operation].mockClear()
}
await act(async () => {
await vi.advanceTimersByTimeAsync(30_000)
})
intervals.push(bodyWork(worktrees))
}
for (const snapshot of [atMount, ...intervals]) {
for (const work of Object.values(snapshot)) {
expect(work.worktrees.every((id) => visible.includes(id))).toBe(true)
expect(work.calls).toBeLessThanOrEqual(visible.length)
}
}
// Only the selected worktree binds an upstream ref; sections never add resolutions.
expect(probes.upstreamRef).not.toHaveBeenCalled()
counts.push({ atMount, intervals })
await act(async () => root.render(null))
}
// Ten times the attached worktrees costs the same when the same sections are on screen.
expect(counts[1]).toEqual(counts[0])
})
it.each([3, 30])(
'freezes collapsed and off-screen bodies among %i sections on unrelated app writes',
async (count) => {
const worktrees = setup(count)
const [active, collapsed, offScreen] = worktrees
for (const row of worktrees) {
probes.visible.add(row.id)
}
await render(<FolderWorkspaceSourceControlPanel />)
const collapsedSection = container.querySelector(
`[data-source-control-worktree="${collapsed!.id}"]`
)!
await act(async () =>
collapsedSection.querySelector<HTMLButtonElement>('button[aria-expanded]')!.click()
)
await scrollTo(worktrees.filter((row) => row !== offScreen).map((row) => row.id))
const writes = [
() =>
useAppStore.setState((state) => ({
worktreesByRepo: {
repo: state.worktreesByRepo.repo!.map((row) =>
row.id === collapsed!.id ? { ...row, lastActivityAt: 99 } : row
)
}
})),
() =>
useAppStore.setState((state) => ({
settings: {
...state.settings!,
theme: state.settings?.theme === 'dark' ? 'light' : 'dark'
}
})),
() =>
useAppStore.setState((state) => ({
activeGroupIdByWorktree: {
...state.activeGroupIdByWorktree,
'folder:feature': 'other-group'
},
activeTabTypeByWorktree: {
...state.activeTabTypeByWorktree,
'folder:feature': 'terminal'
}
})),
() =>
useAppStore.setState((state) => ({
groupsByWorktree: {
...state.groupsByWorktree,
'folder:feature': [
{ id: 'other-group', worktreeId: 'folder:feature', tabOrder: [], activeTabId: null }
]
}
})),
() =>
useAppStore.setState((state) => ({
openFiles: [
...state.openFiles,
{
id: 'unrelated-file',
worktreeId: 'elsewhere',
filePath: '/elsewhere/other.ts',
relativePath: 'other.ts',
language: 'typescript',
mode: 'edit',
isDirty: false
}
]
})),
() =>
useAppStore.setState((state) => ({
gitStatusByWorktree: {
...state.gitStatusByWorktree,
elsewhere: [{ path: 'other.ts', status: 'modified', area: 'unstaged' }]
}
})),
writeUnrelatedGeneration
]
for (const [writeIndex, write] of writes.entries()) {
probes.bodyRenders.clear()
probes.eligibility.mockClear()
await act(async () => write())
for (const row of worktrees) {
expect(probes.bodyRenders.get(row.id) ?? 0, `write ${writeIndex}, owner ${row.id}`).toBe(0)
}
expect(probes.eligibility).not.toHaveBeenCalled()
}
probes.bodyRenders.clear()
await act(async () =>
useAppStore.setState((state) => ({
openFiles: [
...state.openFiles,
{
id: 'active-file',
worktreeId: active!.id,
filePath: `${active!.path}/a.ts`,
relativePath: 'a.ts',
language: 'typescript',
mode: 'edit',
isDirty: false
}
],
activeFileIdByWorktree: {
...state.activeFileIdByWorktree,
'folder:feature': 'active-file'
},
activeTabTypeByWorktree: { ...state.activeTabTypeByWorktree, 'folder:feature': 'editor' }
}))
)
expect(
container
.querySelector(
`[data-source-control-worktree="${active!.id}"] [data-source-control-path="a.ts"]`
)
?.getAttribute('data-current')
).toBe('true')
expect(probes.bodyRenders.get(active!.id)).toBeGreaterThan(0)
for (const row of worktrees.slice(1)) {
expect(probes.bodyRenders.get(row.id) ?? 0).toBe(0)
}
// The frozen views ignore their own status and owner changes, while a relevant live update is visible.
probes.bodyRenders.clear()
await act(async () =>
useAppStore.setState((state) => ({
gitStatusByWorktree: {
...state.gitStatusByWorktree,
[active!.id]: [
...state.gitStatusByWorktree[active!.id]!,
{ path: 'visible.ts', status: 'modified', area: 'unstaged' }
],
[offScreen!.id]: [
...state.gitStatusByWorktree[offScreen!.id]!,
{ path: 'hidden.ts', status: 'modified', area: 'unstaged' }
]
}
}))
)
expect(probes.bodyRenders.get(active!.id)).toBeGreaterThan(0)
expect(probes.bodyRenders.get(collapsed!.id) ?? 0).toBe(0)
expect(probes.bodyRenders.get(offScreen!.id) ?? 0).toBe(0)
expect(
container.querySelector(`[data-source-control-worktree="${active!.id}"]`)!.textContent
).toContain('visible.ts')
await scrollTo(worktrees.map((row) => row.id))
expect(
container.querySelector(`[data-source-control-worktree="${offScreen!.id}"]`)!.textContent
).toContain('hidden.ts')
}
)
it('keeps the selected workspace panel idle when another worktree writes generation records', async () => {
const [worktree] = setup(1)
useAppStore.setState({
activeWorktreeId: worktree!.id,
activeWorkspaceKey: `worktree:${worktree!.id}`
})
await render(<SourceControlPanel />)
expect(probes.eligibility).toHaveBeenCalled()
probes.eligibility.mockClear()
renders = 0
await act(async () => writeUnrelatedGeneration())
expect(probes.eligibility).not.toHaveBeenCalled()
expect(renders).toBe(0)
})
it.each(['off-screen', 'collapsed'])(
'keeps selection and draft while %s and resumes the current owner snapshot',
async (inactive) => {
@@ -158,79 +158,6 @@ async function render(visible = true) {
)
}
it.each([3, 30])(
'bounds observation, watches and unrelated renders for %i attached worktrees',
async (count) => {
const worktrees = setup(count)
await render()
// One status per visible repository is necessary for its badge; collapsed bodies do no compare work.
expect(probes.status).toHaveBeenCalledTimes(count)
expect(probes.body).not.toHaveBeenCalled()
// Every mounted section watches its tree, collapsed ones included (R4).
expect(probes.watches).toEqual(worktrees.map((row) => row.id))
probes.header.mockClear()
await act(async () => useAppStore.setState({ activeFileId: 'unrelated-file' }))
expect(probes.header).not.toHaveBeenCalled()
await act(async () =>
useAppStore.setState({
sshConnectionStates: new Map([
[
'other-host',
{ targetId: 'other-host', status: 'connected', error: null, reconnectAttempt: 0 }
]
])
})
)
expect(probes.header).not.toHaveBeenCalled()
await act(async () =>
useAppStore.setState((state) => ({
gitStatusByWorktree: {
...state.gitStatusByWorktree,
[worktrees[0]!.id]: [{ path: 'a.ts', status: 'modified', area: 'unstaged' }]
}
}))
)
expect(probes.header.mock.calls).toEqual([['feature-0']])
expect(probes.body).not.toHaveBeenCalled()
// Agent changes wake the matching collapsed badge, using the existing scheduler's coalescing.
probes.status.mockClear()
await act(async () => {
window.dispatchEvent(
new CustomEvent(ORCA_WORKTREE_FILE_CHANGE_EVENT, {
detail: {
runtimeEnvironmentId: null,
payload: {
worktreePath: worktrees[0]!.path,
events: [{ absolutePath: `${worktrees[0]!.path}/a.ts`, kind: 'change' }]
}
}
})
)
await vi.advanceTimersByTimeAsync(4000)
})
expect(probes.status).toHaveBeenCalledTimes(1)
expect(probes.status.mock.calls[0]?.[0]?.worktreeId).toBe(worktrees[0]!.id)
const toggles = container.querySelectorAll<HTMLButtonElement>('button[aria-expanded]')
for (const toggle of toggles) {
await act(async () => toggle.click())
}
expect(probes.body).toHaveBeenCalledTimes(count)
expect(probes.watches).toEqual(worktrees.map((row) => row.id))
await render(false)
probes.status.mockClear()
await act(async () => {
await vi.advanceTimersByTimeAsync(120000)
})
expect(probes.status).not.toHaveBeenCalled()
expect(probes.watches).toEqual([])
}
)
function ActiveStatusObserver() {
useWorktreeGitStatusObservation(null)
return null
}
function ScopedStatusObserver({ owner }: { owner: SourceControlScope }) {
useWorktreeGitStatusObservation(owner, { enabled: true })
return null
@@ -272,48 +199,6 @@ it('filters file events by the section runtime host when a same-id local twin ex
}
})
it('refreshes an open diff after a file event with the sidebar closed', async () => {
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Mac')
const [worktree] = setup(1)
const path = '/Users/test/Library/Containers/app/worktree'
useAppStore.setState({
activeWorktreeId: worktree!.id,
activeWorkspaceKey: `worktree:${worktree!.id}`,
worktreesByRepo: { repo: [{ ...worktree!, path }] },
rightSidebarOpen: false,
openFiles: [
{
id: 'open-diff',
filePath: `${path}/a.ts`,
relativePath: 'a.ts',
worktreeId: worktree!.id,
language: 'typescript',
isDirty: false,
mode: 'diff'
}
]
})
await act(async () => root.render(<ActiveStatusObserver />))
expect(probes.status).toHaveBeenCalledTimes(1)
probes.status.mockClear()
await act(async () => {
window.dispatchEvent(
new CustomEvent(ORCA_WORKTREE_FILE_CHANGE_EVENT, {
detail: {
runtimeEnvironmentId: null,
payload: {
worktreePath: path,
events: [{ absolutePath: `${path}/a.ts`, kind: 'change' }]
}
}
})
)
await vi.advanceTimersByTimeAsync(4000)
})
expect(probes.status).toHaveBeenCalledTimes(1)
expect(probes.status.mock.calls[0]?.[0]?.worktreeId).toBe(worktree!.id)
})
it('includes a paired-runtime descendant of a local attached worktree as a section', async () => {
const [parent] = setup(1)
const remote = {
@@ -274,68 +274,3 @@ it.each(['explicit', 'repository'] as const)(
expect(stream!.unsubscribe).toHaveBeenCalled()
}
)
it('keeps ordinary warm watches, shares roots and releases only section-owned ones', async () => {
const children = Array.from({ length: 30 }, (_, i) => worktreeRow('repo', `/child-${i}`, i))
const others = [1, 2].map((i) => worktreeRow('other', `/other-${i}`, i))
const openFile = (worktree: Worktree) => ({
id: `${worktree.id}::file`,
filePath: `${worktree.path}/file.ts`,
relativePath: 'file.ts',
worktreeId: worktree.id,
language: 'typescript',
isDirty: false,
mode: 'edit' as const
})
setup({
children,
others,
openFiles: [...others, children[0]!].map(openFile),
// The three recently used workspaces: two ordinary ones and one that is also a section.
lastVisitedAtByWorktreeId: Object.fromEntries(
[...others, children[0]!].map((row, i) => [row.id, 100 + i])
)
})
await render()
const watched = watchedPaths(watchWorktree.mock.calls)
// Thirty sections never take the recently used workspaces' slots.
for (const row of others) {
expect(watched).toContain(row.path)
}
// The section and an open file in the same worktree share one root.
expect(watched.filter((path) => path === children[0]!.path)).toHaveLength(1)
expect(unwatchWorktree).not.toHaveBeenCalled()
await render(false)
const released = watchedPaths(unwatchWorktree.mock.calls).sort()
expect(released).toEqual(
children
.slice(1)
.map((row) => row.path)
.sort()
)
})
it.each(['direct', 'nested'] as const)(
'does not claim a watch for unserved %s SSH',
async (route) => {
const child = worktreeRow('rrepo', '/remote/child', 0, { hostId: 'ssh:nested-ssh' })
setup({ children: [child] })
useAppStore.setState({
repos: useAppStore.getState().repos.map((repo) =>
repo.id === 'rrepo'
? {
...repo,
connectionId: 'nested-ssh',
executionHostId: route === 'nested' ? 'runtime:env-1' : 'ssh:nested-ssh'
}
: repo
)
})
await render()
expect(watchWorktree).not.toHaveBeenCalled()
expect(runtimeStreams).toEqual([])
// Scheduling is preserved; unsupported SSH status still reaches the original remote rejection boundary.
expect(probes.status.mock.calls.map(([request]) => request.worktreeId)).toEqual([child.id])
}
)
@@ -244,42 +244,4 @@ describe('Checks comment edit/delete ownership', () => {
)
expect(comments().map((c) => c.id)).toEqual([9001, 9002])
})
it('reports a GitHub failure from the owning server without changing the list', async () => {
mocks.callRuntimeRpc.mockResolvedValue({
ok: false,
error: { type: 'permission_denied', message: 'Resource not accessible by integration' }
})
const { hook, comments } = render({
repo: repoRow({ executionHostId: 'runtime:owner-server' })
})
await act(async () => {
await expect(hook.result.current.handleEditComment(COMMENT, 'new body')).resolves.toBe(false)
})
expect(mocks.toastError).toHaveBeenCalledWith('Resource not accessible by integration')
expect(comments().map((c) => c.body)).toEqual(['old body', 'other'])
})
it('drops a result that lands after the panel moved to another review', async () => {
let finish: (value: { ok: false; error: { message: string } }) => void = () => {}
mocks.callRuntimeRpc.mockReturnValue(new Promise((resolve) => (finish = resolve)))
const { hook, comments, moveAway } = render({
repo: repoRow({ executionHostId: 'runtime:owner-server' })
})
let pending: Promise<boolean> = Promise.resolve(true)
act(() => {
pending = hook.result.current.handleEditComment(COMMENT, 'new body')
})
moveAway()
await act(async () => {
finish({ ok: false, error: { message: 'stale failure' } })
await pending
})
expect(mocks.toastError).not.toHaveBeenCalled()
expect(comments().map((c) => c.body)).toEqual(['old body', 'other'])
})
})
@@ -90,14 +90,6 @@ afterEach(() => {
})
describe('usePluginDeclarativeState', () => {
it('stores a write made before a workspace switch under the workspace it was made in', async () => {
const panel = await mountReadyPanel()
await panel.switchTo('wt-b')
// The guest flushes the edit it made in A as the switch's hydrate reaches it.
panel.persist({ hydration: panel.first, value: 'typed in A' })
expect(panel.writes()).toEqual([expect.objectContaining({ workspace: 'wt-a' })])
})
it('keeps each write with its own workspace across a quick A, B, A switch', async () => {
const panel = await mountReadyPanel()
const inB = await panel.switchTo('wt-b')
@@ -131,13 +123,4 @@ describe('usePluginDeclarativeState', () => {
panel.persist({ hydration: latest + 1, value: 'never sent' })
expect(panel.stored()).toEqual([['wt-b', 'eight back']])
})
it('still stores plugin-scope entries tagged with an earlier hydration', async () => {
const panel = await mountReadyPanel()
await panel.switchTo('wt-b')
panel.persist({ hydration: panel.first, scope: 'plugin', value: 'global' })
expect(panel.writes()).toEqual([
expect.objectContaining({ entries: [expect.objectContaining({ scope: 'plugin' })] })
])
})
})
@@ -53,17 +53,6 @@ describe('showSourceControlEntryFailureToast', () => {
storeState.activeWorktreeId = 'wt-1'
})
it('still reports a failure belonging to a worktree the user has left, naming it', () => {
// Suppress stale recovery actions after switches, but still report the original workspace's failure.
storeState.activeWorktreeId = 'wt-2'
show({ worktreeId: 'wt-1', worktreeName: 'feature-a', onRetry: vi.fn() })
expect(toastError).toHaveBeenCalledTimes(1)
expect(lastToast().title).toBe('Failed to stage “src/app.ts” in feature-a')
expect(lastToast().options.action).toBeUndefined()
expect(lastToast().options.duration).toBeUndefined()
})
it('retires a Retry action that became stale after a worktree switch', () => {
const onRetry = vi.fn()
show({ onRetry })
@@ -1,85 +0,0 @@
// @vitest-environment happy-dom
import { act, useState, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it } from 'vitest'
import { useAppStore } from '@/store'
import { readStoreListenerCount } from '@/store/store-listener-census'
import { useSourceControlStoreActions, type SourceControlStoreActions } from './use-store-actions'
const originalState = useAppStore.getState()
let root: Root | null = null
let container: HTMLDivElement | null = null
function mount(node: ReactNode): void {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
act(() => root?.render(node))
}
function unmount(): void {
if (root) {
act(() => root?.unmount())
}
root = null
container?.remove()
container = null
}
function listenerCount(): number {
const count = readStoreListenerCount()
if (count === null) {
throw new Error('store listener census unavailable')
}
return count
}
afterEach(() => {
unmount()
useAppStore.setState(originalState, true)
})
describe('useSourceControlStoreActions store subscriptions', () => {
it('subscribes to no store slices', () => {
const baseline = listenerCount()
function Probe(): null {
useSourceControlStoreActions()
return null
}
mount(<Probe />)
// Fixed-identity actions come from getState(); generation records are selected per owner by their consumers.
expect(listenerCount() - baseline).toBe(0)
unmount()
expect(listenerCount()).toBe(baseline)
})
it('returns the same object across an unrelated store write and re-render', () => {
let latest: SourceControlStoreActions | null = null
let rerender: (() => void) | null = null
function Probe(): null {
const [, setTick] = useState(0)
rerender = () => setTick((t) => t + 1)
latest = useSourceControlStoreActions()
return null
}
mount(<Probe />)
const first = latest
expect(first).not.toBeNull()
act(() => {
useAppStore.setState({
rightSidebarOpen: !originalState.rightSidebarOpen
})
})
act(() => rerender?.())
expect(latest).toBe(first)
})
})
@@ -1,73 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, renderHook } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterEach, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
import { SourceControlScopeProvider } from '../panel/worktree-scope'
import { useSourceControlNoteOpening } from './use-note-opening'
const initialAppState = useAppStore.getInitialState()
afterEach(() => {
cleanup()
useAppStore.setState(initialAppState, true)
})
it('opens a folder section note in the folder group while the child keeps the content', () => {
const openFile = vi.fn<AppState['openFile']>(() => 'file')
useAppStore.setState({
openFile,
activeWorktreeId: 'folder:feature',
activeGroupIdByWorktree: { 'folder:feature': 'folder-group' }
})
const worktree = makeWorktree('wt-child', 'wt-child', { instanceId: 'instance-1' })
const owner = {
worktreeId: 'wt-child',
repoId: 'repo-1',
executionHostId: 'ssh:box' as const,
worktree,
attachedAt: 1,
repo: { ...makeRepo(), connectionId: 'box' },
placementWorkspaceId: 'folder:feature'
}
const { result } = renderHook(
() =>
useSourceControlNoteOpening({
activeWorktreeId: 'wt-child',
worktreePath: '/tmp/wt-child',
entries: [],
branchEntries: [],
branchSummary: null,
handleOpenDiff: vi.fn(),
openCommittedDiff: vi.fn(),
sourceControlRef: { current: null }
}),
{
wrapper: ({ children }: { children: ReactNode }) => (
<SourceControlScopeProvider owner={owner}>{children}</SourceControlScopeProvider>
)
}
)
result.current.handleOpenComment({
id: 'note-1',
worktreeId: 'wt-child',
filePath: 'README.md',
source: 'markdown',
lineNumber: 3,
body: 'check',
createdAt: 1,
side: 'modified'
})
expect(openFile).toHaveBeenCalledWith(
expect.objectContaining({ worktreeId: 'wt-child', filePath: '/tmp/wt-child/README.md' }),
{
placementWorkspaceId: 'folder:feature',
executionHostId: 'ssh:box',
targetGroupId: 'folder-group'
}
)
})
@@ -5,9 +5,6 @@ import { act, cleanup, renderHook } from '@testing-library/react'
import type { ReactNode } from 'react'
import { afterEach, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import type { AppState } from '@/store/types'
import { useSourceControlHostedReviewCreated } from '../review/use-hosted-review-created'
import { useSourceControlStoreActions } from '../listing/use-store-actions'
import { makeRepo, makeWorktree } from '../../../worktree-jump/worktree-jump-palette-test-fixtures'
import type { ExecutionHostId } from '../../../../../../shared/execution-host'
import { useSourceControlCommitAction } from '../commit/use-commit-action'
@@ -207,95 +204,3 @@ it('retires only the removed host owner on authoritative purge and fences late d
.purgeWorktreeTerminalState([hostWorktreeTarget({ id: 'wt-removed', hostId: 'local' })])
expect(getWorktreeOperationStore('wt-removed', 'local', 'instance-1').getState().draft).toBe('')
})
it('writes the base ref and opens aggregate diffs on the section host while the folder stays active', async () => {
const updateRepo = vi.fn<AppState['updateRepo']>()
const openAllDiffs = vi.fn<AppState['openAllDiffs']>()
useAppStore.setState({
activeWorktreeId: 'folder:feature',
activeWorkspaceExecutionHostId: 'local',
updateRepo,
openAllDiffs
})
const section = renderHook(() => useSourceControlStoreActions(), {
wrapper: sectionWrapper('wt-twin', 'ssh:box')
})
await section.result.current.updateRepo('repo-1', { worktreeBaseRef: 'release' })
expect(updateRepo).toHaveBeenCalledWith(
'repo-1',
{ worktreeBaseRef: 'release' },
{ hostId: 'ssh:box' }
)
section.result.current.openAllDiffs('wt-twin', '/remote/repo')
expect(openAllDiffs).toHaveBeenCalledWith(
'wt-twin',
'/remote/repo',
undefined,
undefined,
undefined,
{
placementWorkspaceId: 'folder:feature',
runtimeEnvironmentId: null,
executionHostId: 'ssh:box'
}
)
expect(useAppStore.getState().activeWorktreeId).toBe('folder:feature')
})
it('opens a section file here and its checks in the folder overview without changing workspace', () => {
const openFile = vi.fn<AppState['openFile']>(() => 'file')
const revealInExplorer = vi.fn<AppState['revealInExplorer']>()
const setRightSidebarOpen = vi.fn<AppState['setRightSidebarOpen']>()
const setRightSidebarTab = vi.fn<AppState['setRightSidebarTab']>()
useAppStore.setState({
activeWorktreeId: 'folder:feature',
activeGroupIdByWorktree: { 'folder:feature': 'folder-group' },
openFile,
revealInExplorer,
setRightSidebarOpen,
setRightSidebarTab
})
const section = renderHook(
() => {
const actions = useSourceControlStoreActions()
const checks = useSourceControlHostedReviewCreated({
activeRepo: null,
activeWorktreeId: 'wt-child',
branchName: 'feature',
fallbackGitHubPRNumber: null,
linkedAzureDevOpsPR: null,
linkedBitbucketPR: null,
linkedGitHubPR: 123,
linkedGitLabMR: null,
linkedGiteaPR: null,
fetchHostedReviewForBranch: actions.fetchHostedReviewForBranch,
fetchPRForBranch: actions.fetchPRForBranch,
refreshActiveGitStatusAfterMutation: async () => {},
setRightSidebarOpen: actions.setRightSidebarOpen,
setRightSidebarTab: actions.setRightSidebarTab,
updateWorktreeMeta: actions.updateWorktreeMeta
})
return { actions, checks }
},
{ wrapper: sectionWrapper('wt-child', 'ssh:box') }
)
section.result.current.actions.revealInExplorer('wt-child', '/remote/repo/a.ts')
expect(openFile).toHaveBeenCalledWith(
expect.objectContaining({
filePath: '/remote/repo/a.ts',
worktreeId: 'wt-child',
mode: 'edit'
}),
{
placementWorkspaceId: 'folder:feature',
executionHostId: 'ssh:box',
runtimeEnvironmentId: null,
targetGroupId: 'folder-group',
preview: false
}
)
expect(revealInExplorer).not.toHaveBeenCalled()
section.result.current.checks.openHostedReviewInChecks()
expect(setRightSidebarTab).toHaveBeenCalledWith('pr-checks')
expect(useAppStore.getState().activeWorktreeId).toBe('folder:feature')
})
@@ -1,98 +0,0 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
noteUserWorkspaceSwitch,
resetWorkspaceSwitchUserActsForTests
} from '@/lib/workspace-switch-user-act'
const getRuntimeGitHistoryMock = vi.hoisted(() => vi.fn())
vi.mock('@/runtime/source-control/git/runtime-git-status-client', () => ({
getRuntimeGitHistory: getRuntimeGitHistoryMock
}))
vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => null }))
import { useSourceControlGitHistory } from './use-git-history'
type Props = Parameters<typeof useSourceControlGitHistory>[0]
const props = (overrides: Partial<Props> = {}): Props => ({
activeRepoTarget: { kind: 'local' },
activeWorktreeId: 'wt-1',
worktreePath: '\\\\wsl.localhost\\Ubuntu\\repo',
branchName: 'main',
compareBaseRef: 'main',
isFolder: false,
isBranchVisible: true,
isGitHistoryExpanded: true,
isGitHistoryVisible: true,
worktreeMap: new Map([
['wt-1', {}],
['wt-2', {}]
]),
...overrides
})
const wakes = (): unknown[] =>
getRuntimeGitHistoryMock.mock.calls.map((call) => call[1].wakeWslDistro)
beforeEach(() => {
getRuntimeGitHistoryMock.mockReset().mockResolvedValue({ items: [] })
resetWorkspaceSwitchUserActsForTests()
})
afterEach(cleanup)
// E9: only the user's own act may start a stopped WSL distro for a Commits read.
describe('Commits history and a stopped WSL distro', () => {
it('stays background on a mount or remount with Commits already open', async () => {
const first = renderHook(() => useSourceControlGitHistory(props()))
await act(async () => {})
first.unmount()
renderHook(() => useSourceControlGitHistory(props()))
await act(async () => {})
expect(wakes()).toEqual([undefined, undefined])
})
it('wakes once for the user switching to the workspace, never again on a remount', async () => {
noteUserWorkspaceSwitch('wt-1')
const first = renderHook(() => useSourceControlGitHistory(props()))
await act(async () => {})
first.unmount()
renderHook(() => useSourceControlGitHistory(props()))
await act(async () => {})
expect(wakes()).toEqual([true, undefined])
})
it('wakes when the user expands Commits, not when Orca re-shows it or a refresh follows', async () => {
const { result, rerender } = renderHook((p: Props) => useSourceControlGitHistory(p), {
initialProps: props({ isGitHistoryExpanded: false })
})
await act(async () => {})
rerender(props())
await act(async () => {})
// Hidden and shown again by Orca (a sidebar re-show), with no switch.
rerender(props({ isGitHistoryVisible: false }))
rerender(props())
await act(async () => {})
await act(async () => result.current.refreshGitHistoryRef.current())
expect(wakes()).toEqual([true, undefined, undefined])
})
it("does not take another worktree's restored expanded view as a click", async () => {
const { rerender } = renderHook((p: Props) => useSourceControlGitHistory(p), {
initialProps: props({ isGitHistoryExpanded: false })
})
await act(async () => {})
rerender(props({ activeWorktreeId: 'wt-2' }))
await act(async () => {})
expect(wakes()).toEqual([undefined])
})
it("keeps the Refresh button's explicit wake", async () => {
const { result } = renderHook(() => useSourceControlGitHistory(props()))
await act(async () => {})
await act(async () => result.current.refreshGitHistory({ wakeWslDistro: true }))
expect(wakes()).toEqual([undefined, true])
})
})
@@ -2,7 +2,6 @@
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import CommentMarkdown from './CommentMarkdown'
import type { DocumentCodeBlockRenderer } from './comment-markdown-element-renderers'
const mocks = vi.hoisted(() => ({
mermaid: vi.fn(() => {
@@ -18,88 +17,7 @@ afterEach(() => {
mocks.mermaid.mockClear()
})
/** Ignore inter-block line breaks because they have no rendered layout. */
function blocksOf(container: HTMLElement): string[] {
return Array.from(container.firstElementChild?.children ?? [], (block) => block.outerHTML)
}
const REPLY = [
'First paragraph with **bold** and a [link](https://example.com).',
'',
'```ts',
'const a = 1',
'```',
'',
'- one',
'- two',
'',
'| a | b |',
'| - | - |',
'| 1 | 2 |',
'',
'Last paragraph.'
].join('\n')
describe('growing comment markdown', () => {
it('renders a streamed reply to the same document as the finished one', () => {
const whole = render(<CommentMarkdown content={REPLY} variant="document" />)
const expected = blocksOf(whole.container)
whole.unmount()
const streamed = render(<CommentMarkdown content="" variant="document" growing />)
for (let end = 5; end < REPLY.length + 5; end += 5) {
streamed.rerender(
<CommentMarkdown content={REPLY.slice(0, end)} variant="document" growing />
)
}
streamed.rerender(<CommentMarkdown content={REPLY} variant="document" />)
expect(blocksOf(streamed.container)).toEqual(expected)
})
it('does not render a finished block again as later text arrives', () => {
const renderCodeBlock = vi.fn<DocumentCodeBlockRenderer>(({ children }) => (
<pre>{children}</pre>
))
const fence = '```ts\nconst a = 1\n```\n\n'
const view = render(
<CommentMarkdown
content={`${fence}Next`}
variant="document"
renderCodeBlock={renderCodeBlock}
growing
/>
)
const afterFence = renderCodeBlock.mock.calls.length
expect(afterFence).toBeGreaterThan(0)
let content = `${fence}Next`
for (const more of [' paragraph', ' keeps', ' growing.\n\nAnd another.']) {
content += more
view.rerender(
<CommentMarkdown
content={content}
variant="document"
renderCodeBlock={renderCodeBlock}
growing
/>
)
}
expect(renderCodeBlock.mock.calls.length).toBe(afterFence)
})
it('does not remount earlier text when a kept tag closes', () => {
const props = { variant: 'document', unknownHtmlAsText: true, growing: true } as const
const view = render(<CommentMarkdown content={'Intro.\n\nPress <kbd>Ctrl'} {...props} />)
const press = view.container.querySelectorAll('p')[1]
for (const content of [
'Intro.\n\nPress <kbd>Ctrl</kbd> to copy.',
'Intro.\n\nPress <kbd>Ctrl</kbd> to copy.\n\nTail.'
]) {
view.rerender(<CommentMarkdown content={content} {...props} />)
expect(view.container.querySelectorAll('p')[1]).toBe(press)
}
})
it('links a path in an earlier block once the host confirms it', () => {
const content = 'Edited src/a.ts.\n\nThen more text'
const view = render(
@@ -1,55 +0,0 @@
// @vitest-environment happy-dom
import { act, cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { resetSystemPrefersDarkSubscriptionForTests } from '@/hooks/theme/use-system-prefers-dark'
const mermaidProps: { current: { isDark: boolean } | null } = vi.hoisted(() => ({ current: null }))
vi.mock('@/components/editor/MermaidBlock', () => ({
default: (props: { isDark: boolean }) => {
mermaidProps.current = props
return null
}
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({ settings: { theme: 'system' } })
}))
import CommentMermaidBlock from './CommentMermaidBlock'
const originalMatchMedia = window.matchMedia
afterEach(() => {
cleanup()
mermaidProps.current = null
resetSystemPrefersDarkSubscriptionForTests()
window.matchMedia = originalMatchMedia
})
describe('CommentMermaidBlock system theme', () => {
it('follows a system color-scheme change while mounted', () => {
let matches = false
const listeners = new Set<EventListener>()
const media = {
get matches() {
return matches
},
addEventListener: (_type: string, listener: EventListener) => listeners.add(listener),
removeEventListener: (_type: string, listener: EventListener) => listeners.delete(listener)
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook only reads `matches` and (un)subscribes to `change`.
window.matchMedia = (() => media) as unknown as typeof window.matchMedia
render(<CommentMermaidBlock content="graph TD; A-->B" />)
expect(mermaidProps.current?.isDark).toBe(false)
act(() => {
matches = true
for (const listener of listeners) {
listener(new MediaQueryListEvent('change', { matches: true }))
}
})
expect(mermaidProps.current?.isDark).toBe(true)
})
})
@@ -1,127 +0,0 @@
import { describe, expect, it } from 'vitest'
import { htmlRendersAsText } from './comment-markdown-unknown-html-as-text'
import { splitMarkdownTopLevelBlocks, type MarkdownBlockSplit } from './markdown-top-level-blocks'
const REPLY = [
'Here is the plan.',
'',
'## Steps',
'',
'- first item',
'- second item',
'',
' continued inside the list',
'',
'1. one',
'2. two',
'',
'A loose list follows.',
'',
'1. first',
'',
'2. second',
'',
' with a paragraph of its own',
'',
'3. third',
'',
'> a quote',
'that continues lazily',
'',
'```ts',
'const a = 1',
'',
'## not a heading',
'```',
'',
'Intro to the table',
'| a | b |',
'| - | - |',
'| 1 | 2 |',
'',
'Setext title',
'===',
'',
'Closing words with `code` and **bold**.',
''
].join('\n')
function streamed(source: string, step: number): string[] {
let split: MarkdownBlockSplit | null = null
for (let end = step; end < source.length + step; end += step) {
split = splitMarkdownTopLevelBlocks(source.slice(0, end), split)
}
return (split?.blocks ?? []).map((block) => block.text)
}
function cut(source: string, previous: MarkdownBlockSplit | null = null): string[] {
return splitMarkdownTopLevelBlocks(source, previous).blocks.map((block) => block.text)
}
describe('splitMarkdownTopLevelBlocks', () => {
it.each([1, 3, 7])('reaches the same cut when streamed %i characters at a time', (step) => {
expect(streamed(REPLY, step)).toEqual(cut(REPLY))
})
it.each([
['a link definition', 'See [the docs][docs].\n\nMore.\n\n[docs]: https://example.com\n'],
['a footnote', 'A claim.[^1]\n\nMore.\n\n[^1]: The source.\n'],
['a nested definition', 'See [the docs][docs].\n\n> [docs]: https://example.com\n'],
['raw HTML', 'Before.\n\n<details>\n\nhidden **text**\n\n</details>\n'],
['raw HTML inside a quote', 'Before.\n\n> <table><tr><td>cell\n\nAfter.\n'],
['an inline tag left open', 'Before <sub>low.\n\nAfter.\n']
])('leaves a document with %s whole, however it arrived', (_name, source) => {
expect(cut(source)).toEqual([source])
expect(streamed(source, 2)).toEqual([source])
})
describe('when unknown HTML renders as text', () => {
const rendersAsText = (html: string): boolean =>
htmlRendersAsText(html, new Set(['b', 'sub', 'details']))
function streamedAsText(source: string): string[] {
let split: MarkdownBlockSplit | null = null
for (let end = 2; end < source.length + 2; end += 2) {
split = splitMarkdownTopLevelBlocks(
source.slice(0, end),
split,
true,
undefined,
undefined,
rendersAsText
)
}
return (split?.blocks ?? []).map((block) => block.text)
}
it.each([
['a type parameter', 'A Vec<T> of Promise<void>.\n\nAfter.\n', 2],
['an unknown tag block', '<example>\n\nInside.\n\n</example>\n', 3]
])('keeps cutting a document with %s', (_name, source, blocks) => {
expect(streamedAsText(source)).toHaveLength(blocks)
expect(streamedAsText(source).join('')).toBe(source)
})
it.each([
['a kept tag', 'Before <sub>low</sub>.\n\nAfter.\n'],
['a kept tag beside an unknown one', 'A <example><b>bold</b>.\n\nAfter.\n'],
[
'bold the HTML parser carries out of a table',
'<b><table><tr><td>one</b></td></tr></table>\n\nAfter.\n'
],
['a comment', '<!-- a --!><b>oops -->\n\nAfter.\n'],
['a malformed kept tag', '<details title=a<>\n\nAfter **x**\n']
])('leaves a document with %s whole', (_name, source) => {
expect(streamedAsText(source)).toEqual([source])
})
it('leaves raw unknown HTML whole when it does not render as text', () => {
expect(cut('<select>\n\nAfter **bold**.\n')).toHaveLength(1)
expect(cut('A Vec<T>.\n\nAfter.\n')).toHaveLength(1)
})
})
it('starts over when the source is replaced rather than appended to', () => {
const first = splitMarkdownTopLevelBlocks('one\n\ntwo\n', null)
expect(cut('other\n\ntext\n', first)).toEqual(['other\n\n', 'text\n'])
})
})
@@ -1,7 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AppState } from '@/store/types'
import type { ProjectMovePauseResponse } from '../../../../shared/project-move-pause'
import { toRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-runtime-pty-id'
const mocks = vi.hoisted(() => {
const tabsByWorktree: Record<string, { id: string }[]> = {}
@@ -104,32 +103,6 @@ describe('project move renderer completion', () => {
expect(f.responses.map((response) => response.phase)).toEqual(['prepare', 'persist'])
})
it('resolves every twin chat host and the terminal owner instead of assuming local', async () => {
const f = fixture()
const id = 'repo::/wt'
mocks.state.worktreesByRepo = {
repo: [
{ id, repoId: 'repo', hostId: 'local' },
{ id, repoId: 'repo', hostId: 'runtime:env' }
]
}
mocks.state.tabsByWorktree = { [id]: [{ id: 'tab' }] }
mocks.state.ptyIdsByTabId = { tab: [toRemoteRuntimePtyId('pty', 'env')] }
await f.prepare([id])
expect(f.responses[0]).toEqual({
requestId: 'move-1',
protocol: 1,
phase: 'prepare',
targets: [
{
worktreeId: id,
terminalHost: { kind: 'environment', environmentId: 'env' },
chatHosts: [{ kind: 'self' }, { kind: 'environment', environmentId: 'env' }]
}
]
})
})
it('refuses ambiguous twins before starting a sleep', async () => {
const f = fixture()
const id = 'repo::/wt'
@@ -1,102 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PRCheckDetail } from '../../github/check-types'
import { getCheckSeverityRank, sortChecksBySeverity } from './pr-check-severity-order'
import { mapGitLabPipelineJobStatusToConclusion } from './gitlab/gitlab-pipeline-checks'
const check = (name: string, conclusion: string | null) =>
({ name, conclusion }) as Pick<PRCheckDetail, 'name' | 'conclusion'>
describe('PR check severity order', () => {
it('orders failures, then in-flight work, then successes', () => {
const sorted = sortChecksBySeverity([
check('success', 'success'),
check('skipped', 'skipped'),
check('pending', null),
check('cancelled', 'cancelled'),
check('failure', 'failure')
])
expect(sorted.map((c) => c.name)).toEqual([
'failure',
'cancelled',
'pending',
'success',
'skipped'
])
})
it('covers every known conclusion and preserves input order within equal ranks', () => {
const sorted = sortChecksBySeverity([
check('success-a', 'success'),
check('unknown', 'future_state'),
check('skipped', 'skipped'),
check('action-required', 'action_required'),
check('pending', 'pending'),
check('timed-out', 'timed_out'),
check('neutral', 'neutral'),
check('cancelled', 'cancelled'),
check('failure', 'failure'),
check('success-b', 'success'),
check('failure-b', 'failure-b')
])
expect(sorted.map((c) => c.name)).toEqual([
'action-required',
'timed-out',
'failure',
'cancelled',
'pending',
'success-a',
'success-b',
'neutral',
'skipped',
'unknown',
'failure-b'
])
})
// Object rank tables expose prototype names as functions, producing NaN comparisons and unstable ordering.
it('treats prototype property names as unknown conclusions', () => {
for (const inherited of ['constructor', 'toString', 'hasOwnProperty', '__proto__']) {
expect(getCheckSeverityRank(inherited)).toBe(
getCheckSeverityRank('stale_from_a_future_github')
)
}
const sorted = sortChecksBySeverity([
check('inherited', 'constructor'),
check('failure', 'failure'),
check('success', 'success')
])
expect(sorted.map((c) => c.name)).toEqual(['failure', 'success', 'inherited'])
})
it('treats a missing conclusion as pending', () => {
expect(getCheckSeverityRank(null)).toBe(getCheckSeverityRank('pending'))
expect(getCheckSeverityRank(undefined)).toBe(getCheckSeverityRank('pending'))
})
it('leaves the input array untouched', () => {
const checks = [check('success', 'success'), check('failure', 'failure')]
sortChecksBySeverity(checks)
expect(checks.map((c) => c.name)).toEqual(['success', 'failure'])
})
it('orders normalized provider states with stable ties', () => {
const checks = [
// Preserve an unrecognized provider value so it remains visibly unknown.
check('future', 'future_state'),
check('manual', mapGitLabPipelineJobStatusToConclusion('manual')),
check('pass-a', 'success'),
check('pass-b', 'success')
]
// A manual GitLab gate is neutral, so it sinks below the passing checks a reviewer reads first.
expect(sortChecksBySeverity(checks).map((item) => item.name)).toEqual([
'pass-a',
'pass-b',
'manual',
'future'
])
})
})
@@ -233,10 +233,6 @@ describe('buildFixBrokenChecksPrompt', () => {
})
describe('truncateLogTailForPrompt', () => {
it('keeps the last prompt log lines and normalizes CRLF', () => {
expect(truncateLogTailForPrompt('one\r\ntwo\r\nthree')).toBe('one\ntwo\nthree')
})
it('retains the exact line and code-unit boundaries without adding a byte clamp', () => {
expect(PROMPT_LOG_TAIL_LINES).toBe(150)
expect(PROMPT_LOG_TAIL_SCAN_CODE_UNITS).toBe(256 * 1024)
@@ -1,214 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
resolveCreatePrIntentRemoteStep,
resolveCreateReviewIntentEligibility,
type CreateReviewIntentKind
} from './source-control-create-review-intent'
import type { HostedReviewCreationBlockedReason } from '../hosted-review/hosted-review-contracts'
import type { GitUpstreamStatus } from '../../git/status/git-status-types'
function unavailableEligibility(blockedReason: HostedReviewCreationBlockedReason) {
return {
provider: 'github' as const,
review: null,
canCreate: false,
blockedReason,
nextAction: null,
defaultBaseRef: 'main',
reviewLookupOutcome: 'unavailable' as const
}
}
describe('resolveCreateReviewIntentEligibility', () => {
it('rejects unavailable eligibility when the default branch is unknown', () => {
expect(
resolveCreateReviewIntentEligibility({
stagedCount: 1,
hasStageableChanges: true,
hasMessage: true,
hasUnresolvedConflicts: false,
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: {
...unavailableEligibility('dirty'),
defaultBaseRef: null
}
})
).toEqual({ eligible: false, kind: null })
})
it('stays ineligible when no local blocker remains under unavailable lookup', () => {
expect(
resolveCreateReviewIntentEligibility({
stagedCount: 0,
hasStageableChanges: false,
hasMessage: true,
hasUnresolvedConflicts: false,
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: {
...unavailableEligibility('dirty'),
blockedReason: null
}
})
).toEqual({ eligible: false, kind: null })
})
it('keeps dirty local preparation eligible when review lookup is unavailable', () => {
expect(
resolveCreateReviewIntentEligibility({
stagedCount: 0,
hasStageableChanges: true,
hasMessage: true,
hasUnresolvedConflicts: false,
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: unavailableEligibility('dirty')
})
).toEqual({ eligible: true, kind: 'dirty' })
})
it('still requires a message before committing staged changes without lookup authority', () => {
expect(
resolveCreateReviewIntentEligibility({
stagedCount: 1,
hasStageableChanges: false,
hasMessage: false,
hasUnresolvedConflicts: false,
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: unavailableEligibility('dirty')
})
).toEqual({ eligible: true, kind: 'message_required' })
})
it.each<{
blockedReason: HostedReviewCreationBlockedReason
blockedKind: CreateReviewIntentKind
stagedCount?: number
hasStageableChanges?: boolean
branchCommitsAhead?: number
upstreamStatus?: GitUpstreamStatus
}>([
{
blockedReason: 'no_upstream',
blockedKind: 'no_upstream',
branchCommitsAhead: 1,
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 }
},
{
blockedReason: 'needs_push',
blockedKind: 'needs_push',
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 1,
behind: 0
}
},
{
blockedReason: 'needs_sync',
blockedKind: 'needs_sync',
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 0,
behind: 1
}
},
{
blockedReason: 'needs_sync',
blockedKind: 'force_push',
branchCommitsAhead: 1,
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 2,
behind: 1,
behindCommitsArePatchEquivalent: true
}
}
])(
'keeps recoverable $blockedKind preparation eligible when review lookup is unavailable',
({
blockedReason,
blockedKind,
stagedCount = 0,
hasStageableChanges = false,
branchCommitsAhead,
upstreamStatus
}) => {
expect(
resolveCreateReviewIntentEligibility({
stagedCount,
hasStageableChanges,
hasMessage: true,
hasUnresolvedConflicts: false,
upstreamStatus,
hostedReviewCreation: unavailableEligibility(blockedReason),
branchCommitsAhead
})
).toEqual({ eligible: true, kind: blockedKind })
}
)
})
describe('resolveCreatePrIntentRemoteStep', () => {
it('resolves safe remote steps for publish, push, and patch-equivalent force-push', () => {
expect(
resolveCreatePrIntentRemoteStep({
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
branchCommitsAhead: 2,
hasCurrentBranch: true,
hostedReviewCreation: { canCreate: false, blockedReason: 'no_upstream' }
})
).toBe('publish')
expect(
resolveCreatePrIntentRemoteStep({
upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 },
hasCurrentBranch: true,
hostedReviewCreation: { canCreate: false, blockedReason: 'needs_push' }
})
).toBe('push')
expect(
resolveCreatePrIntentRemoteStep({
upstreamStatus: {
hasUpstream: true,
ahead: 3,
behind: 2,
behindCommitsArePatchEquivalent: true
},
branchCommitsAhead: 3,
hasCurrentBranch: true,
hostedReviewCreation: { canCreate: false, blockedReason: 'needs_sync' }
})
).toBe('force_push')
})
it('fast-forwards behind-only branches, blocks diverged and unpublished-without-commits branches', () => {
// Diverged branches require an explicit stop; automatic merge would reconcile without consent.
expect(
resolveCreatePrIntentRemoteStep({
upstreamStatus: { hasUpstream: true, ahead: 1, behind: 1 },
hasCurrentBranch: true,
hostedReviewCreation: { canCreate: false, blockedReason: 'needs_sync' }
})
).toBe('blocked')
// Behind with no local commits: pure --ff-only (never plain merge sync).
expect(
resolveCreatePrIntentRemoteStep({
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 3 },
hasCurrentBranch: true,
hostedReviewCreation: { canCreate: false, blockedReason: 'needs_sync' }
})
).toBe('fast_forward')
expect(
resolveCreatePrIntentRemoteStep({
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
branchCommitsAhead: 0,
hasCurrentBranch: true,
hostedReviewCreation: { canCreate: false, blockedReason: 'no_upstream' }
})
).toBe('blocked')
})
})
@@ -1,107 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PRInfo } from '../../github/pull-request-types'
import { resolveSourceControlCommitAreaPrimaryActionDecision } from './source-control-primary-action-decision'
import type { SourceControlPrimaryActionDecisionInputs } from './source-control-primary-action-decision-types'
import {
resolveSourceControlNextStep,
type SourceControlNextStepInputs
} from './source-control-next-step-decision'
function local(overrides: Partial<SourceControlPrimaryActionDecisionInputs> = {}) {
return resolveSourceControlCommitAreaPrimaryActionDecision({
stagedCount: 0,
hasUnstagedChanges: false,
hasStageableChanges: false,
hasPartiallyStagedChanges: false,
hasMessage: false,
hasUnresolvedConflicts: false,
isCommitting: false,
isRemoteOperationActive: false,
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
...overrides
})
}
const OPEN_PR: PRInfo = {
number: 7,
title: 'feat',
state: 'open',
url: 'https://github.com/o/r/pull/7',
checksStatus: 'success',
updatedAt: '2026-11-01T00:00:00Z',
mergeable: 'MERGEABLE',
mergeStateStatus: 'CLEAN',
reviewDecision: 'APPROVED'
}
function step(overrides: Partial<SourceControlNextStepInputs> = {}, pr: Partial<PRInfo> = {}) {
return resolveSourceControlNextStep({
local: local(),
conflictOperation: null,
hasUnresolvedConflicts: false,
hasChangesToCommit: false,
reviewCreation: 'unavailable',
review: { kind: 'github', pr: { ...OPEN_PR, ...pr }, failedChecks: 0, pendingChecks: 0 },
...overrides
})
}
describe('resolveSourceControlNextStep', () => {
it('walks the branch from local work to merge in order', () => {
expect(step({ hasUnresolvedConflicts: true, conflictOperation: 'merge' })).toEqual({
kind: 'resolve_conflicts',
scope: 'local'
})
expect(step({ conflictOperation: 'merge' }).kind).toBe('complete_merge')
expect(
step({
hasChangesToCommit: true,
local: local({ hasStageableChanges: true, hasUnstagedChanges: true })
}).kind
).toBe('commit')
expect(step({ hasChangesToCommit: true, local: local({ stagedCount: 2 }) }).kind).toBe('commit')
// A fetch on a clean branch blocks commits but leaves nothing to commit.
expect(
step({ local: local({ isRemoteOperationActive: true, inFlightRemoteOpKind: 'fetch' }) }).kind
).toBe('merge')
expect(
step({ local: local({ upstreamStatus: { hasUpstream: true, ahead: 3, behind: 0 } }) }).kind
).toBe('remote')
expect(
step(
{},
{ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY', reviewDecision: 'REVIEW_REQUIRED' }
)
).toEqual({ kind: 'resolve_conflicts', scope: 'review' })
expect(
step({ review: { kind: 'github', pr: OPEN_PR, failedChecks: 1, pendingChecks: 0 } })
).toEqual({ kind: 'fix_checks', failedChecks: 1 })
expect(step({}, { autoMergeEnabled: true }).kind).toBe('auto_merge_on')
expect(step().kind).toBe('merge')
expect(step({}, { state: 'merged' }).kind).toBe('merged')
})
it('offers Create PR only once nothing local is left, pushing first when the host asks', () => {
expect(step({ review: { kind: 'none' }, reviewCreation: 'create' })).toEqual({
kind: 'create_review',
pushFirst: false
})
expect(
step({
local: local({ upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 } }),
review: { kind: 'none' },
reviewCreation: 'push_and_create'
})
).toEqual({ kind: 'create_review', pushFirst: true })
expect(
step({
local: local({ stagedCount: 1, hasMessage: true }),
hasChangesToCommit: true,
review: { kind: 'none' },
reviewCreation: 'create'
}).kind
).toBe('commit')
expect(step({ review: { kind: 'none' } }).kind).toBe('none')
expect(step({ review: { kind: 'loading' }, reviewCreation: 'create' }).kind).toBe('none')
})
})
@@ -1,145 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
resolveSourceControlCommitAreaPrimaryActionDecision,
resolveSourceControlPrimaryActionDecision
} from './source-control-primary-action-decision'
import type { SourceControlPrimaryActionDecisionInputs } from './source-control-primary-action-decision-types'
function inputs(
overrides: Partial<SourceControlPrimaryActionDecisionInputs> = {}
): SourceControlPrimaryActionDecisionInputs {
return {
stagedCount: 0,
hasUnstagedChanges: false,
hasStageableChanges: false,
hasPartiallyStagedChanges: false,
hasMessage: false,
hasUnresolvedConflicts: false,
isCommitting: false,
isRemoteOperationActive: false,
upstreamStatus: undefined,
...overrides
}
}
describe('source-control primary action decision', () => {
it('returns Stage All for a dirty tree before remote actions', () => {
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
inputs({
hasUnstagedChanges: true,
hasStageableChanges: true,
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 4 }
})
)
expect(result.kind).toBe('stage')
expect(result.titleIntent).toBe('stage_all_changes')
expect(result.disabled).toBe(false)
})
it('blocks commits while unresolved conflicts exist', () => {
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
inputs({ stagedCount: 1, hasMessage: true, hasUnresolvedConflicts: true })
)
expect(result).toMatchObject({
kind: 'commit',
titleIntent: 'resolve_conflicts_before_commit',
disabled: true
})
})
it('blocks unpublished branch publishing when HEAD is detached', () => {
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
inputs({
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 },
hasCurrentBranch: false
})
)
expect(result).toMatchObject({
kind: 'commit',
titleIntent: 'checkout_branch_before_publish',
disabled: true
})
})
it('mirrors in-flight remote operation semantics', () => {
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
inputs({
isRemoteOperationActive: true,
inFlightRemoteOpKind: 'pull',
upstreamStatus: { hasUpstream: true, ahead: 3, behind: 0 }
})
)
expect(result).toMatchObject({
kind: 'pull',
labelIntent: 'pull',
titleIntent: 'action_in_progress',
disabled: true
})
})
it('marks patch-equivalent diverged branches as force-push-with-lease decisions', () => {
const result = resolveSourceControlCommitAreaPrimaryActionDecision(
inputs({
branchCommitsAhead: 4,
upstreamStatus: {
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 14,
behind: 3,
behindCommitsArePatchEquivalent: true
}
})
)
expect(result).toMatchObject({
kind: 'push',
labelIntent: 'force_push',
titleIntent: 'force_push_with_lease',
requiresForceWithLease: true,
count: 4,
upstreamName: 'origin/feature'
})
})
it('keeps review creation out of commit-area decisions', () => {
const input = inputs({
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: {
provider: 'gitlab',
review: null,
canCreate: true,
blockedReason: null,
nextAction: null,
reviewLookupOutcome: 'not_found'
}
})
expect(resolveSourceControlPrimaryActionDecision(input).kind).toBe('create_pr')
expect(resolveSourceControlCommitAreaPrimaryActionDecision(input).kind).toBe('commit')
})
it('keeps in-flight review intents out of commit-area decisions', () => {
const input = inputs({ isPrIntentInFlight: true })
expect(resolveSourceControlPrimaryActionDecision(input).kind).toBe('create_pr_intent')
expect(resolveSourceControlCommitAreaPrimaryActionDecision(input).kind).toBe('commit')
})
it('returns disabled create review while hosted-review creation eligibility is loading', () => {
const input = inputs({
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: {
provider: 'gitlab',
review: null,
canCreate: false,
blockedReason: null,
nextAction: null,
reviewLookupOutcome: 'not_found'
},
isHostedReviewCreationLoading: true
})
expect(resolveSourceControlPrimaryActionDecision(input)).toMatchObject({
kind: 'create_pr',
titleIntent: 'checking_review_creation',
disabled: true
})
expect(resolveSourceControlCommitAreaPrimaryActionDecision(input).kind).toBe('commit')
})
})
@@ -1,54 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { HostedReviewCreationEligibility } from './hosted-review-contracts'
import { isHostedReviewCreateEligibilityConfirmable } from './hosted-review-create-confirmation'
function eligibility(
overrides: Partial<HostedReviewCreationEligibility> = {}
): HostedReviewCreationEligibility {
return {
provider: 'github',
review: null,
canCreate: true,
blockedReason: null,
nextAction: null,
reviewLookupOutcome: 'not_found',
...overrides
}
}
describe('isHostedReviewCreateEligibilityConfirmable', () => {
const base = { reviewLookup: 'not_found' as const, hasHardRefreshError: false }
it('fails closed on every hard-block: null, existing_review, positive, hard error, unavailable', () => {
expect(isHostedReviewCreateEligibilityConfirmable({ ...base, eligibility: null })).toBe(false)
expect(
isHostedReviewCreateEligibilityConfirmable({
...base,
eligibility: eligibility({ canCreate: false, blockedReason: 'existing_review' })
})
).toBe(false)
expect(
isHostedReviewCreateEligibilityConfirmable({
...base,
eligibility: eligibility(),
reviewLookup: 'positive_unresolved'
})
).toBe(false)
expect(
isHostedReviewCreateEligibilityConfirmable({
...base,
eligibility: eligibility(),
hasHardRefreshError: true
})
).toBe(false)
expect(
isHostedReviewCreateEligibilityConfirmable({
...base,
eligibility: eligibility({
blockedReason: 'needs_push',
reviewLookupOutcome: 'unavailable'
})
})
).toBe(false)
})
})
@@ -1,51 +0,0 @@
import { describe, expect, it } from 'vitest'
import { resolveCreateReviewSeedFromCommits } from './hosted-review-create-seed'
const github = { provider: 'github' as const, title: '', body: '' }
describe('resolveCreateReviewSeedFromCommits', () => {
it('uses the only commit for both title and description, as GitHub does', () => {
expect(
resolveCreateReviewSeedFromCommits({
branch: 'kelvin/fix-sync',
eligibility: github,
commits: [
{ subject: 'fix(git): keep the PR panel in sync', body: 'Refetch after a force push.' }
]
})
).toEqual({
title: 'fix(git): keep the PR panel in sync',
body: 'Refetch after a force push.'
})
})
it('titles several commits from the branch and lists their subjects oldest first', () => {
expect(
resolveCreateReviewSeedFromCommits({
branch: 'refs/heads/kelvin/mobile-redesign',
eligibility: github,
commits: [{ subject: 'feat: add ListRow' }, { subject: 'feat: rebuild Tasks' }]
})
).toEqual({ title: 'Mobile redesign', body: '- feat: add ListRow\n- feat: rebuild Tasks' })
})
it('keeps a title and description the host already supplied', () => {
expect(
resolveCreateReviewSeedFromCommits({
branch: 'kelvin/x',
eligibility: { provider: 'github', title: 'Host title', body: 'Host body' },
commits: [{ subject: 'one', body: 'commit body' }]
})
).toEqual({ title: 'Host title', body: 'Host body' })
})
it('falls back to the branch seed when no commits are known', () => {
expect(
resolveCreateReviewSeedFromCommits({
branch: 'kelvin/add-thing',
eligibility: github,
commits: []
})
).toEqual({ title: 'Add thing', body: '' })
})
})
@@ -1,22 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
resolveHostedReviewCreationProvider,
supportsHostedReviewCreation
} from './hosted-review-creation-providers'
describe('hosted review creation providers', () => {
// The create-review blocker uses this list; missing implemented providers would remain unsupported in the UI.
it('accepts every provider Orca can create a review on', () => {
for (const provider of ['github', 'gitlab', 'bitbucket', 'azure-devops', 'gitea'] as const) {
expect(supportsHostedReviewCreation(provider)).toBe(true)
expect(resolveHostedReviewCreationProvider(provider)).toBe(provider)
}
})
it('rejects unsupported and missing providers, falling back to GitHub copy', () => {
expect(supportsHostedReviewCreation('unsupported')).toBe(false)
expect(supportsHostedReviewCreation(null)).toBe(false)
expect(supportsHostedReviewCreation(undefined)).toBe(false)
expect(resolveHostedReviewCreationProvider('unsupported')).toBe('github')
})
})
@@ -1,32 +0,0 @@
import { describe, expect, it } from 'vitest'
import { hostedReviewInfoFromGitHubPRInfo } from './hosted-review-github'
import type { PRInfo } from '../../github/pull-request-types'
const pr: PRInfo = {
number: 12,
title: 'Add queue badges',
state: 'open',
url: 'https://github.com/acme/orca/pull/12',
checksStatus: 'pending',
updatedAt: '2026-05-12T00:00:00.000Z',
mergeable: 'MERGEABLE',
headSha: 'abc123'
}
describe('hostedReviewInfoFromGitHubPRInfo', () => {
it('maps PRInfo into sidebar hosted review metadata', () => {
const githubRepository = { owner: 'upstream', repo: 'orca' }
const review = hostedReviewInfoFromGitHubPRInfo({ ...pr, prRepo: githubRepository })
expect(review).toMatchObject({
provider: 'github',
number: 12,
title: 'Add queue badges',
state: 'open',
status: 'pending',
mergeable: 'MERGEABLE',
headSha: 'abc123',
githubRepository
})
})
})
@@ -1,226 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
hasPositiveHostedReviewNumberLink,
hasResolvableHostedReviewPushTargetLink,
hasUsableHostedReviewPushTarget,
resolveHostedReviewActionUpstreamStatus,
resolveHostedReviewStateForActions
} from './hosted-review-push-target'
const unrelatedUpstream = {
hasUpstream: true,
upstreamName: 'origin/helper-branch',
ahead: 1,
behind: 0
}
describe('resolveHostedReviewActionUpstreamStatus', () => {
it('blocks action status from using a local upstream while linked review state is loading', () => {
expect(
resolveHostedReviewActionUpstreamStatus({
hasHostedReviewLink: true,
hasResolvableHostedReviewPushTargetLink: false,
hostedReviewState: null,
isHostedReviewStateLoading: true,
canUseHostedReviewPushTarget: false,
upstreamStatus: unrelatedUpstream
})
).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
})
it('blocks action status from using a local upstream for open reviews without a target', () => {
expect(
resolveHostedReviewActionUpstreamStatus({
hasHostedReviewLink: true,
hasResolvableHostedReviewPushTargetLink: false,
hostedReviewState: 'open',
isHostedReviewStateLoading: false,
canUseHostedReviewPushTarget: false,
upstreamStatus: unrelatedUpstream
})
).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
})
it('blocks explicit linked review metadata even when review state is unavailable', () => {
expect(
resolveHostedReviewActionUpstreamStatus({
hasHostedReviewLink: true,
hasResolvableHostedReviewPushTargetLink: true,
hostedReviewState: null,
isHostedReviewStateLoading: false,
canUseHostedReviewPushTarget: false,
upstreamStatus: unrelatedUpstream
})
).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
})
it('does not block unknown-state review links when no target lookup exists', () => {
expect(
resolveHostedReviewActionUpstreamStatus({
hasHostedReviewLink: true,
hasResolvableHostedReviewPushTargetLink: false,
hostedReviewState: null,
isHostedReviewStateLoading: false,
canUseHostedReviewPushTarget: false,
upstreamStatus: unrelatedUpstream
})
).toBe(unrelatedUpstream)
})
it('does not alter status for closed reviews', () => {
expect(
resolveHostedReviewActionUpstreamStatus({
hasHostedReviewLink: true,
hasResolvableHostedReviewPushTargetLink: true,
hostedReviewState: 'closed',
isHostedReviewStateLoading: false,
canUseHostedReviewPushTarget: false,
upstreamStatus: unrelatedUpstream
})
).toBe(unrelatedUpstream)
})
})
describe('hasResolvableHostedReviewPushTargetLink', () => {
it('accepts only hosted-review links with supported target lookup APIs', () => {
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitHubPR: 12 })).toBe(true)
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitLabMR: 34 })).toBe(true)
// Why: a queue-discovered same-repo PR (no persisted linkedPR) is resolvable.
expect(hasResolvableHostedReviewPushTargetLink({ fallbackGitHubPR: 8333 })).toBe(true)
expect(
hasResolvableHostedReviewPushTargetLink({ linkedGitHubPR: null, fallbackGitHubPR: 8333 })
).toBe(true)
expect(hasResolvableHostedReviewPushTargetLink({ fallbackGitHubPR: 0 })).toBe(false)
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitHubPR: null })).toBe(false)
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitHubPR: 0 })).toBe(false)
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitLabMR: -1 })).toBe(false)
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitLabMR: 1.5 })).toBe(false)
expect(hasResolvableHostedReviewPushTargetLink({ linkedGitHubPR: Number.NaN })).toBe(false)
expect(hasResolvableHostedReviewPushTargetLink({})).toBe(false)
})
})
describe('hasPositiveHostedReviewNumberLink', () => {
it('blocks resolver-less providers without treating them as resolvable', () => {
// Providers without resolvers still block unsafe pushes; keep resolvable links a strict subset of positive links.
for (const provider of ['linkedBitbucketPR', 'linkedAzureDevOpsPR', 'linkedGiteaPR'] as const) {
const args = { [provider]: 42 }
expect(hasPositiveHostedReviewNumberLink(args)).toBe(true)
expect(hasResolvableHostedReviewPushTargetLink(args)).toBe(false)
}
})
})
describe('resolveHostedReviewStateForActions', () => {
it('treats explicit linked review metadata as open when live state is unavailable', () => {
expect(
resolveHostedReviewStateForActions({
hostedReviewState: null,
hasResolvableHostedReviewPushTargetLink: true
})
).toBe('open')
})
})
describe('hasUsableHostedReviewPushTarget', () => {
it('accepts either persisted target metadata or branch-configured push metadata', () => {
expect(
hasUsableHostedReviewPushTarget({
pushTarget: { remoteName: 'fork', branchName: 'feature' }
})
).toBe(true)
expect(
hasUsableHostedReviewPushTarget({
pushTarget: { remoteName: 'fork', branchName: 'feature' },
upstreamStatus: {
hasUpstream: true,
upstreamName: 'fork/feature',
ahead: 1,
behind: 0
}
})
).toBe(true)
expect(
hasUsableHostedReviewPushTarget({
upstreamStatus: {
hasUpstream: false,
ahead: 0,
behind: 0,
hasConfiguredPushTarget: true
}
})
).toBe(true)
expect(
hasUsableHostedReviewPushTarget({
hasResolvableHostedReviewPushTargetLink: true,
upstreamStatus: {
hasUpstream: false,
ahead: 0,
behind: 0,
hasConfiguredPushTarget: true
}
})
).toBe(false)
expect(
hasUsableHostedReviewPushTarget({
pushTarget: { remoteName: 'fork', branchName: 'feature' },
upstreamStatus: unrelatedUpstream
})
).toBe(false)
expect(hasUsableHostedReviewPushTarget({ upstreamStatus: unrelatedUpstream })).toBe(false)
})
it('keeps blocking a review whose upstream tracks an unrelated fork/helper head', () => {
expect(
hasUsableHostedReviewPushTarget({
hasResolvableHostedReviewPushTargetLink: true,
branchName: 'feature',
upstreamStatus: unrelatedUpstream
})
).toBe(false)
})
it('keeps blocking a resolvable review with no real upstream', () => {
expect(
hasUsableHostedReviewPushTarget({
hasResolvableHostedReviewPushTargetLink: true,
branchName: 'feature',
upstreamStatus: { hasUpstream: false, ahead: 0, behind: 0 }
})
).toBe(false)
})
})
describe('resolveHostedReviewActionUpstreamStatus with a same-repo upstream', () => {
it('does not block push for a queue-discovered open PR whose upstream tracks the branch', () => {
// Queue-discovered PRs lack persisted links but still resolve matching upstream targets; missing that wrongly disabled Push.
const realUpstream = {
hasUpstream: true,
upstreamName: 'origin/fix-f1-codex-wsl-path-trust',
ahead: 1,
behind: 0
}
const hasResolvable = hasResolvableHostedReviewPushTargetLink({
linkedGitHubPR: null,
fallbackGitHubPR: 8333,
linkedGitLabMR: null
})
expect(hasResolvable).toBe(true)
const canUseHostedReviewPushTarget = hasUsableHostedReviewPushTarget({
hasResolvableHostedReviewPushTargetLink: hasResolvable,
branchName: 'fix-f1-codex-wsl-path-trust',
upstreamStatus: realUpstream
})
expect(canUseHostedReviewPushTarget).toBe(true)
expect(
resolveHostedReviewActionUpstreamStatus({
hasHostedReviewLink: true,
hasResolvableHostedReviewPushTargetLink: hasResolvable,
hostedReviewState: 'open',
isHostedReviewStateLoading: false,
canUseHostedReviewPushTarget,
upstreamStatus: realUpstream
})
).toBe(realUpstream)
})
})
@@ -1,23 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
isRemoteHeadRef,
normalizeHostedReviewBaseRef,
normalizeHostedReviewHeadRef
} from './hosted-review-refs'
describe('hosted review ref normalization', () => {
it('normalizes local and remote head refs to branch names', () => {
expect(normalizeHostedReviewHeadRef(' refs/heads/feature/create-pr ')).toBe('feature/create-pr')
expect(normalizeHostedReviewHeadRef('refs/remotes/origin/feature/create-pr')).toBe(
'feature/create-pr'
)
})
it('strips common remote prefixes from base refs', () => {
expect(normalizeHostedReviewBaseRef('origin/main')).toBe('main')
expect(normalizeHostedReviewBaseRef('refs/remotes/upstream/release/1.0')).toBe('release/1.0')
})
})
import { isRemoteHeadRef } from './hosted-review-refs'
describe('isRemoteHeadRef', () => {
it('recognizes only a remote symbolic HEAD slot', () => {
@@ -68,48 +50,3 @@ describe('isRemoteHeadRef', () => {
expect(remotes).toEqual(['foo', 'foo/bar'])
})
})
/** Counts array copies (`[...remotes]`, one per sort) and elements copied, via the spread iterator. */
function instrumentRemotes(names: readonly string[]): {
remotes: readonly string[]
counts: { copies: number; copiedElements: number }
} {
const remotes = [...names]
const counts = { copies: 0, copiedElements: 0 }
Object.defineProperty(remotes, Symbol.iterator, {
configurable: true,
value: function* countingIterator(this: readonly string[]) {
counts.copies += 1
for (let index = 0; index < this.length; index += 1) {
counts.copiedElements += 1
yield this[index] as string
}
}
})
return { remotes, counts }
}
describe('isRemoteHeadRef fast path', () => {
it('does not copy remotes while rejecting ordinary refs', () => {
const ordinaryRefs = Array.from(
{ length: 80 },
(_unused, index) => `origin/feature/branch-${index}`
)
const remoteNames = ['origin', 'upstream', 'fork']
const after = instrumentRemotes(remoteNames)
for (const ref of ordinaryRefs) {
expect(isRemoteHeadRef(ref, after.remotes)).toBe(false)
}
expect(after.counts).toEqual({ copies: 0, copiedElements: 0 })
})
it('copies each remote once for a symbolic HEAD candidate', () => {
const remoteNames = ['origin', 'upstream', 'fork']
const after = instrumentRemotes(remoteNames)
expect(isRemoteHeadRef('refs/remotes/origin/HEAD', after.remotes)).toBe(true)
expect(after.counts).toEqual({ copies: 1, copiedElements: 3 })
})
})
@@ -1,41 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { GitStatusEntry } from '../../git/status/git-status-types'
import { getConflictResolutions } from './source-control-entry-actions'
function conflict(overrides: Partial<GitStatusEntry>): GitStatusEntry {
return {
path: 'a.txt',
area: 'unstaged',
status: 'modified',
conflictKind: 'both_modified',
conflictStatus: 'unresolved',
...overrides
}
}
describe('getConflictResolutions', () => {
it('offers both sides of a delete/modify conflict while the surviving file is present', () => {
expect(getConflictResolutions(conflict({ conflictKind: 'deleted_by_us' }))).toEqual([
'keep_file',
'keep_deletion'
])
})
it('offers only the deletion once the worktree file is gone', () => {
expect(
getConflictResolutions(conflict({ conflictKind: 'deleted_by_them', status: 'deleted' }))
).toEqual(['keep_deletion'])
expect(
getConflictResolutions(conflict({ conflictKind: 'both_modified', status: 'deleted' }))
).toEqual(['keep_deletion'])
})
it('marks content conflicts resolved as edited', () => {
expect(getConflictResolutions(conflict({}))).toEqual(['mark_resolved'])
})
it('offers nothing for resolved rows or rows inside a submodule the parent cannot stage', () => {
expect(getConflictResolutions(conflict({ conflictStatus: 'resolved_locally' }))).toEqual([])
expect(getConflictResolutions(conflict({ submoduleRoot: 'sub' }))).toEqual([])
})
})
@@ -1,23 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
DEFAULT_SOURCE_CONTROL_GROUP_ORDER,
normalizeSourceControlGroupOrder
} from './source-control-group-order'
describe('normalizeSourceControlGroupOrder', () => {
it('keeps supported source control group orders', () => {
expect(normalizeSourceControlGroupOrder('changes-first')).toBe('changes-first')
expect(normalizeSourceControlGroupOrder('staged-first')).toBe('staged-first')
})
it('maps the legacy untracked-first preference to combined Changes first', () => {
expect(normalizeSourceControlGroupOrder('untracked-first')).toBe('changes-first')
})
it('falls back to the default for malformed values', () => {
expect(normalizeSourceControlGroupOrder('tracked-first')).toBe(
DEFAULT_SOURCE_CONTROL_GROUP_ORDER
)
expect(normalizeSourceControlGroupOrder(undefined)).toBe(DEFAULT_SOURCE_CONTROL_GROUP_ORDER)
})
})
@@ -1,167 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { GitStatusEntry } from '../../git/status/git-status-types'
import {
buildSourceControlDisplaySections,
getSourceControlSectionViewAction,
resolveSourceControlGroupOrder,
splitPinnedSourceControlConflicts,
type SourceControlEntryGroups
} from './source-control-section-order'
function entry(partial: Partial<GitStatusEntry> & { path: string }): GitStatusEntry {
return {
area: 'unstaged',
status: 'modified',
...partial
}
}
function groups(partial: Partial<SourceControlEntryGroups>): SourceControlEntryGroups {
return {
staged: [],
unstaged: [],
untracked: [],
...partial
}
}
describe('buildSourceControlDisplaySections', () => {
it('combines working files in path order before staged changes by default', () => {
const sections = buildSourceControlDisplaySections(
groups({
staged: [entry({ area: 'staged', path: 'staged.ts' })],
unstaged: [entry({ path: 'working.ts' })],
untracked: [entry({ area: 'untracked', path: 'new.ts', status: 'untracked' })]
}),
resolveSourceControlGroupOrder(undefined)
)
expect(sections.map((section) => section.id)).toEqual(['unstaged', 'staged'])
expect(sections[0]?.items.map((item) => item.path)).toEqual(['new.ts', 'working.ts'])
})
it('pins conflict rows and removes them from the normal Changes section', () => {
const unresolved = entry({
area: 'unstaged',
path: 'conflict.ts',
conflictStatus: 'unresolved'
})
const resolved = entry({
area: 'unstaged',
path: 'resolved.ts',
conflictStatus: 'resolved_locally'
})
const normal = entry({ area: 'unstaged', path: 'normal.ts' })
const input = groups({ unstaged: [unresolved, resolved, normal] })
const split = splitPinnedSourceControlConflicts(input)
const sections = buildSourceControlDisplaySections(
input,
resolveSourceControlGroupOrder('changes-first')
)
expect(split.pinnedConflicts.map((item) => item.path)).toEqual(['conflict.ts', 'resolved.ts'])
expect(split.normalGroups.unstaged.map((item) => item.path)).toEqual(['normal.ts'])
expect(sections.map((section) => section.id)).toEqual(['conflicts', 'unstaged'])
expect(sections[0]?.items.map((item) => item.path)).toEqual(['conflict.ts', 'resolved.ts'])
expect(sections[1]?.items.map((item) => item.path)).toEqual(['normal.ts'])
})
it('pins locally resolved staged conflicts and removes them from Staged Changes', () => {
const resolvedStaged = entry({
area: 'staged',
path: 'resolved-staged.ts',
conflictKind: 'both_modified',
conflictStatus: 'resolved_locally'
})
const staged = entry({ area: 'staged', path: 'staged.ts' })
const input = groups({ staged: [resolvedStaged, staged] })
const split = splitPinnedSourceControlConflicts(input)
const sections = buildSourceControlDisplaySections(
input,
resolveSourceControlGroupOrder('staged-first')
)
expect(split.pinnedConflicts).toEqual([resolvedStaged])
expect(split.normalGroups.staged).toEqual([staged])
expect(sections.map((section) => section.id)).toEqual(['conflicts', 'staged'])
expect(sections[0]?.items[0]?.area).toBe('staged')
expect(sections[1]?.items).toEqual([staged])
})
it('routes the pinned Conflicts section to conflict review', () => {
const sections = buildSourceControlDisplaySections(
groups({
unstaged: [
entry({
area: 'unstaged',
path: 'conflict.ts',
conflictKind: 'both_modified',
conflictStatus: 'unresolved'
}),
entry({ area: 'unstaged', path: 'normal.ts' })
]
}),
resolveSourceControlGroupOrder('changes-first')
)
expect(getSourceControlSectionViewAction(sections[0]!)).toEqual({
kind: 'conflict-review',
entries: [{ path: 'conflict.ts', conflictKind: 'both_modified' }]
})
expect(getSourceControlSectionViewAction(sections[1]!)).toEqual({
kind: 'combined-diff',
area: 'unstaged',
entries: [entry({ area: 'unstaged', path: 'normal.ts' })]
})
})
it('routes locally resolved-only conflict sections to combined diff', () => {
const resolved = entry({
area: 'unstaged',
path: 'resolved.ts',
conflictKind: 'both_modified',
conflictStatus: 'resolved_locally'
})
const sections = buildSourceControlDisplaySections(
groups({
unstaged: [resolved]
}),
resolveSourceControlGroupOrder('changes-first')
)
expect(getSourceControlSectionViewAction(sections[0]!)).toEqual({
kind: 'combined-diff',
area: 'unstaged',
entries: [resolved]
})
})
it('uses a generic combined diff action for mixed-area resolved conflict sections', () => {
const unstaged = entry({
area: 'unstaged',
path: 'resolved-unstaged.ts',
conflictKind: 'both_modified',
conflictStatus: 'resolved_locally'
})
const staged = entry({
area: 'staged',
path: 'resolved-staged.ts',
conflictKind: 'both_modified',
conflictStatus: 'resolved_locally'
})
const sections = buildSourceControlDisplaySections(
groups({
staged: [staged],
unstaged: [unstaged]
}),
resolveSourceControlGroupOrder('staged-first')
)
expect(getSourceControlSectionViewAction(sections[0]!)).toEqual({
kind: 'combined-diff',
entries: [unstaged, staged]
})
})
})
@@ -19,60 +19,7 @@ function entry(partial: Partial<GitStatusEntry> & { path: string }): GitStatusEn
}
}
function labels(nodes: ReturnType<typeof flattenSourceControlTree>): string[] {
return nodes.map((node) => `${node.type}:${node.path}`)
}
describe('buildSourceControlTree', () => {
it('orders numbered sibling directories naturally, matching the file rows', () => {
const tree = buildSourceControlTree('unstaged', [
entry({ path: 'migrations/100 - b/a.sql' }),
entry({ path: 'migrations/9 - a/a.sql' }),
entry({ path: 'migrations/99 - c/a.sql' })
])
expect(labels(flattenSourceControlTree(tree, new Set()))).toEqual([
'directory:migrations',
'directory:migrations/9 - a',
'file:migrations/9 - a/a.sql',
'directory:migrations/99 - c',
'file:migrations/99 - c/a.sql',
'directory:migrations/100 - b',
'file:migrations/100 - b/a.sql'
])
})
it('keeps conflict rows ahead of normal rows within a directory', () => {
const tree = buildGitStatusSourceControlTree('unstaged', [
entry({ path: 'src/a.ts' }),
entry({
path: 'src/z.ts',
conflictKind: 'both_modified',
conflictStatus: 'unresolved'
})
])
expect(labels(flattenSourceControlTree(tree, new Set()))).toEqual([
'directory:src',
'file:src/z.ts',
'file:src/a.ts'
])
})
it('omits collapsed directory descendants from the visible rows', () => {
const tree = buildSourceControlTree('staged', [
entry({ path: 'src/app.ts', area: 'staged' }),
entry({ path: 'docs/readme.md', area: 'staged' })
])
const collapsed = new Set(['dir::staged::src'])
expect(labels(flattenSourceControlTree(tree, collapsed))).toEqual([
'directory:docs',
'file:docs/readme.md',
'directory:src'
])
})
it('collects all descendant file entries for directory actions', () => {
const tree = buildSourceControlTree('unstaged', [
entry({ path: 'src/app.ts' }),
@@ -88,45 +35,6 @@ describe('buildSourceControlTree', () => {
])
})
it('compacts single-child directory chains for visible source-control rows', () => {
const tree = buildSourceControlTree('branch', [
{ path: 'src/renderer/src/mockups.css', status: 'modified' },
{ path: 'src/renderer/src/components/App.tsx', status: 'added' }
])
const compacted = compactSourceControlTree(tree)
expect(labels(flattenSourceControlTree(compacted, new Set()))).toEqual([
'directory:src/renderer/src',
'directory:src/renderer/src/components',
'file:src/renderer/src/components/App.tsx',
'file:src/renderer/src/mockups.css'
])
expect(compacted[0]).toMatchObject({
name: 'src/renderer/src',
path: 'src/renderer/src',
depth: 0,
key: 'dir::branch::src/renderer/src'
})
})
it('preserves real branching folders when compacting directory chains', () => {
const tree = buildSourceControlTree('unstaged', [
entry({ path: 'src/main/index.ts' }),
entry({ path: 'src/renderer/index.ts' })
])
const compacted = compactSourceControlTree(tree)
expect(labels(flattenSourceControlTree(compacted, new Set()))).toEqual([
'directory:src',
'directory:src/main',
'file:src/main/index.ts',
'directory:src/renderer',
'file:src/renderer/index.ts'
])
})
it('can preserve file node areas from mixed conflict section entries', () => {
const tree = compactSourceControlTree(
buildGitStatusSourceControlTree('unstaged', [
@@ -1,18 +1,11 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import {
PUSH_FAILURE_PROMPT_FILE_LIMIT,
PUSH_FAILURE_SUMMARY_SCAN_CODE_UNITS,
buildFixPushFailurePrompt,
hasExpandedPushFailureDetails,
isPushHookFailure,
sanitizePushFailureDetails,
summarizePushFailure
} from './source-control-push-failure'
afterEach(() => {
vi.restoreAllMocks()
})
describe('push failure detection and summary', () => {
it('detects explicit pre-push hook failures', () => {
const raw =
@@ -42,22 +35,6 @@ describe('push failure detection and summary', () => {
expect(isPushHookFailure(raw)).toBe(false)
}
})
it('strips ANSI and control output before details and comparison', () => {
const raw = '\u001b[31mhusky - pre-push hook failed\u001b[0m\u0007\neslint failed'
expect(sanitizePushFailureDetails(raw)).toBe('husky - pre-push hook failed\neslint failed')
expect(summarizePushFailure(raw)).toBe('Lint failed during push.')
})
it('bounds summary analysis for pathological single-line logs', () => {
const split = vi.spyOn(String.prototype, 'split')
const raw = 'x'.repeat(PUSH_FAILURE_SUMMARY_SCAN_CODE_UNITS + 10_000)
expect(summarizePushFailure(raw)).toBe('x'.repeat(PUSH_FAILURE_SUMMARY_SCAN_CODE_UNITS))
expect(hasExpandedPushFailureDetails(raw, 'Push failed.')).toBe(true)
expect(split).not.toHaveBeenCalled()
})
})
describe('buildFixPushFailurePrompt', () => {
@@ -1,75 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PRComment } from '../../github/comment-types'
import { createBotAuthorOverrideSet } from './pr-bot-author-overrides'
import {
filterPRCommentsByAudience,
getPRCommentAudienceCounts,
isBotPRComment
} from './pr-comment-audience'
function comment(overrides: Partial<PRComment>): PRComment {
return {
id: overrides.id ?? 1,
author: overrides.author ?? 'user',
authorAvatarUrl: '',
body: '',
createdAt: '',
url: '',
...overrides
}
}
describe('pr comment audience filtering', () => {
it('classifies provider metadata and automation logins', () => {
expect(isBotPRComment(comment({ author: 'alice', isBot: true }))).toBe(true)
expect(isBotPRComment(comment({ author: 'github-actions[bot]' }))).toBe(true)
expect(isBotPRComment(comment({ author: 'renovate-bot' }))).toBe(true)
expect(isBotPRComment(comment({ author: 'preview-automation' }))).toBe(true)
expect(isBotPRComment(comment({ author: 'qodo-ai-reviewer', isBot: false }))).toBe(true)
expect(isBotPRComment(comment({ author: 'coderabbitai', isBot: false }))).toBe(true)
expect(isBotPRComment(comment({ author: 'sonarcloud' }))).toBe(true)
expect(isBotPRComment(comment({ author: 'codium-ai-reviewer' }))).toBe(true)
expect(isBotPRComment(comment({ author: 'octocat' }))).toBe(false)
expect(isBotPRComment(comment({ author: 'robotics-dev' }))).toBe(false)
expect(isBotPRComment(comment({ author: 'human-botany' }))).toBe(false)
})
it('counts and filters human and bot comments', () => {
const comments = [
comment({ id: 1, author: 'yasinkavakli' }),
comment({ id: 2, author: 'chatgpt-codex-connector', isBot: true }),
comment({ id: 3, author: 'github-actions[bot]' })
]
expect(getPRCommentAudienceCounts(comments)).toEqual({ all: 3, human: 1, bot: 2 })
expect(filterPRCommentsByAudience(comments, 'all')).toBe(comments)
expect(filterPRCommentsByAudience(comments, 'human').map((item) => item.id)).toEqual([1])
expect(filterPRCommentsByAudience(comments, 'bot').map((item) => item.id)).toEqual([2, 3])
})
it('classifies manually overridden authors as bots regardless of heuristics', () => {
const overrides = createBotAuthorOverrideSet([' GretelFlux ', '', 'other-bot-account'])
expect(isBotPRComment(comment({ author: 'gretelflux' }))).toBe(false)
expect(isBotPRComment(comment({ author: 'gretelflux' }), overrides)).toBe(true)
expect(isBotPRComment(comment({ author: ' Gretelflux ' }), overrides)).toBe(true)
expect(isBotPRComment(comment({ author: 'yasinkavakli' }), overrides)).toBe(false)
})
it('applies overrides to counts and filters', () => {
const overrides = createBotAuthorOverrideSet(['gretelflux'])
const comments = [
comment({ id: 1, author: 'yasinkavakli' }),
comment({ id: 2, author: 'gretelflux' }),
comment({ id: 3, author: 'github-actions[bot]' })
]
expect(getPRCommentAudienceCounts(comments, overrides)).toEqual({ all: 3, human: 1, bot: 2 })
expect(filterPRCommentsByAudience(comments, 'human', overrides).map((item) => item.id)).toEqual(
[1]
)
expect(filterPRCommentsByAudience(comments, 'bot', overrides).map((item) => item.id)).toEqual([
2, 3
])
})
})
@@ -1,38 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { PRComment } from '../../github/comment-types'
import {
getPRCommentGroupCount,
getPRCommentGroupId,
groupPRComments,
isResolvedPRCommentGroup
} from './pr-comment-groups'
function comment(overrides: Partial<PRComment>): PRComment {
return {
id: overrides.id ?? 1,
author: 'user',
authorAvatarUrl: '',
body: '',
createdAt: '',
url: '',
...overrides
}
}
describe('pr comment groups', () => {
it('groups review thread replies while preserving first-comment order', () => {
const comments = [
comment({ id: 1 }),
comment({ id: 2, threadId: 'thread-a', isResolved: true }),
comment({ id: 3, threadId: 'thread-a' }),
comment({ id: 4 })
]
const groups = groupPRComments(comments)
expect(groups.map(getPRCommentGroupId)).toEqual(['comment:1', 'thread:thread-a', 'comment:4'])
expect(groups[0]).toEqual({ kind: 'standalone', comment: comments[0] })
expect(groups[1]).toMatchObject({ root: { id: 2 }, replies: [{ id: 3 }] })
expect(getPRCommentGroupCount(groups[1])).toBe(2)
expect(isResolvedPRCommentGroup(groups[1])).toBe(true)
})
})
@@ -1,19 +0,0 @@
import { describe, expect, it } from 'vitest'
import { formatPrCommentRelativeTime } from './pr-comment-time'
const NOW = Date.parse('2026-06-21T20:00:00.000Z')
describe('formatPrCommentRelativeTime', () => {
it('formats recent and older comment timestamps compactly', () => {
expect(formatPrCommentRelativeTime('2026-06-21T19:59:45.000Z', NOW)).toBe('just now')
expect(formatPrCommentRelativeTime('2026-06-21T19:35:00.000Z', NOW)).toBe('25m ago')
expect(formatPrCommentRelativeTime('2026-06-21T17:00:00.000Z', NOW)).toBe('3h ago')
expect(formatPrCommentRelativeTime('2026-06-18T20:00:00.000Z', NOW)).toBe('3d ago')
expect(formatPrCommentRelativeTime('2026-04-21T20:00:00.000Z', NOW)).toBe('2mo ago')
expect(formatPrCommentRelativeTime('2024-06-21T20:00:00.000Z', NOW)).toBe('2y ago')
})
it('returns an empty label for invalid timestamps', () => {
expect(formatPrCommentRelativeTime('not a date', NOW)).toBe('')
})
})