test: remove 287 low-value suites from combined audit (#42659)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-15 00:17:59 -08:00
committed by GitHub
co-authored by OrcaWin
parent 2b4026d4cb
commit 7b5f3176f3
300 changed files with 74 additions and 34368 deletions
-4
View File
@@ -80,7 +80,6 @@ on:
- 'src/main/ai-vault/parsing/session-scanner-jsonl-reader.test.ts'
- 'src/main/ai-vault/parsing/session-scanner-jsonl-reader.ts'
- 'src/main/ai-vault/parsing/session-scanner-messages.ts'
- 'src/main/ai-vault/parsing/session-scanner-parse-wsl-stall.test.ts'
- 'src/main/ai-vault/parsing/session-scanner-text-normalization.ts'
- 'src/main/ai-vault/parsing/session-scanner-token-values.ts'
- 'src/main/ai-vault/prompts/session-scanner-first-user-prompt-capture.ts'
@@ -94,7 +93,6 @@ on:
- 'src/main/ai-vault/scope/session-scanner-scope-inventory.ts'
- 'src/main/ai-vault/scope/session-scanner-scope-walk.test.ts'
- 'src/main/ai-vault/scope/session-scanner-scope-walk.ts'
- 'src/main/ai-vault/scope/session-scanner-scope.test.ts'
- 'src/main/ai-vault/session-scanner.ts'
- 'src/shared/error-message.ts'
- 'src/shared/surrogate-safe-text-slice.ts'
@@ -213,7 +211,6 @@ on:
- 'src/main/ai-vault/parsing/session-scanner-jsonl-reader.test.ts'
- 'src/main/ai-vault/parsing/session-scanner-jsonl-reader.ts'
- 'src/main/ai-vault/parsing/session-scanner-messages.ts'
- 'src/main/ai-vault/parsing/session-scanner-parse-wsl-stall.test.ts'
- 'src/main/ai-vault/parsing/session-scanner-text-normalization.ts'
- 'src/main/ai-vault/parsing/session-scanner-token-values.ts'
- 'src/main/ai-vault/prompts/session-scanner-first-user-prompt-capture.ts'
@@ -227,7 +224,6 @@ on:
- 'src/main/ai-vault/scope/session-scanner-scope-inventory.ts'
- 'src/main/ai-vault/scope/session-scanner-scope-walk.test.ts'
- 'src/main/ai-vault/scope/session-scanner-scope-walk.ts'
- 'src/main/ai-vault/scope/session-scanner-scope.test.ts'
- 'src/main/ai-vault/session-scanner.ts'
- 'src/shared/error-message.ts'
- 'src/shared/surrogate-safe-text-slice.ts'
-8
View File
@@ -114,7 +114,6 @@
1 test-no-real-time-wait:src/main/agent-hooks/installation/scripts/posix-hook-json-stdin-reader.test.ts
2 test-no-real-time-wait:src/main/agent-hooks/server/testing/http/server-transport-interference.test.ts
1 test-no-real-time-wait:src/main/agent-hooks/server/testing/main-agent/server-turn-ended-at.test.ts
1 test-no-real-time-wait:src/main/agent-hooks/server/testing/main-agent/server-turn-started-at.test.ts
1 test-no-real-time-wait:src/main/ai-vault/scanners/opencode/reader-transport/session-scanner-opencode-sqlite-process.test.ts
1 test-no-real-time-wait:src/main/antigravity/windows-hook-json-post.test.ts
1 test-no-real-time-wait:src/main/antigravity/windows-hook-payload-delivery.test.ts
@@ -184,7 +183,6 @@
3 test-no-real-time-wait:src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx
1 test-no-real-time-wait:src/renderer/src/components/terminal-pane/layout/split/resolve-split-cwd.test.ts
2 test-no-real-time-wait:src/renderer/src/components/terminal-pane/pty-connection/emulation/fish-color-scheme-child-stdin.node-pty.test.ts
1 test-no-real-time-wait:src/renderer/src/components/terminal-pane/viewport/terminal-fit-restore.test.ts
1 test-no-real-time-wait:src/renderer/src/hooks/runtime-project-refresh-scheduler.test.ts
2 test-no-real-time-wait:src/renderer/src/store/slices/workspace-cleanup/workspace-cleanup-removal-preflight.test.ts
2 test-no-real-time-wait:src/shared/__tests__/process-host/windows-tree-kill-late-taskkill.win32.test.ts
@@ -357,7 +355,6 @@
2 test-no-wall-clock:src/main/agent-hooks/installation/commands/windows-native-hook-exe-hosts.test.ts
2 test-no-wall-clock:src/main/agent-hooks/installation/scripts/managed-hook-timeout.test.ts
1 test-no-wall-clock:src/main/agent-hooks/server/testing/http/server-endpoint-file-lifecycle.test.ts
4 test-no-wall-clock:src/main/agent-hooks/server/testing/identity/server-launch-resume-identity.test.ts
3 test-no-wall-clock:src/main/agent-hooks/server/testing/replay/spool.test.ts
3 test-no-wall-clock:src/main/agent-hooks/server/testing/structured-ingress/server-ingest-structured-status.test.ts
1 test-no-wall-clock:src/main/ai-vault/parse-cache/session-parse-cache-preview-upgrade.test.ts
@@ -383,15 +380,11 @@
1 test-no-wall-clock:src/main/daemon/transport/framing/testing/daemon-stream-binary-negotiation.test.ts
2 test-no-wall-clock:src/main/external-conversations/external-conversation-cancel-settlement.test.ts
3 test-no-wall-clock:src/main/external-conversations/external-conversation-cancel.test.ts
2 test-no-wall-clock:src/main/git/command-runner/host-routing/git-command-resolution-windows-checkout.test.ts
1 test-no-wall-clock:src/main/git/refs/maintenance/pack-refs-lock-ownership.test.ts
2 test-no-wall-clock:src/main/git/refs/maintenance/repo-ref-maintenance-real-git.test.ts
1 test-no-wall-clock:src/main/git/source-control/diffs/blob-absence-real-git.test.ts
2 test-no-wall-clock:src/main/git/worktrees/catalog/worktree-created-description-real-git.test.ts
3 test-no-wall-clock:src/main/github/pull-requests/refresh/pr-refresh-coordinator-active-visible-priority.test.ts
8 test-no-wall-clock:src/main/github/pull-requests/refresh/pr-refresh-coordinator-alias-coalescing.test.ts
3 test-no-wall-clock:src/main/github/pull-requests/refresh/pr-refresh-coordinator-completion-order.test.ts
2 test-no-wall-clock:src/main/github/pull-requests/refresh/pr-refresh-queue-growth-bound.test.ts
2 test-no-wall-clock:src/main/grok/grok-hook-stdin-no-eof.test.ts
1 test-no-wall-clock:src/main/ipc/terminal-render-desync-evidence.test.ts
2 test-no-wall-clock:src/main/ipc/worktrees-orphan-directory-cleanup.test.ts
@@ -472,7 +465,6 @@
2 test-no-wall-clock:src/main/wsl/execution/wsl-runner.wsl.test.ts
3 test-no-wall-clock:src/main/wsl/orcad/wsl-orcad-real-distro-lane.test.ts
1 test-no-wall-clock:src/renderer/src/components/activity/activity-auto-mark-read-loop.react185.test.tsx
2 test-no-wall-clock:src/renderer/src/components/artifacts/ArtifactCollection.test.tsx
1 test-no-wall-clock:src/renderer/src/components/browser-pane/assemble-chrome/browser-workspace-pane.adoption-chrome.test.tsx
1 test-no-wall-clock:src/renderer/src/components/sidebar/workspace-card/review/use-workspace-reference-details.test.tsx
1 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-list/listing/WorktreeList.lineage-agent-expansion-coupling.test.tsx
-1
View File
@@ -5,7 +5,6 @@ import original from '../mobile/vitest.config.ts'
const nodeFiles = [
'scripts/check-tests-typecheck-incremental-cache.test.ts',
'src/mock-server-key-pair.test.ts',
'src/mobile-release/prepare-android-release-script.test.ts',
'src/navigation/query-string-named-exports.test.ts',
'src/terminal/document/native-document-bundle.test.ts',
'src/transport/encryption/mobile-e2ee-v2-client-session.test.ts',
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -5,8 +5,7 @@ import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { promisify } from 'node:util'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { writeMobileWebBundleFixtureTree } from '../../build/mobile-web-app/mobile-web-bundle-fixture-tree.mjs'
import { describe, expect, it } from 'vitest'
const REPO_ROOT = join(import.meta.dirname, '../../..', '..')
@@ -71,12 +70,6 @@ describe('electron-builder config', () => {
}
})
// All-negation lists implicitly include **/*; missing examples exclusion shipped hostile-panel in 1.4.160-rc.3. Exercise real matcher, not pattern spelling.
// CI never creates ~270MB/branch out/electron-dev caches; only local packages exposed this bloat.
// Duplicated relay.js added 14MB to asar and AV quarantine gutted the entire archive (#20966); packaged app uses resources/relay.
// Bundled agent-browser answered "No skills found" because only its binary shipped (#17705).
const hostBuilderPlatform = { darwin: 'mac', linux: 'linux', win32: 'win' }[process.platform]
it.runIf(hostBuilderPlatform)(
@@ -114,84 +107,4 @@ describe('electron-builder config', () => {
}
}
)
// CLI Node mode cannot read asar; packed entry sends orca serve to Electron (#27307).
// Swept one-time /tmp asar extracts cause web-client 500s and phone bundle failures until restart (#27976).
it('validates each AppImage before electron-builder publishes it', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-electron-builder-appimage-'))
try {
const appImage = join(root, 'orca-linux.AppImage')
await writeFile(appImage, 'not an ELF')
await chmod(appImage, 0o755)
expect(() =>
electronBuilderConfig.artifactBuildCompleted({ file: appImage, arch: 1 })
).toThrow(/ELF header is outside/)
expect(() =>
electronBuilderConfig.artifactBuildCompleted({ file: join(root, 'orca-ide.deb') })
).not.toThrow()
} finally {
await rm(root, { recursive: true, force: true })
}
})
})
describe('arch-aware packaging guard', () => {
// electron-builder Arch enum: ia32=0, x64=1, armv7l=2, arm64=3.
const HOST_ARCH = process.arch === 'arm64' ? 3 : 1
// Native tests need valid mobile bundle for beforePack; use temp build, leaving bundle verification to its own suite.
let scratch
let bundleDir
beforeAll(async () => {
scratch = await mkdtemp(join(tmpdir(), 'orca-electron-builder-guard-'))
bundleDir = join(scratch, 'mobile-web')
await writeMobileWebBundleFixtureTree({ outDir: bundleDir })
})
afterAll(async () => {
await rm(scratch, { recursive: true, force: true })
})
const packHost = (arch) =>
electronBuilderConfig.beforePack(
{ electronPlatformName: process.platform, arch },
bundleDir,
() => undefined
)
it('refuses to package a dark layout-owner build', () => {
const previous = process.env.ORCA_LAYOUT_OWNER
process.env.ORCA_LAYOUT_OWNER = 'main'
try {
expect(() => packHost(HOST_ARCH)).toThrow('ORCA_LAYOUT_OWNER=main')
} finally {
if (previous === undefined) {
delete process.env.ORCA_LAYOUT_OWNER
} else {
process.env.ORCA_LAYOUT_OWNER = previous
}
}
})
it('refuses to package an orcad template marked by a dark test build', async () => {
const templateDir = join(scratch, 'orcad-template')
await mkdir(templateDir, { recursive: true })
await writeFile(join(templateDir, 'ORCA_LAYOUT_OWNER_MAIN'), 'dark\n')
try {
expect(() =>
electronBuilderConfig.beforePack(
{ electronPlatformName: process.platform, arch: HOST_ARCH },
bundleDir,
() => undefined,
templateDir
)
).toThrow(`${templateDir} holds an ORCA_LAYOUT_OWNER=main build`)
} finally {
await rm(templateDir, { recursive: true, force: true })
}
})
// The host's own build is the foreign-arch case: a Windows checkout compiles these for its CPU only.
})
@@ -15,7 +15,6 @@ const draft = { id: 259, draft: true, tag_name: tag, body: 'original notes' }
function fixture(options = {}) {
const writes = []
const reads = []
let wrote = false
const response = (body, status = 200) => ({
ok: status === 200,
@@ -27,7 +26,6 @@ function fixture(options = {}) {
const fetchImpl = async (url, request = {}) => {
const route = url.replace('https://api.github.com/repos/test/repo', '')
const method = request.method ?? 'GET'
reads.push(route)
if (route === `/git/ref/tags/${tag}`) {
if (options.missing || (wrote && options.deleted)) {
return response({}, 404)
@@ -72,7 +70,6 @@ function fixture(options = {}) {
}
return {
writes,
reads,
run: () =>
createDraftRelease({
repo: 'test/repo',
@@ -114,14 +111,6 @@ describe('release draft source and uploader contract', () => {
}
)
it('peels the captured tag object ID instead of re-dereferencing the tag name', async () => {
const f = fixture({ annotated: true })
await f.run()
expect(f.reads).toContain(`/git/tags/${tagObject}`)
expect(f.reads).toContain(`/git/commits/${checkout}`)
expect(f.reads.some((route) => route.startsWith('/commits/'))).toBe(false)
})
it.each([
{ missing: true },
{ annotated: true, commit: 'e'.repeat(40) },
@@ -187,31 +176,10 @@ describe('release draft source and uploader contract', () => {
const workflow = parse(readFileSync('.github/workflows/release-cut.yml', 'utf8'))
describe('release-cut draft wiring', () => {
it('passes the approved source and only the job token to the helper', () => {
const job = workflow.jobs['create-release']
const step = job.steps.find((s) => s.id === 'draft')
expect(step.env).toMatchObject({
SOURCE_SHA: '${{ needs.cut.outputs.source_sha }}',
GH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
})
expect(job.outputs.tag_object).toBe('${{ steps.draft.outputs.tag_object }}')
})
it('isolates both real-process skill release suites', () => {
for (const name of ['skill-sharing-release-gate', 'skill-sharing-linux-floor-release-gate']) {
const step = workflow.jobs[name].steps.find((s) => s.name?.startsWith('Run skill package'))
expect(step.env.ORCA_VITEST_ISOLATE_EVERY_FILE).toBe('1')
}
})
const steps = workflow.jobs['publish-release'].steps
const guardIndex = steps.findIndex(
(s) => s.name === 'Verify release tag still matches the drafted source'
)
it('re-checks the tag immediately before undrafting', () => {
expect(steps[guardIndex + 1].name).toBe('Publish release')
expect(workflow.jobs['publish-release'].needs).toContain('create-release')
})
it.skipIf(process.platform === 'win32').each([
['lightweight tag unchanged', { type: 'commit', sha: checkout }, checkout, 0],
@@ -110,14 +110,11 @@ const NODE_RUNTIME_INCLUDE = [
'src/main/pty/process-retirement/posix-pty-process-groups.integration.test.ts',
'src/shared/remote-runtime/ephemeral-vm-recipes/ephemeral-vm-recipe-output-capture.test.ts',
'src/main/native-chat/agent-session-journal/**/*.test.ts',
'src/main/runtime/agent-session/records/agent-session-conversation-name-store.test.ts',
'src/main/runtime/rpc/methods/structured-sessions/attachments/structured-agent-session-visual.test.ts',
// Bun resolves 'ws' to its built-in client, which bypasses the patched package.
'src/main/runtime/rpc/transports/websocket/ws-empty-payload-frame-write.test.ts',
'src/shared/remote-runtime/remote-runtime-client.test.ts',
'src/shared/remote-runtime/remote-runtime-connect-bound.test.ts',
// Bun's built-in ws client ignores maxPayload; the product opener only runs on Node.
'src/shared/remote-runtime/requests/remote-runtime-request-websocket.test.ts',
'src/shared/remote-runtime/requests/remote-runtime-subscription-connect-bound.test.ts',
'src/main/codex/configuration/shared-server/codex-shared-server-probe.test.ts',
'src/main/opencode/model-launch/opencode-launch-model-context-supervision.test.ts',
@@ -166,7 +166,6 @@ export const WINDOWS_BOUNDARY_TESTS = [
'src/shared/agent-launch/startup/omp-fresh-launch-cmd.win32.test.ts',
'src/shared/windows-batch-spawn.win32.test.ts',
'src/main/claude/structured/launch/claude-structured-child-env.test.ts',
'src/main/agent-hooks/installation/commands/windows-hook-payload-delivery.test.ts',
'src/main/jcode/hook-gate-script.test.ts',
'src/main/agent-hooks/installation/commands/windows-native-hook-exe-hosts.test.ts',
'src/main/agent-hooks/installation/commands/windows-batch-hook-launcher.test.ts',
@@ -1,55 +0,0 @@
import { createElement, useState } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { BottomDrawer } from './BottomDrawer'
import { MountedBottomDrawer } from './mounted-bottom-drawer'
import { useBottomDrawerFollowUp } from './use-bottom-drawer-follow-up'
// BottomDrawer also carries the option pager, which loads these at import.
vi.mock('react-native', () => ({ StyleSheet: { create: <T,>(sheet: T) => sheet }, View: 'View' }))
vi.mock('react-native-reanimated', async () =>
(await import('../../test-support/reanimated-pager-stub')).reanimatedPagerStub()
)
vi.mock('./mounted-bottom-drawer', () => ({ MountedBottomDrawer: 'MountedBottomDrawer' }))
describe('drawer handoff', () => {
let renderer: ReactTestRenderer | undefined
let handoff: ReturnType<typeof useBottomDrawerFollowUp> | undefined
function Harness() {
const [visible, setVisible] = useState(true)
handoff = useBottomDrawerFollowUp(() => setVisible(false))
return (
<BottomDrawer visible={visible} {...handoff.drawerProps}>
Source
</BottomDrawer>
)
}
function mount() {
act(() => {
renderer = create(createElement(Harness))
})
}
afterEach(() => act(() => renderer?.unmount()))
it('opens the next surface once, after the source drawer has unmounted', () => {
mount()
const next = vi.fn(() => expect(renderer?.toJSON()).toBeNull())
act(() => handoff?.closeThen(next))
expect(next).not.toHaveBeenCalled()
expect(renderer?.toJSON()).not.toBeNull()
const onHidden = renderer?.root.findByType(MountedBottomDrawer).props.onHidden
act(() => onHidden())
act(() => onHidden())
expect(next).toHaveBeenCalledTimes(1)
})
it('drops queued work when its screen unmounts', () => {
mount()
const next = vi.fn()
const afterClose = handoff?.drawerProps.onAfterClose
act(() => handoff?.closeThen(next))
act(() => renderer?.unmount())
act(() => afterClose?.())
expect(next).not.toHaveBeenCalled()
})
})
@@ -1,40 +0,0 @@
import { describe, expect, it } from 'vitest'
import { boundConnectionDiagnosticsReport } from './connection-diagnostics-submission'
describe('submitConnectionDiagnostics', () => {
it('preserves the newest complete events when bounding a UTF-8 report', () => {
const report = [
'Orca Mobile connection diagnostics',
'State: reconnecting',
'',
'Recent connection history (3 events, oldest first):',
`oldest ${'😀'.repeat(20)}`,
`middle ${'😀'.repeat(20)}`,
`newest ${'😀'.repeat(20)}`
].join('\n')
const bounded = boundConnectionDiagnosticsReport(report, 180)
expect(new TextEncoder().encode(bounded).byteLength).toBeLessThanOrEqual(180)
expect(bounded).toContain('newest')
expect(bounded).not.toContain('oldest')
expect(bounded).toMatch(/older omitted/)
})
it('retains a bounded form of the newest event when that event exceeds the budget', () => {
const report = [
'Orca Mobile connection diagnostics',
'State: reconnecting',
'',
'Recent connection history (2 events, oldest first):',
'old event',
`newest oversized ${'😀'.repeat(1_000)}`
].join('\n')
const bounded = boundConnectionDiagnosticsReport(report, 220)
expect(new TextEncoder().encode(bounded).byteLength).toBeLessThanOrEqual(220)
expect(bounded).toContain('newest oversized')
expect(bounded).toContain('[truncated]')
})
})
@@ -1,95 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { AccountProviderRateLimits, AccountsSnapshot } from '../../accounts/accounts-snapshot'
import { selectHomeUsageCards } from './home-usage-cards'
function limits(
provider: 'claude' | 'codex',
usedPercent: number,
overrides: Partial<AccountProviderRateLimits> = {}
): AccountProviderRateLimits {
return {
provider,
session: { usedPercent, windowMinutes: 300, resetsAt: null, resetDescription: null },
weekly: null,
updatedAt: 1_000,
error: null,
status: 'ok',
...overrides
}
}
function snapshot(overrides: {
claude?: Partial<AccountsSnapshot['claude']>
codex?: Partial<AccountsSnapshot['codex']>
rateLimits?: Partial<AccountsSnapshot['rateLimits']>
}): AccountsSnapshot {
return {
claude: { accounts: [], activeAccountId: null, ...overrides.claude },
codex: { accounts: [], activeAccountId: null, ...overrides.codex },
rateLimits: {
claude: null,
codex: null,
claudeTarget: { runtime: 'host', wslDistro: null },
codexTarget: { runtime: 'host', wslDistro: null },
inactiveClaudeAccounts: [],
inactiveCodexAccounts: [],
...overrides.rateLimits
}
}
}
describe('selectHomeUsageCards', () => {
it('lists the active managed account first, with no System default, and inactive usage from its own fetch', () => {
const cards = selectHomeUsageCards(
snapshot({
claude: {
accounts: [
{ id: 'a', email: 'a@example.com', organizationName: 'Acme' },
{ id: 'b', email: 'b@example.com' }
],
activeAccountId: 'b'
},
rateLimits: {
claude: limits('claude', 70),
inactiveClaudeAccounts: [
{ accountId: 'a', rateLimits: limits('claude', 10), updatedAt: 1, isFetching: true }
]
}
}),
'claude'
)
expect(
cards.map((card) => [
card.email,
card.subtitle,
card.active,
card.limits?.session?.usedPercent,
card.fetching
])
).toEqual([
['b@example.com', null, true, 70, false],
['a@example.com', 'Acme', false, 10, true]
])
})
it('reads the Codex active account for the rate-limit runtime target', () => {
const cards = selectHomeUsageCards(
snapshot({
codex: {
accounts: [
{ id: 'host', email: 'host@example.com', updatedAt: 1, workspaceLabel: 'Team' },
{ id: 'wsl', email: 'wsl@example.com', updatedAt: 1 }
],
activeAccountId: 'host',
activeAccountIdsByRuntime: { host: 'host', wsl: { Ubuntu: 'wsl' } }
},
rateLimits: { codexTarget: { runtime: 'wsl', wslDistro: 'Ubuntu' } }
}),
'codex'
)
expect(cards.map((card) => [card.accountId, card.subtitle, card.active])).toEqual([
['wsl', null, true],
['host', 'Team', false]
])
})
})
@@ -1,70 +0,0 @@
import { createElement } from 'react'
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
import { useActiveWorktreeScroll } from './use-active-worktree-scroll'
type Row = { row: { worktreeId: string; isActive?: boolean } }
type Sections = { key: string; data: Row[] }[]
const listOf = (active: string | null, folded = false): Sections => [
{
key: 'all',
data: folded
? []
: ['a', 'b', 'c'].map((worktreeId) => ({
row: { worktreeId, isActive: worktreeId === active }
}))
}
]
const NONE = new Set<string>()
const FOLDED = new Set(['all'])
function mount(sections: Sections, collapsedGroups: ReadonlySet<string> = NONE) {
const scrollToLocation = vi.fn()
function Probe(props: { sections: Sections; collapsedGroups: ReadonlySet<string> }): null {
const { sectionListRef } = useActiveWorktreeScroll(props.sections, props.collapsedGroups)
Object.assign(sectionListRef, { current: { scrollToLocation } })
return null
}
let renderer: ReactTestRenderer | null = null
act(() => {
renderer = create(createElement(Probe, { sections, collapsedGroups }))
})
const update = (next: Sections, nextCollapsed: ReadonlySet<string> = NONE) =>
act(() => {
renderer?.update(createElement(Probe, { sections: next, collapsedGroups: nextCollapsed }))
})
return { scrollToLocation, update }
}
describe('useActiveWorktreeScroll', () => {
it('scrolls to the highlighted row, and again when another workspace is highlighted', () => {
const list = mount(listOf('b'))
expect(list.scrollToLocation).toHaveBeenLastCalledWith(
expect.objectContaining({ sectionIndex: 0, itemIndex: 1 })
)
list.update(listOf('c'))
expect(list.scrollToLocation).toHaveBeenLastCalledWith(
expect.objectContaining({ itemIndex: 2 })
)
expect(list.scrollToLocation).toHaveBeenCalledTimes(2)
})
it('never scrolls when a fold or search hides the row and it comes back', () => {
const list = mount(listOf('b'))
list.update(listOf('b', true), FOLDED)
list.update(listOf('b'), NONE)
list.update(listOf(null))
list.update(listOf('b'))
expect(list.scrollToLocation).toHaveBeenCalledTimes(1)
})
it('never scrolls to a highlight an unfold first reveals', () => {
const list = mount(listOf('b', true), FOLDED)
list.update(listOf('b'), NONE)
expect(list.scrollToLocation).not.toHaveBeenCalled()
list.update(listOf('c'), NONE)
expect(list.scrollToLocation).toHaveBeenCalledTimes(1)
})
})
@@ -1,83 +0,0 @@
import { spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
const scriptPath = fileURLToPath(
new URL('../../scripts/prepare-android-release.mjs', import.meta.url)
)
const appConfig = {
expo: {
version: '0.0.22',
android: {
versionCode: 4
}
}
}
let tempDirs: string[] = []
function createAppConfig() {
const dir = mkdtempSync(join(tmpdir(), 'orca-android-release-'))
tempDirs.push(dir)
const configPath = join(dir, 'app.json')
const contents = `${JSON.stringify(appConfig, null, 2)}\n`
writeFileSync(configPath, contents)
const outputPath = join(dir, 'github-output')
writeFileSync(outputPath, '')
return { configPath, contents, outputPath }
}
function scenarioEnv(configPath: string, outputPath: string, inputs: Record<string, string>) {
const env = { ...process.env }
for (const key of Object.keys(env)) {
if (key.startsWith('MOBILE_ANDROID_') || key === 'GITHUB_REF' || key === 'GITHUB_OUTPUT') {
delete env[key]
}
}
return { ...env, MOBILE_APP_CONFIG_PATH: configPath, GITHUB_OUTPUT: outputPath, ...inputs }
}
describe('prepare Android release script', () => {
afterEach(() => {
for (const dir of tempDirs) {
rmSync(dir, { force: true, recursive: true })
}
tempDirs = []
})
it('rejects release-only Android versionCode bumps', () => {
const { configPath, contents, outputPath } = createAppConfig()
const result = spawnSync(process.execPath, [scriptPath], {
encoding: 'utf8',
env: scenarioEnv(configPath, outputPath, { MOBILE_ANDROID_BUMP_VERSION_CODE: 'true' })
})
expect(result.status).toBe(1)
expect(readFileSync(configPath, 'utf8')).toBe(contents)
expect(readFileSync(outputPath, 'utf8')).toBe('')
expect(result.stderr).toContain(
'Android versionCode changes must be committed in mobile/app.json before release'
)
})
it('rejects release versions that do not match committed app config', () => {
const { configPath, contents, outputPath } = createAppConfig()
const result = spawnSync(process.execPath, [scriptPath], {
encoding: 'utf8',
env: scenarioEnv(configPath, outputPath, { MOBILE_ANDROID_RELEASE_VERSION: '0.0.23' })
})
expect(result.status).toBe(1)
expect(readFileSync(configPath, 'utf8')).toBe(contents)
expect(readFileSync(outputPath, 'utf8')).toBe('')
expect(result.stderr).toContain(
'MOBILE_ANDROID_RELEASE_VERSION must match the committed mobile app version'
)
})
})
@@ -1,146 +0,0 @@
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
import { runProcess } from '@orca/process-host'
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
import { removeTreeSync } from '../../../../shared/windows-transient-lock-removal'
import { tmpdir } from 'node:os'
import { basename, join } from 'node:path'
import type * as osModule from 'node:os'
const { homedirMock } = vi.hoisted(() => ({ homedirMock: vi.fn<() => string>() }))
vi.mock('electron', () => ({
app: {
getPath: () => '/tmp/orca-user-data'
}
}))
vi.mock('os', async (importOriginal) => {
const actual = await importOriginal<typeof osModule>()
return {
...actual,
homedir: homedirMock.mockImplementation(actual.homedir)
}
})
import { CopilotHookService } from '../../../copilot/hook-service'
import {
WINDOWS_POWERSHELL_WARM_UP_TIMEOUT_MS,
warmWindowsEncodedPowerShell
} from './testing/windows-powershell-warm-up.test-fixture'
// Multiple chunks and non-ASCII expose pipe truncation and code-page conversion.
const PAYLOAD = JSON.stringify({
session_id: 'a3f9c1e0-payload-delivery',
hook_event_name: 'PreToolUse',
tool_name: 'Bash',
tool_input: { command: 'echo "café 日本語 😀" & echo %PATH% | more' },
transcript: 'y'.repeat(4096)
})
type Runner = {
name: string
program: () => string
args: (command: string) => string[]
verbatim?: boolean
}
const SYSTEM32 = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
const CMD = join(SYSTEM32, 'cmd.exe')
const POWERSHELL = join(SYSTEM32, 'WindowsPowerShell', 'v1.0', 'powershell.exe')
function findPwsh(): string | undefined {
const candidates = [
join(process.env.ProgramFiles ?? 'C:\\Program Files', 'PowerShell', '7', 'pwsh.exe'),
join(process.env.LOCALAPPDATA ?? '', 'Microsoft', 'WindowsApps', 'pwsh.exe')
]
// lstat: the Store install is an app-execution alias that existsSync cannot follow.
return candidates.find((candidate) => lstatSync(candidate, { throwIfNoEntry: false }))
}
// Copilot's `<pwsh|powershell> -nop -nol -c <command>`; pwsh only where it is installed.
const COPILOT_RUNNERS: Runner[] = [POWERSHELL, findPwsh()].flatMap((program) =>
program
? [
{
name: `${basename(program)} (Copilot)`,
program: () => program,
args: (c: string) => ['-nop', '-nol', '-c', c]
}
]
: []
)
type Entry = { command?: string; powershell?: string; hooks?: { command?: string }[] }
const commandsOf = (entries: Entry[] | undefined): (string | undefined)[] =>
(entries ?? []).flatMap((entry) =>
entry.hooks ? entry.hooks.map((h) => h.command) : (entry.command ?? entry.powershell)
)
function readEvent(path: string, event: string): (string | undefined)[] {
return commandsOf(JSON.parse(readFileSync(path, 'utf8')).hooks?.[event])
}
// Inherited Orca endpoints must not redirect test payloads away from this listener.
function hookEnvironment(extra: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const base = Object.fromEntries(
Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('ORCA_'))
)
return { ...base, ORCA_BACKGROUND_LAUNCH: '1', ...extra }
}
describe.skipIf(process.platform !== 'win32')('Windows managed hook payload delivery', () => {
let root = ''
beforeAll(
() => warmWindowsEncodedPowerShell(hookEnvironment({})),
WINDOWS_POWERSHELL_WARM_UP_TIMEOUT_MS
)
afterEach(() => {
vi.unstubAllEnvs()
homedirMock.mockImplementation(() => process.env.HOME ?? tmpdir())
if (root) {
removeTreeSync(root)
root = ''
}
})
// The control verifies profile-based AutoRun ran; printing it must not corrupt Copilot gate JSON.
it('Copilot answers exactly {} when the cmd AutoRun prints', async () => {
root = mkdtempSync(join(tmpdir(), 'orca-hook-autorun-'))
const home = join(root, 'user')
mkdirSync(home)
homedirMock.mockReturnValue(home)
for (const name of ['USERPROFILE', 'HOME']) {
vi.stubEnv(name, home)
}
vi.stubEnv('COPILOT_HOME', undefined)
writeFileSync(join(home, '.cmd_aliases.cmd'), '@echo autorun-noise\r\n', 'utf8')
expect(new CopilotHookService().install().state).toBe('installed')
const [command] = readEvent(join(home, '.copilot', 'hooks', 'orca.json'), 'PreToolUse')
if (!command) {
throw new Error('Missing managed Copilot PreToolUse command')
}
const env = hookEnvironment({ USERPROFILE: home, HOME: home })
const autoRun = await runProcess({ program: CMD, args: ['/c', 'rem'], env, timeoutMs: 15_000 })
for (const runner of COPILOT_RUNNERS) {
const result = await runProcess({
program: runner.program(),
args: runner.args(command),
env,
input: PAYLOAD,
timeoutMs: 15_000
})
expect(result.code, `${runner.name} exit code`).toBe(0)
expect(result.stdout, `${runner.name} stdout`).toBe('{}\r\n')
}
if (autoRun.stdout.includes('autorun-noise')) {
const bare = command.replace(/^\S+\/cmd\.exe \/d \/c /, '')
const control = await runProcess({
program: POWERSHELL,
args: ['-nop', '-nol', '-c', bare],
env,
input: PAYLOAD,
timeoutMs: 15_000
})
expect(control.stdout, 'without /d the AutoRun prints first').toContain('autorun-noise')
}
}, 60_000)
})
@@ -1,59 +0,0 @@
import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { ManagedAgentHookTarget } from '../../../shared/managed-agent-hook-targets'
import { detectLocalManagedAgentCliPresence } from './local-agent-cli-presence'
const { mergePersistedWindowsPathAsyncMock } = vi.hoisted(() => ({
mergePersistedWindowsPathAsyncMock: vi.fn(async (_env: NodeJS.ProcessEnv) => undefined)
}))
vi.mock('../../pty/environment/windows-environment-path', () => ({
mergePersistedWindowsPathAsync: mergePersistedWindowsPathAsyncMock
}))
const codexTarget: ManagedAgentHookTarget = {
agent: 'codex',
tuiAgent: 'codex',
executableCandidates: ['codex']
}
const claudeTarget: ManagedAgentHookTarget = {
agent: 'claude',
tuiAgent: 'claude',
executableCandidates: ['claude']
}
describe('detectLocalManagedAgentCliPresence', () => {
let tmpDir: string | null = null
afterEach(() => {
if (tmpDir) {
rmSync(tmpDir, { recursive: true, force: true })
tmpDir = null
}
})
it.runIf(process.platform !== 'win32')(
'accepts executable symlinks and rejects broken symlinks',
async () => {
tmpDir = mkdtempSync(join(tmpdir(), 'orca-cli-presence-'))
const binDir = join(tmpDir, 'bin')
mkdirSync(binDir)
const targetPath = join(tmpDir, 'codex-real')
writeFileSync(targetPath, '#!/bin/sh\n')
chmodSync(targetPath, 0o755)
symlinkSync(targetPath, join(binDir, 'codex'))
symlinkSync(join(tmpDir, 'missing'), join(binDir, 'claude'))
const result = await detectLocalManagedAgentCliPresence(
[codexTarget, claudeTarget],
{ agentCmdOverrides: {} },
{ pathEnv: binDir, pathDelimiter: ':', platform: process.platform }
)
expect(result.codex?.state).toBe('found')
expect(result.claude?.state).toBe('missing')
}
)
})
@@ -1,45 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
vi.mock('electron', () => ({ app: { getPath: () => '/tmp/userData' } }))
import { getManagedScript as getClaudeManagedScript } from '../../../claude/hooks/hook-script'
import { getManagedScript as getCursorManagedScript } from '../../../cursor/hook-script'
import { runVendorHook } from './testing/vendor-hook-delivery.test-fixture'
const CLAUDE_SCRIPT_OPTIONS = {
skipWhenDevinImportsClaude: true,
skipWhenGrokImportsClaude: true
}
describe('Grok vendor hook replay guard', () => {
it('drops Grok-replayed hooks without suppressing their protocol response', async () => {
for (const script of [
getClaudeManagedScript('local', CLAUDE_SCRIPT_OPTIONS),
getCursorManagedScript('local')
]) {
expect((await runVendorHook(script, { GROK_HOOK_EVENT: 'Stop' })).requests).toEqual([])
}
})
it.skipIf(process.platform === 'win32')(
'keeps Grok-replayed hooks out of the spool',
async () => {
for (const [source, script] of [
['claude', getClaudeManagedScript('posix', CLAUDE_SCRIPT_OPTIONS)],
['cursor', getCursorManagedScript('posix')]
]) {
const ordinary = await runVendorHook(script, { GROK_HOOK_EVENT: '' }, 'spool')
expect(ordinary.requests).toEqual([])
expect(ordinary.spooled).toHaveLength(1)
expect(JSON.parse(ordinary.spooled[0])).toMatchObject({
source,
paneKey: 'tab:leaf',
payload: { hook_event_name: 'Stop' }
})
const replayed = await runVendorHook(script, { GROK_HOOK_EVENT: 'Stop' }, 'spool')
expect(replayed.requests).toEqual([])
expect(replayed.spooled).toEqual([])
}
}
)
})
@@ -1,157 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as CodexHookHashLookup from '../../../codex/trust/hooks/codex-hook-hash-lookup'
import {
existsSync,
chmodSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
utimesSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { spawnSync } from 'node:child_process'
import { join } from 'node:path'
import type * as osModule from 'node:os'
let isolatedUserDataDir = ''
let previousUserDataPath: string | undefined
beforeEach(() => {
vi.stubEnv('GROK_HOME', undefined)
vi.stubEnv('KIMI_CODE_HOME', undefined)
vi.stubEnv('COPILOT_HOME', undefined)
vi.stubEnv('KIRO_HOME', undefined)
vi.stubEnv('XDG_CONFIG_HOME', undefined)
previousUserDataPath = process.env.ORCA_USER_DATA_PATH
isolatedUserDataDir = mkdtempSync(join(tmpdir(), 'orca-hook-refresh-user-data-'))
process.env.ORCA_USER_DATA_PATH = isolatedUserDataDir
})
afterEach(() => {
if (previousUserDataPath === undefined) {
delete process.env.ORCA_USER_DATA_PATH
} else {
process.env.ORCA_USER_DATA_PATH = previousUserDataPath
}
rmSync(isolatedUserDataDir, { recursive: true, force: true })
vi.unstubAllEnvs()
})
const { homedirMock } = vi.hoisted(() => ({
homedirMock: vi.fn<() => string>()
}))
vi.mock('../../../codex/trust/hooks/codex-hook-hash-lookup', async (importOriginal) => ({
...(await importOriginal<typeof CodexHookHashLookup>()),
resolveCodexHookAnswerForLaunch: async () =>
(
await import('../../../codex/hooks/installation/hook-service-test-harness')
).codexHookAnswerForTests()
}))
vi.mock('electron', () => ({
app: {
getPath: () => '/tmp/orca-user-data'
}
}))
vi.mock('os', async (importOriginal) => {
const actual = await importOriginal<typeof osModule>()
return {
...actual,
homedir: homedirMock.mockImplementation(actual.homedir)
}
})
import { refreshManagedScriptIfPresent } from './managed-hook-script-refresh'
import { ClaudeHookService } from '../../../claude/hook-service'
describe('refreshManagedScriptIfPresent', () => {
it('rewrites an existing script and refuses to create a missing one', async () => {
const dir = mkdtempSync(join(tmpdir(), 'orca-hook-refresh-unit-'))
try {
const present = join(dir, 'present.cmd')
writeFileSync(present, 'stale')
expect(await refreshManagedScriptIfPresent(present, 'fresh')).toBe(true)
expect(readFileSync(present, 'utf8')).toBe('fresh')
if (process.platform !== 'win32') {
chmodSync(present, 0o600)
}
const fixedTime = new Date(1_000)
utimesSync(present, fixedTime, fixedTime)
expect(await refreshManagedScriptIfPresent(present, 'fresh')).toBe(true)
expect(statSync(present).mtimeMs).toBe(fixedTime.getTime())
if (process.platform !== 'win32') {
expect(statSync(present).mode & 0o777).toBe(0o755)
}
const missing = join(dir, 'missing.cmd')
expect(await refreshManagedScriptIfPresent(missing, 'fresh')).toBe(false)
expect(existsSync(missing)).toBe(false)
} finally {
rmSync(dir, { recursive: true, force: true })
}
})
})
describe('managed hook script refresh', () => {
it.skipIf(process.platform === 'win32')(
'refreshes existing POSIX hook and statusline locale handling without a CLI config',
async () => {
const home = mkdtempSync(join(tmpdir(), 'orca-hook-locale-refresh-'))
homedirMock.mockReturnValue(home)
try {
const hooksDir = join(home, '.orca', 'agent-hooks')
mkdirSync(hooksDir, { recursive: true })
for (const name of ['claude-hook.sh', 'claude-statusline.sh']) {
writeFileSync(join(hooksDir, name), '#!/bin/sh\ncurl --connect-timeout 0.5\n')
}
await new ClaudeHookService().refreshManagedScripts()
for (const name of ['claude-hook.sh', 'claude-statusline.sh']) {
const observed = join(home, `${name}.locale`)
const result = spawnSync(
'/bin/sh',
[
'-c',
'curl() { printf "%s|%s" "$LC_ALL" "$LC_NUMERIC" > "$ORCA_TEST_CURL_LOCALE"; command -p cat >/dev/null; }; . "$1"',
'hook',
join(hooksDir, name)
],
{
input: '{"hook_event_name":"Stop","rate_limits":{},"total_duration_ms":1000}',
encoding: 'utf8',
timeout: 5_000,
env: {
...process.env,
LC_ALL: 'de_DE.UTF-8',
LC_NUMERIC: 'de_DE.UTF-8',
TMPDIR: home,
CLAUDE_JOB_DIR: '',
ORCA_AGENT_HOOK_ENDPOINT: '',
ORCA_AGENT_HOOK_PORT: '1',
ORCA_AGENT_HOOK_TOKEN: 'test-token',
ORCA_PANE_KEY: 'pane-1',
ORCA_TEST_CURL_LOCALE: observed
}
}
)
expect(result.error, name).toBeUndefined()
expect(result.status, name).toBe(0)
expect(existsSync(observed), `${name} delivered to curl`).toBe(true)
expect(readFileSync(observed, 'utf8'), name).toBe('|C')
expect(statSync(join(hooksDir, name)).mode & 0o777).toBe(0o755)
}
expect(existsSync(join(home, '.claude'))).toBe(false)
expect(readdirSync(hooksDir).sort()).toEqual(['claude-hook.sh', 'claude-statusline.sh'])
} finally {
homedirMock.mockImplementation(() => process.env.HOME ?? tmpdir())
rmSync(home, { recursive: true, force: true })
}
}
)
})
@@ -1,37 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
createLocalPtyInventoryReader,
createLocalTmuxManagedPtyResolver
} from './local-tmux-managed-pty'
import type { PtyProcessInfo } from '../../providers/pty/process-info'
const row: PtyProcessInfo = {
id: 'pty',
incarnationId: 'generation',
rootProcessId: 123,
cwd: '/workspace',
title: 'shell',
worktreeId: 'folder:workspace'
}
describe.skipIf(process.platform === 'win32')('authenticated local tmux root resolution', () => {
it('refuses a binding changed during capture and excludes WSL roots from a native probe', async () => {
let binding = 'pty'
const resolve = createLocalTmuxManagedPtyResolver(
createLocalPtyInventoryReader({
getPtyId: () => binding,
listProcesses: async () => {
binding = 'replacement'
return [row]
}
})
)
expect(await resolve('pane')).toBeNull()
const wsl = createLocalTmuxManagedPtyResolver(
createLocalPtyInventoryReader({
getPtyId: () => 'pty',
listProcesses: async () => [{ ...row, wslDistro: 'Ubuntu-24.04' }]
})
)
expect(await wsl('pane')).toBeNull()
})
})
@@ -1,82 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer } from '../../agent-hook-server'
import { PANE } from '../server.test-fixtures'
vi.mock('../../../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn() }))
afterEach(() => vi.restoreAllMocks())
const working = { agentType: 'omp', state: 'working', prompt: 'new turn' }
describe('OMP retired-pane remote ingress', () => {
it('recovers a legacy source-less OMP new turn using validated payload identity', () => {
const server = new AgentHookServer()
server.retirePaneAuthority(PANE)
server.ingestRemote(
{ paneKey: PANE, hookEventName: 'before_agent_start', payload: working },
'ssh'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
observation: expect.objectContaining({ boundary: true })
})
])
})
it.each([
{ payload: { ...working, state: 'invalid' } },
{ payload: { ...working, agentType: 'claude' } },
{ isReplay: 'true' },
{ isReplay: null },
{ launchToken: 42 },
{ providerSessionOnly: true }
])('rejected metadata leaves retirement intact: %j', (invalid) => {
const server = new AgentHookServer()
server.retirePaneAuthority(PANE)
const input = {
paneKey: PANE,
source: 'omp',
hookEventName: 'before_agent_start',
payload: working,
...invalid
}
// Exercise raw JSON ingress, including malformed fields a typed caller cannot create.
server.ingestRemote(JSON.parse(JSON.stringify(input)), 'ssh')
expect(server.getStatusSnapshot()).toEqual([])
server.ingestRemote(
{
paneKey: PANE,
source: 'omp',
hookEventName: 'agent_end',
payload: { ...working, state: 'done' }
},
'ssh'
)
expect(server.getStatusSnapshot()).toEqual([])
server.ingestRemote(
{ paneKey: PANE, source: 'omp', hookEventName: 'before_agent_start', payload: working },
'ssh'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({ paneKey: PANE, state: 'working' })
])
})
it('a replay cannot restore a retired OMP pane', () => {
const server = new AgentHookServer()
server.retirePaneAuthority(PANE)
server.ingestRemote(
{ paneKey: PANE, hookEventName: 'before_agent_start', isReplay: true, payload: working },
'ssh'
)
expect(server.getStatusSnapshot()).toEqual([])
server.ingestRemote(
{ paneKey: PANE, source: 'omp', hookEventName: 'before_agent_start', payload: working },
'ssh'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({ paneKey: PANE, state: 'working' })
])
})
})
@@ -1,159 +0,0 @@
import { readSavedStatus, hasSavedStatus } from '../persistence/last-status.test-fixtures'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, GOOD_PANE, postHookEvent, PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({
track: trackMock
}))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
const REMINTED = '$$MFRGGZDFMY:L$$'
const FOREIGN = '$$ONXW2ZJAON:L$$'
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('reminted $$ pane keys on the OMP hook pipeline', () => {
let userDataPath: string
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-reminted-pane-'))
})
afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
})
function lastStatusPath(): string {
return join(userDataPath, 'agent-hooks', 'last-status.json')
}
it('routes a reminted $$ key to its canonical pane and settles at done', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
server.registerPaneKeyAlias(REMINTED, PANE, 'pty-remint')
const start = await postHookEvent(
server,
buildBody(
{ hook_event_name: 'before_agent_start', prompt: 'finish the reminted pane' },
{ paneKey: REMINTED, launchToken: 'launch-remint' }
),
'/hook/omp'
)
const end = await postHookEvent(
server,
buildBody(
{ hook_event_name: 'agent_end' },
{ paneKey: REMINTED, launchToken: 'launch-remint' }
),
'/hook/omp'
)
expect(start.status).toBe(204)
expect(end.status).toBe(204)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
tabId: 'tab-1',
state: 'done',
prompt: 'finish the reminted pane',
agentType: 'omp'
})
])
server.flushStatusPersistSync()
expect(hasSavedStatus(lastStatusPath())).toBe(true)
const file: {
entries: Record<string, { payload: { state: string } }>
} = JSON.parse(readSavedStatus(lastStatusPath()))
expect(file.entries[PANE]?.payload.state).toBe('done')
expect(file.entries[REMINTED]).toBeUndefined()
} finally {
server.stop()
}
})
it('does not let a foreign reminted key stamp another pane', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
server.registerPaneKeyAlias(REMINTED, PANE, 'pty-remint')
await postHookEvent(
server,
buildBody(
{ hook_event_name: 'before_agent_start', prompt: 'owned turn' },
{ paneKey: REMINTED }
),
'/hook/omp'
)
const foreign = await postHookEvent(
server,
buildBody(
{ hook_event_name: 'agent_end', prompt: 'stolen done' },
{ paneKey: FOREIGN, tabId: 'tab-1' }
),
'/hook/omp'
)
expect(foreign.status).toBe(204)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
state: 'working',
prompt: 'owned turn',
agentType: 'omp'
})
])
} finally {
server.stop()
}
})
it('does not rebind a reminted token onto a different pane', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
// Different spawns can share $$ tokens; stale remints must not overwrite the newer pane owner.
server.registerPaneKeyAlias(REMINTED, PANE, 'pty-a', 10, { authorityVerified: true })
server.registerPaneKeyAlias(REMINTED, GOOD_PANE, 'pty-b', 20, { authorityVerified: true })
const start = await postHookEvent(
server,
buildBody(
{ hook_event_name: 'before_agent_start', prompt: 'owned remint turn' },
{ paneKey: REMINTED, tabId: 'tab-good' }
),
'/hook/omp'
)
expect(start.status).toBe(204)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
tabId: 'tab-1',
state: 'working',
prompt: 'owned remint turn',
agentType: 'omp'
})
])
} finally {
server.stop()
}
})
})
@@ -1,174 +0,0 @@
import { readSavedStatus } from '../persistence/last-status.test-fixtures'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
import { LEAF_1, LEAF_2, LEAF_3, LEAF_4, LEAF_5, recentTs } from '../server.test-fixtures'
const REMOVED = 'repo-1::/workspace/removed'
const KEPT = 'repo-1::/workspace/kept'
const LOCAL_PANE = makePaneKey('tab-local', LEAF_1)
const WSL_PANE = makePaneKey('tab-wsl', LEAF_2)
const SSH_PANE = makePaneKey('tab-ssh', LEAF_3)
const SSH_COMMITMENT_PANE = makePaneKey('tab-ssh-idle', LEAF_4)
const OTHER_PANE = makePaneKey('tab-other', LEAF_5)
function row(paneKey: string, worktreeId: string, connectionId: string | null) {
const receivedAt = recentTs()
return {
paneKey,
tabId: paneKey.split(':')[0],
worktreeId,
connectionId,
receivedAt,
stateStartedAt: receivedAt,
payload: { state: 'working', prompt: 'stranded', agentType: 'codex' }
}
}
describe('AgentHookServer removed-worktree retirement', () => {
let userDataPath: string
const lastStatusPath = () => join(userDataPath, 'agent-hooks', 'last-status.json')
beforeEach(() => {
_internals.resetCachesForTests()
userDataPath = mkdtempSync(join(tmpdir(), 'orca-removed-worktree-'))
mkdirSync(join(userDataPath, 'agent-hooks'), { recursive: true })
writeFileSync(
lastStatusPath(),
JSON.stringify({
version: 2,
entries: {
[LOCAL_PANE]: row(LOCAL_PANE, REMOVED, null),
[WSL_PANE]: row(WSL_PANE, REMOVED, 'wsl:Ubuntu'),
[SSH_PANE]: row(SSH_PANE, REMOVED, 'user@box'),
[OTHER_PANE]: row(OTHER_PANE, KEPT, null)
}
}),
'utf8'
)
})
afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
})
it('retires only the removing host rows and commitments, then persists the pruned map', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
// An SSH commitment recorded this session outlives its row across a disconnect clear.
server.ingestRemote(
{
paneKey: SSH_COMMITMENT_PANE,
tabId: 'tab-ssh-idle',
worktreeId: REMOVED,
launchToken: 'idle-launch',
payload: { state: 'working', prompt: 'idle', agentType: 'codex' }
},
'idle@box'
)
server.clearStatusEntriesForConnection('idle@box')
const persisted = () => {
server.flushStatusPersistSync()
const file = JSON.parse(readSavedStatus(lastStatusPath()))
return {
entries: Object.keys(file.entries).sort(),
commitments: Object.keys(file.authorityCommitments ?? {})
}
}
server.dropStatusEntriesForRemovedWorktree(REMOVED, 'runtime:env-1')
expect(persisted().entries).toHaveLength(4)
server.dropStatusEntriesForRemovedWorktree(REMOVED, 'local')
expect(persisted()).toEqual({
entries: [OTHER_PANE, SSH_PANE].sort(),
commitments: [SSH_COMMITMENT_PANE]
})
server.dropStatusEntriesForRemovedWorktree(REMOVED, 'ssh:user%40box')
expect(persisted()).toEqual({ entries: [OTHER_PANE], commitments: [SSH_COMMITMENT_PANE] })
server.dropStatusEntriesForRemovedWorktree(REMOVED, 'ssh:idle%40box')
expect(persisted().commitments).toEqual([])
} finally {
server.stop()
}
})
it('fences only the retired pane, so its kept tab still reports a new agent', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
server.dropStatusEntriesForRemovedWorktree(REMOVED, 'local')
const newPane = makePaneKey('tab-local', '66666666-6666-4666-8666-666666666666')
const done = { state: 'done', prompt: 'late', agentType: 'codex' } as const
server.ingestTerminalStatus({ paneKey: LOCAL_PANE, connectionId: null, payload: done })
server.ingestTerminalStatus({ paneKey: newPane, connectionId: null, payload: done })
const panes = server.getStatusSnapshot().map((entry) => entry.paneKey)
expect(panes).toContain(newPane)
expect(panes).not.toContain(LOCAL_PANE)
} finally {
server.stop()
}
})
it.each([
{ occupant: 'the removed worktree', sshWorktree: KEPT, localWorktree: REMOVED, host: 'local' },
{
occupant: 'another owner',
sshWorktree: REMOVED,
localWorktree: KEPT,
host: 'ssh:user%40box'
}
] as const)(
'decides a reused pane by its occupant: $occupant',
async ({ sshWorktree, localWorktree, host }) => {
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
const pane = makePaneKey('tab-reused', '77777777-7777-4777-8777-777777777777')
const working = { state: 'working', prompt: 'live', agentType: 'codex' } as const
const reportLocally = (state: 'working' | 'done') =>
server.ingestTerminalStatus({
paneKey: pane,
worktreeId: localWorktree,
connectionId: null,
payload: { ...working, state }
})
server.ingestRemote(
{
paneKey: pane,
tabId: 'tab-reused',
worktreeId: sshWorktree,
launchToken: 'ssh',
payload: working
},
'user@box'
)
server.clearStatusEntriesForConnection('user@box')
reportLocally('working')
server.dropStatusEntriesForRemovedWorktree(REMOVED, host)
reportLocally('done')
server.flushStatusPersistSync()
const file = JSON.parse(readSavedStatus(lastStatusPath()))
if (localWorktree === REMOVED) {
expect(file.authorityCommitments?.[pane]).toMatchObject({ worktreeId: KEPT })
expect(file.entries[pane]).toBeUndefined()
} else {
expect(file.authorityCommitments?.[pane]).toBeUndefined()
expect(file.entries[pane]).toMatchObject({ worktreeId: KEPT, payload: { state: 'done' } })
expect(file.entries[pane].launchTokenHash).toBeUndefined()
}
} finally {
server.stop()
}
}
)
})
@@ -1,75 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { AgentHookServer } from '../../agent-hook-server'
import type { ClaudeStatusLineRateLimits } from '../../../../../shared/claude-statusline-rate-limits'
describe('AgentHookServer /statusline/claude', () => {
let server: AgentHookServer
beforeEach(async () => {
server = new AgentHookServer()
await server.start({ env: 'production' })
})
afterEach(() => {
server.stop()
})
function post(body: string, token?: string): Promise<Response> {
const env = server.buildPtyEnv()
return fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/statusline/claude`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Orca-Agent-Hook-Token': token ?? env.ORCA_AGENT_HOOK_TOKEN
},
body
})
}
it('forwards parsed rate limits to the statusline listener', async () => {
const events: ClaudeStatusLineRateLimits[] = []
server.setClaudeStatusLineListener((event) => {
events.push(event)
})
const payload = JSON.stringify({
rate_limits: {
five_hour: { used_percentage: 12.5, resets_at: 1738425600 },
seven_day: { used_percentage: 40, resets_at: 1712059200 }
}
})
const body = new URLSearchParams({
paneKey: 'pane-1',
configDir: '/home/dev/managed',
payload
}).toString()
await expect(post(body)).resolves.toMatchObject({ status: 204 })
expect(events).toEqual([
{
configDir: '/home/dev/managed',
fiveHour: { used_percentage: 12.5, resets_at: 1738425600 },
sevenDay: { used_percentage: 40, resets_at: 1712059200 }
}
])
})
it('rejects posts with a bad token and ignores payloads without rate limits', async () => {
const events: ClaudeStatusLineRateLimits[] = []
server.setClaudeStatusLineListener((event) => {
events.push(event)
})
await expect(post('payload={}', 'wrong-token')).resolves.toMatchObject({ status: 403 })
const noLimits = new URLSearchParams({
paneKey: 'pane-1',
payload: JSON.stringify({ context_window: { used_percentage: 8 } })
}).toString()
await expect(post(noLimits)).resolves.toMatchObject({ status: 204 })
await expect(post('payload=not-json')).resolves.toMatchObject({ status: 204 })
expect(events).toEqual([])
})
})
@@ -1,170 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({
track: trackMock
}))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('AgentHookServer listener replay', () => {
it('accepts raw JSON hook bodies with base64 metadata headers', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
const env = server.buildPtyEnv()
const response = await fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/hook/claude`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Orca-Agent-Hook-Token': env.ORCA_AGENT_HOOK_TOKEN,
'X-Orca-Agent-Hook-Meta-Encoding': 'base64',
'X-Orca-Agent-Hook-Meta': Buffer.from(
[PANE, 'tab-1', '', 'wt-1', 'production', ''].join('\x1f')
).toString('base64')
},
body: JSON.stringify({ hook_event_name: 'UserPromptSubmit', prompt: 'raw JSON' })
})
expect(response.status).toBe(204)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
worktreeId: 'wt-1',
state: 'working',
prompt: 'raw JSON'
})
])
} finally {
server.stop()
}
})
it('ignores local nested Claude Stop while a parent Codex hook status is active', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
const env = server.buildPtyEnv()
const listener = vi.fn()
server.setListener(listener)
const postHook = async (
source: 'codex' | 'claude',
payload: Record<string, unknown>
): Promise<void> => {
const response = await fetch(
`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/hook/${source}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Orca-Agent-Hook-Token': env.ORCA_AGENT_HOOK_TOKEN
},
body: JSON.stringify(buildBody(payload))
}
)
expect(response.status).toBe(204)
}
await postHook('codex', {
hook_event_name: 'UserPromptSubmit',
prompt: 'parent codex'
})
await postHook('claude', {
hook_event_name: 'Stop',
last_assistant_message: 'child finished'
})
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
state: 'working',
prompt: 'parent codex',
agentType: 'codex'
})
])
const snapshot = server.getStatusSnapshot()[0]
expect(snapshot.lastAssistantMessage).toBeUndefined()
expect(listener).toHaveBeenCalledTimes(1)
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({
payload: expect.objectContaining({
state: 'working',
prompt: 'parent codex',
agentType: 'codex'
})
})
)
} finally {
server.stop()
}
})
it('accepts form-encoded hook posts from Unix managed scripts', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
const env = server.buildPtyEnv()
const params = new URLSearchParams({
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'repo::/tmp/worktree with "quotes"',
env: 'production',
version: env.ORCA_AGENT_HOOK_VERSION ?? '',
payload: JSON.stringify({
hook_event_name: 'UserPromptSubmit',
prompt: 'form encoded'
})
})
const response = await fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/hook/claude`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Orca-Agent-Hook-Token': env.ORCA_AGENT_HOOK_TOKEN
},
body: params
})
expect(response.status).toBe(204)
const listener = vi.fn()
server.setListener(listener)
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'repo::/tmp/worktree with "quotes"',
connectionId: null,
receivedAt: expect.any(Number),
stateStartedAt: expect.any(Number),
payload: expect.objectContaining({
state: 'working',
prompt: 'form encoded',
agentType: 'claude'
})
})
)
} finally {
server.stop()
}
})
})
@@ -1,58 +0,0 @@
import '../memory-agent-identity-store.test-fixture'
import { mkdtemp, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { makePaneKey } from '../../../../../shared/workspace-layout/stable-pane-id'
import { AgentHookServer } from '../../agent-hook-server'
const PANE = makePaneKey('external-live', '11111111-1111-4111-8111-111111111111')
const PROVIDER_SESSION = { key: 'session_id' as const, id: 'session-live' }
function seedLocalIdentity(server: AgentHookServer): void {
server.ingestRemote(
{
paneKey: PANE,
providerSession: PROVIDER_SESSION,
payload: { state: 'working', prompt: 'test', agentType: 'gemini' }
},
'seed-connection'
)
server.ingestTerminalStatus({
paneKey: PANE,
connectionId: null,
payload: { state: 'working', prompt: 'test', agentType: 'gemini' }
})
}
describe('AgentHookServer AI Vault liveness identity', () => {
it('hydrates dismissed provider identity without reviving visible status', async () => {
const userDataPath = await mkdtemp(join(tmpdir(), 'orca-ai-vault-liveness-'))
const first = new AgentHookServer()
try {
await first.start({ env: 'production', userDataPath })
seedLocalIdentity(first)
first.dropStatusEntry(PANE)
first.flushStatusPersistSync()
first.stop()
const second = new AgentHookServer()
await second.start({ env: 'production', userDataPath })
try {
expect(second.getStatusSnapshot()).toEqual([
expect.objectContaining({
providerSession: PROVIDER_SESSION,
providerSessionOnly: true,
restoredUnconfirmed: true
})
])
expect(second.getStatusChangeSnapshot()).toEqual([])
} finally {
second.stop()
}
} finally {
first.stop()
await rm(userDataPath, { recursive: true, force: true })
}
})
})
@@ -1,85 +0,0 @@
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { AgentHookServer } from '../../agent-hook-server'
import { HOOK_SILENCE_BEFORE_PROGRAM_STATUS_FALLBACK_MS } from '../../program-status/hook-precedence'
import { buildBody, ingestProgramStatusOsc, PANE, postHookEvent } from '../server.test-fixtures'
const SESSION = '70efcb3f-ae17-4863-9318-649f5469408b'
let dir: string
let server: AgentHookServer
beforeEach(async () => {
dir = mkdtempSync(join(tmpdir(), 'orca-claude-continuation-'))
server = new AgentHookServer()
await server.start({ env: 'production', userDataPath: dir })
})
afterEach(() => {
vi.useRealTimers()
server.stop()
rmSync(dir, { recursive: true, force: true })
})
async function claudeHook(payload: Record<string, unknown>): Promise<void> {
const response = await postHookEvent(server, buildBody({ session_id: SESSION, ...payload }))
expect(response.status).toBe(204)
}
function recordedSession() {
return server.getStatusSnapshot()[0]?.providerSession
}
function seedResume(providerSession: { cwd?: string; effort?: string }): void {
server.recordLaunchResumeIdentity({
paneKey: PANE,
worktreeId: 'wt-1',
agentType: 'claude',
providerSession: { key: 'session_id', id: SESSION, ...providerSession },
launchedAt: Date.now()
})
}
it('keeps the start folder and effort when a terminal status row precedes the next hook', async () => {
await claudeHook({ hook_event_name: 'SessionStart', source: 'startup', cwd: '/repo' })
await claudeHook({
hook_event_name: 'PostToolUse',
tool_name: 'Bash',
cwd: '/repo/sub',
effort: { level: 'max' }
})
await claudeHook({ hook_event_name: 'Stop', cwd: '/repo/sub', effort: { level: 'max' } })
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(Date.now() + HOOK_SILENCE_BEFORE_PROGRAM_STATUS_FALLBACK_MS)
// Claude reports OSC 7501 `working` just before UserPromptSubmit.
ingestProgramStatusOsc(server, '\x1b]7501;state=working:app=claude-code\x1b\\')
await claudeHook({ hook_event_name: 'UserPromptSubmit', prompt: 'next', cwd: '/repo/sub' })
expect(recordedSession()).toMatchObject({ id: SESSION, cwd: '/repo', effort: 'max' })
})
it('records where a resumed process really started over the folder its seed named', async () => {
seedResume({ cwd: '/repo' })
await claudeHook({ hook_event_name: 'SessionStart', source: 'resume', cwd: '/elsewhere' })
expect(recordedSession()?.cwd).toBe('/elsewhere')
})
it('names the pane session from a --fork-session start, which Claude 2.1.214+ reports as fork', async () => {
// Captured from Claude 2.1.321 `claude --resume <id> --fork-session` before any message.
await claudeHook({
session_id: 'b47ec7fc-07e4-4012-8b28-1d5daa938f52',
transcript_path:
'/Users/u/.claude/projects/-private-tmp-w39942fork/b47ec7fc-07e4-4012-8b28-1d5daa938f52.jsonl',
cwd: '/private/tmp/w39942fork',
hook_event_name: 'SessionStart',
source: 'fork'
})
expect(recordedSession()).toMatchObject({
id: 'b47ec7fc-07e4-4012-8b28-1d5daa938f52',
cwd: '/private/tmp/w39942fork'
})
})
@@ -1,131 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, postHookEvent, PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({ track: trackMock }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: getCohortAtEmitMock }))
const RESUMED = { key: 'session_id' as const, id: 'resumed-session' }
// Codex may stay silent until the resumed pane's first turn; seed identity at launch for every reader.
describe('a resume launch seeds the pane identity', () => {
const servers: AgentHookServer[] = []
let userDataPath: string
beforeEach(() => {
_internals.resetCachesForTests()
getCohortAtEmitMock.mockReturnValue({})
userDataPath = mkdtempSync(join(tmpdir(), 'orca-launch-resume-identity-'))
})
afterEach(() => {
for (const server of servers) {
server.stop()
}
servers.length = 0
rmSync(userDataPath, { recursive: true, force: true })
vi.restoreAllMocks()
})
async function startServer(): Promise<AgentHookServer> {
const server = new AgentHookServer()
servers.push(server)
await server.start({ env: 'production', userDataPath })
return server
}
it('yields to a newer agent report', async () => {
const server = await startServer()
const launchedAt = Date.now() - 1_000
const response = await postHookEvent(
server,
buildBody({ hook_event_name: 'SessionStart', session_id: 'agent-reported' }),
'/hook/codex'
)
expect(response.status).toBe(204)
server.recordLaunchResumeIdentity({
paneKey: PANE,
agentType: 'codex',
providerSession: RESUMED,
launchedAt
})
expect(server.getStatusSnapshotForPane(PANE)[0]?.providerSession?.id).toBe('agent-reported')
})
it("replaces a row left by the pane's previous process", async () => {
const server = await startServer()
const response = await postHookEvent(
server,
buildBody({ hook_event_name: 'Stop', session_id: 'previous-run' }),
'/hook/codex'
)
expect(response.status).toBe(204)
server.recordLaunchResumeIdentity({
paneKey: PANE,
agentType: 'codex',
providerSession: RESUMED,
launchedAt: Date.now() + 1_000
})
expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({
providerSession: RESUMED,
providerSessionOnly: true
})
await postHookEvent(
server,
buildBody({ hook_event_name: 'UserPromptSubmit', session_id: RESUMED.id, prompt: 'go on' }),
'/hook/codex'
)
const [row] = server.getStatusSnapshotForPane(PANE)
expect(row?.providerSessionOnly).toBeUndefined()
expect(row).toMatchObject({ state: 'working', providerSession: RESUMED })
})
it('keeps the seeded identity when terminal evidence starts the first turn', async () => {
const server = await startServer()
server.recordLaunchResumeIdentity({
paneKey: PANE,
agentType: 'opencode2',
providerSession: RESUMED,
launchedAt: Date.now()
})
server.ingestTerminalStatus({
paneKey: PANE,
origin: 'process',
payload: { state: 'working', prompt: '', agentType: 'opencode2' }
})
expect(server.getStatusSnapshotForPane(PANE)[0]).toMatchObject({
state: 'working',
providerSession: RESUMED
})
})
it('keeps the seeded identity across a host restart', async () => {
const server = await startServer()
server.recordLaunchResumeIdentity({
paneKey: PANE,
worktreeId: 'wt-1',
agentType: 'codex',
providerSession: RESUMED,
launchedAt: Date.now()
})
server.stop()
servers.length = 0
const restarted = await startServer()
expect(restarted.getStatusSnapshotForPane(PANE)).toEqual([
expect.objectContaining({
agentType: 'codex',
providerSession: RESUMED,
providerSessionOnly: true
})
])
})
})
@@ -1,128 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer } from '../../agent-hook-server'
import { buildBody, PANE, postHookEvent } from '../server.test-fixtures'
vi.mock('../../../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: vi.fn(() => ({ nth_repo_added: 2 }))
}))
const route = { paneKey: PANE, tabId: 'tab-1', worktreeId: 'wt-1' }
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('nested provider ownership boundaries', () => {
it.each([
['ssh-b', 'wt-1', 'codex'],
['conn-1', 'wt-2', 'codex'],
['conn-1', 'wt-1', 'claude']
] as const)(
'scopes ownership to host %s and workspace %s',
(connectionId, worktreeId, agentType) => {
const server = new AgentHookServer()
server.ingestRemote(
{ ...route, payload: { state: 'working', prompt: 'Parent', agentType: 'claude' } },
'conn-1'
)
server.ingestRemote(
{
...route,
worktreeId,
payload: { state: 'working', prompt: 'Other', agentType: 'codex' }
},
connectionId
)
expect(server.getStatusSnapshot()[0]).toMatchObject({ agentType, connectionId, worktreeId })
}
)
it.each([
[null, 'wsl:Ubuntu'],
['wsl:Ubuntu', null]
] as const)('retains WSL ownership across %s to %s transport', (before, after) => {
const server = new AgentHookServer()
const worktreeId = 'repo:://wsl.localhost/Ubuntu/home/user/project'
server.ingestRemote(
{
...route,
worktreeId,
payload: { state: 'working', prompt: 'Parent', agentType: 'claude' }
},
before
)
server.ingestRemote(
{ ...route, worktreeId, payload: { state: 'working', prompt: 'Nested', agentType: 'codex' } },
after
)
expect(server.getStatusSnapshot()[0]).toMatchObject({ agentType: 'claude', prompt: 'Parent' })
})
it('admits a local HTTP owner despite a remote pane-key collision', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
server.ingestRemote(
{ ...route, payload: { state: 'working', prompt: 'Remote', agentType: 'claude' } },
'ssh-a'
)
const response = await postHookEvent(
server,
buildBody({
hook_event_name: 'UserPromptSubmit',
session_id: 'local-codex',
prompt: 'Local'
}),
'/hook/codex'
)
expect(response.status).toBe(204)
expect(server.getStatusSnapshot()[0]).toMatchObject({
connectionId: null,
agentType: 'codex',
prompt: 'Local',
providerSession: { id: 'local-codex' }
})
} finally {
server.stop()
}
})
it('keeps nested Pi identity-only metadata off an active owner and permits it after completion', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
const server = new AgentHookServer()
server.ingestRemote(
{
...route,
payload: { state: 'working', prompt: 'Parent work', agentType: 'claude' },
providerSession: { key: 'session_id', id: 'claude-parent' }
},
'conn-1'
)
const before = server.getStatusSnapshot()
const pi = {
...route,
providerSessionOnly: true,
providerSession: {
key: 'session_id' as const,
id: 'pi-child',
transcriptPath: '/tmp/pi-child.jsonl'
},
payload: { state: 'done' as const, prompt: '', agentType: 'pi' as const }
}
server.ingestRemote(pi, 'conn-1')
expect(server.getStatusSnapshot()).toEqual(before)
server.ingestRemote(
{ ...route, payload: { state: 'done', prompt: 'Parent work', agentType: 'claude' } },
'conn-1'
)
server.ingestRemote(pi, 'conn-1')
expect(server.getStatusSnapshot()[0]).toMatchObject({
agentType: 'pi',
providerSessionOnly: true,
providerSession: { id: 'pi-child' }
})
})
})
@@ -1,86 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, postHookEvent } from '../server.test-fixtures'
vi.mock('../../../../telemetry/client', () => ({
track: vi.fn()
}))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: vi.fn(() => ({ nth_repo_added: 2 }))
}))
beforeEach(() => {
_internals.resetCachesForTests()
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('AgentHookServer nested agent model fence', () => {
// Readers retain sparse model labels, so a transient child label could remain on the parent.
it('never pushes a nested codex model to the renderer from real hook bodies', async () => {
vi.useRealTimers()
const server = new AgentHookServer()
await server.start({ env: 'production' })
const pushed: { agentType?: string; model?: string }[] = []
server.setListener((event) => pushed.push(event.payload))
try {
const steps: [string, Record<string, unknown>][] = [
['/hook/claude', { hook_event_name: 'UserPromptSubmit', session_id: 'c-1', prompt: 'p' }],
[
'/hook/codex',
{
hook_event_name: 'SessionStart',
session_id: 'x-1',
model: 'gpt-6-astra',
source: 'startup'
}
],
[
'/hook/codex',
{
hook_event_name: 'UserPromptSubmit',
session_id: 'x-1',
prompt: 'n',
model: 'gpt-6-astra'
}
],
[
'/hook/codex',
{
hook_event_name: 'PreToolUse',
session_id: 'x-1',
model: 'gpt-6-astra',
tool_name: 'shell'
}
],
[
'/hook/codex',
{
hook_event_name: 'Stop',
session_id: 'x-1',
model: 'gpt-6-astra',
stop_hook_active: false
}
],
['/hook/claude', { hook_event_name: 'Stop', session_id: 'c-1' }]
]
for (const [path, body] of steps) {
expect((await postHookEvent(server, buildBody(body), path)).status).toBe(204)
}
const [row] = server.getStatusSnapshot()
expect(row).toMatchObject({ agentType: 'claude', state: 'done' })
expect(row?.model).toBeUndefined()
} finally {
server.stop()
}
expect(pushed).toHaveLength(2)
expect(pushed.every((payload) => payload.agentType === 'claude')).toBe(true)
expect(pushed.map((payload) => payload.model)).not.toContain('gpt-6-astra')
})
})
@@ -1,82 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer } from '../../agent-hook-server'
import { buildBody, PANE, postHookEvent } from '../server.test-fixtures'
import type { probeAgentProcessPresence } from '../../../../../shared/agent-status/process/agent-process-presence-probe'
vi.mock('../../../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: () => ({}) }))
const probe = vi.hoisted(() => vi.fn<typeof probeAgentProcessPresence>(async () => 'live'))
vi.mock('../../../../../shared/agent-status/process/agent-process-presence-probe', () => ({
probeAgentProcessPresence: probe
}))
const servers: AgentHookServer[] = []
const parent = { pid: 4001, platform: 'linux', startTime: 'birth-4001' }
const child = { pid: 4002, platform: 'linux', startTime: 'birth-4002' }
afterEach(() => {
for (const server of servers.splice(0)) {
server.stop()
}
probe.mockReset().mockResolvedValue('live')
})
async function claudeHook(
server: AgentHookServer,
process: typeof parent,
payload: Record<string, unknown>
): Promise<void> {
await postHookEvent(server, buildBody(payload, { agentProcess: JSON.stringify(process) }))
}
async function parentWorking(): Promise<AgentHookServer> {
const server = new AgentHookServer()
servers.push(server)
await server.start({ env: 'production' })
await claudeHook(server, parent, {
hook_event_name: 'UserPromptSubmit',
session_id: 'claude-parent',
prompt: 'Parent task'
})
return server
}
describe('a second Claude process in the same pane', () => {
it('does not replace the live owner row when a nested claude -p fails', async () => {
const server = await parentWorking()
const emitted = vi.fn()
server.setListener(emitted)
await claudeHook(server, child, {
hook_event_name: 'UserPromptSubmit',
session_id: 'claude-child',
prompt: 'Child task'
})
await claudeHook(server, child, {
hook_event_name: 'StopFailure',
session_id: 'claude-child',
error: 'rate_limit'
})
const row = server.getStatusSnapshot().find((entry) => entry.paneKey === PANE)
expect(row).toMatchObject({
state: 'working',
prompt: 'Parent task',
providerSession: { id: 'claude-parent' }
})
expect(emitted.mock.calls.some(([event]) => event.payload.state === 'done')).toBe(false)
})
it('lets a new Claude take the pane once the old owner process exited', async () => {
const server = await parentWorking()
probe.mockResolvedValue('exited')
await claudeHook(server, child, {
hook_event_name: 'UserPromptSubmit',
session_id: 'claude-next',
prompt: 'Next task'
})
const row = server.getStatusSnapshot().find((entry) => entry.paneKey === PANE)
expect(row).toMatchObject({
state: 'working',
prompt: 'Next task',
providerSession: { id: 'claude-next' }
})
})
})
@@ -1,410 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, PANE, RUNNING_SHELL } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({
track: trackMock
}))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('AgentHookServer listener replay', () => {
it('infers a cancel of a live Claude main agent turn and keeps the shell it leaves running as monitoring', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
claudeRunningNonAgentTask: true,
payload: {
state: 'working',
prompt: 'run in background',
agentType: 'claude',
mainAgent: { state: 'working', stateStartedAt: 900 }
}
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'run in background',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(true)
// Cancel the main agent without discarding live shell inventory or marking the working row interrupted.
const inferred = server.getStatusSnapshot()[0]
expect(inferred).toMatchObject({
state: 'working',
workingMode: 'monitoring',
mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 1_500 }
})
expect(inferred.interrupted).toBeUndefined()
expect(server._getStateForTests().claudeRunningNonAgentTaskPaneKeys.has(PANE)).toBe(true)
} finally {
vi.useRealTimers()
}
})
it("starts a relayed Claude pane's cancel clock at each cancel, not at an earlier one", () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
const relayTurn = (prompt: string, startedAt: number): void =>
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: {
state: 'working',
prompt,
agentType: 'claude',
mainAgent: { state: 'working', stateStartedAt: startedAt }
}
},
'conn-1'
)
const cancel = (prompt: string): boolean => {
const baseline = server.getStatusSnapshot()[0]
return server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: prompt,
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
}
relayTurn('first', 900)
vi.setSystemTime(1_500)
expect(cancel('first')).toBe(true)
// Relay-owned turns must not inherit the local record's earlier cancellation clock.
vi.setSystemTime(5_000)
relayTurn('second', 5_000)
vi.setSystemTime(6_000)
expect(cancel('second')).toBe(true)
expect(server.getStatusSnapshot()[0]).toMatchObject({
state: 'done',
mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 6_000 }
})
} finally {
vi.useRealTimers()
}
})
it('infers a cancel of a live Claude main agent turn beside a working subagent and keeps the row working', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: {
state: 'working',
prompt: 'delegate',
agentType: 'claude',
subagents: [{ id: 'a1', state: 'working', startedAt: 900 }],
mainAgent: { state: 'working', stateStartedAt: 900 }
}
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'delegate',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(true)
// Relayed rows carry their own child evidence; main-agent cancellation must preserve it.
expect(server.getStatusSnapshot()[0]).toMatchObject({
state: 'working',
subagents: [{ id: 'a1', state: 'working' }],
mainAgent: { state: 'done', outcome: 'cancellation' }
})
expect(server.getStatusSnapshot()[0].workingMode).toBeUndefined()
} finally {
vi.useRealTimers()
}
})
it('does not infer a cancel at the idle prompt of a Claude row held open by child work', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
claudeRunningNonAgentTask: true,
payload: {
state: 'working',
workingMode: 'monitoring',
prompt: 'run in background',
agentType: 'claude',
mainAgent: { state: 'done', stateStartedAt: 900 }
}
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
// Ctrl+C at an idle lead's prompt stops none of the child work shown.
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'run in background',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(false)
expect(server.getStatusSnapshot()[0]).toMatchObject({
state: 'working',
workingMode: 'monitoring',
mainAgent: { state: 'done' }
})
} finally {
vi.useRealTimers()
}
})
it('blocks local HTTP interrupt inference for provider-owned work without exposing transport metadata', async () => {
const server = new AgentHookServer()
await server.start({ env: 'production' })
try {
const env = server.buildPtyEnv()
const postHook = (payload: Record<string, unknown>): Promise<Response> =>
fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/hook/claude`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Orca-Agent-Hook-Token': env.ORCA_AGENT_HOOK_TOKEN
},
body: JSON.stringify(buildBody(payload))
})
await expect(
postHook({ hook_event_name: 'UserPromptSubmit', prompt: 'run in background' })
).resolves.toMatchObject({ status: 204 })
await expect(
postHook({ hook_event_name: 'Stop', background_tasks: [RUNNING_SHELL] })
).resolves.toMatchObject({ status: 204 })
const baseline = server.getStatusSnapshot()[0]
expect(baseline).not.toHaveProperty('claudeRunningNonAgentTask')
expect(baseline).toMatchObject({ state: 'working', workingMode: 'monitoring' })
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'run in background',
baselineAgentType: 'claude',
intent: 'plain-escape'
})
).toBe(false)
await expect(
postHook({
hook_event_name: 'Stop',
background_tasks: [],
session_crons: [{ id: 'cron-1' }]
})
).resolves.toMatchObject({ status: 204 })
const cronBaseline = server.getStatusSnapshot()[0]
expect(server._getStateForTests().claudeActiveSessionCronPaneKeys.has(PANE)).toBe(true)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: cronBaseline.receivedAt,
baselineStateStartedAt: cronBaseline.stateStartedAt,
baselinePrompt: 'run in background',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(false)
} finally {
server.stop()
}
})
it('uses replayed Claude background metadata only before a live observation', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
isReplay: true,
claudeRunningNonAgentTask: true,
payload: { state: 'working', prompt: 'stale replay', agentType: 'claude' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'stale replay',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(false)
vi.setSystemTime(1_500)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
claudeRunningNonAgentTask: false,
payload: { state: 'working', prompt: 'stale replay', agentType: 'claude' }
},
'conn-1'
)
vi.setSystemTime(2_000)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
isReplay: true,
claudeRunningNonAgentTask: true,
payload: { state: 'working', prompt: 'stale replay', agentType: 'claude' }
},
'conn-1'
)
const liveBaseline = server.getStatusSnapshot()[0]
vi.setSystemTime(2_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: liveBaseline.receivedAt,
baselineStateStartedAt: liveBaseline.stateStartedAt,
baselinePrompt: 'stale replay',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(true)
} finally {
vi.useRealTimers()
}
})
it('preserves an inferred interrupted row when OpenCode immediately reports SessionIdle', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
const listener = vi.fn()
server.setListener(listener)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: 'long task', agentType: 'opencode' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'long task',
baselineAgentType: 'opencode',
intent: 'plain-escape',
inputCount: 2
})
).toBe(true)
vi.setSystemTime(1_501)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'done', prompt: 'long task', agentType: 'opencode' }
},
'conn-1'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
paneKey: PANE,
state: 'done',
prompt: 'long task',
agentType: 'opencode',
interrupted: true,
receivedAt: 1_500,
stateStartedAt: 1_500
})
])
expect(listener).toHaveBeenLastCalledWith(
expect.objectContaining({
paneKey: PANE,
payload: expect.objectContaining({ state: 'done', interrupted: true })
})
)
} finally {
vi.useRealTimers()
}
})
})
@@ -1,262 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({
track: trackMock
}))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('AgentHookServer listener replay', () => {
it('does not let late same-turn working hooks resurrect an inferred interrupt', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: 'long task', agentType: 'pi' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'long task',
baselineAgentType: 'pi',
intent: 'ctrl-c'
})
).toBe(true)
vi.setSystemTime(6_000)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: {
state: 'working',
prompt: 'long task',
agentType: 'pi',
toolName: 'bash',
toolInput: '/bin/sleep 90'
}
},
'conn-1'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
state: 'done',
prompt: 'long task',
agentType: 'pi',
interrupted: true,
receivedAt: 1_500,
stateStartedAt: 1_500
})
])
} finally {
vi.useRealTimers()
}
})
it('allows an immediate same-prompt retry after an inferred interrupt', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
hasExplicitPrompt: true,
payload: { state: 'working', prompt: 'retryable task', agentType: 'pi' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'retryable task',
baselineAgentType: 'pi',
intent: 'ctrl-c'
})
).toBe(true)
vi.setSystemTime(2_000)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
hasExplicitPrompt: true,
payload: { state: 'working', prompt: 'retryable task', agentType: 'pi' }
},
'conn-1'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
state: 'working',
prompt: 'retryable task',
agentType: 'pi',
receivedAt: 2_000,
stateStartedAt: 2_000
})
])
} finally {
vi.useRealTimers()
}
})
it('suppresses same-turn Claude tool progress after the stale suppression window', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
hasExplicitPrompt: true,
hookEventName: 'UserPromptSubmit',
payload: { state: 'working', prompt: 'repeat task', agentType: 'claude' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'repeat task',
baselineAgentType: 'claude',
intent: 'ctrl-c'
})
).toBe(true)
vi.setSystemTime(16_501)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
hasExplicitPrompt: true,
hookEventName: 'PostToolUse',
payload: {
state: 'working',
prompt: 'repeat task',
agentType: 'claude',
toolName: 'bash',
toolInput: '/bin/sleep 90'
}
},
'conn-1'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
state: 'done',
prompt: 'repeat task',
agentType: 'claude',
interrupted: true,
receivedAt: 1_500,
stateStartedAt: 1_500
})
])
} finally {
vi.useRealTimers()
}
})
it('allows generic non-explicit same-prompt working after the stale suppression window', () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
try {
const server = new AgentHookServer()
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: 'repeat task', agentType: 'pi' }
},
'conn-1'
)
const baseline = server.getStatusSnapshot()[0]
vi.setSystemTime(1_500)
expect(
server.inferInterrupt({
paneKey: PANE,
baselineUpdatedAt: baseline.receivedAt,
baselineStateStartedAt: baseline.stateStartedAt,
baselinePrompt: 'repeat task',
baselineAgentType: 'pi',
intent: 'ctrl-c'
})
).toBe(true)
vi.setSystemTime(16_501)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: 'repeat task', agentType: 'pi' }
},
'conn-1'
)
expect(server.getStatusSnapshot()).toEqual([
expect.objectContaining({
state: 'working',
prompt: 'repeat task',
agentType: 'pi',
interrupted: undefined,
receivedAt: 16_501,
stateStartedAt: 16_501
})
])
} finally {
vi.useRealTimers()
}
})
})
@@ -1,161 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { appendFileSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, postHookEvent, PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({ track: trackMock }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: getCohortAtEmitMock }))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
// Fake only Date so real hook sockets keep working.
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(1_000_000)
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
describe('main agent turn verdicts and clocks', () => {
let server: AgentHookServer
beforeEach(async () => {
server = new AgentHookServer()
await server.start({ env: 'production' })
})
afterEach(() => {
server.stop()
})
async function post(path: string, payload: Record<string, unknown>): Promise<void> {
const response = await postHookEvent(server, buildBody(payload), path)
expect(response.status).toBe(204)
}
it.each(['idle_prompt notification', 'session end'] as const)(
'keeps a cancelled Grok turn verdict when a %s restates done',
async (restatement) => {
const turn = { sessionId: 'session-1', promptId: 'prompt-1' }
await post('/hook/grok', { hookEventName: 'user_prompt_submit', ...turn, prompt: 'go' })
await post('/hook/grok', { hookEventName: 'stop_cancelled', ...turn })
const cancelled = server.getStatusSnapshot()[0]?.mainAgent
expect(cancelled).toMatchObject({ state: 'done', outcome: 'cancellation' })
// Advance past late-event suppression so the restatement is published.
vi.setSystemTime(1_030_000)
await post(
'/hook/grok',
restatement === 'session end'
? { hookEventName: 'session_end', ...turn }
: { hookEventName: 'notification', ...turn, notificationType: 'idle_prompt' }
)
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'done', mainAgent: cancelled })
}
)
it('keeps a recorded Codex cancellation across a late root Stop', async () => {
const directory = mkdtempSync(join(tmpdir(), 'codex-turn-verdict-'))
const transcriptPath = join(directory, 'rollout.jsonl')
try {
writeFileSync(
transcriptPath,
'{"type":"event_msg","payload":{"type":"task_started","turn_id":"turn-1"}}\n'
)
await post('/hook/codex', {
hook_event_name: 'UserPromptSubmit',
prompt: 'long task',
transcript_path: transcriptPath
})
vi.setSystemTime(1_001_000)
appendFileSync(
transcriptPath,
'{"type":"event_msg","payload":{"type":"turn_aborted","turn_id":"turn-1","reason":"interrupted"}}\n'
)
await vi.waitFor(
() => expect(server.getStatusSnapshot()[0]?.mainAgent?.outcome).toBe('cancellation'),
{ timeout: 2_000 }
)
} finally {
rmSync(directory, { recursive: true, force: true })
}
const cancelled = server.getStatusSnapshot()[0]?.mainAgent
expect(cancelled).toMatchObject({ state: 'done', outcome: 'cancellation' })
vi.setSystemTime(1_002_000)
await post('/hook/codex', { hook_event_name: 'Stop' })
// Older readers need interrupted even when they cannot read mainAgent.
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'done', interrupted: true })
// Advance past suppression before child activity republishes the main agent.
vi.setSystemTime(1_060_000)
await post('/hook/codex', { hook_event_name: 'SubagentStart', agent_id: 'child-1' })
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'working', mainAgent: cancelled })
})
it('keeps a host-confirmed Codex cancellation across a late relayed root Stop', () => {
const relayed = (
hookEventName: string,
payload: Record<string, unknown>,
extra: Record<string, unknown> = {}
) =>
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
hookEventName,
...extra,
payload: { prompt: 'long task', agentType: 'codex', ...payload }
},
'conn-1'
)
relayed('UserPromptSubmit', { state: 'working' }, { hasExplicitPrompt: true })
vi.setSystemTime(1_001_000)
server.ingestRemote(
{
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: {
prompt: 'long task',
agentType: 'codex',
state: 'done',
interrupted: true,
mainAgent: { state: 'done', outcome: 'cancellation', stateStartedAt: 1_001_000 }
}
},
'conn-1'
)
const cancelled = server.getStatusSnapshot()[0]?.mainAgent
expect(cancelled).toMatchObject({ state: 'done', outcome: 'cancellation' })
vi.setSystemTime(1_002_000)
relayed('Stop', { state: 'done' })
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'done', interrupted: true })
vi.setSystemTime(1_060_000)
relayed(
'SubagentStart',
{
state: 'working',
subagents: [{ id: 'child-1', state: 'working', startedAt: 1_060_000 }]
},
{ toolAgentId: 'child-1' }
)
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'working', mainAgent: cancelled })
})
})
@@ -1,139 +0,0 @@
import { readSavedStatus } from '../persistence/last-status.test-fixtures'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, postHookEvent, PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({ track: trackMock }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: getCohortAtEmitMock }))
beforeEach(() => {
_internals.resetCachesForTests()
getCohortAtEmitMock.mockReturnValue({})
})
afterEach(() => {
vi.restoreAllMocks()
})
// Separate host timestamps by advancing real time between events.
async function nextMillisecond(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 5))
}
// Host-stamped turn starts survive reader reloads without reconstructing state history.
describe('the host-stamped turn start', () => {
const servers: AgentHookServer[] = []
let userDataPath: string
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-turn-started-at-'))
})
afterEach(() => {
for (const server of servers) {
server.stop()
}
servers.length = 0
rmSync(userDataPath, { recursive: true, force: true })
})
async function startServer(): Promise<AgentHookServer> {
const server = new AgentHookServer()
servers.push(server)
await server.start({ env: 'production', userDataPath })
return server
}
async function post(server: AgentHookServer, payload: Record<string, unknown>): Promise<void> {
const response = await postHookEvent(server, buildBody(payload))
expect(response.status).toBe(204)
await nextMillisecond()
}
function turnStartedAt(server: AgentHookServer): number | undefined {
return server.getStatusSnapshot()[0]?.turnStartedAt
}
it('is not stamped by a child agent, and the same prompt sent again is a new turn', async () => {
const server = await startServer()
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'Rename the module' })
const started = turnStartedAt(server)
await post(server, {
hook_event_name: 'UserPromptSubmit',
prompt: 'child task',
agent_id: 'child-1'
})
expect(turnStartedAt(server)).toBe(started)
// Esc at a permission prompt fires no hook, so the resend follows the stale wait directly.
await post(server, { hook_event_name: 'PermissionRequest', tool_name: 'Bash', tool_input: {} })
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'Rename the module' })
expect(turnStartedAt(server)).toBeGreaterThan(started!)
})
it('is not stamped by a relayed replay', async () => {
const server = await startServer()
const envelope = {
paneKey: PANE,
tabId: 'tab-1',
source: 'claude' as const,
hookEventName: 'UserPromptSubmit',
payload: { state: 'working' as const, prompt: 'relayed', agentType: 'claude' as const }
}
server.ingestRemote(envelope, 'ssh-turn')
const started = turnStartedAt(server)
expect(started).toEqual(expect.any(Number))
await nextMillisecond()
server.ingestRemote({ ...envelope, isReplay: true }, 'ssh-turn')
expect(turnStartedAt(server)).toBe(started)
})
// An OSC repaint can land before the prompt hook; it must not count from the finished turn.
it('drops the stamp when a finished main agent runs again with no turn-opening event', async () => {
const server = await startServer()
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'Rename the module' })
await post(server, { hook_event_name: 'Stop' })
expect(turnStartedAt(server)).toEqual(expect.any(Number))
server.ingestTerminalStatus({
paneKey: PANE,
tabId: 'tab-1',
worktreeId: 'wt-1',
payload: { state: 'working', prompt: 'Rename the module', agentType: 'claude' }
})
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'working' })
expect(server.getStatusSnapshot()[0]).not.toHaveProperty('turnStartedAt')
})
it('reaches the live push and survives a restart through the status file', async () => {
const server = await startServer()
const pushed: (number | undefined)[] = []
server.setListener((status) => pushed.push(status.turnStartedAt))
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'Rename the module' })
await post(server, { hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: {} })
const started = turnStartedAt(server)
expect(pushed.at(-1)).toBe(started)
server.flushStatusPersistSync()
server.stop()
const file = JSON.parse(readSavedStatus(join(userDataPath, 'agent-hooks', 'last-status.json')))
expect(file.entries[PANE].turnStartedAt).toBe(started)
const restarted = await startServer()
expect(restarted.getStatusSnapshot()[0]).toMatchObject({
restoredUnconfirmed: true,
turnStartedAt: started
})
await post(restarted, { hook_event_name: 'PostToolUse', tool_name: 'Bash', tool_input: {} })
expect(turnStartedAt(restarted)).toBe(started)
})
})
@@ -1,124 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, PANE } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({ track: trackMock }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: getCohortAtEmitMock }))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('Claude child permission lifecycle', () => {
async function createServer(): Promise<{
server: AgentHookServer
postClaudeHook: (payload: Record<string, unknown>) => Promise<Response>
}> {
const server = new AgentHookServer()
await server.start({ env: 'production' })
const env = server.buildPtyEnv()
return {
server,
postClaudeHook: (payload) =>
fetch(`http://127.0.0.1:${env.ORCA_AGENT_HOOK_PORT}/hook/claude`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Orca-Agent-Hook-Token': env.ORCA_AGENT_HOOK_TOKEN
},
body: JSON.stringify(buildBody(payload))
})
}
}
it('clears a child permission when an API error ends that child', async () => {
const { server, postClaudeHook } = await createServer()
try {
await postClaudeHook({ hook_event_name: 'UserPromptSubmit', prompt: 'guarded task' })
await postClaudeHook({
hook_event_name: 'PermissionRequest',
agent_id: 'a-blocked',
agent_type: 'general-purpose',
tool_name: 'Bash',
tool_input: { command: 'false' }
})
// Lead progress replaces the listener wait slot; the server must retain the child prompt.
await postClaudeHook({ hook_event_name: 'PreToolUse', tool_name: 'Read' })
await postClaudeHook({
hook_event_name: 'StopFailure',
agent_id: 'a-other',
error: 'rate_limit'
})
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'waiting', toolName: 'Bash' })
await postClaudeHook({
hook_event_name: 'StopFailure',
agent_id: 'a-blocked',
agent_type: 'general-purpose',
error: 'rate_limit'
})
const status = server.getStatusSnapshot()[0]
expect(status).toMatchObject({ paneKey: PANE, state: 'working', agentType: 'claude' })
expect(status?.toolName).toBe('Read')
expect(status?.interactivePrompt).toBeUndefined()
expect(status?.subagents).toBeUndefined()
} finally {
server.stop()
}
})
it('settles a finished lead when its background child fails on a pending permission', async () => {
const { server, postClaudeHook } = await createServer()
try {
await postClaudeHook({
hook_event_name: 'UserPromptSubmit',
prompt: 'research in background'
})
await postClaudeHook({
hook_event_name: 'SubagentStart',
agent_id: 'a6324370b7bede0c7',
agent_type: 'general-purpose'
})
await postClaudeHook({
hook_event_name: 'Stop',
last_assistant_message: 'Started the research.',
background_tasks: [{ id: 'a6324370b7bede0c7', type: 'subagent', status: 'running' }]
})
await postClaudeHook({
hook_event_name: 'PermissionRequest',
agent_id: 'a6324370b7bede0c7',
agent_type: 'general-purpose',
tool_name: 'Bash',
tool_input: { command: 'false' }
})
expect(server.getStatusSnapshot()[0]).toMatchObject({ state: 'waiting', toolName: 'Bash' })
await postClaudeHook({
hook_event_name: 'StopFailure',
agent_id: 'a6324370b7bede0c7',
agent_type: 'general-purpose',
error: 'rate_limit'
})
const status = server.getStatusSnapshot()[0]
expect(status).toMatchObject({ paneKey: PANE, state: 'done', agentType: 'claude' })
expect(status?.interactivePrompt).toBeUndefined()
expect(status?.subagents).toBeUndefined()
} finally {
server.stop()
}
})
})
@@ -1,212 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, postHookEvent, PANE, RUNNING_SHELL } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({ track: trackMock }))
vi.mock('../../../../telemetry/cohort-classifier', () => ({ getCohortAtEmit: getCohortAtEmitMock }))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
const CHILD_PERMISSION = {
hook_event_name: 'PermissionRequest',
agent_id: 'achild-a',
tool_name: 'Bash',
tool_input: { command: 'rm scratch' }
}
// Persist the settled main-agent fact so child lifecycle hooks, rather than OSC, can settle the restored row.
describe('Claude rows held open by child agents', () => {
let userDataPath: string
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-child-held-main-agent-'))
})
afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
})
async function startServer(): Promise<AgentHookServer> {
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
return server
}
async function restart(server: AgentHookServer): Promise<AgentHookServer> {
server.flushStatusPersistSync()
server.stop()
return startServer()
}
async function post(server: AgentHookServer, payload: Record<string, unknown>): Promise<void> {
await postHookEvent(server, buildBody(payload))
}
async function settleMainAgentBeside(server: AgentHookServer, childIds: string[]): Promise<void> {
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'delegate' })
for (const id of childIds) {
await post(server, { hook_event_name: 'SubagentStart', agent_id: id })
}
await post(server, { hook_event_name: 'Stop' })
}
function row(server: AgentHookServer) {
return server.getStatusSnapshot()[0]
}
it('settles a restored child permission wait once the child is approved and stops', async () => {
let server = await startServer()
await settleMainAgentBeside(server, ['achild-a'])
await post(server, CHILD_PERMISSION)
server = await restart(server)
try {
await post(server, {
hook_event_name: 'PreToolUse',
agent_id: 'achild-a',
tool_name: 'Bash',
tool_input: { command: 'rm scratch' },
tool_use_id: 'toolu-approved'
})
expect(row(server)?.state).toBe('working')
await post(server, { hook_event_name: 'SubagentStop', agent_id: 'achild-a' })
expect(row(server)).toMatchObject({ state: 'done', mainAgent: { state: 'done' } })
expect(row(server)?.restoredUnconfirmed).toBeUndefined()
} finally {
server.stop()
}
})
it('does not settle a restored row whose main agent resumed behind a child permission', async () => {
let server = await startServer()
await settleMainAgentBeside(server, ['achild-a', 'achild-b'])
await post(server, CHILD_PERMISSION)
await post(server, { hook_event_name: 'PreToolUse', tool_name: 'Read' })
expect(row(server)).toMatchObject({
state: 'waiting',
toolName: 'Bash',
mainAgent: { state: 'working' }
})
server = await restart(server)
try {
await post(server, { hook_event_name: 'SubagentStop', agent_id: 'achild-a' })
await post(server, { hook_event_name: 'SubagentStop', agent_id: 'achild-b' })
expect(row(server)).toMatchObject({ state: 'working', restoredUnconfirmed: true })
} finally {
server.stop()
}
})
it('settles a restored row after an inferred child answer when no shell ran', async () => {
let server = await startServer()
await settleMainAgentBeside(server, ['achild-a'])
await post(server, {
hook_event_name: 'PreToolUse',
agent_id: 'achild-a',
tool_name: 'AskUserQuestion',
tool_use_id: 'toolu-question'
})
const waiting = row(server)
expect(
server.inferQuestionAnswered({
paneKey: PANE,
baselineUpdatedAt: waiting?.receivedAt ?? 0,
baselineStateStartedAt: waiting?.stateStartedAt ?? 0,
baselinePrompt: waiting?.prompt ?? '',
baselineAgentType: 'claude'
})
).toBe(true)
server = await restart(server)
try {
await post(server, { hook_event_name: 'SubagentStop', agent_id: 'achild-a' })
expect(row(server)).toMatchObject({ state: 'done', mainAgent: { state: 'done' } })
} finally {
server.stop()
}
})
// Preserve shell evidence beside mainAgent so restart cannot mistake the row for shell-free.
async function expectShellHeldRowStaysOpenAfterRestart(
server: AgentHookServer,
childEvents: Record<string, unknown>[]
): Promise<void> {
const restarted = await restart(server)
try {
for (const event of childEvents) {
await post(restarted, event)
}
expect(row(restarted)?.state).toBe('working')
} finally {
restarted.stop()
}
}
it('keeps the shell fact when OSC repaints a shell-held row', async () => {
const server = await startServer()
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'build in background' })
await post(server, { hook_event_name: 'Stop', background_tasks: [RUNNING_SHELL] })
server.ingestTerminalStatus({
paneKey: PANE,
connectionId: null,
payload: { state: 'working', prompt: 'title text', agentType: 'claude' }
})
expect(row(server)).toMatchObject({ state: 'working', prompt: 'title text' })
await expectShellHeldRowStaysOpenAfterRestart(server, [
{ hook_event_name: 'SubagentStart', agent_id: 'achild-b' },
{ hook_event_name: 'SubagentStop', agent_id: 'achild-b' }
])
})
it('keeps the shell fact when an answered child question is inferred', async () => {
const server = await startServer()
await post(server, { hook_event_name: 'UserPromptSubmit', prompt: 'delegate' })
await post(server, { hook_event_name: 'SubagentStart', agent_id: 'achild-a' })
await post(server, {
hook_event_name: 'Stop',
background_tasks: [{ id: 'achild-a', type: 'subagent', status: 'running' }, RUNNING_SHELL]
})
await post(server, {
hook_event_name: 'PreToolUse',
agent_id: 'achild-a',
tool_name: 'AskUserQuestion',
tool_use_id: 'toolu-question'
})
const waiting = row(server)
expect(waiting).toMatchObject({ state: 'waiting', toolName: 'AskUserQuestion' })
expect(
server.inferQuestionAnswered({
paneKey: PANE,
baselineUpdatedAt: waiting?.receivedAt ?? 0,
baselineStateStartedAt: waiting?.stateStartedAt ?? 0,
baselinePrompt: waiting?.prompt ?? '',
baselineAgentType: 'claude'
})
).toBe(true)
expect(row(server)).toMatchObject({ state: 'working', mainAgent: { state: 'done' } })
await expectShellHeldRowStaysOpenAfterRestart(server, [
{ hook_event_name: 'SubagentStop', agent_id: 'achild-a' }
])
})
})
@@ -1,126 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { AgentHookServer, _internals } from '../../agent-hook-server'
import { buildBody, postHookEvent } from '../server.test-fixtures'
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
getCohortAtEmitMock: vi.fn(),
trackMock: vi.fn()
}))
vi.mock('../../../../telemetry/client', () => ({
track: trackMock
}))
vi.mock('../../../../telemetry/cohort-classifier', () => ({
getCohortAtEmit: getCohortAtEmitMock
}))
beforeEach(() => {
_internals.resetCachesForTests()
trackMock.mockReset()
getCohortAtEmitMock.mockReset()
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 2 })
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('Last-status persistence', () => {
let userDataPath: string
beforeEach(() => {
userDataPath = mkdtempSync(join(tmpdir(), 'orca-laststatus-'))
})
afterEach(() => {
rmSync(userDataPath, { recursive: true, force: true })
})
it('keeps a legacy Stop unconfirmed when only a restored child gates it', async () => {
const firstServer = new AgentHookServer()
await firstServer.start({ env: 'production', userDataPath })
await postHookEvent(
firstServer,
buildBody({
hook_event_name: 'SubagentStart',
agent_id: 'a0000000000000013',
agent_type: 'reviewer'
})
)
firstServer.flushStatusPersistSync()
firstServer.stop()
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
await postHookEvent(server, buildBody({ hook_event_name: 'Stop' }))
expect(server.getStatusSnapshot()[0]).toMatchObject({
state: 'working',
restoredUnconfirmed: true,
subagents: [expect.objectContaining({ id: 'a0000000000000013' })]
})
expect(server.getStatusChangeSnapshot()[0]?.observedInCurrentRuntime).toBe(false)
} finally {
server.stop()
}
})
it('waits for a lead boundary after a post-restart idle matches a restored teammate', async () => {
const firstServer = new AgentHookServer()
await firstServer.start({ env: 'production', userDataPath })
await postHookEvent(
firstServer,
buildBody({
hook_event_name: 'SubagentStart',
agent_id: 'areviewer-6d3cb5b5',
agent_type: 'reviewer'
})
)
firstServer.flushStatusPersistSync()
firstServer.stop()
const server = new AgentHookServer()
await server.start({ env: 'production', userDataPath })
try {
const restored = server.getStatusSnapshot()[0]
expect(restored).toMatchObject({
state: 'working',
restoredUnconfirmed: true,
subagents: [expect.objectContaining({ id: 'areviewer-6d3cb5b5' })]
})
await postHookEvent(
server,
buildBody({ hook_event_name: 'TeammateIdle', teammate_name: 'reviewer' })
)
expect(server.getStatusSnapshot()[0]).toMatchObject({
state: 'working',
restoredUnconfirmed: true,
subagents: [expect.objectContaining({ id: 'areviewer-6d3cb5b5', state: 'idle' })]
})
expect(server.getStatusChangeSnapshot()[0]?.observedInCurrentRuntime).toBe(false)
await postHookEvent(
server,
buildBody({
hook_event_name: 'Stop',
background_tasks: [{ id: 'treviewer', type: 'teammate', status: 'running' }]
})
)
expect(server.getStatusSnapshot()[0]).toMatchObject({
state: 'done',
subagents: [expect.objectContaining({ id: 'areviewer-6d3cb5b5', state: 'idle' })]
})
expect(server.getStatusSnapshot()[0]?.restoredUnconfirmed).toBeUndefined()
} finally {
server.stop()
}
})
})
@@ -1,190 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as NodeFsPromisesModule from 'node:fs/promises'
import type { SessionFileCandidate } from '../session-scanner-types'
const STALLED_PATH = '\\\\wsl.localhost\\Ubuntu\\home\\ada\\.claude\\projects\\p\\a.jsonl'
const mocks = vi.hoisted(() => ({ open: vi.fn(), readdir: vi.fn() }))
// Claude also counts sibling subagents; mock readdir to avoid real UNC access under fake timers.
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof NodeFsPromisesModule>()),
open: mocks.open,
readdir: mocks.readdir
}))
import * as transcriptFs from '../../native-chat/transcript-filesystem/wsl-transcript-fs-access'
import { parseAgentSessionFileCached } from '../parse-cache/session-scanner-parse-cache'
import {
registerTranscriptConsumer,
resetTranscriptConsumersForTests
} from '../transcripts/session-transcript-consumers'
import { resetSessionParseCacheForTests } from '../parse-cache/session-parse-cache-store'
import {
resetWslTranscriptFsGateForTests,
WSL_TRANSCRIPT_FS_SCAN_TIMEOUT_MS
} from '../../native-chat/transcript-filesystem/wsl-transcript-fs-gate'
import { WSL_TRANSCRIPT_FS_ROUTE_QUARANTINE_BASE_MS } from '../../native-chat/transcript-filesystem/wsl-transcript-fs-route-quarantine'
import { WslTranscriptFsError } from '../../native-chat/transcript-filesystem/wsl-transcript-fs-error'
type ReadResult = { bytesRead: number; buffer: Buffer }
// Settle stalled calls between cases to release their scan permits.
let releaseStall: (() => void) | undefined
function stalls(): Promise<ReadResult> {
return new Promise<ReadResult>((resolve) => {
releaseStall = () => resolve({ bytesRead: 0, buffer: Buffer.alloc(0) })
})
}
function servingHandle(body: Buffer) {
return {
read: vi.fn(async (buffer: Buffer, offset: number, length: number, position: number) => {
const slice = body.subarray(position, Math.min(position + length, body.length))
slice.copy(buffer, offset)
return { bytesRead: slice.length, buffer }
}),
close: vi.fn(async () => {})
}
}
function record(index: number, text: string): string {
return JSON.stringify({
type: 'user',
sessionId: 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee',
timestamp: new Date(1740000000000 + index * 60_000).toISOString(),
cwd: '/repo/app',
gitBranch: 'main',
message: { role: 'user', content: text }
})
}
function body(count: number): Buffer {
return Buffer.from(
Array.from({ length: count }, (_, index) => `${record(index, `line ${index}`)}\n`).join('')
)
}
function candidate(path: string, bytes: Buffer, mtimeMs: number): SessionFileCandidate {
return {
agent: 'claude',
file: {
path,
mtimeMs,
modifiedAt: new Date(mtimeMs).toISOString(),
sizeBytes: bytes.length
},
codexHome: null
}
}
// Late results cannot clear quarantine; advance through backoff before retrying.
async function releaseAndSettle(): Promise<void> {
releaseStall?.()
releaseStall = undefined
await vi.advanceTimersByTimeAsync(WSL_TRANSCRIPT_FS_ROUTE_QUARANTINE_BASE_MS)
}
beforeEach(() => {
resetWslTranscriptFsGateForTests()
resetSessionParseCacheForTests()
resetTranscriptConsumersForTests()
mocks.open.mockReset()
mocks.readdir.mockReset()
mocks.readdir.mockResolvedValue([])
releaseStall = undefined
// performance.now drives the route quarantine clock, so it must be faked too.
vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout', 'Date', 'performance'] })
})
afterEach(async () => {
await releaseAndSettle()
resetTranscriptConsumersForTests()
vi.restoreAllMocks()
vi.useRealTimers()
})
describe('AI Vault session parse with a stalled WSL transcript body read', () => {
it('cancels stalled EOF finalization, cleans up, and admits the next read', async () => {
const bytes = body(2)
const file = candidate(STALLED_PATH.replace('a.jsonl', 'finalization.jsonl'), bytes, 1)
mocks.open.mockResolvedValue(servingHandle(bytes))
let entered!: () => void
const finalizing = new Promise<void>((resolve) => {
entered = resolve
})
const readdir = vi.spyOn(transcriptFs, 'wslGatedReaddir').mockImplementationOnce(() => {
entered()
return new Promise(() => {})
})
const finishes = vi.fn()
registerTranscriptConsumer({
beginRead: () => ({ message: () => {}, finish: finishes })
})
const controller = new AbortController()
const first = parseAgentSessionFileCached(
file,
'linux',
undefined,
undefined,
controller.signal
)
const cancelled = first.catch((error) => {
expect(error).toMatchObject({
name: 'AbortError',
message: 'Agent Session History scan was cancelled'
})
return 'cancelled'
})
await finalizing
let nextMessageCount: number | undefined
const next = parseAgentSessionFileCached(file, 'linux').then((session) => {
nextMessageCount = session?.messageCount
return session
})
let result: string | undefined
void cancelled.then((value) => {
result = typeof value === 'string' ? value : 'completed'
})
controller.abort()
await vi.advanceTimersByTimeAsync(100)
expect(result).toBe('cancelled')
expect(finishes).toHaveBeenNthCalledWith(1, expect.objectContaining({ cancelled: true }))
expect(nextMessageCount).toBe(2)
await next
expect(finishes).toHaveBeenCalledTimes(2)
readdir.mockRestore()
})
it('caches no resume point when the newline probe is refused mid-append', async () => {
const first = body(2)
mocks.open.mockResolvedValue(servingHandle(first))
const seeded = await parseAgentSessionFileCached(candidate(STALLED_PATH, first, 1), 'linux')
expect(seeded?.messageCount).toBe(2)
const grown = body(3)
const serving = servingHandle(grown)
mocks.open.mockResolvedValue({
// Only the 1-byte resume probe stalls; the body read stays healthy.
read: vi.fn((buffer: Buffer, offset: number, length: number, position: number) =>
length === 1 ? stalls() : serving.read(buffer, offset, length, position)
),
close: vi.fn(async () => {})
})
const refused = expect(
parseAgentSessionFileCached(candidate(STALLED_PATH, grown, 2), 'linux')
).rejects.toBeInstanceOf(WslTranscriptFsError)
await vi.advanceTimersByTimeAsync(WSL_TRANSCRIPT_FS_SCAN_TIMEOUT_MS + 1)
await refused
await releaseAndSettle()
mocks.open.mockResolvedValue(servingHandle(grown))
const recovered = await parseAgentSessionFileCached(candidate(STALLED_PATH, grown, 2), 'linux')
resetSessionParseCacheForTests()
const cold = await parseAgentSessionFileCached(candidate(STALLED_PATH, grown, 2), 'linux')
expect(recovered).toEqual(cold)
expect(recovered?.messageCount).toBe(3)
})
})
@@ -1,210 +0,0 @@
import { chmod, mkdtemp, mkdir, rm, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { scanAiVaultSessions } from '../session-scanner'
import { isolatedScanRoots } from '../scanner-fixtures/session-scanner-test-fixtures'
import type { AiVaultScanOptions } from '../session-scanner-types'
let tempRoots: string[] = []
afterEach(async () => {
await Promise.all(tempRoots.map((root) => rm(root, { recursive: true, force: true })))
tempRoots = []
})
function scopedScanOptions(claudeProjectsDir: string, extra: Partial<AiVaultScanOptions>) {
return {
...isolatedScanRoots(claudeProjectsDir),
claudeProjectsDir,
...extra
} satisfies AiVaultScanOptions
}
// Recent out-of-scope sessions fill the recency pass, so the old session can only come from the scope pass.
async function writeOldClaudeSessionBehindRecentOnes(
claudeRoot: string,
old: { dirName: string; cwd: string }
): Promise<void> {
await writeClaudeSession({
claudeRoot,
...old,
sessionId: 'old-in-scope',
iso: '2026-01-01T00:00:00.000Z'
})
for (let index = 0; index < 4; index++) {
await writeClaudeSession({
claudeRoot,
dirName: `-other-${index}`,
sessionId: `recent-${index}`,
cwd: `/other/${index}`,
iso: `2026-06-2${index}T00:00:00.000Z`
})
}
}
const LONG_PATH = `/repo/${'nested-folder/'.repeat(16)}app`
async function writeClaudeSession(args: {
claudeRoot: string
dirName: string
sessionId: string
cwd: string
iso: string
leadingCwdlessLine?: boolean
}): Promise<void> {
const dir = join(args.claudeRoot, args.dirName)
await mkdir(dir, { recursive: true })
const records: unknown[] = []
if (args.leadingCwdlessLine) {
records.push({ type: 'last-prompt', sessionId: args.sessionId })
}
records.push({
type: 'user',
sessionId: args.sessionId,
timestamp: args.iso,
cwd: args.cwd,
message: { role: 'user', content: `work in ${args.cwd}` }
})
const filePath = join(dir, `${args.sessionId}.jsonl`)
await writeFile(filePath, records.map((record) => JSON.stringify(record)).join('\n'))
const time = new Date(args.iso)
await utimes(filePath, time, time)
}
describe('scanAiVaultSessions scope inclusion', () => {
it('surfaces in-scope sessions older than the global recency cap', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-scope-'))
tempRoots.push(root)
const claudeRoot = join(root, 'claude-projects')
await writeClaudeSession({
claudeRoot,
dirName: '-repo-app',
sessionId: 'old-in-scope',
cwd: '/repo/app',
iso: '2026-01-01T00:00:00.000Z',
leadingCwdlessLine: true
})
await writeClaudeSession({
claudeRoot,
dirName: '-repo-app-packages-ui',
sessionId: 'old-in-scope-subdir',
cwd: '/repo/app/packages/ui',
iso: '2026-01-02T00:00:00.000Z'
})
for (let index = 0; index < 4; index++) {
await writeClaudeSession({
claudeRoot,
dirName: `-other-${index}`,
sessionId: `recent-${index}`,
cwd: `/other/${index}`,
iso: `2026-06-2${index}T00:00:00.000Z`
})
}
const withoutScope = await scanAiVaultSessions(scopedScanOptions(claudeRoot, { limit: 2 }))
const withScope = await scanAiVaultSessions(
scopedScanOptions(claudeRoot, { limit: 2, scopePaths: ['/repo/app'] })
)
const ids = (result: { sessions: { sessionId: string }[] }) =>
result.sessions.map((session) => session.sessionId)
expect(ids(withoutScope)).not.toContain('old-in-scope')
expect(ids(withoutScope)).not.toContain('old-in-scope-subdir')
expect(ids(withScope)).toContain('old-in-scope')
expect(ids(withScope)).toContain('old-in-scope-subdir')
})
it('does not assign a native transcript to a WSL distro from cwd alone', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-scope-'))
tempRoots.push(root)
const claudeRoot = join(root, 'claude-projects')
await writeClaudeSession({
claudeRoot,
dirName: '-home-ada-repo',
sessionId: 'old-wsl-in-scope',
cwd: '/home/ada/repo',
iso: '2026-01-01T00:00:00.000Z'
})
for (let index = 0; index < 4; index++) {
await writeClaudeSession({
claudeRoot,
dirName: `-other-wsl-${index}`,
sessionId: `recent-wsl-${index}`,
cwd: `/other/wsl/${index}`,
iso: `2026-06-2${index}T00:00:00.000Z`
})
}
const result = await scanAiVaultSessions(
scopedScanOptions(claudeRoot, {
limit: 2,
scopePaths: ['\\\\wsl.localhost\\Ubuntu\\home\\ada\\repo']
})
)
expect(result.sessions.map((session) => session.sessionId)).not.toContain('old-wsl-in-scope')
})
it.each([
{ layout: 'a drive root', scopePath: 'C:\\', dirName: 'C--repo', cwd: 'C:\\repo' },
{
layout: 'a Windows path in another case',
scopePath: 'c:\\users\\ada\\repo',
dirName: 'C--Users-Ada-repo',
cwd: 'C:\\Users\\Ada\\repo'
},
{
layout: 'the other WSL share spelling',
scopePath: '\\\\wsl.localhost\\Ubuntu\\home\\ada\\repo',
dirName: '--wsl--Ubuntu-home-ada-repo',
cwd: '\\\\wsl$\\Ubuntu\\home\\ada\\repo'
},
{
// Claude Code names a bucket over 200 characters `<first 200>-<hash>`.
layout: 'a long path',
scopePath: LONG_PATH,
dirName: `${LONG_PATH.replace(/[^a-zA-Z0-9]/g, '-').slice(0, 200)}-1a2b3c`,
cwd: LONG_PATH
}
])('finds older in-scope Claude sessions for $layout', async ({ scopePath, dirName, cwd }) => {
const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-scope-'))
tempRoots.push(root)
const claudeRoot = join(root, 'claude-projects')
await writeOldClaudeSessionBehindRecentOnes(claudeRoot, { dirName, cwd })
const result = await scanAiVaultSessions(
scopedScanOptions(claudeRoot, { limit: 2, scopePaths: [scopePath] })
)
expect(result.sessions.map((session) => session.sessionId)).toContain('old-in-scope')
})
// chmod cannot lock out root, and Windows ignores the mode bits.
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(
'reports an unreadable in-scope folder instead of listing it as empty',
async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-ai-vault-scope-'))
tempRoots.push(root)
const claudeRoot = join(root, 'claude-projects')
await writeOldClaudeSessionBehindRecentOnes(claudeRoot, {
dirName: '-repo-app',
cwd: '/repo/app'
})
const bucket = join(claudeRoot, '-repo-app')
await chmod(bucket, 0o000)
try {
const result = await scanAiVaultSessions(
scopedScanOptions(claudeRoot, { limit: 2, scopePaths: ['/repo/app'] })
)
expect(result.issues.map((issue) => issue.path)).toContain(bucket)
} finally {
await chmod(bucket, 0o755)
}
}
)
})
@@ -1,144 +0,0 @@
import { closeTestStores, createStore, testState } from '../persistence/persistence-test-harness'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import type { Repo } from '../../shared/repo-types'
import { AutomationService } from './automation-service'
import { TICK_MS } from './testing/automation-service-tick.test-fixture'
import { createAwakeClock, type AwakeClock } from '../power/awake-clock'
import {
AUTOMATION_WAKE_SETTLE_MS,
automationSystemSettled
} from './dispatch/automation-wake-settle'
vi.mock('electron', () => ({
app: {
getPath: () => testState.dir
},
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'),
decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length)
}
}))
const makeRepo = (): Repo => ({
id: 'r1',
path: '/repo',
displayName: 'test',
badgeColor: '#fff',
addedAt: 1
})
type CounterModel = 'counts sleep (macOS/Windows Node)' | 'stops in sleep (Linux)'
describe.each<CounterModel>(['counts sleep (macOS/Windows Node)', 'stops in sleep (Linux)'])(
'scheduled catch-up after a wake, on a monotonic counter that %s',
(model) => {
let clock: AwakeClock
/** Sleep the fake monotonic counter missed: fake performance only moves with timers. */
let countedSleep = 0
beforeEach(() => {
testState.dir = mkdtempSync(join(tmpdir(), 'orca-automations-wake-test-'))
vi.useFakeTimers({
toFake: [
'Date',
'performance',
'setTimeout',
'clearTimeout',
'setInterval',
'clearInterval',
'setImmediate',
'clearImmediate'
]
})
// Why: offset-less times are local, matching the schedule's local BYHOUR in any CI TZ.
vi.setSystemTime(new Date('2026-11-09T08:00:00'))
countedSleep = 0
clock = createAwakeClock({
wallNow: () => Date.now(),
monotonicNow: () => performance.now() + countedSleep,
after: (beat, ms) => {
setTimeout(beat, ms)
}
})
})
afterEach(async () => {
await closeTestStores()
vi.useRealTimers()
rmSync(testState.dir, { recursive: true, force: true })
})
async function awakeFor(ms: number): Promise<void> {
await vi.advanceTimersByTimeAsync(ms)
}
/** Asleep: no timers run; the wall clock moves on, and so does the counter if it counts sleep. */
function asleepUntil(when: string): void {
const ms = new Date(when).getTime() - Date.now()
vi.setSystemTime(Date.now() + ms)
if (model === 'counts sleep (macOS/Windows Node)') {
countedSleep += ms
}
}
async function scheduledService(opts: { tickMs?: number; graceMinutes?: number } = {}) {
const tickMs = opts.tickMs ?? TICK_MS
const store = await createStore()
store.addRepo(makeRepo())
const automation = store.createAutomation({
name: 'GitHub i18n issue and PR scan',
prompt: 'Scan',
agentId: 'codex',
projectId: 'r1',
workspaceMode: 'existing',
workspaceId: 'r1::wt1',
timezone: 'America/Los_Angeles',
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: new Date('2026-11-08T00:00:00').getTime(),
missedRunGraceMinutes: opts.graceMinutes ?? 720
})
const launch = vi.fn(async () => ({ workspaceId: 'wt-1', terminalSessionId: 'tab-1' }))
const service = new AutomationService(store, {
tickMs,
terminalLauncher: { launch, available: () => true }
})
service.wakeGate.settled = () => automationSystemSettled(clock)
service.start()
await awakeFor(tickMs)
const statuses = () => store.listAutomationRuns(automation.id).map((run) => run.status)
return { store, automation, launch, service, statuses }
}
it('does not start a missed run during a dark wake, then catches it up once awake and stable', async () => {
// A one-second tick runs the scheduler inside the two-second dark wake.
const { store, automation, launch, service, statuses } = await scheduledService({
tickMs: 1_000
})
// The machine sleeps through 09:00 and dark-wakes at 09:08:30 with no resume event.
clock.suspend()
asleepUntil('2026-11-09T09:08:30')
await awakeFor(2_000)
expect(statuses()).toEqual([])
// Back to sleep, then a full wake at 09:19:37.
asleepUntil('2026-11-09T09:19:37')
clock.resume()
await awakeFor(AUTOMATION_WAKE_SETTLE_MS - 1_000)
expect(statuses()).toEqual([])
expect(launch).not.toHaveBeenCalled()
await awakeFor(2_000)
await vi.waitFor(() => expect(statuses()).toEqual(['dispatched']))
expect(store.listAutomationRuns(automation.id)[0]!.scheduledFor).toBe(
new Date('2026-11-09T09:00:00').getTime()
)
expect(launch).toHaveBeenCalledOnce()
service.stop()
})
}
)
@@ -1,35 +0,0 @@
import { describe, expect, it } from 'vitest'
import { ExternalAutomationManagerCache } from './external-automation-manager-cache'
import { ExternalAutomationProbeCancelledError } from './external-automation-probe-scheduler'
import type { ExternalAutomationManager } from '../../../shared/automations/external/external-automation'
function manager(id: string): ExternalAutomationManager {
return {
id,
provider: 'openclaw',
label: id,
targetLabel: 'this computer',
target: { type: 'local' },
status: 'available',
error: null,
canManage: true,
jobs: []
}
}
const sshOwner = 'owner:desktop:ssh:target-a:3'
describe('ExternalAutomationManagerCache', () => {
it('propagates cancellation and leaves the previous entry untouched', async () => {
const cache = new ExternalAutomationManagerCache()
const key = { ownerKey: sshOwner, provider: 'openclaw' }
cache.write(key, manager('openclaw:ssh:target-a'))
await expect(
cache.resolve(key, () => Promise.reject(new ExternalAutomationProbeCancelledError()), {
refresh: true
})
).rejects.toBeInstanceOf(ExternalAutomationProbeCancelledError)
expect(cache.read(key)?.manager?.id).toBe('openclaw:ssh:target-a')
})
})
@@ -1,141 +0,0 @@
import { closeTestStores, createStore, testState } from '../../persistence/persistence-test-harness'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { mkdtempSync, rmSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import type { Repo } from '../../../shared/repo-types'
import type { Automation } from '../../../shared/automations/automation-definition'
import type { AutomationRun } from '../../../shared/automations/history/automation-run'
import { AutomationService } from '../automation-service'
import type {
AutomationRunCompletionObservation,
AutomationRunTerminalObserver
} from './run-completion-watcher'
vi.mock('electron', () => ({
app: { getPath: () => testState.dir },
ipcMain: { handle: () => {} },
safeStorage: {
isEncryptionAvailable: () => true,
encryptString: (plaintext: string) => Buffer.from(`encrypted:${plaintext}`, 'utf-8'),
decryptString: (ciphertext: Buffer) => ciphertext.toString('utf-8').slice('encrypted:'.length)
}
}))
type TestStore = Awaited<ReturnType<typeof createStore>>
const makeRepo = (): Repo => ({
id: 'r1',
path: '/repo',
displayName: 'test',
badgeColor: '#fff',
addedAt: 1
})
const LAUNCH_TARGET = {
workspaceId: 'wt1',
terminalSessionId: 'tab-1',
terminalPaneKey: 'tab-1:11111111-2222-4333-8444-555555555555',
terminalPtyId: 'pty-1'
}
const OPEN_LAUNCHER = { launch: async () => ({ ...LAUNCH_TARGET }), available: () => true }
function createAutomation(store: TestStore): Automation {
store.addRepo(makeRepo())
return store.createAutomation({
name: 'Nightly check',
prompt: 'Check the repo',
agentId: 'claude',
projectId: 'r1',
workspaceMode: 'existing',
workspaceId: 'wt1',
timezone: 'UTC',
rrule: 'FREQ=DAILY;BYHOUR=9;BYMINUTE=0',
dtstart: new Date('2026-05-12T00:00:00Z').getTime()
})
}
function retainDispatchedRun(store: TestStore, automation: Automation): AutomationRun {
const run = store.createAutomationRun(automation, Date.now() - 60_000, 'manual')
return store.updateAutomationRun({
runId: run.id,
status: 'dispatched',
...LAUNCH_TARGET,
error: null
})
}
function readRun(store: TestStore, automationId: string, runId: string): AutomationRun {
const run = store.listAutomationRuns(automationId).find((entry) => entry.id === runId)
if (!run) {
throw new Error('run missing')
}
return run
}
function createPaneSurface(): {
observer: AutomationRunTerminalObserver
mountPane: () => void
settleObservation: (value: AutomationRunCompletionObservation) => void
observedHandles: string[]
} {
const state = { mounted: false }
const observedHandles: string[] = []
let resolveObservation: ((value: AutomationRunCompletionObservation) => void) | null = null
return {
observer: {
resolveRunTerminal: (run) => (state.mounted && run.terminalPaneKey ? 'handle-1' : null),
observeCompletion: (handle) => {
observedHandles.push(handle)
return new Promise<AutomationRunCompletionObservation>((resolve) => {
resolveObservation = resolve
})
}
},
mountPane: () => {
state.mounted = true
},
settleObservation: (value) => resolveObservation?.(value),
observedHandles
}
}
describe('reconciling retained runs against a graph that has not published yet', () => {
beforeEach(() => {
testState.dir = mkdtempSync(join(tmpdir(), 'orca-retained-run-'))
vi.useFakeTimers()
// Away from the automation's 09:00 UTC occurrence so timer advances stay inert.
vi.setSystemTime(new Date('2026-06-01T12:00:00Z'))
})
afterEach(async () => {
await closeTestStores()
vi.useRealTimers()
rmSync(testState.dir, { recursive: true, force: true })
})
it('re-attaches to the run once the restored pane mounts seconds later', async () => {
const store = await createStore()
const automation = createAutomation(store)
const retained = retainDispatchedRun(store, automation)
const surface = createPaneSurface()
const service = new AutomationService(store, {
terminalObserver: surface.observer,
terminalLauncher: OPEN_LAUNCHER
})
service.start()
await vi.advanceTimersByTimeAsync(5_000)
expect(surface.observedHandles).toEqual([])
surface.mountPane()
await vi.advanceTimersByTimeAsync(5_000)
expect(surface.observedHandles).toEqual(['handle-1'])
surface.settleObservation({ status: 'completed', error: null })
await vi.advanceTimersByTimeAsync(0)
expect(readRun(store, automation.id, retained.id).status).toBe('completed')
service.stop()
})
})
@@ -1,112 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const browserMocks = vi.hoisted(() => ({
appGetPathMock: vi.fn(() => '/downloads'),
shellOpenExternalMock: vi.fn(),
browserWindowFromWebContentsMock: vi.fn(),
menuBuildFromTemplateMock: vi.fn(),
guestOffMock: vi.fn(),
guestOnMock: vi.fn(),
guestSetBackgroundThrottlingMock: vi.fn(),
guestSetWindowOpenHandlerMock: vi.fn(),
guestOpenDevToolsMock: vi.fn(),
webContentsFromIdMock: vi.fn(),
screenGetCursorScreenPointMock: vi.fn(() => ({ x: 0, y: 0 })),
openPopupWithOriginBarMock: vi.fn()
}))
vi.mock('electron', () => ({
app: { prependListener: vi.fn(), off: vi.fn(), getPath: browserMocks.appGetPathMock },
BrowserWindow: { fromWebContents: browserMocks.browserWindowFromWebContentsMock },
clipboard: { writeText: vi.fn() },
shell: { openExternal: browserMocks.shellOpenExternalMock },
Menu: { buildFromTemplate: browserMocks.menuBuildFromTemplateMock },
screen: { getCursorScreenPoint: browserMocks.screenGetCursorScreenPointMock },
webContents: { fromId: browserMocks.webContentsFromIdMock }
}))
import { browserManager } from '../../guest-lifecycle/manager/browser-manager'
import { runWithActiveRpcCaller } from '../../../runtime/rpc/callers/active-call'
import { withAgentInputInjection } from '../../cdp/input/agent-input-injection'
import {
guestNavigationMethods,
rendererWebContentsId,
resetBrowserManagerMocks,
resetBrowserManagerState
} from '../../guest-lifecycle/manager/browser-manager-test-harness'
import type { BrowserExtensionCaller } from '../api-runtime/browser-extension-namespace-registry'
import { placeBrowserExtensionCallerTab } from './browser-extension-agent-caller'
const workSession = { name: 'work' }
const otherSession = { name: 'other' }
/** Registers a guest page; `click` delivers an activating input to it. */
function registerPage(browserPageId: string, id: number, worktreeId: string) {
const on = vi.fn()
const guest = {
...guestNavigationMethods(),
id,
session: workSession,
isDestroyed: vi.fn(() => false),
isCurrentlyAudible: vi.fn(() => false),
getType: vi.fn(() => 'webview'),
setBackgroundThrottling: vi.fn(),
setWindowOpenHandler: vi.fn(),
setIgnoreMenuShortcuts: vi.fn(),
on,
prependListener: vi.fn(),
off: vi.fn(),
openDevTools: vi.fn()
}
const previous = browserMocks.webContentsFromIdMock.getMockImplementation()
browserMocks.webContentsFromIdMock.mockImplementation((lookup: number) =>
lookup === id ? guest : previous?.(lookup)
)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the manager and input tracker call only the methods stubbed above.
const contents = guest as unknown as Electron.WebContents
browserManager.attachGuestPolicies(contents)
browserManager.registerGuest({
browserPageId,
worktreeId,
webContentsId: id,
rendererWebContentsId
})
const onInput = on.mock.calls.find(([event]) => event === 'input-event')?.[1]
return { contents, click: () => onInput({}, { type: 'mouseUp' }) }
}
function caller(session: typeof workSession, extra: Partial<BrowserExtensionCaller> = {}) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: placement reads session, webContents and userGesture only.
return { session, browserProfileId: 'work', fromWorker: true, ...extra } as BrowserExtensionCaller
}
beforeEach(() => {
resetBrowserManagerMocks(browserMocks)
resetBrowserManagerState()
browserMocks.webContentsFromIdMock.mockImplementation(() => null)
})
describe('an extension tab set off while an agent drives a page in its profile', () => {
it("opens in the background in the agent's workspace, unless the user set it off", async () => {
const userPage = registerPage('user-page', 11, 'wt-user')
const agentPage = registerPage('agent-page', 12, 'wt-agent')
const command = runWithActiveRpcCaller({ kind: 'local-cli' }, () =>
browserManager.beginAgentCommand('agent-page')
)
const placeFromWorker = () => placeBrowserExtensionCallerTab(caller(workSession), true)
const agentPlacement = { active: false, worktreeId: 'wt-agent' }
expect(placeFromWorker()).toEqual(agentPlacement)
expect(placeBrowserExtensionCallerTab(caller(otherSession), true)).toEqual({ active: true })
expect(
placeBrowserExtensionCallerTab(caller(workSession, { userGesture: true }), true)
).toEqual({ active: true })
userPage.click()
expect(placeFromWorker()).toEqual({ active: true })
await withAgentInputInjection(agentPage.contents, async () => agentPage.click())
expect(placeFromWorker()).toEqual(agentPlacement)
command.end()
})
})
@@ -1,65 +0,0 @@
import { existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { build } from 'esbuild'
import { afterEach, expect, it } from 'vitest'
import { JSONC_PARSER_ESM_ALIAS } from '../../../../../config/build-plugins/jsonc-parser-esm'
import { describeClaudeProfile } from '../claude-profile-paths'
import { runClaudeProfileSetupInWorker } from './claude-profile-setup-worker'
const roots: string[] = []
afterEach(() => roots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true })))
it('runs account setup in the built worker entry, off the calling thread', async () => {
const root = mkdtempSync(join(tmpdir(), 'claude-setup-worker-'))
roots.push(root)
const userHome = join(root, 'personal')
const dataRoot = join(root, 'data')
mkdirSync(join(userHome, '.claude'), { recursive: true })
const profile = describeClaudeProfile(dataRoot, 'a', {
executionHostId: 'local',
runtime: 'host'
})
mkdirSync(profile.home, { recursive: true })
const entry = join(root, 'claude-profile-setup-worker-entry.js')
await build({
entryPoints: [
resolve('src/main/claude-accounts/profiles/setup/claude-profile-setup-worker-entry.ts')
],
outfile: entry,
bundle: true,
alias: JSONC_PARSER_ESM_ALIAS,
platform: 'node',
format: 'cjs',
external: ['electron'],
logLevel: 'silent'
})
const report = await runClaudeProfileSetupInWorker(
{
dataRoot,
profile,
userHome,
userConfigDir: undefined,
hooks: false,
claudeVersion: undefined
},
entry
)
expect(report.outcome).toBe('prepared')
expect(existsSync(join(dataRoot, 'claude-profiles', 'a', 'profile.json'))).toBe(true)
await expect(
runClaudeProfileSetupInWorker(
{
dataRoot,
profile,
userHome,
userConfigDir: undefined,
hooks: false,
claudeVersion: undefined
},
join(root, 'missing-entry.js')
)
).rejects.toThrow()
})
@@ -1,46 +0,0 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { vi, describe, expect, it } from 'vitest'
import type * as GitBashModule from '../../pty/shell-startup/git-bash'
vi.mock('electron', () => ({
app: {
getPath: () => '/tmp/userData'
}
}))
const { gitBashAvailableMock } = vi.hoisted(() => ({ gitBashAvailableMock: { value: true } }))
vi.mock('../../pty/shell-startup/git-bash', async (importOriginal) => ({
...(await importOriginal<typeof GitBashModule>()),
isGitBashAvailable: () => gitBashAvailableMock.value
}))
import { ClaudeHookService } from '../hook-service'
describe('ClaudeHookService.install', () => {
it('does not re-install a managed statusLine the user deleted, until remove() resets the opt-out', () => {
const tmpHome = mkdtempSync(join(tmpdir(), 'orca-claude-statusline-optout-'))
vi.stubEnv('HOME', tmpHome)
vi.stubEnv('USERPROFILE', tmpHome)
try {
const settingsPath = join(tmpHome, '.claude', 'settings.json')
new ClaudeHookService().install()
expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).statusLine).toBeTruthy()
const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'))
delete settings.statusLine
writeFileSync(settingsPath, JSON.stringify(settings))
new ClaudeHookService().install()
expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).statusLine).toBeUndefined()
new ClaudeHookService().remove()
new ClaudeHookService().install()
expect(JSON.parse(readFileSync(settingsPath, 'utf-8')).statusLine).toBeTruthy()
} finally {
vi.unstubAllEnvs()
rmSync(tmpHome, { recursive: true, force: true })
}
})
})
@@ -1,105 +0,0 @@
import { spawn } from 'node:child_process'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { getManagedStatusLineScript } from './statusline-script'
const ORIGINAL_PLATFORM = process.platform
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: ORIGINAL_PLATFORM })
})
describe.skipIf(process.platform === 'win32')('statusline curl throttle (posix behavioral)', () => {
const PANE_KEY = 'tab-1:00000000-0000-4000-8000-000000000000'
const dirs: string[] = []
afterEach(() => {
for (const dir of dirs.splice(0)) {
rmSync(dir, { recursive: true, force: true })
}
})
function rateLimitPayload(durationMs: number): string {
return JSON.stringify({
cost: { total_duration_ms: durationMs },
rate_limits: { five_hour: { used_percentage: 12 } }
})
}
function makeHarness(): {
scriptPath: string
dir: string
curlLog: string
payloadLog: string
dateLog: string
catLog: string
} {
const dir = mkdtempSync(join(tmpdir(), 'orca-statusline-throttle-'))
dirs.push(dir)
const curlLog = join(dir, 'curl.log')
const payloadLog = join(dir, 'payload.log')
const dateLog = join(dir, 'date.log')
const catLog = join(dir, 'cat.log')
const scriptPath = join(dir, 'statusline.sh')
writeFileSync(scriptPath, getManagedStatusLineScript('posix'))
const binDir = join(dir, 'stub-bin')
mkdirSync(binDir, { recursive: true })
writeFileSync(
join(binDir, 'curl'),
`#!/bin/sh\n/bin/cat > "${payloadLog}"\nprintf 'x\\n' >> "${curlLog}"\nexit 0\n`,
{ mode: 0o755 }
)
writeFileSync(
join(binDir, 'date'),
`#!/bin/sh\nprintf 'x\\n' >> "${dateLog}"\nprintf '2000000000\\n'\n`,
{ mode: 0o755 }
)
writeFileSync(join(binDir, 'cat'), `#!/bin/sh\nprintf 'x\\n' >> "${catLog}"\n/bin/cat "$@"\n`, {
mode: 0o755
})
return { scriptPath, dir, curlLog, payloadLog, dateLog, catLog }
}
function runScript(
scriptPath: string,
dir: string,
payload: string,
paneKey = PANE_KEY
): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn('sh', [scriptPath], {
env: {
PATH: `${join(dir, 'stub-bin')}:${process.env.PATH ?? ''}`,
TMPDIR: dir,
ORCA_AGENT_HOOK_PORT: '65535',
ORCA_AGENT_HOOK_TOKEN: 'test-token',
ORCA_PANE_KEY: paneKey
},
stdio: ['pipe', 'ignore', 'pipe']
})
let stderr = ''
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString()
})
child.on('error', reject)
child.on('close', (code) => {
if (code === 0) {
resolve()
} else {
reject(new Error(`statusline script exited ${code}: ${stderr}`))
}
})
child.stdin.write(payload)
child.stdin.end()
})
}
it('preserves multiline payloads while capturing stdin with shell builtins', async () => {
const { scriptPath, dir, payloadLog } = makeHarness()
const payload = JSON.stringify(JSON.parse(rateLimitPayload(1_000)), null, 2)
await runScript(scriptPath, dir, payload)
expect(readFileSync(payloadLog, 'utf8')).toBe(payload)
})
})
@@ -1,197 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentJournalItemBody } from '../../../shared/agent-session/journal/agent-session-journal-types'
import { agentJournalItemKey } from '../../../shared/agent-session/journal/agent-session-journal-item-key'
import { createClaudeJournalTranslator } from './journal/claude-structured-journal-translation'
function setup() {
const rows = new Map<string, AgentJournalItemBody>()
const translator = createClaudeJournalTranslator({
sink: {
appendItem: (identity, body) => {
if (body.kind !== 'turn') {
rows.set(agentJournalItemKey(identity), body)
}
},
appendTombstone: vi.fn(),
publish: vi.fn()
}
})
const frame = (message: Record<string, unknown>, observedAt?: number): void =>
translator.handle({
type: 'message',
sessionId: 'orca-session',
...(observedAt === undefined ? {} : { observedAt }),
message: {
session_id: 'session',
parent_tool_use_id: null,
...message
}
})
const stream = (uuid: string, event: Record<string, unknown>, observedAt?: number): void =>
frame({ type: 'stream_event', uuid, event }, observedAt)
const final = (uuid: string, content: unknown[], observedAt?: number): void =>
frame(
{
type: 'assistant',
uuid,
message: { id: 'message-1', role: 'assistant', content }
},
observedAt
)
return { rows, translator, frame, stream, final }
}
function reasoning(text: string, lifecycle: Record<string, unknown>): AgentJournalItemBody {
return {
kind: 'message',
role: 'reasoning',
blocks: [{ type: 'text', text }],
...lifecycle
}
}
afterEach(() => vi.useRealTimers())
describe('structured Claude reasoning', () => {
it('writes final-only thinking closed, with no span it never saw', () => {
const { rows, final, translator } = setup()
final('final-only', [{ type: 'thinking', thinking: 'Inspecting the request' }], 5_000)
expect([...rows.values()]).toEqual([
reasoning('Inspecting the request', { state: 'completed' })
])
translator.dispose()
})
it('streams a running row and closes that same row on its final frame', () => {
vi.useFakeTimers()
const { rows, stream, final, translator } = setup()
stream('start', { type: 'message_start', message: { id: 'message-1' } }, 1_000)
stream(
'thinking-start',
{ type: 'content_block_start', index: 0, content_block: { type: 'thinking', thinking: '' } },
1_000
)
stream(
'delta-1',
{
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: 'Inspecting ' }
},
3_000
)
translator.flush()
const firstKey = [...rows.keys()][0]
expect(rows.get(firstKey!)).toEqual(reasoning('Inspecting ', { state: 'running' }))
stream(
'delta-2',
{
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: 'the request' }
},
4_000
)
translator.flush()
expect([...rows.keys()]).toEqual([firstKey])
final('thinking-final', [{ type: 'thinking', thinking: 'Inspecting the request' }], 9_000)
expect([...rows.keys()]).toEqual([firstKey])
expect(rows.get(firstKey!)).toEqual(
reasoning('Inspecting the request', { state: 'completed', completedAt: 9_000 })
)
stream('text-start', {
type: 'content_block_start',
index: 1,
content_block: { type: 'text', text: '' }
})
stream('text-delta', {
type: 'content_block_delta',
index: 1,
delta: { type: 'text_delta', text: 'Here is the answer' }
})
translator.flush()
final('text-final', [{ type: 'text', text: 'Here is the answer' }])
expect([...rows.values()].map((body) => body.kind === 'message' && body.role)).toEqual([
'reasoning',
'assistant'
])
expect(translator.pendingStreamedBlocks).toBe(0)
translator.dispose()
})
it('closes a streamed row with its streamed text when the final frame carries none', () => {
vi.useFakeTimers()
const { rows, stream, final, translator } = setup()
stream(
'delta',
{
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: 'Summary' }
},
1_000
)
final('thinking-final', [{ type: 'thinking', thinking: '', signature: 'sig' }], 2_000)
expect([...rows.values()]).toEqual([
reasoning('Summary', { state: 'completed', completedAt: 2_000 })
])
expect(translator.pendingStreamedBlocks).toBe(0)
translator.dispose()
})
it('reconciles an empty final block before the next thinking block', () => {
const { rows, stream, final, translator } = setup()
stream('empty-start', {
type: 'content_block_start',
index: 0,
content_block: { type: 'thinking', thinking: '' }
})
final('empty-final', [{ type: 'thinking', thinking: '' }])
stream('next-start', {
type: 'content_block_start',
index: 1,
content_block: { type: 'thinking', thinking: 'Next thought' }
})
translator.flush()
const key = [...rows.keys()][0]
final('next-final', [{ type: 'thinking', thinking: 'Next thought' }])
expect([...rows.keys()]).toEqual([key])
expect(translator.pendingStreamedBlocks).toBe(0)
translator.dispose()
})
it('writes streamed thinking into the turn its first delta opened', () => {
vi.useFakeTimers()
const scopes: unknown[] = []
const translator = createClaudeJournalTranslator({
sink: {
appendItem: (_identity, body, options) => {
if (body.kind === 'message' && body.role === 'reasoning') {
scopes.push(options.turnScope)
}
},
appendTombstone: vi.fn(),
publish: vi.fn()
}
})
translator.handle({
type: 'message',
sessionId: 'orca-session',
message: {
type: 'stream_event',
session_id: 'session',
parent_tool_use_id: null,
uuid: 'delta',
event: {
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: 'Considering' }
}
}
})
translator.flush()
expect(scopes).toEqual([{ kind: 'turn', turnItemId: expect.stringContaining('turn') }])
translator.dispose()
})
})
@@ -1,73 +0,0 @@
import { expect, it, vi } from 'vitest'
import type { AgentJournalItemBody } from '../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentJournalItemIdentity } from '../../../../shared/agent-session/journal/agent-session-journal-identity'
import type {
StructuredAgentSessionEventSink,
StructuredAgentSessionItemAppendOptions
} from '../../../native-chat/agent-session-wire/journal-ingress/structured-agent-session-event-sink'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
function recorder() {
const writes: {
identity: AgentJournalItemIdentity
body: AgentJournalItemBody
options: StructuredAgentSessionItemAppendOptions
}[] = []
const sink: StructuredAgentSessionEventSink = {
appendItem: (identity, body, options) => writes.push({ identity, body, options }),
appendTombstone: vi.fn(),
publish: vi.fn()
}
const scopeOfProse = (text: string) =>
writes.findLast(
(write) =>
write.body.kind === 'message' &&
write.body.blocks.some((block) => block.type === 'text' && block.text === text)
)?.options.turnScope
return { sink, writes, scopeOfProse }
}
function frame(message: Record<string, unknown>, startsTurn = false) {
return {
type: 'message' as const,
sessionId: 'orca-session',
...(startsTurn ? { startsTurn: true as const } : {}),
message: { session_id: 'claude-session', ...message }
}
}
function prose(uuid: string, text: string, parentToolUseId: string | null = null) {
return frame({
type: 'assistant',
uuid,
parent_tool_use_id: parentToolUseId,
message: { role: 'assistant', content: [{ type: 'text', text }] }
})
}
it("scopes the session's and a subagent's rows to the open turn, and a late one to none", () => {
const { sink, writes, scopeOfProse } = recorder()
const translator = createClaudeJournalTranslator({ sink, fallbackIdPrefix: 'test' })
translator.handle(
frame(
{
type: 'user',
uuid: 'user-1',
parent_tool_use_id: null,
message: { role: 'user', content: [{ type: 'text', text: 'go' }] }
},
true
)
)
translator.handle(prose('assistant-1', 'Working on it'))
translator.handle(prose('child-1', 'Child mid-turn', 'toolu_1'))
translator.handle(frame({ type: 'result', subtype: 'success', uuid: 'result-1', result: 'ok' }))
translator.handle(prose('child-2', 'Child after the turn', 'toolu_1'))
const turnRecord = writes.find((write) => write.body.kind === 'turn')
expect(turnRecord?.options.turnScope).toEqual({ kind: 'thread' })
const turnKey = expect.stringContaining('turn-lifecycle')
expect(scopeOfProse('Working on it')).toEqual({ kind: 'turn', turnItemId: turnKey })
expect(scopeOfProse('Child mid-turn')).toEqual({ kind: 'turn', turnItemId: turnKey })
expect(scopeOfProse('Child after the turn')).toEqual({ kind: 'thread' })
})
@@ -1,107 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { AgentJournalItemBody } from '../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentJournalItemIdentity } from '../../../../shared/agent-session/journal/agent-session-journal-identity'
import type { StructuredAgentSessionEventSink } from '../../../native-chat/agent-session-wire/journal-ingress/structured-agent-session-event-sink'
import { createClaudeJournalTranslator } from './claude-structured-journal-translation'
function sinkState() {
const items: { identity: AgentJournalItemIdentity; body: AgentJournalItemBody }[] = []
const sink: StructuredAgentSessionEventSink = {
appendItem: (identity, body) => items.push({ identity, body }),
appendTombstone: () => {},
publish: vi.fn()
}
return { sink, items }
}
function providerFrameKinds(items: { body: AgentJournalItemBody }[]): string[] {
return items.flatMap((item) =>
item.body.kind === 'status' && item.body.providerFrame ? [item.body.providerFrame.kind] : []
)
}
/** Shape reported from Claude Code 2.1.316; UUIDs are test identities. */
function pluginUi(subtype: string | undefined, uuid: string, fields: Record<string, unknown>) {
return {
type: 'message' as const,
sessionId: 'orca-session',
message: {
type: 'system',
subtype,
plugin: 'fos-skills',
...fields,
uuid,
session_id: 'claude-session'
}
}
}
function userTurn(uuid: string, text: string) {
return {
type: 'message' as const,
sessionId: 'orca-session',
startsTurn: true as const,
message: {
type: 'user',
uuid,
session_id: 'claude-session',
parent_tool_use_id: null,
isReplay: true,
message: { role: 'user', content: text }
}
}
}
function assistantReply(uuid: string, text: string) {
return {
type: 'message' as const,
sessionId: 'orca-session',
message: {
type: 'assistant',
uuid,
session_id: 'claude-session',
parent_tool_use_id: null,
message: { role: 'assistant', content: [{ type: 'text', text }] }
}
}
}
describe('Claude plugin UI frames', () => {
it('keeps plugin status lines and toasts off the transcript for a whole turn', () => {
const state = sinkState()
const translator = createClaudeJournalTranslator({ sink: state.sink })
translator.handle(userTurn('user-1', 'Reply with exactly PROBE_OK and nothing else.'))
translator.handle(pluginUi('ui_status', 'ui-1', { text: 'PR 6 · 진행 13 · 대기 6 /board' }))
translator.handle(assistantReply('assistant-1', 'PROBE_OK'))
// Captured from Claude Code 2.1.316 with an isolated test plugin.
translator.handle(
pluginUi('ui_toast', 'ui-2', {
plugin: 'qa-plugin-ui',
text: 'QA plugin toast: prompt seen',
timeout_ms: 4000
})
)
translator.handle(pluginUi('ui_status', 'ui-3', { text: null }))
translator.handle({
type: 'message',
sessionId: 'orca-session',
message: {
type: 'result',
subtype: 'success',
uuid: 'result-1',
session_id: 'claude-session',
is_error: false,
result: 'PROBE_OK',
terminal_reason: 'completed'
}
})
expect(providerFrameKinds(state.items)).toEqual([])
expect(
state.items.flatMap((item) =>
item.body.kind === 'message' && item.body.role === 'assistant' ? [item.body.blocks] : []
)
).toEqual([[{ type: 'text', text: 'PROBE_OK' }]])
})
})
@@ -1,97 +0,0 @@
import { describe, expect, it } from 'vitest'
import type { AgentJournalItemIdentity } from '../../../../shared/agent-session/journal/agent-session-journal-identity'
import type { AgentJournalProducerLinkage } from '../../../../shared/agent-session/journal/agent-session-journal-types'
import type { StructuredAgentSessionAppendOptions } from '../../../native-chat/agent-session-wire/journal-ingress/structured-agent-session-event-sink'
import { createClaudeStreamedTextCheckpoints } from './claude-streamed-text-checkpoints'
import type {
ClaudeSubagentLinkageSource,
ClaudeSubagentLinkageVerdict
} from '../../subagents/claude-subagent-linkage'
function identityOf(uuid: string): AgentJournalItemIdentity {
return { provider: 'claude', sessionId: 'claude-session', uuid }
}
/** Scopeless blocks are root-owned and must never consult the child resolver. */
const unconsultedProducer: ClaudeSubagentLinkageSource = {
linkageFor: () => {
throw new Error('resolver consulted for a block with no scope')
},
settledLinkageFor: () => {
throw new Error('resolver consulted for a block with no scope')
}
}
const CHILD_LINKAGE: AgentJournalProducerLinkage = {
agentId: 'task-1',
providerParentRef: 'toolu_1',
producerKind: 'agent'
}
/** Resolve announcements mid-stream; use the raw reference if no announcement arrives. */
function scriptedProducer() {
let verdict: ClaudeSubagentLinkageVerdict = { kind: 'pending' }
const settledFallback: Extract<ClaudeSubagentLinkageVerdict, { kind: 'linked' }> = {
kind: 'linked',
linkage: { agentId: 'toolu_1', providerParentRef: 'toolu_1', producerKind: 'agent' }
}
return {
source: {
linkageFor: () => verdict,
settledLinkageFor: () => (verdict.kind === 'pending' ? settledFallback : verdict)
} satisfies ClaudeSubagentLinkageSource,
resolve: (linkage: AgentJournalProducerLinkage) => {
verdict = { kind: 'linked', linkage }
}
}
}
function checkpoints(producer: ClaudeSubagentLinkageSource = unconsultedProducer) {
const rows: { uuid: string; text: string }[] = []
const stamps: StructuredAgentSessionAppendOptions[] = []
let scheduled: (() => void) | null = null
const store = createClaudeStreamedTextCheckpoints({
producer,
persist: (identity, text, options) => {
rows.push({ uuid: 'uuid' in identity ? identity.uuid : '', text })
stamps.push(options)
},
schedule: (run) => {
scheduled = run
return () => {
scheduled = null
}
}
})
return {
store,
rows,
stamps,
runWindow: () => {
const run = scheduled as (() => void) | null
run?.()
}
}
}
describe('claude streamed text checkpoints', () => {
it('writes a checkpoint at once while the producing agent is provisional', () => {
const producer = scriptedProducer()
const { store, rows, stamps, runWindow } = checkpoints(producer.source)
store.append(identityOf('block-1'), 'partial', 'toolu_1')
runWindow()
expect(rows).toEqual([{ uuid: 'block-1', text: 'partial' }])
expect(stamps.at(-1)).toMatchObject({ agentId: 'toolu_1' })
// Flush advances text; reattribute corrects linkage even when text is unchanged.
producer.resolve(CHILD_LINKAGE)
store.flush()
expect(stamps.at(-1)).toMatchObject({ agentId: 'toolu_1' })
store.reattribute()
expect(rows.at(-1)).toEqual({ uuid: 'block-1', text: 'partial' })
expect(stamps.at(-1)).toEqual(CHILD_LINKAGE)
})
})
@@ -1,216 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { ClaudeStructuredSessionEvent } from '../claude-structured-session-state'
import {
PROVIDER_SESSION_ID,
acquired,
adapterFor,
fakeClaude,
identityFor
} from '../claude-structured-session-test-support'
import { claudeOpened } from '../claude-structured-startup-settled-test-support'
/** Claude Code 2.1.280 list_models fixture; default and opus[1m] resolve to the same model. */
const CATALOG = [
{
value: 'default',
resolvedModel: 'claude-opus-5-5[1m]',
displayName: 'Default (recommended)',
supportsEffort: true,
supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']
},
{
value: 'opus[1m]',
resolvedModel: 'claude-opus-5-5[1m]',
displayName: 'Opus (1M context)',
supportsEffort: true,
supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']
},
{
value: 'sonnet',
resolvedModel: 'claude-sonnet-5',
displayName: 'Sonnet',
supportsEffort: true,
supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max']
},
{ value: 'haiku', resolvedModel: 'claude-haiku-4-5-20251001', displayName: 'Haiku' }
]
/** Claude Code 2.1.280 scratch-config responses: applied includes env resolution; effective reflects settings. */
const SETTINGS = {
noOverride: {
effective: {},
sources: [],
applied: { model: 'claude-opus-5-5[1m]', effort: 'medium', advisor: null, ultracode: false }
},
settingsHaiku: {
effective: { model: 'haiku' },
sources: [{ source: 'userSettings', settings: { model: 'haiku' } }],
applied: { model: 'claude-haiku-4-5-20251001', effort: null, advisor: null, ultracode: false }
}
}
function startedWithoutATurn(settings: unknown) {
// The real startup proof before any turn is the SessionStart hook frame, which names no model.
return fakeClaude({
initProof: 'session-start',
initModels: CATALOG,
settings,
routes: { list_models: () => CATALOG }
})
}
describe('Claude model before the first turn', () => {
it('uses native applied effort despite settings High, before and after a reply', async () => {
// Claude Code 2.1.318: no flag applies Medium; --effort low applies Low.
const claude = startedWithoutATurn({
effective: { effortLevel: 'high' },
applied: { model: 'claude-opus-5-5', effort: 'medium', ultracode: false }
})
const events: ClaudeStructuredSessionEvent[] = []
const adapter = await acquired(claude, {}, events)
const current = async () =>
(await adapter.readOptions({ sessionId: 'session-1', fence: 7 })).current
expect((await current()).effort).toBe('medium')
expect((await current()).confirmed ?? []).not.toContain('effort')
const connection = claude.connections[0]!
connection.getSettings = async () => ({
effective: { effortLevel: 'high' },
applied: { model: 'claude-opus-5-5', effort: 'low', ultracode: false }
})
connection.handlers.onMessage?.({
type: 'result',
subtype: 'success',
uuid: 'finished',
session_id: PROVIDER_SESSION_ID,
is_error: false
})
await vi.waitFor(async () => expect((await current()).effort).toBe('low'))
expect((await current()).confirmed ?? []).not.toContain('effort')
const started = await claudeOpened(adapter, 'session-1')
expect(started.reportedOptions).not.toHaveProperty('effort')
const reported = events.find((event) => event.type === 'options-reported')
expect(reported).toMatchObject({
reportedOptions: expect.not.objectContaining({ effort: expect.anything() })
})
await adapter.closeAll()
})
it('clears inferred effort when native settings explicitly report none', async () => {
const claude = startedWithoutATurn({
effective: { effortLevel: 'high' },
applied: { model: 'claude-opus-5-5', effort: 'medium', ultracode: false }
})
const adapter = await acquired(claude)
expect((await adapter.readOptions({ sessionId: 'session-1', fence: 7 })).current.effort).toBe(
'medium'
)
// The recorded Haiku response explicitly names no effort parameter.
claude.connections[0]!.getSettings = async () => ({
...SETTINGS.settingsHaiku,
effective: { model: 'haiku', effortLevel: 'high' }
})
claude.connections[0]!.handlers.onMessage?.({
type: 'result',
subtype: 'success',
uuid: 'haiku-finished',
session_id: PROVIDER_SESSION_ID,
is_error: false
})
await vi.waitFor(async () =>
expect(
(await adapter.readOptions({ sessionId: 'session-1', fence: 7 })).current.effort
).toBeUndefined()
)
await adapter.closeAll()
})
it('lets the first turn confirm or correct it', async () => {
const claude = startedWithoutATurn(SETTINGS.settingsHaiku)
const adapter = await acquired(claude)
claude.connections[0]!.handlers.onMessage?.({
type: 'system',
subtype: 'init',
session_id: PROVIDER_SESSION_ID,
uuid: 'turn-init-uuid',
model: 'claude-sonnet-5',
apiKeySource: 'none'
})
const { current } = await adapter.readOptions({ sessionId: 'session-1', fence: 7 })
expect(current.model).toBe('claude-sonnet-5')
expect(current.confirmed).toContain('model')
})
it('starts a new chat on the model Claude will apply, which is what the session persists', async () => {
const events: ClaudeStructuredSessionEvent[] = []
await acquired(startedWithoutATurn(SETTINGS.settingsHaiku), {}, events)
const reported = events.find((event) => event.type === 'options-reported')
expect(reported).toMatchObject({ reportedOptions: { model: 'claude-haiku-4-5-20251001' } })
})
it('shows the applied effort but never persists it as the session options', async () => {
const events: ClaudeStructuredSessionEvent[] = []
const adapter = await acquired(startedWithoutATurn(SETTINGS.noOverride), {}, events)
const started = await claudeOpened(adapter, 'session-1')
expect(started.reportedOptions).not.toHaveProperty('model')
// Saved, it would be restored on every reopen past a later settings.json change.
expect(started.reportedOptions).not.toHaveProperty('effort')
const reported = events.find((event) => event.type === 'options-reported')
expect(
reported?.type === 'options-reported' ? reported.reportedOptions : null
).not.toHaveProperty('effort')
const { current } = await adapter.readOptions({ sessionId: 'session-1', fence: 7 })
expect(current.effort).toBe('medium')
})
it('refreshes what Claude applies at turn completion, without work on an options read', async () => {
const settings = structuredClone(SETTINGS.settingsHaiku)
const claude = startedWithoutATurn(settings)
const adapter = await acquired(claude)
await adapter.setOption({ sessionId: 'session-1', fence: 7, key: 'model', value: 'sonnet' })
Object.assign(settings.applied, { model: 'claude-sonnet-5', effort: 'high' })
const reads = claude.connections[0]!.calls.filter(
(call) => call.subtype === 'get_settings'
).length
const { current } = await adapter.readOptions({ sessionId: 'session-1', fence: 7 })
expect(current.model).toBe('sonnet')
expect(current.effort).toBeUndefined()
expect(
claude.connections[0]!.calls.filter((call) => call.subtype === 'get_settings')
).toHaveLength(reads)
claude.connections[0]!.handlers.onMessage?.({
type: 'result',
subtype: 'success',
uuid: 'finished',
session_id: PROVIDER_SESSION_ID,
is_error: false
})
await vi.waitFor(async () => {
expect(
(await adapter.readOptions({ sessionId: 'session-1', fence: 7 })).current
).toMatchObject({ model: 'sonnet', effort: 'high' })
})
})
it('drops the applied effort once a restored model replaces the model it described', async () => {
const claude = startedWithoutATurn(SETTINGS.noOverride)
const events: ClaudeStructuredSessionEvent[] = []
const adapter = adapterFor(claude, { resumesTranscript: true, continuesChain: true }, events)
await adapter.acquire({
identity: identityFor(),
fence: 7,
spawnToken: 'spawn-9',
options: { model: 'haiku' }
})
const started = await claudeOpened(adapter, 'session-1')
expect(started.reportedOptions).not.toHaveProperty('model')
expect(started.reportedOptions.effort).toBeUndefined()
})
})
@@ -1,99 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { AgentJournalItemBody } from '../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentJournalItemIdentity } from '../../../../shared/agent-session/journal/agent-session-journal-identity'
import type { StructuredAgentSessionEventSink } from '../../../native-chat/agent-session-wire/journal-ingress/structured-agent-session-event-sink'
import { createClaudeJournalTranslator } from '../journal/claude-structured-journal-translation'
function sinkState() {
const items: { identity: AgentJournalItemIdentity; body: AgentJournalItemBody }[] = []
const sink: StructuredAgentSessionEventSink = {
appendItem: (identity, body) => items.push({ identity, body }),
appendTombstone: () => {},
publish: vi.fn()
}
return { sink, items }
}
function providerFrameKinds(items: { body: AgentJournalItemBody }[]): string[] {
return items.flatMap((item) =>
item.body.kind === 'status' && item.body.providerFrame ? [item.body.providerFrame.kind] : []
)
}
/** Queue-bookkeeping shape captured from CLI 2.1.258 emission sites. */
function commandLifecycle(state: 'started' | 'completed' | 'cancelled', uuid: string) {
return {
type: 'message' as const,
sessionId: 'orca-session',
message: {
type: 'command_lifecycle',
command_uuid: 'command-1',
state,
uuid,
session_id: 'claude-session'
}
}
}
function userTurn(uuid: string, text: string) {
return {
type: 'message' as const,
sessionId: 'orca-session',
startsTurn: true as const,
message: {
type: 'user',
uuid,
session_id: 'claude-session',
parent_tool_use_id: null,
isReplay: true,
message: { role: 'user', content: text }
}
}
}
function assistantReply(uuid: string, text: string) {
return {
type: 'message' as const,
sessionId: 'orca-session',
message: {
type: 'assistant',
uuid,
session_id: 'claude-session',
parent_tool_use_id: null,
message: { role: 'assistant', content: [{ type: 'text', text }] }
}
}
}
describe('Claude command_lifecycle frames', () => {
it('keeps queue bookkeeping off the transcript for a whole turn', () => {
const state = sinkState()
const translator = createClaudeJournalTranslator({ sink: state.sink })
translator.handle(userTurn('user-1', 'Reply with exactly PROBE_OK and nothing else.'))
translator.handle(commandLifecycle('started', 'lifecycle-1'))
translator.handle(assistantReply('assistant-1', 'PROBE_OK'))
translator.handle(commandLifecycle('completed', 'lifecycle-2'))
translator.handle(commandLifecycle('completed', 'lifecycle-3'))
translator.handle({
type: 'message',
sessionId: 'orca-session',
message: {
type: 'result',
subtype: 'success',
uuid: 'result-1',
session_id: 'claude-session',
is_error: false,
result: 'PROBE_OK',
terminal_reason: 'completed'
}
})
expect(providerFrameKinds(state.items)).toEqual([])
expect(
state.items.flatMap((item) =>
item.body.kind === 'message' && item.body.role === 'assistant' ? [item.body.blocks] : []
)
).toEqual([[{ type: 'text', text: 'PROBE_OK' }]])
})
})
@@ -1,143 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { agentJournalItemKey } from '../../../../shared/agent-session/journal/agent-session-journal-item-key'
import type { AgentJournalItemBody } from '../../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentJournalItemIdentity } from '../../../../shared/agent-session/journal/agent-session-journal-identity'
import type { AgentJournalTurnScope } from '../../../../shared/agent-session/journal/agent-session-journal-turn-scope'
import { readAgentJournalTurn } from '../../../../shared/agent-session/lifecycle/records/agent-session-turn-record'
import type { StructuredAgentSessionEventSink } from '../../../native-chat/agent-session-wire/journal-ingress/structured-agent-session-event-sink'
import { journalTimelineItemsView } from '../../../native-chat/agent-session-journal/writing/journal-timeline-view'
import { resolveJournalTimelineItem } from '../../../native-chat/agent-session-journal/writing/journal-timeline-resolution'
import {
CAPTURED_COMPACT_SESSION_ID,
type CapturedCompactEvent
} from '../../fixtures/claude-captured-compact-frames.test-fixture'
import { createClaudeJournalTranslator } from '../journal/claude-structured-journal-translation'
const COMMAND_IDENTITY: AgentJournalItemIdentity = {
provider: 'orca',
clientMessageId: 'command-turn:cmd-1'
}
const COMMAND_KEY = agentJournalItemKey(COMMAND_IDENTITY)
const NOW = 1_800_000_000_000
const RUNNING = {
kind: 'turn' as const,
turnId: 'compact:cmd-1',
state: 'running' as const,
userItemId: 'orca:submission:cmd-1',
requestedAt: 900,
startedAt: 1_000
}
// Usage limits expire by the system clock; pin it so the quota reset stays an hour ahead.
beforeEach(() => {
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(NOW)
})
afterEach(() => {
vi.useRealTimers()
})
type Row = { key: string; body: AgentJournalItemBody; turnScope?: AgentJournalTurnScope }
function harness() {
const rows = new Map<string, Row>([[COMMAND_KEY, { key: COMMAND_KEY, body: RUNNING }]])
const writes: Row[] = []
const write = (
identity: AgentJournalItemIdentity,
body: AgentJournalItemBody,
turnScope?: AgentJournalTurnScope
) => {
const row = { key: agentJournalItemKey(identity), body, ...(turnScope ? { turnScope } : {}) }
rows.set(row.key, row)
writes.push(row)
}
const view = {
...journalTimelineItemsView(
() => null,
() =>
[...rows.values()].map(({ key, body, turnScope }, sequence) => ({
itemId: key,
body,
...(turnScope ? { turnScope } : {}),
revision: 1,
sequence,
observedAt: 0
}))
),
itemBody: (itemId: string) => rows.get(itemId)?.body ?? null
}
const revise: NonNullable<StructuredAgentSessionEventSink['tryReviseResolvedItem']> = (
bytes,
resolution,
options
) => {
const resolved = resolveJournalTimelineItem(view, resolution, options.turnScope, bytes)
if (resolved.kind === 'resolved') {
write(resolved.item.identity, resolved.item.body, options.turnScope)
}
return { accepted: true }
}
const sink: StructuredAgentSessionEventSink = {
appendItem: (identity, body, options) => write(identity, body, options.turnScope),
appendTombstone: vi.fn(),
publish: vi.fn(),
tryReviseResolvedItem: revise,
tryReviseResolvedItemAndPublish: revise
}
const translator = createClaudeJournalTranslator({ sink, fallbackIdPrefix: 'test' })
const frame = (message: Record<string, unknown>, extra: Record<string, unknown> = {}) =>
translator.handle({
type: 'message',
sessionId: 'orca-session',
message: { session_id: CAPTURED_COMPACT_SESSION_ID, ...message },
observedAt: 5_000,
...extra
})
const begin = (sentUuid: string) =>
translator.beginCommand({
clientMessageId: 'cmd-1',
turnId: RUNNING.turnId,
identity: COMMAND_IDENTITY,
resultIdentity: { provider: 'orca', clientMessageId: 'command-result:cmd-1' },
running: RUNNING,
providerSessionId: CAPTURED_COMPACT_SESSION_ID,
sentUuid
})
const replay = (events: readonly CapturedCompactEvent[]) => {
for (const event of events) {
if ('sent' in event) {
if (event.sent.text === '/compact') {
begin(event.sent.uuid)
}
} else if ('interrupt' in event) {
translator.commandInterruptRequested(RUNNING.turnId)
} else {
frame(event.frame)
}
}
}
const commandTurn = () => readAgentJournalTurn(rows.get(COMMAND_KEY)?.body)
const drawn = () =>
[...rows.values()].filter((row) => row.key !== COMMAND_KEY && row.body.kind !== 'turn')
return { translator, rows, writes, frame, begin, replay, commandTurn, drawn }
}
describe('a captured /compact as the open Claude turn', () => {
it('leaves the command running at a result that names another input', () => {
const { begin, frame, commandTurn, translator } = harness()
begin('compact-input')
frame({ type: 'system', subtype: 'compact_boundary', uuid: 'boundary' })
frame({ type: 'result', subtype: 'success', is_error: false, user_message_uuid: 'earlier' })
expect(commandTurn()?.state).toBe('running')
expect(translator.commandTurnId).toBe(RUNNING.turnId)
frame({
type: 'result',
subtype: 'success',
is_error: false,
user_message_uuid: 'compact-input'
})
expect(commandTurn()).toMatchObject({ state: 'completed', outcome: 'success' })
})
})
@@ -1,52 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
awaitClaudeRequestEnd,
CLAUDE_STOP_GRACE_MS,
settleClaudeTurnEndWaiters
} from './claude-request-end-wait'
import type { ClaudeSession } from '../claude-structured-session-state'
type InFlight = { translator: { currentTurnId: string | null }; dispatchWaiters: unknown[] }
function claudeWith(
currentTurnId: string | null = 'turn-1',
unanswered = 0
): { claude: ClaudeSession; state: InFlight } {
const state: InFlight = {
translator: { currentTurnId },
dispatchWaiters: Array.from({ length: unanswered }, () => ({}))
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the wait reads only `translator.currentTurnId` and `dispatchWaiters`, and keys its waiters by the session's identity.
return { claude: state as unknown as ClaudeSession, state }
}
async function settledWithin(promise: Promise<void>, ms: number): Promise<boolean> {
let settled = false
void promise.then(() => {
settled = true
})
await vi.advanceTimersByTimeAsync(ms)
return settled
}
describe("a Stop's wait for Claude to wind down what it has in flight", () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('waits on a send Claude has not echoed, through its turn, to that turn’s end', async () => {
const { claude, state } = claudeWith(null, 1)
const waiting = awaitClaudeRequestEnd(claude, CLAUDE_STOP_GRACE_MS)
state.dispatchWaiters.length = 0
state.translator.currentTurnId = 'turn-1'
settleClaudeTurnEndWaiters(claude)
expect(await settledWithin(waiting, 0)).toBe(false)
state.translator.currentTurnId = null
settleClaudeTurnEndWaiters(claude)
expect(await settledWithin(waiting, 0)).toBe(true)
})
})
@@ -1,84 +0,0 @@
import { AGENT_JOURNAL_THREAD_SCOPE } from '../../../shared/agent-session/journal/agent-session-journal-types'
import { describe, expect, it } from 'vitest'
import type { AgentJournalItemBody } from '../../../shared/agent-session/journal/agent-session-journal-types'
import type { AgentJournalItemIdentity } from '../../../shared/agent-session/journal/agent-session-journal-identity'
import type { StructuredAgentSessionAppendOptions } from '../../native-chat/agent-session-wire/journal-ingress/structured-agent-session-event-sink'
import { ClaudeProvisionalRowCorrections } from './claude-provisional-row-corrections'
import type { ClaudeSubagentLinkageVerdict } from './claude-subagent-linkage'
function identityOf(toolUseId: string): AgentJournalItemIdentity {
return { provider: 'orca', clientMessageId: `claude-tool:claude-session:${toolUseId}` }
}
const RUNNING: AgentJournalItemBody = {
kind: 'tool-call',
callId: 'toolu_2',
name: 'Task',
input: null,
state: 'running'
}
const COMPLETED: AgentJournalItemBody = { ...RUNNING, state: 'completed' }
function ledger(initial: Record<string, ClaudeSubagentLinkageVerdict> = {}) {
const verdicts = new Map(Object.entries(initial))
const rewrites: {
identity: AgentJournalItemIdentity
body: AgentJournalItemBody
options: StructuredAgentSessionAppendOptions
}[] = []
let published = 0
let refuse = false
const settledFor = (ref: string): ClaudeSubagentLinkageVerdict => {
const verdict = verdicts.get(ref)
return verdict && verdict.kind !== 'pending'
? verdict
: { kind: 'linked', linkage: { agentId: ref, providerParentRef: ref, producerKind: 'agent' } }
}
const corrections = new ClaudeProvisionalRowCorrections({
turnScope: () => AGENT_JOURNAL_THREAD_SCOPE,
linkageFor: (ref) => verdicts.get(ref) ?? { kind: 'pending' },
settledLinkageFor: (ref) => {
const verdict = settledFor(ref)
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: `settledFor` returns only the linked arm, never `pending`.
return verdict as Exclude<ClaudeSubagentLinkageVerdict, { kind: 'pending' }>
},
rewrite: (identity, body, options) => {
if (refuse) {
return false
}
rewrites.push({ identity, body, options })
return true
},
publish: () => {
published += 1
}
})
return {
corrections,
rewrites,
announce: (ref: string, agentId: string) =>
verdicts.set(ref, {
kind: 'linked',
linkage: { agentId, providerParentRef: ref, producerKind: 'agent' }
}),
publishes: () => published,
setRefusing: (value: boolean) => {
refuse = value
}
}
}
describe('ClaudeProvisionalRowCorrections', () => {
it('carries the NEWEST body when a row is written provisionally twice', () => {
const { corrections, rewrites, announce } = ledger()
const stamp = corrections.stampFor('toolu_1')
stamp(identityOf('toolu_2'), RUNNING)
stamp(identityOf('toolu_2'), COMPLETED)
announce('toolu_1', 'task-1')
corrections.retry()
expect(rewrites).toHaveLength(1)
expect(rewrites[0]?.body).toEqual(COMPLETED)
})
})
@@ -1,70 +0,0 @@
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, rename, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { resolveAppImageExtractedRoot } from './appimage-extracted-root'
import { resolveAppImageLauncherEndpointPath } from './appimage-stable-launcher'
import type * as PreExtractionModule from './appimage-copy-pre-extraction'
type PreExtraction = typeof PreExtractionModule
const created: string[] = []
let preExtraction: PreExtraction
beforeEach(async () => {
// Why: the module holds this launch's in-flight copy, so every test starts a fresh launch.
vi.resetModules()
preExtraction = await import('./appimage-copy-pre-extraction')
})
afterEach(async () => {
await Promise.all(created.splice(0).map((path) => rm(path, { recursive: true, force: true })))
})
async function makeFixture() {
const root = await mkdtemp(join(tmpdir(), 'orca-appimage-pre-extract-'))
created.push(root)
const appImagePath = join(root, 'apps', 'Orca.AppImage')
await mkdir(join(root, 'apps'), { recursive: true })
await writeFile(appImagePath, 'release 1.0.0', { mode: 0o755 })
return { root, appImagePath, cacheRootPath: join(root, 'cache') }
}
async function writePayload(cwd: string): Promise<void> {
const launcherDir = join(cwd, 'squashfs-root', 'resources', 'bin')
await mkdir(launcherDir, { recursive: true })
await writeFile(join(launcherDir, 'orca-ide'), '', { mode: 0o755 })
}
const acceptCache = async (): Promise<string | null> => null
describe('preExtractDownloadedAppImageUpdate', () => {
it('unpacks the download into the copy its install will run from', async () => {
const { root, appImagePath, cacheRootPath } = await makeFixture()
const downloadedFile = join(root, 'apps', '.pending-Orca.AppImage')
await writeFile(downloadedFile, 'release 2.0.0', { mode: 0o755 })
const staged = await preExtraction.preExtractDownloadedAppImageUpdate(
downloadedFile,
async () => true,
{
appImagePath,
cacheRootPath,
probeCache: acceptCache,
runExtract: async (path, cwd) => {
expect(path).toBe(downloadedFile)
await writePayload(cwd)
},
log: () => {}
}
)
expect(staged).toBe(true)
// What the updater's install does on one filesystem: a rename, which keeps the file identity.
await rename(downloadedFile, appImagePath)
const installed = resolveAppImageExtractedRoot({ appImagePath, cacheRootPath })!
expect(existsSync(installed.payloadLauncherPath)).toBe(true)
expect(existsSync(resolveAppImageLauncherEndpointPath(cacheRootPath, 'installed'))).toBe(false)
})
})
@@ -1,83 +0,0 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../../shared/global-settings-types'
import type { CodexManagedAccount } from '../../../shared/managed-account-types'
import {
readStoredCodexCredentialState,
waitForManagedCodexAuthReady
} from './managed-codex-auth-readiness'
const roots: string[] = []
const testIdToken = 'e30.eyJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20ifQ.sig'
const testChatGptAuth = {
auth_mode: 'chatgpt',
tokens: {
access_token: 'access',
id_token: testIdToken,
refresh_token: 'refresh',
account_id: 'account'
},
last_refresh: '2026-07-31T00:00:00Z'
}
afterEach(() => {
vi.useRealTimers()
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
describe('waitForManagedCodexAuthReady', () => {
it('waits for a partial managed ChatGPT credential to become complete', async () => {
vi.useFakeTimers()
const fixture = createFixture()
writeAuth(fixture.home, {
tokens: { access_token: 'access', id_token: testIdToken }
})
expect(readStoredCodexCredentialState(join(fixture.home, 'auth.json'))).toBe('incomplete')
let resolved = false
const readiness = waitForManagedCodexAuthReady(fixture.args)
void readiness?.then(() => {
resolved = true
})
await vi.advanceTimersByTimeAsync(50)
expect(resolved).toBe(false)
writeAuth(fixture.home, testChatGptAuth)
await vi.advanceTimersByTimeAsync(25)
await expect(readiness).resolves.toBe(true)
})
})
function createFixture(): {
root: string
home: string
args: Parameters<typeof waitForManagedCodexAuthReady>[0]
} {
const root = mkdtempSync(join(tmpdir(), 'orca-managed-codex-auth-'))
roots.push(root)
const home = join(root, 'account', 'home')
mkdirSync(home, { recursive: true })
const account = {
id: 'account-1',
managedHomePath: home,
managedHomeRuntime: 'host'
} as CodexManagedAccount
return {
root,
home,
args: {
codexHomePath: home,
settings: { codexManagedAccounts: [account] } as GlobalSettings,
target: { runtime: 'host' }
}
}
}
function writeAuth(home: string, auth: Record<string, unknown>): void {
writeFileSync(join(home, 'auth.json'), JSON.stringify(auth), { mode: 0o600 })
}
@@ -1,46 +0,0 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { dirname, join, toNamespacedPath } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { prepareCodexSessionResume } from './codex-session-resume-preparation'
const windowsIt = process.platform === 'win32' ? it : it.skip
const cleanupPaths: string[] = []
afterEach(() => {
for (const cleanupPath of cleanupPaths.splice(0)) {
rmSync(cleanupPath, { recursive: true, force: true })
}
})
describe('Codex session resume on native Windows', () => {
windowsIt('resumes an extended-length rollout from its ordinary trusted home', async () => {
const homePath = mkdtempSync(join(tmpdir(), 'orca-codex-resume-'))
cleanupPaths.push(homePath)
const rolloutPath = join(
homePath,
'sessions',
'2026',
'07',
'20',
'rollout-019f81b9-19a9-7651-a8d1-352d9420bd11.jsonl'
)
mkdirSync(dirname(rolloutPath), { recursive: true })
writeFileSync(rolloutPath, '{"type":"session_meta"}\n')
const transcriptPath = toNamespacedPath(rolloutPath)
const resolveVerifiedResumeHome = vi.fn(async () => homePath)
await expect(
prepareCodexSessionResume({
sessionId: '019f81b9-19a9-7651-a8d1-352d9420bd11',
transcriptPath,
trustedCodexHomes: [homePath],
getSelectedAccountCodexHome: () => null,
systemCodexHomePath: homePath,
sharedRuntimeCodexHomePath: null,
resolveVerifiedResumeHome
})
).resolves.toEqual({ outcome: 'resume', codexHomePath: homePath })
expect(resolveVerifiedResumeHome).toHaveBeenCalledWith({ homePath, transcriptPath })
})
})
@@ -1,143 +0,0 @@
import { assert, describe, expect, it } from 'vitest'
import { acquired, fakeCodex } from '../session-lifecycle/codex-structured-session-adapter-fixture'
import type { AgentJournalMessageItem } from '../../../../shared/agent-session/journal/agent-session-journal-types'
import {
queuedMessageWithEdit,
type QueuedMessageEdit
} from '../../../../shared/queued-message-edit'
function skill(name: string, path: string, enabled = true) {
return { name, path, description: `${name} skill`, scope: 'user', enabled }
}
const PERSONAL_REVIEW = '/home/me/.agents/skills/review/SKILL.md'
const REPO_REVIEW = '/work/repo/.agents/skills/review/SKILL.md'
const PET = '/home/me/.codex/plugins/cache/work/pets/skills/create-pet/SKILL.md'
async function settle(): Promise<void> {
for (let turn = 0; turn < 5; turn += 1) {
await Promise.resolve()
}
}
describe('a running Codex chat lists and runs the skills Codex loaded', () => {
it('reports Codex names and files, re-lists on change, and sends a pick by its file', async () => {
let listed = [
skill('review', PERSONAL_REVIEW),
skill('review', REPO_REVIEW),
skill('work-pets:create-pet', PET),
skill('old', '/home/me/.agents/skills/old/SKILL.md', false)
]
const codex = fakeCodex({
'skills/list': () => ({ data: [{ cwd: '/work/repo', skills: listed, errors: [] }] }),
'turn/start': () => ({ turn: { id: 'turn-1' } })
})
const adapter = await acquired(codex)
await settle()
const reported = adapter.readCommands('session-1') ?? []
expect(reported.filter((row) => row.kind === 'command').map((row) => row.name)).toEqual(
expect.arrayContaining(['model', 'clear', 'compact', 'goal'])
)
expect(reported.filter((row) => row.kind === 'skill')).toEqual(
listed
.filter(({ enabled }) => enabled)
.map(({ name, path, description }) => ({
name,
path,
description,
kind: 'skill'
}))
)
listed = [...listed, skill('fresh', '/home/me/.agents/skills/fresh/SKILL.md')]
codex.connections[0].handlers.onNotification?.('skills/changed', {})
await settle()
expect(
codex.connections[0].calls.findLast((call) => call.method === 'skills/list')
).toMatchObject({ params: { cwds: ['/work/repo'], forceReload: true } })
expect(adapter.readCommands('session-1')?.some((row) => row.name === 'fresh')).toBe(true)
await adapter.dispatch({
sessionId: 'session-1',
clientMessageId: 'pick-1',
fence: 7,
body: {
kind: 'message',
role: 'user',
blocks: [{ type: 'text', text: '$review this, then $gone' }],
skills: [
{ name: 'review', path: REPO_REVIEW },
// Removed from the text after selection.
{ name: 'work-pets:create-pet', path: PET },
// Unloaded paths remain plain $name text.
{ name: 'gone', path: '/elsewhere/gone/SKILL.md' }
]
}
})
expect(
codex.connections[0].calls.findLast((call) => call.method === 'turn/start')?.params?.input
).toEqual([
{ type: 'text', text: '$review this, then $gone' },
{ type: 'skill', name: 'review', path: REPO_REVIEW }
])
const text = 'project: $review personal: $review'
const skills = [
{ name: 'review', path: REPO_REVIEW },
{ name: 'review', path: PERSONAL_REVIEW }
]
const body: AgentJournalMessageItem = {
kind: 'message',
role: 'user',
blocks: [
{ type: 'text', text },
{ type: 'image-ref', path: '/work/repo/example.png' }
],
sentAs: 'goal',
from: { kind: 'agent', senders: [], orchestration: null },
skills
}
const { blocks: _blocks, skills: _picks, ...retainedFields } = body
const edit = (body: AgentJournalMessageItem, input: QueuedMessageEdit) => {
const result = queuedMessageWithEdit(body, input)
assert(result.ok)
return result.body
}
const unchanged = edit(body, { text })
expect(unchanged).toBe(body)
expect(unchanged.skills).toBe(skills)
const deleted = edit(body, { text: 'later' })
const retyped = edit(deleted, { text })
for (const [edited, expectedSkills] of [
[unchanged, skills],
[edit(body, { text: 'project: $review' }), []],
[edit(body, { text: `${text} please` }), []],
[
edit(body, {
text,
images: [{ type: 'image-ref', path: '/work/repo/example.png', alt: 'edited' }]
}),
[]
],
[retyped, []],
[edit(body, { text: '/review', skills: [{ name: 'review', path: REPO_REVIEW }] }), []]
] as const) {
expect(edited).toMatchObject(retainedFields)
const adapter = await acquired(codex)
await settle()
const connection = codex.connections.at(-1)!
await adapter.dispatch({
sessionId: 'session-1',
clientMessageId: 'queued-pick',
fence: 7,
body: edited
})
expect(connection.calls.find((call) => call.method === 'turn/start')?.params?.input).toEqual([
...edited.blocks.filter((block) => block.type === 'text'),
{ type: 'localImage', path: '/work/repo/example.png' },
...expectedSkills.map((s) => ({ type: 'skill', ...s }))
])
}
})
})
@@ -1,73 +0,0 @@
import { expect, it } from 'vitest'
import { chat, SESSION, THREAD, turn } from '../threads/codex-thread-catch-up-contract-test-fixture'
import { codexNonMessageIdentity } from './codex-thread-item-identity'
import { codexTurnLifecycleIdentity } from '../journal/codex-structured-journal-translation-turns'
import { agentJournalItemKey } from '../../../../shared/agent-session/journal/agent-session-journal-item-key'
import { projectStructuredItemsToNativeChat } from '../../../../shared/agent-session/view/structured-agent-session-projection'
import { nativeChatGeneratedImageAnswers } from '../../../../shared/native-chat/transcript/images/native-chat-generated-image-answers'
import { readTestJournalItems } from '../../../native-chat/agent-session-journal/journal-observation.test-support'
it.each(['complete', 'sparse'])(
'enriches %s authoritative saved generation once and preserves the result through replay',
async (shape) => {
const c = await chat({ now: 1000 })
const identity = codexNonMessageIdentity(THREAD, 'turn-1', 'image-1')
const key = agentJournalItemKey(identity)
const turnIdentity = codexTurnLifecycleIdentity(SESSION, 'turn-1')
await c.journal.appendItem(
turnIdentity,
{ kind: 'turn', turnId: 'turn-1', state: 'completed', outcome: 'success' },
{ fence: 1, turnScope: { kind: 'thread' } }
)
await c.journal.appendItem(
identity,
{
kind: 'message',
role: 'assistant',
blocks: [
{ type: 'text', text: 'Generated image' },
{ type: 'image-ref', path: '/tmp/elephant.png' }
]
},
{ fence: 1, turnScope: { kind: 'turn', turnItemId: agentJournalItemKey(turnIdentity) } }
)
const supplied = {
...turn(1),
items: [
{
type: 'imageGeneration',
id: 'image-1',
...(shape === 'complete' ? { status: 'completed', savedPath: '/tmp/elephant.png' } : {})
},
{ type: 'agentMessage', id: 'answer', text: 'Here is your elephant.' }
]
}
await c.open([], { returnedTurns: [supplied], paginate: false })
const saved = (await c.journal.reads.item(key))!
expect(saved.body).toEqual({
kind: 'message',
role: 'assistant',
blocks: [
{ type: 'text', text: 'Generated image', presentation: 'generated-image' },
{ type: 'image-ref', path: '/tmp/elephant.png' }
]
})
const messages = projectStructuredItemsToNativeChat(await readTestJournalItems(c.journal))
const projected = nativeChatGeneratedImageAnswers(
messages,
messages.map(() => 'turn-1'),
new Set(['turn-1']),
() => []
)
expect(
projected.find((row) =>
row.blocks.some((block) => block.type === 'text' && block.text === 'Here is your elephant.')
)?.blocks
).toEqual([
{ type: 'text', text: 'Here is your elephant.' },
{ type: 'image-ref', path: '/tmp/elephant.png' }
])
await c.open([], { returnedTurns: [supplied], paginate: false })
expect(await c.journal.reads.item(key)).toEqual(saved)
}
)
@@ -1,185 +0,0 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { createCodexJournalTranslator } from './codex-structured-journal-translation'
import {
LIFECYCLE_KEY,
THREAD_ID,
TURN_ID,
notification,
recorder,
reduced,
translatorFor
} from './codex-turn-lifecycle-contract-test-fixture'
const DIAGNOSTIC = 'Provider diagnostic: internal request failure'
function error(code: unknown, extra: Record<string, unknown> = {}) {
return {
threadId: THREAD_ID,
turnId: TURN_ID,
willRetry: false,
error: { message: DIAGNOSTIC, codexErrorInfo: code },
...extra
}
}
function statuses(tap: ReturnType<typeof recorder>) {
return reduced(tap.rows).flatMap((row) =>
row.body.kind === 'status' && row.body.failure?.kind !== 'providerRetrying' ? [row.body] : []
)
}
describe('Codex terminal failure categories', () => {
it('ends a recorded-shape failed completion carrying an unknown future code', () => {
const frames: {
case: string
method: string
params: {
threadId: string
turn?: { id: string; status: string; error?: Record<string, unknown> }
}
}[] = readFileSync(
join(__dirname, '../../__fixtures__/codex-app-server-turn-endings.jsonl'),
'utf8'
)
.trim()
.split('\n')
.map((line) => JSON.parse(line))
const captured = frames.find(
(frame) => frame.case === '0.141.0-conn-refused' && frame.method === 'turn/completed'
)
if (!captured?.params.turn) {
throw new Error('Missing captured failed completion')
}
const tap = recorder()
const translator = createCodexJournalTranslator({
sink: tap.sink,
sessionId: 'session-1',
primaryThreadId: () => captured.params.threadId
})
const on = (method: string, params: unknown) =>
translator.handle({
type: 'notification',
sessionId: 'session-1',
threadId: captured.params.threadId,
method,
params
})
on('turn/started', { turn: { id: captured.params.turn.id } })
expect(reduced(tap.rows).map((row) => row.body)).toMatchObject([
{ kind: 'turn', state: 'running' }
])
expect(
on('turn/completed', {
...captured.params,
turn: {
...captured.params.turn,
error: { ...captured.params.turn.error, codexErrorInfo: 'futureFailureAfter0167' }
}
})
).toEqual({ accepted: true })
expect(reduced(tap.rows).map((row) => row.body)).toMatchObject([
{ kind: 'turn', state: 'completed', outcome: 'failure' },
{ kind: 'status', failure: { kind: 'providerResponseFailed' } }
])
})
it('shows a completion-only safety stop while preserving an ordinary Stop', () => {
for (const error of [{ message: DIAGNOSTIC, codexErrorInfo: 'tooManyDenials' }, null]) {
const tap = recorder()
const translator = translatorFor(tap)
translator.handle(notification('turn/started', { turn: { id: TURN_ID } }))
translator.handle(
notification('turn/completed', { turn: { id: TURN_ID, status: 'interrupted', error } })
)
expect(reduced(tap.rows).find((row) => row.key === LIFECYCLE_KEY)?.body).toMatchObject({
kind: 'turn',
state: 'interrupted',
outcome: 'cancellation'
})
expect(statuses(tap)).toMatchObject(
error ? [{ failure: { kind: 'providerSafetyStopped' } }] : []
)
}
})
it('keeps completion-only sign-in diagnostics behind Details', () => {
const tap = recorder()
const translator = translatorFor(tap)
translator.handle(notification('turn/started', { turn: { id: TURN_ID } }))
translator.handle(
notification('turn/completed', {
turn: {
id: TURN_ID,
status: 'failed',
error: error({ responseStreamDisconnected: { httpStatusCode: 401 } }).error
}
})
)
expect(statuses(tap)).toMatchObject([{ failure: { kind: 'notSignedIn' } }])
expect(statuses(tap)[0]?.text).not.toContain(DIAGNOSTIC)
expect(statuses(tap)[0]?.providerFrame?.payload.head).toContain(DIAGNOSTIC)
})
it('keeps recovered sign-in advice through completion backpressure without another failure row', () => {
const tap = recorder()
let rejectCompletion = true
tap.sink.tryAppendItem = (identity, body, options) => {
if (body.kind === 'turn' && body.state === 'completed' && rejectCompletion) {
rejectCompletion = false
return { accepted: false, reason: 'backpressure' }
}
tap.sink.appendItem(identity, body, options)
return { accepted: true }
}
const translator = translatorFor(tap)
translator.handle(notification('turn/started', { turn: { id: TURN_ID } }))
translator.handle(
notification('error', {
...error({ responseStreamDisconnected: { httpStatusCode: 401 } }, { willRetry: true }),
error: {
message: 'Reconnecting... 1/2',
codexErrorInfo: { responseStreamDisconnected: { httpStatusCode: 401 } },
additionalDetails: DIAGNOSTIC
}
})
)
translator.handle(notification('error', error('other')))
const completion = notification('turn/completed', {
turn: { id: TURN_ID, status: 'failed', error: error('other').error }
})
expect(translator.handle(completion)).toEqual({ accepted: false, reason: 'backpressure' })
expect(translator.handle(completion)).toEqual({ accepted: true })
const terminal = tap.rows.filter(
(row) => row.body.kind === 'status' && row.body.failure?.kind !== 'providerRetrying'
)
expect(terminal.map((row) => row.body)).toMatchObject([{ failure: { kind: 'notSignedIn' } }])
expect(statuses(tap)[0]?.text).not.toContain(DIAGNOSTIC)
expect(statuses(tap)[0]?.providerFrame?.payload.head).toContain(DIAGNOSTIC)
expect(reduced(tap.rows).find((row) => row.key === LIFECYCLE_KEY)?.body).toMatchObject({
state: 'completed',
outcome: 'failure'
})
})
it('uses the last non-other retry only for matching text and scope, including empty details', () => {
const tap = recorder()
const translator = translatorFor(tap)
const retry = (code: unknown, details: string) =>
notification('error', {
...error(code, { willRetry: true }),
error: { message: DIAGNOSTIC, codexErrorInfo: code, additionalDetails: details }
})
translator.handle(retry({ responseStreamDisconnected: { httpStatusCode: 401 } }, ''))
translator.handle(notification('error', error('other')))
expect(statuses(tap).at(-1)?.failure?.kind).toBe('providerResponseFailed')
translator.handle(retry({ responseStreamDisconnected: { httpStatusCode: 401 } }, DIAGNOSTIC))
translator.handle(retry('futureRetryCode', DIAGNOSTIC))
translator.handle(notification('error', error('other')))
expect(statuses(tap).at(-1)?.failure?.kind).toBe('providerResponseFailed')
translator.handle(retry({ responseStreamDisconnected: { httpStatusCode: 401 } }, DIAGNOSTIC))
translator.handle(notification('error', error('other', { turnId: 'another-turn' })))
expect(statuses(tap).at(-1)?.failure?.kind).toBe('providerResponseFailed')
})
})
@@ -1,119 +0,0 @@
import { describe, expect, it } from 'vitest'
import { parseMinidumpCrashSignature } from './minidump-crash-signature'
const STREAM_TYPE_MODULE_LIST = 4
const STREAM_TYPE_MEMORY_LIST = 5
const STREAM_TYPE_SYSTEM_INFO = 7
const VER_PLATFORM_WIN32_NT = 2
const STREAM_TYPE_EXCEPTION = 6
const CONTEXT_SIZE = 0x4d0
// Electron 43.7.8 x64 bytes at electron.exe+0x23d3f88 for comparison with dump capture.
type FaultDump = {
code: number
rip: bigint
params?: bigint[]
rsp?: bigint
contextFlags?: number
memory?: { start: bigint; bytes: Buffer }[]
modules?: { base: bigint; size: number; name: string }[]
platformId?: number
}
function buildFaultDump(options: FaultDump): Buffer {
const streamCount =
1 +
(options.memory ? 1 : 0) +
(options.modules ? 1 : 0) +
(options.platformId === undefined ? 0 : 1)
let cursor = 32 + streamCount * 12
const regions: Buffer[] = []
const append = (buf: Buffer): number => {
const rva = cursor
regions.push(buf)
cursor += buf.length
return rva
}
const context = Buffer.alloc(CONTEXT_SIZE)
context.writeUInt32LE(options.contextFlags ?? 0x0010001f, 0x30)
context.writeBigUInt64LE(options.rsp ?? 0n, 0x98)
const contextRva = append(context)
const exception = Buffer.alloc(168)
exception.writeUInt32LE(7, 0)
exception.writeUInt32LE(options.code, 8)
exception.writeBigUInt64LE(options.rip, 24)
const params = options.params ?? []
exception.writeUInt32LE(params.length, 32)
params.forEach((value, index) => exception.writeBigUInt64LE(value, 40 + index * 8))
exception.writeUInt32LE(CONTEXT_SIZE, 160)
exception.writeUInt32LE(contextRva, 164)
const streams = [{ type: STREAM_TYPE_EXCEPTION, size: 168, rva: append(exception) }]
if (options.memory) {
const dataRvas = options.memory.map((range) => append(range.bytes))
const list = Buffer.alloc(4 + options.memory.length * 16)
list.writeUInt32LE(options.memory.length, 0)
options.memory.forEach((range, index) => {
list.writeBigUInt64LE(range.start, 4 + index * 16)
list.writeUInt32LE(range.bytes.length, 12 + index * 16)
list.writeUInt32LE(dataRvas[index], 16 + index * 16)
})
streams.push({ type: STREAM_TYPE_MEMORY_LIST, size: list.length, rva: append(list) })
}
if (options.modules) {
const nameRvas = options.modules.map((module) => {
const name = Buffer.from(module.name, 'utf16le')
const length = Buffer.alloc(4)
length.writeUInt32LE(name.length, 0)
return append(Buffer.concat([length, name]))
})
const list = Buffer.alloc(4 + options.modules.length * 108)
list.writeUInt32LE(options.modules.length, 0)
options.modules.forEach((module, index) => {
list.writeBigUInt64LE(module.base, 4 + index * 108)
list.writeUInt32LE(module.size, 12 + index * 108)
list.writeUInt32LE(nameRvas[index], 24 + index * 108)
})
streams.push({ type: STREAM_TYPE_MODULE_LIST, size: list.length, rva: append(list) })
}
if (options.platformId !== undefined) {
const systemInfo = Buffer.alloc(56)
systemInfo.writeUInt32LE(options.platformId, 20)
streams.push({ type: STREAM_TYPE_SYSTEM_INFO, size: 56, rva: append(systemInfo) })
}
const header = Buffer.alloc(32 + streamCount * 12)
header.writeUInt32LE(0x504d444d, 0)
header.writeUInt32LE(streamCount, 8)
header.writeUInt32LE(32, 12)
streams.forEach((stream, index) => {
header.writeUInt32LE(stream.type, 32 + index * 12)
header.writeUInt32LE(stream.size, 36 + index * 12)
header.writeUInt32LE(stream.rva, 40 + index * 12)
})
return Buffer.concat([header, ...regions])
}
describe('minidump fault context', () => {
it('keeps only code bytes for non-AV exceptions and truncates at the captured range', async () => {
const signature = await parseMinidumpCrashSignature(
buildFaultDump({
code: 0x80000003,
rip: 0x5000n,
params: [1n, 0x20n],
memory: [
{ start: 0x100n, bytes: Buffer.from('aa', 'hex') },
{ start: 0x4ffen, bytes: Buffer.from('9090cc0f0b', 'hex') }
],
platformId: VER_PLATFORM_WIN32_NT,
modules: [{ base: 0x4000n, size: 0x2000, name: 'Orca.exe' }]
})
)
expect(signature?.fault).toEqual({ codeBytes: 'cc0f0b' })
})
})
@@ -1,38 +0,0 @@
import { describe, expect, it } from 'vitest'
import { AvccFrameParser, type AvccFrame } from './avcc-frame-parser'
function packet(tag: number, payload: number[]): number[] {
const length = payload.length + 1
return [
length >>> 24,
(length >>> 16) & 0xff,
(length >>> 8) & 0xff,
length & 0xff,
tag,
...payload
]
}
function parse(chunks: number[][]): { frame: AvccFrame; bytes: number[] }[] {
const frames: { frame: AvccFrame; bytes: number[] }[] = []
const parser = new AvccFrameParser((frame) =>
frames.push({ frame, bytes: [...new Uint8Array(frame.bytes)] })
)
for (const chunk of chunks) {
parser.push(Uint8Array.from(chunk))
}
return frames
}
describe('AvccFrameParser', () => {
it('reassembles description, IDR and delta packets split anywhere across chunks', () => {
const wire = [...packet(1, [1, 0x64, 0, 0x33]), ...packet(2, [9, 9, 9]), ...packet(3, [7])]
const frames = parse([wire.slice(0, 3), wire.slice(3, 11), wire.slice(11, 12), wire.slice(12)])
expect(frames.map(({ frame, bytes }) => [frame.config, frame.keyFrame, bytes])).toEqual([
[true, false, [1, 0x64, 0, 0x33]],
[false, true, [9, 9, 9]],
[false, false, [7]]
])
})
})
@@ -1,202 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import {
runEnvironmentDoctor,
type EnvironmentDoctorCommandResult,
type EnvironmentDoctorDeps
} from './environment-doctor'
const ROOT = '/work/repo'
const ok = (stdout = ''): EnvironmentDoctorCommandResult => ({
code: 0,
stdout,
stderr: '',
timedOut: false
})
const failed = (stderr: string, code = 128): EnvironmentDoctorCommandResult => ({
code,
stdout: '',
stderr,
timedOut: false
})
function makeDeps(options: {
files?: Record<string, string>
installed?: string[]
agents?: string[]
git?: (args: readonly string[]) => EnvironmentDoctorCommandResult
agentStatus?: Record<string, EnvironmentDoctorCommandResult>
freeBytes?: number
setup?: EnvironmentDoctorCommandResult
}): EnvironmentDoctorDeps {
const files = options.files ?? {}
return {
platform: 'linux',
prepare: async () => {},
run: async (program, args) => {
if (program === 'git') {
return options.git?.(args) ?? failed('not a git repository')
}
return options.agentStatus?.[program] ?? ok()
},
findCommands: async (commands) =>
new Set(commands.filter((command) => (options.installed ?? ['git']).includes(command))),
detectAgents: async () => options.agents ?? [],
readFile: async (path) => files[path.replace(`${ROOT}/`, '')] ?? null,
exists: async (path) => path.replace(`${ROOT}/`, '') in files,
isDirectory: async (path) => path === ROOT || path.startsWith(`${ROOT}/`),
freeBytes: async () => options.freeBytes ?? 50 * 1024 ** 3,
runSetupScript: vi.fn(async () => options.setup ?? ok())
}
}
function gitRepo(overrides: Record<string, EnvironmentDoctorCommandResult> = {}) {
return (args: readonly string[]): EnvironmentDoctorCommandResult => {
const key = args.join(' ')
for (const [prefix, result] of Object.entries(overrides)) {
if (key.startsWith(prefix)) {
return result
}
}
if (key.startsWith('rev-parse --is-inside-work-tree')) {
return ok(`true\n${ROOT}\n/work/main/.git\n`)
}
if (key === 'symbolic-ref --quiet --short HEAD') {
return ok('feature\n')
}
if (key === 'remote') {
return ok('origin\n')
}
return ok()
}
}
function statuses(result: Awaited<ReturnType<typeof runEnvironmentDoctor>>) {
return Object.fromEntries(result.checks.map((check) => [check.id, check.status]))
}
describe('runEnvironmentDoctor', () => {
it('fails on a missing repo-implied CLI but only warns on a signed-out agent', async () => {
const result = await runEnvironmentDoctor(
{ worktreePath: ROOT },
makeDeps({
files: { 'package.json': '{"packageManager":"pnpm@10.0.0"}' },
installed: ['git', 'node', 'claude', 'codex'],
agents: ['claude', 'codex', 'gemini'],
agentStatus: { claude: failed('', 1) },
git: gitRepo()
})
)
expect(result.ok).toBe(false)
expect(statuses(result)).toMatchObject({
'tool.git': 'pass',
'tool.node': 'pass',
'tool.pnpm': 'fail',
'agent.claude': 'warn',
'agent.codex': 'pass',
'agent.cli': 'pass',
'git.fetch': 'pass',
'git.push': 'pass',
'setup.script': 'warn',
'disk.free': 'pass'
})
expect(result.checks.find((check) => check.id === 'agent.claude')?.remediation).toContain(
'claude auth login'
)
})
it('reports an unreachable remote without leaking the credential in its URL', async () => {
const result = await runEnvironmentDoctor(
{ worktreePath: ROOT },
makeDeps({
git: gitRepo({
'ls-remote': failed(
"fatal: unable to access 'https://x-access-token:ghp_secret123@github.com/o/r.git/': 403"
)
})
})
)
expect(statuses(result)['git.fetch']).toBe('fail')
expect(statuses(result)['git.push']).toBeUndefined()
expect(JSON.stringify(result)).not.toContain('ghp_secret123')
})
it('probes push with a dry run that skips hooks and targets a probe ref', async () => {
const calls: string[] = []
const deps = makeDeps({
git: (args) => {
calls.push(args.join(' '))
return gitRepo({ push: failed('remote: Permission denied', 1) })(args)
}
})
const result = await runEnvironmentDoctor({ worktreePath: ROOT }, deps)
expect(statuses(result)['git.push']).toBe('fail')
expect(calls).toContain(
'push --dry-run --no-verify --porcelain origin HEAD:refs/heads/orca-env-doctor-push-probe'
)
})
it('runs the setup script only when asked and gates on its exit code', async () => {
const files = { 'orca.yaml': 'scripts:\n setup: pnpm install\n' }
const passive = makeDeps({ files, git: gitRepo() })
const passiveResult = await runEnvironmentDoctor({ worktreePath: ROOT }, passive)
expect(passive.runSetupScript).not.toHaveBeenCalled()
expect(statuses(passiveResult)['setup.run']).toBe('warn')
const active = makeDeps({ files, git: gitRepo(), setup: failed('ERR_PNPM_NO_LOCKFILE', 1) })
const activeResult = await runEnvironmentDoctor({ worktreePath: ROOT, runSetup: true }, active)
expect(active.runSetupScript).toHaveBeenCalledWith({
worktreePath: ROOT,
rootPath: '/work/main',
script: 'pnpm install'
})
expect(activeResult.ok).toBe(false)
expect(activeResult.checks.find((check) => check.id === 'setup.run')).toMatchObject({
status: 'fail',
message: 'Setup script exited with code 1.'
})
expect(activeResult.checks.find((check) => check.id === 'setup.run')?.remediation).toContain(
'ERR_PNPM_NO_LOCKFILE'
)
})
it('reads the repo root when started in a subfolder, and fails on an unreadable checkout', async () => {
const files = { 'package.json': '{"packageManager":"pnpm@10.0.0"}' }
const fromSubfolder = await runEnvironmentDoctor(
{ worktreePath: `${ROOT}/src` },
makeDeps({ files, installed: ['git', 'node', 'pnpm'], git: gitRepo() })
)
expect(fromSubfolder.worktreePath).toBe(ROOT)
expect(statuses(fromSubfolder)['tool.pnpm']).toBe('pass')
const dubious = await runEnvironmentDoctor(
{ worktreePath: ROOT },
makeDeps({
git: gitRepo({
'rev-parse --is-inside-work-tree': failed(
"fatal: detected dubious ownership in repository at '/work/repo'"
)
})
})
)
expect(statuses(dubious)['git.repo']).toBe('fail')
expect(dubious.ok).toBe(false)
})
it('skips Git checks for a folder workspace and fails on a nearly full disk', async () => {
const result = await runEnvironmentDoctor(
{ worktreePath: ROOT },
makeDeps({ freeBytes: 512 * 1024 ** 2 })
)
expect(statuses(result)).toMatchObject({
'git.repo': 'pass',
'agent.cli': 'warn',
'disk.free': 'fail'
})
expect(statuses(result)['git.fetch']).toBeUndefined()
expect(result.ok).toBe(false)
})
})
@@ -1,92 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { applicationInfo, openPath, runProcess } = vi.hoisted(() => ({
applicationInfo: vi.fn(),
openPath: vi.fn(),
runProcess: vi.fn()
}))
vi.mock('electron', () => ({
app: { getApplicationInfoForProtocol: applicationInfo },
shell: { openPath }
}))
vi.mock('@orca/process-host', () => ({ runProcess }))
import { openPathInDefaultApp } from './default-app-location-open'
import type { RuntimeFileOpenPosition } from '../../shared/file-access/runtime-editor-file-open'
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')!
const filePath = '/tmp/project with spaces/main #1.json'
function setPlatform(value: NodeJS.Platform): void {
Object.defineProperty(process, 'platform', { configurable: true, value })
}
describe('openPathInDefaultApp', () => {
beforeEach(() => {
vi.resetAllMocks()
setPlatform('darwin')
applicationInfo.mockResolvedValue({ path: '/Applications/Cursor.app' })
runProcess.mockResolvedValue({ code: 0, timedOut: false })
openPath.mockResolvedValue('')
})
afterEach(() => {
Object.defineProperty(process, 'platform', originalPlatform)
})
it.each([
['Cursor.app', { line: 551, column: 7 }, '551:7'],
['Visual Studio Code.app', { line: 551 }, '551'],
['Visual Studio Code - Insiders.app', { line: 551, column: 7 }, '551:7']
])(
'hands the line to a default %s instead of the OS opener',
async (bundle, location, suffix) => {
const appPath = `/Users/ada/Applications/${bundle}`
applicationInfo.mockResolvedValue({ path: appPath })
await expect(openPathInDefaultApp(filePath, location)).resolves.toBe('')
expect(applicationInfo).toHaveBeenCalledWith(
'file:///tmp/project%20with%20spaces/main%20%231.json'
)
expect(runProcess).toHaveBeenCalledWith(
expect.objectContaining({
program: '/usr/bin/open',
args: ['-n', '-g', '-a', appPath, '--args', '--goto', `${filePath}:${suffix}`],
env: {},
timeoutMs: null
})
)
expect(openPath).not.toHaveBeenCalled()
}
)
it.each<[string, () => void]>([
['another default app', () => applicationInfo.mockResolvedValue({ path: '/A/Zed.app' })],
['a failed association lookup', () => applicationInfo.mockRejectedValue(new Error('none'))],
['an open that exits non-zero', () => runProcess.mockResolvedValue({ code: 1 })],
['an open that cannot spawn', () => runProcess.mockRejectedValue(new Error('ENOENT'))]
])('opens once with the OS opener on %s', async (_name, arrange) => {
arrange()
openPath.mockResolvedValue('no app')
await expect(openPathInDefaultApp(filePath, { line: 551 })).resolves.toBe('no app')
expect(openPath).toHaveBeenCalledTimes(1)
expect(openPath).toHaveBeenCalledWith(filePath)
})
it.each<[string, NodeJS.Platform, RuntimeFileOpenPosition | undefined]>([
['links without a line', 'darwin', undefined],
['a zero line', 'darwin', { line: 0 }],
['a fractional line', 'darwin', { line: 1.5 }],
['a zero column', 'darwin', { line: 551, column: 0 }],
['Linux', 'linux', { line: 551 }],
['Windows', 'win32', { line: 551 }]
])('leaves %s to the OS opener', async (_name, platform, location) => {
setPlatform(platform)
await expect(openPathInDefaultApp(filePath, location)).resolves.toBe('')
expect(applicationInfo).not.toHaveBeenCalled()
expect(runProcess).not.toHaveBeenCalled()
expect(openPath).toHaveBeenCalledWith(filePath)
})
})
@@ -1,43 +0,0 @@
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { resolveGitCommandWithoutProbe } from './git-command-resolution'
import {
recordHostAutocrlf,
recordLineEndingSample,
resetWslWindowsCheckoutGitConfigForTests,
wslWindowsCheckoutKey
} from './wsl-windows-checkout-git-config'
import { resetWslLinkedWorktreeGitRoutingForTests } from './wsl-linked-worktree-git-routing'
const originalPlatform = process.platform
const DRIVE_REPO = String.raw`C:\Users\dev\repo`
function detectWindowsCheckout(): void {
const key = wslWindowsCheckoutKey(DRIVE_REPO, 'Ubuntu')
recordHostAutocrlf(key, 'true', Date.now())
recordLineEndingSample(key, 'windows', 'clone', Date.now())
}
describe('WSL git on a Windows Git checkout', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
resetWslWindowsCheckoutGitConfigForTests()
resetWslLinkedWorktreeGitRoutingForTests()
})
it('runs WSL git with Windows Git line-ending and stat settings once detected', () => {
detectWindowsCheckout()
const resolved = resolveGitCommandWithoutProbe(['add', '--', 'a.ts'], {
cwd: DRIVE_REPO,
wslDistro: 'Ubuntu'
})
expect(resolved.binary).toBe('wsl.exe')
expect(resolved.args.join(' ')).toMatch(/core\.autocrlf=true.+core\.checkStat=minimal.+add/)
expect(resolved.args.join(' ')).not.toContain('trustctime')
})
})
@@ -1,214 +0,0 @@
import { EventEmitter } from 'node:events'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, spawnMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('node:child_process', () => ({
execFile: execFileMock,
execFileSync: vi.fn(),
spawn: spawnMock
}))
import {
awaitWindowsHostGitEnvironmentReady,
configureWindowsHostGitEnvironmentReadiness
} from './windows-host-git-environment'
import { gitExecFileAsync, gitExecFileAsyncBuffer } from '../git-exec-file'
import { gitSpawnAfterWindowsEnvironmentReady } from '../git-spawn'
import { gitStreamStdout } from '../git-stream-stdout'
import { _resetGitAdmissionForTests } from '../admission/git-subprocess-admission'
type MockChildProcess = EventEmitter & {
stdout: EventEmitter
stderr: EventEmitter
pid: number
kill: ReturnType<typeof vi.fn>
}
function createMockChildProcess(pid: number): MockChildProcess {
const child = new EventEmitter() as MockChildProcess
child.stdout = new EventEmitter()
child.stderr = new EventEmitter()
child.pid = pid
child.kill = vi.fn()
return child
}
function deferred(): { promise: Promise<void>; resolve: () => void } {
let resolve!: () => void
const promise = new Promise<void>((next) => {
resolve = next
})
return { promise, resolve }
}
async function withPlatform<T>(platform: NodeJS.Platform, fn: () => Promise<T>): Promise<T> {
const original = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
try {
return await fn()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: original })
}
}
describe('Windows host Git environment readiness', () => {
const originalPath = process.env.Path
beforeEach(() => {
execFileMock.mockReset()
spawnMock.mockReset()
})
afterEach(() => {
_resetGitAdmissionForTests()
configureWindowsHostGitEnvironmentReadiness(null)
if (originalPath === undefined) {
delete process.env.Path
} else {
process.env.Path = originalPath
}
})
it('does not gate spawned Git routed through WSL on the Windows host profile', async () => {
await withPlatform('win32', async () => {
const waitUntilReady = vi.fn(() => new Promise<void>(() => {}))
const child = createMockChildProcess(1234)
spawnMock.mockReturnValue(child)
execFileMock.mockImplementation((_cmd, _args, _options, callback) => {
callback(Object.assign(new Error('missing SSH config'), { code: 1 }), '', '')
return child
})
configureWindowsHostGitEnvironmentReadiness(waitUntilReady)
await expect(
gitSpawnAfterWindowsEnvironmentReady(['clone', 'url', 'repo'], {
cwd: String.raw`C:\repo`,
wslDistro: 'Ubuntu',
stdio: ['ignore', 'ignore', 'pipe']
})
).resolves.toBe(child)
expect(waitUntilReady).not.toHaveBeenCalled()
expect(spawnMock).toHaveBeenCalled()
})
})
it('does not spawn Git when canceled during Windows environment readiness', async () => {
await withPlatform('win32', async () => {
const ready = deferred()
const controller = new AbortController()
configureWindowsHostGitEnvironmentReadiness(() => ready.promise)
const spawned = gitSpawnAfterWindowsEnvironmentReady(['status'], {
cwd: String.raw`C:\repo`,
signal: controller.signal,
stdio: ['ignore', 'ignore', 'pipe']
})
controller.abort()
await expect(spawned).rejects.toMatchObject({ name: 'AbortError' })
expect(spawnMock).not.toHaveBeenCalled()
ready.resolve()
})
})
it('cancels async Git immediately while Windows environment readiness is pending', async () => {
await withPlatform('win32', async () => {
const ready = deferred()
const controller = new AbortController()
configureWindowsHostGitEnvironmentReadiness(() => ready.promise)
const operation = gitExecFileAsync(['status'], {
cwd: String.raw`C:\repo`,
signal: controller.signal
})
controller.abort()
await expect(operation).rejects.toMatchObject({ name: 'AbortError' })
expect(execFileMock).not.toHaveBeenCalled()
ready.resolve()
})
})
it('does not gate WSL paths at a synchronous Git consumer boundary', async () => {
await withPlatform('win32', async () => {
const waitUntilReady = vi.fn(() => new Promise<void>(() => {}))
configureWindowsHostGitEnvironmentReadiness(waitUntilReady)
await expect(
awaitWindowsHostGitEnvironmentReady({
cwd: String.raw`\\wsl.localhost\Ubuntu\home\me\repo`
})
).resolves.toBeUndefined()
expect(waitUntilReady).not.toHaveBeenCalled()
})
})
it('does not gate Git routed through WSL on the Windows host profile', async () => {
await withPlatform('win32', async () => {
const waitUntilReady = vi.fn(() => new Promise<void>(() => {}))
const child = createMockChildProcess(1234)
execFileMock.mockImplementation((_cmd, _args, _options, callback) => {
callback(null, 'ok', '')
return child
})
configureWindowsHostGitEnvironmentReadiness(waitUntilReady)
await expect(
gitExecFileAsync(['status'], {
cwd: String.raw`C:\repo`,
wslDistro: 'Ubuntu'
})
).resolves.toEqual({ stdout: 'ok', stderr: '' })
expect(waitUntilReady).not.toHaveBeenCalled()
expect(execFileMock).toHaveBeenCalled()
})
})
it('gates buffered native Git reads on the current generation', async () => {
await withPlatform('win32', async () => {
const ready = deferred()
const child = createMockChildProcess(1234)
execFileMock.mockImplementation((_cmd, _args, _options, callback) => {
callback(null, Buffer.from('blob'), Buffer.alloc(0))
return child
})
configureWindowsHostGitEnvironmentReadiness(() => ready.promise)
const read = gitExecFileAsyncBuffer(['show', 'HEAD:file'], { cwd: String.raw`C:\repo` })
expect(execFileMock).not.toHaveBeenCalled()
ready.resolve()
await expect(read).resolves.toEqual({ stdout: Buffer.from('blob') })
})
})
it('gates native Git streams and refreshes their snapshotted PATH', async () => {
await withPlatform('win32', async () => {
const ready = deferred()
const child = createMockChildProcess(1234)
spawnMock.mockReturnValue(child)
configureWindowsHostGitEnvironmentReadiness(() => ready.promise)
process.env.Path = 'hydrated-stream-path'
const stream = gitStreamStdout(['status'], {
cwd: String.raw`C:\repo`,
env: { Path: 'stale-stream-path' },
onStdout: () => {}
})
expect(spawnMock).not.toHaveBeenCalled()
ready.resolve()
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledOnce())
expect(spawnMock.mock.calls[0]?.[2]?.env?.Path).toBe('hydrated-stream-path')
child.emit('close', 0)
await expect(stream).resolves.toEqual({ stoppedEarly: false })
})
})
})
@@ -1,75 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { WslResult } from '../../../wsl/execution/wsl-runner'
import {
pendingWslWindowsCheckoutGitConfig,
resetWslWindowsCheckoutProbesForTests,
type HostGitResult,
type WslWindowsCheckoutProbeDeps
} from './wsl-windows-checkout-git-probe'
import {
resetWslWindowsCheckoutGitConfigForTests,
withWslWindowsCheckoutGitConfig
} from './wsl-windows-checkout-git-config'
import {
HOST_CONFIG_SYSTEM_AUTOCRLF,
PLAIN_WINDOWS_CLONE_SAMPLE
} from './wsl-windows-checkout-line-endings.test-fixture'
afterEach(() => {
resetWslWindowsCheckoutGitConfigForTests()
resetWslWindowsCheckoutProbesForTests()
})
const CWD = String.raw`C:\Users\dev\repo`
const READ = ['status', '--porcelain=v2']
function host(overrides: Partial<HostGitResult> = {}): HostGitResult {
return { code: 0, stdout: HOST_CONFIG_SYSTEM_AUTOCRLF, stderr: '', timedOut: false, ...overrides }
}
function wsl(overrides: Partial<WslResult> = {}): WslResult {
return {
environmentResolved: true,
code: 0,
stdout: PLAIN_WINDOWS_CLONE_SAMPLE,
stderr: '',
timedOut: false,
outputTruncated: false,
...overrides
}
}
type HostRunner = NonNullable<WslWindowsCheckoutProbeDeps['runHostGit']>
type WslRunner = NonNullable<WslWindowsCheckoutProbeDeps['runWsl']>
type IdentityReader = NonNullable<WslWindowsCheckoutProbeDeps['checkoutIdentity']>
function harness(
overrides: { runHostGit?: HostRunner; runWsl?: WslRunner; checkoutIdentity?: IdentityReader } = {}
) {
const clock = { now: 1_000_000 }
const deps = {
platform: 'win32' as const,
now: () => clock.now,
runHostGit: vi.fn<HostRunner>(overrides.runHostGit ?? (async () => host())),
runWsl: vi.fn<WslRunner>(overrides.runWsl ?? (async () => wsl())),
checkoutIdentity: vi.fn<IdentityReader>(overrides.checkoutIdentity ?? (async () => 'clone-1')),
admit: vi.fn(async () => ({ queueWaitMs: 0, release: vi.fn() }))
}
const run = (args: string[], signal?: AbortSignal) =>
pendingWslWindowsCheckoutGitConfig(args, { cwd: CWD, wslDistro: 'Ubuntu', signal }, deps)
const flagged = (args: string[]) =>
withWslWindowsCheckoutGitConfig(args, CWD, 'Ubuntu', 'win32', clock.now).includes(
'core.autocrlf=true'
)
return { clock, deps, run, flagged }
}
describe('pendingWslWindowsCheckoutGitConfig', () => {
it('stops waiting when the command aborts', async () => {
const { run } = harness({ runHostGit: vi.fn(() => new Promise<HostGitResult>(() => {})) })
const controller = new AbortController()
const waiting = run(READ, controller.signal)
controller.abort()
await expect(waiting).resolves.toBeUndefined()
})
})
@@ -1,48 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock, execFileSyncMock, spawnMock } = vi.hoisted(() => ({
execFileMock: vi.fn(),
execFileSyncMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('child_process', () => ({
execFile: execFileMock,
execFileSync: execFileSyncMock,
spawn: spawnMock
}))
import { fakeSpawnReturning } from '../../../../shared/__fixtures__/fake-spawned-child'
import { ghExecFileAsync } from './gh-exec-file'
import { _resetGhRateLimitBreaker } from './gh-rate-limit-breaker'
const PRIMARY_RATE_LIMIT_STDERR =
'gh: API rate limit exceeded for user ID 1775218. Please wait. (HTTP 403)'
function mockGhFailure(stderr: string): void {
spawnMock.mockImplementation(fakeSpawnReturning({ stderr, code: 1 }))
}
beforeEach(() => {
execFileMock.mockReset()
spawnMock.mockReset()
})
afterEach(() => {
_resetGhRateLimitBreaker()
})
describe('ghExecFileAsync rate-limit breaker', () => {
it('spawns once for a primary 403, then short-circuits same-bucket calls', async () => {
mockGhFailure(PRIMARY_RATE_LIMIT_STDERR)
await expect(
ghExecFileAsync(['api', '--cache', '120s', 'search/issues?q=repo:a/b&per_page=1'])
).rejects.toThrow('rate limit')
expect(spawnMock).toHaveBeenCalledTimes(1)
await expect(
ghExecFileAsync(['api', '--cache', '120s', 'search/issues?q=repo:c/d&per_page=1'])
).rejects.toMatchObject({ ghRateLimitBlocked: true })
expect(spawnMock).toHaveBeenCalledTimes(1)
})
})
@@ -1,40 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { execFileMock } = vi.hoisted(() => ({ execFileMock: vi.fn() }))
vi.mock('node:child_process', () => ({
execFile: execFileMock,
execFileSync: vi.fn(),
spawn: vi.fn()
}))
import { gitExecFileAsyncBuffer } from './git-exec-file'
import { _resetGitAdmissionForTests } from './admission/git-subprocess-admission'
import { configureWindowsHostGitEnvironmentReadiness } from './host-routing/windows-host-git-environment'
beforeEach(() => execFileMock.mockReset())
afterEach(() => {
_resetGitAdmissionForTests()
configureWindowsHostGitEnvironmentReadiness(null)
})
describe('buffered Git cancellation', () => {
it('cancels Windows environment readiness without launching Git', async () => {
const platform = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
try {
configureWindowsHostGitEnvironmentReadiness(() => new Promise(() => {}))
const controller = new AbortController()
const read = gitExecFileAsyncBuffer(['show', 'HEAD:file'], {
cwd: String.raw`C:\repo`,
signal: controller.signal
})
const rejected = expect(read).rejects.toMatchObject({ name: 'AbortError' })
controller.abort()
await rejected
expect(execFileMock).not.toHaveBeenCalled()
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
}
})
})
@@ -1,47 +0,0 @@
import { execFileSync } from 'node:child_process'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { getBranchConflictKind } from './repo-branch-conflict'
describe('branch conflict real Git contract', () => {
const tempPaths: string[] = []
afterEach(() => {
for (const path of tempPaths.splice(0)) {
rmSync(path, { recursive: true, force: true })
}
})
it('decides remote conflicts from one batched probe across many remotes', async () => {
const repoPath = mkdtempSync(join(tmpdir(), 'orca-branch-conflict-'))
tempPaths.push(repoPath)
const git = (...args: string[]): string =>
execFileSync('git', args, { cwd: repoPath, encoding: 'utf8' })
git('init', '--quiet')
git('config', 'user.name', 'Orca Test')
git('config', 'user.email', 'orca@example.test')
git('config', 'commit.gpgSign', 'false')
git('config', 'core.hooksPath', '.git/no-hooks')
writeFileSync(join(repoPath, 'fixture.txt'), 'base\n')
git('add', 'fixture.txt')
git('commit', '--quiet', '-m', 'base')
const head = git('rev-parse', 'HEAD').trim()
for (let index = 0; index < 12; index += 1) {
git('remote', 'add', `remote${index}`, 'https://example.test/repo.git')
}
git('update-ref', 'refs/remotes/remote7/taken', head)
await expect(getBranchConflictKind(repoPath, 'taken')).resolves.toBe('remote')
await expect(getBranchConflictKind(repoPath, 'free')).resolves.toBeNull()
await expect(
getBranchConflictKind(repoPath, 'taken', 'refs/remotes/remote7/taken')
).resolves.toBeNull()
git('branch', 'local-only', head)
await expect(getBranchConflictKind(repoPath, 'local-only')).resolves.toBe('local')
})
})
@@ -1,39 +0,0 @@
import { afterEach, expect, it, vi } from 'vitest'
const stream = vi.hoisted(() => vi.fn())
vi.mock('../../command-runner/git-stream-stdout', () => ({ gitStreamStdout: stream }))
import { readBranchRefSnapshot } from './branch-ref-snapshot-read'
afterEach(() => stream.mockReset())
it('keeps raw output complete under aggregate pressure and distinguishes an intrinsic bound', async () => {
stream.mockImplementation(async (_args, options) => {
for (const name of ['first', 'second']) {
if (options.onStdout(`refs/heads/${name}\0${'a'.repeat(40)}\u00001\0\n`) === true) {
return { stoppedEarly: true }
}
}
return { stoppedEarly: false }
})
const page: string[] = []
const pressured = await readBranchRefSnapshot(
'/repo',
{},
(ref) => {
page.push(ref.refName)
return page.length === 2
},
undefined,
{ reserve: () => false, release: () => {} }
)
expect(page).toEqual(['refs/heads/first', 'refs/heads/second'])
expect(pressured).toEqual({ snapshot: null, oversized: false })
expect(await readBranchRefSnapshot('/repo', {}, () => true, 100)).toEqual({
snapshot: null,
oversized: true
})
const recovered = await readBranchRefSnapshot('/repo', {}, () => true)
expect(recovered.snapshot?.refs.map((ref) => ref.refName)).toEqual([
'refs/heads/first',
'refs/heads/second'
])
})
@@ -1,105 +0,0 @@
import { afterEach, expect, it, vi } from 'vitest'
import type * as FsPromises from 'node:fs/promises'
import type * as SnapshotReader from './branch-ref-snapshot-read'
import type * as DistroCatalog from '../../../wsl/discovery/distro-catalog'
const cwd = String.raw`C:\repo-linked`
const commonDir = String.raw`C:\repo-main\.git`
const gitDir = String.raw`C:\repo-main\.git\worktrees\linked`
const io = vi.hoisted(() => ({ remote: vi.fn(), refs: vi.fn() }))
vi.mock('../../../wsl/discovery/distro-catalog', async (original) => ({
...(await original<typeof DistroCatalog>()),
listRunningWslDistrosAsync: async () => ['Ubuntu']
}))
vi.mock('node:fs/promises', async (original) => ({
...(await original<typeof FsPromises>()),
realpath: async (path: string) => path,
stat: async (path: string) => ({
isFile: () => path.endsWith('.git'),
isDirectory: () => !path.endsWith('.git'),
dev: 1n,
ino: 2n,
birthtimeNs: 3n
}),
readFile: async () => String.raw`gitdir: C:\repo-main\.git\worktrees\linked`
}))
vi.mock('../../../../shared/git/metadata/git-common-directory', () => ({
resolveGitCommonDirectory: async () => String.raw`C:\repo-main\.git`
}))
vi.mock('../../source-control/resolve-git-dir', () => ({
resolveGitDir: async () => String.raw`C:\repo-main\.git\worktrees\linked`
}))
vi.mock('./branch-ref-snapshot-read', async (original) => ({
...(await original<typeof SnapshotReader>()),
readBranchRemoteNames: io.remote,
readBranchRefSnapshot: io.refs
}))
import { searchBranchRefSnapshot, _resetBranchRefSnapshotsForTests } from './branch-ref-snapshot'
import {
prepareWslLinkedWorktreeGitRouting,
resetWslLinkedWorktreeGitRoutingForTests
} from '../../command-runner/host-routing/wsl-linked-worktree-git-routing'
import { resolveBranchSnapshotIdentity } from './branch-ref-snapshot-identity'
import { trackGitRefMutation } from '../../command-runner/git-ref-mutation-state'
import { subscribeGitRefChanges } from '../git-ref-change-signal'
import { _resetWslBackgroundDistroGateForTests } from '../../../wsl/discovery/wsl-background-distro-gate'
const platform = process.platform
afterEach(() => {
_resetBranchRefSnapshotsForTests()
resetWslLinkedWorktreeGitRoutingForTests()
_resetWslBackgroundDistroGateForTests()
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
vi.restoreAllMocks()
io.remote.mockReset()
io.refs.mockReset()
})
it('keeps a live host snapshot across expiry of the filesystem-only linked-route proof', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
const clock = vi.spyOn(Date, 'now').mockReturnValue(10_000)
// The reader boundary models execution's existing route settlement; it is not a real WSL benchmark.
io.remote.mockImplementation(async () => {
await prepareWslLinkedWorktreeGitRouting(cwd, 'Ubuntu')
return []
})
io.refs.mockResolvedValue({
snapshot: {
refs: [
{
refName: 'refs/heads/topic',
label: 'topic',
searchName: 'topic',
oid: 'a'.repeat(40),
committedAt: 0,
symref: ''
}
]
},
oversized: false
})
expect(await searchBranchRefSnapshot(cwd, 'topic', 20, { wslDistro: 'Ubuntu' })).toEqual([
{ refName: 'refs/heads/topic', localBranchName: 'topic' }
])
clock.mockReturnValue(40_001)
expect(await resolveBranchSnapshotIdentity(cwd, { wslDistro: 'Ubuntu' })).toMatchObject({
commonDir,
gitDir
})
expect(await searchBranchRefSnapshot(cwd, 'topic', 20, { wslDistro: 'Ubuntu' })).toEqual([
{ refName: 'refs/heads/topic', localBranchName: 'topic' }
])
expect(io.remote).toHaveBeenCalledTimes(1)
expect(io.refs).toHaveBeenCalledTimes(1)
clock.mockReturnValue(70_002)
const events: string[] = []
const offOwner = subscribeGitRefChanges(commonDir, () => events.push('owner'))
const offOther = subscribeGitRefChanges(String.raw`C:\other\.git`, () => events.push('other'))
try {
trackGitRefMutation(['commit'])()
expect(events).toEqual(['owner', 'other', 'owner', 'other'])
} finally {
offOwner()
offOther()
}
})
@@ -1,36 +0,0 @@
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import * as gitExecFileModule from '../../command-runner/git-exec-file'
import { runProcess } from '@orca/process-host'
import { getGitRepoRootAsync, isGitRepoAsync } from './repo-detection'
let fixture: string
async function git(cwd: string, args: string[]): Promise<void> {
const result = await runProcess({ program: 'git', args, cwd, timeoutMs: 15_000 })
if (result.code !== 0) {
throw new Error(result.stderr)
}
}
beforeEach(() => {
fixture = mkdtempSync(join(tmpdir(), 'orca-repo-async-'))
})
afterEach(() => {
vi.restoreAllMocks()
rmSync(fixture, { recursive: true, force: true })
})
describe('asynchronous repository detection with real Git', () => {
it('preserves genuine metadata fallback when asynchronous Git cannot answer', async () => {
const repo = join(fixture, 'fallback repo')
const nested = join(repo, 'packages')
mkdirSync(nested, { recursive: true })
await git(repo, ['init', '--quiet'])
vi.spyOn(gitExecFileModule, 'gitExecFileAsync').mockRejectedValue(new Error('Git unavailable'))
expect(await isGitRepoAsync(nested)).toBe(true)
expect(await getGitRepoRootAsync(nested)).toBe(repo.replaceAll('\\', '/'))
expect(await isGitRepoAsync(join(repo, '.git'))).toBe(false)
})
})
@@ -1,102 +0,0 @@
import type * as GitExecFileModule from '../../command-runner/git-exec-file'
import type * as GitStreamStdoutModule from '../../command-runner/git-stream-stdout'
import type * as GitProcessEnvModule from '../../command-runner/git-environment/git-process-env'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as BoundedFileReader from '../../../../shared/file-content/node-bounded-file-reader'
import path from 'node:path'
import {
createBoundedFileReaderModuleMock,
createFsPromisesModuleMock,
createGitExecFileModuleMock,
createGitStreamStdoutModuleMock,
createGitProcessEnvModuleMock
} from './status-test-harness'
const {
gitExecFileAsyncMock,
gitExecFileAsyncBufferMock,
gitStreamOptionsMock,
lstatMock,
realpathMock,
readFileMock,
statMock,
rmMock,
existsSyncMock
} = vi.hoisted(() => ({
gitExecFileAsyncMock: vi.fn(),
gitExecFileAsyncBufferMock: vi.fn(),
gitStreamOptionsMock: vi.fn(),
lstatMock: vi.fn(),
realpathMock: vi.fn(),
readFileMock: vi.fn(),
statMock: vi.fn(),
rmMock: vi.fn(),
existsSyncMock: vi.fn()
}))
vi.mock('../../command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
...createGitExecFileModuleMock({ gitExecFileAsyncMock, gitExecFileAsyncBufferMock })
}))
vi.mock('../../command-runner/git-stream-stdout', async (importOriginal) => ({
...(await importOriginal<typeof GitStreamStdoutModule>()),
...createGitStreamStdoutModuleMock({ gitExecFileAsyncMock, gitStreamOptionsMock })
}))
vi.mock('../../command-runner/git-environment/git-process-env', async (importOriginal) => ({
...(await importOriginal<typeof GitProcessEnvModule>()),
...createGitProcessEnvModuleMock()
}))
vi.mock('fs/promises', () =>
createFsPromisesModuleMock({ lstatMock, realpathMock, readFileMock, statMock, rmMock })
)
vi.mock('fs', () => ({
existsSync: existsSyncMock
}))
vi.mock('../../../../shared/file-content/node-bounded-file-reader', async (importOriginal) =>
createBoundedFileReaderModuleMock(await importOriginal<typeof BoundedFileReader>(), {
readFileMock,
statMock
})
)
import { getDiff } from './file-diff'
describe('getDiff', () => {
beforeEach(() => {
gitExecFileAsyncMock.mockReset()
gitExecFileAsyncBufferMock.mockReset()
lstatMock.mockReset()
readFileMock.mockReset()
statMock.mockReset()
existsSyncMock.mockReset()
statMock.mockResolvedValue({
isFile: () => true,
size: 12
})
})
it('does not treat an unreadable working-tree image as a deletion', async () => {
const pngBuffer = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00])
const workingTreePath = path.join('/repo', 'assets/unreadable.png')
gitExecFileAsyncBufferMock.mockResolvedValueOnce({ stdout: pngBuffer })
statMock.mockImplementation(async () => ({ isFile: () => true, size: 5 }))
readFileMock.mockImplementation(async (target: string) => {
if (target === workingTreePath) {
throw new Error('EIO')
}
return Buffer.from('')
})
const result = await getDiff('/repo', 'assets/unreadable.png', false)
expect(result.kind).toBe('binary')
if (result.kind !== 'binary') {
throw new Error('expected binary diff result')
}
expect(result.modifiedDeleted).toBeUndefined()
})
})
@@ -1,45 +0,0 @@
import { execFileSync } from 'node:child_process'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import { parsePorcelainV1Records } from './porcelain-v1-records'
const tempRoots: string[] = []
const git = (args: string[], cwd: string): string =>
execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
function createRepo(): string {
const repo = mkdtempSync(join(tmpdir(), 'orca-porcelain-v1-'))
tempRoots.push(repo)
git(['init', '-q', '-b', 'main'], repo)
git(['config', 'user.email', 'test@example.com'], repo)
git(['config', 'user.name', 'Test'], repo)
return repo
}
afterEach(() => {
for (const root of tempRoots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
describe('parsePorcelainV1Records', () => {
it('reports a rename alongside a later untracked entry', () => {
const repo = createRepo()
writeFileSync(join(repo, 'original.txt'), 'content\n')
git(['add', '-A'], repo)
git(['commit', '-qm', 'init'], repo)
git(['mv', 'original.txt', 'renamed.txt'], repo)
mkdirSync(join(repo, 'node_modules'))
writeFileSync(join(repo, 'node_modules', 'pkg.js'), 'x')
const records = parsePorcelainV1Records(git(['status', '--porcelain', '-z'], repo))
expect(records).toEqual([
{ xy: 'R ', path: 'renamed.txt' },
{ xy: '??', path: 'node_modules/' }
])
})
})
@@ -1,73 +0,0 @@
import { execFileSync } from 'node:child_process'
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import * as path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { invalidateGitBranchLineTotalInFlight } from '../../../../shared/git/status/git-branch-line-total'
import { clearEffectiveUpstreamStatusCacheForTests } from '../upstream/effective-upstream-status-cache'
import { clearSubmodulePathsCacheForTests } from '../submodules/submodule-paths'
import { getStatus } from './status-read'
import { invalidateGitReadCaches } from '../git-read-cache-invalidation'
const tempRoots: string[] = []
function git(repo: string, args: string[]): string {
return execFileSync('git', args, {
cwd: repo,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe']
}).trim()
}
async function createFixtureRepo(): Promise<{ repo: string; mergeBase: string }> {
const root = await mkdtemp(path.join(tmpdir(), 'orca-branch-line-total-exec-'))
tempRoots.push(root)
const repo = path.join(root, 'repo')
execFileSync('git', ['init', '-q', repo])
git(repo, ['config', 'user.email', 'test@example.com'])
git(repo, ['config', 'user.name', 'Test User'])
git(repo, ['config', 'commit.gpgSign', 'false'])
await write(repo, 'f.txt', 'a\nb\n')
git(repo, ['add', '-A'])
git(repo, ['commit', '-q', '-m', 'base'])
await write(repo, 'f.txt', 'a\nb\nc\n')
return { repo, mergeBase: git(repo, ['rev-parse', 'HEAD']) }
}
async function write(repo: string, relativePath: string, contents: string): Promise<void> {
const target = path.join(repo, relativePath)
await mkdir(path.dirname(target), { recursive: true })
await writeFile(target, contents)
}
function resetGitReadCaches(): void {
clearEffectiveUpstreamStatusCacheForTests()
clearSubmodulePathsCacheForTests()
invalidateGitReadCaches()
invalidateGitBranchLineTotalInFlight()
}
beforeEach(() => {
resetGitReadCaches()
})
afterEach(async () => {
resetGitReadCaches()
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
describe('branch line total completeness', () => {
it('omits the total when the status listing hit its limit', async () => {
const { repo, mergeBase } = await createFixtureRepo()
await write(repo, 'u1.txt', 'a\n')
await write(repo, 'u2.txt', 'b\n')
await write(repo, 'u3.txt', 'c\n')
const result = await getStatus(repo, { branchLineTotalMergeBase: mergeBase, limit: 1 })
expect(result.didHitLimit).toBe(true)
expect(result.branchLineTotal).toBeUndefined()
expect(result.branchLineTotalUnavailable).toBeUndefined()
})
})
@@ -1,92 +0,0 @@
// Untracked line counts use Node I/O, so WSL guest paths need Win32 spelling while host paths stay literal.
import * as path from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as FsPromises from 'node:fs/promises'
import type * as BoundedFileReader from '../../../../shared/file-content/node-bounded-file-reader'
import type { GitStatusEntry } from '../../../../shared/git/status/git-status-types'
const { lstatPaths, untrackedFiles } = vi.hoisted(() => ({
lstatPaths: [] as string[],
untrackedFiles: new Map<string, string>()
}))
vi.mock('node:fs/promises', async (importOriginal) => {
const actual = await importOriginal<typeof FsPromises>()
return {
...actual,
lstat: async (target: string) => {
lstatPaths.push(target)
const contents = untrackedFiles.get(target)
if (contents === undefined) {
throw Object.assign(new Error(`ENOENT: ${target}`), { code: 'ENOENT' })
}
return {
size: Buffer.byteLength(contents),
// Distinct per read so the stat-keyed untracked cache never serves another case's entry.
mtimeMs: lstatPaths.length,
ctimeMs: lstatPaths.length,
isSymbolicLink: () => false,
isFile: () => true
}
}
}
})
vi.mock('../../../../shared/file-content/node-bounded-file-reader', async (importOriginal) => {
const actual = await importOriginal<typeof BoundedFileReader>()
return {
...actual,
readNodeFileWithinLimit: async (target: string) => {
const contents = untrackedFiles.get(target)
if (contents === undefined) {
throw Object.assign(new Error(`ENOENT: ${target}`), { code: 'ENOENT' })
}
return { buffer: Buffer.from(contents) }
}
}
})
import { attachLineStats } from './status-line-stats'
const GUEST_WORKTREE = '/home/me/repo'
const UNC_WORKTREE = String.raw`\\wsl.localhost\Ubuntu\home\me\repo`
function untrackedEntry(entryPath: string): GitStatusEntry {
return { path: entryPath, area: 'untracked', status: 'added' }
}
describe('untracked line stats on a WSL worktree read by a Windows host', () => {
beforeEach(() => {
lstatPaths.length = 0
untrackedFiles.clear()
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
})
afterEach(() => {
vi.restoreAllMocks()
})
it('re-spells the guest form only, leaving every host-openable spelling byte-identical', async () => {
const cases: { worktree: string; expected: string }[] = [
{ worktree: GUEST_WORKTREE, expected: UNC_WORKTREE },
{ worktree: UNC_WORKTREE, expected: UNC_WORKTREE },
{
worktree: '//wsl.localhost/Ubuntu/home/me/repo',
expected: '//wsl.localhost/Ubuntu/home/me/repo'
},
{ worktree: '/home/me/my repo ', expected: '/home/me/my repo ' }
]
for (const { worktree, expected } of cases) {
lstatPaths.length = 0
untrackedFiles.clear()
const target = path.join(expected, 'fresh.txt')
untrackedFiles.set(target, 'one\ntwo\n')
const entries = [untrackedEntry('fresh.txt')]
await attachLineStats(worktree, entries, { wslDistro: 'Ubuntu' })
expect(entries[0].added, worktree).toBe(2)
}
})
})
@@ -1,52 +0,0 @@
import { execFileSync } from 'node:child_process'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
import type { GitPushTarget } from '../../../../shared/worktree/types'
import { getUpstreamStatus } from './upstream'
// Real Git verifies missing-ref exit/stderr handling before a fork remote is materialized.
describe('getUpstreamStatus with a deferred (not-yet-materialized) fork remote', () => {
const tempPaths: string[] = []
afterEach(() => {
for (const path of tempPaths.splice(0)) {
rmSync(path, { recursive: true, force: true })
}
})
it('reports the graceful "publish" state instead of 0 ahead/0 behind', async () => {
const repoPath = mkdtempSync(join(tmpdir(), 'orca-deferred-fork-remote-'))
tempPaths.push(repoPath)
const git = (...args: string[]): string =>
execFileSync('git', args, { cwd: repoPath, encoding: 'utf8' })
git('init', '--quiet')
git('config', 'user.name', 'Orca Test')
git('config', 'user.email', 'orca@example.test')
git('config', 'commit.gpgSign', 'false')
git('config', 'core.hooksPath', '.git/no-hooks')
writeFileSync(join(repoPath, 'fixture.txt'), 'base\n')
git('add', 'fixture.txt')
git('commit', '-m', 'base')
git('branch', '-M', 'contributor/fix')
// Persist a push target without adding its deferred remote.
const pushTarget: GitPushTarget = {
remoteName: 'pr-contributor-orca',
branchName: 'contributor/fix',
remoteUrl: 'git@github.com:contributor/orca.git'
}
const status = await getUpstreamStatus(repoPath, pushTarget)
expect(status).toEqual({
hasUpstream: false,
upstreamName: 'pr-contributor-orca/contributor/fix',
ahead: 0,
behind: 0,
hasConfiguredPushTarget: true
})
})
})
@@ -1,155 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
getOwnerRepoForRemoteMock,
getEnterpriseGitHubRepoSlugMock,
extractExecErrorMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
getOwnerRepoForRemoteMock: vi.fn(),
getEnterpriseGitHubRepoSlugMock: vi.fn(),
extractExecErrorMock: vi.fn((error: unknown) => {
const value = error as { stderr?: string; stdout?: string; message?: string }
return {
stderr: value?.stderr ?? value?.message ?? '',
stdout: value?.stdout ?? ''
}
}),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('../../execution/github-legacy-command', () => ({
execFileAsync: vi.fn()
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
githubRepoContext: vi.fn((repoPath: string, connectionId?: string | null, options = {}) => ({
...options,
repoPath,
connectionId: connectionId ?? null
})),
ghRepoExecOptions: vi.fn(
(context: { repoPath: string; connectionId?: string | null; wslDistro?: string }) =>
context.connectionId
? {}
: { cwd: context.repoPath, ...(context.wslDistro ? { wslDistro: context.wslDistro } : {}) }
),
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: vi.fn()
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: acquireMock,
release: releaseMock
}))
vi.mock('../../repositories/github-enterprise-repository', () => ({
getEnterpriseGitHubRepoSlug: getEnterpriseGitHubRepoSlugMock,
isGitHubHostAuthenticated: vi.fn().mockResolvedValue(true)
}))
import { createGitHubPullRequest } from './create-github-pull-request'
import { _resetOriginGitHubApiRepositoryCache } from '../../repositories/github-api-repository-remote-probe'
beforeEach(() => {
_resetOriginGitHubApiRepositoryCache()
})
describe('createGitHubPullRequest', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getOwnerRepoForRemoteMock.mockReset()
// Bridge explicit-origin creation to the fixtures that define getOwnerRepoMock.
getOwnerRepoForRemoteMock.mockImplementation(
async (repoPath: string, remoteName: string, connectionId?: string | null, opts = {}) =>
remoteName === 'origin' ? getOwnerRepoMock(repoPath, connectionId, opts) : null
)
getEnterpriseGitHubRepoSlugMock.mockReset()
getEnterpriseGitHubRepoSlugMock.mockResolvedValue(null)
extractExecErrorMock.mockClear()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
})
it('returns the existing PR when gh reports an already-open pull request', async () => {
getOwnerRepoMock.mockResolvedValueOnce({ owner: 'acme', repo: 'widgets' })
ghExecFileAsyncMock
.mockRejectedValueOnce({ stderr: 'a pull request already exists for feature/existing' })
.mockResolvedValueOnce({
stdout: JSON.stringify([
{
number: 44,
url: 'https://github.com/acme/widgets/pull/44'
}
])
})
await expect(
createGitHubPullRequest(
'/repo-root',
{
provider: 'github',
base: 'main',
head: 'refs/remotes/origin/feature/existing',
title: 'Existing'
},
'local'
)
).resolves.toEqual({
ok: false,
code: 'already_exists',
error: 'A pull request already exists for this branch.',
reason: 'existing_review',
existingReview: {
number: 44,
url: 'https://github.com/acme/widgets/pull/44'
}
})
expect(ghExecFileAsyncMock.mock.calls[1]).toEqual([
[
'pr',
'list',
'--repo',
'acme/widgets',
'--head',
'feature/existing',
'--base',
'main',
'--state',
'open',
'--limit',
'2',
'--json',
'number,url'
],
// Pin dotcom against process-level GH_HOST.
{ cwd: '/repo-root', host: 'github.com' }
])
})
})
@@ -1,206 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { exec, guard, spend } = vi.hoisted(() => ({ exec: vi.fn(), guard: vi.fn(), spend: vi.fn() }))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: exec
}))
vi.mock('../../api/rate-limit', () => ({
repositoryRateLimitGuard: guard,
noteRepositoryRateLimitSpend: spend
}))
import { detectRepositoryMergeMetadata } from './repository-merge-metadata'
import { _resetMergeQueueCacheForTests } from './repository-merge-metadata-cache'
const repository = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const options = { cwd: '/repo' }
function response(autoMergeAllowed: boolean): { stdout: string } {
return {
stdout: JSON.stringify({
data: { repository: { autoMergeAllowed, mergeQueue: null, ref: { rules: { nodes: [] } } } }
})
}
}
beforeEach(() => {
vi.resetAllMocks()
vi.stubEnv('GH_CONFIG_DIR', undefined)
vi.stubEnv('XDG_CONFIG_HOME', undefined)
_resetMergeQueueCacheForTests()
guard.mockReturnValue({ blocked: false })
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllEnvs()
})
describe('repository merge metadata reuse', () => {
it.each([true, false])(
'retains a successful concurrent read when a newer read fails (failure first: %s)',
async (failureFirst) => {
const older = Promise.withResolvers<{ stdout: string }>()
const newer = Promise.withResolvers<{ stdout: string }>()
exec.mockReturnValueOnce(older.promise).mockReturnValueOnce(newer.promise)
const first = detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
const second = detectRepositoryMergeMetadata(
repository,
'main',
{ cwd: '/another-worktree' },
'local:host'
)
if (failureFirst) {
newer.reject(new Error('ECONNRESET'))
await second
older.resolve(response(true))
await first
} else {
older.resolve(response(true))
await first
newer.reject(new Error('ECONNRESET'))
await second
}
await expect(
detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
).resolves.toMatchObject({ autoMergeAllowed: true })
expect(exec).toHaveBeenCalledTimes(2)
}
)
it('does not couple independent abort signals or cache an aborted result', async () => {
const canceled = Promise.withResolvers<{ stdout: string }>()
const surviving = Promise.withResolvers<{ stdout: string }>()
const firstController = new AbortController()
const secondController = new AbortController()
exec.mockReturnValueOnce(canceled.promise).mockReturnValueOnce(surviving.promise)
const first = detectRepositoryMergeMetadata(
repository,
'main',
{ ...options, signal: firstController.signal },
'local:host'
)
const second = detectRepositoryMergeMetadata(
repository,
'main',
{ ...options, signal: secondController.signal },
'local:host'
)
expect(exec).toHaveBeenCalledTimes(2)
firstController.abort()
canceled.reject(firstController.signal.reason)
await expect(first).resolves.toMatchObject({ autoMergeAllowed: null })
surviving.resolve(response(false))
await expect(second).resolves.toMatchObject({ autoMergeAllowed: false })
await expect(
detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
).resolves.toMatchObject({ autoMergeAllowed: false })
expect(exec).toHaveBeenCalledTimes(2)
})
it('retries immediately with a new signal after the only probe aborts', async () => {
const canceled = Promise.withResolvers<{ stdout: string }>()
const controller = new AbortController()
exec.mockReturnValueOnce(canceled.promise).mockResolvedValueOnce(response(true))
const first = detectRepositoryMergeMetadata(
repository,
'main',
{ ...options, signal: controller.signal },
'local:host'
)
controller.abort()
canceled.reject(controller.signal.reason)
await first
await expect(
detectRepositoryMergeMetadata(
repository,
'main',
{ ...options, signal: new AbortController().signal },
'local:host'
)
).resolves.toMatchObject({ autoMergeAllowed: true })
expect(exec).toHaveBeenCalledTimes(2)
})
it('retains a surviving cache writer when a newer independent probe aborts', async () => {
const older = Promise.withResolvers<{ stdout: string }>()
const canceled = Promise.withResolvers<{ stdout: string }>()
const controller = new AbortController()
exec.mockReturnValueOnce(older.promise).mockReturnValueOnce(canceled.promise)
const first = detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
const second = detectRepositoryMergeMetadata(
repository,
'main',
{ ...options, signal: controller.signal },
'local:host'
)
controller.abort()
canceled.reject(controller.signal.reason)
await second
older.resolve(response(false))
await first
await expect(
detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
).resolves.toMatchObject({ autoMergeAllowed: false })
expect(exec).toHaveBeenCalledTimes(2)
})
it('does not spend quota for an already aborted probe', async () => {
const controller = new AbortController()
controller.abort()
exec.mockResolvedValue(response(true))
await expect(
detectRepositoryMergeMetadata(
repository,
'main',
{ ...options, signal: controller.signal },
'local:host'
)
).resolves.toMatchObject({ autoMergeAllowed: null })
expect(exec).not.toHaveBeenCalled()
expect(spend).not.toHaveBeenCalled()
})
it('keeps newer worktree settings when an older directory probe completes later', async () => {
const older = Promise.withResolvers<{ stdout: string }>()
const newer = Promise.withResolvers<{ stdout: string }>()
exec.mockReturnValueOnce(older.promise).mockReturnValueOnce(newer.promise)
const first = detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
const second = detectRepositoryMergeMetadata(
repository,
'main',
{ cwd: '/another-worktree' },
'local:host'
)
newer.resolve(response(false))
await expect(second).resolves.toMatchObject({ autoMergeAllowed: false })
older.resolve(response(true))
await expect(first).resolves.toMatchObject({ autoMergeAllowed: true })
await expect(
detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
).resolves.toMatchObject({ autoMergeAllowed: false })
expect(exec).toHaveBeenCalledTimes(2)
})
it('allows a stale successor without letting the older completion replace its cache', async () => {
vi.useFakeTimers()
const older = Promise.withResolvers<{ stdout: string }>()
const newer = Promise.withResolvers<{ stdout: string }>()
exec.mockReturnValueOnce(older.promise).mockReturnValueOnce(newer.promise)
const first = detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
vi.advanceTimersByTime(60_000)
const second = detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
newer.resolve(response(false))
await expect(second).resolves.toMatchObject({ autoMergeAllowed: false })
older.resolve(response(true))
await expect(first).resolves.toMatchObject({ autoMergeAllowed: true })
await expect(
detectRepositoryMergeMetadata(repository, 'main', options, 'local:host')
).resolves.toMatchObject({ autoMergeAllowed: false })
expect(exec).toHaveBeenCalledTimes(2)
})
})
@@ -1,167 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { ghExecFileAsyncMock, getOwnerRepoMock, rateLimitGuardMock } = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
rateLimitGuardMock: vi.fn(() => ({ blocked: false }))
}))
vi.mock('../../execution/github-legacy-command', () => ({
execFileAsync: vi.fn()
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
githubRepoContext: (repoPath: string, connectionId?: string | null) => ({
repoPath,
connectionId: connectionId ?? null
}),
ghRepoExecOptions: (context: { repoPath: string }) => ({ cwd: context.repoPath }),
getOwnerRepoForRemote: (
repoPath: string,
remoteName: string,
connectionId?: string | null,
localGitOptions?: unknown
) =>
remoteName === 'origin'
? getOwnerRepoMock(repoPath, connectionId, localGitOptions)
: Promise.resolve(null),
_resetOwnerRepoCache: vi.fn()
}))
vi.mock('../../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock,
resolveIssueSource: vi.fn()
}))
vi.mock('../../../git/command-runner/process-capture/exec-error', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
extractExecError: vi.fn((err: unknown) => ({ stderr: String(err), stdout: '' }))
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: vi.fn(),
release: vi.fn()
}))
vi.mock('../../api/gh-error-classification', () => ({
classifyGhError: (stderr: string) => ({ type: 'unknown', message: stderr }),
classifyListIssuesError: (stderr: string) => ({ type: 'unknown', message: stderr })
}))
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
gitExecFileAsync: vi.fn()
}))
vi.mock('../../api/rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: vi.fn(),
getRateLimit: vi.fn(async () => ({ ok: false, error: 'not probed in tests' })),
repositoryRateLimitGuard: vi.fn(() => ({ blocked: false })),
noteRepositoryRateLimitSpend: vi.fn(),
spendsSharedGitHubComQuota: vi.fn(() => true)
}))
import { getWorkItem } from './get-work-item'
import { _resetMergeQueueCacheForTests } from '../detect/repository-merge-metadata-cache'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
import { _resetOriginGitHubApiRepositoryCache } from '../../repositories/github-api-repository-remote-probe'
// Metadata and complete checks are independent provider responses.
function mockPullRequestDetail(payload: Record<string, unknown>): void {
const head = 'a'.repeat(40)
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'pr' && args[1] === 'view') {
return { stdout: JSON.stringify({ ...payload, headRefOid: head }) }
}
if (args.includes('graphql') && args.some((arg) => arg.includes('statusCheckRollup'))) {
return {
stdout: JSON.stringify({
data: {
repository: {
pullRequest: {
number: payload.number,
headRefOid: head,
commits: {
nodes: [
{
commit: {
oid: head,
statusCheckRollup: {
contexts: {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: these test payloads contain object rollup rows only.
nodes: (payload.statusCheckRollup as Record<string, unknown>[]).map(
(raw) => ({
...raw,
__typename:
raw.__typename ?? (raw.state ? 'StatusContext' : 'CheckRun'),
...(!raw.state ? { checkSuite: { databaseId: 7 } } : {})
})
),
pageInfo: { hasNextPage: false, endCursor: null }
}
},
checkSuites: {
nodes: [],
pageInfo: { hasNextPage: false, endCursor: null }
}
}
}
]
}
}
}
}
})
}
}
throw new Error('unexpected gh call')
})
}
describe('work item checksSummary', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
_resetOwnerRepoCache()
_resetOriginGitHubApiRepositoryCache()
_resetMergeQueueCacheForTests()
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
})
it('counts a skipped run as passing and reads a StatusContext state as its conclusion', async () => {
mockPullRequestDetail({
number: 42,
title: 'Add feature',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/42',
updatedAt: '2026-04-01T00:00:00Z',
statusCheckRollup: [
{ __typename: 'CheckRun', status: 'COMPLETED', conclusion: 'SKIPPED' },
// StatusContext supplies state instead of CheckRun's status/conclusion.
{ __typename: 'StatusContext', state: 'SUCCESS' },
{ __typename: 'CheckRun', status: 'COMPLETED', conclusion: 'NEUTRAL' }
]
})
const item = await getWorkItem('/repo-root', 42, 'pr', null, {}, 'origin')
expect(item?.checksSummary).toEqual({
state: 'success',
total: 3,
passed: 2,
failed: 0,
cancelled: 0,
pending: 0,
neutral: 1
})
})
})
@@ -1,203 +0,0 @@
import type * as GithubLegacyCommandModule from '../../execution/github-legacy-command'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubOwnerRepoSelectionModule from '../../repositories/github-owner-repo-selection'
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GithubOperationSlotsModule from '../../execution/github-operation-slots'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
getIssueOwnerRepoMock,
getOwnerRepoForRemoteMock,
resolvePRRepositoryCandidatesMock,
resolveIssueSourceMock,
rateLimitGuardMock,
noteRateLimitSpendMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
getIssueOwnerRepoMock: vi.fn(),
getOwnerRepoForRemoteMock: vi.fn(),
resolvePRRepositoryCandidatesMock: vi.fn(),
resolveIssueSourceMock: vi.fn(),
rateLimitGuardMock: vi.fn(() => ({ blocked: false })),
noteRateLimitSpendMock: vi.fn(),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('../../execution/github-legacy-command', async () => {
const actual = await vi.importActual<typeof GithubLegacyCommandModule>(
'../../execution/github-legacy-command'
)
return {
...actual,
execFileAsync: vi.fn()
}
})
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async () => {
const actual = await vi.importActual<typeof GhExecFileModule>(
'../../../git/command-runner/hosted-cli/gh-exec-file'
)
return {
...actual,
ghExecFileAsync: ghExecFileAsyncMock
}
})
vi.mock('../../repositories/github-owner-repo-selection', async () => {
const actual = await vi.importActual<typeof GithubOwnerRepoSelectionModule>(
'../../repositories/github-owner-repo-selection'
)
return {
...actual,
getOwnerRepo: getOwnerRepoMock,
resolveIssueSource: resolveIssueSourceMock
}
})
vi.mock('../../repositories/github-repository-identity', async () => {
const actual = await vi.importActual<typeof GithubRepositoryIdentityModule>(
'../../repositories/github-repository-identity'
)
return {
...actual,
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
_resetOwnerRepoCache: vi.fn()
}
})
vi.mock('../../execution/github-operation-slots', async () => {
const actual = await vi.importActual<typeof GithubOperationSlotsModule>(
'../../execution/github-operation-slots'
)
return {
...actual,
acquire: acquireMock,
release: releaseMock
}
})
vi.mock('../../api/rate-limit', () => ({
rateLimitGuard: rateLimitGuardMock,
noteRateLimitSpend: noteRateLimitSpendMock,
getRateLimit: vi.fn(async () => ({ ok: false, error: 'not probed in tests' })),
repositoryRateLimitGuard: vi.fn(() => ({ blocked: false })),
noteRepositoryRateLimitSpend: vi.fn(),
spendsSharedGitHubComQuota: () => true
}))
vi.mock('../../repositories/github-api-repository', async (importOriginal) => {
return {
...(await importOriginal<typeof GithubApiRepositoryModule>()),
resolveIssueGitHubApiRepositorySource: (
repoPath: string,
preference: unknown,
connectionId?: string | null,
localGitOptions?: unknown
) => resolveIssueSourceMock(repoPath, preference, connectionId, localGitOptions),
getIssueGitHubApiRepository: (repoPath: string, connectionId?: string | null) =>
getIssueOwnerRepoMock(repoPath, connectionId),
resolveGitHubApiRepositoryCandidates: (
repoPath: string,
connectionId?: string | null,
localGitOptions?: unknown
) => resolvePRRepositoryCandidatesMock(repoPath, connectionId, localGitOptions)
}
})
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) => {
return {
...(await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()),
getOriginGitHubApiRepository: (
repoPath: string,
connectionId?: string | null,
localGitOptions?: unknown
) => getOwnerRepoMock(repoPath, connectionId, localGitOptions),
getGitHubApiRepositoryForRemote: (
repoPath: string,
remoteName: string,
connectionId?: string | null,
localGitOptions?: unknown
) =>
remoteName === 'origin'
? getOwnerRepoMock(repoPath, connectionId, localGitOptions)
: getOwnerRepoForRemoteMock(repoPath, remoteName, connectionId, localGitOptions)
}
})
import { getWorkItem } from './get-work-item'
import { _resetOwnerRepoCache } from '../../repositories/github-repository-identity'
describe('GitHub issue open-by-number origin preference', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getIssueOwnerRepoMock.mockReset()
getOwnerRepoForRemoteMock.mockReset()
resolvePRRepositoryCandidatesMock.mockReset()
resolveIssueSourceMock.mockReset()
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
noteRateLimitSpendMock.mockReset()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
getOwnerRepoForRemoteMock.mockImplementation(
async (repoPath: string, remoteName: string, connectionId?: string | null, opts = {}) =>
remoteName === 'origin' ? getOwnerRepoMock(repoPath, connectionId, opts) : null
)
resolvePRRepositoryCandidatesMock.mockImplementation(async (repoPath, connectionId) => {
const origin = await getOwnerRepoMock(repoPath, connectionId)
const repository = origin ? { host: 'github.com', ...origin } : null
return { candidates: repository ? [repository] : [], headRepo: repository }
})
_resetOwnerRepoCache()
})
it('does not run a bare issue lookup when explicit origin identity is unresolved', async () => {
resolveIssueSourceMock.mockResolvedValueOnce({ source: null, fellBack: false })
await expect(getWorkItem('/repo-root', 7, 'issue', null, {}, 'origin')).resolves.toBeNull()
expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', 'origin', null, {})
expect(getIssueOwnerRepoMock).not.toHaveBeenCalled()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
it('skips the issue probe on untyped open when origin identity is unresolved', async () => {
const origin = { owner: 'fork', repo: 'orca', host: 'github.com' }
resolveIssueSourceMock.mockResolvedValueOnce({ source: null, fellBack: false })
getOwnerRepoMock.mockResolvedValue(origin)
resolvePRRepositoryCandidatesMock.mockResolvedValue({ candidates: [origin], headRepo: origin })
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
number: 7,
title: 'Origin PR',
state: 'open',
labels: [],
isDraft: false,
url: 'https://github.com/fork/orca/pull/7',
baseRefName: 'main',
headRefName: 'origin/fix',
updatedAt: '2026-04-02T00:00:00Z',
author: { login: 'octocat' }
})
})
const item = await getWorkItem('/repo-root', 7, undefined, null, {}, 'origin')
expect(resolveIssueSourceMock).toHaveBeenCalledWith('/repo-root', 'origin', null, {})
expect(getIssueOwnerRepoMock).not.toHaveBeenCalled()
expect(ghExecFileAsyncMock.mock.calls[0]?.[0]).toEqual(
expect.arrayContaining(['pr', 'view', '--repo', 'fork/orca'])
)
expect(item).toMatchObject({ number: 7, type: 'pr' })
})
})
@@ -1,215 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { getPRForBranch } from './get-pr-for-branch'
import { resetPRForBranchMocks } from '../client-test-harness'
import type { RestPullRequest } from './pull-request-lookup-data'
import type { GitHubRepositoryIdentity as OwnerRepo } from '../../../../shared/github/repositories/repository-identity'
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
resolvePRRepositoryCandidatesMock,
gitExecFileAsyncMock
} = clientMocks
const ACME = { owner: 'acme', repo: 'widgets' }
function primeGit(
trackedUpstream: string,
remoteHeads: Record<string, string> = { origin: 'develop' }
): void {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'for-each-ref' && args.includes('--format=%(refname)%00%(upstream)')) {
return { stdout: `refs/heads/feature/my-change\0${trackedUpstream}\n`, stderr: '' }
}
if (args[0] === 'for-each-ref' && args.includes('--format=%(refname)%00%(symref)')) {
const stdout = Object.entries(remoteHeads)
.filter(([remote]) => args.includes(`refs/remotes/${remote}/HEA[D]`))
.map(
([remote, branch]) => `refs/remotes/${remote}/HEAD\0refs/remotes/${remote}/${branch}\n`
)
.join('')
return { stdout, stderr: '' }
}
if (args[0] === 'rev-parse' && args[1] === 'HEAD') {
return { stdout: 'feature-head-oid\n', stderr: '' }
}
throw new Error(`unexpected git call: ${args.join(' ')}`)
})
}
function restPR(
headRefName = 'develop',
headRepo: OwnerRepo = ACME,
prRepo: OwnerRepo = ACME
): RestPullRequest {
return {
number: 7,
title: 'Release develop',
state: 'closed',
merged_at: null,
html_url: `https://${prRepo.host ?? 'github.com'}/${prRepo.owner}/${prRepo.repo}/pull/7`,
updated_at: '2025-01-01T00:00:00Z',
draft: false,
mergeable: null,
base: { ref: 'release', sha: 'release-oid' },
head: {
ref: headRefName,
sha: 'old-develop-oid',
repo: { name: headRepo.repo, owner: { login: headRepo.owner } }
}
}
}
function primeGh(
listsByHead: Record<string, RestPullRequest[]>,
exactPR: RestPullRequest | RestPullRequest[] = Object.values(listsByHead).flat()[0] ?? restPR()
): void {
const repositorySlug = (pr: RestPullRequest): string =>
new URL(pr.html_url ?? '').pathname.split('/').slice(1, 3).join('/')
const repositoryHost = (pr: RestPullRequest): string => new URL(pr.html_url ?? '').host
const exactPRs = Array.isArray(exactPR) ? exactPR : [exactPR]
const prsByRepositoryAndNumber = new Map(
[...Object.values(listsByHead).flat(), ...exactPRs].map((pr) => [
`${repositoryHost(pr)}/${repositorySlug(pr)}#${pr.number}`,
pr
])
)
ghExecFileAsyncMock.mockImplementation(
async (args: string[], options: { host?: string } = {}) => {
const host = options.host ?? 'github.com'
const match = args[1]?.match(/^repos\/([^/]+\/[^/]+)\/pulls\?head=([^&]+)/)
if (args[0] === 'api' && match) {
const list = (listsByHead[decodeURIComponent(match[2])] ?? []).filter(
(pr) => repositoryHost(pr) === host && repositorySlug(pr) === match[1]
)
return { stdout: JSON.stringify(list) }
}
const exactRestMatch = args[1]?.match(/^repos\/([^/]+\/[^/]+)\/pulls\/(\d+)$/)
if (args[0] === 'api' && exactRestMatch) {
const pr = prsByRepositoryAndNumber.get(`${host}/${exactRestMatch[1]}#${exactRestMatch[2]}`)
if (pr) {
return { stdout: JSON.stringify(pr) }
}
throw new Error('HTTP 404 Not Found')
}
const repoIndex = args.indexOf('--repo')
const exact = prsByRepositoryAndNumber.get(`${host}/${args[repoIndex + 1]}#${args[2]}`)
if (args[0] === 'pr' && args[1] === 'view') {
if (!exact) {
throw new Error('HTTP 404 Not Found')
}
return {
stdout: JSON.stringify({
number: exact.number,
title: exact.title,
state: exact.merged_at ? 'MERGED' : exact.state.toUpperCase(),
url: exact.html_url,
statusCheckRollup: [],
updatedAt: exact.updated_at,
isDraft: exact.draft,
mergeable: 'UNKNOWN',
baseRefName: exact.base?.ref,
headRefName: exact.head?.ref,
baseRefOid: exact.base?.sha,
headRefOid: exact.head?.sha,
headRepositoryOwner: exact.head?.repo?.owner,
headRepository: exact.head?.repo ? { name: exact.head.repo.name } : null
})
}
}
throw new Error(`gh unavailable: ${args.join(' ')}`)
}
)
}
describe('issue #26948: a branch tracking the default branch', () => {
beforeEach(() => {
resetPRForBranchMocks(clientMocks)
getOwnerRepoMock.mockResolvedValue(ACME)
})
it('does not query another GitHub server for a known public GitHub head', async () => {
const enterprise = { ...ACME, host: 'ghe.example' }
resolvePRRepositoryCandidatesMock.mockResolvedValue({
candidates: [enterprise, ACME],
headRepo: ACME
})
primeGit('')
primeGh({
'acme:feature/my-change': [
{ ...restPR('feature/my-change', enterprise, enterprise), state: 'open' }
]
})
expect(await getPRForBranch('/repo-root', 'feature/my-change')).toBeNull()
expect(
ghExecFileAsyncMock.mock.calls.some(([, options]) => options?.host === 'ghe.example')
).toBe(false)
})
})
@@ -1,108 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import type * as GhExecFileModule from '../../../git/command-runner/hosted-cli/gh-exec-file'
import type * as GithubApiRepositoryModule from '../../repositories/github-api-repository'
import type * as GithubApiRepositoryRemoteProbeModule from '../../repositories/github-api-repository-remote-probe'
import type * as GitExecFileModule from '../../../git/command-runner/git-exec-file'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitHubEnterpriseRepositoryModule from '../../repositories/github-enterprise-repository'
const { clientMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('../client-test-mocks')
return { clientMocks: moduleMocks.createGitHubClientMocks(), moduleMocks }
})
vi.mock('../../execution/github-legacy-command', () =>
moduleMocks.githubLegacyCommandModuleMock(clientMocks)
)
vi.mock('../../repositories/github-owner-repo-selection', () =>
moduleMocks.githubOwnerSelectionModuleMock(clientMocks)
)
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
...moduleMocks.githubRepositoryIdentityModuleMock(clientMocks)
}))
vi.mock('../../api/gh-error-classification', () =>
moduleMocks.githubErrorClassificationModuleMock()
)
vi.mock('../../execution/github-operation-slots', () =>
moduleMocks.githubOperationSlotsModuleMock(clientMocks)
)
vi.mock('../../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: clientMocks.gitExecFileAsyncMock
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GhExecFileModule>()),
...moduleMocks.githubHostedCliModuleMock(clientMocks)
}))
vi.mock('../../repositories/local-git-config-signature', () =>
moduleMocks.localGitConfigSignatureModuleMock(clientMocks)
)
vi.mock('../../repositories/github-enterprise-repository', async (importOriginal) =>
moduleMocks.githubEnterpriseRepositoryModuleMock(
await importOriginal<typeof GitHubEnterpriseRepositoryModule>()
)
)
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(clientMocks))
vi.mock('../../repositories/github-api-repository', async (importOriginal) =>
moduleMocks.githubApiRepositoryModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryModule>()
)
)
vi.mock('../../repositories/github-api-repository-remote-probe', async (importOriginal) =>
moduleMocks.githubApiRepositoryRemoteProbeModuleMock(
clientMocks,
await importOriginal<typeof GithubApiRepositoryRemoteProbeModule>()
)
)
import { getRestPRForBranch } from './pr-branch-lookup'
import { resetPRForBranchMocks } from '../client-test-harness'
const { ghExecFileAsyncMock } = clientMocks
describe('PR lookup conditional REST reads', () => {
beforeEach(() => resetPRForBranchMocks(clientMocks))
it('revalidates both mixed and active discovery responses before falling back to history', async () => {
const repo = { owner: 'acme', repo: 'widgets', host: 'github.com' }
const old = Array.from({ length: 10 }, (_, i) => ({
number: i + 1,
state: 'closed',
title: 'Old',
head: { ref: 'feature' }
}))
const open = { number: 11, state: 'open', title: 'Active', head: { ref: 'feature' } }
const http = (data: unknown, etag: string) => ({
stdout: `HTTP/2.0 200 OK\nEtag: "${etag}"\r\n\r\n${JSON.stringify(data)}`
})
ghExecFileAsyncMock
.mockResolvedValueOnce(http(old, 'recent'))
.mockResolvedValueOnce(http([open], 'active'))
.mockRejectedValueOnce(
Object.assign(new Error('304'), { stdout: 'HTTP/2.0 304 Not Modified\n\n' })
)
.mockRejectedValueOnce(
Object.assign(new Error('304'), { stdout: 'HTTP/2.0 304 Not Modified\n\n' })
)
await expect(
getRestPRForBranch(repo, 'acme', 'feature', { cwd: '/repo' })
).resolves.toMatchObject({ number: 11 })
await expect(
getRestPRForBranch(repo, 'acme', 'feature', { cwd: '/repo' })
).resolves.toMatchObject({ number: 11 })
expect(ghExecFileAsyncMock.mock.calls[2][0]).toContain('If-None-Match: "recent"')
expect(ghExecFileAsyncMock.mock.calls[3][0]).toContain('If-None-Match: "active"')
})
})
@@ -1,77 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../../repositories/github-repository-identity'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import type {
GitHubRepoContext,
LocalGitExecOptions
} from '../../repositories/github-repository-identity'
import { __resetHostedReviewBranchCacheForTests } from '../../../source-control/hosted-review-branch-cache'
const mocks = vi.hoisted(() => ({ resolve: vi.fn(), acquire: vi.fn(), release: vi.fn() }))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: mocks.acquire,
release: mocks.release
}))
vi.mock('../../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
githubRepoContext: (
repoPath: string,
connectionId: string | null,
options: LocalGitExecOptions
) => ({
repoPath,
connectionId,
...options
}),
ghRepoExecOptions: (context: GitHubRepoContext) => context
}))
vi.mock('./branch-lookup-resolution', () => ({
resolvePRForBranchOutcome: mocks.resolve
}))
vi.mock('./pr-for-branch-outcome', async () => ({
getPRForBranchOutcome: (await import('./pr-for-branch-outcome')).getPRForBranchOutcome
}))
vi.mock('../fetch/repo-slug-upstream', async () => ({
getRepoSlug: vi.fn(async () => ({ owner: 'acme', repo: 'orca' }))
}))
vi.mock('./pr-lookup-rate-limit', async () => ({
getGitHubPRLookupRateLimitBlock: vi.fn(async () => null)
}))
vi.mock('../create/create-github-pull-request', async () => ({
createGitHubPullRequest: vi.fn()
}))
vi.mock('../../../gitlab/merge-requests/merge-request-lookup', () => ({
getProjectSlug: vi.fn(async () => null)
}))
import { getPRForBranchOutcome } from './pr-for-branch-outcome'
const outcome: PRRefreshOutcome = { kind: 'no-pr', fetchedAt: 1 }
beforeEach(() => {
vi.clearAllMocks()
mocks.resolve.mockReset()
__resetHostedReviewBranchCacheForTests()
mocks.acquire.mockResolvedValue(undefined)
})
afterEach(() => {
vi.unstubAllEnvs()
vi.useRealTimers()
})
describe('PR lookup coalescing', () => {
it('cleans up errors so later reads can recover', async () => {
mocks.resolve.mockRejectedValueOnce(new Error('network down')).mockResolvedValue(outcome)
const results = await Promise.all([
getPRForBranchOutcome('/repo', 'topic'),
getPRForBranchOutcome('/repo', 'topic')
])
expect(results.every((result) => result.kind === 'upstream-error')).toBe(true)
expect(mocks.resolve).toHaveBeenCalledTimes(1)
await expect(getPRForBranchOutcome('/repo', 'topic')).resolves.toEqual(outcome)
expect(mocks.resolve).toHaveBeenCalledTimes(2)
})
})
@@ -1,209 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../repositories/github-repository-identity'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({ exec: vi.fn(), guard: vi.fn() }))
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: mocks.exec
}))
vi.mock('../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
ghRepoExecOptions: () => ({}),
githubRepoContext: () => ({})
}))
vi.mock('../api/rate-limit', () => ({
repositoryRateLimitGuard: mocks.guard,
noteRepositoryRateLimitSpend: vi.fn()
}))
import { getIssueBodyAndComments, getIssueDetailsViaGraphQL } from './issue-work-item-details'
const repository = { owner: 'acme', repo: 'widgets' }
describe('explicit task refresh conversation reads', () => {
beforeEach(() => {
mocks.exec.mockReset()
mocks.guard.mockReset()
mocks.guard.mockReturnValue({ blocked: false })
})
it('bypasses body, comments and timeline caches and reads every comments page', async () => {
mocks.exec.mockImplementation(async (args: string[]) => ({
stdout: args.some((arg) => arg.includes('/comments?'))
? JSON.stringify([
[{ id: 1, user: null, body: 'first', created_at: '', html_url: '' }],
[{ id: 2, user: null, body: 'second', created_at: '', html_url: '' }]
])
: args.some((arg) => arg.includes('/timeline?'))
? ''
: JSON.stringify({ body: 'current', assignees: [] })
}))
const result = await getIssueBodyAndComments('/repo', 42, repository, null, {}, true)
expect(result.body).toBe('current')
expect(result.comments.map((comment) => comment.body)).toEqual(['first', 'second'])
for (const [args] of mocks.exec.mock.calls.filter(([args]) =>
args.some((arg: string) => arg.startsWith('repos/'))
)) {
expect(args).not.toContain('--cache')
}
expect(
mocks.exec.mock.calls.find(([args]) =>
args.some((arg: string) => arg.includes('/comments?'))
)?.[0]
).toEqual(expect.arrayContaining(['--paginate', '--slurp']))
})
it('marks only the comments the reading gh account wrote', async () => {
const comment = (id: number, login: string) => ({
id,
user: { login, avatar_url: '' },
body: 'LGTM',
created_at: '',
html_url: ''
})
mocks.exec.mockImplementation(async (args: string[]) => ({
stdout: args.includes('user')
? 'Me\n'
: args.some((arg) => arg.includes('/comments?'))
? JSON.stringify([[comment(1, 'alice')], [comment(2, 'me')]])
: args.some((arg) => arg.includes('/timeline?'))
? ''
: JSON.stringify({ body: 'current' })
}))
const result = await getIssueBodyAndComments('/repo', 42, repository, null, {}, true)
expect(result.comments.map((c) => [c.author, c.viewerDidAuthor])).toEqual([
['alice', undefined],
['me', true]
])
})
it('retains ordinary fallback behavior but does not report an explicit rate-limited read as success', async () => {
mocks.guard.mockReturnValue({ blocked: true })
expect(await getIssueBodyAndComments('/repo', 42, repository)).toEqual({
body: '',
comments: [],
assignees: [],
timelineItems: []
})
await expect(getIssueBodyAndComments('/repo', 42, repository, null, {}, true)).rejects.toThrow()
expect(mocks.exec).not.toHaveBeenCalled()
})
it('falls back from incomplete GraphQL conversations and malformed timelines', async () => {
mocks.exec.mockResolvedValueOnce({
stdout: JSON.stringify({
data: {
repository: {
issue: { body: 'new', comments: { nodes: [], pageInfo: { hasNextPage: true } } }
}
}
})
})
expect(
await getIssueDetailsViaGraphQL('/repo', 42, repository, null, {}, false, true)
).toBeNull()
mocks.exec.mockImplementation(async (args: string[]) => ({
stdout: args.some((arg) => arg.includes('/timeline?'))
? 'not-json'
: args.some((arg) => arg.includes('/comments?'))
? '[]'
: '{"body":"new"}'
}))
await expect(getIssueBodyAndComments('/repo', 42, repository, null, {}, true)).rejects.toThrow()
})
})
it('pairs issue and comment image sources through collapsed and REST detail reads', async () => {
mocks.exec.mockReset()
mocks.guard.mockReturnValue({ blocked: false })
const body =
'![shot](https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488)'
const html =
'<img src="https://private-user-images.githubusercontent.com/1/ce11040a-fb66-4289-927f-547b16dfc488.png?jwt=TEST">'
mocks.exec.mockImplementation(async (args: string[]) => ({
stdout: JSON.stringify(
args.includes('graphql')
? {
data: {
repository: {
issue: {
body,
bodyHTML: html,
comments: { nodes: [{ databaseId: 1, body, bodyHTML: html }] }
}
}
}
}
: args.some((arg) => arg.includes('/timeline?'))
? []
: args.some((arg) => arg.includes('/comments?'))
? [{ id: 1, user: null, body, body_html: html }]
: { body, body_html: html }
)
}))
const collapsed = await getIssueDetailsViaGraphQL('/repo', 42, repository)
const rest = await getIssueBodyAndComments('/repo', 42, repository)
for (const details of [collapsed!, rest]) {
expect(details.body).toBe(body)
expect(details.bodyImageUrls).toEqual({
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
'https://private-user-images.githubusercontent.com/1/ce11040a-fb66-4289-927f-547b16dfc488.png?jwt=TEST'
})
expect(details.comments[0].bodyImageUrls).toEqual(details.bodyImageUrls)
}
for (const [args] of mocks.exec.mock.calls.filter(([args]) =>
args.some((arg: string) => /\/issues\/42(?:$|\/comments)/.test(arg))
)) {
expect(args).toContain('Accept: application/vnd.github.full+json')
expect(args).not.toContain('--cache')
}
})
it.each(['optional HTML', 'required body', 'pathless', 'rejection without errors'] as const)(
'admits rejected issue stdout only for available optional HTML failures (%s)',
async (input) => {
mocks.exec.mockReset()
mocks.guard.mockReturnValue({ blocked: false })
const body =
'![shot](https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488)'
const html =
'<img src="https://private-user-images.githubusercontent.com/1/ce11040a-fb66-4289-927f-547b16dfc488.png?jwt=TEST">'
mocks.exec.mockRejectedValueOnce(
Object.assign(new Error('GraphQL failed'), {
stdout: JSON.stringify({
data: {
repository: {
issue: {
body,
bodyHTML: html,
comments: {
pageInfo: { hasNextPage: false },
nodes: [{ databaseId: 1, body, bodyHTML: html }]
}
}
}
},
errors:
input === 'rejection without errors'
? undefined
: [
{
path:
input === 'pathless'
? undefined
: ['repository', 'issue', input === 'required body' ? 'body' : 'bodyHTML']
}
]
})
})
)
mocks.exec.mockResolvedValueOnce({ stdout: '[]' })
const result = await getIssueDetailsViaGraphQL('/repo', 42, repository, null, {}, false, true)
if (input !== 'optional HTML') {
expect(result).toBeNull()
expect(mocks.exec).toHaveBeenCalledTimes(1)
return
}
expect(result?.body).toBe(body)
expect(result?.bodyImageUrls).toBeUndefined()
expect(result?.comments[0].bodyImageUrls).toEqual({
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
'https://private-user-images.githubusercontent.com/1/ce11040a-fb66-4289-927f-547b16dfc488.png?jwt=TEST'
})
expect(mocks.exec).toHaveBeenCalledTimes(2)
}
)
@@ -1,40 +0,0 @@
import { afterEach, expect, it, vi } from 'vitest'
import { setPRAutoRefreshCadence } from '../../../shared/review-refresh-policy'
const mocks = vi.hoisted(() => ({
read: vi.fn(async () => []),
pending: vi.fn(() => [{ headSha: 'old-head', movedAt: 1, expiresAt: null }])
}))
vi.mock('../pull-requests/refresh/pr-refresh-coordinator', () => ({ schedulePRWatchRead: vi.fn() }))
vi.mock('./pr-head-check-read', () => ({ readPRHeadCheckRollup: mocks.read }))
import { PREarlierHeadJobs } from './pr-earlier-head-jobs'
import { prWatchPlanDueAt } from '../pull-requests/refresh/pr-refresh-watch-jobs'
afterEach(() => setPRAutoRefreshCadence('default'))
it.each([
['default', 120_000],
['reduced', 10 * 60_000],
['minimal', 20 * 60_000],
// A chat is waiting for the result, so off slows to minimal instead of stopping.
['off', 20 * 60_000]
] as const)(
'an earlier head’s next read follows the %s watch cadence',
async (cadence, interval) => {
setPRAutoRefreshCadence(cadence)
const jobs = new PREarlierHeadJobs({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: this read-only probe only requests pending heads.
database: { read: async () => mocks.pending() } as never,
apply: async () => false,
logger: { warn: vi.fn() },
now: () => 1_000
})
const readAt = await jobs.read(
{ prKey: 'github.com/acme/repo#7', headSha: 'old-head' },
{ repoId: 'r', repoPath: '/repo', branch: 'topic', repoKind: 'git', cacheKey: 'k' }
)
expect(readAt).toBe(1_000)
expect(prWatchPlanDueAt({ kind: 'head', at: readAt! })).toBe(1_000 + interval)
}
)
@@ -1,58 +0,0 @@
import { expect, it, vi } from 'vitest'
import { makeCandidate, makePR } from './pr-refresh-coordinator-test-harness'
import { getPRForBranchOutcome } from '../../client/lookup/pr-for-branch-outcome'
import { runHostedPRRead } from './pr-refresh-coordinator'
import { refreshIntervalForCandidate } from './pr-refresh-candidate-policy'
vi.mock('../../client/lookup/branch-lookup-resolution', () => ({
resolvePRForBranchOutcome: async () => ({
kind: 'found',
fetchedAt: Date.now(),
pr: makePR({
headSha: 'head',
checksStatus: 'success',
checksReadComplete: true,
checksFinal: true,
autoMergeEnabled: false,
mergeQueueRequired: false
})
})
}))
vi.mock('../../execution/github-operation-slots', () => ({
acquire: async () => {},
release: () => {}
}))
it('stretches a confirmed quiet PR’s interval by the cadence, as it does every other interval', async () => {
const candidate = makeCandidate({
branch: 'branch',
currentHeadOid: 'head',
cachedHeadOid: 'head',
cachedFetchedAt: 2_000,
cachedPRState: 'open',
cachedChecksStatus: 'success',
cachedHasPR: true
})
const read = () =>
runHostedPRRead(candidate, () =>
getPRForBranchOutcome('/repo', 'branch', null, null, null, { currentHeadOid: 'head' })
)
vi.useFakeTimers()
vi.setSystemTime(1_000)
await read()
vi.setSystemTime(2_000)
await read()
const unconfirmed = { ...candidate, fallbackPRNumber: 9 }
expect(
(['default', 'reduced', 'minimal', 'off'] as const).map((cadence) => [
refreshIntervalForCandidate(candidate, cadence),
refreshIntervalForCandidate(unconfirmed, cadence)
])
).toEqual([
[300_000, 120_000],
[1_500_000, 600_000],
[3_000_000, 1_200_000],
[Infinity, Infinity]
])
vi.useRealTimers()
})
@@ -1,88 +0,0 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PRInfo } from '../../../../shared/github/pull-requests/pull-request-types'
const { coordinatorMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./pr-refresh-coordinator-test-mocks')
return { coordinatorMocks: moduleMocks.createPRRefreshCoordinatorMocks(), moduleMocks }
})
vi.mock('./pr-refresh-renderer-port', () => moduleMocks.rendererPortModuleMock(coordinatorMocks))
vi.mock('../../client/lookup/pr-for-branch-outcome', () => {
return moduleMocks.clientModuleMock(coordinatorMocks)
})
vi.mock('../../repositories/github-api-repository-remote-probe', () => {
return {
getOriginGitHubApiRepository: coordinatorMocks.getOriginGitHubApiRepositoryMock
}
})
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(coordinatorMocks))
import { makeCandidate, makePR } from './pr-refresh-coordinator-test-harness'
const { sendMock, getPRForBranchOutcomeMock } = coordinatorMocks
function deferred<T>(): {
promise: Promise<T>
resolve: (value: T) => void
reject: (error: unknown) => void
} {
let resolve!: (value: T) => void
let reject!: (error: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
describe('pr-refresh-coordinator', () => {
beforeEach(() => {
moduleMocks.resetPRRefreshCoordinatorMocks(coordinatorMocks)
})
// Transforming the coordinator's module graph is the slow first step on a loaded machine: do it
// once under the hook limit, so each test still imports its own fresh instance, quickly.
beforeAll(async () => {
await import('./pr-refresh-coordinator')
})
afterEach(() => {
// A body that failed must not leave its timers to run on the next test's clock.
vi.clearAllTimers()
vi.useRealTimers()
})
it('preserves an active refresh queued while a visible refresh is in flight', async () => {
const { enqueuePRRefresh, reportVisiblePRRefreshCandidates } =
await import('./pr-refresh-coordinator')
const visibleOutcome = deferred<{
kind: 'found'
pr: PRInfo
fetchedAt: number
}>()
getPRForBranchOutcomeMock.mockReturnValueOnce(visibleOutcome.promise).mockResolvedValueOnce({
kind: 'found',
pr: makePR({ checksStatus: 'success', state: 'merged' }),
fetchedAt: Date.now()
})
const candidate = makeCandidate()
reportVisiblePRRefreshCandidates([candidate], 1, 1)
await vi.advanceTimersByTimeAsync(0)
enqueuePRRefresh({ ...candidate, cachedFetchedAt: Date.now() }, 'active', 80, 1)
visibleOutcome.resolve({
kind: 'found',
pr: makePR({ checksStatus: 'pending', mergeable: 'MERGEABLE' }),
fetchedAt: Date.now()
})
await vi.advanceTimersByTimeAsync(0)
const inFlightEvents = sendMock.mock.calls
.map(([, event]) => event)
.filter((event) => event.status === 'in-flight')
expect(inFlightEvents.map((event) => event.reason)).toEqual(['visible', 'active'])
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
})
})
@@ -1,56 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import { makeCandidate, makePR } from './pr-refresh-coordinator-test-harness'
const { coordinatorMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./pr-refresh-coordinator-test-mocks')
return { coordinatorMocks: moduleMocks.createPRRefreshCoordinatorMocks(), moduleMocks }
})
vi.mock('./pr-refresh-renderer-port', () => moduleMocks.rendererPortModuleMock(coordinatorMocks))
vi.mock('../../client/lookup/pr-for-branch-outcome', () => {
return moduleMocks.clientModuleMock(coordinatorMocks)
})
vi.mock('../../repositories/github-api-repository-remote-probe', () => {
return {
getOriginGitHubApiRepository: coordinatorMocks.getOriginGitHubApiRepositoryMock
}
})
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(coordinatorMocks))
const { getPRForBranchOutcomeMock } = coordinatorMocks
function found(state: 'open' | 'closed' | 'merged'): PRRefreshOutcome {
return { kind: 'found', pr: makePR({ state, checksStatus: 'success' }), fetchedAt: Date.now() }
}
describe('main refresh completion ordering', () => {
beforeEach(() => moduleMocks.resetPRRefreshCoordinatorMocks(coordinatorMocks))
afterEach(() => vi.useRealTimers())
it('preserves a newer failure gate when an older successful lookup completes', async () => {
let finishOlder: (outcome: PRRefreshOutcome) => void = () => {}
getPRForBranchOutcomeMock
.mockImplementationOnce(
() => new Promise<PRRefreshOutcome>((resolve) => (finishOlder = resolve))
)
.mockImplementation(async () => ({
kind: 'upstream-error',
errorType: 'rate_limited',
message: 'wait',
fetchedAt: Date.now(),
retryDisabledUntil: Date.now() + 300_000
}))
const { reportVisiblePRRefreshCandidates, refreshPRNow } =
await import('./pr-refresh-coordinator')
const candidate = makeCandidate({ isSelected: true })
reportVisiblePRRefreshCandidates([candidate], 1, 1)
await vi.advanceTimersByTimeAsync(0)
await refreshPRNow(candidate)
finishOlder(found('open'))
await vi.advanceTimersByTimeAsync(60_000)
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
await expect(refreshPRNow(candidate)).resolves.toMatchObject({ errorType: 'rate_limited' })
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
})
})
@@ -1,76 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { GitHubPRRefreshCandidate } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
const { coordinatorMocks, moduleMocks } = await vi.hoisted(async () => {
const moduleMocks = await import('./pr-refresh-coordinator-test-mocks')
return { coordinatorMocks: moduleMocks.createPRRefreshCoordinatorMocks(), moduleMocks }
})
vi.mock('./pr-refresh-renderer-port', () => moduleMocks.rendererPortModuleMock(coordinatorMocks))
vi.mock('../../client/lookup/pr-for-branch-outcome', () => {
return moduleMocks.clientModuleMock(coordinatorMocks)
})
vi.mock('../../repositories/github-api-repository-remote-probe', () => {
return {
getOriginGitHubApiRepository: coordinatorMocks.getOriginGitHubApiRepositoryMock
}
})
vi.mock('../../api/rate-limit', () => moduleMocks.rateLimitModuleMock(coordinatorMocks))
import { makeCandidate, makePR } from './pr-refresh-coordinator-test-harness'
const { getPRForBranchOutcomeMock } = coordinatorMocks
const LINKED_PR_KEY = 'local::runtime:host::/repo::pr::42'
describe('pr-refresh queue growth bounds', () => {
beforeEach(() => {
moduleMocks.resetPRRefreshCoordinatorMocks(coordinatorMocks)
getPRForBranchOutcomeMock.mockImplementation(async () => ({
kind: 'found' as const,
pr: makePR({ checksStatus: 'pending' as const }),
fetchedAt: Date.now()
}))
})
afterEach(() => {
vi.useRealTimers()
})
it('keeps the newer alias when a branch switch lands mid-request', async () => {
const { reportVisiblePRRefreshCandidates, _getPRRefreshAliasCountForTests } =
await import('./pr-refresh-coordinator')
const churnCandidate = (index: number): GitHubPRRefreshCandidate =>
makeCandidate({
cacheKey: `/repo::churn/${index}`,
branch: `churn/${index}`,
worktreeId: 'wt-churn',
linkedPRNumber: 42,
cachedFetchedAt: null
})
let switched = false
getPRForBranchOutcomeMock.mockImplementation(async () => {
if (!switched) {
switched = true
// Re-enqueue after dequeue so the older in-flight snapshot merges into a newer queued entry.
reportVisiblePRRefreshCandidates([churnCandidate(2)], 2, 1)
}
return {
kind: 'found' as const,
pr: makePR({ checksStatus: 'pending' as const }),
fetchedAt: Date.now()
}
})
reportVisiblePRRefreshCandidates([churnCandidate(1)], 1, 1)
await vi.advanceTimersByTimeAsync(120_000)
expect(_getPRRefreshAliasCountForTests(LINKED_PR_KEY)).toBe(1)
const broadcasts = coordinatorMocks.sendMock.mock.calls.filter(
(call) => call[0] === 'gh:prRefreshEvent'
) as [string, { aliases: { cacheKey: string }[] }][]
const lastBroadcast = broadcasts.at(-1)
expect(lastBroadcast?.[1].aliases.map((alias) => alias.cacheKey)).toEqual(['/repo::churn/2'])
})
})
@@ -1,128 +0,0 @@
import { afterEach, expect, it, vi } from 'vitest'
import type { PRRefreshOutcome } from '../../../../shared/github/pull-requests/refresh/pull-request-refresh-types'
import type { PullRequestLookupData } from '../../client/lookup/pull-request-lookup-data'
import { makeCandidate } from './pr-refresh-coordinator-test-harness'
const fixture = vi.hoisted(() => ({
lookup: vi.fn(),
conflict: vi.fn(),
stack: { number: 2, position: 1, size: 2, baseRefName: 'main', baseSha: 'base-sha' }
}))
vi.mock('../../repositories/github-api-repository', () => ({
resolveGitHubApiRepositoryCandidates: async () => ({
candidates: [{ owner: 'acme', repo: 'widgets' }],
headRepo: { owner: 'acme', repo: 'widgets' }
})
}))
vi.mock('../../repositories/github-api-repository-remote-probe', () => ({
getOriginGitHubApiRepository: async () => ({ owner: 'acme', repo: 'widgets' })
}))
vi.mock('../../api/rate-limit', () => ({
repositoryRateLimitGuard: () => ({ blocked: false }),
spendsSharedGitHubComQuota: () => false
}))
vi.mock('../../client/lookup/pr-number-lookup', () => ({ lookupPRByNumber: fixture.lookup }))
vi.mock('../../client/lookup/pr-stack-summary-cache', () => ({
getCachedGitHubPRStackSummary: async () => fixture.stack
}))
vi.mock('../stacks/github-pr-stack', () => ({
hydrateGitHubPRStack: async () => fixture.stack
}))
vi.mock('../../client/detect/repository-merge-metadata', () => ({
detectRepositoryMergeMetadata: async () => ({ mergeQueueRequired: true })
}))
vi.mock('../conflicts/conflict-summary', () => ({
getPRConflictSummary: fixture.conflict
}))
const data: PullRequestLookupData = {
number: 12,
title: 'Conflicting change',
state: 'OPEN',
url: 'https://github.com/acme/widgets/pull/12',
updatedAt: '2026-11-11T00:00:00Z',
mergeable: 'CONFLICTING',
baseRefName: 'main',
baseRefOid: 'base-sha',
headRefName: 'topic',
headRefOid: 'head-sha',
checksUnavailable: false,
statusCheckRollup: [{ __typename: 'CheckRun', name: 'build', status: 'IN_PROGRESS' }]
}
const summary = { baseRef: 'main', baseCommit: 'base-sh', commitsBehind: 1, files: ['a.ts'] }
afterEach(() => vi.useRealTimers())
it('watches preserve provider checks while full and phone reads retain derived details', async () => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
fixture.lookup.mockImplementation(async () => ({
data: { ...data },
dataRepo: { owner: 'acme', repo: 'widgets' }
}))
fixture.conflict.mockResolvedValue(summary)
const { addPRRefreshOutcomeObserver, schedulePRWatchRead } =
await import('./pr-refresh-coordinator')
const { getPRForBranchOutcome, readRecentPhonePROutcome } =
await import('../../client/lookup/pr-for-branch-outcome')
const candidate = makeCandidate({ branch: 'topic', linkedPRNumber: 12 })
const watched = Promise.withResolvers<PRRefreshOutcome>()
const unsubscribe = addPRRefreshOutcomeObserver((_, outcome) => watched.resolve(outcome))
schedulePRWatchRead({ prKey: 'github.com/acme/widgets#12' }, candidate, Date.now())
await vi.advanceTimersByTimeAsync(0)
const watch = await watched.promise
unsubscribe()
expect(watch).toMatchObject({
kind: 'found',
pr: { number: 12, headSha: 'head-sha', state: 'open', checksStatus: 'pending' }
})
if (watch.kind !== 'found') {
throw new Error('watch did not find the PR')
}
expect(watch.pr.conflictSummary).toBeUndefined()
expect(watch.pr.stack).toBeUndefined()
expect(watch.pr.mergeQueueRequired).toBeUndefined()
// An uncapped partial read must also stay separate from a concurrent full read.
const provider = Promise.withResolvers<void>()
const entered = Promise.withResolvers<void>()
const conflict = Promise.withResolvers<typeof summary>()
fixture.lookup.mockImplementation(async () => {
entered.resolve()
await provider.promise
return { data: { ...data }, dataRepo: { owner: 'acme', repo: 'widgets' } }
})
fixture.conflict.mockReturnValue(conflict.promise)
const partial = getPRForBranchOutcome('/repo', 'topic', 12, null, null, {
includeDerivedData: false
})
await entered.promise
const full = getPRForBranchOutcome('/repo', 'topic', 12, null, null)
provider.resolve()
try {
expect(await partial).toMatchObject({ kind: 'found', pr: { conflictSummary: undefined } })
expect(
await readRecentPhonePROutcome('/repo', 'topic', 12, null, null, {
currentHeadOid: 'head-sha'
})
).toBeNull()
} finally {
conflict.resolve(summary)
}
const complete = await full
expect(complete).toMatchObject({
kind: 'found',
pr: {
number: 12,
headSha: 'head-sha',
checksStatus: 'pending',
conflictSummary: { files: ['a.ts'], commitsBehind: 1 },
stack: { number: 2, baseRefName: 'main' },
mergeQueueRequired: true
}
})
expect(
await readRecentPhonePROutcome('/repo', 'topic', 12, null, null, { currentHeadOid: 'head-sha' })
).toEqual(complete)
})
@@ -1,53 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { ghExecFileAsyncMock, rateLimitGuardMock, noteRateLimitSpendMock } = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
rateLimitGuardMock: vi.fn(() => ({ blocked: false })),
noteRateLimitSpendMock: vi.fn()
}))
vi.mock('../../../git/command-runner/hosted-cli/gh-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../../api/rate-limit', () => ({
repositoryRateLimitGuard: rateLimitGuardMock,
noteRepositoryRateLimitSpend: noteRateLimitSpendMock
}))
import { _resetGitHubPRStackCacheForTests } from './github-pr-stack'
import { mergeGitHubPRStack } from './github-pr-stack-async-merge'
const repository = { owner: 'stablyai', repo: 'orca', host: 'github.com' }
beforeEach(() => {
vi.useRealTimers()
ghExecFileAsyncMock.mockReset()
rateLimitGuardMock.mockReset()
rateLimitGuardMock.mockReturnValue({ blocked: false })
noteRateLimitSpendMock.mockReset()
_resetGitHubPRStackCacheForTests()
})
describe('mergeGitHubPRStack', () => {
it('reports an in-progress merge after polling is exhausted', async () => {
vi.useFakeTimers()
ghExecFileAsyncMock.mockResolvedValue({
stdout: JSON.stringify({ status: 'pending', details: { uuid: 'merge-uuid' } })
})
const result = mergeGitHubPRStack({
repository,
prNumber: 202,
method: 'squash',
mergeAction: 'direct_merge',
ghOptions: { cwd: '/repo' }
})
await vi.advanceTimersByTimeAsync(180_000)
await expect(result).resolves.toMatchObject({
ok: false,
error: 'GitHub is still merging this stack. Refresh to check its status.'
})
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(181)
})
})
@@ -1,68 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitExecFileModule from '../../git/command-runner/git-exec-file'
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
gitExecFileAsyncMock: vi.fn<(args: string[]) => Promise<{ stdout: string; stderr: string }>>()
}))
vi.mock('../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitExecFileModule>()),
gitExecFileAsync: gitExecFileAsyncMock
}))
import { _resetOwnerRepoCache } from './github-repository-identity'
import { _resetRemoteNameListingCache } from '../../git/remotes/remote-name-listing'
import {
_resetOriginGitHubApiRepositoryCache,
getGitHubApiRepositoryForRemote
} from './github-api-repository-remote-probe'
// Why: no real .git config means no config signature, so caches use their short unsigned TTLs.
const REPO_PATH = '/does-not-exist/orca-remote-probe-fixture'
function mockRemotes(remotes: Record<string, string>): void {
gitExecFileAsyncMock.mockImplementation((args) => {
if (args[0] === 'remote' && args.length === 1) {
return Promise.resolve({ stdout: Object.keys(remotes).join('\n'), stderr: '' })
}
if (args[0] === 'remote' && args[1] === 'get-url') {
const url = remotes[args[2]]
if (url) {
return Promise.resolve({ stdout: `${url}\n`, stderr: '' })
}
return Promise.reject(
Object.assign(new Error(`git remote get-url ${args[2]}`), {
stderr: `error: No such remote '${args[2]}'`
})
)
}
return Promise.reject(new Error(`unexpected git exec: ${JSON.stringify(args)}`))
})
}
beforeEach(() => {
vi.useFakeTimers()
gitExecFileAsyncMock.mockReset()
_resetOriginGitHubApiRepositoryCache()
_resetOwnerRepoCache()
_resetRemoteNameListingCache()
})
afterEach(() => {
vi.useRealTimers()
})
describe('getGitHubApiRepositoryForRemote', () => {
it('still resolves a configured upstream remote', async () => {
mockRemotes({
origin: 'https://github.com/acme/orca.git',
upstream: 'https://github.com/stablyai/orca.git'
})
await expect(getGitHubApiRepositoryForRemote(REPO_PATH, 'upstream')).resolves.toEqual({
owner: 'stablyai',
repo: 'orca',
host: 'github.com'
})
})
})
@@ -1,55 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as GitRunner from '../../git/command-runner/git-exec-file'
const { gitExecFileAsyncMock, readLocalGitConfigSignatureMock } = vi.hoisted(() => ({
gitExecFileAsyncMock: vi.fn(),
readLocalGitConfigSignatureMock: vi.fn<() => Promise<string | undefined>>(async () => 'sig-1')
}))
vi.mock('../../git/command-runner/git-exec-file', async (importOriginal) => ({
...(await importOriginal<typeof GitRunner>()),
gitExecFileAsync: gitExecFileAsyncMock
}))
vi.mock('./local-git-config-signature', () => ({
readLocalGitConfigSignature: readLocalGitConfigSignatureMock
}))
import { getOwnerRepoForRemote, _resetOwnerRepoCache } from './github-repository-identity'
const REPO = '/tmp/signed-cache-repo'
const THIRTY_SECONDS = 30_000
let remoteUrl = 'https://github.com/stablyai/orca.git'
const remoteGetUrlCalls = (): number =>
gitExecFileAsyncMock.mock.calls.filter(([args]) => (args as string[])[1] === 'get-url').length
beforeEach(() => {
_resetOwnerRepoCache()
vi.useRealTimers()
gitExecFileAsyncMock.mockReset()
remoteUrl = 'https://github.com/stablyai/orca.git'
readLocalGitConfigSignatureMock.mockReset()
readLocalGitConfigSignatureMock.mockImplementation(async () => 'sig-1')
gitExecFileAsyncMock.mockImplementation(async () => ({ stdout: remoteUrl }))
})
describe('owner/repo identity cache', () => {
it('re-probes as soon as the git config signature changes', async () => {
vi.useFakeTimers()
await getOwnerRepoForRemote(REPO, 'origin')
expect(remoteGetUrlCalls()).toBe(1)
remoteUrl = 'https://github.com/other-org/orca.git'
readLocalGitConfigSignatureMock.mockImplementation(async () => 'sig-2')
vi.setSystemTime(Date.now() + THIRTY_SECONDS)
await expect(getOwnerRepoForRemote(REPO, 'origin')).resolves.toEqual({
owner: 'other-org',
repo: 'orca'
})
expect(remoteGetUrlCalls()).toBe(2)
vi.useRealTimers()
})
})
@@ -1,218 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../repositories/github-repository-identity'
import type * as WorkItemDetailsTestLookup from './work-item-details-test-lookup'
import { beforeEach, describe, expect, it, vi } from 'vitest'
type RateLimitGuardResult =
| { blocked: false }
| { blocked: true; remaining: number; limit: number; resetAt: number }
const {
ghExecFileAsyncMock,
getEnterpriseGitHubRepoSlugMock,
getOwnerRepoMock,
getWorkItemMock,
getPRChecksMock,
getPRCommentsMock,
noteRepositoryRateLimitSpendMock,
repositoryRateLimitGuardMock
} = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getEnterpriseGitHubRepoSlugMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
getWorkItemMock: vi.fn(),
getPRChecksMock: vi.fn(),
getPRCommentsMock: vi.fn(),
noteRepositoryRateLimitSpendMock:
vi.fn<
(
repository: { host?: string } | null | undefined,
bucket: string,
cost?: number,
options?: { cwd?: string; host?: string }
) => void
>(),
repositoryRateLimitGuardMock: vi.fn<
(
repository: { host?: string } | null | undefined,
bucket: string,
options?: { cwd?: string; host?: string }
) => RateLimitGuardResult
>(() => ({ blocked: false }))
}))
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
getOwnerRepoForRemote: getOwnerRepoMock,
ghRepoExecOptions: vi.fn((context) => ({ cwd: context.repoPath })),
githubRepoContext: vi.fn((repoPath, connectionId, localGitOptions) => ({
repoPath,
connectionId: connectionId ?? null,
...localGitOptions
}))
}))
vi.mock('../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: vi.fn()
}))
vi.mock('../execution/github-operation-slots', () => ({
acquire: vi.fn(),
release: vi.fn()
}))
vi.mock('../client/fetch/get-work-item', async () => ({
getWorkItem: getWorkItemMock,
getWorkItemWithRepository: (
await vi.importActual<typeof WorkItemDetailsTestLookup>('./work-item-details-test-lookup')
).makeWorkItemDetailsLookupMock(getWorkItemMock),
getWorkItemByOwnerRepo: vi.fn()
}))
vi.mock('../client/check/get-pr-checks', async () => ({
getPRChecks: getPRChecksMock
}))
vi.mock('../client/fetch/get-pr-comments', async () => ({
getPRComments: getPRCommentsMock
}))
vi.mock('../repositories/github-enterprise-repository', () => ({
getEnterpriseGitHubRepoSlug: getEnterpriseGitHubRepoSlugMock,
isGitHubHostAuthenticated: vi.fn().mockResolvedValue(true)
}))
vi.mock('../api/rate-limit', () => ({
rateLimitGuard: vi.fn(() => ({ blocked: false })),
noteRateLimitSpend: vi.fn(),
repositoryRateLimitGuard: repositoryRateLimitGuardMock,
noteRepositoryRateLimitSpend: noteRepositoryRateLimitSpendMock
}))
import { getWorkItemDetails } from './work-item-details'
import { _resetOriginGitHubApiRepositoryCache } from '../repositories/github-api-repository-remote-probe'
beforeEach(() => {
_resetOriginGitHubApiRepositoryCache()
})
function pullRequestItem(number: number, title: string): Record<string, unknown> {
return {
id: `pr:${number}`,
type: 'pr',
number,
title,
state: 'open',
url: `https://github.com/acme/widgets/pull/${number}`,
labels: [],
updatedAt: '2026-07-16T00:00:00Z',
author: 'pr-author'
}
}
function auxiliaryPRResponse(args: string[]): { stdout: string } {
const query = args.find((arg) => arg.startsWith('query=')) ?? ''
if (query.includes('viewerViewedState')) {
return {
stdout: JSON.stringify({
data: {
repository: {
viewedPullRequest: {
id: 'PR_file_list',
files: { pageInfo: { hasNextPage: false }, nodes: [] }
}
}
}
})
}
}
if (query.includes('participants(first: 100)')) {
return {
stdout: JSON.stringify({
data: { repository: { pullRequest: { participants: { nodes: [] } } } }
})
}
}
return { stdout: JSON.stringify({ data: {} }) }
}
describe('getWorkItemDetails PR file listing', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getOwnerRepoMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
getEnterpriseGitHubRepoSlugMock.mockReset()
getEnterpriseGitHubRepoSlugMock.mockResolvedValue(null)
getWorkItemMock.mockReset()
getPRChecksMock.mockReset()
getPRChecksMock.mockResolvedValue([])
getPRCommentsMock.mockReset()
getPRCommentsMock.mockResolvedValue([])
noteRepositoryRateLimitSpendMock.mockReset()
repositoryRateLimitGuardMock.mockReset()
repositoryRateLimitGuardMock.mockReturnValue({ blocked: false })
})
it('loads files beyond the first 100-result REST page', async () => {
getWorkItemMock.mockResolvedValueOnce(pullRequestItem(108, 'Large PR'))
const restFile = (index: number) => ({
filename: `src/file-${index}.ts`,
status: 'modified',
additions: 1,
deletions: 0,
changes: 1,
patch: '@@ -1 +1 @@'
})
ghExecFileAsyncMock.mockImplementation(async (args: string[]) => {
const endpoint = args.find((arg) => arg.startsWith('repos/')) ?? ''
if (endpoint === 'repos/acme/widgets/pulls/108') {
return { stdout: JSON.stringify({ head: { sha: 'head' }, base: { sha: 'base' } }) }
}
if (endpoint === 'repos/acme/widgets/pulls/108/files?per_page=100') {
return {
stdout: JSON.stringify(Array.from({ length: 100 }, (_, index) => restFile(index)))
}
}
if (endpoint === 'repos/acme/widgets/pulls/108/files?per_page=100&page=2') {
return { stdout: JSON.stringify([restFile(100)]) }
}
return auxiliaryPRResponse(args)
})
const details = await getWorkItemDetails('/repo-root', 108, 'pr')
expect(details?.files).toHaveLength(101)
expect(details?.files?.at(-1)?.path).toBe('src/file-100.ts')
const fileEndpoints = ghExecFileAsyncMock.mock.calls
.map(([args]) => (args as string[]).find((arg) => arg.includes('/files?')))
.filter(Boolean)
expect(fileEndpoints).toEqual([
'repos/acme/widgets/pulls/108/files?per_page=100',
'repos/acme/widgets/pulls/108/files?per_page=100&page=2'
])
expect(
noteRepositoryRateLimitSpendMock.mock.calls.filter(([, bucket]) => bucket === 'core')
).toHaveLength(3)
expect(
repositoryRateLimitGuardMock.mock.calls.filter(([, bucket]) => bucket === 'core')
).toHaveLength(3)
})
it('does not run bare PR detail commands when local host resolution fails', async () => {
getWorkItemMock.mockResolvedValueOnce({
...pullRequestItem(9, 'Unresolved PR'),
prRepo: { owner: 'team', repo: 'orca' }
})
getOwnerRepoMock.mockResolvedValue(null)
const details = await getWorkItemDetails('/repo-root', 9, 'pr')
expect(details).toMatchObject({ body: '', comments: [], checks: [], filesUnavailable: true })
expect(getPRCommentsMock).not.toHaveBeenCalled()
expect(getPRChecksMock).not.toHaveBeenCalled()
expect(ghExecFileAsyncMock).not.toHaveBeenCalled()
})
})
@@ -1,212 +0,0 @@
import type * as GithubRepositoryIdentityModule from '../repositories/github-repository-identity'
import type * as WorkItemDetailsTestLookup from './work-item-details-test-lookup'
import { beforeEach, describe, expect, it, vi } from 'vitest'
type RateLimitGuardResult =
| { blocked: false }
| { blocked: true; remaining: number; limit: number; resetAt: number }
const {
ghExecFileAsyncMock,
getOwnerRepoMock,
getOwnerRepoForRemoteMock,
getEnterpriseGitHubRepoSlugMock,
getEnterpriseGitHubRepoSlugForRemoteMock,
getWorkItemMock,
getWorkItemByOwnerRepoMock,
getPRChecksMock,
getPRCommentsMock,
repositoryRateLimitGuardMock,
noteRepositoryRateLimitSpendMock,
ghRepoExecOptionsMock,
githubRepoContextMock,
acquireMock,
releaseMock
} = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn(),
getOwnerRepoMock: vi.fn(),
getOwnerRepoForRemoteMock: vi.fn(),
getEnterpriseGitHubRepoSlugMock: vi.fn(),
getEnterpriseGitHubRepoSlugForRemoteMock: vi.fn(),
getWorkItemMock: vi.fn(),
getWorkItemByOwnerRepoMock: vi.fn(),
getPRChecksMock: vi.fn(),
getPRCommentsMock: vi.fn(),
repositoryRateLimitGuardMock: vi.fn<
(
repository: { host?: string } | null | undefined,
bucket: 'core' | 'graphql' | 'search',
options?: { cwd?: string; wslDistro?: string }
) => RateLimitGuardResult
>(() => ({ blocked: false })),
noteRepositoryRateLimitSpendMock: vi.fn(),
ghRepoExecOptionsMock: vi.fn((context) =>
context.connectionId
? {}
: { cwd: context.repoPath, ...(context.wslDistro ? { wslDistro: context.wslDistro } : {}) }
),
githubRepoContextMock: vi.fn((repoPath, connectionId, localGitOptions) => ({
repoPath,
connectionId: connectionId ?? null,
...localGitOptions
})),
acquireMock: vi.fn(),
releaseMock: vi.fn()
}))
vi.mock('../../git/command-runner/hosted-cli/gh-exec-file', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('../repositories/github-owner-repo-selection', () => ({
getOwnerRepo: getOwnerRepoMock
}))
vi.mock('../repositories/github-repository-identity', async (importOriginal) => ({
...(await importOriginal<typeof GithubRepositoryIdentityModule>()),
getOwnerRepoForRemote: getOwnerRepoForRemoteMock,
ghRepoExecOptions: ghRepoExecOptionsMock,
githubRepoContext: githubRepoContextMock
}))
vi.mock('../execution/github-operation-slots', () => ({
acquire: acquireMock,
release: releaseMock
}))
vi.mock('../client/fetch/get-work-item', async () => ({
getWorkItem: getWorkItemMock,
getWorkItemWithRepository: (
await vi.importActual<typeof WorkItemDetailsTestLookup>('./work-item-details-test-lookup')
).makeWorkItemDetailsLookupMock(getWorkItemMock),
getWorkItemByOwnerRepo: getWorkItemByOwnerRepoMock
}))
vi.mock('../client/check/get-pr-checks', async () => ({
getPRChecks: getPRChecksMock
}))
vi.mock('../client/fetch/get-pr-comments', async () => ({
getPRComments: getPRCommentsMock
}))
vi.mock('../repositories/github-enterprise-repository', () => ({
getEnterpriseGitHubRepoSlug: getEnterpriseGitHubRepoSlugMock,
getEnterpriseGitHubRepoSlugForRemote: getEnterpriseGitHubRepoSlugForRemoteMock,
isGitHubHostAuthenticated: vi.fn().mockResolvedValue(true)
}))
vi.mock('../api/rate-limit', () => ({
repositoryRateLimitGuard: repositoryRateLimitGuardMock,
noteRepositoryRateLimitSpend: noteRepositoryRateLimitSpendMock
}))
import { getWorkItemDetails } from './work-item-details'
import { _resetOriginGitHubApiRepositoryCache } from '../repositories/github-api-repository-remote-probe'
beforeEach(() => {
_resetOriginGitHubApiRepositoryCache()
})
describe('getWorkItemDetails', () => {
beforeEach(() => {
ghExecFileAsyncMock.mockReset()
getOwnerRepoMock.mockReset()
getOwnerRepoForRemoteMock.mockReset()
getEnterpriseGitHubRepoSlugMock.mockReset()
getEnterpriseGitHubRepoSlugMock.mockResolvedValue(null)
getEnterpriseGitHubRepoSlugForRemoteMock.mockReset()
getEnterpriseGitHubRepoSlugForRemoteMock.mockResolvedValue(null)
getWorkItemMock.mockReset()
getWorkItemByOwnerRepoMock.mockReset()
getPRChecksMock.mockReset()
getPRCommentsMock.mockReset()
repositoryRateLimitGuardMock.mockReset()
repositoryRateLimitGuardMock.mockReturnValue({ blocked: false })
noteRepositoryRateLimitSpendMock.mockReset()
ghRepoExecOptionsMock.mockClear()
githubRepoContextMock.mockClear()
acquireMock.mockReset()
releaseMock.mockReset()
acquireMock.mockResolvedValue(undefined)
})
it('caps issue timeline pagination by supported activity items', async () => {
getWorkItemMock.mockResolvedValueOnce({
id: 'issue:923',
type: 'issue',
number: 923,
title: 'Use upstream issues',
state: 'open',
url: 'https://github.com/acme/widgets/issues/923',
labels: [],
updatedAt: '2026-04-01T00:00:00Z',
author: 'issue-author'
})
getOwnerRepoForRemoteMock.mockResolvedValue({ owner: 'acme', repo: 'widgets' })
const makeTimelineEvent = (page: number, index: number, event: string): string =>
JSON.stringify({
id: `${page}:${index}`,
event,
actor: { login: 'issue-author', avatar_url: 'https://x/y' },
assignee: { login: `assignee-${page}-${index}` },
created_at: '2026-04-01T00:00:00Z'
})
const makeTimelinePage = (page: number, supportedCount: number): string =>
Array.from({ length: 100 }, (_, index) =>
makeTimelineEvent(page, index, index < supportedCount ? 'assigned' : 'subscribed')
).join('\n')
ghExecFileAsyncMock
.mockResolvedValueOnce({
stdout: JSON.stringify({
data: {
repository: {
issue: {
body: 'Issue body',
assignees: { nodes: [] },
participants: { nodes: [] },
comments: { nodes: [] }
}
}
}
})
})
.mockResolvedValueOnce({ stdout: makeTimelinePage(1, 0) })
.mockResolvedValueOnce({ stdout: makeTimelinePage(2, 0) })
.mockResolvedValueOnce({ stdout: makeTimelinePage(3, 10) })
.mockResolvedValueOnce({ stdout: makeTimelinePage(4, 100) })
.mockResolvedValueOnce({ stdout: makeTimelinePage(5, 100) })
.mockResolvedValueOnce({ stdout: makeTimelinePage(6, 100) })
const details = await getWorkItemDetails('/repo-root', 923, 'issue')
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(7)
expect(ghExecFileAsyncMock.mock.calls[1][0]).toContain(
'repos/acme/widgets/issues/923/timeline?per_page=100&page=1'
)
expect(ghExecFileAsyncMock.mock.calls[2][0]).toContain(
'repos/acme/widgets/issues/923/timeline?per_page=100&page=2'
)
expect(ghExecFileAsyncMock.mock.calls[3][0]).toContain(
'repos/acme/widgets/issues/923/timeline?per_page=100&page=3'
)
expect(ghExecFileAsyncMock.mock.calls[6][0]).toContain(
'repos/acme/widgets/issues/923/timeline?per_page=100&page=6'
)
expect(
ghExecFileAsyncMock.mock.calls.some((call) =>
call[0].includes('repos/acme/widgets/issues/923/timeline?per_page=100&page=7')
)
).toBe(false)
const timelineItems = details?.timelineItems
if (!timelineItems) {
throw new Error('Expected timeline items to be present')
}
expect(timelineItems).toHaveLength(300)
expect(timelineItems.at(0)).toMatchObject({ assignee: 'assignee-3-0' })
expect(timelineItems.at(-1)).toMatchObject({ assignee: 'assignee-6-89' })
})
// Number-based detail lookups must retain the list's source, avoiding same-number fork/upstream collisions.
})
@@ -1,39 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { glabExecFileAsyncMock } = vi.hoisted(() => ({ glabExecFileAsyncMock: vi.fn() }))
vi.mock('../../git/command-runner/hosted-cli/glab-exec-file', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
glabExecFileAsync: glabExecFileAsyncMock
}))
import {
_resetKnownHostsCache,
getGlabKnownHosts,
rememberGlabKnownHost
} from './gitlab-known-host-probe'
describe('getGlabKnownHosts', () => {
afterEach(() => vi.restoreAllMocks())
beforeEach(() => {
glabExecFileAsyncMock.mockReset()
_resetKnownHostsCache()
})
it('preserves a native auth refresh while an older native probe is in flight', async () => {
let resolveProbe!: (value: { stdout: string; stderr: string }) => void
glabExecFileAsyncMock.mockImplementationOnce(
() =>
new Promise((resolve) => {
resolveProbe = resolve
})
)
const staleProbe = getGlabKnownHosts()
rememberGlabKnownHost('gitlab.refreshed.test')
resolveProbe({ stdout: 'Logged in to gitlab.com as user\n', stderr: '' })
await expect(staleProbe).resolves.toEqual(['gitlab.com', 'gitlab.refreshed.test'])
await expect(getGlabKnownHosts()).resolves.toEqual(['gitlab.com', 'gitlab.refreshed.test'])
})
})
@@ -1,73 +0,0 @@
import { afterEach, expect, it, vi } from 'vitest'
type RebaseHandler = (
event: unknown,
args: { worktreePath: string; baseRef: string }
) => Promise<void>
const { handlers, capture } = vi.hoisted(() => ({
handlers: new Map<string, RebaseHandler>(),
capture: vi.fn()
}))
vi.mock('electron', () => ({
ipcMain: { handle: (channel: string, handler: RebaseHandler) => handlers.set(channel, handler) }
}))
vi.mock('../../../git/command-runner/process-capture/exec-file-capture', () => ({
execFileCapture: capture,
execFileCaptureToTermination: capture
}))
vi.mock('../../../../shared/git/fetch/git-fetch-head-lock', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
runWithGitFetchHeadLock: (_cwd: string, _signal: AbortSignal | undefined, run: () => unknown) =>
run()
}))
vi.mock('../../../../shared/git/worktrees/git-worktree-operation-lock', () => ({
runWithGitWorktreeOperationLock: (
_cwd: string,
_signal: AbortSignal | undefined,
run: () => unknown
) => run()
}))
import { registerGitRemoteBranchMutationHandlers } from './branch-mutation-handlers'
afterEach(() => {
vi.useRealTimers()
vi.unstubAllEnvs()
})
it('surfaces the fetch diagnostic and retry guidance through the rebase IPC handler', async () => {
vi.useFakeTimers()
vi.stubEnv('GIT_SSH_COMMAND', 'ssh')
const original = Object.assign(
new Error('error: fetching ref refs/remotes/origin/main failed: incorrect old value provided'),
{
code: 1,
stderr: 'error: fetching ref refs/remotes/origin/main failed: incorrect old value provided'
}
)
capture.mockImplementation((_command, args, options) => {
options.onChildTerminated()
if (args[0] === 'fetch') {
return Promise.reject(original)
}
return Promise.resolve({ stdout: args[0] === 'remote' ? 'origin\n' : '', stderr: '' })
})
const store = {
getRepos: () => [{ id: 'repo', path: '/repo', displayName: 'repo', badgeColor: '', addedAt: 0 }]
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This handler only reads getRepos and checks optional runtime accessors.
registerGitRemoteBranchMutationHandlers({ store } as never)
const result = handlers.get('git:rebaseFromBase')!(null, {
worktreePath: '/repo',
baseRef: 'origin/main'
})
const rejection = expect(result).rejects.toMatchObject({
message: expect.stringMatching(
/fetching ref refs\/remotes\/origin\/main failed: incorrect old value provided[\s\S]+Another Git process updated remote references\. Try again\./
),
cause: expect.objectContaining({ cause: original })
})
await vi.runAllTimersAsync()
await rejection
})

Some files were not shown because too many files have changed in this diff Show More