Remove 390 low-value test cases and 18 whole suites (#38209)

Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
OrcaWin
2026-11-04 11:49:38 -08:00
committed by GitHub
co-authored by Codex
parent 8d662f5c63
commit 9160eb60e6
132 changed files with 126 additions and 8744 deletions
-1
View File
@@ -1110,7 +1110,6 @@ jobs:
xvfb-run --auto-servernum pnpm exec vitest run --config config/vitest.config.ts
--no-file-parallelism
src/main/browser/client-pages/renderer/browser-client-page-renderer-lifecycle.electron.test.ts
src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts
src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts
src/main/browser/cookies/browser-google-cookie-clear.electron.test.ts
src/main/browser/guest-lifecycle/popups/popup-origin-chrome.electron.test.ts
-2
View File
@@ -620,7 +620,6 @@
1 test-timeout-needs-reason:src/main/browser/cookies/browser-cookie-import-validated-partition.electron.test.ts
1 test-timeout-needs-reason:src/main/browser/guest-lifecycle/find/browser-find-frame-traversal.electron.test.ts
1 test-timeout-needs-reason:src/main/browser/identity/browser-session-ua-cloudflare-live.electron.test.ts
1 test-timeout-needs-reason:src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts
1 test-timeout-needs-reason:src/main/claude/claude-background-run-isolation-real-cli.test.ts
1 test-timeout-needs-reason:src/main/claude/claude-child-exit-proof-ladder.test.ts
1 test-timeout-needs-reason:src/main/claude/claude-stop-ack-race.test.ts
@@ -1316,7 +1315,6 @@
4 test-no-wall-clock:src/main/updater/lifecycle/serve-supervisor-install-park.test.ts
4 test-no-wall-clock:src/renderer/src/components/cmd-j/palette-live-status.test.tsx
4 test-no-wall-clock:src/renderer/src/components/sidebar/workspace-card/surface/WorktreeCard.compact-hover.test.tsx
4 test-no-wall-clock:src/renderer/src/components/status-bar/provider-segment-balance.test.tsx
4 test-no-wall-clock:src/renderer/src/components/task-page/jira/task-page-jira-cache-selectors.test.ts
4 test-no-wall-clock:tests/e2e/cross-version-wire/release-checkout.unit.test.ts
4 test-timeout-needs-reason:src/main/claude/claude-agent-sdk-exit-proof.test.ts
File diff suppressed because one or more lines are too long
-16
View File
@@ -4268,7 +4268,6 @@
"src/main/persistence/terminal-topology/duplicate-pty-pane-repair.test.ts": 2087,
"src/main/persistence/terminal-topology/editor-renames-across-partitions.test.ts": 526,
"src/main/persistence/terminal-topology/editor-tab-layout-apply.test.ts": 2243,
"src/main/persistence/terminal-topology/layout-owner-written-session-golden.test.ts": 1961,
"src/main/persistence/terminal-topology/open-in-new-group-layout-apply.test.ts": 1943,
"src/main/persistence/terminal-topology/pane-last-grid.test.ts": 5642,
"src/main/persistence/terminal-topology/persistence-binding-graft-placement.test.ts": 1949,
@@ -5910,7 +5909,6 @@
"src/main/runtime/terminal/stop/exact-terminal-stop-liveness.test.ts": 4321,
"src/main/runtime/terminal/stop/terminal-update-admission.test.ts": 48,
"src/main/runtime/terminal/surface/closed-pane-exit-cleanup-main.test.ts": 4281,
"src/main/runtime/terminal/surface/main-default-pane-acquisition.test.ts": 5812,
"src/main/runtime/terminal/surface/main-terminal-acquisition.test.ts": 4330,
"src/main/runtime/terminal/surface/pty-pane-copies-untouched-main.test.ts": 4585,
"src/main/runtime/terminal/surface/terminal-surface-replies-main.test.ts": 4324,
@@ -7127,7 +7125,6 @@
"src/renderer/src/components/editor/EditorPanel.diff-rich-owner.test.tsx": 2823,
"src/renderer/src/components/editor/EditorPanelHeaderPath.test.tsx": 2664,
"src/renderer/src/components/editor/external-changes/ExternalFileChangeBanner.test.tsx": 2142,
"src/renderer/src/components/editor/ImageViewer.test.tsx": 1872,
"src/renderer/src/components/editor/notebook/cells/IpynbCellEditor.external-sync.test.tsx": 1660,
"src/renderer/src/components/editor/notebook/kernel/IpynbKernelToolbar.test.tsx": 736,
"src/renderer/src/components/editor/notebook/document/IpynbRendering.test.tsx": 2143,
@@ -7483,7 +7480,6 @@
"src/renderer/src/components/mobile/paired-mobile-devices.test.ts": 192,
"src/renderer/src/components/mobile/use-mobile-page-paired-devices.test.ts": 475,
"src/renderer/src/components/native-chat-launch-resume-decision.test.tsx": 1844,
"src/renderer/src/components/native-chat-restart-action-notifications.test.ts": 2125,
"src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx": 3261,
"src/renderer/src/components/native-chat-resume-failed-chat-watch.test.ts": 1954,
"src/renderer/src/components/native-chat-resume-failure-guidance.test.ts": 240,
@@ -7802,7 +7798,6 @@
"src/renderer/src/components/network/CustomAddressDialog.test.tsx": 568,
"src/renderer/src/components/new-workspace/ComposerParentWorktreePicker.test.tsx": 870,
"src/renderer/src/components/new-workspace/DirectWorkItemSetupDialog.test.tsx": 5296,
"src/renderer/src/components/new-workspace/NewWorkspaceComposerAgentSection.test.tsx": 895,
"src/renderer/src/components/new-workspace/NewWorkspaceComposerNameSection.test.tsx": 2493,
"src/renderer/src/components/new-workspace/ProjectCombobox.dialog-handoff.test.tsx": 1043,
"src/renderer/src/components/new-workspace/ProjectCombobox.test.tsx": 848,
@@ -7905,9 +7900,7 @@
"src/renderer/src/components/right-sidebar/file-explorer/panel/FileExplorerToolbar.test.tsx": 1837,
"src/renderer/src/components/right-sidebar/file-explorer/mutations/FileExplorerVirtualRows.row-handlers.test.tsx": 3415,
"src/renderer/src/components/right-sidebar/hosted-review/folder-checks/FolderWorkspacePrChecksPanel.test.tsx": 863,
"src/renderer/src/components/right-sidebar/hosted-review/folder-checks/FolderWorkspacePrChecksRow.test.tsx": 293,
"src/renderer/src/components/right-sidebar/FolderWorkspaceWorktreesPanel.test.tsx": 461,
"src/renderer/src/components/right-sidebar/hosted-review/provider-actions/HostedReviewActions.draft.test.tsx": 1587,
"src/renderer/src/components/right-sidebar/plugin-panels/PluginPanel.test.tsx": 2710,
"src/renderer/src/components/right-sidebar/PortsPanel.test.tsx": 1618,
"src/renderer/src/components/right-sidebar/hosted-review/creation/PullRequestComposer.generate-tooltip.test.tsx": 1382,
@@ -7995,7 +7988,6 @@
"src/renderer/src/components/right-sidebar/source-control/ai/runSourceControlAgentActionStart.test.ts": 1917,
"src/renderer/src/components/right-sidebar/source-control/panel/source-control-branch-context-row.test.tsx": 790,
"src/renderer/src/components/right-sidebar/source-control/review/source-control-create-pr-intent-flow.test.ts": 71,
"src/renderer/src/components/right-sidebar/source-control/review/source-control-dropdown-items.create-pr-intent.test.ts": 324,
"src/renderer/src/components/right-sidebar/source-control/panel/source-control-dropdown-items.test.ts": 409,
"src/renderer/src/components/right-sidebar/source-control/review/source-control-manual-review-url.test.ts": 77,
"src/renderer/src/components/right-sidebar/source-control/review/source-control-primary-action.create-pr-intent.test.ts": 340,
@@ -8197,7 +8189,6 @@
"src/renderer/src/components/sidebar/LocalGitToolchainScanBanner.test.tsx": 2025,
"src/renderer/src/components/sidebar/MarkdownImageLightbox.test.tsx": 2284,
"src/renderer/src/components/sidebar/project-admission/dialog/NonGitFolderDialog.test.tsx": 2473,
"src/renderer/src/components/sidebar/worktree-list/rows/NoticeHostGlyph.test.tsx": 2697,
"src/renderer/src/components/sidebar/host-path-browser/RemoteFileBrowser.keyboard.test.tsx": 825,
"src/renderer/src/components/sidebar/host-path-browser/RemoteFileBrowser.paste.test.tsx": 708,
"src/renderer/src/components/sidebar/SetupGuideSidebarEntry.test.tsx": 386,
@@ -8305,7 +8296,6 @@
"src/renderer/src/components/sidebar/workspace-board/search/workspace-kanban-search.test.ts": 176,
"src/renderer/src/components/sidebar/workspace-board/drag/workspace-kanban-sidebar-drop.test.ts": 1891,
"src/renderer/src/components/sidebar/workspace-actions/context-menu/workspace-lineage-menu-actions.test.ts": 235,
"src/renderer/src/components/sidebar/worktree-list/controls/workspace-options-menu-items.test.tsx": 630,
"src/renderer/src/components/sidebar/workspace-card/review/workspace-reference-detail-read.test.ts": 662,
"src/renderer/src/components/sidebar/workspace-card/review/workspace-reference-refresh-targets.test.ts": 945,
"src/renderer/src/components/sidebar/worktree-list/parity/workspace-view-evaluator-parity.test.ts": 2374,
@@ -8318,7 +8308,6 @@
"src/renderer/src/components/sidebar/workspace-card/activity/worktree-agent-row-selectors.test.ts": 376,
"src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-compact-agent-row.stable-message.test.tsx": 2166,
"src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-idle-child-agents.test.ts": 1899,
"src/renderer/src/components/sidebar/workspace-card/surface/worktree-card-option-items.test.tsx": 459,
"src/renderer/src/components/sidebar/workspace-card/activity/worktree-card-status-inputs.test.ts": 55,
"src/renderer/src/components/sidebar/workspace-actions/deletion/worktree-delete-host-qualification.test.ts": 1225,
"src/renderer/src/components/sidebar/worktree-list/navigation/worktree-keyboard-cycle.test.ts": 131,
@@ -8426,7 +8415,6 @@
"src/renderer/src/components/status-bar/owner-usage-rate-limits.test.ts": 260,
"src/renderer/src/components/status-bar/ports-status-popover-rows.test.tsx": 710,
"src/renderer/src/components/status-bar/provider-account-sync-key.test.ts": 310,
"src/renderer/src/components/status-bar/provider-segment-balance.test.tsx": 2245,
"src/renderer/src/components/status-bar/provider-segment-monthly-window.test.tsx": 2106,
"src/renderer/src/components/status-bar/provider-segment-stale-warning.test.tsx": 423,
"src/renderer/src/components/status-bar/remote-host-connection-status.test.ts": 200,
@@ -8656,7 +8644,6 @@
"src/renderer/src/components/terminal-pane/cache-timer-seeding.test.ts": 108,
"src/renderer/src/components/terminal-pane/claude-detached-pane-restart.test.ts": 1938,
"src/renderer/src/components/terminal-pane/claude-diff-panel-notice-banner.test.tsx": 1986,
"src/renderer/src/components/terminal-pane/claude-old-terminal-banner.test.tsx": 2146,
"src/renderer/src/components/terminal-pane/claude-side-question-completion.test.ts": 2305,
"src/renderer/src/components/terminal-pane/closed-terminal-leaf-notice-layout-owner.test.ts": 1032,
"src/renderer/src/components/terminal-pane/codex-backfill-error-detector.test.ts": 26,
@@ -8891,7 +8878,6 @@
"src/renderer/src/components/terminal-pane/split-right-white-screen.test.ts": 53,
"src/renderer/src/components/terminal-pane/stale-document-visibility.test.ts": 88,
"src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.test.ts": 329,
"src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.test.ts": 88,
"src/renderer/src/components/terminal-pane/terminal-agent-session-fork.test.ts": 1377,
"src/renderer/src/components/terminal-pane/terminal-alternate-screen-parse.test.ts": 70,
"src/renderer/src/components/terminal-pane/terminal-appearance.test.ts": 339,
@@ -9157,7 +9143,6 @@
"src/renderer/src/components/workspace-space/SpaceWindowStatePublisher.test.tsx": 2008,
"src/renderer/src/components/worktree-creation/PendingNativeChatCreation.test.tsx": 4075,
"src/renderer/src/components/worktree-jump/WorktreeJumpPalette.linear-url.test.tsx": 3372,
"src/renderer/src/components/worktree-jump/WorktreeJumpPalette.mount-gating.test.tsx": 3635,
"src/renderer/src/components/worktree-jump/WorktreeJumpPalette.recent-tabs.behavior.test.tsx": 4445,
"src/renderer/src/components/worktree-jump/WorktreeJumpPalette.recent-tabs.test.tsx": 9384,
"src/renderer/src/components/worktree-jump/WorktreeJumpPalette.task-search.test.tsx": 10126,
@@ -9903,7 +9888,6 @@
"src/renderer/src/store/repos/wsl-moved-repo-ui-retention.test.ts": 2920,
"src/renderer/src/store/running-terminal-close-confirm.test.ts": 50,
"src/renderer/src/store/runtime-status-subscriptions.test.ts": 182,
"src/renderer/src/store/selectors.test.ts": 1816,
"src/renderer/src/store/slices/active-selection-owner-scoping.test.ts": 1909,
"src/renderer/src/store/slices/agent-generated-tab-title.test.ts": 1976,
"src/renderer/src/store/slices/agent-hibernation-live-anchor-e2e.test.ts": 1783,
-1
View File
@@ -51,7 +51,6 @@ export const UNIT_EXCLUDE = [
'src/main/browser/guest-lifecycle/popups/popup-origin-chrome.electron.test.ts',
'src/main/browser/guest-lifecycle/navigation/browser-link-materialization.electron.test.ts',
'src/main/browser/client-pages/renderer/browser-client-page-renderer-lifecycle.electron.test.ts',
'src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts',
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
'src/main/browser/guest-lifecycle/popups/browser-popup-compatibility.electron.test.ts',
'src/main/browser/guest-lifecycle/popups/browser-popup-adapter-compatibility.electron.test.ts',
-2
View File
@@ -7,7 +7,6 @@ export const LINUX_PACKAGE_TESTS = [
'src/main/browser/guest-lifecycle/popups/popup-origin-chrome.electron.test.ts',
'src/main/browser/guest-lifecycle/navigation/browser-link-materialization.electron.test.ts',
'src/main/browser/client-pages/renderer/browser-client-page-renderer-lifecycle.electron.test.ts',
'src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts',
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
'src/main/browser/guest-lifecycle/popups/browser-popup-compatibility.electron.test.ts',
'src/main/browser/guest-lifecycle/popups/browser-popup-adapter-compatibility.electron.test.ts',
@@ -48,7 +47,6 @@ export const WINDOWS_BOUNDARY_TESTS = [
'config/scripts/package-electron-runtime-contract.test.mjs',
'config/scripts/electron-builder-runtime-resources.test.mjs',
'src/main/browser/client-pages/renderer/browser-client-page-renderer-lifecycle.electron.test.ts',
'src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts',
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts',
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
'src/main/providers/pty-repaint-wide-char-buffer.node-pty.test.ts',
@@ -65,7 +65,7 @@ describe('unit support routing', () => {
'src/renderer/src/components/native-chat/NativeChatStructuredSession.next-test-harness.tsx',
'src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx',
'src/main/providers/windows-conpty-wide-char-duplication.node-pty.test.ts',
'src/main/browser/network/egress/browser-route-webrtc-egress.electron.test.ts'
'src/main/browser/client-pages/renderer/browser-client-page-renderer-lifecycle.electron.test.ts'
]) {
const result = classifyPrJobs([unitHarness, file])
expect(result.should_run, file).toBe(true)
@@ -17,58 +17,6 @@ function finished(update: Partial<ToolCallUpdate>): ToolCallUpdate {
}
describe('Grok subagent session updates', () => {
it('reads a spawn with its description, its spawning turn and nothing it cannot show', () => {
expect(
grokSubagentNotification({
sessionUpdate: 'subagent_spawned',
subagent_id: 'subagent-1',
child_session_id: 'subagent-1',
parent_prompt_id: 'prompt-1',
subagent_type: 'general-purpose',
description: 'Count note lines',
model: 'grok-4.7'
})
).toEqual({ id: 'subagent-1', state: 'working', label: 'Count note lines', turn: 'prompt-1' })
})
it('labels a spawn with no description by its type', () => {
expect(
grokSubagentNotification({
sessionUpdate: 'subagent_spawned',
subagent_id: 'subagent-1',
subagent_type: 'explore'
})
).toEqual({ id: 'subagent-1', state: 'working', label: 'explore' })
})
it('reads progress as tokens only', () => {
expect(
grokSubagentNotification({
sessionUpdate: 'subagent_progress',
subagent_id: 'subagent-1',
tokens_used: 4091
})
).toEqual({ id: 'subagent-1', tokens: 4091 })
})
it.each([
['completed', { state: 'completed', result: '4' }],
['failed', { state: 'failed' }],
['cancelled', { state: 'stopped' }],
['something-new', {}]
] as const)('reads a %s finish', (status, expected) => {
expect(
grokSubagentNotification({
sessionUpdate: 'subagent_finished',
subagent_id: 'subagent-1',
status,
output: status === 'completed' ? '4' : undefined,
error: status === 'completed' ? undefined : 'Subagent was cancelled',
tokens_used: 0
})
).toEqual({ id: 'subagent-1', ...expected })
})
it('ignores every other update', () => {
expect(grokSubagentNotification({ sessionUpdate: 'turn_completed' })).toBeUndefined()
expect(grokSubagentNotification({ sessionUpdate: 'subagent_spawned' })).toBeUndefined()
@@ -96,21 +44,6 @@ describe('Grok subagent session updates', () => {
describe('Grok subagent tool calls', () => {
const spawnInput = { description: 'Count note lines', prompt: 'Read notes.txt' }
it('ends a foreground spawn with its reply, the machine blocks removed', () => {
expect(
grokToolSubagents(
finished({
rawOutput: {
type: 'SubagentCompleted',
subagent_id: 'subagent-1',
output: `4${SUBAGENT_RESULT_TAIL}`
}
}),
tool('spawn_subagent', spawnInput)
)
).toEqual([{ id: 'subagent-1', state: 'completed', label: 'Count note lines', result: '4' }])
})
it('keeps a background spawn running from its acknowledgement, labelled by the prompt when undescribed', () => {
expect(
grokToolSubagents(
@@ -177,13 +110,4 @@ describe('Grok subagent tool calls', () => {
])
expect(grokToolBackgroundTasks(update, tool('kill_command_or_subagent'))).toEqual([])
})
it('infers a kill with no result from the ids it named', () => {
expect(
grokToolSubagents(
finished({}),
tool('kill_command_or_subagent', { task_ids: ['subagent-1', 'subagent-1'] })
)
).toEqual([{ id: 'subagent-1', state: 'stopped', knownOnly: true }])
})
})
@@ -175,15 +175,4 @@ describe('agent launch cancellation', () => {
expect(result.prompt).toEqual({ delivery: 'submit', outcome: 'handed-to-terminal' })
})
it('hands its signal to the terminal prompt delivery', async () => {
const controller = new AbortController()
const h = harness({ lineCarriesPrompt: false })
await h.run(CREATE, controller.signal)
expect(h.deliverTerminalPrompt).toHaveBeenCalledWith(
expect.objectContaining({ signal: controller.signal })
)
})
})
@@ -55,128 +55,7 @@ const BASE: RuntimeManagedWorktreeCreateArgs = {
lineage: { noParent: true }
}
const FULL: RuntimeManagedWorktreeCreateArgs = {
...BASE,
navigation: 'caller',
createdWithAgent: 'claude',
startupLaunchPreferences: { model: 'opus', effort: 'high' },
startupAgentArgs: '--verbose',
startupExtraAgentArgs: '--extra',
startupCwd: 'packages/app',
startupPaneKey: 'tab-1:leaf-1',
pendingFirstAgentMessageRename: true,
automationProvenance: {
kind: 'created-by-automation',
automationId: 'auto-1',
automationNameSnapshot: 'Nightly',
automationRunId: 'run-1',
automationRunTitleSnapshot: 'Nightly run',
createdAt: 1,
executionTargetType: 'local',
executionTargetId: 'local',
projectId: 'project-1'
},
cliProvenance: { kind: 'created-by-cli', createdAt: 1, startupAgent: 'claude' },
lineage: { parentWorktree: 'id:parent', callerTerminalHandle: 'term_parent' },
comment: 'from the CLI',
linkedIssue: 7
}
describe('createWorktreeWithStartupAgent', () => {
it('forwards the startup agent and prompt to the create with the chat default on', async () => {
const { runtime, created, create } = harness()
const prompt = 'x'.repeat(3000)
const args = {
...FULL,
startupAgent: 'claude' as const,
startupPrompt: prompt,
startupLaunchSource: 'cli'
}
const result = await create(args)
expect(result).toBe(created)
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(1)
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(args)
expect(runtime.getStructuredAgentSessionCreateSupport).not.toHaveBeenCalled()
})
it('forwards a post-start agent and prompt to the create', async () => {
const { runtime, create } = harness()
const args = { ...FULL, startupAgent: 'aider' as const, startupPrompt: 'fix the bug' }
await create(args)
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(args)
})
it('omits a blank startup prompt from the create request', async () => {
for (const startupPrompt of ['', ' \n']) {
const { runtime, create } = harness()
await create({ ...FULL, startupAgent: 'codex', startupPrompt })
expect(runtime.createManagedWorktree).toHaveBeenCalledWith({ ...FULL, startupAgent: 'codex' })
}
})
it('forwards a linked draft through `startupDraft` with its resolved agent', async () => {
const { runtime, create } = harness({ draftAgent: 'codex' })
const { createdWithAgent: _requested, ...request } = FULL
const draft = 'https://github.com/o/r/issues/1'
await create({ ...request, startupDraft: draft })
expect(runtime.resolveStartupDraftAgent).toHaveBeenCalledWith(
{ id: 'repo-1', connectionId: null, executionHostId: null },
undefined
)
expect(runtime.createManagedWorktree).toHaveBeenCalledWith({
...request,
startupDraft: draft,
startupDraftAgent: 'codex'
})
})
it('asks for the requested draft agent and keeps it as the request named it', async () => {
const { runtime, create } = harness()
await create({ ...FULL, startupDraft: 'https://x/1' })
expect(runtime.resolveStartupDraftAgent).toHaveBeenCalledWith(expect.anything(), 'claude')
expect(runtime.createManagedWorktree).toHaveBeenCalledWith({
...FULL,
startupDraft: 'https://x/1',
startupDraftAgent: 'claude'
})
})
it('creates without the draft when it resolves no agent, so the create does not detect again', async () => {
const { runtime, create } = harness({ draftAgent: null })
await create({ ...FULL, startupDraft: 'https://x/1' })
expect(runtime.resolveStartupDraftAgent).toHaveBeenCalledTimes(1)
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(FULL)
})
it('passes a request with no agent, a blank draft, or a prebuilt command straight to the create', async () => {
for (const args of [
BASE,
{ ...BASE, startupDraft: ' ' },
{
...BASE,
startupAgent: 'claude' as const,
startup: { command: 'claude' }
}
]) {
const { runtime, create } = harness()
await create(args)
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(args)
expect(runtime.showRepo).not.toHaveBeenCalled()
}
})
it('builds no second agent when the create spawned none here', async () => {
const { runtime, created, create } = harness({
created: {
@@ -194,13 +73,4 @@ describe('createWorktreeWithStartupAgent', () => {
expect(result).toBe(created)
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(1)
})
it('surfaces a create failure unchanged', async () => {
const failure = new Error(
'Selected agent is disabled. Choose an enabled agent before creating.'
)
const { create } = harness({ createError: failure })
await expect(create({ ...BASE, startupAgent: 'claude' })).rejects.toBe(failure)
})
})
@@ -5,11 +5,7 @@ import {
searchHit,
searchResults
} from '../../shared/ai-vault/search/ai-vault-search-test-fixture'
import {
searchSessionService,
setSessionSearchService,
sessionSearchServiceStatus
} from './session-search-service-registry'
import { searchSessionService, setSessionSearchService } from './session-search-service-registry'
import {
encodeSessionSearchCursor,
decodeSessionSearchCursor,
@@ -19,24 +15,6 @@ import {
afterEach(() => setSessionSearchService(null))
test.each(['runtime', 'relay'] as const)(
'publishes the current supported catalog independently of indexed data on %s',
async (transport) => {
expect(await sessionSearchServiceStatus({}, transport)).toMatchObject({
enabled: false,
supportedAgents: [...AI_VAULT_AGENTS],
supportsQoderHistory: true,
supportsJcodeHistory: true
})
const service = fakeSearchService()
setSessionSearchService(service)
expect((await sessionSearchServiceStatus({}, transport)).supportedAgents).toEqual(
AI_VAULT_AGENTS
)
expect(service.status.mock.lastCall).toEqual([])
}
)
test.each(['runtime', 'relay'] as const)(
'keeps an empty decoder catalog authoritative over both history flags on %s',
async (transport) => {
@@ -62,30 +40,6 @@ test.each(['runtime', 'relay'] as const)(
}
)
test.each(['qoder', 'jcode'] as const)(
'projects the independent %s history capability before retrieval',
async (agent) => {
const service = fakeSearchService()
setSessionSearchService(service)
const supportField = agent === 'qoder' ? 'supportsQoderHistory' : 'supportsJcodeHistory'
await searchSessionService({ query: 'proof', [supportField]: true }, 'relay')
expect(service.search).toHaveBeenCalledExactlyOnceWith(
{
query: 'proof',
limit: 20,
filters: {
agents: AI_VAULT_AGENTS.filter((candidate) =>
agent === 'qoder'
? !['jcode', 'kiro'].includes(candidate)
: !['codebuddy', 'zcode', 'qoder', 'kiro'].includes(candidate)
)
}
},
undefined
)
}
)
test.each(['runtime', 'relay'] as const)(
'keeps an unsupported-only filter narrow through normal service checks on %s',
async (transport) => {
@@ -51,18 +51,6 @@ describe('mergeAiVaultListResults', () => {
vi.useRealTimers()
})
it('keeps the latest input scannedAt instead of restamping the merge', () => {
const merged = mergeAiVaultListResults(
[
{ ...listResult([]), scannedAt: '2026-08-02T00:00:00.000Z' },
{ ...listResult([]), scannedAt: '2026-08-02T00:00:05.000Z' }
],
undefined
)
expect(merged.scannedAt).toBe('2026-08-02T00:00:05.000Z')
})
it('ignores a future or unparsable remote stamp so the merge cannot pin the renderer guard', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-08-02T00:00:10.000Z'))
@@ -119,51 +107,6 @@ describe('mergeAiVaultListResults', () => {
).toBe('2026-08-02T00:00:04.000Z')
})
it('resolves an equal-instant tie the same way in either leg order', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-08-02T00:00:10.000Z'))
const noMillis = { ...listResult([]), scannedAt: '2026-08-02T00:00:05Z' }
const withMillis = { ...listResult([]), scannedAt: '2026-08-02T00:00:05.000Z' }
expect(mergeAiVaultListResults([noMillis, withMillis], undefined).scannedAt).toBe(
'2026-08-02T00:00:05.000Z'
)
expect(mergeAiVaultListResults([withMillis, noMillis], undefined).scannedAt).toBe(
'2026-08-02T00:00:05.000Z'
)
})
it('normalizes the merged stamp so a leg ISO variant cannot leak into it', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-08-02T00:00:10.000Z'))
expect(
mergeAiVaultListResults(
[{ ...listResult([]), scannedAt: '2026-08-01T19:00:05.000-05:00' }],
undefined
).scannedAt
).toBe('2026-08-02T00:00:05.000Z')
})
it('accepts the oldest representable stamp and rejects one millisecond beyond it', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-08-02T00:00:10.000Z'))
expect(
mergeAiVaultListResults(
[{ ...listResult([]), scannedAt: '-271821-04-20T00:00:00.000Z' }],
undefined
).scannedAt
).toBe('-271821-04-20T00:00:00.000Z')
expect(
mergeAiVaultListResults(
[{ ...listResult([]), scannedAt: '-271821-04-19T23:59:59.999Z' }],
undefined
).scannedAt
).toBe('2026-08-02T00:00:10.000Z')
})
it('does not cap an Unlimited all-host merge', () => {
const sessions = Array.from({ length: 1001 }, (_, index) => session(index))
const merged = mergeAiVaultListResults([{ ...listResult([]), sessions }], undefined, true)
@@ -73,70 +73,6 @@ async function settleAcknowledgements(): Promise<void> {
}
describe('live screencast image allocation', () => {
it('replays captured PNG and JPEG bytes without copying the decoded image again', async () => {
const fixtures = [
{
format: 'png',
path: 'docs/site/public/docs/remote-server-add-client.png',
width: 832,
height: 832
},
{
format: 'jpeg',
path: 'docs/site/public/docs/posters/file-drag.jpg',
width: 1200,
height: 682
}
] as const
const captures = fixtures.map((fixture) => {
const bytes = readFileSync(resolve(fixture.path))
return { ...fixture, bytes, data: bytes.toString('base64'), expectedDigest: digest(bytes) }
})
const nativeUint8Array = globalThis.Uint8Array
let imageCopyBytes = 0
globalThis.Uint8Array = new Proxy(nativeUint8Array, {
construct(target, argumentsList, newTarget) {
const source: unknown = argumentsList[0]
const bytes: unknown = Reflect.construct(target, argumentsList, newTarget)
if (!(bytes instanceof nativeUint8Array)) {
throw new Error('Unexpected byte constructor result')
}
if (Buffer.isBuffer(source)) {
imageCopyBytes += bytes.byteLength
}
return bytes
}
})
try {
for (const fixture of captures) {
const seen: number[] = []
const route = createLiveFrameRoute(fixture.format, (wire) => {
const frame = decodeBrowserScreencastFrame(wire)
if (!frame) {
throw new Error('Captured frame was not encoded')
}
expect(frame.format).toBe(fixture.format)
expect(frame.metadata.imageWidth).toBe(fixture.width)
expect(frame.metadata.imageHeight).toBe(fixture.height)
expect(frame.image.byteLength).toBe(fixture.bytes.byteLength)
expect(digest(frame.image)).toBe(fixture.expectedDigest)
seen.push(frame.seq)
return true
})
for (let sessionId = 1; sessionId <= 12; sessionId++) {
route.emit(fixture.data, sessionId)
}
await settleAcknowledgements()
expect(seen).toEqual(Array.from({ length: 12 }, (_, index) => index))
expect(route.acknowledgements).toEqual(Array.from({ length: 12 }, (_, index) => index + 1))
route.pacer.clearPending(true)
}
expect(imageCopyBytes).toBe(0)
} finally {
globalThis.Uint8Array = nativeUint8Array
}
})
it('owns pooled and offset images through refused sends and keeps encoded bytes independent', async () => {
vi.useFakeTimers()
const bytes = readFileSync(resolve('resources/tray/orca-menu-barTemplate.png'))
@@ -6,9 +6,6 @@ import {
updateContextMenuItem
} from './browser-extension-context-menu-model'
import {
ACTION_MENU_CONTEXTS,
arrangeExtensionContextMenuGroup,
buildContextMenuOnClickData,
contextMenuTargetFromParams,
visibleContextMenuTree
} from './browser-extension-context-menu-target'
@@ -114,14 +111,6 @@ describe('what a right-click shows', () => {
])
})
it('shows action items only for the toolbar menu', () => {
const toolbar = {
contexts: new Set<string>(ACTION_MENU_CONTEXTS),
pageUrl: 'https://site.test/'
}
expect(visibleContextMenuTree(items, toolbar).map((item) => item.key)).toEqual(['"toolbar"'])
})
it('drops a parent whose children are all filtered out', () => {
const nested = build(
{ id: 'parent', title: 'Parent', contexts: ['all'] },
@@ -129,19 +118,6 @@ describe('what a right-click shows', () => {
)
expect(visibleContextMenuTree(nested, pageClick)).toEqual([])
})
it('shows one item inline and several under a submenu named after the extension', () => {
const one = visibleContextMenuTree(build({ id: 'a', title: 'A' }), pageClick)
expect(arrangeExtensionContextMenuGroup('ext', 'Vault', one)?.items).toEqual(one)
const two = visibleContextMenuTree(
build({ id: 'a', title: 'A' }, { id: 'b', title: 'B' }),
pageClick
)
expect(arrangeExtensionContextMenuGroup('ext', 'Vault', two)?.items).toEqual([
{ key: '', title: 'Vault', type: 'normal', checked: false, enabled: true, children: two }
])
expect(arrangeExtensionContextMenuGroup('ext', 'Vault', [])).toBeNull()
})
})
describe('clicking an item', () => {
@@ -160,32 +136,4 @@ describe('clicking an item', () => {
['r2', true]
])
})
it("builds Chrome's OnClickData", () => {
const [item] = build({ id: 'fill', title: 'Fill', contexts: ['editable'], parentId: undefined })
const target = contextMenuTargetFromParams(
{
isEditable: true,
linkURL: 'https://site.test/next',
srcURL: 'https://cdn.test/a.png',
frameURL: 'https://frame.test/',
selectionText: 'me@site.test'
},
'https://site.test/login',
true
)
expect(
buildContextMenuOnClickData({ item, target, frameId: 12, editable: true, mediaType: 'image' })
).toEqual({
menuItemId: 'fill',
mediaType: 'image',
linkUrl: 'https://site.test/next',
srcUrl: 'https://cdn.test/a.png',
pageUrl: 'https://site.test/login',
frameUrl: 'https://frame.test/',
frameId: 12,
selectionText: 'me@site.test',
editable: true
})
})
})
@@ -1,199 +0,0 @@
import { spawnSync } from 'node:child_process'
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterAll, describe, expect, it } from 'vitest'
import { resolveElectronProbeLaunch } from '../../electron-fixtures/electron-probe-display-launch'
const electronBinary = createRequire(import.meta.url)('electron') as string
const fixtureRoots: string[] = []
type ProbeResult = {
packets: number
policy: string
resolvedProxy: string
}
afterAll(() => {
const failures: unknown[] = []
for (const root of fixtureRoots) {
try {
rmSync(root, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 })
} catch (error) {
failures.push(error)
}
}
if (failures.length > 0) {
throw new AggregateError(failures, 'Failed to clean up WebRTC egress fixtures')
}
})
function probeMain(resultPath: string, protectedGuest: boolean): string {
return `
const { app, BrowserWindow, session } = require('electron')
// This data-channel probe does not need Linux GPU initialization.
if (process.platform === 'linux') app.disableHardwareAcceleration()
const dgram = require('node:dgram')
const net = require('node:net')
const os = require('node:os')
const { writeFileSync } = require('node:fs')
let phase = 'app-ready'
function enterPhase(next) {
phase = next
console.error('[webrtc-egress] ' + phase)
}
function bind(socket, host) {
return new Promise((resolve, reject) => {
socket.once('error', reject)
socket.bind(0, host, () => {
socket.off('error', reject)
resolve(socket.address())
})
})
}
function listen(server, host) {
return new Promise((resolve, reject) => {
server.once('error', reject)
server.listen(0, host, () => {
server.off('error', reject)
resolve(server.address())
})
})
}
function viewerAddress() {
for (const entries of Object.values(os.networkInterfaces())) {
for (const entry of entries || []) {
if (entry.family === 'IPv4' && !entry.internal) return entry.address
}
}
return '127.0.0.1'
}
async function probe() {
const udp = dgram.createSocket('udp4')
const tcp = net.createServer((socket) => socket.destroy())
const packets = []
udp.on('message', (message) => packets.push(message.length))
enterPhase('bind-listeners')
const [udpAddress, tcpAddress] = await Promise.all([
bind(udp, '0.0.0.0'),
listen(tcp, '127.0.0.1')
])
const partition = 'persist:webrtc-egress-${protectedGuest}-' + Date.now()
const routeSession = session.fromPartition(partition, { cache: false })
enterPhase('configure-proxy')
await routeSession.setProxy({
mode: 'fixed_servers',
proxyRules: 'socks5://127.0.0.1:' + tcpAddress.port,
proxyBypassRules: '<-loopback>'
})
enterPhase('close-connections')
await routeSession.closeAllConnections()
enterPhase('resolve-proxy')
const resolvedProxy = await routeSession.resolveProxy('https://example.invalid/')
enterPhase('create-window')
const window = new BrowserWindow({
show: false,
webPreferences: { partition, sandbox: true, nodeIntegration: false, contextIsolation: true }
})
if (${protectedGuest}) {
window.webContents.setWebRTCIPHandlingPolicy('disable_non_proxied_udp')
}
const policy = window.webContents.getWebRTCIPHandlingPolicy()
enterPhase('load-page')
await window.loadURL('data:text/html,<title>WebRTC egress probe</title>')
const target = viewerAddress()
const script = \`
(async () => {
const peer = new RTCPeerConnection({
iceServers: [{ urls: 'stun:\${target}:\${udpAddress.port}' }],
iceCandidatePoolSize: 0
})
globalThis.__webrtcEgressPeer = peer
peer.createDataChannel('probe')
const offer = await peer.createOffer()
await peer.setLocalDescription(offer)
})()
\`
enterPhase('renderer-webrtc')
await window.webContents.executeJavaScript(script)
// Hidden renderer timers may be throttled; the packet observation clock belongs to the host.
enterPhase('observe-packets')
await new Promise((resolve) => setTimeout(resolve, 3000))
enterPhase('close-peer')
await window.webContents.executeJavaScript('globalThis.__webrtcEgressPeer.close()')
enterPhase('drain-packets')
await new Promise((resolve) => setTimeout(resolve, 500))
enterPhase('cleanup')
window.destroy()
udp.close()
tcp.close()
return { packets: packets.length, policy, resolvedProxy }
}
async function run() {
const timeout = setTimeout(() => {
writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify({
error: 'WebRTC egress probe timed out', phase, protectedGuest: ${protectedGuest}
}))
app.exit(2)
}, 20000)
await app.whenReady()
const result = await probe()
enterPhase('write-result')
writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify(result))
clearTimeout(timeout)
app.quit()
}
run().catch((error) => {
writeFileSync(${JSON.stringify(resultPath)}, JSON.stringify({ error: String(error?.stack || error), phase, protectedGuest: ${protectedGuest} }))
app.exit(1)
})
`
}
function runProbe(protectedGuest: boolean): ProbeResult {
const root = mkdtempSync(join(tmpdir(), 'orca-browser-webrtc-egress-'))
fixtureRoots.push(root)
const mainPath = join(root, 'main.cjs')
const resultPath = join(root, 'result.json')
writeFileSync(mainPath, probeMain(resultPath, protectedGuest))
const { ELECTRON_RUN_AS_NODE: _electronRunAsNode, ...env } = process.env
const electronArgs = [mainPath, `--user-data-dir=${join(root, 'profile')}`]
const { executable, args } = resolveElectronProbeLaunch({
electronBinary,
electronArgs,
platform: process.platform,
display: env.DISPLAY
})
const run = spawnSync(executable, args, { encoding: 'utf8', env, timeout: 30_000 })
const rawResult = existsSync(resultPath) ? readFileSync(resultPath, 'utf8') : 'no result'
expect(run.error).toBeUndefined()
expect(run.status, `${rawResult}\n${run.stdout}\n${run.stderr}`).toBe(0)
if (rawResult === 'no result') {
throw new Error(`${rawResult}\n${run.stdout}\n${run.stderr}`)
}
const parsed = JSON.parse(rawResult) as ProbeResult | { error: string }
if ('error' in parsed) {
throw new Error(parsed.error)
}
return parsed
}
describe('browser route WebRTC egress under Electron', () => {
it('blocks direct UDP after applying the exact guest policy', () => {
const baseline = runProbe(false)
const protectedGuest = runProbe(true)
expect(protectedGuest.resolvedProxy).toMatch(/^SOCKS5 127\.0\.0\.1:\d+$/)
expect(baseline.packets).toBeGreaterThan(0)
expect(protectedGuest.policy).toBe('disable_non_proxied_udp')
expect(protectedGuest.packets).toBe(0)
}, 45_000)
})
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import type { ClaudeUsageTokenTotals } from './types'
import { MODEL_PRICING, estimateCostUsd, normalizeModelForPricing } from './claude-model-pricing'
import { estimateCostUsd, normalizeModelForPricing } from './claude-model-pricing'
const MTOK = 1_000_000
@@ -19,10 +19,6 @@ function estimate(model: string, ...counts: number[]): number | null {
}
describe('model pricing name matching', () => {
it.each(Object.keys(MODEL_PRICING))('resolves the table key %s to its own row', (key) => {
expect(normalizeModelForPricing(key)).toBe(key)
})
it.each([
[' ANTHROPIC/claude-opus-4.1-thinking ', 'claude-opus-4-1'],
['claude-opus-4-20250514', 'claude-opus-4'],
@@ -65,34 +61,7 @@ describe('model pricing name matching', () => {
})
})
describe('point-release rates that differ from their major', () => {
it('bills Opus 5.5 below Opus 5 across every bucket', () => {
expect(estimate('claude-opus-5-5', 1_000_000, 1_000_000, 1_000_000, 0)).toBeCloseTo(24.2)
expect(estimate('claude-opus-5-5', 0, 0, 0, 1_000_000, 400_000)).toBeCloseTo(6.2)
})
it('bills Sonnet 5.5 cache reads at half of Sonnet 5', () => {
expect(estimate('claude-sonnet-5-5', 0, 0, 1_000_000, 0)).toBeCloseTo(0.1)
expect(estimate('claude-sonnet-5-5', 1_000_000, 1_000_000, 0, 1_000_000, 0)).toBeCloseTo(14.5)
expect(estimate('claude-sonnet-5-5', 0, 0, 0, 1_000_000, 1_000_000)).toBeCloseTo(4)
})
it('bills Fable 5.1 cache reads at a quarter of Fable 5', () => {
expect(estimate('claude-fable-5-1', 0, 0, 1_000_000, 0)).toBeCloseTo(0.25)
expect(estimate('claude-fable-5', 0, 0, 1_000_000, 0)).toBeCloseTo(1)
expect(estimate('claude-fable-5-1m', 0, 0, 1_000_000, 0)).toBeCloseTo(1)
})
})
describe('estimateCostUsd cache-write TTL rates', () => {
it('bills 5-minute cache writes at 1.25x base input', () => {
expect(estimate('claude-opus-5', 0, 0, 0, 1_000_000, 0)).toBeCloseTo(6.25)
})
it('bills 1-hour cache writes at 2x base input', () => {
expect(estimate('claude-opus-5', 0, 0, 0, 1_000_000, 1_000_000)).toBeCloseTo(10)
})
it('splits a mixed write bucket without double-billing the 1-hour share', () => {
expect(estimate('claude-opus-5', 0, 0, 0, 1_000_000, 400_000)).toBeCloseTo(7.75)
})
@@ -101,10 +70,6 @@ describe('estimateCostUsd cache-write TTL rates', () => {
expect(estimate('claude-opus-5', 0, 0, 0, 1_000, 5_000)).toBeCloseTo(0.01)
})
it('keeps Sonnet 4.6 one-hour writes flat across its full 1M window', () => {
expect(estimate('claude-sonnet-4-6', 0, 0, 0, 400_000, 400_000)).toBeCloseTo(2.4)
})
it('still returns null for unknown models', () => {
expect(estimate('gpt-5', 0, 0, 0, 1_000_000, 1_000_000)).toBeNull()
})
@@ -125,15 +90,6 @@ describe('estimateCostUsd long-context requests', () => {
).toBeCloseTo(0.1 + 0.5)
})
it('bills Sonnet 4.5 long-context requests at its premium rates', () => {
const long = tokens(MTOK, MTOK, MTOK, MTOK, MTOK)
expect(estimateCostUsd('claude-sonnet-4-5', { ...long, longContext: long })).toBeCloseTo(
6 + 22.5 + 0.6 + 12
)
// A large aggregate does not imply any individual request crossed the threshold.
expect(estimate('claude-sonnet-4-5', 5 * MTOK)).toBeCloseTo(15)
})
it('ignores a long-context subset for a model with flat rates', () => {
const long = tokens(MTOK)
expect(estimateCostUsd('claude-sonnet-4-6', { ...long, longContext: long })).toBeCloseTo(3)
@@ -147,27 +103,3 @@ describe('estimateCostUsd long-context requests', () => {
})
// Rate source: platform.claude.com/docs/en/about-claude/pricing.
describe('estimateCostUsd Claude Sonnet 5 rates', () => {
it('bills base input at $2/MTok', () => {
expect(estimate('claude-sonnet-5', 1_000_000, 0, 0, 0, 0)).toBeCloseTo(2)
})
it('bills output at $10/MTok', () => {
expect(estimate('claude-sonnet-5', 0, 1_000_000, 0, 0, 0)).toBeCloseTo(10)
})
it('bills cache hits at $0.20/MTok', () => {
expect(estimate('claude-sonnet-5', 0, 0, 1_000_000, 0, 0)).toBeCloseTo(0.2)
})
it('bills 5-minute cache writes at $2.50/MTok and 1-hour writes at $4/MTok', () => {
expect(estimate('claude-sonnet-5', 0, 0, 0, 1_000_000, 0)).toBeCloseTo(2.5)
expect(estimate('claude-sonnet-5', 0, 0, 0, 1_000_000, 1_000_000)).toBeCloseTo(4)
})
it('keeps the full 1M window at flat rates with no long-context tier', () => {
expect(estimate('claude-sonnet-5', 1_000_000, 0, 0, 0, 0)).toBeCloseTo(
estimate('claude-sonnet-5', 500_000, 0, 0, 0, 0)! * 2
)
})
})
@@ -35,70 +35,6 @@ function sdkOptions(overrides: Partial<SdkSpawnOptions> = {}): SdkSpawnOptions {
}
describe('claude agent SDK process spawn', () => {
it('routes the SDK spawn through Orca and retains the pid the lease adjudicates on', () => {
const process = fakeSpawn()
const spawn = createClaudeCodeProcessSpawn(process.spawnImpl, 'win32')
expect(spawn.pid).toBeUndefined()
expect(spawn.child).toBeNull()
const child = spawn.spawn(sdkOptions())
expect(child).toBe(process.child)
expect(spawn.child).toBe(process.child)
expect(spawn.pid).toBe(4321)
expect(spawn.supervised).toBe(false)
expect(process.specs[0]).toEqual({
program: '/usr/local/bin/claude',
args: ['--output-format', 'stream-json'],
cwd: '/work/repo',
env: { PATH: '/usr/bin', CLAUDE_CONFIG_DIR: '/accounts/one' },
detached: false,
stdio: ['pipe', 'pipe', 'pipe']
})
})
it.each(['darwin', 'linux'] as const)(
'starts Claude under the provider supervisor on %s, which is then the pid the lease records',
(platform) => {
const process = fakeSpawn()
const spawn = createClaudeCodeProcessSpawn(process.spawnImpl, platform)
spawn.spawn(sdkOptions())
const [spec] = process.specs
if (!spec) {
throw new Error('the spawner never built a spec')
}
expect(spawn.supervised).toBe(true)
expect(spawn.pid).toBe(4321)
expect(spec.program).toBe(globalThis.process.execPath)
expect(spec.args?.[0]).toBe('-e')
expect(spec.args?.slice(2)).toEqual([
'--',
'/usr/local/bin/claude',
'--output-format',
'stream-json'
])
expect(spec.detached).toBe(true)
expect(spec.cwd).toBe('/work/repo')
const supervisorSpec = JSON.parse(
Buffer.from(String(spec.env?.ORCA_PROVIDER_SUPERVISOR_SPEC), 'base64').toString()
)
// Orca exit must trigger Claude's close ladder, including descendant cleanup.
expect(vi.mocked(createProviderSpawnSpec)).toHaveBeenLastCalledWith(
expect.anything(),
expect.anything(),
platform,
{ closeRequest: 'stdin-end-and-sigterm' }
)
expect(supervisorSpec).toMatchObject({
closeRequest: 'stdin-end-and-sigterm',
cwd: '/work/repo',
ownerPid: globalThis.process.pid
})
expect(spec.env).toMatchObject({ PATH: '/usr/bin', CLAUDE_CONFIG_DIR: '/accounts/one' })
}
)
it.each([
{ platform: 'darwin', specSupervised: false },
{ platform: 'win32', specSupervised: true }
@@ -141,14 +77,6 @@ describe('claude agent SDK process spawn', () => {
}
)
it('keeps the child out of the SDK abort path so exit proof stays Orca-owned', () => {
const process = fakeSpawn()
const controller = new AbortController()
createClaudeCodeProcessSpawn(process.spawnImpl).spawn(sdkOptions({ signal: controller.signal }))
expect(process.specs[0]).not.toHaveProperty('signal')
})
it('drains stderr into a bounded tail so an exit error still carries it', async () => {
const process = fakeSpawn()
const spawn = createClaudeCodeProcessSpawn(process.spawnImpl)
@@ -4,110 +4,13 @@ import { readManagedHookDetectionResult } from '../agent-hooks/managed-hook-dete
import { getClaudeManagedHookPlan } from './claude-managed-hook-events'
vi.mock('@orca/process-host', () => ({ runProcess: vi.fn() }))
import { compareAppVersions } from '../../shared/app-version'
import fixture from './__fixtures__/claude-hook-event-enums.json'
import {
CLAUDE_HOOK_EVENT_FIRST_VERSIONS,
CLAUDE_STATUS_LINE_FIRST_VERSION,
claudeKnowsHookEvent,
claudeKnowsStatusLine,
parseClaudeCliVersion,
parseClaudeCliVersionOutput,
probeClaudeCliVersion,
UNRESOLVED_CLAUDE_VERSION,
type ClaudeHookEventName
probeClaudeCliVersion
} from './claude-hook-event-versions'
const enums: Record<string, string[]> = fixture.enums
const previousPublished: Record<string, string> = fixture.previousPublishedVersion
const topLevelSettings: Record<string, { strict: boolean; keys: string[] }> =
fixture.topLevelSettings
const versions = Object.keys(enums).sort(compareAppVersions)
const isTableEvent = (name: string): name is ClaudeHookEventName =>
name in CLAUDE_HOOK_EVENT_FIRST_VERSIONS
const tableEntries = Object.keys(CLAUDE_HOOK_EVENT_FIRST_VERSIONS)
.filter(isTableEvent)
.map((event): [ClaudeHookEventName, string] => [event, CLAUDE_HOOK_EVENT_FIRST_VERSIONS[event]])
describe('Claude hook event version table', () => {
it.each(tableEntries)('pins %s to the first release whose enum knows it', (event, first) => {
expect(versions.find((version) => enums[version].includes(event))).toBe(first)
for (const version of versions) {
expect(enums[version].includes(event), `${event} in ${version}`).toBe(
compareAppVersions(version, first) >= 0
)
}
const before = previousPublished[first]
expect(enums[before], `enum for ${before}`).toBeDefined()
expect(enums[before]).not.toContain(event)
})
})
describe('Claude statusLine version floor', () => {
it('pins statusLine to the first release whose settings schema knows it', () => {
for (const [version, schema] of Object.entries(topLevelSettings)) {
expect(schema.keys.includes('statusLine'), version).toBe(
compareAppVersions(version, CLAUDE_STATUS_LINE_FIRST_VERSION) >= 0
)
}
const before = previousPublished[CLAUDE_STATUS_LINE_FIRST_VERSION]
expect(topLevelSettings[before].strict).toBe(true)
expect(topLevelSettings[before].keys).not.toContain('statusLine')
})
it.each([
['1.0.63', false],
['1.0.64 (Claude Code)', true],
[undefined, true]
] as const)('%s knows statusLine: %s', (version, expected) => {
expect(claudeKnowsStatusLine(version)).toBe(expected)
})
})
describe('claudeKnowsHookEvent', () => {
it.each([
['2.1.77', 'StopFailure', false],
['2.1.78 (Claude Code)', 'StopFailure', true],
['2.1.75', 'PostCompact', false],
['2.1.76', 'PostCompact', true],
['2.1.32', 'TeammateIdle', false],
['2.0.55', 'PostToolUseFailure', false],
['2.0.44', 'PermissionRequest', false],
['2.0.42', 'SubagentStart', false],
['1.0.84', 'SessionEnd', false],
['1.0.61', 'SessionStart', false],
['1.0.62', 'SessionStart', true],
['1.0.52', 'UserPromptSubmit', false],
['1.0.40', 'SubagentStop', false],
['1.0.30', 'Stop', false],
['1.0.22', 'PreToolUse', false]
] as const)('%s knows %s: %s', (version, event, expected) => {
expect(claudeKnowsHookEvent(version, event)).toBe(expected)
})
it.each([undefined, null, 'unknown'])(
'grants an unresolved version (%s) only the events its assumed release knows',
(version) => {
const known = tableEntries
.filter(([event]) => claudeKnowsHookEvent(version, event))
.map(([event]) => event)
expect(known.sort()).toEqual(
[
'PostToolUse',
'PreToolUse',
'SessionStart',
'Stop',
'SubagentStop',
'UserPromptSubmit'
].sort()
)
for (const event of known) {
expect(enums[UNRESOLVED_CLAUDE_VERSION]).toContain(event)
}
}
)
})
describe('parseClaudeCliVersion', () => {
it('extracts Claude Code version output', () => {
expect(parseClaudeCliVersion('2.1.261 (Claude Code)')).toBe('2.1.261')
@@ -98,18 +98,6 @@ describe('claude streamed text checkpoints', () => {
expect(rows).toEqual([{ uuid: 'block-1', text: 'partial answer' }])
})
it('keeps a block whose final frame arrived out of the settlement sweep', () => {
const { store } = checkpoints()
store.append(identityOf('block-1'), 'one')
store.append(identityOf('block-2'), 'two')
store.forget('claude:claude-session:block-1')
expect(store.pending).toBe(1)
store.settle()
expect(store.pending).toBe(0)
})
it('flushes text the widening checkpoint interval has not written yet', () => {
const { store, rows } = checkpoints()
@@ -121,18 +109,6 @@ describe('claude streamed text checkpoints', () => {
expect(rows).toHaveLength(1)
})
it("stamps a block streamed inside a child with that child's linkage", () => {
const producer = scriptedProducer()
producer.resolve(CHILD_LINKAGE)
const { store, rows, stamps, runWindow } = checkpoints(producer.source)
store.append(identityOf('block-1'), 'hello', 'toolu_1')
runWindow()
expect(rows).toEqual([{ uuid: 'block-1', text: 'hello' }])
expect(stamps).toEqual([CHILD_LINKAGE])
})
it("writes no linkage keys for a block the session's own agent streamed", () => {
const { store, stamps, runWindow } = checkpoints()
@@ -162,22 +138,6 @@ describe('claude streamed text checkpoints', () => {
expect(stamps.at(-1)).toEqual(CHILD_LINKAGE)
})
it('writes a held block under the raw reference when no announcement comes', () => {
const producer = scriptedProducer()
const { store, rows, stamps, runWindow } = checkpoints(producer.source)
store.append(identityOf('block-1'), 'never announced', 'toolu_1')
runWindow()
expect(rows).toEqual([{ uuid: 'block-1', text: 'never announced' }])
expect(stamps).toEqual([
{ agentId: 'toolu_1', providerParentRef: 'toolu_1', producerKind: 'agent' }
])
store.reattribute()
expect(rows).toHaveLength(1)
})
it('re-resolves a block’s producer on every checkpoint', () => {
const producer = scriptedProducer()
producer.resolve(CHILD_LINKAGE)
@@ -1,69 +1,19 @@
import { runProcess } from '@orca/process-host'
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { createAgentHookMemorySftp } from '../agent-hooks/agent-hook-memory-sftp.test-fixture'
import { wrapRuntimeHomePosixHookCommand } from '../agent-hooks/runtime-home-hook-command'
import { ClaudeHookService } from './hook-service'
import {
CLAUDE_HOOK_SETTINGS,
getManagedEventHook,
getManagedLifecycleHook,
getRemoteManagedCommand,
getRemoteManagedEventCommand
} from './hook-settings'
import { getManagedEventHook } from './hook-settings'
vi.mock('electron', () => ({ app: { getPath: () => '/tmp/userData' } }))
const SCRIPT_PATH = '/home/test/.orca/agent-hooks/claude-hook.sh'
const CURRENT_CLAUDE = { claudeVersion: '2.1.261 (Claude Code)' }
// Claude echoes the full command after /compact, so keep it short.
const MAX_ECHOED_COMMAND_LENGTH = 400
afterEach(() => vi.restoreAllMocks())
describe('PostCompact managed hook command', () => {
it.each(['darwin', 'linux'] as const)('is the short POSIX form on %s', (platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
const { command } = getManagedEventHook('PostCompact', SCRIPT_PATH)
expect(command.length).toBeLessThan(MAX_ECHOED_COMMAND_LENGTH)
expect(command).toContain('if [ -n "${HOME-}" ] && ')
expect(command).toContain('/bin/sh "${HOME-}/.orca/agent-hooks/claude-hook.sh"')
expect(command).toContain('ORCA_HOOK_AGENT_PID="${PPID:-}"')
expect(command).not.toMatch(/powershell|OSTYPE|\/home\/test/i)
})
it('leaves every other event on the full wrapper', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('linux')
expect(getManagedEventHook('Stop', SCRIPT_PATH)).toEqual(
getManagedLifecycleHook(SCRIPT_PATH, CLAUDE_HOOK_SETTINGS)
)
})
it('keeps the Windows launcher on win32, whose bare script path is already short', () => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
const windowsPath = 'C:\\Users\\alice\\.orca\\agent-hooks\\claude-hook.cmd'
expect(getManagedEventHook('PostCompact', windowsPath)).toEqual(
getManagedLifecycleHook(windowsPath, CLAUDE_HOOK_SETTINGS)
)
})
it.each(['darwin', 'linux', 'win32'] as const)(
'uses the short POSIX form only for remote PostCompact from %s',
(platform) => {
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
expect(getRemoteManagedEventCommand('PostCompact', SCRIPT_PATH)).toBe(
wrapRuntimeHomePosixHookCommand('claude-hook')
)
expect(getRemoteManagedEventCommand('Stop', SCRIPT_PATH)).toBe(
getRemoteManagedCommand(SCRIPT_PATH)
)
}
)
})
describe.skipIf(process.platform === 'win32')('PostCompact command execution', () => {
let home: string
@@ -109,67 +59,4 @@ describe.skipIf(process.platform === 'win32')('PostCompact command execution', (
expect(result.stdout).toBe('')
expect(result.stderr).toBe('')
})
it.each([{}, { HOME: '' }])('skips script lookup without a runtime HOME: %j', async (env) => {
const lookupProbe = [
'probe_test() {',
' case "$1" in -f|-r|-x) printf "unexpected script lookup: %s\\n" "$2" >&2; return 1 ;;',
' *) command [ "$@" ;; esac',
'}',
'alias [=probe_test',
getManagedEventHook('PostCompact', SCRIPT_PATH).command
].join('\n')
const result = await run(lookupProbe, env)
expect(result.code).toBe(0)
expect(result.stdout).toBe('')
expect(result.stderr).toBe('')
})
})
describe.skipIf(process.platform === 'win32')('ClaudeHookService PostCompact install', () => {
it('writes the short form, reports installed, and replaces an old full-wrapper entry', () => {
const home = mkdtempSync(join(tmpdir(), 'orca-post-compact-install-'))
vi.stubEnv('HOME', home)
try {
const settingsPath = join(home, '.claude', 'settings.json')
mkdirSync(join(home, '.claude'), { recursive: true })
const fullWrapper = getManagedLifecycleHook(SCRIPT_PATH).command
writeFileSync(
settingsPath,
JSON.stringify({
hooks: {
PostCompact: [{ hooks: [{ type: 'command', command: fullWrapper }] }]
}
})
)
const status = new ClaudeHookService().install(CURRENT_CLAUDE)
expect(status.state).toBe('installed')
const written = JSON.parse(readFileSync(settingsPath, 'utf-8'))
expect(written.hooks.PostCompact).toEqual([
{ hooks: [getManagedEventHook('PostCompact', SCRIPT_PATH)] }
])
expect(written.hooks.Stop[0].hooks[0].command).toBe(fullWrapper)
} finally {
vi.unstubAllEnvs()
rmSync(home, { recursive: true, force: true })
}
})
})
describe('ClaudeHookService remote PostCompact install', () => {
it('writes the short form for PostCompact and the full wrapper elsewhere', async () => {
const { sftp, fs } = createAgentHookMemorySftp()
await new ClaudeHookService().installRemote(sftp, '/home/dev', CURRENT_CLAUDE)
const written = JSON.parse(fs.files.get('/home/dev/.claude/settings.json') ?? '{}')
const remoteScript = '/home/dev/.orca/agent-hooks/claude-hook.sh'
expect(written.hooks.PostCompact[0].hooks[0].command).toBe(
getRemoteManagedEventCommand('PostCompact', remoteScript)
)
expect(written.hooks.Stop[0].hooks[0].command).toBe(getRemoteManagedCommand(remoteScript))
})
})
@@ -134,28 +134,6 @@ describe('a streamed reasoning row the provider never finishes', () => {
expect(translator.pendingStreamedBlocks).toBe(0)
})
it('is not rewritten by the token tally Claude sends after every thinking delta', () => {
const { translator, message, thinkingDelta, reasoningWrites } = setup()
for (let index = 0; index < 20; index += 1) {
thinkingDelta(`delta-${index}`, 'more words ', 1_000 + index)
translator.handle(
message(
{
type: 'system',
subtype: 'thinking_tokens',
estimated_tokens: index,
estimated_tokens_delta: 1,
uuid: `tokens-${index}`
},
1_000 + index
)
)
}
expect(reasoningWrites()).toEqual([])
vi.advanceTimersByTime(100)
expect(reasoningWrites()).toHaveLength(1)
})
// Timings of the first block in a captured 2.1.280 session with summarized display.
it('spans from the block start to its final frame, though text arrives seconds later', () => {
const { translator, message, thinkingDelta, reasoningWrites } = setup()
@@ -53,13 +53,6 @@ function reasoning(text: string, lifecycle: Record<string, unknown>): AgentJourn
afterEach(() => vi.useRealTimers())
describe('structured Claude reasoning', () => {
it.each(['', ' ', '\n\t'])('omits blank final thinking %j', (thinking) => {
const { rows, final, translator } = setup()
final('final-only', [{ type: 'thinking', thinking }])
expect([...rows.values()]).toEqual([])
translator.dispose()
})
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)
@@ -201,17 +194,4 @@ describe('structured Claude reasoning', () => {
expect(scopes).toEqual([{ kind: 'turn', turnItemId: expect.stringContaining('turn') }])
translator.dispose()
})
it('omits whitespace-only thinking deltas', () => {
vi.useFakeTimers()
const { rows, stream, translator } = setup()
stream('delta', {
type: 'content_block_delta',
index: 0,
delta: { type: 'thinking_delta', thinking: ' \n ' }
})
translator.flush()
expect(rows.size).toBe(0)
translator.dispose()
})
})
@@ -3,14 +3,9 @@ import { describe, expect, it, vi } from 'vitest'
import type { AgentSessionJournalIdentity } from '../../../../shared/agent-session/journal/agent-session-journal-identity'
import { agentSessionRecordFixture } from '../../../native-chat/agent-session-record-test-fixture'
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
import type { NativeChatVisualsLaunch } from '../../../native-chat/native-chat-visuals-delivery'
import {
NATIVE_CHAT_SCRATCH_DIR_ENV,
type NativeChatVisualsLaunch
} from '../../../native-chat/native-chat-visuals-delivery'
import {
CLAUDE_CLI_FLAG_PROBE_KILL_AFTER_MS,
CLAUDE_PLUGIN_DIR_FLAG,
CLAUDE_THINKING_DISPLAY_FLAG,
type ClaudeCliFlag,
type ClaudeCliFlagFact
} from '../../claude-cli-flag-support'
@@ -59,80 +54,6 @@ function launch(options: {
}
describe('a Claude chat launch with inline visuals', () => {
it('loads the skill plugin, grants the chat folder beside the user directories, and names it', async () => {
const prepareVisuals = vi.fn(async () => VISUALS)
const resolved = await launch({
prepareVisuals,
supports: async (flag) => flag === CLAUDE_PLUGIN_DIR_FLAG,
launchArgs: ['--add-dir', '/shared/notes']
})
expect(prepareVisuals).toHaveBeenCalledWith(SESSION_ID)
expect(resolved.options.plugins).toEqual([{ type: 'local', path: VISUALS.skill.pluginDir }])
expect(resolved.options.additionalDirectories).toEqual(['/shared/notes', VISUALS.folder])
expect(resolved.env?.[NATIVE_CHAT_SCRATCH_DIR_ENV]).toBe(VISUALS.folder)
})
it.each([
{
name: 'prepared visuals',
visuals: VISUALS,
directories: ['/shared/notes', '/state/agent-session-attachments', VISUALS.folder]
},
{
name: 'unavailable visuals',
visuals: null,
directories: ['/shared/notes', '/state/agent-session-attachments']
}
])('keeps user folders and attachment access with $name', async ({ visuals, directories }) => {
const resolved = await launch({
prepareVisuals: async () => visuals,
supports: async (flag) => flag === CLAUDE_PLUGIN_DIR_FLAG,
launchArgs: ['--add-dir', '/shared/notes'],
attachmentDirectory: '/state/agent-session-attachments'
})
expect(resolved.options.additionalDirectories).toEqual(directories)
})
it('still grants and names the folder when the CLI cannot load a plugin by path', async () => {
const resolved = await launch({
prepareVisuals: async () => VISUALS,
supports: async () => false
})
expect(resolved.options).not.toHaveProperty('plugins')
expect(resolved.options.additionalDirectories).toEqual([VISUALS.folder])
expect(resolved.env?.[NATIVE_CHAT_SCRATCH_DIR_ENV]).toBe(VISUALS.folder)
})
it('without a prepared folder, grants nothing and drops a folder inherited from another chat', async () => {
const resolved = await launch({
prepareVisuals: async () => null,
supports: async () => true,
env: { [NATIVE_CHAT_SCRATCH_DIR_ENV]: '/state/chat-scratch/other-chat' }
})
expect(resolved.options).not.toHaveProperty('plugins')
expect(resolved.options).not.toHaveProperty('additionalDirectories')
expect(resolved.env).not.toHaveProperty(NATIVE_CHAT_SCRATCH_DIR_ENV)
})
it('never asks about the plugin flag on a host that delivers no visuals', async () => {
const supports = vi.fn(async (_flag: ClaudeCliFlag) => true)
const resolved = await launch({ supports })
expect(supports.mock.calls.map(([flag]) => flag)).not.toContain(CLAUDE_PLUGIN_DIR_FLAG)
expect(resolved.options).not.toHaveProperty('plugins')
})
it('waits for the version up to the probe kill time for the plugin, never the short budget', async () => {
const supports = vi.fn(
async (_flag: ClaudeCliFlag, _launch: unknown, _budgetMs?: number) => true
)
await launch({ prepareVisuals: async () => VISUALS, supports })
expect(supports).toHaveBeenCalledWith(
CLAUDE_PLUGIN_DIR_FLAG,
expect.objectContaining({ command: '/usr/local/bin/claude' }),
CLAUDE_CLI_FLAG_PROBE_KILL_AFTER_MS
)
})
it('asks for readable thinking again once the plugin check has learned the version', async () => {
let versionKnown = false
const supports = vi.fn(async (flag: ClaudeCliFlag, _launch: unknown, budgetMs?: number) => {
@@ -147,14 +68,4 @@ describe('a Claude chat launch with inline visuals', () => {
expect(resolved.options.plugins).toHaveLength(1)
expect(resolved.options.extraArgs).toMatchObject({ 'thinking-display': 'summarized' })
})
it('never re-asks for thinking on a host without visuals', async () => {
const supports = vi.fn(
async (_flag: ClaudeCliFlag, _launch: unknown, _budgetMs?: number) => false
)
await launch({ supports })
expect(
supports.mock.calls.filter(([flag]) => flag === CLAUDE_THINKING_DISPLAY_FLAG)
).toHaveLength(1)
})
})
@@ -8,10 +8,6 @@ import { readHooksJson, writeHooksJson } from '../agent-hooks/installer-utils'
import { ClaudeHookService } from './hook-service'
import { getWindowsManagedLifecycleHook } from './hook-settings'
import { getWindowsClaudeHookEntry, getWindowsClaudeHookPayloadPath } from './windows-hook-files'
import { codebuddyHookService } from '../codebuddy/hook-service'
import { qoderHookService, qoderCnHookService } from '../qoder/hook-service'
import { qwenCodeHookService } from '../qwen-code/hook-service'
import { openClaudeHookService } from '../openclaude/hook-service'
const { home } = vi.hoisted(() => ({ home: { path: '' } }))
vi.mock('node:os', async (original) => ({
@@ -41,16 +37,6 @@ afterEach(() => {
})
describe('Windows Claude hook files', () => {
it('installs a delegating entry and a payload that answers before bounded delivery', () => {
expect(service.install().state).toBe('installed')
expect(readFileSync(entry, 'utf8')).toBe(getWindowsClaudeHookEntry())
const body = readFileSync(payload, 'utf8')
expect(body.indexOf('echo {}')).toBeLessThan(body.indexOf('curl.exe'))
expect(body).toContain('--connect-timeout 0.5 --max-time 1.5')
expect(body).toContain('--data-urlencode "payload@-" >nul 2>&1\r\nexit /b 0')
expect(body).toContain('DEVIN_PROJECT_DIR')
})
it('repairs a missing payload from the surviving entry without touching settings', async () => {
service.install()
rmSync(payload)
@@ -140,24 +126,4 @@ describe('Windows Claude hook files', () => {
expect(config).toContain('echo user-hook')
expect(service.getStatus().state).toBe('installed')
})
it('keeps compatible agents on their existing single-file scripts', async () => {
for (const [name, compatible] of [
['qoder', qoderHookService],
['qoder-cn', qoderCnHookService],
['qwen-code', qwenCodeHookService],
['codebuddy', codebuddyHookService],
['openclaude', openClaudeHookService]
] as const) {
expect(compatible.install().state).toBe('installed')
await compatible.refreshManagedScripts()
const path = join(home.path, '.orca', 'agent-hooks', `${name}-hook.cmd`)
const source = name === 'openclaude' ? 'claude' : name
expect(readFileSync(path, 'utf8')).toContain(`/hook/${source}`)
expect(readFileSync(path, 'utf8')).not.toContain('claude-hook-impl.cmd')
expect(existsSync(join(home.path, '.orca', 'agent-hooks', `${name}-hook-impl.cmd`))).toBe(
false
)
}
})
})
@@ -57,10 +57,6 @@ describe('resolveCodexSharedServerBinary', () => {
const legacy = installPackage('standalone', 'current')
expect(await resolveCodexSharedServerBinary(home)).toBe(legacy)
})
it('is null when no package is installed', async () => {
expect(await resolveCodexSharedServerBinary(home)).toBeNull()
})
})
describe('readFeatureEnabled', () => {
@@ -149,34 +149,4 @@ describe('terminal history restore memory limits', () => {
expect(restore?.snapshotAnsi).toBe('safe checkpoint')
expect(restore?.oscLinks).toHaveLength(oscLinks.length)
})
// Call the reader directly so fallback behavior cannot hide a parsing regression.
it('parses link-dense checkpoints without a structural pre-scan budget', () => {
const { sessionPath } = createSession('link-dense-checkpoint')
const checkpointPath = join(sessionPath, 'checkpoint.json')
writeFileSync(checkpointPath, `{"snapshotAnsi":"","values":[${'0,'.repeat(2_000_000)}0]}`)
expect(
readTerminalHistoryJson<{ values: number[] }>(
checkpointPath,
TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES
).values
).toHaveLength(2_000_001)
})
// V8 parses JSON iteratively; recursive parsing could abort the daemon instead of throwing.
it('parses deeply nested checkpoints without the retired nesting-depth cap', () => {
const { sessionPath } = createSession('deeply-nested-checkpoint')
const checkpointPath = join(sessionPath, 'checkpoint.json')
const depth = 50_000
writeFileSync(
checkpointPath,
`{"snapshotAnsi":"","nested":${'['.repeat(depth)}${']'.repeat(depth)}}`
)
expect(
readTerminalHistoryJson<{ nested: unknown[] }>(
checkpointPath,
TERMINAL_HISTORY_CHECKPOINT_MAX_BYTES
).nested
).toHaveLength(1)
})
})
@@ -8,7 +8,6 @@ import {
resetWslWindowsCheckoutGitConfigForTests,
UNPROVEN_CHECKOUT_WRITE_GIT_CONFIG_ARGS,
usesWindowsCheckoutGitConfig,
WINDOWS_CHECKOUT_GIT_CONFIG_ARGS,
withWslWindowsCheckoutGitConfig,
wslWindowsCheckoutKey,
WSL_WINDOWS_CHECKOUT_TTL_MS
@@ -93,18 +92,6 @@ describe('which commands run with Windows Git settings', () => {
expect(withWslWindowsCheckoutGitConfig(read, CWD, 'Ubuntu', 'win32', NOW)).toEqual(read)
})
it('flags reads as soon as the host answers, before the sample', () => {
recordHostAutocrlf(KEY, 'true', NOW)
expect(withWslWindowsCheckoutGitConfig(read, CWD, 'Ubuntu', 'win32', NOW)).toEqual([
...WINDOWS_CHECKOUT_GIT_CONFIG_ARGS,
...read
])
expect(withWslWindowsCheckoutGitConfig(write, CWD, 'Ubuntu', 'win32', NOW)).toEqual([
...UNPROVEN_CHECKOUT_WRITE_GIT_CONFIG_ARGS,
...write
])
})
it('stops flagging reads once the sample proves an LF checkout', () => {
recordHostAutocrlf(KEY, 'true', NOW)
recordLineEndingSample(KEY, 'not-windows', 'id', NOW)
+2 -129
View File
@@ -1,22 +1,13 @@
import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import {
getEffectiveKeybindingsForAction,
keybindingMatchesAction,
LEGACY_TAB_SWITCH_BINDINGS,
type KeybindingActionId,
type KeybindingInput
} from '../../shared/keybindings'
import { getEffectiveKeybindingsForAction } from '../../shared/keybindings'
import { getUserKeybindingsPath, writeKeybindingOverride } from './keybinding-file'
import { KeybindingService } from './keybinding-service'
// Verify effective bindings and keystroke matching for fresh/existing cohorts across all platforms.
const PLATFORMS: NodeJS.Platform[] = ['darwin', 'linux', 'win32']
const SWAPPED_ACTIONS = Object.keys(LEGACY_TAB_SWITCH_BINDINGS) as KeybindingActionId[]
function makeCohort(pending: boolean): {
controller: { isPending: () => boolean; markSeeded: () => void }
seeded: () => boolean
@@ -36,22 +27,6 @@ function makeCohort(pending: boolean): {
}
// Physical bracket codes are layout-independent; Mod maps to the platform modifier.
function bracketPress(opts: {
code: 'BracketLeft' | 'BracketRight'
alt: boolean
shift: boolean
platform: NodeJS.Platform
}): KeybindingInput {
const isMac = opts.platform === 'darwin'
return {
code: opts.code,
key: opts.code === 'BracketRight' ? ']' : '[',
meta: isMac,
control: !isMac,
alt: opts.alt,
shift: opts.shift
}
}
describe('KeybindingService tab-switch cohort seeding', () => {
let home: string
@@ -64,84 +39,6 @@ describe('KeybindingService tab-switch cohort seeding', () => {
rmSync(home, { recursive: true, force: true })
})
it.each(PLATFORMS)(
'fresh install (%s) adopts the swapped defaults and writes no file',
(platform) => {
const cohort = makeCohort(false)
const service = new KeybindingService({
homePath: home,
platform,
legacyTabSwitchSeed: cohort.controller
})
expect(cohort.seeded()).toBe(false)
expect(existsSync(getUserKeybindingsPath(home))).toBe(false)
const overrides = service.getOverrides()
expect(getEffectiveKeybindingsForAction('tab.nextAllTypes', platform, overrides)).toEqual([
'Mod+Shift+BracketRight'
])
expect(getEffectiveKeybindingsForAction('tab.nextSameType', platform, overrides)).toEqual([
'Mod+Alt+BracketRight'
])
expect(
keybindingMatchesAction(
'tab.nextAllTypes',
bracketPress({ code: 'BracketRight', alt: false, shift: true, platform }),
platform,
overrides
)
).toBe(true)
expect(
keybindingMatchesAction(
'tab.nextSameType',
bracketPress({ code: 'BracketRight', alt: false, shift: true, platform }),
platform,
overrides
)
).toBe(false)
}
)
it.each(PLATFORMS)(
'existing install (%s) keeps the pre-swap chords exactly, on a clean profile',
(platform) => {
const cohort = makeCohort(true)
const service = new KeybindingService({
homePath: home,
platform,
legacyTabSwitchSeed: cohort.controller
})
expect(cohort.seeded()).toBe(true)
const overrides = service.getOverrides()
for (const actionId of SWAPPED_ACTIONS) {
expect(getEffectiveKeybindingsForAction(actionId, platform, overrides)).toEqual(
LEGACY_TAB_SWITCH_BINDINGS[actionId]
)
}
// Existing installs keep the inverse of fresh-install tab-switch defaults.
const shiftRight = bracketPress({ code: 'BracketRight', alt: false, shift: true, platform })
expect(keybindingMatchesAction('tab.nextSameType', shiftRight, platform, overrides)).toBe(
true
)
expect(keybindingMatchesAction('tab.nextAllTypes', shiftRight, platform, overrides)).toBe(
false
)
const altLeft = bracketPress({ code: 'BracketLeft', alt: true, shift: false, platform })
expect(keybindingMatchesAction('tab.previousAllTypes', altLeft, platform, overrides)).toBe(
true
)
expect(keybindingMatchesAction('tab.previousSameType', altLeft, platform, overrides)).toBe(
false
)
}
)
it('existing install keeps a rebound action AND pins the pre-swap default on the rest', () => {
const platform: NodeJS.Platform = 'darwin'
writeKeybindingOverride(getUserKeybindingsPath(home), platform, 'tab.nextSameType', ['Mod+K'])
@@ -168,30 +65,6 @@ describe('KeybindingService tab-switch cohort seeding', () => {
])
})
it('is idempotent: a second launch after the seed changes nothing', () => {
const platform: NodeJS.Platform = 'linux'
const cohort = makeCohort(true)
const first = new KeybindingService({
homePath: home,
platform,
legacyTabSwitchSeed: cohort.controller
})
const afterFirst = first.getOverrides()
// Reuse the cohort after markSeeded to model the persisted done flag.
const second = new KeybindingService({
homePath: home,
platform,
legacyTabSwitchSeed: cohort.controller
})
expect(second.getOverrides()).toEqual(afterFirst)
for (const actionId of SWAPPED_ACTIONS) {
expect(getEffectiveKeybindingsForAction(actionId, platform, second.getOverrides())).toEqual(
LEGACY_TAB_SWITCH_BINDINGS[actionId]
)
}
})
it('leaves the cohort pending when the seed write fails (retries next launch)', () => {
// A file at homePath forces mkdir failure; the cohort must remain pending.
const fileHome = join(home, 'not-a-dir')
@@ -21,7 +21,6 @@ import {
remoteNodeRuntimeDir
} from './orcad-remote-node-runtime'
import {
parseRemoteRuntimeExitReport,
REMOTE_NODE_RUNTIME_MISSING,
REMOTE_NODE_RUNTIME_READY
} from '../../ssh/orcad-remote-node-runtime-report'
@@ -81,18 +80,6 @@ describe.skipIf(process.platform === 'win32')('remote pinned-Node runtime store'
)
})
it('splits the runtime exit status from the loader output it reported', () => {
expect(
parseRemoteRuntimeExitReport(
'ORCA_NODE_RUNTIME_SELFTEST_FAILED\nORCA_RUNTIME_EXIT=126\nsh: node: Permission denied\n'
)
).toEqual({ exitStatus: 126, output: 'sh: node: Permission denied' })
expect(parseRemoteRuntimeExitReport('no report')).toEqual({
exitStatus: null,
output: 'no report'
})
})
it('reports a runtime whose bytes do not hash to the pin as missing', () => {
const root = mkdtempSync(join(tmpdir(), 'orcad-runtime-store-'))
directories.push(root)
@@ -14,49 +14,15 @@ import {
inventoryRemoteInstallDirs,
ORCAD_INSTALL_MODEL,
RELAY_INSTALL_MODEL,
remoteInstallDirName,
remoteInstallDirOwner,
remoteInstallGcPermits,
remoteInstallListingRegexSource,
remoteInstallVersionDirRegex
} from './remote-install-model'
const RELAY_DIRS = ['relay-0.1.0+abcdef123456', 'relay-v0.1.0', 'relay-1.2.3']
const ORCAD_DIRS = ['orcad-0.1.0+abcdef123456', 'orcad-v0.1.0', 'orcad-1.2.3']
it.each(['linux-x64', 'darwin-arm64', 'win32-x64'] as const)(
'requires the runtime reference, node-pty and both profile workers on %s',
(platform) => {
const isWindows = platform.startsWith('win32')
const artifacts = ORCAD_INSTALL_MODEL.requiredArtifacts(getRemoteHostPlatform(platform))
expect(artifacts).toContain('.runtime-node')
expect(artifacts).not.toContain('bun-runtime')
expect(artifacts).not.toContain('bun-runtime.exe')
expect(artifacts).toContain(
`node_modules/node-pty/build/Release/${isWindows ? 'conpty.node' : 'pty.node'}`
)
expect(artifacts).toContain('profile-state-writer-worker-entry.js')
expect(artifacts).toContain('profile-state-backup-worker-entry.js')
expect(artifacts.includes('windows-process-tree.node')).toBe(isWindows)
}
)
describe('remote install namespace', () => {
it('names each model its own version dir', () => {
expect(remoteInstallDirName(RELAY_INSTALL_MODEL, '0.1.0+aa')).toBe('relay-0.1.0+aa')
expect(remoteInstallDirName(ORCAD_INSTALL_MODEL, '0.1.0+aa')).toBe('orcad-0.1.0+aa')
})
it("requires the host's own search binary in a completed standalone runtime install", () => {
for (const platform of ['linux-arm64', 'darwin-x64', 'win32-arm64'] as const) {
expect(
ORCAD_INSTALL_MODEL.requiredArtifacts(getRemoteHostPlatform(platform)).filter(
(file) => file.startsWith('ripgrep/') && !file.startsWith('ripgrep/licenses/')
)
).toEqual([orcadRipgrepArtifact(platform)])
}
})
it.skipIf(process.platform === 'win32')('rejects an install missing its search binary', () => {
const dir = mkdtempSync(join(tmpdir(), 'orcad-remote-probe-'))
try {
@@ -76,13 +42,6 @@ describe('remote install namespace', () => {
}
})
it('keeps the relay listing pattern byte-identical to the one it shipped with', () => {
// Preserve the shipped listing scope: widening it changes which existing directories GC can see.
expect(remoteInstallListingRegexSource(RELAY_INSTALL_MODEL)).toBe(
String.raw`^relay-(v?[0-9]+\.[0-9]+\.[0-9]+(\+[0-9a-f]+)?)(\.gc-tombstone\.[0-9]+\.[0-9]+)?$`
)
})
it('refuses a dir prefix that could escape a remote glob or quote', () => {
const injected = { ...RELAY_INSTALL_MODEL, dirPrefix: "relay'; rm -rf ~" }
expect(() => remoteInstallVersionDirRegex(injected)).toThrow('Unsafe remote install dir prefix')
@@ -116,13 +75,6 @@ describe('GC ownership — each model collects only its own namespace', () => {
}
})
it('groups a mixed listing without losing anything to the wrong owner', () => {
const inventory = inventoryRemoteInstallDirs([...RELAY_DIRS, ...ORCAD_DIRS, 'something-else'])
expect(inventory.relay).toEqual(RELAY_DIRS)
expect(inventory.orcad).toEqual(ORCAD_DIRS)
expect(inventory.unknown).toEqual(['something-else'])
})
it('gives the shared runtime store its own owner that no version-dir GC may take', () => {
expect(remoteInstallDirOwner('runtimes')).toBe('runtimes')
expect(remoteInstallGcPermits(RELAY_INSTALL_MODEL, 'runtimes')).toBe(false)
@@ -88,23 +88,6 @@ it.each([1, 10])(
}
)
it('backs off a successful partial target, keeps saved-only reads passive, and retries after expiry', async () => {
const probe = vi.fn<AgentModelCatalogProbe>(async () => ({
origin: 'probe',
models: menu('new')
}))
const { service, advance } = setup(probe)
const params = { agent: 'opencode2', sessionId: 'session-a', selectedModel: B }
await service.read({ ...params, savedOnly: true })
expect(probe).not.toHaveBeenCalled()
await service.read({ ...params, waitForListing: true })
await Promise.all(Array.from({ length: 10 }, () => service.demand(params)))
expect(probe).toHaveBeenCalledTimes(1)
advance()
await service.read({ ...params, waitForListing: true })
expect(probe).toHaveBeenCalledTimes(2)
})
it.each([
{ n: 1, partial: false },
{ n: 10, partial: false },
@@ -10,10 +10,7 @@ import {
JOURNAL_DB_SCHEMA_VERSION
} from './journal-database-schema'
import { journalDatabasePath } from './journal-host-database'
import {
closeTestJournalHostDatabases,
openTestJournalHostDatabase
} from './journal-host-database-test-support'
import { closeTestJournalHostDatabases } from './journal-host-database-test-support'
import { insertCommandReceiptIfAbsent, readCommandReceipt } from './command-receipt-table'
import { commandReceiptScope } from './command-receipt-schema'
import {
@@ -47,49 +44,6 @@ export function registerCommandReceiptSchemaTests(): void {
}
describe('command receipt schema at writable open', () => {
it('creates the operation/caller primary key and session index on a fresh host database', () => {
const database = openTestJournalHostDatabase(root)
expect(hasReceiptTable(database.db)).toBe(true)
expect(journalPragmaNumber(database.db, 'user_version')).toBe(JOURNAL_DB_SCHEMA_VERSION)
expect(
database.db.prepare('PRAGMA table_info(agent_session_command_receipts)').all()
).toEqual([
expect.objectContaining({ name: 'operation_id', pk: 1 }),
expect.objectContaining({ name: 'session_id', pk: 0 }),
expect.objectContaining({ name: 'caller_key', pk: 2 }),
expect.objectContaining({ name: 'method', pk: 0 }),
expect.objectContaining({ name: 'fingerprint', pk: 0 }),
expect.objectContaining({ name: 'status', pk: 0 }),
expect.objectContaining({ name: 'result_json', pk: 0 }),
expect.objectContaining({ name: 'rejection_json', pk: 0 }),
expect.objectContaining({ name: 'accepted_at', pk: 0 })
])
const indexes = database.db.prepare('PRAGMA index_list(agent_session_command_receipts)').all()
expect(indexes).toHaveLength(2)
expect(indexes).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: 'sqlite_autoindex_agent_session_command_receipts_1',
origin: 'pk'
}),
expect.objectContaining({ name: 'agent_session_command_receipts_session', origin: 'c' })
])
)
expect(
database.db.prepare('PRAGMA index_info(agent_session_command_receipts_session)').all()
).toEqual([expect.objectContaining({ name: 'session_id' })])
expect(
database.db.prepare('PRAGMA foreign_key_list(agent_session_command_receipts)').all()
).toEqual([
expect.objectContaining({
table: 'agent_session_records',
from: 'session_id',
to: 'session_id',
on_delete: 'CASCADE'
})
])
})
it.each([3, 4])('adds the table to an older-shaped version %i database', (version) => {
const seeded = new Database(dbPath)
seeded.exec(createJournalTablesSql())
@@ -1,6 +1,6 @@
import type { createFixture } from './journal-unsent-send-hold-contract-test-fixture'
import { describe, expect, it, vi } from 'vitest'
import type { AgentJournalSubmission } from '../../../shared/agent-session/journal/agent-session-journal-types'
import { USER_MESSAGE_SOURCE } from '../../../shared/agent-session/agent-session-message-source'
export function registerJournalUnsentSendHoldQueueTests(
@@ -136,15 +136,6 @@ export function registerJournalUnsentSendHoldQueueTests(
})
describe('the source a send records', () => {
it('is folded from the row; a row without one folds without it', async () => {
const journal = await fixture.open()
await fixture.accept(journal, 'with-source', { origin: 'host', source: USER_MESSAGE_SOURCE })
await fixture.accept(journal, 'without', { origin: 'client' })
const folded: AgentJournalSubmission | undefined = journal.submission('with-source')
expect(folded?.source).toEqual({ kind: 'user' })
expect(journal.submission('without')).not.toHaveProperty('source')
})
it('keeps only the kind of an agent’s source, never its senders', async () => {
const journal = await fixture.open()
await fixture.accept(journal, 'mail', { origin: 'host', source: fixture.MAIL_SOURCE })
@@ -74,36 +74,6 @@ describe('failed hook provider frames', () => {
)
})
it.each([undefined, null, 'invalid', [null], [{ kind: 'error', text: ' ' }]])(
'keeps a named error visible when Codex entries are missing or malformed: %j',
(entries) => {
expect(
unhandledProviderFrameJournalItem('codex', CODEX_KIND, {
run: { eventName: 'stop', status: 'failed', entries }
})?.body
).toMatchObject({ text: 'Codex Stop hook failed.', tone: 'error' })
}
)
it('uses a readable fallback when a failed hook carries no name', () => {
expect(
unhandledProviderFrameJournalItem('codex', CODEX_KIND, {
run: { status: 'failed', entries: [{ kind: 'error', text: 'Timed out' }] }
})?.body.text
).toBe('Codex hook failed: Timed out')
expect(
unhandledProviderFrameJournalItem('codex', CODEX_KIND, {
run: { status: 'failed' },
message: 'Hook could not start'
})?.body.text
).toBe('Codex hook failed: Hook could not start')
expect(
unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, {
outcome: 'error'
})?.body.text
).toBe('Claude hook failed.')
})
it('shows Claude stderr and the exit code instead of the wire event name', () => {
expect(
unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, {
@@ -146,16 +116,6 @@ describe('failed hook provider frames', () => {
).toBe(`Claude SessionStart hook failed: ${expected}`)
})
it('retains a Claude exit code when there is no output', () => {
expect(
unhandledProviderFrameJournalItem('claude', CLAUDE_KIND, {
hook_name: 'SessionStart:startup',
outcome: 'error',
exit_code: 1
})?.body.text
).toBe('Claude SessionStart:startup hook failed (exit code 1).')
})
it('keeps successful hooks hidden even when they produce diagnostic output', () => {
expect(
unhandledProviderFrameJournalItem('codex', CODEX_KIND, {
@@ -187,14 +147,4 @@ describe('failed hook provider frames', () => {
Buffer.byteLength(row?.body.providerFrame?.payload.head ?? '', 'utf8')
).toBeLessThanOrEqual(64)
})
it('leaves other providers and unrelated frame kinds on their existing fallback', () => {
const payload = { outcome: 'error', hook_name: 'Other hook', stderr: 'Failure' }
expect(
unhandledProviderFrameJournalItem('future-provider', CLAUDE_KIND, payload)?.body.text
).toBe(`future-provider · ${CLAUDE_KIND}`)
expect(
unhandledProviderFrameJournalItem('claude', 'message:future/event', payload)?.body.text
).toBe('claude · message:future/event')
})
})
@@ -176,21 +176,4 @@ describe('what a chat host answers for one send', () => {
expect(result).toEqual({ kind })
expect(readReceipt).toHaveBeenCalledWith('mail')
})
it.each([4, 40])('answers %i sends with no card left by key, over a long history', (count) => {
const j = journal()
for (let item = 0; item < 500; item += 1) {
j.submit(item * 3 + 1, `history-${item}`)
j.handOver(item * 3 + 2, `history-${item}`)
j.accept(item * 3 + 3, `history-${item}`)
}
let receipts = 0
for (let send = 0; send < count; send += 1) {
const { result, readReceipt } = answer(j.state, `gone-${send}`)
expect(result).toEqual({ kind: 'absent' })
receipts += readReceipt.mock.calls.length
}
// One receipt read per send, and no history walk (the reader above throws on one).
expect(receipts).toBe(count)
})
})
@@ -69,17 +69,6 @@ function lastName(events: readonly AgentSessionStatusEvent[]): string | undefine
return event?.type === 'status' ? event.session.conversationName : undefined
}
it('publishes a held chat its saved name once, and not again for an unchanged record', async () => {
const { feed, events, name } = await namedFeed()
expect(lastName(events)).toBeUndefined()
name('Explain the parser')
expect(lastName(events)).toBe('Explain the parser')
const count = events.length
feed.publish('session')
feed.publishConversationName('session')
expect(events).toHaveLength(count + 1)
})
it('names a chat whose tab closed before naming finished, and keeps it for later subscribers', async () => {
const { feed, events, sessions, name } = await namedFeed()
feed.revokeLive('session')
@@ -94,21 +83,6 @@ it('names a chat whose tab closed before naming finished, and keeps it for later
})
})
it('keeps the name on the summary a closing chat retains', async () => {
const { feed, events, name } = await namedFeed()
name('Explain the parser')
feed.close('session')
expect(lastName(events)).toBe('Explain the parser')
})
it('publishes naming activity without needing a chat summary', async () => {
const { feed, events } = await namedFeed()
const count = events.length
feed.publishConversationName('unknown-session')
expect(events).toHaveLength(count + 1)
expect(events.at(-1)).toEqual({ type: 'snapshot', sessions: [], namingActiveSessionIds: [] })
})
it('publishes cold names from durable records across a host restart without a status row', () => {
let record = { ...agentSessionRecordFixture(), conversationName: 'Before' }
const open = () =>
@@ -20,31 +20,6 @@ import {
journals
} from './structured-agent-session-subscriber-contract-test-fixture'
it('reports every content publication to the journal hook, subscribed or not', async () => {
const journal = await journals.open({
identity: {
sessionId: SESSION,
workspaceId: 'workspace-1',
hostId: 'local',
agent: 'codex',
providerHandle: codexProviderHandle('thread-1')
},
stateDirectory: join(fixture.root, 'hook-journal')
})
const published: string[] = []
const subscribers = new AgentSessionSubscribers({
onJournalPublished: (sessionId, published_journal) => {
expect(published_journal).toBe(journal)
published.push(sessionId)
}
})
subscribers.publish(SESSION, journal)
subscribers.snapshot(SESSION, journal, 1)
expect(published).toEqual([SESSION, SESSION])
})
it('settles a session nobody is reading, from running to idle', async () => {
// Never open a subscriber: status must publish independently of transcript readers.
const journal = await journals.open({
@@ -1,197 +0,0 @@
import type * as NodeCrypto from 'node:crypto'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH,
LAYOUT_OWNER_WRITTEN_SESSION_WORKTREE_ID as WORKTREE
} from '../../../shared/__fixtures__/layout-owner-written-session-golden-paths'
import type { LayoutCommand } from '../../../shared/workspace-layout/workspace-layout-command-types'
import type { WorkspaceSessionState } from '../../../shared/workspace-session/workspace-session-state-types'
import type { Store } from '../loading-store/store'
import { ProfileStateSqliteAuthority } from '../profile-state/profile-state-sqlite-authority'
import { windowViewSaveForMain } from '../window-save-view'
import {
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID,
openTopologyStore
} from './terminal-topology-profile-fixture'
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
vi.mock('../../telemetry/client', () => ({ track: vi.fn() }))
vi.mock('../../ssh/config/ssh-config-parser', () => ({
loadUserSshConfig: () => ({ hosts: [] }),
sshConfigHostsToTargets: () => []
}))
// Deterministic minted IDs keep persisted bytes stable.
const minting = vi.hoisted(() => ({ on: false, next: 0 }))
vi.mock('node:crypto', async (importOriginal) => {
const actual = await importOriginal<typeof NodeCrypto>()
return {
...actual,
randomUUID: () =>
minting.on
? `00000000-0000-4000-8000-${String(++minting.next).padStart(12, '0')}`
: actual.randomUUID()
}
})
const UPDATE_ENV = 'ORCA_UPDATE_LAYOUT_OWNER_GOLDEN'
const leaf = (n: number): string => `${n}${n}${n}${n}${n}${n}${n}${n}-1111-4111-8111-111111111111`
const on = { workspace: WORKTREE }
const directories: string[] = []
afterEach(() => {
minting.on = false
vi.useRealTimers()
for (const directory of directories.splice(0)) {
rmSync(directory, { recursive: true, force: true })
}
})
const COMMANDS: LayoutCommand[] = [
{ ...on, type: 'createTerminalTab', tabId: 'tab-shell', leafId: leaf(1), createdAt: 1 },
{ ...on, type: 'createTerminalTab', tabId: 'tab-build', leafId: leaf(2), createdAt: 2 },
{
...on,
type: 'splitPane',
tabId: 'tab-shell',
leafId: leaf(1),
direction: 'vertical',
newLeafId: leaf(3)
},
{ ...on, type: 'renameTab', tabId: 'tab-build', kind: 'custom', title: 'Build' },
{
...on,
type: 'openEditorTab',
tabId: 'tab-readme',
fileId: '/fixture/local/README.md',
contentType: 'editor',
createdAt: 3,
file: { filePath: '/fixture/local/README.md', relativePath: 'README.md', language: 'markdown' }
},
{
...on,
type: 'openBrowserTab',
tabId: 'tab-docs',
browserTabId: 'browser-docs',
pageId: 'page-docs',
createdAt: 4,
browserCreatedAt: 4
},
{ ...on, type: 'createTerminalTab', tabId: 'tab-scratch', leafId: leaf(4), createdAt: 5 }
]
function windowSave(stored: WorkspaceSessionState): WorkspaceSessionState {
const page = {
id: 'page-docs',
workspaceId: 'browser-docs',
worktreeId: WORKTREE,
url: 'https://example.com/docs',
title: 'Docs',
loading: false,
faviconUrl: null,
canGoBack: false,
canGoForward: false,
loadError: null,
createdAt: 4
}
return {
...stored,
activeRepoId: 'repo-local',
activeWorktreeId: WORKTREE,
activeTabId: 'tab-shell',
browserPagesByWorkspace: { 'browser-docs': [page] },
browserTabsByWorktree: {
[WORKTREE]: (stored.browserTabsByWorktree?.[WORKTREE] ?? []).map((tab) => ({
...tab,
url: page.url,
title: page.title
}))
}
}
}
async function apply(store: Store, command: LayoutCommand): Promise<void> {
const result = await store.applyLayoutCommand(command)
expect(result.ok, command.type).toBe(true)
}
function writtenSession(directory: string): string {
const authority = new ProfileStateSqliteAuthority(join(directory, 'profile-state.db'), 'topology')
const serialized = authority.readSerializedState()
authority.close()
const profile: { workspaceSession: unknown } = JSON.parse(serialized ?? '{}')
return `${JSON.stringify(profile.workspaceSession, null, 2)}\n`
}
// This probes current switch-off readers, not compatibility with older releases.
describe('the session a layout-owner build writes (downgrade probe, ledger L8)', () => {
it('is the checked-in golden, byte for byte', async () => {
expect(WORKTREE).toBe(FIXTURE_GIT_WORKTREE_ID)
const directory = mkdtempSync(join(tmpdir(), 'orca-layout-owner-golden-'))
directories.push(directory)
const store = await openTopologyStore(directory, emptyTerminalSessionProfile())
vi.useFakeTimers({ toFake: ['Date'], now: 1_700_000_000_000 })
minting.on = true
for (const command of COMMANDS.slice(0, 4)) {
await apply(store, command)
}
const shellGroup = store.getWorkspaceSession().tabGroups?.[WORKTREE]?.[0]?.id
await apply(store, {
...on,
type: 'splitGroup',
tabId: 'tab-build',
besideGroupId: shellGroup!,
direction: 'right'
})
for (const command of COMMANDS.slice(4)) {
await apply(store, command)
}
await store.persistPtyBinding({
worktreeId: WORKTREE,
tabId: 'tab-shell',
leafId: leaf(1),
ptyId: 'pty-shell',
incarnationId: 'inc-shell'
})
// Retain closed terminals for exit events arriving after close.
await store.persistPtyBinding({
worktreeId: WORKTREE,
tabId: 'tab-scratch',
leafId: leaf(4),
ptyId: 'pty-scratch',
incarnationId: 'inc-scratch'
})
await apply(store, {
...on,
type: 'closeTabs',
tabIds: ['tab-scratch'],
force: true,
recordClose: 'user'
})
minting.on = false
// Simulate session:set with renderer selection and browser-page state.
const stored = store.getWorkspaceSession()
store.setWorkspaceSession(windowViewSaveForMain(stored, windowSave(stored), undefined))
await store.flushPendingOrThrowAsync()
await store.freezeWritesAsync()
const written = writtenSession(directory)
// Reject machine-specific paths; clocks and minted IDs are deterministic.
expect(written).not.toContain(tmpdir())
if (process.env[UPDATE_ENV] === '1') {
writeFileSync(LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH, written)
}
const golden = JSON.parse(readFileSync(LAYOUT_OWNER_WRITTEN_SESSION_GOLDEN_PATH, 'utf8'))
// Ignore formatter whitespace while retaining key order and values.
expect(
written,
`the switch-on save format changed: regenerate with ${UPDATE_ENV}=1, then the switch-off ` +
'restore tests (layout-owner-written-session-switch-off-load, ' +
'layout-owner-written-session-hydration) must still pass: they are the downgrade proof'
).toBe(`${JSON.stringify(golden, null, 2)}\n`)
})
})
@@ -1,6 +1,5 @@
import { describe, expect, it } from 'vitest'
import {
getWindowsCmdPath,
resolveWindowsPowerShellExecutablePath,
resolveWindowsPowerShellSpawnChain
} from './windows-powershell-executable'
@@ -13,7 +12,6 @@ const WIN_ENV: NodeJS.ProcessEnv = {
ComSpec: 'C:\\Windows\\System32\\cmd.exe'
}
const PWSH7 = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe'
const PATH_PWSH7 = 'D:\\Tools\\PowerShell\\7\\pwsh.exe'
const WINDOWS_POWERSHELL = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe'
// Store alias reparse points fail ConPTY CreateProcessW with access-denied (5).
@@ -22,26 +20,6 @@ const PWSH_STORE_EXE =
'C:\\Program Files\\WindowsApps\\Microsoft.PowerShell_7.6.3.0_x64__8wekyb3d8bbwe\\pwsh.exe'
describe('resolveWindowsPowerShellExecutablePath', () => {
it('returns null on non-Windows platforms', () => {
expect(
resolveWindowsPowerShellExecutablePath('pwsh.exe', {
platform: 'linux',
env: WIN_ENV,
isRealExecutable: () => true
})
).toBeNull()
})
it('resolves pwsh.exe to a real absolute path under Program Files', () => {
expect(
resolveWindowsPowerShellExecutablePath('pwsh.exe', {
platform: 'win32',
env: WIN_ENV,
isRealExecutable: (p) => p === PWSH7
})
).toBe(PWSH7)
})
it('repro: resolves pwsh.exe from a real PATH entry when standard roots miss', () => {
expect(
resolveWindowsPowerShellExecutablePath('pwsh.exe', {
@@ -59,16 +37,6 @@ describe('resolveWindowsPowerShellExecutablePath', () => {
).toBe(PATH_PWSH7)
})
it('resolves powershell.exe to inbox System32 WindowsPowerShell', () => {
expect(
resolveWindowsPowerShellExecutablePath('powershell.exe', {
platform: 'win32',
env: WIN_ENV,
isRealExecutable: (p) => p === WINDOWS_POWERSHELL
})
).toBe(WINDOWS_POWERSHELL)
})
it('repro: never resolves pwsh.exe to the Store App Execution Alias stub', () => {
// Alias existence cannot prove a ConPTY-launchable executable.
const resolved = resolveWindowsPowerShellExecutablePath('pwsh.exe', {
@@ -108,28 +76,9 @@ describe('resolveWindowsPowerShellExecutablePath', () => {
expect(resolved).toBeNull()
}
)
it('returns null for pwsh when no real executable is found', () => {
expect(
resolveWindowsPowerShellExecutablePath('pwsh.exe', {
platform: 'win32',
env: WIN_ENV,
isRealExecutable: () => false
})
).toBeNull()
})
})
describe('resolveWindowsPowerShellSpawnChain', () => {
it('orders pwsh -> Windows PowerShell -> cmd.exe when all resolve', () => {
const chain = resolveWindowsPowerShellSpawnChain('pwsh.exe', {
platform: 'win32',
env: WIN_ENV,
isRealExecutable: (p) => p === PWSH7 || p === WINDOWS_POWERSHELL
})
expect(chain).toEqual([PWSH7, WINDOWS_POWERSHELL, WIN_ENV.ComSpec])
})
it('falls back to Windows PowerShell + cmd.exe when pwsh is only a Store alias', () => {
const chain = resolveWindowsPowerShellSpawnChain('pwsh.exe', {
platform: 'win32',
@@ -139,57 +88,4 @@ describe('resolveWindowsPowerShellSpawnChain', () => {
expect(chain).toEqual([WINDOWS_POWERSHELL, WIN_ENV.ComSpec])
expect(chain).not.toContain(PWSH_STORE_ALIAS)
})
it('uses the real Store package executable ahead of Windows PowerShell', () => {
const chain = resolveWindowsPowerShellSpawnChain('pwsh.exe', {
platform: 'win32',
env: {
...WIN_ENV,
Path: 'C:\\Users\\dev\\AppData\\Local\\Microsoft\\WindowsApps'
},
isRealExecutable: (p) => p === PWSH_STORE_EXE || p === WINDOWS_POWERSHELL,
resolveAppExecutionAlias: (p) => (p === PWSH_STORE_ALIAS ? PWSH_STORE_EXE : null)
})
expect(chain).toEqual([PWSH_STORE_EXE, WINDOWS_POWERSHELL, WIN_ENV.ComSpec])
})
it('orders PATH-resolved pwsh before Windows PowerShell when standard roots miss', () => {
const chain = resolveWindowsPowerShellSpawnChain('pwsh.exe', {
platform: 'win32',
env: {
...WIN_ENV,
Path: 'D:\\Tools\\PowerShell\\7'
},
isRealExecutable: (p) => p === PATH_PWSH7 || p === WINDOWS_POWERSHELL
})
expect(chain).toEqual([PATH_PWSH7, WINDOWS_POWERSHELL, WIN_ENV.ComSpec])
})
it('always ends with cmd.exe even when no PowerShell resolves', () => {
const chain = resolveWindowsPowerShellSpawnChain('powershell.exe', {
platform: 'win32',
env: WIN_ENV,
isRealExecutable: () => false
})
expect(chain).toEqual([WIN_ENV.ComSpec])
})
it('does not duplicate cmd.exe when ComSpec is missing', () => {
const chain = resolveWindowsPowerShellSpawnChain('powershell.exe', {
platform: 'win32',
env: { SystemRoot: 'C:\\Windows' },
isRealExecutable: () => false
})
expect(chain).toEqual(['C:\\Windows\\System32\\cmd.exe'])
})
})
describe('getWindowsCmdPath', () => {
it('prefers ComSpec when set', () => {
expect(getWindowsCmdPath(WIN_ENV)).toBe('C:\\Windows\\System32\\cmd.exe')
})
it('derives cmd.exe from SystemRoot when ComSpec is absent', () => {
expect(getWindowsCmdPath({ SystemRoot: 'D:\\Win' })).toBe('D:\\Win\\System32\\cmd.exe')
})
})
@@ -1,19 +1,11 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
const getAllProcessesMock = vi.fn()
import { __setWindowsProcessTreeLoaderForTests } from '../../windows/windows-process-table'
import {
classifyWindowsTreeKillTarget,
verifyWindowsTreeKillTarget,
WINDOWS_ROOT_IDENTITY_TIMEOUT_MS
} from './windows-pty-root-identity'
// The native reader rejects snapshots that omit the querying process.
const SELF_PROCESS_ROW = { pid: process.pid, ppid: 0, name: 'vitest.exe', commandLine: 'vitest' }
const withSelf = <T>(rows: readonly T[]): (T | typeof SELF_PROCESS_ROW)[] => [
SELF_PROCESS_ROW,
...rows
]
const ORCA_PID = 5000
@@ -34,12 +26,6 @@ describe('classifyWindowsTreeKillTarget', () => {
expect(classifyWindowsTreeKillTarget(4242, rows, ORCA_PID)).toBe('own')
})
it('documents that a recycled PID under another Orca pane still classifies as own', () => {
// Recycled as a tool under another pane, PID 4242 still has Orca ancestry.
const rows = [link(ORCA_PID, 900), link(7000, ORCA_PID), link(7100, 7000), link(4242, 7100)]
expect(classifyWindowsTreeKillTarget(4242, rows, ORCA_PID)).toBe('own')
})
it('rejects a recycled pid whose ancestry never reaches this process', () => {
const rows = [...SYSTEM_CHAIN, link(ORCA_PID, 900), link(4242, 900)]
expect(classifyWindowsTreeKillTarget(4242, rows, ORCA_PID)).toBe('foreign')
@@ -72,10 +58,6 @@ describe('classifyWindowsTreeKillTarget', () => {
expect(classifyWindowsTreeKillTarget(ORCA_PID, rows, ORCA_PID)).toBe('foreign')
})
it.each([0, -1, 1.5, Number.NaN])('rejects the invalid root pid %s', (rootPid) => {
expect(classifyWindowsTreeKillTarget(rootPid, [link(4242, ORCA_PID)], ORCA_PID)).toBe('foreign')
})
it('treats duplicate rows for the root as unknown, not as ownership', () => {
const rows = [link(4242, ORCA_PID), link(4242, 900)]
expect(classifyWindowsTreeKillTarget(4242, rows, ORCA_PID)).toBe('unknown')
@@ -88,21 +70,6 @@ describe('classifyWindowsTreeKillTarget', () => {
})
describe('verifyWindowsTreeKillTarget', () => {
it('classifies a live win32 root against the fresh process table', async () => {
const readRows = vi.fn().mockResolvedValue([link(4242, ORCA_PID)])
await expect(
verifyWindowsTreeKillTarget(4242, { readRows, ownerPid: ORCA_PID, platform: 'win32' })
).resolves.toBe('own')
expect(readRows).toHaveBeenCalledOnce()
})
it('detects the recycled-pid case that taskkill /T /F must not touch', async () => {
const readRows = vi.fn().mockResolvedValue([link(4242, 900), link(900, 4)])
await expect(
verifyWindowsTreeKillTarget(4242, { readRows, ownerPid: ORCA_PID, platform: 'win32' })
).resolves.toBe('foreign')
})
it('returns unknown when the Windows process table is unavailable', async () => {
const readRows = vi.fn().mockResolvedValue(null)
await expect(
@@ -141,45 +108,4 @@ describe('verifyWindowsTreeKillTarget', () => {
vi.useRealTimers()
}
})
it('skips the probe off Windows so POSIX teardown keeps its own guards', async () => {
const readRows = vi.fn()
await expect(
verifyWindowsTreeKillTarget(4242, { readRows, ownerPid: ORCA_PID, platform: 'darwin' })
).resolves.toBe('unknown')
expect(readRows).not.toHaveBeenCalled()
})
})
describe('verifyWindowsTreeKillTarget scan volume', () => {
const NATIVE_ROWS = [
{ pid: ORCA_PID, ppid: 900, name: 'orca.exe', commandLine: 'orca.exe' },
{ pid: 4242, ppid: ORCA_PID, name: 'pwsh.exe', commandLine: 'pwsh.exe' }
]
beforeEach(() => {
getAllProcessesMock.mockReset()
getAllProcessesMock.mockImplementation((cb: (snapshot: unknown) => void) => {
cb(withSelf(NATIVE_ROWS))
})
__setWindowsProcessTreeLoaderForTests(() => ({
ProcessDataFlag: { None: 0, Memory: 1, CommandLine: 2, CreationTime: 4 },
getAllProcesses: getAllProcessesMock
}))
})
afterEach(() => {
__setWindowsProcessTreeLoaderForTests()
})
it('collapses a 32-wide teardown burst into a single process-table scan', async () => {
const verdicts = await Promise.all(
Array.from({ length: 32 }, () =>
verifyWindowsTreeKillTarget(4242, { ownerPid: ORCA_PID, platform: 'win32' })
)
)
expect(getAllProcessesMock.mock.calls.length).toBe(1)
expect(new Set(verdicts)).toEqual(new Set(['own']))
})
})
@@ -3,7 +3,7 @@ import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:f
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { getFishCodexShellLaunchPreflight } from '../../../shared/codex-shell-function'
import {
fishRequirementViolation,
resolveFishBinary
@@ -11,28 +11,12 @@ import {
import {
buildFishVendorConfWrapperFile,
FISH_XDG_DATA_DIRS_PREFIX_ENV,
getFishVendorConfSnippet,
getFishXdgDataDirsLaunchEnv
} from './fish-xdg-data-dirs-handoff'
const ROOT = '/orca/shell-wrappers/abc'
const DATA_DIR = `${ROOT}/fish-xdg-data`
describe('getFishXdgDataDirsLaunchEnv', () => {
it('adds the XDG default after Orca when the variable is unset', () => {
const expected = `${DATA_DIR}:/usr/local/share:/usr/share`
expect(getFishXdgDataDirsLaunchEnv(ROOT, undefined)).toEqual({
XDG_DATA_DIRS: expected,
[FISH_XDG_DATA_DIRS_PREFIX_ENV]: expected
})
})
it('treats an empty XDG_DATA_DIRS as unset', () => {
expect(getFishXdgDataDirsLaunchEnv(ROOT, '')).toEqual(
getFishXdgDataDirsLaunchEnv(ROOT, undefined)
)
})
it.each([['-N'], ['--no-config'], ['--no-c'], ['-lN'], ['-Ni']])(
'skips fish args that disable config: %s',
(arg) => {
@@ -40,44 +24,11 @@ describe('getFishXdgDataDirsLaunchEnv', () => {
}
)
it.for([['-l'], ['-i'], ['--login'], ['--no-execute'], ['--no-'], ['-c', 'echo N']])(
'still hands off for fish args %j',
(args) => {
expect(getFishXdgDataDirsLaunchEnv(ROOT, undefined, args)).not.toEqual({})
}
)
it('prepends to an existing value and records only its own entry', () => {
expect(getFishXdgDataDirsLaunchEnv(ROOT, '/opt/a:/opt/b')).toEqual({
XDG_DATA_DIRS: `${DATA_DIR}:/opt/a:/opt/b`,
[FISH_XDG_DATA_DIRS_PREFIX_ENV]: DATA_DIR
})
})
it('prepends again when the inherited value already names the dir', () => {
const inherited = `${DATA_DIR}:/opt/a`
expect(getFishXdgDataDirsLaunchEnv(ROOT, inherited).XDG_DATA_DIRS).toBe(
`${DATA_DIR}:${inherited}`
)
})
it('skips a dir that XDG_DATA_DIRS cannot represent', () => {
expect(getFishXdgDataDirsLaunchEnv('/odd:root', undefined)).toEqual({})
})
})
describe('fish vendor snippet', () => {
it('embeds the shared fish codex function verbatim', () => {
expect(getFishVendorConfSnippet()).toContain(getFishCodexShellLaunchPreflight())
})
it('lives where fish looks for vendor snippets under the data dir', () => {
expect(buildFishVendorConfWrapperFile(ROOT)[0]).toBe(
`${DATA_DIR}/fish/vendor_conf.d/orca-shell-integration.fish`
)
})
})
const fishLookup = resolveFishBinary(3)
// The test shells run with a minimal PATH.
const fish = fishLookup.available
@@ -52,31 +52,6 @@ describe('parseOpenCodeGoStatusPayload', () => {
})
})
it('accepts numeric microCents', () => {
const parsed = parseOpenCodeGoStatusPayload(
JSON.stringify({
access: {
meters: {
fiveHour: {
resetsAt: '2026-09-18T12:42:04.962Z',
limitMicroCents: 100,
usedMicroCents: 25
},
week: {
resetsAt: '2026-09-21T00:00:00.000Z',
limitMicroCents: 200,
usedMicroCents: 50
}
}
}
})
)
expect(parsed?.session?.usedPercent).toBe(25)
expect(parsed?.weekly?.usedPercent).toBe(25)
expect(parsed?.monthly).toBeNull()
})
it('caps usedPercent at 100 and floors at 0', () => {
const parsed = parseOpenCodeGoStatusPayload(
JSON.stringify({
@@ -100,26 +75,6 @@ describe('parseOpenCodeGoStatusPayload', () => {
expect(parsed?.session?.usedPercent).toBe(100)
expect(parsed?.weekly?.usedPercent).toBe(0)
})
it('returns null for HTML and other non-JSON bodies', () => {
expect(parseOpenCodeGoStatusPayload('<html>rollingUsage:{usagePercent:30}</html>')).toBeNull()
expect(parseOpenCodeGoStatusPayload('')).toBeNull()
expect(parseOpenCodeGoStatusPayload('{not json')).toBeNull()
})
it('returns null when fiveHour or week meters are missing', () => {
expect(
parseOpenCodeGoStatusPayload(
JSON.stringify({
access: {
meters: {
week: { limitMicroCents: '100', usedMicroCents: '10' }
}
}
})
)
).toBeNull()
})
})
describe('parseOpenCodeGoBillingStatusPayload', () => {
@@ -170,12 +125,6 @@ describe('parseOpenCodeGoBillingStatusPayload', () => {
])('rejects unsupported or unsafe billing payloads: $billingMode/$mode', (payload) => {
expect(parseOpenCodeGoBillingStatusPayload(JSON.stringify(payload))).toBeNull()
})
it('fails closed for malformed and non-object bodies', () => {
expect(parseOpenCodeGoBillingStatusPayload('{not json')).toBeNull()
expect(parseOpenCodeGoBillingStatusPayload('[]')).toBeNull()
expect(parseOpenCodeGoBillingStatusPayload('')).toBeNull()
})
})
describe('isOpenCodeGoExplicitNoAccessPayload', () => {
@@ -212,14 +161,4 @@ describe('parseOpenCodeGoUsageApiPayload', () => {
expect(parsed?.weekly.usedPercent).toBe(0)
expect(parsed?.monthly).toBeNull()
})
it('returns null for the console error bodies and other non-usage payloads', () => {
expect(
parseOpenCodeGoUsageApiPayload(
JSON.stringify({ type: 'error', error: { type: 'AuthError', message: 'Unauthorized' } })
)
).toBeNull()
expect(parseOpenCodeGoUsageApiPayload('{not json')).toBeNull()
expect(parseOpenCodeGoUsageApiPayload('')).toBeNull()
})
})
@@ -1,10 +1,8 @@
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import type { RemoteLayoutSnapshot } from '../../shared/remote-layout-snapshot'
import type { RuntimeRpcResponse } from '../../shared/runtime-rpc-envelope'
import { scanSourceTree, stripComments } from '../../shared/source-scan/source-tree-scan'
import {
forgetRemoteLayoutSnapshot,
recordRemoteLayoutFrame,
type RemoteLayoutSnapshotStore
} from './remote-layout-snapshot-recorder'
@@ -113,33 +111,6 @@ describe('remote layout snapshot recorder', () => {
expect(workspacesOf(snapshots)).toEqual({ a: tabs('a', 'vim') })
})
it('saves at once only when tabs or groups change; title ticks ride the next save', () => {
const { store, saves } = memoryStore()
const census = (snapshots: unknown[]) => frame({ type: 'snapshots', snapshots })
const update = (result: Record<string, unknown>) => frame({ type: 'updated', ...result })
recordRemoteLayoutFrame(store, 'env', METHOD, census([tabs('a')]))
recordRemoteLayoutFrame(store, 'env', METHOD, update(tabs('a', 'thinking.')))
recordRemoteLayoutFrame(store, 'env', METHOD, update(tabs('a', 'thinking..')))
const twoTabs = {
...tabs('a'),
tabs: [...tabs('a').tabs, { type: 'terminal', id: 'second', title: 'shell' }]
}
recordRemoteLayoutFrame(store, 'env', METHOD, update(twoTabs))
const grouped = {
...twoTabs,
tabGroups: [{ id: 'g', activeTabId: null, tabOrder: ['second'] }]
}
recordRemoteLayoutFrame(store, 'env', METHOD, update(grouped))
recordRemoteLayoutFrame(
store,
'env',
METHOD,
update({ ...grouped, tabGroups: [{ id: 'g', activeTabId: 'second', tabOrder: ['second'] }] })
)
recordRemoteLayoutFrame(store, 'env', METHOD, update({ ...tabs('a'), tabs: [], removed: true }))
expect(saves).toEqual(['now', 'later', 'later', 'now', 'now', 'later', 'now'])
})
it('keeps each server in its own entry', () => {
const { store, snapshots } = memoryStore()
recordRemoteLayoutFrame(
@@ -169,22 +140,4 @@ describe('remote layout snapshot recorder', () => {
recordRemoteLayoutFrame(store, 'env', METHOD, frame({ type: 'updated', worktree: 'a' }))
expect(saves).toEqual([])
})
it('forgets a removed server', () => {
const { store, snapshots } = memoryStore()
recordRemoteLayoutFrame(store, 'env', METHOD, frame({ type: 'snapshots', snapshots: [] }))
forgetRemoteLayoutSnapshot(store, 'env')
expect(snapshots.has('env')).toBe(false)
})
it('is the only code that writes the snapshot', () => {
const src = join(__dirname, '../..')
const writers = scanSourceTree(src)
.filter(({ source }) => stripComments(source).includes('setRemoteLayoutSnapshot('))
.map(({ relativePath }) => relativePath)
expect(writers.sort()).toEqual([
'main/remote-layout-snapshot/remote-layout-snapshot-persistence.ts',
'main/remote-layout-snapshot/remote-layout-snapshot-recorder.ts'
])
})
})
@@ -105,16 +105,6 @@ describe('DesktopBrowserBridgeRegistry', () => {
expect(MAX_PENDING_PER_ATTACHMENT).toBeLessThan(32)
})
it('gives the desktop a deadline that ends before orcad stops waiting', () => {
const registry = new DesktopBrowserBridgeRegistry()
const events: DesktopBrowserBridgeEvent[] = []
registry.attach({ connectionId: 'c', pairedDeviceId: 'd', emit: (event) => events.push(event) })
registry.forward({ method: 'browser.click', params: {} }, { timeoutMs: 60_000 }).catch(() => {})
expect(events[0]).toMatchObject({ type: 'request', timeoutMs: 50_000 })
})
it('tells a cancel-capable desktop to drop a command the CLI cancelled', async () => {
const registry = new DesktopBrowserBridgeRegistry()
const events: DesktopBrowserBridgeEvent[] = []
@@ -71,13 +71,6 @@ describe('a folder whose project group mixes local and SSH repos (layout-owner b
const { snapshot } = runtime.subscribeWorkspaceLayouts(() => {})
expect(snapshot.map((entry) => entry.key)).toEqual([GIT_KEY, FOLDER_KEY])
})
it('applies a layout command for it to the local partition', async () => {
const { runtime, applyLayoutCommand } = runtimeWithMixedFolder()
const reply = await runtime.applyLayoutCommand(RATIOS)
expect(reply.ok).toBe(true)
expect(applyLayoutCommand).toHaveBeenCalledWith(RATIOS, ['local'], {})
})
})
describe('every folder kind routes where the window saves it (layout-owner build)', () => {
@@ -88,15 +81,6 @@ describe('every folder kind routes where the window saves it (layout-owner build
})
})
describe('the same folder in a window-owned build', () => {
it('keeps refusing to route it', () => {
mocks.layoutOwnerIsMain = false
vi.spyOn(console, 'warn').mockImplementation(() => {})
const { runtime } = runtimeWithMixedFolder()
expect(runtime.subscribeWorkspaceLayouts(() => {}).snapshot).toEqual([])
})
})
describe('a workspace the router throws on', () => {
function streamThrowingOn(key: string) {
const session = localDesktopSession()
@@ -151,21 +135,6 @@ describe('a workspace the router throws on', () => {
)
})
it('a routing warning never hides a later listener failure', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const { stream, route, write } = streamThrowingOn(FOLDER_KEY)
stream.subscribe(() => {
throw new Error('listener failed')
})
route.throws = false
write()
await flush()
expect(warn.mock.calls.map((call) => call[0])).toEqual([
'[workspace-layout] layout stream routing failed:',
'[workspace-layout] layout stream listener failed:'
])
})
it('still fails the whole projection in a window-owned build', () => {
mocks.layoutOwnerIsMain = false
vi.spyOn(console, 'warn').mockImplementation(() => {})
@@ -1,4 +1,4 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { describe, expect, it, vi } from 'vitest'
import type { StructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger'
const installed = vi.hoisted(() => {
@@ -26,142 +26,6 @@ vi.mock('./agent-session/structured-agent-session-runtime', () => ({
}))
import { OrcaRuntimeService } from './orca-runtime'
import { _resetTracerForTests, setActiveSink } from '../observability/tracer'
import type { StructuredAgentSessionStatusSink } from '../native-chat/agent-session-wire/structured-agent-session-status-feed'
import * as structuredMailTarget from './orchestration/structured-session-mail-target'
import { OrcaRuntimeWithRuntimeId } from './construction/runtime-identity-state'
describe('structured status sink wiring', () => {
afterEach(() => {
_resetTracerForTests()
vi.restoreAllMocks()
})
it('hands the host the sink the runtime was constructed with', async () => {
installed.deps = null
const sink: StructuredAgentSessionStatusSink = { publish: vi.fn(), forget: vi.fn() }
const runtime = new OrcaRuntimeService(null, undefined, { structuredAgentStatusSink: sink })
await runtime.ensureStructuredAgentSessionHost()
expect(installed.deps?.['statusSink']).toBe(sink)
})
it('hands the host the trace-file logger', async () => {
installed.logger = null
const push = vi.fn()
setActiveSink({ push, flush: () => {}, close: () => {} })
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const runtime = new OrcaRuntimeService()
await runtime.ensureStructuredAgentSessionHost()
const installedLogger = (): StructuredAgentSessionLogger | null => installed.logger
installedLogger()?.warn('renewing a chat lease failed', {
scope: 'lease-renewal',
sessionId: 'session-1',
error: new Error('database is locked')
})
expect(push).toHaveBeenCalledWith(
expect.objectContaining({
name: 'agentSession.lease-renewal',
attributes: expect.objectContaining({ sessionId: 'session-1' }),
exit: expect.objectContaining({ _tag: 'Failure' })
})
)
})
it('installs without a sink when none was provided', async () => {
installed.deps = null
const runtime = new OrcaRuntimeService()
await runtime.ensureStructuredAgentSessionHost()
expect(installed.deps).not.toBeNull()
expect('statusSink' in (installed.deps ?? {})).toBe(false)
})
})
describe('mail dispatch through the current runtime', () => {
afterEach(() => vi.restoreAllMocks())
it('uses runtime journal and delivery overrides on the idle edge', () => {
const target = new OrcaRuntimeService()
vi.spyOn(structuredMailTarget, 'structuredSessionIdleEdgeMailboxes').mockReturnValue([
'term_mailbox'
])
const events: string[] = []
Object.defineProperties(target, {
notifyStructuredSessionJournalActivity: {
value: function (this: OrcaRuntimeService, sessionId: string) {
expect(this).toBe(target)
expect(arguments.length).toBe(1)
events.push(`journal:${sessionId}`)
}
},
deliverPendingMessagesForHandle: {
value: function (this: OrcaRuntimeService, handle: string) {
expect(this).toBe(target)
expect(arguments.length).toBe(1)
events.push(`delivery:${handle}`)
}
}
})
target.onStructuredSessionStatusForMail({ sessionId: 'session-mailbox', status: 'working' })
expect(events).toEqual(['journal:session-mailbox'])
events.length = 0
target.onStructuredSessionStatusForMail({ sessionId: 'session-mailbox', status: 'idle' })
expect(events).toEqual(['journal:session-mailbox', 'delivery:term_mailbox'])
})
it('uses the runtime delivery override during repoint repair', () => {
const target = new OrcaRuntimeService()
const delivery = vi.fn(function (this: OrcaRuntimeService, handle: string) {
expect(this).toBe(target)
expect(arguments.length).toBe(1)
expect(handle).toBe('term_mailbox')
})
Object.defineProperty(target, 'deliverPendingMessagesForHandle', { value: delivery })
target['repointPendingMessagesForHandle']('term_mailbox')
expect(delivery).toHaveBeenCalledOnce()
})
it('resolves a mailbox target through the runtime custody override', () => {
const target = new OrcaRuntimeService()
Object.defineProperty(target, 'structuredMailboxCustody', {
value: function (this: OrcaRuntimeService, handle: string) {
expect(this).toBe(target)
expect(arguments.length).toBe(1)
return handle === 'run:run-mailbox'
? { sessionId: 'coordinator-session', dispatchId: null }
: 'elsewhere'
}
})
expect(target['resolveStructuredMailboxTarget']('run:run-mailbox')).toEqual({
sessionId: 'coordinator-session',
dispatchId: null
})
expect(target['resolveStructuredMailboxTarget']('term_mailbox')).toBeNull()
})
})
abstract class MailSchedulerPhaseProbe extends OrcaRuntimeWithRuntimeId {
readonly pendingMailDuringConstruction = this.mailPointerRepointScheduler.pendingCount
}
describe('input state before later runtime layers initialize', () => {
it('makes the scheduler available immediately after the original base layer', () => {
expect(
Object.getOwnPropertyDescriptor(
Reflect.construct(MailSchedulerPhaseProbe, []),
'pendingMailDuringConstruction'
)?.value
).toBe(0)
})
})
describe('checked runtime construction', () => {
it('installs subscription callbacks on the runtime with their real registry receiver', () => {
@@ -173,17 +37,4 @@ describe('checked runtime construction', () => {
runtime.cleanupSubscription('construction-subscription')
expect(cleanup).toHaveBeenCalledOnce()
})
it('keeps constructor callbacks lazy and the canonical status producer identity', () => {
const snapshot = vi.fn(() => [])
const stopped = vi.fn()
const runtime = new OrcaRuntimeService(null, undefined, {
getAgentStatusSnapshot: snapshot,
onPtyStopped: stopped
})
expect(runtime.getAgentStatusSnapshotFn).toBe(snapshot)
expect(runtime['onPtyStopped']).toBe(stopped)
expect(snapshot).not.toHaveBeenCalled()
expect(stopped).not.toHaveBeenCalled()
})
})
@@ -1,4 +1,3 @@
/** Chat and terminal agents must receive equivalent preambles and pointers, apart from address and chat wording. */
import { afterAll, afterEach, describe, expect, it, vi } from 'vitest'
import {
getAppEnvironment,
@@ -6,8 +5,7 @@ import {
setAppEnvironment,
type AppEnvironment
} from '../../../shared/app-environment'
import type { OrcaRuntimeService } from '../orca-runtime'
import { deliverWorkerDispatchPreamble } from '../rpc/methods/orchestration/worker/deliver-worker-dispatch-preamble'
import {
localOrchestrationCliCommand,
resolveTerminalOrchestrationCliCommand,
@@ -16,30 +14,15 @@ import {
} from './cli-command'
import { OrchestrationDb } from './db'
import { OrchestrationStructuredMailboxPointerDelivery } from './structured-mailbox-pointer-delivery'
import {
CHAT_REDISPATCH_PARAGRAPH as CHAT_REDISPATCH,
TERMINAL_REDISPATCH_PARAGRAPH as TERMINAL_REDISPATCH
} from './preamble'
const sent = vi.hoisted((): { preambles: string[] } => ({ preambles: [] }))
vi.mock('../rpc/methods/orchestration-structured-worker-session', () => ({
sendStructuredWorkerPreamble: async (args: { preamble: string }) => {
sent.preambles.push(args.preamble)
}
}))
const CHAT_SESSION = '4a1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37'
const CHAT_ADDRESS = `orca_session_id:${CHAT_SESSION}`
const TERMINAL_HANDLE = 'term_worker'
// The structured worker's hidden mailbox handle differs from its visible session address.
const CHAT_WORKER_HANDLE = 'structworker_1'
const db = new OrchestrationDb(':memory:')
const SELF_LINE = `\nYour Orca session ID is: ${CHAT_ADDRESS}`
const previousEnvironment = hasAppEnvironment() ? getAppEnvironment() : null
afterEach(() => {
sent.preambles = []
if (previousEnvironment) {
setAppEnvironment(previousEnvironment)
}
@@ -61,52 +44,6 @@ function installApp(isPackaged: boolean): void {
} satisfies AppEnvironment)
}
function runtime(prompts: string[]): OrcaRuntimeService {
const fake: Pick<
OrcaRuntimeService,
'getNestedWorkerMaxDepth' | 'getTerminalOrchestrationCliCommand' | 'sendTerminalAgentPrompt'
> & {
orchestrationSenderNames: Pick<OrcaRuntimeService['orchestrationSenderNames'], 'snapshotOf'>
} = {
orchestrationSenderNames: { snapshotOf: () => ({ name: null, agent: null }) },
getNestedWorkerMaxDepth: () => 2,
getTerminalOrchestrationCliCommand: () => 'orca',
sendTerminalAgentPrompt: async (handle, text) => {
prompts.push(text)
return { handle, accepted: true, bytesWritten: text.length }
}
}
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: preamble delivery reads only the members the fake implements.
return fake as OrcaRuntimeService
}
type StructuredSession = Parameters<typeof deliverWorkerDispatchPreamble>[0]['structuredSession']
function structuredSession(): StructuredSession {
const session = { host: {}, identity: { sessionId: CHAT_SESSION, agent: 'codex' } }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: delivery reads only identity.sessionId, and the mocked send ignores host.
return session as unknown as StructuredSession
}
async function renderPreamble(worker: 'chat' | 'terminal'): Promise<string> {
const prompts: string[] = []
await deliverWorkerDispatchPreamble({
runtime: runtime(prompts),
db,
structuredSession: worker === 'chat' ? structuredSession() : null,
terminalHandle: worker === 'chat' ? CHAT_WORKER_HANDLE : TERMINAL_HANDLE,
dispatchId: 'ctx_1',
dispatchDepth: 1,
runId: 'run_1',
taskId: 'task_1',
taskSpec: 'do it',
coordinatorHandle: 'term_coord',
devMode: false,
requestId: 'req_1'
})
return worker === 'chat' ? sent.preambles[0]! : prompts[0]!
}
/** Render what the structured lane hands a chat for one mailbox message. */
async function renderChatMail(mailbox: string): Promise<{ text: string; id: string }> {
const texts: string[] = []
@@ -138,27 +75,6 @@ async function renderChatMail(mailbox: string): Promise<{ text: string; id: stri
}
describe('a chat agent and a terminal agent see the same text but for how each is named and called', () => {
it("teaches a chat worker a terminal worker's preamble but for its identity lines and the chat wording", async () => {
const chat = await renderPreamble('chat')
const terminal = await renderPreamble('terminal')
const withoutRedispatch = chat.replace(CHAT_REDISPATCH, TERMINAL_REDISPATCH)
expect(terminal).not.toContain('Orca session ID')
expect(chat).toContain(`Your task ID is: task_1${SELF_LINE}\n`)
expect(chat).not.toContain(CHAT_WORKER_HANDLE)
expect(chat).not.toContain('this terminal')
expect(withoutRedispatch).not.toBe(chat)
expect(withoutRedispatch.split('this chat')).toHaveLength(2)
expect(
withoutRedispatch
.replace(SELF_LINE, '')
.split(CHAT_ADDRESS)
.join(TERMINAL_HANDLE)
.split('this chat')
.join('this terminal')
).toBe(terminal)
})
it.each([
['a packaged app', true],
['a dev build', false]
@@ -62,7 +62,6 @@ describe('opening a terminal worker Orca closed', () => {
}
const open = (address: string) => h.call('orchestration.partyOpen', { address })
const locate = (address: string) => h.call('orchestration.partyLocation', { address })
it('resumes its conversation in a new terminal, and a click after it reports reuses it', async () => {
const { resource, resume, report } = await closedHelper()
@@ -83,18 +82,6 @@ describe('opening a terminal worker Orca closed', () => {
)
})
it('keeps the lookup answer without a saved conversation', async () => {
const { resource, resume } = await closedHelper(false)
expect(await open(resource.terminal_handle)).toEqual(await locate(resource.terminal_handle))
expect(resume).not.toHaveBeenCalled()
})
it('keeps the lookup answer when the conversation was recorded on another host', async () => {
const { resource, resume } = await closedHelper()
resume.mockResolvedValue({ ok: false, reason: 'no_resume_command' })
expect(await open(resource.terminal_handle)).toEqual(await locate(resource.terminal_handle))
})
it('sends a later follow-up into the reopened terminal, not a second one', async () => {
const { helper, resource, resume, report } = await closedHelper()
await open(resource.terminal_handle)
@@ -1,8 +1,6 @@
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { createOrchestrationWorkerReleaseHarness } from './worker-release.test-support'
import { TERMINAL_SEND_METHODS } from '../../terminal/terminal-send-method'
import { sendTerminalStreamInput } from '../../terminal/terminal-input-delivery'
import { eraseRpcMethods, isStreamingMethod, type RpcMethod } from '../../../core'
import { registerBoundPty } from '../../../../bind-first-pane.test-fixture'
const h = createOrchestrationWorkerReleaseHarness()
@@ -61,60 +59,6 @@ it('an unknown handle does not fence another worker or access the database', asy
).resolves.toMatchObject({ state: 'released' })
})
it.each(['unary', 'stream'])('mobile %s bytes do no orchestration database work', async (lane) => {
const worker = await h.startSettledWorker()
const runtime = h.runtime
runtime.registerPreAllocatedHandleForPty('pty-worker', 'term_worker')
registerBoundPty(runtime, 'pty-worker', 'repo::worktree', null, {
tabId: 'tab_worker',
leafId: 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb',
incarnationId: 'runtime_test:term_worker:1'
})
const write = vi.fn(() => true)
runtime.setPtyController({ write, kill: () => true, getForegroundProcess: async () => null })
const commit = vi.fn(async () => {})
vi.spyOn(runtime, 'beginMobileInputFloor').mockReturnValue({ commit, rollback: vi.fn() })
const dbAccess = vi.spyOn(runtime, 'getOrchestrationDb')
const prepare = vi.spyOn(h.db.db, 'prepare')
const exec = vi.spyOn(h.db.db, 'exec')
const params = {
terminal: 'term_worker',
text: 'x',
client: { id: 'phone', type: 'mobile' as const }
}
if (lane === 'stream') {
await expect(sendTerminalStreamInput(runtime, { ...params, isMobile: true })).resolves.toBe(
'delivered'
)
} else {
const method = eraseRpcMethods(TERMINAL_SEND_METHODS).find(
(m): m is RpcMethod => m.name === 'terminal.send' && !isStreamingMethod(m)
)!
await expect(
method.handler(method.params!.parse(params) as never, { runtime } as never)
).resolves.toMatchObject({ send: { accepted: true } })
}
expect(write).toHaveBeenCalledWith('pty-worker', 'x', 'driving')
expect(commit).toHaveBeenCalledTimes(1)
expect(dbAccess).not.toHaveBeenCalled()
expect(prepare).not.toHaveBeenCalled()
expect(exec).not.toHaveBeenCalled()
expect(h.db.getWorkerTerminalResourceByOwner(worker.dispatchId)?.ownership_state).toBe('owned')
})
it('Keep open and the countdown are reachable from a mobile-scoped device token', async () => {
// An older phone's typing report must keep resolving too, or it would surface errors.
const { MOBILE_RPC_METHOD_ALLOWLIST } =
await import('../../../../runtime-rpc/runtime-rpc-mobile-method-allowlist')
for (const method of [
'orchestration.workerAutoClose',
'orchestration.workerKeepOpen',
'orchestration.workerTerminalUserInput'
]) {
expect(MOBILE_RPC_METHOD_ALLOWLIST.has(method)).toBe(true)
}
})
// Boot-wait Keep open must find custody written at terminal creation, or a later release closes a kept pane (#19337, #19608).
it('a phone Keep open during the boot wait keeps the pane and fences the later release', async () => {
const gate = h.deferred<unknown>()
@@ -143,39 +143,4 @@ describe('stored reference lookup', () => {
expect(result.matches[1].agents[0].terminal).toBe('remote-handle')
expect(result.matches[1].workspace.hostId).toBe('ssh:build')
})
it('handles 1000 workspaces with 23 references each without discovering agents for misses', async () => {
const workspaces = Array.from({ length: 1000 }, (_, i) =>
workspace(
String(i),
Array.from({ length: 23 }, (_, n) => `https://github.com/acme/api/pull/${i * 23 + n + 1}`)
)
)
const agents = vi.fn(() => new Map())
const source = { workspaces, agents }
expect((await findWorkspaceReferences({ query: 'MISSING-1' }, source)).matches).toEqual([])
expect(agents).not.toHaveBeenCalled()
expect(
(await findWorkspaceReferences({ query: 'https://github.com/acme/api/pull/23000' }, source))
.matches[0].workspace.id
).toBe('999')
expect(agents).toHaveBeenCalledTimes(1)
})
it.each([0, -1, 1.5, Infinity])('rejects invalid limits %s', async (limit) => {
await expect(
findWorkspaceReferences({ query: linear, limit }, { workspaces: [], agents: () => new Map() })
).rejects.toThrow('positive integer')
})
it('rejects bare numbers and conflicting filters before inspecting agents', async () => {
const source = { workspaces: [], agents: vi.fn(() => new Map()) }
await expect(findWorkspaceReferences({ query: '5123' }, source)).rejects.toThrow(
'full reference URL'
)
await expect(
findWorkspaceReferences({ query: linear, worktree: 'one', repo: 'api' }, source)
).rejects.toThrow('either --worktree or --repo')
expect(source.agents).not.toHaveBeenCalled()
})
})
@@ -92,17 +92,6 @@ describe("a phone's layout change on a windowless server (switch on)", () => {
)
})
it("sends nothing for a group's last tab dropped on its neighbour's facing edge", async () => {
const { runtime } = await headlessRuntime()
const split = { kind: 'split', tabId: SECOND_TAB_ID, targetGroupId: SAVED_GROUP_ID } as const
await runtime.moveMobileSessionTab(WORKTREE, { ...split, splitDirection: 'right' })
const apply = vi.spyOn(runtime, 'applyLayoutCommand')
await runtime.moveMobileSessionTab(WORKTREE, { ...split, splitDirection: 'right' })
expect(apply).not.toHaveBeenCalled()
})
it("answers a split main refuses with main's refusal code", async () => {
const { runtime } = await headlessRuntime()
vi.spyOn(runtime, 'applyLayoutCommand').mockResolvedValue({ ok: false, code: 'invalid_params' })
@@ -17,12 +17,6 @@ describe('terminal run facts', () => {
facts.recordSpawnCommit({ id: 'pending', incarnationId: 'inc-3' })
expect(facts.read('pending', 'inc-3').firstUserInputAt).toBeNull()
})
it('reads a run main never saw committed as not fresh', () => {
expect(new TerminalRunFactsRegister().read('pty-1', 'inc-1')).toEqual({
freshSpawn: false,
firstUserInputAt: null
})
})
it('keeps the first user input across later input and a re-registration of the same process', () => {
const facts = new TerminalRunFactsRegister()
@@ -72,14 +66,6 @@ describe('terminal run facts', () => {
expect(facts.readProcessStartedAt('pty-2', 'inc-9')).toBeNull()
})
it('never reads a reattached process as fresh', () => {
const facts = new TerminalRunFactsRegister()
facts.recordSpawnCommit({ id: 'pty-1', incarnationId: 'inc-1', isReattach: true })
expect(facts.read('pty-1', 'inc-1').freshSpawn).toBe(false)
})
it('starts clean when a new process commits without an incarnation', () => {
const facts = new TerminalRunFactsRegister()
facts.recordSpawnCommit({ id: 'pty-1' })
@@ -105,19 +105,6 @@ for (const host of ['no window', 'a window'] as const) {
expect(await phoneShowsPane(harness.runtime, HEADLESS_SECOND_LEAF_ID)).toBe(false)
})
it(`the exit drops the closed-pane record in its one retirement write: ${route}, with ${host}`, async () => {
const harness = closingHost(host)
await close(harness.runtime)
// The fixture's setter may already be a mock carrying the close's own writes.
const writes = vi.spyOn(harness.runtime['store']!, 'setWorkspaceSession')
writes.mockClear()
harness.runtime.onPtyExit('pty-b', 0, 'inc-b')
expect(writes).toHaveBeenCalledTimes(1)
expect(writes.mock.calls[0]![0].closedTerminalPanesByPaneKey?.[PANE_B]).toBeUndefined()
})
it(`exit then close retires them too: ${route}, with ${host}`, async () => {
const harness = closingHost(host)
@@ -1,183 +0,0 @@
import '../../orca-runtime-test-mocks.spec'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { expect, it, vi } from 'vitest'
import { OrcaRuntimeService } from '../../orca-runtime'
import { OrchestrationDb } from '../../orchestration/db'
import {
openTopologyStoreWithAuthority,
emptyTerminalSessionProfile,
FIXTURE_GIT_WORKTREE_ID
} from '../../../persistence/terminal-topology/terminal-topology-profile-fixture'
import { WindowSession } from '../../../persistence/terminal-topology/terminal-topology-window-session-fixture'
import { getDefaultWorkspaceSession } from '../../../../shared/constants'
import * as loader from '../../../../shared/workspace-layout/workspace-layout-load'
import * as paneQuery from './pty-pane-query'
import * as surfaceIndex from './terminal-surface-index'
import type { ResolvedWorktree } from '../../runtime-worktree-path-identity'
vi.mock('../../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
const leaf = (i: number) => `aaaaaaaa-aaaa-4aaa-8aaa-${String(i).padStart(12, '0')}`
const pane = `tab-main-0:${leaf(0)}`
class DefaultPaneRuntime extends OrcaRuntimeService {
protected override async getResolvedWorktreeMap(): Promise<Map<string, ResolvedWorktree>> {
const worktree = this.buildResolvedWorktreeFromId(FIXTURE_GIT_WORKTREE_ID)
return new Map(worktree ? [[worktree.id, worktree]] : [])
}
captures = 0
protected captureTerminalPaneReader(): paneQuery.PtyPaneOf {
this.captures++
return super.captureTerminalPaneReader()
}
seed(id: string, handle: string, spawnPaneKey?: string, connected = true): void {
this.recordPtyWorktree(id, FIXTURE_GIT_WORKTREE_ID, { connected, spawnPaneKey })
this.terminalIdentity.bindPty(id, handle)
this.terminalIdentity.setHandle(handle, {
handle,
runtimeId: this.runtimeId,
rendererGraphEpoch: this.rendererGraphEpoch,
worktreeId: FIXTURE_GIT_WORKTREE_ID,
tabId: `pty:${id}`,
leafId: `pty:${id}`,
ptyId: id,
ptyGeneration: 0
})
}
}
async function fixture(population: number) {
const directory = mkdtempSync(join(tmpdir(), 'orca-pr34890-finite-'))
const { store } = await openTopologyStoreWithAuthority(directory, emptyTerminalSessionProfile())
const db = new OrchestrationDb(':memory:')
const saved = new WindowSession(getDefaultWorkspaceSession())
for (let i = 0; i < population; i++) {
saved.addTab(FIXTURE_GIT_WORKTREE_ID, `tab-main-${i}`, leaf(i))
saved.bind(`tab-main-${i}`, leaf(i), `pty-main-${i}`)
}
store.setWorkspaceSession(saved.snapshot(), 'local')
await store.flushPendingOrThrowAsync()
const runtime = new DefaultPaneRuntime(store)
runtime.setOrchestrationDb(db)
runtime.seed('coord', 'term_coord')
for (let i = 0; i < population; i++) {
runtime.seed(`pty-main-${i}`, `term_main_${i}`)
}
runtime.seed('replacement', 'term_replacement')
const run = db.createRun({
objective: 'Finite acquisition',
coordinatorHandle: 'term_coord',
coordinatorPaneKey: 'coord-pane'
})
const task = db.createTask({ runId: run.id, spec: 'Main worker' })
db.createDispatchContext({
taskId: task.id,
assigneeHandle: 'term_missing_stored',
assigneePaneKey: pane,
creator: { kind: 'system' },
maxDepth: 10
})
const stop = runtime.watchOrchestrationCrew(() => {})
const rebind = async () => {
const next = structuredClone(store.getWorkspaceSession())
next.terminalLayoutsByTabId['tab-main-0'].ptyIdsByLeafId = { [leaf(0)]: 'replacement' }
store.setWorkspaceSession(next, 'local')
await store.flushPendingOrThrowAsync()
}
return {
runtime,
rebind,
close: async () => {
stop()
db.close()
await store.freezeWritesAsync()
rmSync(directory, { recursive: true, force: true })
}
}
}
it('bounds the real public pane lookup and captured Crew read at N and 10N', async () => {
vi.useFakeTimers({ toFake: ['Date'] })
vi.setSystemTime(new Date('2026-10-29T12:00:00Z'))
try {
for (const population of [3, 30]) {
const f = await fixture(population)
try {
const loads = vi.spyOn(loader, 'loadWorkspaceLayoutWithoutReport')
const capture = vi.spyOn(paneQuery, 'captureMainTerminalPaneLookup')
const qualify = vi.spyOn(surfaceIndex, 'terminalHoldsPane')
const measure = (read: () => unknown) => {
loads.mockClear()
capture.mockClear()
qualify.mockClear()
const value = read()
return {
value,
loads: loads.mock.calls.length,
surfaceCandidates: capture.mock.calls.reduce(
(sum, [surfaces]) => sum + surfaces.leaves.length,
0
),
factCopies: capture.mock.calls.reduce(
(sum, [surfaces]) => sum + surfaces.paneFacts.size,
0
),
qualificationCalls: qualify.mock.calls.length
}
}
expect(f.runtime.getTerminalHandleForPaneKey(pane)).toBe('term_main_0')
f.runtime.getOrchestrationCrewSnapshot('term_coord')
await f.rebind()
const next = measure(() => f.runtime.getTerminalHandleForPaneKey(pane))
expect(next.value).toBe('term_replacement')
expect(next.loads).toBe(2)
expect(next.surfaceCandidates).toBe(population)
expect(next.factCopies).toBe(population)
expect(next.qualificationCalls).toBeLessThanOrEqual(population + 3)
const crew = measure(() => f.runtime.getOrchestrationCrewSnapshot('term_coord'))
expect(crew.loads).toBe(1)
expect(crew.surfaceCandidates).toBe(population)
loads.mockRestore()
capture.mockRestore()
qualify.mockRestore()
} finally {
await f.close()
}
}
} finally {
vi.useRealTimers()
}
})
it('keeps absent and exact/reminted connected public points outside global capture', () => {
const runtime = new DefaultPaneRuntime(null)
expect(runtime.getTerminalHandleForPaneKey(pane)).toBeNull()
expect(runtime.captures).toBe(0)
runtime.seed('fast', 'term_fast')
runtime.attachWindow(1)
runtime.syncWindowGraph(1, {
tabs: [
{
tabId: 'tab-main-0',
worktreeId: FIXTURE_GIT_WORKTREE_ID,
title: 'Terminal',
activeLeafId: leaf(0),
layout: null
}
],
leaves: [
{
tabId: 'tab-main-0',
worktreeId: FIXTURE_GIT_WORKTREE_ID,
leafId: leaf(0),
ptyId: 'fast',
paneRuntimeId: 1
}
]
})
const before = runtime.captures
expect(runtime.getTerminalHandleForPaneKey(pane)).toBe('term_fast')
expect(runtime.getTerminalHandleForPaneKey(`tab-moved:${leaf(0)}`)).toBe('term_fast')
expect(runtime.captures).toBe(before)
})
@@ -14,7 +14,7 @@ import { installSharedSkill } from './skill-install-service'
import { createSkillBundleArchive } from './skill-bundle-creation'
import { installSkillBundle } from './skill-bundle-install-service'
import { writeSkillTarGzip } from './skill-package-tar'
import { detectSkillProvidersInWsl } from './skill-wsl-provider-detection'
import { createWslSkillInstallFilesystem } from './skill-wsl-install-filesystem'
const execFileAsync = promisify(execFile)
@@ -66,10 +66,6 @@ describe.runIf(RUN_REAL_WSL)('real WSL POSIX skill semantics', () => {
}
})
it('detects providers from the selected distro', async () => {
await expect(detectSkillProvidersInWsl(DISTRO)).resolves.toContain('codex')
})
it('preserves case and applies owner-private executable modes', async () => {
const markdown = Buffer.from(
'---\nname: wsl-mode-skill\ndescription: WSL modes\n---\n\n# WSL\n'
@@ -119,12 +119,6 @@ describe('orcad migration blob sender', () => {
).rejects.toThrow('orcad_migration_blob_descriptor_conflict:terminal-restore-hints')
expect(destination.stageChunk).not.toHaveBeenCalled()
})
it('sends nothing and asks nothing when there are no blobs', async () => {
const destination = realDestination([])
await transferOrcadMigrationBlobs({ source: source(), manifest, blobs: [], destination })
expect(destination.readCapabilities).not.toHaveBeenCalled()
})
})
function descriptor(
@@ -92,16 +92,6 @@ it('attaches the alert to the visible main window and explains an unconfirmed wr
expect(recordProfileStateWriteFailureReport).toHaveBeenCalledWith(workerExit, 'dialog')
})
it('tells the user saved changes are safe when no write was left unresolved', async () => {
fixture.state.mainWindow = new FakeWindow(true)
reportProfileStateWriteFailure(conflict)
await vi.waitFor(() => expect(fixture.show).toHaveBeenCalledOnce())
expect(fixture.show.mock.calls[0][1]).toMatchObject({
detail:
'Changes saved before this point are safe. New changes will not be saved until you restart Orca.'
})
})
it('waits for a hidden window to be shown by the user instead of revealing it', async () => {
const window = new FakeWindow(false)
fixture.state.mainWindow = window
@@ -100,39 +100,6 @@ describe('generateBranchNameFromContext', () => {
})
})
it('does not persist stdout-only branch failure detail that may echo the prompt', async () => {
const result = await generateBranchNameFromContext(
{ firstPrompt: 'Customer secret in the first prompt' },
{
agentId: 'custom',
model: '',
customAgentCommand: 'agent'
},
{
kind: 'remote',
cwd: '/repo',
missingBinaryLocation: 'remote PATH',
execute: async () => ({
stdout: 'Customer secret in the first prompt',
stderr: '',
exitCode: 1,
timedOut: false
})
}
)
expect(result).toMatchObject({
success: false,
error: 'agent CLI command failed with code 1.',
failureOutput: {
label: 'agent',
exitCode: 1,
stdout: 'Customer secret in the first prompt',
stderr: ''
}
})
})
it('describes a signal-terminated generator without a null exit code', async () => {
const result = await generateBranchNameFromContext(
{ firstPrompt: 'Fix login flow' },
@@ -159,40 +126,6 @@ describe('generateBranchNameFromContext', () => {
})
})
it('keeps branch-name guidance first without dropping the output contract', async () => {
let prompt = ''
await generateBranchNameFromContext(
{ firstPrompt: 'Fix login flow' },
{
agentId: 'custom',
model: '',
customAgentCommand: 'agent',
customPrompt: 'Prefer auth terminology.',
commandInputTemplate: 'Prefer auth terminology.\n\n{basePrompt}'
},
{
kind: 'remote',
cwd: '/repo',
missingBinaryLocation: 'remote PATH',
execute: async (plan) => {
prompt = plan.stdinPayload ?? ''
return {
stdout: 'fix-login-flow\n',
stderr: '',
exitCode: 0,
timedOut: false
}
}
}
)
expect(prompt.startsWith('Prefer auth terminology.')).toBe(true)
expect(prompt).toContain('Prefer auth terminology.')
expect(prompt).not.toContain('Additional user prompt:')
expect(prompt).toContain('Generate a short git branch name')
expect(prompt).toContain('Output ONLY the branch name on a single line')
})
it('reruns Copilot once on its default model when the plan hides the saved model', async () => {
const calls: string[][] = []
const result = await generateBranchNameFromContext(
@@ -139,21 +139,6 @@ describe('updater linux package recovery IPC handlers', () => {
vi.unstubAllEnvs()
})
it('removes and re-registers both recovery channels', () => {
for (const channel of RECOVERY_CHANNELS) {
expect(removeHandlerMock).toHaveBeenCalledWith(channel)
expect(handleMock.mock.calls.filter(([name]) => name === channel)).toHaveLength(1)
}
})
it('routes both channels through the trusted-renderer guard', () => {
isTrustedUIRendererMock.mockReturnValue(false)
expectBothChannelsRejected(webContents({}))
expect(isTrustedUIRendererMock).toHaveBeenCalledTimes(2)
})
it('serves the current main UI renderer', async () => {
const event = senderEvent(webContents({}))
-51
View File
@@ -64,19 +64,6 @@ describe('terminatePtyJob', () => {
expect(terminatePtyJob(ptyWithHandle(id))).toBe('unavailable')
})
it('passes the shell pid so an id from another backend cannot match', () => {
// winpty/ConPTY IDs can collide; always send shell PID so native code can refuse mismatches.
const terminateJob = vi.fn().mockReturnValue(true)
__setConptyJobNativeForTests(() => ({
terminateJob,
listJobProcessIds: vi.fn(),
assignCurrentProcessToJob: vi.fn().mockReturnValue(true)
}))
terminatePtyJob(ptyWithHandle(3, 9001))
expect(terminateJob).toHaveBeenCalledWith(3, 9001)
})
it('reports unavailable when the shell pid is unusable', () => {
__setConptyJobNativeForTests(() => ({
terminateJob: vi.fn().mockReturnValue(true),
@@ -106,16 +93,6 @@ describe('terminatePtyJob', () => {
})
describe('listPtyJobProcessIds', () => {
it('returns the live pids, including a detached grandchild', () => {
// Detached/reparented grandchildren escape console and parent-PID walks but remain job members.
__setConptyJobNativeForTests(() => ({
terminateJob: vi.fn(),
listJobProcessIds: vi.fn().mockReturnValue([107184, 91480]),
assignCurrentProcessToJob: vi.fn().mockReturnValue(true)
}))
expect(listPtyJobProcessIds(ptyWithHandle(1))).toEqual([107184, 91480])
})
it('reports null when there is no answer to give', () => {
// Null is unverifiable, including after shell exit closes the job; it never proves death.
__setConptyJobNativeForTests(() => null)
@@ -131,16 +108,6 @@ describe('listPtyJobProcessIds', () => {
})
describe('assignHostProcessToKillOnCloseJob', () => {
it('reports whether the host process is now kill-on-close', () => {
const assignCurrentProcessToJob = vi.fn().mockReturnValue(true)
__setConptyJobNativeForTests(() => ({
terminateJob: vi.fn(),
listJobProcessIds: vi.fn(),
assignCurrentProcessToJob
}))
expect(assignHostProcessToKillOnCloseJob()).toBe(true)
})
it('degrades to false when the OS refuses the assignment', () => {
// An outer job that forbids nesting; orphan behaviour is then simply what
// it was before, rather than a crash.
@@ -165,24 +132,6 @@ describe('PTY Job Object teardown breadcrumbs', () => {
}))
}
it('records the shell pid whose job it terminated', () => {
withNative(() => true)
expect(terminatePtyJob(ptyWithHandle(7, 4242))).toBe('terminated')
expect(recordSelfInitiatedTreeKillMock).toHaveBeenCalledWith({
pid: 4242,
site: 'windows-pty-job-teardown',
scope: 'win-pty-job'
})
})
it('records nothing when the native side refused', () => {
withNative(() => false)
expect(terminatePtyJob(ptyWithHandle(7))).toBe('unavailable')
expect(recordSelfInitiatedTreeKillMock).not.toHaveBeenCalled()
})
it('never downgrades a real termination to unavailable because diagnostics failed', () => {
// Breadcrumb failure must not downgrade termination to unavailable and escalate taskkill.
withNative(() => true)
@@ -52,16 +52,6 @@ beforeEach(() => {
})
describe('isWorkspaceNotificationMutedBySidebarFilters', () => {
it('mutes a workspace a Filters switch hides while the setting is off', () => {
expect(muted(CLI_ID)).toBe(true)
expect(muted(PLAIN_ID)).toBe(false)
})
it('mutes nothing when Notify about hidden workspaces is on', () => {
setNotifyHiddenWorkspaces(true)
expect(muted(CLI_ID)).toBe(false)
})
it('fails open before settings or UI state hydrate, and for an unknown workspace', () => {
expect(muted('repo1::/tmp/unknown')).toBe(false)
store.setState({ persistedUIReady: false })
@@ -133,11 +123,6 @@ describe('isWorkspaceNotificationMutedBySidebarFilters', () => {
store.setState({ hideWorkspacesFromOtherDevices: true })
expect(muted(folderWorkspaceKey('f1'))).toBe(true)
})
it('ignores the project filter, which is not a Filters switch', () => {
store.setState({ hideCliCreatedWorkspaces: false, filterRepoIds: ['other-repo'] })
expect(muted(PLAIN_ID)).toBe(false)
})
})
describe('muted workspace attention', () => {
@@ -1,223 +0,0 @@
import type * as ReactModule from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const reactHookRuntime = vi.hoisted(() => ({
states: [] as unknown[],
index: 0
}))
vi.mock('react', async () => {
const actual = await vi.importActual<typeof ReactModule>('react')
return {
...actual,
useMemo<T>(factory: () => T) {
return factory()
},
useCallback<T extends (...args: never[]) => unknown>(callback: T) {
return callback
},
useEffect() {
return undefined
},
useContext() {
return false
},
useRef<T>(initial: T) {
return { current: initial }
},
useState<T>(initial: T | (() => T)) {
const stateIndex = reactHookRuntime.index++
if (!(stateIndex in reactHookRuntime.states)) {
reactHookRuntime.states[stateIndex] =
typeof initial === 'function' ? (initial as () => T)() : initial
}
const setState = (next: T | ((previous: T) => T)): void => {
reactHookRuntime.states[stateIndex] =
typeof next === 'function'
? (next as (previous: T) => T)(reactHookRuntime.states[stateIndex] as T)
: next
}
return [reactHookRuntime.states[stateIndex] as T, setState] as const
}
}
})
vi.mock('lucide-react', () => ({
Image: function Image(props: Record<string, unknown>) {
return { type: 'Image', props }
},
RotateCcw: function RotateCcw(props: Record<string, unknown>) {
return { type: 'RotateCcw', props }
},
X: function X(props: Record<string, unknown>) {
return { type: 'X', props }
},
ZoomIn: function ZoomIn(props: Record<string, unknown>) {
return { type: 'ZoomIn', props }
},
ZoomOut: function ZoomOut(props: Record<string, unknown>) {
return { type: 'ZoomOut', props }
}
}))
vi.mock('@/components/ui/dialog', () => ({
Dialog: function Dialog(props: { children?: unknown }) {
return { type: 'Dialog', props }
},
DialogContent: function DialogContent(props: { children?: unknown }) {
return { type: 'DialogContent', props }
},
DialogDescription: function DialogDescription(props: { children?: unknown }) {
return { type: 'DialogDescription', props }
},
DialogTitle: function DialogTitle(props: { children?: unknown }) {
return { type: 'DialogTitle', props }
}
}))
vi.mock('./PdfViewer', () => ({
default: function PdfViewer(props: Record<string, unknown>) {
return { type: 'PdfViewer', props }
}
}))
type ReactElementLike = {
type: unknown
props: Record<string, unknown>
}
function expandNode(node: unknown): unknown {
if (node == null || typeof node === 'string' || typeof node === 'number') {
return node
}
if (Array.isArray(node)) {
return node.map(expandNode)
}
const el = node as ReactElementLike
if (typeof el.type === 'function') {
return expandNode(el.type(el.props))
}
return {
...el,
props: {
...el.props,
children: expandNode(el.props?.children)
}
}
}
function findElementsByType(node: unknown, typeName: string): ReactElementLike[] {
const results: ReactElementLike[] = []
const visit = (current: unknown): void => {
if (current == null || typeof current === 'string' || typeof current === 'number') {
return
}
if (Array.isArray(current)) {
for (const child of current) {
visit(child)
}
return
}
const el = current as ReactElementLike
if (el.type === typeName) {
results.push(el)
}
visit(el.props?.children)
}
visit(node)
return results
}
function findPreviewImage(node: unknown): ReactElementLike {
const image = findElementsByType(node, 'img').find((element) => element.props.onError)
if (!image) {
throw new Error('preview image not found')
}
return image
}
async function renderExpandedImageViewer(content: string): Promise<unknown> {
reactHookRuntime.index = 0
const module = await import('./ImageViewer')
return expandNode(
module.default({
content,
filePath: '/repo/preview.png',
mimeType: 'image/png'
})
)
}
function pngBase64(width: number): string {
const bytes = Buffer.alloc(24)
Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]).copy(bytes)
bytes.writeUInt32BE(13, 8)
bytes.write('IHDR', 12, 'ascii')
bytes.writeUInt32BE(width, 16)
bytes.writeUInt32BE(1, 20)
return bytes.toString('base64')
}
describe('ImageViewer preview source retry', () => {
beforeEach(() => {
reactHookRuntime.states = []
reactHookRuntime.index = 0
vi.clearAllMocks()
})
it('retries an earlier failed source after a later source loads successfully', async () => {
const failedContent = pngBase64(1)
const loadedContent = pngBase64(2)
const firstRender = await renderExpandedImageViewer(failedContent)
const firstImage = findPreviewImage(firstRender)
expect(firstImage.props.src).toBe(`data:image/png;base64,${failedContent}`)
;(firstImage.props.onError as () => void)()
const failedRender = await renderExpandedImageViewer(failedContent)
expect(findElementsByType(failedRender, 'Image')).toHaveLength(1)
expect(findElementsByType(failedRender, 'img')).toHaveLength(0)
const loadedRender = await renderExpandedImageViewer(loadedContent)
const loadedImage = findPreviewImage(loadedRender)
;(
loadedImage.props.onLoad as (event: {
currentTarget: { naturalWidth: number; naturalHeight: number }
}) => void
)({ currentTarget: { naturalWidth: 12, naturalHeight: 10 } })
const retryRender = await renderExpandedImageViewer(failedContent)
const retryImage = findPreviewImage(retryRender)
expect(retryImage.props.src).toBe(`data:image/png;base64,${failedContent}`)
})
it('shows a failure instead of loading an unsafe raster forever', async () => {
const rendered = await renderExpandedImageViewer(pngBase64(32_769))
expect(findElementsByType(rendered, 'Image')).toHaveLength(1)
expect(findElementsByType(rendered, 'img')).toHaveLength(0)
})
})
describe('ImageViewer reading its bytes by URL', () => {
beforeEach(() => {
reactHookRuntime.states = []
reactHookRuntime.index = 0
vi.clearAllMocks()
})
it('shows an image from its orca-media URL, sized from the grant', async () => {
reactHookRuntime.index = 0
const module = await import('./ImageViewer')
const rendered = expandNode(
module.default({
src: 'orca-media://file/token?revision=r1',
byteSize: 2048,
filePath: '/repo/preview.png',
mimeType: 'image/png'
})
)
expect(findPreviewImage(rendered).props.src).toBe('orca-media://file/token?revision=r1')
expect(JSON.stringify(rendered)).toContain('2.0 KB')
})
})
@@ -195,15 +195,4 @@ describe('combined diff word-wrap lifecycle', () => {
expect(syncWordWrap).toHaveBeenLastCalledWith(mountedEditors[1]?.editor, false)
})
// The Monaco wrapper calls updateOptions on every options identity change, once per mounted section.
it('keeps editor options stable across re-renders that change no option', () => {
const view = render(<DiffSectionBody {...props} />)
view.rerender(<DiffSectionBody {...props} onMount={vi.fn()} onRetrySection={vi.fn()} />)
expect(renderedOptions.at(-1)).toBe(renderedOptions[0])
view.rerender(<DiffSectionBody {...props} sideBySide={false} />)
expect(renderedOptions.at(-1)).not.toBe(renderedOptions[0])
expect(renderedOptions.at(-1)).toMatchObject({ renderSideBySide: false })
})
})
@@ -48,14 +48,6 @@ describe('markdown preview search highlighting keeps react-owned DOM intact (cra
return body
}
it('applies highlights without mutating the react-owned subtree', () => {
const body = render(['hello world'])
const matches = applyMarkdownPreviewSearchHighlights(instance, body, 'lo')
expect(matches.length).toBe(1)
expect(body.querySelector('mark')).toBeNull()
expect(body.textContent).toBe('hello world')
})
it('survives a streamed re-render while highlights are active', () => {
const body = render(['alpha ', 'beta'])
applyMarkdownPreviewSearchHighlights(instance, body, 'beta')
@@ -108,15 +100,6 @@ describe('markdown preview search painting with the CSS Custom Highlight API', (
return div
}
it('paints many matches without a constructor-arg spread', () => {
const instance = {}
const body = bodyWithText('a a a a a a a a')
expect(() => applyMarkdownPreviewSearchHighlights(instance, body, 'a')).not.toThrow()
expect(registry.entries.get(SEARCH_HIGHLIGHT_NAME)?.ranges.size).toBe(8)
clearMarkdownPreviewSearchHighlights(instance)
body.remove()
})
it('unions highlights across previews so a second Find does not clobber the first', () => {
const a = {}
const b = {}
@@ -140,20 +123,6 @@ describe('markdown preview search painting with the CSS Custom Highlight API', (
bodyB.remove()
})
it('navigation repaints only the active highlight, not the full match set', () => {
const instance = {}
const body = bodyWithText('a a a a')
const matches = applyMarkdownPreviewSearchHighlights(instance, body, 'a')
const paintedAfterApply = registry.entries.get(SEARCH_HIGHLIGHT_NAME)
setActiveMarkdownPreviewSearchMatch(instance, matches, 0)
setActiveMarkdownPreviewSearchMatch(instance, matches, 1)
// Navigation must retain the search highlight while moving the active match.
expect(registry.entries.get(SEARCH_HIGHLIGHT_NAME)).toBe(paintedAfterApply)
expect(registry.entries.get(ACTIVE_SEARCH_HIGHLIGHT_NAME)?.ranges.has(matches[1])).toBe(true)
clearMarkdownPreviewSearchHighlights(instance)
body.remove()
})
it('re-apply drops the active highlight until the caller repaints it (same-count rerender)', () => {
const instance = {}
const body = bodyWithText('one two one two')
@@ -129,24 +129,6 @@ function buildScenario(): {
}
describe('disposeClosedEditorModels', () => {
it('scans the model registry once per batch instead of twice per closed diff tab', () => {
const batched = buildScenario()
const batchedRegistry = createRegistry(batched.models)
disposeClosedEditorModels(batchedRegistry, batched.closedTabs)
const perTab = buildScenario()
const perTabRegistry = createRegistry(perTab.models)
disposeByPrefixPerTab(perTabRegistry, perTab.prefixes)
expect(perTabRegistry.counters.getModelsCalls).toBe(CLOSED_DIFF_TAB_COUNT * 2)
expect(perTabRegistry.counters.uriToStringCalls).toBe(
CLOSED_DIFF_TAB_COUNT * 2 * perTab.models.length * 2
)
expect(batchedRegistry.counters.getModelsCalls).toBe(1)
expect(batchedRegistry.counters.uriToStringCalls).toBeLessThanOrEqual(batched.models.length * 2)
})
it('disposes exactly the models the per-tab sweep disposed', () => {
const batched = buildScenario()
disposeClosedEditorModels(createRegistry(batched.models), batched.closedTabs)
@@ -187,11 +169,4 @@ describe('disposeClosedEditorModels', () => {
closed.modifiedModelPath
])
})
it('is a no-op when nothing closed', () => {
const registry = createRegistry([createModel('diff:original:tab-1:tab-1')])
disposeClosedEditorModels(registry, [])
expect(registry.counters.getModelsCalls).toBe(0)
expect(registry.models[0].disposed).toBe(false)
})
})
@@ -8,7 +8,7 @@ import { TaskList, TaskItem } from '@tiptap/extension-list'
import { Table, TableCell, TableHeader, TableRow } from '@tiptap/extension-table'
import { createIsolatedMarkdownExtensionForTests } from '../schema/isolated-markdown-extension-for-tests'
import { handleRichMarkdownCut } from './rich-markdown-cut-handler'
import { normalizeEmptyListItems, normalizeSoftBreaks } from '../schema/rich-markdown-normalize'
import { normalizeEmptyListItems } from '../schema/rich-markdown-normalize'
const showCutLimitErrorMock = vi.hoisted(() => vi.fn())
@@ -75,23 +75,6 @@ function createClipboardEventMock(options?: { failReadback?: boolean }): {
}
describe('rich markdown cut handler behavior', () => {
it('soft-break normalization still creates visible paragraph breaks', () => {
const editor = createEditor('Line one\nLine two\nLine three\n')
normalizeSoftBreaks(editor)
expect(countParagraphs(editor)).toBe(3)
const paragraphs: string[] = []
editor.state.doc.forEach((node) => {
if (node.type.name === 'paragraph') {
expect(node.textContent).not.toContain('\n')
paragraphs.push(node.textContent)
}
})
expect(paragraphs).toEqual(['Line one', 'Line two', 'Line three'])
editor.destroy()
})
it('Cmd+X cuts only a visual line inside a hard-wrapped paragraph', () => {
const editor = createEditor('Alpha segment stays\nMiddle segment is cut\nOmega segment stays')
try {
@@ -1,11 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
extractDetailsSummaryHtml,
isEditableDetailsHtmlBlock,
matchDetailsHtmlBlock,
normalizeDetailsOpeningTag,
parseDetailsAttributes,
parseToggleHeadingVariant,
type DetailsHtmlBlock
} from './details-markdown-html'
@@ -27,110 +23,6 @@ afterEach(() => {
})
describe('details markdown html', () => {
it.each([
['<details>', '<details class="orca-details">'],
['<details open="open">', '<details class="orca-details" open>'],
['<details CLASS="orca-details">', '<details class="orca-details">'],
["<details Class='orca-details'>", '<details class="orca-details">'],
['<details cLaSs=orca-details>', '<details class="orca-details">'],
[
"<details open data-orca-toggle = 'heading-2' class='orca-details'>",
'<details class="orca-details" data-orca-toggle="heading-2" open>'
]
])('normalizes supported opening tag %s like the serializer', (input, expected) => {
expect(normalizeDetailsOpeningTag(input)).toBe(expected)
})
it.each([
'<details id="keep">',
'<details class="custom">',
'<details class="ORCA-DETAILS">',
"<details CLASS='Orca-Details'>",
'<details Class=ORCA-DETAILS>',
'<details data-orca-toggle="heading-6">',
'<details open="false">',
'<detailsish>',
'</details>',
'<summary>',
'<!-- <details> -->'
])('leaves noncanonical or unrelated fragment %s unchanged', (fragment) => {
expect(normalizeDetailsOpeningTag(fragment)).toBe(fragment)
})
it.each(['ORCA-DETAILS', 'Orca-Details'])(
'keeps case-sensitive class %s out of editable details nodes',
(className) => {
expect(
isEditableHtml(`<details class="${className}"><summary>Toggle</summary>Body</details>`)
).toBe(false)
}
)
it('extracts leading summary html without regex capture', () => {
const matchSpy = vi.spyOn(String.prototype, 'match')
const inner = `\n<SUMMARY>${'Heading line\n'.repeat(1_000)}</SUMMARY><p>Body</p>`
const summary = extractDetailsSummaryHtml(inner)
expect(summary?.content).toContain('Heading line')
expect(summary?.rawLength).toBe(inner.indexOf('<p>Body</p>'))
const usedSummaryCapture = matchSpy.mock.calls.some(
([pattern]) =>
pattern instanceof RegExp &&
pattern.source.startsWith('^\\s*<summary') &&
pattern.source.includes('[\\s\\S]')
)
expect(usedSummaryCapture).toBe(false)
})
it('accepts editable details blocks with newline-heavy summaries without summary matching', () => {
const matchSpy = vi.spyOn(String.prototype, 'match')
const block: DetailsHtmlBlock = {
raw: '',
openingAttributes: '',
inner: `<summary>${'Heading line\n'.repeat(1_000)}</summary><p>Body</p>`
}
expect(isEditableDetailsHtmlBlock(block)).toBe(true)
const usedSummaryCapture = matchSpy.mock.calls.some(
([pattern]) =>
pattern instanceof RegExp &&
pattern.source.startsWith('^\\s*<summary') &&
pattern.source.includes('[\\s\\S]')
)
expect(usedSummaryCapture).toBe(false)
})
it('accepts heading-5 toggle variants and rejects unsupported levels', () => {
expect(parseToggleHeadingVariant('heading-5')).toBe('heading-5')
expect(parseToggleHeadingVariant('heading-6')).toBeNull()
expect(parseDetailsAttributes(' data-orca-toggle="heading-5"')).toMatchObject({
variant: 'heading-5'
})
expect(parseDetailsAttributes(' data-orca-toggle="heading-6"')).toMatchObject({
variant: null
})
const editableHeading5: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' data-orca-toggle="heading-5"',
inner: '<summary>Toggle</summary><p>Body</p>'
}
const unsupportedHeading6: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' data-orca-toggle="heading-6"',
inner: '<summary>Toggle</summary><p>Body</p>'
}
expect(isEditableDetailsHtmlBlock(editableHeading5)).toBe(true)
expect(isEditableDetailsHtmlBlock(unsupportedHeading6)).toBe(false)
})
it('accepts nested toggles whose descendants are all editable', () => {
expect(isEditableHtml(nestedToggles(2))).toBe(true)
expect(isEditableHtml(nestedToggles(16))).toBe(true)
})
it('bounds each of many sibling nested toggles independently', () => {
const siblings = (count: number, extra = ''): string =>
Array.from(
@@ -146,16 +38,6 @@ describe('details markdown html', () => {
expect(isEditableHtml(wrap(`${siblings(20)}\n\n${siblings(1, ' id="x"')}`))).toBe(false)
})
it('rejects a toggle whose nested toggle is not itself editable', () => {
const block: DetailsHtmlBlock = {
raw: '',
openingAttributes: ' class="orca-details"',
inner: '<summary>Outer</summary><details id="x"><summary>Inner</summary><p>Body</p></details>'
}
expect(isEditableDetailsHtmlBlock(block)).toBe(false)
})
it('rejects nesting past the recursion guard instead of recursing without bound', () => {
expect(isEditableHtml(nestedToggles(17))).toBe(false)
expect(isEditableHtml(nestedToggles(400))).toBe(false)
@@ -136,21 +136,6 @@ describe('restored client-local editor tabs', () => {
}
)
it('reads a local tab stored outside its own project as user-named', async () => {
const filePath = '/Users/me/notes/audit.md'
const activeFile = createFloatingFile(filePath, {
relativePath: filePath,
worktreeId: 'repo-local::/Users/me/project'
})
await act(async () => root?.render(<HookProbe activeFile={activeFile} />))
await vi.waitFor(() => expect(latestFileContents[activeFile.id]?.content).toBe('# local'))
expect(mocks.readRuntimeFileContent).toHaveBeenCalledWith(
expect.objectContaining({ filePath, access: { kind: 'user-file' } })
)
})
it('keeps an absolute floating file in its panel when a project also owns the path', async () => {
const filePath = '/project/readme.md'
const activeFile = createFloatingFile(filePath, { relativePath: filePath })
@@ -168,18 +153,4 @@ describe('restored client-local editor tabs', () => {
expect.objectContaining({ filePath, access: { kind: 'user-file' } })
)
})
it('keeps a project tab inside its root', async () => {
const activeFile = createFloatingFile('/Users/me/project/README.md', {
relativePath: 'README.md',
worktreeId: 'repo::/Users/me/project'
})
await act(async () => root?.render(<HookProbe activeFile={activeFile} />))
await vi.waitFor(() => expect(latestFileContents[activeFile.id]?.content).toBe('# local'))
expect(mocks.readRuntimeFileContent).toHaveBeenCalledWith(
expect.objectContaining({ access: undefined })
)
})
})
@@ -101,7 +101,6 @@ vi.mock('@/store', () => ({
useAppStore: <T,>(selector: (state: typeof storeState) => T): T => selector(storeState)
}))
// Isolate dot visibility here; store/selectors.test.ts covers the real unread selector.
vi.mock('@/store/selectors', () => ({
selectFloatingWorkspaceHasUnread: (state: typeof storeState): boolean => state.hasFloatingUnread
}))
@@ -1,182 +0,0 @@
import { isValidElement } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { toast } from 'sonner'
import { beforeEach, expect, it, vi } from 'vitest'
import { announceRestartResults } from './native-chat-restart-action-notifications'
import { lastToastShow } from './native-chat-resume-toast.test-support'
vi.mock('sonner', async () =>
(await import('./native-chat-resume-sonner-mock.test-support')).resumeSonnerMock()
)
const show = vi.fn()
const refusedBoth = [
{ sessionId: 'a', outcome: 'refused' as const },
{ sessionId: 'b', outcome: 'refused' as const }
]
beforeEach(() => vi.clearAllMocks())
function titles(): unknown[] {
return vi.mocked(toast).mock.calls.map(([text]) => text)
}
it.each([true, false])('offers View chats on a success toast with a failure list: %s', (listed) => {
announceRestartResults(
['a', 'b'],
[
{ sessionId: 'a', outcome: 'continued' },
{ sessionId: 'b', outcome: 'continued' }
],
listed ? [] : undefined,
show
)
expect(titles()).toEqual(['Resumed 2 chats'])
expect(toast.success).toHaveBeenCalledTimes(1)
expect(vi.mocked(toast).mock.calls[0]?.[1]?.action).toEqual({
label: 'View chats',
onClick: expect.any(Function)
})
expect(lastToastShow()).toBeDefined()
})
it('offers Review on an error toast for chats a resume could not carry on', () => {
announceRestartResults(['a', 'b'], refusedBoth, refusedBoth, show)
expect(titles()).toEqual(['2 chats couldn’t be resumed'])
const options = vi.mocked(toast).mock.calls[0]?.[1]
expect(options).not.toHaveProperty('description')
expect(toast.error).toHaveBeenCalledTimes(1)
expect(options?.action).toEqual({ label: 'Review', onClick: expect.any(Function) })
lastToastShow()?.()
expect(show).toHaveBeenCalledTimes(1)
})
it('offers Review on a warning toast when a resume could not be confirmed', () => {
announceRestartResults(
['a', 'b'],
[
{ sessionId: 'a', outcome: 'continued' },
{ sessionId: 'b', outcome: 'unknown' }
],
[{ sessionId: 'b', outcome: 'unconfirmed' }],
show
)
expect(titles()).toEqual(['Couldn’t confirm 1 chat was resumed'])
expect(toast.warning).toHaveBeenCalledTimes(1)
expect(vi.mocked(toast).mock.calls[0]?.[1]?.action).toEqual({
label: 'Review',
onClick: expect.any(Function)
})
})
it('reports a mixed resume in one toast', () => {
announceRestartResults(
['a', 'b', 'c'],
[
{ sessionId: 'a', outcome: 'continued' },
{ sessionId: 'b', outcome: 'continued' },
{ sessionId: 'c', outcome: 'refused' }
],
[{ sessionId: 'c', outcome: 'refused' }],
show
)
expect(vi.mocked(toast).mock.calls).toEqual([
['1 chat couldn’t be resumed', expect.objectContaining({ description: 'Resumed 2 chats' })]
])
})
it('puts each extra count on its own line when a resume had all three outcomes', () => {
announceRestartResults(
['a', 'b', 'c'],
[
{ sessionId: 'a', outcome: 'continued' },
{ sessionId: 'b', outcome: 'refused' },
{ sessionId: 'c', outcome: 'unknown' }
],
[
{ sessionId: 'b', outcome: 'refused' },
{ sessionId: 'c', outcome: 'unconfirmed' }
],
show
)
expect(toast).toHaveBeenCalledTimes(1)
const [title, options] = vi.mocked(toast).mock.calls[0]!
expect(title).toBe('1 chat couldn’t be resumed')
const description = options?.description
if (!isValidElement(description)) {
throw new Error('Expected one line per count')
}
expect(renderToStaticMarkup(description)).toBe(
'<span class="block">Couldn’t confirm 1 other chat was resumed</span>' +
'<span class="block">Resumed 1 chat</span>'
)
})
it('counts only the requested chats the host still lists as failed', () => {
announceRestartResults(['a', 'b'], refusedBoth, [{ sessionId: 'a', outcome: 'refused' }], show)
expect(titles()).toEqual(['1 chat couldn’t be resumed'])
})
it('says nothing when the host lists none of them as failed', () => {
announceRestartResults(['a', 'b'], refusedBoth, [], show)
expect(toast).not.toHaveBeenCalled()
})
it('counts every chat not carried on when no failure list was read, with nothing to open', () => {
announceRestartResults(['a', 'b'], [], undefined, show)
expect(titles()).toEqual(['2 chats couldn’t be resumed'])
expect(vi.mocked(toast).mock.calls[0]?.[1]).not.toHaveProperty('action')
})
it('counts every chat as unconfirmed when the answer carried no outcomes', () => {
announceRestartResults(['a', 'b'], undefined, undefined, show)
expect(titles()).toEqual(['Couldn’t confirm 2 chats were resumed'])
})
// The host retires an unconfirmed send once the agent is seen carrying on it; the resume must still
// report the chat, and as resumed, not as a failure the list can no longer show.
it('preserves normalized uncertainty when a pending-list read omits the chat', () => {
announceRestartResults(['a'], [{ sessionId: 'a', outcome: 'unknown' }], [], show)
expect(titles()).toEqual(['Couldn’t confirm 1 chat was resumed'])
})
// Unconfirmed means the agent may well be working; "couldn't be resumed" would invite a second send.
// The normalized action result takes precedence over a different host observation.
it('counts normalized uncertainty separately from matching refusal', () => {
announceRestartResults(
['a', 'b'],
[
{ sessionId: 'a', outcome: 'refused' },
{ sessionId: 'b', outcome: 'unknown' }
],
[
{ sessionId: 'a', outcome: 'unconfirmed' },
{ sessionId: 'b', outcome: 'refused' }
],
show
)
expect(vi.mocked(toast).mock.calls).toEqual([
[
'1 chat couldn’t be resumed',
expect.objectContaining({ description: 'Couldn’t confirm 1 other chat was resumed' })
]
])
})
it('leads with the unconfirmed count when nothing was refused', () => {
announceRestartResults(
['a', 'b'],
[
{ sessionId: 'a', outcome: 'unknown' },
{ sessionId: 'b', outcome: 'pending' }
],
undefined,
show
)
expect(vi.mocked(toast).mock.calls).toEqual([
[
'Couldn’t confirm 2 chats were resumed',
expect.not.objectContaining({ description: expect.anything() })
]
])
})
@@ -163,33 +163,5 @@ describe.each([['states each row’s turn', true]])(
screen.getByText('SECOND DONE')
)
})
it('keeps that order once both turns settle', () => {
render(
<NativeChatMessageList
session={session('ready')}
journalItems={journal('completed', statesScope)}
journalSubmissions={[]}
isWorking={false}
workingStartedAt={null}
settledTurns={
new Map([
['user-a', { startedAt: 1, workedSeconds: 17 }],
['user-b', { startedAt: 2, workedSeconds: 51 }]
])
}
expandSignal={false}
/>
)
expectInOrder(
screen.getByText('Run the first job'),
bar('Worked for 17s'),
screen.getByText('FIRST DONE'),
screen.getByText('Then run the second job'),
bar('Worked for 51s'),
screen.getByText('SECOND DONE')
)
})
}
)
@@ -145,21 +145,6 @@ describe('useStructuredAgentSession context usage', () => {
expect(optionReads()).toBe(1)
})
it('asks the host again when a live turn revision fell outside the loaded window', async () => {
answerOptions([
{ used: estimate(150_000), window: WINDOW },
{ used: estimate(170_000), window: WINDOW }
])
const { result, rerender } = render()
await waitFor(() => expect(result.current.contextUsage).toMatchObject({ usedTokens: 150_000 }))
unloadedTurnRevisions = 1
rerender()
await waitFor(() => expect(result.current.contextUsage).toMatchObject({ usedTokens: 170_000 }))
expect(optionReads()).toBe(2)
})
it('keeps one refresh in flight and one behind it however many revisions it missed', async () => {
const replies: ((used: number) => void)[] = []
mocks.call.mockImplementation((_target, method) =>
@@ -179,22 +179,4 @@ describe('desktop structured prompt answers', () => {
})
expect(respondCall()).not.toHaveProperty('answers')
})
it('sends an approval decision without probing the host', async () => {
const { result } = renderHook(() =>
useStructuredAgentSession({
sessionId: 'session-1',
target,
agent: 'claude',
isVisible: true
})
)
await act(async () => {
await result.current.respond(approval, { kind: 'option', optionId: 'allow' })
})
expect(mocks.questionAnswersSupported).not.toHaveBeenCalled()
expect(respondCall()).toMatchObject({ itemId: 'approval-1', optionId: 'allow' })
})
})
@@ -1,140 +0,0 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { TooltipProvider } from '@/components/ui/tooltip'
import { AGENT_CATALOG } from '@/lib/agent-catalog'
import { NewWorkspaceComposerAgentSection } from './NewWorkspaceComposerAgentSection'
afterEach(cleanup)
describe('NewWorkspaceComposerAgentSection agent picker', () => {
it.each([
{ kind: 'project', selectedRepoIsGit: true, matched: true },
{ kind: 'project', selectedRepoIsGit: false, matched: false },
{ kind: 'project-group', selectedRepoIsGit: true, matched: false }
] as const)(
'keeps Blank Terminal and scopes menu width to Create worktree: $kind / $selectedRepoIsGit',
({ kind, selectedRepoIsGit, matched }) => {
const work: { size: number; initial: number; update: number; targetChange: number }[] = []
for (const size of [32, 320]) {
let optionReads = 0
const projectOptions = Array.from({ length: size }, (_, index) => {
const optionKind = index === 0 ? (kind === 'project' ? 'project-group' : 'project') : kind
return {
get id() {
optionReads++
return index === size - 1 ? 'target' : index === 0 ? 'alternate' : `option-${index}`
},
displayName: `Target ${index}`,
badgeColor: '',
detail: `/target-${index}`,
...(optionKind === 'project'
? { kind: optionKind, projectId: `target-${index}` }
: {
kind: optionKind,
projectGroupId: `target-${index}`,
parentPath: `/target-${index}`,
connectionId: null
})
}
})
const onQuickAgentChange = vi.fn()
const section = (selectedProjectId: string, advancedOpen: boolean) => (
<TooltipProvider>
<NewWorkspaceComposerAgentSection
quickAgent="codex"
selectedRepoIsGit={selectedRepoIsGit}
selectedProjectId={selectedProjectId}
projectOptions={projectOptions}
agentSurfaceRows={{
model: {
state: 'settled',
rows: [{ agent: 'codex', kind: 'terminal' }],
detection: {
detectedIds: ['codex'],
isLoading: false,
isRefreshing: false,
detectionFailed: false,
needsServerUpdate: false,
refresh: async () => ['codex']
}
},
selection: { ready: true, cli: false },
contextKey: 'test',
onPick: vi.fn()
}}
onQuickAgentChange={onQuickAgentChange}
onOpenAgentSettings={vi.fn()}
createDisabled={false}
onCreate={vi.fn()}
advancedOpen={advancedOpen}
onToggleAdvanced={vi.fn()}
visibleQuickAgents={AGENT_CATALOG}
defaultTuiAgent="blank"
handleSetDefaultAgent={vi.fn()}
/>
</TooltipProvider>
)
const { rerender, unmount } = render(section('target', false))
const initial = optionReads
expect(initial).toBeLessThanOrEqual(selectedRepoIsGit ? size : 0)
expect(initial).toBeGreaterThanOrEqual(selectedRepoIsGit ? 1 : 0)
expect(screen.getByRole('button', { name: 'Select agent' }).textContent).not.toContain(
'Chat'
)
expect(screen.getByRole('button', { name: 'Select agent' }).textContent).not.toContain(
'(CLI)'
)
fireEvent.pointerDown(screen.getByRole('button', { name: 'Select agent' }), {
button: 0,
ctrlKey: false
})
const blankTerminal = screen.getByRole('menuitem', { name: 'Blank Terminal' })
const menu = screen.getByRole('menu')
expect(menu.classList.contains('w-(--radix-dropdown-menu-trigger-width)')).toBe(matched)
expect(menu.classList.contains('min-w-(--radix-dropdown-menu-trigger-width)')).toBe(matched)
expect(menu.classList.contains('min-w-64')).toBe(!matched)
expect(blankTerminal.querySelector('svg')).not.toBeNull()
expect(screen.queryByRole('menuitem', { name: 'Setup only' })).toBeNull()
expect(screen.getByRole('menuitem', { name: 'Agent settings…' })).toBeTruthy()
fireEvent.click(blankTerminal)
expect(onQuickAgentChange).toHaveBeenCalledWith(null)
optionReads = 0
rerender(section('target', true))
const update = optionReads
expect(update).toBeLessThanOrEqual(selectedRepoIsGit ? size : 0)
expect(update).toBeGreaterThanOrEqual(selectedRepoIsGit ? 1 : 0)
optionReads = 0
rerender(section('alternate', true))
const targetChange = optionReads
expect(targetChange).toBeLessThanOrEqual(selectedRepoIsGit ? 1 : 0)
expect(targetChange).toBeGreaterThanOrEqual(selectedRepoIsGit ? 1 : 0)
fireEvent.pointerDown(screen.getByRole('button', { name: 'Select agent' }), {
button: 0,
ctrlKey: false
})
const changedMenu = screen.getByRole('menu')
const changedMatched = selectedRepoIsGit && kind === 'project-group'
expect(changedMenu.classList.contains('w-(--radix-dropdown-menu-trigger-width)')).toBe(
changedMatched
)
expect(changedMenu.classList.contains('min-w-(--radix-dropdown-menu-trigger-width)')).toBe(
changedMatched
)
expect(changedMenu.classList.contains('min-w-64')).toBe(!changedMatched)
expect(onQuickAgentChange).toHaveBeenCalledExactlyOnceWith(null)
work.push({ size, initial, update, targetChange })
unmount()
}
expect(work[1].initial).toBeLessThanOrEqual(10 * work[0].initial)
expect(work[1].update).toBeLessThanOrEqual(10 * work[0].update)
expect(work[1].targetChange).toBeLessThanOrEqual(work[0].targetChange)
console.info('project option reads', { kind, selectedRepoIsGit, work })
}
)
})
@@ -84,31 +84,6 @@ afterEach(() => {
})
describe('SetProjectLocationDialog', () => {
it('keeps the parent caller in charge of close and saves an existing folder', async () => {
const onClose = vi.fn()
const onReady = vi.fn()
const user = renderDialog({ onClose, onReady })
expect(screen.getByTestId('set-project-location-dialog')).toBeTruthy()
expect(screen.getByText('Set project location')).toBeTruthy()
expect(screen.getByText('Choose where orca lives on openclaw.')).toBeTruthy()
await user.click(screen.getByRole('button', { name: /Browse folder/ }))
await user.click(screen.getByRole('button', { name: 'Browse host filesystem' }))
await user.click(screen.getByRole('button', { name: 'Select remote folder' }))
await user.click(screen.getByRole('button', { name: 'Set location' }))
expect(storeMocks.setupProjectExistingFolder).toHaveBeenCalledWith({
projectId: 'project-orca',
hostId: 'ssh:openclaw',
path: '/remote/orca',
kind: 'git',
displayName: 'orca'
})
expect(onReady).toHaveBeenCalledWith(JSON.stringify(['setup-openclaw', 'ssh:openclaw']))
expect(onClose).not.toHaveBeenCalled()
})
it('backs out of the host browser on Escape instead of discarding the form', async () => {
const onClose = vi.fn()
const user = renderDialog({ onClose })
@@ -3,30 +3,15 @@ import type {
ComputerUsePermissionSetupResult,
ComputerUsePermissionStatusResult
} from '../../../../shared/computer-use-permissions-types'
import {
COMPUTER_USE_SKILL_NAME,
ORCA_CLI_SKILL_NAME,
ORCA_LINEAR_SKILL_NAME,
ORCHESTRATION_SKILL_NAME
} from '@/lib/agent-feature-skill-names'
import { ORCHESTRATION_SKILL_NAME } from '@/lib/agent-feature-skill-names'
import { BROWSER_USE_ENABLED_STORAGE_KEY } from '@/lib/browser-use-setup-state'
import {
ORCHESTRATION_ENABLED_STORAGE_KEY,
ORCHESTRATION_SETUP_DISMISSED_STORAGE_KEY
} from '@/lib/orchestration-setup-state'
import { ORCHESTRATION_ENABLED_STORAGE_KEY } from '@/lib/orchestration-setup-state'
import {
runOnboardingFeatureSetup,
type OnboardingFeatureSetupDeps,
type OnboardingFeatureSetupSelection
} from './onboarding-feature-setup'
const ALL_SKILL_NAMES = [
ORCA_CLI_SKILL_NAME,
COMPUTER_USE_SKILL_NAME,
ORCHESTRATION_SKILL_NAME,
ORCA_LINEAR_SKILL_NAME
]
const GRANTED_COMPUTER_USE_STATUS: ComputerUsePermissionStatusResult = {
platform: 'darwin',
helperAppPath: '/Applications/Orca Computer Use.app',
@@ -70,40 +55,6 @@ describe('onboarding feature setup runner', () => {
vi.unstubAllGlobals()
})
it('runs selected feature setup through injected deps only', async () => {
const deps = createDeps({
getComputerUsePermissionStatus: vi.fn(
async (): Promise<ComputerUsePermissionStatusResult> => ({
platform: 'darwin',
helperAppPath: '/Applications/Orca Computer Use.app',
helperUnavailableReason: null,
permissions: [
{ id: 'accessibility', status: 'not-granted' },
{ id: 'screenshots', status: 'granted' }
]
})
)
})
const result = await runOnboardingFeatureSetup(
{ browserUse: true, computerUse: true, orchestration: true, linearTickets: true },
deps
)
expect(result).toEqual({
selectedIds: ['browserUse', 'computerUse', 'orchestration', 'linearTickets'],
skillNames: ALL_SKILL_NAMES,
computerUsePermissionsOpened: true,
warnings: []
})
expect(deps.getComputerUsePermissionStatus).toHaveBeenCalledTimes(1)
expect(deps.openComputerUsePermissionSetup).toHaveBeenCalledTimes(1)
expect(deps.storage.get(BROWSER_USE_ENABLED_STORAGE_KEY)).toBe('1')
expect(deps.storage.get(ORCHESTRATION_ENABLED_STORAGE_KEY)).toBe('1')
expect(deps.removeStorageItem).toHaveBeenCalledWith(ORCHESTRATION_SETUP_DISMISSED_STORAGE_KEY)
expect(deps.notifyOrchestrationStateChanged).toHaveBeenCalledTimes(1)
})
it('keeps invasive Browser Use and Computer Use setup untouched when only Orchestration is selected', async () => {
const deps = createDeps()
const selection: OnboardingFeatureSetupSelection = {
@@ -124,25 +75,6 @@ describe('onboarding feature setup runner', () => {
expect(deps.storage.get(ORCHESTRATION_ENABLED_STORAGE_KEY)).toBe('1')
})
it('clears feature markers when no setup items are selected', async () => {
const deps = createDeps()
const result = await runOnboardingFeatureSetup(
{ browserUse: false, computerUse: false, orchestration: false, linearTickets: false },
deps
)
expect(result).toEqual({
selectedIds: [],
skillNames: [],
computerUsePermissionsOpened: false,
warnings: []
})
expect(deps.storage.get(BROWSER_USE_ENABLED_STORAGE_KEY)).toBe('0')
expect(deps.storage.get(ORCHESTRATION_ENABLED_STORAGE_KEY)).toBe('0')
expect(deps.getComputerUsePermissionStatus).not.toHaveBeenCalled()
})
it('keeps main’s skill selection but skips permissions when the Computer Use helper is unavailable', async () => {
// Missing helpers report every permission ungranted; do not call openSetup, which would throw.
const unavailableStatus: ComputerUsePermissionStatusResult = {
@@ -17,7 +17,7 @@ import {
import { aiVaultSearchHitToSession } from '../search/ai-vault-search-session'
import type { AiVaultSessionVirtualList } from '../session-list/AiVaultSessionVirtualList'
import type { AiVaultResumeInChatEligibility } from '../session-actions/ai-vault-session-resume-in-chat'
import { resolveAiVaultPanelSessionListRequest } from './ai-vault-panel-session-list-request'
import {
writeAiVaultViewOptions,
readAiVaultViewOptions
@@ -120,20 +120,6 @@ afterEach(() => {
window.localStorage.clear()
})
it('builds the exact request the Session History panel sends for the active workspace', async () => {
const { default: AiVaultPanel } = await import('../panel/AiVaultPanel')
render(<AiVaultPanel />)
const request = resolveAiVaultPanelSessionListRequest(useAppStore.getState(), WORKTREE_ID)
expect(request.scopePaths).toEqual(['/repo/wt'])
expect(request.sessionLimit).toBe(500)
expect(panelRequests.at(-1)).toEqual(request)
fireEvent.click(screen.getByRole('radio', { name: /^All$/ }))
expect(panelRequests.at(-1)?.scopePaths).toEqual(['/repo/wt', '/repo/sibling', '/repos/repo-1'])
fireEvent.click(screen.getByRole('radio', { name: /^Workspace$/ }))
expect(panelRequests.at(-1)).toEqual(request)
})
it.each([false, true])(
'delivers alias rows to native resume in Workspace and All (bound: %s)',
async (bound) => {
@@ -5,11 +5,7 @@ import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it } from 'vitest'
import { TEST_REPO, makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
import type { AiVaultSessionResumeTargetState } from '../session-actions/ai-vault-session-resume'
import {
buildAiVaultHostScopeOptions,
buildRuntimeAiVaultHostScopeOptions,
useAiVaultExecutionHostScope
} from './ai-vault-host-scope'
import { useAiVaultExecutionHostScope } from './ai-vault-host-scope'
import type { ExecutionHostId, ExecutionHostScope } from '../../../../../../shared/execution-host'
type HostScopeResult = ReturnType<typeof useAiVaultExecutionHostScope>
@@ -80,33 +76,6 @@ afterEach(() => {
})
describe('useAiVaultExecutionHostScope', () => {
it('defaults SSH worktrees to their SSH execution host', async () => {
await renderHook({
activeWorktreeId: 'repo-1::/remote/repo',
resumeTargetState: stateForWorktree({
worktreeId: 'repo-1::/remote/repo',
repoId: 'repo-1',
hostId: 'ssh:dev-box'
})
})
expect(latest?.executionHostScope).toBe('ssh:dev-box')
expect(latest?.activeExecutionHostScope).toBe('ssh:dev-box')
})
it('defaults local worktrees to local history', async () => {
await renderHook({
activeWorktreeId: 'repo-1::/local/repo',
resumeTargetState: stateForWorktree({
worktreeId: 'repo-1::/local/repo',
repoId: 'repo-1'
})
})
expect(latest?.executionHostScope).toBe('local')
expect(latest?.activeExecutionHostScope).toBeNull()
})
it('does not claim local history when the active workspace host cannot be resolved (#13713)', async () => {
await renderHook({
activeWorktreeId: 'repo-1::/remote/repo',
@@ -122,35 +91,6 @@ describe('useAiVaultExecutionHostScope', () => {
expect(latest?.executionHostScope).toBe('all')
})
it('keeps local history when no workspace is selected at all', async () => {
await renderHook({
activeWorktreeId: null,
resumeTargetState: {
folderWorkspaces: [],
projectGroups: [],
repos: [],
worktreesByRepo: {}
}
})
expect(latest?.executionHostScope).toBe('local')
})
it('defaults runtime worktrees to their runtime execution host', async () => {
await renderHook({
activeWorktreeId: 'repo-1::/runtime/repo',
resumeTargetState: stateForWorktree({
worktreeId: 'repo-1::/runtime/repo',
repoId: 'repo-1',
executionHostId: 'runtime:remote-server'
}),
availableExecutionHostScopes: ['runtime:remote-server'] as const
})
expect(latest?.executionHostScope).toBe('runtime:remote-server')
expect(latest?.activeExecutionHostScope).toBe('runtime:remote-server')
})
it('preserves manual host scope changes across unrelated rerenders', async () => {
const props = {
activeWorktreeId: 'repo-1::/remote/repo',
@@ -189,36 +129,3 @@ describe('useAiVaultExecutionHostScope', () => {
expect(latest?.executionHostScope).toBe('runtime:remote-server')
})
})
describe('buildAiVaultHostScopeOptions', () => {
it('adds saved runtime hosts between active SSH and all hosts', () => {
const runtimeHostOptions = buildRuntimeAiVaultHostScopeOptions([
{ id: 'remote-server', name: 'VPS Orca Server' }
])
expect(
buildAiVaultHostScopeOptions({
activeExecutionHostScope: 'ssh:dev-box',
runtimeHostOptions
})
).toEqual([
{ id: 'local', label: expect.any(String) },
{ id: 'ssh:dev-box', label: 'dev-box' },
{ id: 'runtime:remote-server', label: 'VPS Orca Server' },
{ id: 'all', label: 'All hosts' }
])
})
it('keeps a runtime active host visible even before the saved environment list hydrates', () => {
expect(
buildAiVaultHostScopeOptions({
activeExecutionHostScope: 'runtime:remote-server',
runtimeHostOptions: []
})
).toEqual([
{ id: 'local', label: expect.any(String) },
{ id: 'runtime:remote-server', label: 'remote-server' },
{ id: 'all', label: 'All hosts' }
])
})
})
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { searchHit } from '../../../../../../shared/ai-vault/search/ai-vault-search-test-fixture'
import * as correspondence from '../../../../../../shared/ai-vault/scanning/ai-vault-session-cwd-aliases'
import { resolveAiVaultSessionResumeInChatEligibility } from '../session-actions/ai-vault-session-resume-in-chat'
import {
aiVaultSearchHitToSession,
@@ -12,43 +12,6 @@ import {
afterEach(() => vi.restoreAllMocks())
describe('withAiVaultHistoryPreviews', () => {
it('bounds identity indexing and correspondence work when history adds aliases at N and 10N', () => {
const copy = vi.spyOn(correspondence, 'withAiVaultSessionCwdAliases')
for (const size of [100, 1000]) {
let identityReads = 0
const hits = Array.from({ length: size }, (_, index) => {
const row = aiVaultSearchHitToSession(
{ ...searchHit(), sessionId: `session-${index}`, cwd: '/private/var/example' },
'local'
)
return {
...row,
get sessionId() {
identityReads += 1
return row.sessionId
}
}
})
const history = hits.map((hit) => ({ ...hit }))
expect(withAiVaultHistoryPreviews(hits, history).every((row) => !row.cwdAliases)).toBe(true)
const updated = history.map((row) => ({
...row,
cwdAliases: ['/var/example'],
get sessionId() {
identityReads += 1
return row.sessionId
}
}))
identityReads = 0
copy.mockClear()
const hydrated = withAiVaultHistoryPreviews(hits, updated)
expect(hydrated.every((row) => row.cwdAliases?.[0] === '/var/example')).toBe(true)
expect(copy).toHaveBeenCalledTimes(size)
expect(identityReads).toBeGreaterThanOrEqual(2 * size)
expect(identityReads).toBeLessThanOrEqual(4 * size)
}
})
it('restores resume correspondence when history arrives after a search hit, even with an existing preview', () => {
const hit = aiVaultSearchHitToSession(
{ ...searchHit(), agent: 'claude', cwd: '/private/var/example' },
@@ -81,35 +44,6 @@ describe('withAiVaultHistoryPreviews', () => {
expect(withAiVaultHistoryPreviews([hit], rows)[0].cwdAliases).toBeUndefined()
}
})
it("shows a paired server hit's conversation preview from its scanned history row", () => {
// A relay hit arrives without its transcript path, so its row id differs from the scanned row's.
const hitSession = aiVaultSearchHitToSession(
{ ...searchHit(), source: { presence: 'present' as const } },
'runtime:env-1'
)
const turns = [
{ role: 'user' as const, text: 'Fix the flaky test', timestamp: null },
{ role: 'assistant' as const, text: 'Done', timestamp: null }
]
const [hydrated] = withAiVaultHistoryPreviews(
[hitSession],
[
{
...hitSession,
id: 'runtime:env-1:codex:host-session:/host/transcript.jsonl',
filePath: '/host/transcript.jsonl',
previewMessages: turns,
firstUserPrompt: 'Fix the flaky test'
}
]
)
expect(hydrated.previewMessages).toEqual(turns)
expect(hydrated.firstUserPrompt).toBe('Fix the flaky test')
expect(hydrated.resumeCommand).toBe('host-resume-command')
})
})
describe('aiVaultSearchHitToSession', () => {
@@ -131,20 +65,6 @@ describe('aiVaultSearchHitToSession', () => {
expect(session.accountWslDistro).toBe('Ubuntu')
})
it('does not invent a path or resume command when transport withholds them', () => {
const hit = {
...searchHit(),
source: { presence: 'unverifiable' as const },
resumeCommand: undefined
}
const session = aiVaultSearchHitToSession(hit, 'runtime:cloud')
expect(session.filePath).toBe('')
expect(session.resumeCommand).toBe('')
expect(canResumeAiVaultSearchHit(hit)).toBe(false)
expect(hasAiVaultSearchHitPath(hit)).toBe(false)
})
it('opens a native chat hit through its owner when transport withholds the command', () => {
const hit = {
...searchHit(),
@@ -156,11 +76,4 @@ describe('aiVaultSearchHitToSession', () => {
expect(canResumeAiVaultSearchHit(hit)).toBe(true)
expect(hasAiVaultSearchHitPath(hit)).toBe(false)
})
it('gates resume and path actions on their own source fields', () => {
const withoutResume = { ...searchHit(), resumeCommand: undefined }
expect(canResumeAiVaultSearchHit(withoutResume)).toBe(false)
expect(hasAiVaultSearchHitPath(withoutResume)).toBe(true)
})
})
@@ -107,12 +107,6 @@ describe('resuming a conversation from history in a chat', () => {
expect(resumeInChat()).toEqual({ available: false, reason: 'workspace' })
})
it('is offered in a workspace on the machine that recorded it', () => {
mocks.owner.mockReturnValue('local')
expect(resumeInChat()).toEqual({ available: true, workspaceId: PAIRED_WORKSPACE })
})
it('asks the host that would run it whether it can resume, not another', () => {
mocks.owner.mockReturnValue('local')
mocks.localCapabilities.mockReturnValue([])
@@ -128,13 +122,6 @@ describe('paired runtime conversation ownership', () => {
mocks.owner.mockReturnValue('runtime:server-1')
})
it('offers a conversation on its recording runtime', () => {
expect(resumeInChat(remoteSession)).toEqual({
available: true,
workspaceId: PAIRED_WORKSPACE
})
})
it.each(['local', 'runtime:server-2', null])(
'refuses moving the remote conversation to %s',
(owner) => {
@@ -1,9 +1,5 @@
import { beforeEach, describe, expect, it } from 'vitest'
import {
ACTIVE_CHECKS_STATUS_INPUT_KEYS,
clearActiveChecksStatusCacheForTests,
getActiveChecksStatus
} from './active-checks-status'
import { clearActiveChecksStatusCacheForTests, getActiveChecksStatus } from './active-checks-status'
import type { AppState } from '../../../store/types'
import type { PRInfo } from '../../../../../shared/github/pull-request-types'
@@ -46,44 +42,6 @@ describe('getActiveChecksStatus', () => {
expect(getActiveChecksStatus(state)).toBe('success')
})
it('uses GitLab MR pipeline status when the active branch has no GitHub PR cache entry', () => {
const state = {
activeWorktreeId: 'wt-1',
repos: [{ id: 'repo-1', path: '/repo' }],
worktreesByRepo: {
'repo-1': [
{
id: 'wt-1',
repoId: 'repo-1',
branch: 'refs/heads/feature/gitlab',
linkedGitLabMR: 7
}
]
},
prCache: {},
hostedReviewCache: {
'local::repo-1::feature/gitlab': {
data: {
provider: 'gitlab',
number: 7,
title: 'GitLab MR',
state: 'open',
url: 'https://gitlab.com/acme/orca/-/merge_requests/7',
status: 'success',
updatedAt: '2026-05-20T00:00:00Z',
mergeable: 'MERGEABLE'
},
fetchedAt: 2
}
}
} as unknown as Pick<
AppState,
'activeWorktreeId' | 'repos' | 'worktreesByRepo' | 'prCache' | 'hostedReviewCache'
>
expect(getActiveChecksStatus(state)).toBe('success')
})
it('does not show stale GitHub PR status for a linked GitLab MR while MR status is loading', () => {
const state = {
activeWorktreeId: 'wt-1',
@@ -109,91 +67,4 @@ describe('getActiveChecksStatus', () => {
expect(getActiveChecksStatus(state)).toBeNull()
})
it('hides the matching suppressed GitHub PR status', () => {
const state = {
activeWorktreeId: 'wt-1',
repos: [{ id: 'repo-1', path: '/repo' }],
worktreesByRepo: {
'repo-1': [
{
id: 'wt-1',
repoId: 'repo-1',
branch: 'refs/heads/feature/test',
linkedPR: null,
suppressedGitHubPR: 12
}
]
},
prCache: {
'repo-1::feature/test': { data: makePR('failure'), fetchedAt: 2 }
}
} as unknown as Pick<AppState, 'activeWorktreeId' | 'repos' | 'worktreesByRepo' | 'prCache'>
expect(getActiveChecksStatus(state)).toBeNull()
})
})
describe('getActiveChecksStatus caching', () => {
beforeEach(() => {
clearActiveChecksStatusCacheForTests()
})
function makeState(prCache: Record<string, unknown>) {
return {
activeWorktreeId: 'wt-1',
repos: [{ id: 'repo-1', path: '/repo' }],
worktreesByRepo: {
'repo-1': [{ id: 'wt-1', repoId: 'repo-1', branch: 'refs/heads/feature/test' }]
},
prCache
} as unknown as Pick<AppState, 'activeWorktreeId' | 'repos' | 'worktreesByRepo' | 'prCache'>
}
it('reuses the cached status when every input reference is unchanged', () => {
const prCache = { 'repo-1::feature/test': { data: makePR('success'), fetchedAt: 2 } }
const state = makeState(prCache)
expect(getActiveChecksStatus(state)).toBe('success')
// A different state object carrying the same field references must still hit the cache.
expect(getActiveChecksStatus({ ...state })).toBe('success')
})
it('recomputes when a keyed input reference changes', () => {
expect(
getActiveChecksStatus(
makeState({ 'repo-1::feature/test': { data: makePR('success'), fetchedAt: 2 } })
)
).toBe('success')
expect(
getActiveChecksStatus(
makeState({ 'repo-1::feature/test': { data: makePR('failure'), fetchedAt: 3 } })
)
).toBe('failure')
})
it('reads no store field the cache is not keyed on', () => {
// Every property read must invalidate the cache, even if a cast bypasses its derived input type.
const reads = new Set<PropertyKey>()
const keyed = new Set<PropertyKey>(ACTIVE_CHECKS_STATUS_INPUT_KEYS)
const state = new Proxy(
makeState({ 'repo-1::feature/test': { data: makePR('success'), fetchedAt: 2 } }),
{
get(target, prop, receiver) {
reads.add(prop)
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
return Reflect.get(target, prop, receiver)
},
has(target, prop) {
reads.add(prop)
return Reflect.has(target, prop)
}
}
)
expect(getActiveChecksStatus(state)).toBe('success')
expect([...reads].filter((prop) => !keyed.has(prop))).toEqual([])
// Require a nonempty read set so the guard cannot pass vacuously.
expect(reads.has('prCache')).toBe(true)
})
})
@@ -2,14 +2,10 @@ import { afterEach, describe, expect, it } from 'vitest'
import type { Worktree } from '../../../../../../shared/worktree/types'
import { useAppStore } from '@/store/index'
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
import {
FLOATING_TERMINAL_WORKTREE_ID,
getDefaultSettings
} from '../../../../../../shared/constants'
import {
captureFileExplorerOperationGuard,
getFileExplorerOperationOwner,
getFileExplorerOperationOwnerFromState
getFileExplorerOperationOwner
} from '../navigation/file-explorer-operation-owner'
const initialState = useAppStore.getInitialState()
@@ -31,24 +27,6 @@ afterEach(() => {
})
describe('file explorer operation generations', () => {
it('routes floating file ownership and mutations locally while a runtime is focused', () => {
useAppStore.setState({
settings: { ...getDefaultSettings('/home/me'), activeRuntimeEnvironmentId: 'focused-env' },
activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID,
activeWorkspaceExecutionHostId: 'runtime:focused-env'
})
const owner = getFileExplorerOperationOwner(FLOATING_TERMINAL_WORKTREE_ID)
const guard = captureFileExplorerOperationGuard(FLOATING_TERMINAL_WORKTREE_ID, owner)
expect(owner).toEqual({ kind: 'local' })
expect(
getFileExplorerOperationOwnerFromState(useAppStore.getState(), FLOATING_TERMINAL_WORKTREE_ID)
).toEqual(owner)
expect(guard.route.target).toEqual({ kind: 'local' })
expect(guard.route.expectedExecutionHostId).toBe('local')
expect(() => guard.assertCurrent()).not.toThrow()
})
it('invalidates a nested SSH mutation when that target reconnects', () => {
useAppStore.setState({
repos: [],
@@ -70,27 +70,6 @@ it('runs one bounded retry across root and expanded-wave loading transitions', a
expect(props.refreshTree).toHaveBeenCalledTimes(1)
expect(props.loadDir).not.toHaveBeenCalled()
})
it.each(['fresh-empty', 'directory-error', 'root-error'])('does not loop on %s', async (state) => {
const { props, stale } = params()
if (state === 'fresh-empty') {
stale.clear()
}
if (state === 'directory-error') {
props.dirCache['/repo/link'].error = 'ENOENT'
}
if (state === 'root-error') {
props.rootError = 'ETIMEDOUT'
}
const hook = renderHook(useFileExplorerTreeLoadEffects, { initialProps: props })
for (let i = 0; i < 3; i++) {
hook.rerender({ ...props, loadingDirPaths: new Set() })
await act(async () => {
await Promise.resolve()
})
}
expect(props.refreshTree).not.toHaveBeenCalled()
expect(props.loadDir).not.toHaveBeenCalled()
})
it.each(['root', 'directory'])('stops after a retry records a %s error', async (failure) => {
const { props } = params()
const pending = pendingRefresh()
@@ -136,12 +115,6 @@ it('does not let an old workspace completion release a new workspace retry', asy
})
expect(second.props.refreshTree).toHaveBeenCalledTimes(1)
})
it("only the user's own switch to a workspace lets its open read start a stopped WSL distro", () => {
const { props } = params()
noteUserWorkspaceSwitch(props.workspaceKey)
renderHook(useFileExplorerTreeLoadEffects, { initialProps: props })
expect(props.resetAndLoad).toHaveBeenCalledExactlyOnceWith({ wakeWslDistro: true })
})
it('an old switch no consumer used is not redeemed by a later mount (E9)', () => {
vi.useFakeTimers()
try {
@@ -1,233 +0,0 @@
// @vitest-environment happy-dom
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { CheckStatus } from '../../../../../../shared/github/pull-request-types'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import type { ParentPrChecksRow, ParentPrChecksRowStatus } from './parent-pr-checks-row-types'
vi.mock('@/i18n/i18n', () => ({
translate: (_key: string, fallback: string, values?: Record<string, unknown>) =>
values ? fallback.replace('{{value0}}', String(values.value0)) : fallback
}))
vi.mock('@/lib/app-http-link-open', () => ({
openAppHttpLink: vi.fn()
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <span>{children}</span>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('../../checks-panel/presentation/check-presentation', () => ({
CHECK_COLOR: {
success: 'success-color',
failure: 'failure-color',
pending: 'pending-color',
neutral: 'neutral-color'
},
CHECK_ICON: {
success: (props: { className?: string }) => <span data-icon="success" {...props} />,
failure: (props: { className?: string }) => <span data-icon="failure" {...props} />,
pending: (props: { className?: string }) => <span data-icon="pending" {...props} />,
neutral: (props: { className?: string }) => <span data-icon="neutral" {...props} />
},
PullRequestIcon: (props: { className?: string }) => <span data-icon="review" {...props} />,
prStateColor: () => 'state-color'
}))
vi.mock('../../checks-panel/presentation/checks-list', () => ({
ChecksList: () => <div data-testid="checks-list" />
}))
import { FolderWorkspacePrChecksRow } from './FolderWorkspacePrChecksRow'
let container: HTMLDivElement
let root: Root
function makeWorktree(): Worktree {
return {
id: 'repo-1::/child',
path: '/child',
head: 'abc',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false,
repoId: 'repo-1',
displayName: 'Enable child worktrees',
comment: '',
linkedIssue: null,
linkedPR: 12,
linkedLinearIssue: null,
linkedGitLabMR: null,
linkedGitLabIssue: null,
isArchived: false,
isUnread: false,
isPinned: false,
sortOrder: 0,
lastActivityAt: 0
}
}
function makeRepo(): Repo {
return {
id: 'repo-1',
path: '/repo',
displayName: 'Repo',
badgeColor: '#fff',
addedAt: 1,
kind: 'git'
}
}
function makeRow(
overrides: Partial<ParentPrChecksRow> & {
status?: ParentPrChecksRowStatus
checkTone?: CheckStatus
} = {}
): ParentPrChecksRow {
return {
id: 'row-1',
refreshIdentity: 'row-1::feature',
worktree: makeWorktree(),
repo: makeRepo(),
branch: 'feature',
status: 'success',
group: 'passing',
checkTone: 'success',
title: 'Review title',
reviewNumber: 12,
reviewLabel: '#12',
reviewUrl: 'https://example.test/pr/12',
reviewState: 'open',
reviewStatus: 'success',
provider: 'github',
summary: 'Checks passing',
detailNames: [],
checks: [],
isRefreshing: false,
hasLinkedReview: true,
...overrides
}
}
function renderRow(row: ParentPrChecksRow): void {
act(() => {
root.render(
<FolderWorkspacePrChecksRow
row={row}
expanded={false}
onToggle={vi.fn()}
onLoadCheckDetails={vi.fn(async () => null)}
/>
)
})
}
function iconNames(): string[] {
return [...container.querySelectorAll<HTMLElement>('[data-icon]')].map(
(icon) => icon.dataset.icon ?? ''
)
}
describe('FolderWorkspacePrChecksRow', () => {
beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => root.unmount())
container.remove()
})
it.each([
{
status: 'success' as const,
checkTone: 'success' as const,
icon: 'success',
color: 'success-color',
summary: 'Checks passing'
},
{
status: 'failing' as const,
checkTone: 'failure' as const,
icon: 'failure',
color: 'failure-color',
summary: 'Checks failing'
}
])('shows $icon status as summary metadata after the review identity', (state) => {
renderRow(
makeRow({
status: state.status,
checkTone: state.checkTone,
summary: state.summary
})
)
expect(iconNames()).toEqual(['review', state.icon])
expect(container.textContent).toContain('Enable child worktrees')
expect(container.textContent).toContain(state.summary)
expect(container.querySelector(`[data-icon="${state.icon}"]`)?.className).toContain(state.color)
expect(container.querySelector(`[data-icon="${state.icon}"]`)?.className).not.toContain(
'animate-spin'
)
})
it('spins pending summary status like regular pending check rows', () => {
renderRow(
makeRow({
status: 'pending',
group: 'pending',
checkTone: 'pending',
summary: 'Checks pending'
})
)
expect(iconNames()).toEqual(['review', 'pending'])
expect(container.querySelector('[data-icon="pending"]')?.className).toContain('pending-color')
expect(container.querySelector('[data-icon="pending"]')?.className).toContain('animate-spin')
})
it('spins loading summary status because it uses the pending check icon', () => {
renderRow(
makeRow({
status: 'loading',
group: 'unavailable',
checkTone: 'pending',
reviewLabel: null,
reviewUrl: null,
reviewState: null,
provider: null,
summary: 'Checking review status...'
})
)
expect(iconNames()).toEqual(['review', 'pending'])
expect(container.querySelector('[data-icon="pending"]')?.className).toContain('animate-spin')
})
it('does not render a dashed neutral status glyph for rows without a known check state', () => {
renderRow(
makeRow({
status: 'noReview',
group: 'noPr',
checkTone: 'neutral',
reviewLabel: null,
reviewUrl: null,
reviewState: null,
provider: null,
summary: 'No PR linked'
})
)
expect(iconNames()).toEqual(['review'])
expect(container.querySelector('[data-icon="neutral"]')).toBeNull()
expect(container.textContent).toContain('No PR linked')
})
})
@@ -1,204 +0,0 @@
import type { ReactNode } from 'react'
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi, type Mock } from 'vitest'
import type { Repo } from '../../../../../../shared/repo-types'
import type { Worktree } from '../../../../../../shared/worktree/types'
import HostedReviewActions from './HostedReviewActions'
import type { HostedReviewActionInfo } from './use-hosted-review-actions'
import type { PRInfo } from '../../../../../../shared/github/pull-request-types'
import { hostedReviewInfoFromGitHubPRInfo as gitHubPRToChecksPanelReview } from '../../../../../../shared/source-control/hosted-review/hosted-review-github'
const actionMocks = vi.hoisted(
(): {
actionError: string | null
handleMarkReadyForReview: Mock
handleCloseReview: Mock
} => ({
actionError: null,
handleMarkReadyForReview: vi.fn(),
handleCloseReview: vi.fn()
})
)
vi.mock('@/store/index', () => ({ useAppStore: () => false }))
vi.mock('./use-hosted-review-actions', () => ({
useHostedReviewActions: () => ({
merging: false,
readying: false,
stateUpdating: null,
actionError: actionMocks.actionError,
handleMerge: vi.fn(),
handleAutoMerge: vi.fn(),
handleMarkReadyForReview: actionMocks.handleMarkReadyForReview,
handleCloseReview: actionMocks.handleCloseReview,
handleReopenReview: vi.fn()
})
}))
vi.mock('@/components/ui/dropdown-menu', () => ({
DropdownMenu: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuItem: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuSeparator: () => <hr />
}))
const repo = { id: 'repo-1', path: '/repo' } as Repo
const worktree = { id: 'worktree-1' } as Worktree
function renderDraft(provider: HostedReviewActionInfo['provider']): string {
return renderToStaticMarkup(
<HostedReviewActions
review={{
provider,
number: 42,
state: 'draft',
status: 'success',
mergeable: 'UNKNOWN'
}}
repo={repo}
worktree={worktree}
onRefreshReview={vi.fn().mockResolvedValue(undefined)}
/>
)
}
describe('HostedReviewActions draft state', () => {
it.each([
['github', 'PR'],
['gitlab', 'MR']
] as const)('renders Ready as primary and Close as secondary for %s', (provider, shortLabel) => {
const markup = renderDraft(provider)
expect(markup).toContain('Mark ready for review')
expect(markup).toContain(`Close ${shortLabel}`)
expect(markup).not.toContain('Merge')
expect(markup).not.toContain('auto-merge')
})
it.each(['azure-devops', 'gitea'] as const)(
'does not misroute unsupported %s drafts through GitHub',
(provider) => {
expect(renderDraft(provider)).toBe('')
}
)
})
describe('HostedReviewActions GitHub merge selection', () => {
it.each([
{
mergeable: 'MERGEABLE',
mergeStateStatus: 'UNSTABLE',
status: 'pending',
label: 'Squash and merge',
autoMerge: false
},
{
mergeable: 'MERGEABLE',
mergeStateStatus: 'UNSTABLE',
status: 'failure',
label: 'Squash and merge',
autoMerge: false
},
{
mergeable: 'UNKNOWN',
mergeStateStatus: 'UNKNOWN',
status: 'pending',
label: 'Checking',
autoMerge: false
},
{ mergeable: 'UNKNOWN', status: 'pending', label: 'Checking', autoMerge: false },
{
mergeable: 'MERGEABLE',
mergeStateStatus: 'BLOCKED',
status: 'pending',
label: 'Enable auto-merge',
autoMerge: true
},
{
mergeable: 'MERGEABLE',
mergeStateStatus: 'BLOCKED',
reviewDecision: 'REVIEW_REQUIRED',
status: 'success',
label: 'Enable auto-merge',
autoMerge: true
},
{
mergeable: 'MERGEABLE',
mergeStateStatus: 'BLOCKED',
autoMergeEnabled: true,
status: 'pending',
label: 'Disable auto-merge',
autoMerge: true
},
{
mergeable: 'MERGEABLE',
mergeStateStatus: 'UNSTABLE',
mergeQueueRequired: true,
status: 'pending',
label: 'Merge when ready',
autoMerge: false
},
{
mergeable: 'CONFLICTING',
mergeStateStatus: 'DIRTY',
mergeQueueRequired: true,
status: 'pending',
label: 'Conflicts',
autoMerge: false
}
] as const)('renders $label for $mergeStateStatus / $status', (scenario) => {
const pr: PRInfo = {
number: 31259,
title: 'fix(i18n): restore localized editor safety copy',
state: 'open',
url: 'https://github.com/stablyai/orca/pull/31259',
updatedAt: '2026-10-21T16:31:00Z',
headSha: 'e4353ead012043474fc723e50aea198a76d32979',
autoMergeAllowed: true,
checksStatus: scenario.status,
...scenario
}
const markup = renderToStaticMarkup(
<HostedReviewActions
review={gitHubPRToChecksPanelReview(pr)}
githubPR={pr}
repo={repo}
worktree={worktree}
onRefreshReview={vi.fn().mockResolvedValue(undefined)}
/>
)
expect(markup).toContain(scenario.label)
expect(markup.includes('Enable auto-merge') || markup.includes('Disable auto-merge')).toBe(
scenario.autoMerge
)
if (scenario.label === 'Checking' || scenario.label === 'Conflicts') {
expect(markup).toMatch(/<button[^>]*disabled=""/)
}
})
})
describe('HostedReviewActions merged state', () => {
it('keeps a failed merge error visible when the reread shows the review merged', () => {
actionMocks.actionError = 'Connection lost'
try {
const markup = renderToStaticMarkup(
<HostedReviewActions
review={{
provider: 'github',
number: 42,
state: 'merged',
status: 'success',
mergeable: 'UNKNOWN'
}}
repo={repo}
worktree={worktree}
onRefreshReview={vi.fn().mockResolvedValue(undefined)}
/>
)
expect(markup).toContain('Delete Workspace')
expect(markup).toContain('Connection lost')
} finally {
actionMocks.actionError = null
}
})
})
@@ -1,188 +0,0 @@
import { describe, expect, it } from 'vitest'
import { resolveDropdownItems } from '../panel/source-control-dropdown-items'
import type {
DropdownActionInputs,
DropdownItem
} from '../../../../../../shared/source-control/dropdown/source-control-dropdown-item-types'
function inputs(overrides: Partial<DropdownActionInputs> = {}): DropdownActionInputs {
return {
stagedCount: 0,
hasUnstagedChanges: false,
hasStageableChanges: false,
hasPartiallyStagedChanges: false,
hasMessage: false,
hasUnresolvedConflicts: false,
isCommitting: false,
isRemoteOperationActive: false,
upstreamStatus: undefined,
...overrides
}
}
describe('resolveDropdownItems Create PR intent', () => {
it('enables the push-before-PR recovery action when review creation is only blocked by unpushed commits', () => {
const items = resolveDropdownItems(
inputs({
upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 },
hostedReviewCreation: {
provider: 'github',
review: null,
canCreate: false,
blockedReason: 'needs_push',
nextAction: 'push',
reviewLookupOutcome: 'not_found'
}
})
)
const byKind = Object.fromEntries(
items.filter((e) => e.kind !== 'separator').map((e) => [e.kind, e])
)
expect(byKind.create_pr.disabled).toBe(false)
expect(byKind.create_pr.hint).toBe('Push first')
expect(byKind.push_create_pr.label).toBe('Push before PR')
expect(byKind.push_create_pr.disabled).toBe(false)
})
it.each([
{
name: 'push',
provider: 'github' as const,
upstreamStatus: { hasUpstream: true as const, ahead: 2, behind: 0 },
blockedReason: 'needs_push' as const,
expectedTitle: 'Push local commits before creating a pull request'
},
{
name: 'force push',
provider: 'gitlab' as const,
upstreamStatus: {
hasUpstream: true as const,
upstreamName: 'origin/feature',
ahead: 2,
behind: 1,
behindCommitsArePatchEquivalent: true
},
blockedReason: 'needs_sync' as const,
expectedTitle: 'Force push with lease before creating a merge request'
}
])(
'keeps $name-before-review recovery available when review lookup is unavailable',
({ provider, upstreamStatus, blockedReason, expectedTitle }) => {
const items = resolveDropdownItems(
inputs({
branchCommitsAhead: 2,
upstreamStatus,
hostedReviewCreation: {
provider,
review: null,
canCreate: false,
blockedReason,
nextAction: blockedReason === 'needs_push' ? 'push' : 'sync',
reviewLookupOutcome: 'unavailable'
}
})
)
const pushCreate = items.find((item): item is DropdownItem => item.kind === 'push_create_pr')
expect(pushCreate?.disabled).toBe(false)
expect(pushCreate?.title).toBe(expectedTitle)
expect(pushCreate?.hint).toBeUndefined()
}
)
it('uses GitLab MR copy for create and push-before-create rows', () => {
const items = resolveDropdownItems(
inputs({
upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 },
hostedReviewCreation: {
provider: 'gitlab',
review: null,
canCreate: false,
blockedReason: 'needs_push',
nextAction: 'push',
reviewLookupOutcome: 'not_found'
}
})
)
const byKind = Object.fromEntries(
items.filter((e) => e.kind !== 'separator').map((e) => [e.kind, e])
)
expect(byKind.create_pr.label).toBe('Create MR')
expect(byKind.create_pr.hint).toBe('Push first')
expect(byKind.create_pr.disabled).toBe(false)
expect(byKind.push_create_pr.label).toBe('Push before MR')
expect(byKind.push_create_pr.title).toBe('Push local commits before creating a merge request')
expect(byKind.push_create_pr.disabled).toBe(false)
})
it.each(['azure-devops', 'gitea'] as const)(
'enables push-before-PR recovery for %s review creation',
(provider) => {
const items = resolveDropdownItems(
inputs({
upstreamStatus: { hasUpstream: true, ahead: 2, behind: 0 },
hostedReviewCreation: {
provider,
review: null,
canCreate: false,
blockedReason: 'needs_push',
nextAction: 'push',
reviewLookupOutcome: 'not_found'
}
})
)
const byKind = Object.fromEntries(
items.filter((e) => e.kind !== 'separator').map((e) => [e.kind, e])
)
expect(byKind.create_pr.label).toBe('Create PR')
expect(byKind.create_pr.hint).toBe('Push first')
expect(byKind.create_pr.disabled).toBe(false)
expect(byKind.push_create_pr.label).toBe('Push before PR')
expect(byKind.push_create_pr.title).toBe('Push local commits before creating a pull request')
expect(byKind.push_create_pr.disabled).toBe(false)
}
)
it.each([
['azure-devops', 'Set ORCA_AZURE_DEVOPS_TOKEN in this environment'],
['gitea', 'Set ORCA_GITEA_TOKEN in this environment']
] as const)('uses token auth copy when %s PR creation needs authentication', (provider, hint) => {
const items = resolveDropdownItems(
inputs({
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: {
provider,
review: null,
canCreate: false,
blockedReason: 'auth_required',
nextAction: 'authenticate',
reviewLookupOutcome: 'not_found'
}
})
)
const byKind = Object.fromEntries(
items.filter((e) => e.kind !== 'separator').map((e) => [e.kind, e])
)
expect(byKind.create_pr.hint).toBe(hint)
})
it('uses GitLab auth copy when MR creation needs authentication', () => {
const items = resolveDropdownItems(
inputs({
upstreamStatus: { hasUpstream: true, ahead: 0, behind: 0 },
hostedReviewCreation: {
provider: 'gitlab',
review: null,
canCreate: false,
blockedReason: 'auth_required',
nextAction: 'authenticate',
reviewLookupOutcome: 'not_found'
}
})
)
const byKind = Object.fromEntries(
items.filter((e) => e.kind !== 'separator').map((e) => [e.kind, e])
)
expect(byKind.create_pr.hint).toBe('Run glab auth login in this environment')
})
})
@@ -130,23 +130,4 @@ describe('Chat settings deep links', () => {
)
await waitFor(() => expect(scrollIntoView.mock.contexts).toContain(visuals))
})
it('activates Chat and scrolls to the separate naming section for its deep link', async () => {
const scrollIntoView = vi.spyOn(HTMLElement.prototype, 'scrollIntoView')
const { container } = render(
<NavigationHarness initialSection="appearance" targetSection="chat-names" />
)
await waitFor(() => {
expect(screen.getByRole('status', { name: 'Selected settings page' }).textContent).toBe(
'chat'
)
})
const names = container.querySelector('#chat-names')
const appearance = container.querySelector('#chat-appearance')
expect(names?.parentElement).toBe(appearance?.parentElement)
expect(names?.querySelector('[role="switch"]')).toBe(
screen.getByRole('switch', { name: 'Name chats automatically' })
)
await waitFor(() => expect(scrollIntoView.mock.contexts).toContain(names))
})
})
@@ -123,53 +123,6 @@ describe('EphemeralVmsPane', () => {
document.body.replaceChildren()
})
it('renders the skill panel and recipe, and opens the composer with the recipe selected', async () => {
const container = await renderPane()
await vi.waitFor(() => expect(container.textContent).toContain('Cloud Sandbox'))
await vi.waitFor(() => expect(container.textContent).toContain('Cloud VM setup skill'))
expect(container.textContent).toContain('What the skill does, with you')
const useButton = [...container.querySelectorAll('button')].find(
(button) => button.textContent === 'Use in workspace'
)
expect(useButton).toBeDefined()
await act(async () => {
useButton?.dispatchEvent(new MouseEvent('click', { bubbles: true }))
})
expect(storeMocks.openModal).toHaveBeenCalledWith('new-workspace-composer', {
initialRepoId: 'repo-1',
initialEphemeralVmRecipeId: 'cloud-sandbox',
telemetrySource: 'settings'
})
})
it('does not schedule a reset when clipboard completion arrives after unmount', async () => {
let finishClipboard!: () => void
vi.mocked(window.api.ui.writeClipboardText).mockReturnValueOnce(
new Promise<void>((resolve) => {
finishClipboard = resolve
})
)
const container = await renderPane()
const setTimeout = vi.spyOn(window, 'setTimeout')
try {
await act(async () => {
container.querySelector<HTMLButtonElement>('button[aria-label="Copy"]')?.click()
})
await act(async () => roots.pop()?.unmount())
setTimeout.mockClear()
await act(async () => {
finishClipboard()
await Promise.resolve()
})
expect(setTimeout.mock.calls.filter(([, delay]) => delay === 1500)).toHaveLength(0)
} finally {
setTimeout.mockRestore()
}
})
it('ignores an older refresh that finishes after a plugin-content refresh', async () => {
const listRecipeCatalog = window.api.ephemeralVm.listRecipeCatalog as ReturnType<typeof vi.fn>
let resolveInitial!: (catalog: unknown[]) => void
@@ -48,53 +48,6 @@ function makeState(overrides: Partial<AddRepoSkipFinalizationState>): AddRepoSki
}
describe('finalizeImportedRepoAfterSkip', () => {
it('keeps skipped imported worktrees visible without activating a worktree', () => {
const state = makeState({
activeRepoId: 'repo-old',
filterRepoIds: ['repo-old'],
showActiveOnly: true,
hideDefaultBranchWorkspace: false,
worktreesByRepo: {
'repo-new': [makeWorktree({ id: 'repo-new::/repo/feature', repoId: 'repo-new' })]
}
})
finalizeImportedRepoAfterSkip(state, 'repo-new')
expect(state.setActiveRepo).toHaveBeenCalledWith('repo-new')
expect(state.setFilterRepoIds).toHaveBeenCalledWith(['repo-old', 'repo-new'])
expect(state.setShowActiveOnly).toHaveBeenCalledWith(false)
expect(state.setHideDefaultBranchWorkspace).not.toHaveBeenCalled()
})
it('leaves the project filter off when the import lands with no filter', () => {
const state = makeState({ filterRepoIds: [] })
finalizeImportedRepoAfterSkip(state, 'repo-new')
expect(state.setFilterRepoIds).not.toHaveBeenCalled()
})
it('clears default-branch hiding when it would hide every imported worktree', () => {
const state = makeState({
hideDefaultBranchWorkspace: true,
worktreesByRepo: {
'repo-new': [
makeWorktree({
id: 'repo-new::/repo/main',
repoId: 'repo-new',
isMainWorktree: true,
branch: 'refs/heads/main'
})
]
}
})
finalizeImportedRepoAfterSkip(state, 'repo-new')
expect(state.setHideDefaultBranchWorkspace).toHaveBeenCalledWith(false)
})
it('preserves default-branch hiding when importing a folder project with only its root', () => {
const state = makeState({
hideDefaultBranchWorkspace: true,
@@ -178,42 +131,4 @@ describe('finalizeImportedRepoAfterSkip', () => {
expect(state.setAlwaysShowDefaultBranchWorkspace).toHaveBeenCalledWith(true)
})
it('leaves the default-branch exemption alone when sleeping workspaces are shown', () => {
const state = makeState({
showSleepingWorkspaces: true,
alwaysShowDefaultBranchWorkspace: false,
worktreesByRepo: {
'repo-new': [
makeWorktree({
id: 'repo-new::/repo/main',
repoId: 'repo-new',
isMainWorktree: true,
branch: 'refs/heads/main'
})
]
}
})
finalizeImportedRepoAfterSkip(state, 'repo-new')
expect(state.setAlwaysShowDefaultBranchWorkspace).not.toHaveBeenCalled()
})
it('still reveals the imported repo when it has no discovered worktrees yet', () => {
const state = makeState({
activeRepoId: 'repo-old',
filterRepoIds: ['repo-old'],
showActiveOnly: true,
hideDefaultBranchWorkspace: true,
worktreesByRepo: { 'repo-new': [] }
})
finalizeImportedRepoAfterSkip(state, 'repo-new')
expect(state.setActiveRepo).toHaveBeenCalledWith('repo-new')
expect(state.setFilterRepoIds).toHaveBeenCalledWith(['repo-old', 'repo-new'])
expect(state.setShowActiveOnly).toHaveBeenCalledWith(false)
expect(state.setHideDefaultBranchWorkspace).not.toHaveBeenCalled()
})
})
@@ -69,19 +69,6 @@ async function review(): Promise<void> {
}
describe('nested deletion confirmation', () => {
it('lists hidden Git registrations and warns about lost changes before any deletion', async () => {
render(
<DeleteNestedWorktreesDialog target={target} worktreeName="parent" dismissToast={vi.fn()} />
)
await review()
expect(mocks.preview).toHaveBeenCalledWith({ worktreeId: target.id, hostId: 'local' })
expect(screen.getByRole('dialog')).toHaveTextContent('including uncommitted changes')
expect(screen.getAllByRole('listitem')).toHaveLength(2)
expect(mocks.remove).not.toHaveBeenCalled()
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
expect(mocks.remove).not.toHaveBeenCalled()
})
it('sends only the reviewed set to the confirmed host and refreshes after success', async () => {
const dismissToast = vi.fn()
const onDeleted = vi.fn()
@@ -81,18 +81,6 @@ afterEach(() => {
vi.clearAllMocks()
})
it('keeps the main Force Delete button a one-time action', async () => {
const savePreference = vi.fn().mockResolvedValue(undefined)
const { container, onForceDelete } = renderRecovery(savePreference)
expect(container.querySelector('button[aria-label="More force delete options"]')).not.toBeNull()
expect(document.querySelector('[role="menuitem"]')).toBeNull()
await act(async () => click(container, 'button[data-variant="destructive"]'))
expect(savePreference).not.toHaveBeenCalled()
expect(onForceDelete).toHaveBeenCalledOnce()
})
it('keeps portaled menu focus from triggering toast focus restoration', async () => {
const { container, onOutsideBlur } = renderRecovery(vi.fn())
const trigger = container.querySelector('button[aria-label="More force delete options"]')
@@ -100,28 +100,6 @@ afterEach(cleanup)
beforeEach(() => vi.clearAllMocks())
describe('loaded deletion disclosure and existing hydration', () => {
it('shows pending and failed detail checks alongside a known dirty warning', async () => {
let failRead: (error: Error) => void = () => {
throw new Error('Status read has not started')
}
vi.mocked(getRuntimeGitStatus).mockImplementationOnce(
() =>
new Promise((_resolve, reject) => {
failRead = reject
})
)
render(<Preview worktree={target('known-dirty')} />)
expect(screen.getByText('Uncommitted or untracked changes')).toBeVisible()
expect(screen.getByText('· Checking…')).toBeVisible()
await act(async () => {
failRead(new Error('Details unavailable'))
})
expect(screen.getByText('Uncommitted or untracked changes')).toBeVisible()
expect(screen.getByText('· Details unavailable')).toBeVisible()
expect(screen.queryByRole('button')).not.toBeInTheDocument()
expect(screen.queryByText(/No uncommitted|0 changes/)).not.toBeInTheDocument()
})
it('expands and collapses a hydrated snapshot without requesting status again', async () => {
vi.mocked(getRuntimeGitStatus).mockResolvedValue({
entries: [
@@ -1,156 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReactNode } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { i18n } from '@/i18n/i18n'
import { WorktreeCardStatusSlot } from './WorktreeCardStatusSlot'
import { getWorktreeCardPrDisplay } from '../review/worktree-card-pr-display'
import { gitlabMrCheckCases } from '../../../../../../shared/source-control/checks/gitlab/gitlab-mr-checks.test-fixture'
import { buildParentPrChecksProjection } from '../../../right-sidebar/hosted-review/folder-checks/parent-pr-checks-rows'
import { FolderWorkspacePrChecksRow } from '../../../right-sidebar/hosted-review/folder-checks/FolderWorkspacePrChecksRow'
import { presentGitLabMRMergeState } from '../../../right-sidebar/hosted-review/provider-actions/gitlab-mr-merge-state'
import { getActiveChecksStatus } from '../../../right-sidebar/checks-panel/active-checks-status'
import {
ReviewChecksBadge,
ReviewChecksIndicator
} from '../metadata/WorktreeCardMetadataStatusBadges'
import { WorktreeReferenceStack } from '../review/WorktreeReferenceStack'
import type { WorkspaceReferenceDetailsMap } from '../review/workspace-reference-details'
import { getFolderWorkspaceCardPrDisplay } from '../review/folder-workspace-card-pr-display'
import { formatHostedReviewLabel } from '../../../status-bar/hosted-review-label'
import { reviewRefreshIntervalMs } from '../../../../../../shared/review-refresh-policy'
import { summarizeWorkspaceReviewChecks } from '../../../../../../shared/workspace-review-checks'
import { getWorkspaceAttachmentKey } from '../../../../../../shared/workspace-attachments/workspace-attachment-normalization'
import {
referenceRepo,
referenceWorkspace
} from '../review/workspace-reference-fixtures.test-support'
const mocks = vi.hoisted(() => ({
status: 'active',
sleeping: false
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <span data-tooltip-root="">{children}</span>,
TooltipContent: ({ children }: { children: ReactNode }) => (
<span data-tooltip-content="">{children}</span>
),
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('../activity/use-worktree-activity-status', () => ({
useWorktreeActivityStatus: () => mocks.status
}))
vi.mock('../../workspace-actions/sleep/use-worktree-sleep-state', () => ({
useIsSleepingWorktree: () => mocks.sleeping
}))
describe('WorktreeCardStatusSlot', () => {
afterEach(async () => {
await i18n.changeLanguage('en')
})
beforeEach(() => {
mocks.status = 'active'
mocks.sleeping = false
})
it.each(gitlabMrCheckCases)(
'presents the normalized $name without claiming passing',
({ review: mapped, cardLabel, tone, rowSummary, mergeLabel, directMergeAvailable }) => {
const { status } = mapped
const worktree = { ...referenceWorkspace, linkedPR: null, linkedGitLabMR: 7 }
const hostedReviewCache = {
'local::repo::feature': { data: mapped, fetchedAt: 1, linkedReviewHintKey: 'gitlab:7' }
}
const context = {
worktrees: [worktree],
repos: [referenceRepo],
settings: null,
hostedReviewCache,
prCache: {},
checksCache: {}
}
const projection = buildParentPrChecksProjection(context)
const card = renderToStaticMarkup(
<WorktreeCardStatusSlot
worktreeId={worktree.id}
showStatus
showUnreadAction={false}
isUnread={false}
unreadTooltip=""
onPointerDown={vi.fn()}
onToggleUnread={vi.fn()}
newCardStyle
prDisplay={getWorktreeCardPrDisplay(mapped, null, 7)}
/>
)
expect(card).toContain(cardLabel)
expect(card).toContain(tone)
expect(card).not.toContain('Passing')
const row = renderToStaticMarkup(
<FolderWorkspacePrChecksRow
row={projection.rows[0]}
expanded={false}
onToggle={vi.fn()}
onLoadCheckDetails={async () => null}
/>
)
expect(row).toContain(rowSummary)
expect(row).not.toMatch(/Checks passing|lucide-circle-check/)
expect(projection.summary.passing).toBe(0)
expect(presentGitLabMRMergeState(mapped)).toMatchObject({
label: mergeLabel,
directMergeAvailable
})
expect(
getActiveChecksStatus({
...context,
activeWorktreeId: worktree.id,
worktreesByRepo: { repo: [worktree] }
})
).toBe(status)
const item = { provider: 'gitlab', type: 'mr', number: 7 } as const
const details = {
[getWorkspaceAttachmentKey(item)]: { title: mapped.title, url: mapped.url, review: mapped }
} satisfies WorkspaceReferenceDetailsMap
expect(summarizeWorkspaceReviewChecks([item], details)).toMatchObject({
state: status,
passed: 0
})
expect(
renderToStaticMarkup(<WorktreeReferenceStack items={[item]} details={details} />)
).toContain(tone)
const checkLabel = { failure: 'Failing', pending: 'Pending', neutral: '' }[status]
for (const Badge of [ReviewChecksBadge, ReviewChecksIndicator]) {
const badge = renderToStaticMarkup(<Badge status={mapped.status} />)
expect(badge).toEqual(checkLabel ? expect.stringContaining(`Checks: ${checkLabel}`) : '')
}
expect(
getFolderWorkspaceCardPrDisplay({
...context,
folderWorkspaceId: 'folder',
worktreeMap: new Map([[worktree.id, worktree]]),
repoMap: new Map([['repo', referenceRepo]]),
worktreeLineageById: {},
workspaceLineageByChildKey: {
child: {
childWorkspaceKey: `worktree:${worktree.id}`,
parentWorkspaceKey: 'folder:folder',
origin: 'cli',
capture: { source: 'env-workspace', confidence: 'inferred' },
createdAt: 1
}
}
})
).toMatchObject({ state: 'open', status })
expect(formatHostedReviewLabel(mapped)).toBe(`MR !7 Open, ${status}`)
expect(reviewRefreshIntervalMs({ state: mapped.state, checksStatus: mapped.status })).toBe(
120_000
)
expect(reviewRefreshIntervalMs({ state: 'merged', checksStatus: mapped.status })).toBe(
status === 'pending' ? 60_000 : null
)
}
)
})
@@ -1,224 +0,0 @@
// @vitest-environment happy-dom
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
WorktreeCardDisplayMenuItem,
WorktreeCardLayoutMenuItem,
WorktreeCardPropertiesMenuItem
} from './worktree-card-option-items'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const setWorktreeCardMode = vi.fn()
const setWorktreeCardProperties = vi.fn()
const setAgentActivityDisplayMode = vi.fn()
const setShowAgentModels = vi.fn()
let showAgentModels: boolean | undefined
let settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
let projectGroups: unknown[] = []
let worktreeCardProperties = [
'status',
'unread',
'issue',
'linear-issue',
'jira-issue',
'pr',
'automation',
'comment',
'ports',
'inline-agents'
]
vi.mock('@/store', () => ({
useAppStore: (selector: (state: unknown) => unknown) =>
selector({
agentActivityDisplayMode: 'compact',
persistedUIReady: true,
showAgentModels,
setShowAgentModels,
projectGroups,
setAgentActivityDisplayMode,
setWorktreeCardMode,
setWorktreeCardProperties,
settings,
worktreeCardProperties
})
}))
vi.mock('@/components/ui/dropdown-menu', async () => {
const ReactModule = await import('react')
type RadioItemProps = {
children: ReactNode
onSelect?: (event: { preventDefault: () => void }) => void
onValueChange?: (value: string) => void
value: string
}
return {
DropdownMenuCheckboxItem: ({
children,
checked,
onCheckedChange
}: {
children: ReactNode
checked?: boolean
onCheckedChange?: (checked: boolean) => void
}) => (
<button
type="button"
data-checked={checked ? 'true' : 'false'}
onClick={() => onCheckedChange?.(!checked)}
>
{children}
</button>
),
DropdownMenuLabel: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuRadioGroup: ({
children,
onValueChange,
value
}: {
children: ReactNode
onValueChange?: (value: string) => void
value?: string
}) => (
<div data-radio-group-value={value}>
{ReactModule.Children.map(children, (child) =>
ReactModule.isValidElement<RadioItemProps>(child)
? ReactModule.cloneElement(child, { onValueChange })
: child
)}
</div>
),
DropdownMenuRadioItem: ({ children, onSelect, onValueChange, value }: RadioItemProps) => (
<button
type="button"
data-radio-item-value={value}
onClick={() => {
onSelect?.({ preventDefault: vi.fn() })
onValueChange?.(value)
}}
>
{children}
</button>
),
DropdownMenuSeparator: () => <hr />,
DropdownMenuSub: ({ children }: { children: ReactNode }) => <>{children}</>,
DropdownMenuSubContent: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DropdownMenuSubTrigger: ({ children }: { children: ReactNode }) => <div>{children}</div>
}
})
let root: Root | null = null
let container: HTMLDivElement | null = null
function renderMenu(): void {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
act(() => {
root?.render(
settings.experimentalNewWorktreeCardStyle ? (
<WorktreeCardDisplayMenuItem preserveWorkspaceBoardOpen={false} />
) : (
<>
<WorktreeCardLayoutMenuItem preserveWorkspaceBoardOpen={false} />
<WorktreeCardPropertiesMenuItem preserveWorkspaceBoardOpen={false} />
</>
)
)
})
}
beforeEach(() => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: false }
projectGroups = []
worktreeCardProperties = [
'status',
'unread',
'issue',
'linear-issue',
'jira-issue',
'pr',
'automation',
'comment',
'ports',
'inline-agents'
]
showAgentModels = undefined
setShowAgentModels.mockReset()
setAgentActivityDisplayMode.mockReset()
setWorktreeCardMode.mockReset()
setWorktreeCardProperties.mockReset()
})
afterEach(() => {
act(() => {
root?.unmount()
})
root = null
container?.remove()
container = null
document.body.innerHTML = ''
})
describe('worktree card menu items', () => {
it.each([false, true])(
'toggles model badges in the existing property menu (new style: %s)',
(newStyle) => {
settings.experimentalNewWorktreeCardStyle = newStyle
renderMenu()
const button = [...document.querySelectorAll('button')].find(
(candidate) => candidate.textContent === 'Agent model'
)
expect(button?.dataset.checked).toBe('true')
act(() => button?.click())
expect(setShowAgentModels).toHaveBeenCalledWith(false)
showAgentModels = false
act(() => root?.unmount())
root = null
renderMenu()
const hiddenButton = [...document.querySelectorAll('button')].find(
(candidate) =>
candidate.textContent === 'Agent model' && candidate.dataset.checked === 'false'
)
act(() => hiddenButton?.click())
expect(setShowAgentModels).toHaveBeenLastCalledWith(true)
}
)
it('applies the compact card mode preset from the visible card layout menu', () => {
renderMenu()
const compactLayoutButton = document.querySelector<HTMLButtonElement>(
'[data-radio-group-value="detailed"] [data-radio-item-value="compact"]'
)
expect(compactLayoutButton).not.toBeNull()
act(() => {
compactLayoutButton?.click()
})
expect(setWorktreeCardMode).toHaveBeenCalledWith('Compact')
})
it('keeps branch-only copy when project groups are unavailable', () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
renderMenu()
expect(container?.textContent).toContain('Branch name')
expect(container?.textContent).not.toContain('Branch / folder path')
})
it('mentions folder paths when project groups can create folder workspaces', () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
projectGroups = [{ id: 'group-1' }]
renderMenu()
expect(container?.textContent).toContain('Branch / folder path')
expect(container?.textContent).not.toContain('Branch name')
})
})
@@ -19,42 +19,6 @@ describe('submitFolderWorkspaceCreate on an Orca server', () => {
beforeEach(() => {
mocks.activateAndRevealFolderWorkspace.mockReturnValue({ primaryTabId: 'tab-1' })
})
it('preserves SSH group ownership when creating and activating a folder workspace', async () => {
const projectGroup = {
...makeProjectGroup(),
connectionId: 'ssh-1',
executionHostId: 'ssh:ssh-1'
}
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace({ connectionId: 'ssh-1' }))
const onOpenChange = vi.fn()
await submitFolderWorkspaceCreate({
projectGroup,
name: 'SSH workspace',
lastAutoName: '',
linkedWorkItem: null,
note: '',
quickAgent: null,
autoRenameBranchFromWork: false,
agentCmdOverrides: {},
isRemote: true,
runtimeEnvironmentId: null,
createFolderWorkspace,
onOpenChange
})
expect(createFolderWorkspace).toHaveBeenCalledWith({
projectGroupId: 'group-1',
name: 'SSH workspace',
connectionId: 'ssh-1',
linkedTask: null
})
expect(onOpenChange).toHaveBeenCalledWith(false)
expect(mocks.activateAndRevealFolderWorkspace).toHaveBeenCalledWith('folder-workspace-1', {
agent: null,
runtimeEnvironmentId: null
})
})
it('quotes quick-agent startup for POSIX when the folder group is a local WSL UNC path', async () => {
const createFolderWorkspace = vi.fn(async () => makeFolderWorkspace())
@@ -1,195 +0,0 @@
// @vitest-environment happy-dom
import { act, type ReactNode } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
WORKSPACE_GROUP_BY_CHOICES,
WORKSPACE_VIEW_OPTIONS,
WORKSPACE_VIEW_SECTION_LABELS,
getShownWorkspaceViewSections,
type WorkspaceViewOptionContext,
type WorkspaceViewOptionKey
} from '../../../../../../shared/workspace-view/workspace-view-options'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
const mocks = vi.hoisted(() => {
const state: Record<string, unknown> = {}
const hostOptions: { id: string }[] = []
return { state, hostOptions }
})
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: Record<string, unknown>) => unknown) => selector(mocks.state),
{ getState: () => mocks.state }
)
}))
vi.mock('./use-sidebar-host-scope-options', () => ({
useSidebarHostScopeOptions: () => ({ hostOptions: mocks.hostOptions, hostScopeOptions: [] })
}))
vi.mock('./sidebar-host-options', () => ({
shouldShowHostScopeControls: (hosts: { id: string }[]) => hosts.some((h) => h.id !== 'local'),
getSidebarHostVisibilityLabel: () => 'All hosts'
}))
vi.mock('./SidebarHostScopeMenuSection', () => ({
SidebarHostScopeMenuSection: () => <div data-option="hosts">Hosts</div>
}))
vi.mock('./SidebarRepositoryFilterSection', () => ({
default: () => <div data-option="projects">Projects</div>
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: () => null
}))
vi.mock('@/components/ui/dropdown-menu', () => {
const Pass = ({ children }: { children?: ReactNode }) => <div>{children}</div>
return {
DropdownMenuCheckboxItem: Pass,
DropdownMenuLabel: Pass,
DropdownMenuRadioGroup: Pass,
DropdownMenuRadioItem: Pass,
DropdownMenuSeparator: () => <hr />,
DropdownMenuShortcut: Pass,
DropdownMenuSub: Pass,
DropdownMenuSubContent: () => null,
DropdownMenuSubTrigger: Pass,
DropdownMenuItem: Pass
}
})
import { WorkspaceOptionsMenuItems } from './workspace-options-menu-items'
const BASE_CONTEXT: WorkspaceViewOptionContext = {
groupBy: 'none',
hideSleepingWorkspaces: false,
hideWorkspacesFromOtherDevices: false,
newCardStyle: false,
hasRemoteHosts: false,
projectCount: 1,
mayHaveServerHosts: false
}
function setState(context: WorkspaceViewOptionContext, catalogHydrated = true): void {
mocks.hostOptions = context.hasRemoteHosts ? [{ id: 'local' }, { id: 'ssh:box' }] : []
const noop = vi.fn()
mocks.state = {
repos: Array.from({ length: context.projectCount }, (_, i) => ({ id: `repo-${i}` })),
groupBy: context.groupBy,
setGroupBy: noop,
sortBy: 'name',
setSortBy: noop,
projectOrderBy: 'manual',
setProjectOrderBy: noop,
showSleepingWorkspaces: !context.hideSleepingWorkspaces,
setShowSleepingWorkspaces: noop,
alwaysShowDefaultBranchWorkspace: true,
setAlwaysShowDefaultBranchWorkspace: noop,
hideDefaultBranchWorkspace: false,
setHideDefaultBranchWorkspace: noop,
hideAutomationGeneratedWorkspaces: false,
setHideAutomationGeneratedWorkspaces: noop,
hideCliCreatedWorkspaces: false,
setHideCliCreatedWorkspaces: noop,
hideDetachedHeadWorkspaces: false,
setHideDetachedHeadWorkspaces: noop,
hideWorkspacesFromOtherDevices: context.hideWorkspacesFromOtherDevices,
setHideWorkspacesFromOtherDevices: noop,
runtimeEnvironments: context.mayHaveServerHosts ? [{ id: 'server' }] : [],
runtimeEnvironmentCatalogHydrated: catalogHydrated,
settings: {
compactWorktreeCards: false,
experimentalNewWorktreeCardStyle: context.newCardStyle
},
worktreeCardProperties: ['status'],
setWorktreeCardProperties: noop,
setWorktreeCardMode: noop,
agentActivityDisplayMode: 'compact',
setAgentActivityDisplayMode: noop,
projectGroups: [],
visibleWorkspaceHostIds: null,
setVisibleWorkspaceHostIds: noop,
setWorkspaceHostScope: noop
}
}
let container: HTMLDivElement
let root: Root
beforeEach(() => {
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
})
afterEach(() => {
act(() => root.unmount())
container.remove()
})
// Probe the control’s own text because Group by’s heading belongs to the whole section.
const PROBE: Partial<Record<WorkspaceViewOptionKey, string>> = {
groupBy: WORKSPACE_GROUP_BY_CHOICES[0].label
}
function renderedOptionKeys(): WorkspaceViewOptionKey[] {
const text = container.textContent ?? ''
return WORKSPACE_VIEW_OPTIONS.map((option) => ({
key: option.key,
at: text.indexOf(PROBE[option.key] ?? option.label)
}))
.filter((option) => option.at >= 0)
.sort((a, b) => a.at - b.at)
.map((option) => option.key)
}
const CONTEXTS: WorkspaceViewOptionContext[] = [
BASE_CONTEXT,
{
...BASE_CONTEXT,
groupBy: 'repo',
hideSleepingWorkspaces: true,
hasRemoteHosts: true,
projectCount: 3,
mayHaveServerHosts: true
},
{ ...BASE_CONTEXT, newCardStyle: true, hideWorkspacesFromOtherDevices: true }
]
describe('Workspace options menu renders from the shared option list', () => {
for (const context of CONTEXTS) {
it(`shows one control per shown option, in order: ${JSON.stringify(context)}`, () => {
setState(context)
act(() => root.render(<WorkspaceOptionsMenuItems />))
const sections = getShownWorkspaceViewSections(context)
expect(renderedOptionKeys()).toEqual(
sections.flatMap(({ options }) => options.map((option) => option.key))
)
for (const { section } of sections) {
const heading = WORKSPACE_VIEW_SECTION_LABELS[section]
if (heading) {
expect(container.textContent).toContain(heading)
}
}
})
}
it('renders every option in some context', () => {
const everShown = new Set(
CONTEXTS.flatMap((context) =>
getShownWorkspaceViewSections(context).flatMap(({ options }) =>
options.map((option) => option.key)
)
)
)
expect([...everShown].sort()).toEqual(WORKSPACE_VIEW_OPTIONS.map((o) => o.key).sort())
})
it('keeps the other-client filter while the server catalog is still loading', () => {
setState(BASE_CONTEXT, false)
act(() => root.render(<WorkspaceOptionsMenuItems />))
expect(renderedOptionKeys()).toContain('hideWorkspacesFromOtherDevices')
})
})
@@ -4,13 +4,10 @@ import { describe, expect, it } from 'vitest'
import {
computeProjectGroupHeaderDropPreview,
getProjectGroupHeaderDragBucketKey,
getProjectGroupTabOrderUpdatesForSidebarDrop,
getSidebarOrderedProjectGroupHeaderIdsByBucket,
mapSidebarProjectGroupDropIndexToSiblingInsertIndex
getProjectGroupTabOrderUpdatesForSidebarDrop
} from './project-group-header-drop'
import type { Row } from '../../../../../../shared/sidebar-list/row-types'
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
import type { Repo } from '../../../../../../shared/repo-types'
function group(id: string, overrides: Partial<ProjectGroup> = {}): ProjectGroup {
return {
@@ -29,21 +26,6 @@ function group(id: string, overrides: Partial<ProjectGroup> = {}): ProjectGroup
}
describe('getProjectGroupHeaderDragBucketKey', () => {
it('uses root for top-level groups', () => {
expect(getProjectGroupHeaderDragBucketKey(group('root'))).toBe('root')
})
it('scopes child groups to their parent bucket', () => {
const root = group('root')
const child = group('child', { parentGroupId: root.id })
const groupsById = new Map([
[root.id, root],
[child.id, child]
])
expect(getProjectGroupHeaderDragBucketKey(child, groupsById)).toBe('parent:root')
})
it('falls back to root when persisted parent metadata is missing', () => {
const orphan = group('orphan', { parentGroupId: 'missing' })
@@ -51,83 +33,6 @@ describe('getProjectGroupHeaderDragBucketKey', () => {
})
})
describe('getSidebarOrderedProjectGroupHeaderIdsByBucket', () => {
it('groups Project Group headers by effective parent bucket', () => {
const rootA = group('root-a')
const rootB = group('root-b')
const childA = group('child-a', { parentGroupId: rootA.id })
const repo = { id: 'repo-a', projectGroupId: rootA.id } as Repo
const groupsById = new Map([
[rootA.id, rootA],
[rootB.id, rootB],
[childA.id, childA]
])
const rows: Row[] = [
{
type: 'header',
key: 'project-group:root-a',
label: 'A',
count: 0,
icon: { kind: 'project' },
projectGroup: rootA
},
{
type: 'header',
key: 'repo:repo-a',
label: 'repo',
count: 0,
icon: { kind: 'project' },
repo
},
{
type: 'header',
key: 'project-group:child-a',
label: 'A child',
count: 0,
icon: { kind: 'project' },
projectGroup: childA
},
{
type: 'header',
key: 'project-group:root-b',
label: 'B',
count: 0,
icon: { kind: 'project' },
projectGroup: rootB
}
]
expect(getSidebarOrderedProjectGroupHeaderIdsByBucket(rows, groupsById)).toEqual(
new Map([
['root', ['root-a', 'root-b']],
['parent:root-a', ['child-a']]
])
)
})
})
describe('mapSidebarProjectGroupDropIndexToSiblingInsertIndex', () => {
it('keeps upward drops at the same target index after removing the source', () => {
expect(
mapSidebarProjectGroupDropIndexToSiblingInsertIndex({
sidebarDropIndex: 0,
sourceIndex: 2,
siblingCount: 2
})
).toBe(0)
})
it('shifts downward drops because the source header is removed first', () => {
expect(
mapSidebarProjectGroupDropIndexToSiblingInsertIndex({
sidebarDropIndex: 3,
sourceIndex: 0,
siblingCount: 2
})
).toBe(2)
})
})
describe('computeProjectGroupHeaderDropPreview', () => {
it('uses row-model header indices instead of mounted subset order', () => {
const preview = computeProjectGroupHeaderDropPreview({
@@ -167,27 +72,6 @@ describe('computeProjectGroupHeaderDropPreview', () => {
expect(preview).toBeNull()
})
it('uses the whole Project Group section for the final boundary slot', () => {
const preview = computeProjectGroupHeaderDropPreview({
pointerY: 400,
containerTop: 0,
scrollTop: 0,
sidebarProjectGroupHeaderIds: ['a', 'b', 'c'],
rects: [
{
groupId: 'c',
bucketKey: 'root',
headerIndex: 2,
top: 300,
bottom: 328,
sectionBottom: 380
}
]
})
expect(preview).toEqual({ dropIndex: 3, dropIndicatorY: 383 })
})
})
describe('getProjectGroupTabOrderUpdatesForSidebarDrop', () => {
@@ -208,18 +92,4 @@ describe('getProjectGroupTabOrderUpdatesForSidebarDrop', () => {
{ groupId: 'c', tabOrder: 3 }
])
})
it('returns no updates when the drop keeps the group in the same slot', () => {
const groups = [group('a', { tabOrder: 0 }), group('b', { tabOrder: 1 })]
const projectGroupById = new Map(groups.map((entry) => [entry.id, entry]))
expect(
getProjectGroupTabOrderUpdatesForSidebarDrop({
sidebarProjectGroupHeaderIds: ['a', 'b'],
draggedGroupId: 'a',
sidebarDropIndex: 1,
projectGroupById
})
).toEqual([])
})
})
@@ -147,24 +147,5 @@ for (const hostId of ['ssh:builder', 'runtime:builder'] as const) {
expect(after.some((row) => row.type === 'item' && row.worktree.id === remote.id)).toBe(true)
expect(after.some((row) => row.type === 'item' && row.worktree.id === local.id)).toBe(false)
})
it(`${hostId} ${groupBy}: combined view retains its single section collapse key`, () => {
const { local, remote } = setup(hostId)
const worktrees = [local, remote]
const rows = sections(hostId, groupBy, worktrees, new Set([laneKey]), {}, null)
expect(rows.filter((row) => row.type === 'header' && row.key === laneKey)).toHaveLength(1)
expect(rows.some((row) => row.type === 'item')).toBe(false)
const expanded = sections(
hostId,
groupBy,
worktrees,
new Set([`${laneKey}:host:${hostId}`]),
{},
null
)
expect(expanded.filter((row) => row.type === 'item')).toHaveLength(2)
expect(expanded.some((row) => row.type === 'header' && row.collapseKey !== undefined)).toBe(
false
)
})
}
}
@@ -125,30 +125,6 @@ describe('worktree keyboard cycling with a resolved active host', () => {
executionHostId: 'local'
})
})
it('steps to the previous row when the active host resolved to local', () => {
renderProbe('b', 'local')
press('up')
expect(activateAndRevealWorktree).toHaveBeenCalledWith('a', {
navigationIntent: 'user-open',
revealInSidebar: false,
executionHostId: 'local'
})
})
it('still steps normally when the active host is unqualified', () => {
renderProbe('b', null)
press('down')
expect(activateAndRevealWorktree).toHaveBeenCalledWith('c', {
navigationIntent: 'user-open',
revealInSidebar: false,
executionHostId: 'local'
})
})
})
describe('workspace keyboard cycling after closing the final tab', () => {
@@ -1,13 +1,7 @@
import { describe, expect, it } from 'vitest'
import type { HostSectionRow } from '../../../../../../shared/sidebar-list/host-section-rows'
import type { FolderWorkspaceRow } from '../../../../../../shared/sidebar-list/row-types'
import {
getCyclableRowIdentity,
getCyclableWorktreeIds,
getCyclableWorktrees,
resolveActiveCycleIdentity,
resolveCycledWorktreeId
} from './worktree-keyboard-cycle'
import { getCyclableRowIdentity, getCyclableWorktreeIds } from './worktree-keyboard-cycle'
import { getRenderedWorkspaceRowsInSidebarOrder as getCyclableWorktreeRows } from './worktree-sidebar-row-preference'
const folderRow: FolderWorkspaceRow = {
@@ -44,46 +38,6 @@ const folderRow: FolderWorkspaceRow = {
groupDepth: 0
}
describe('resolveCycledWorktreeId', () => {
const worktreeIds = ['a', 'b', 'c']
it('steps to the next and previous worktree', () => {
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: 'a', direction: 'down' })).toBe(
'b'
)
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: 'b', direction: 'up' })).toBe(
'a'
)
})
it('wraps around at both ends', () => {
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: 'c', direction: 'down' })).toBe(
'a'
)
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: 'a', direction: 'up' })).toBe(
'c'
)
})
it('enters from the matching end when the active worktree is not cyclable', () => {
expect(
resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: 'hidden', direction: 'down' })
).toBe('a')
expect(
resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: 'hidden', direction: 'up' })
).toBe('c')
expect(
resolveCycledWorktreeId({ worktreeIds, activeWorktreeId: null, direction: 'down' })
).toBe('a')
})
it('has nothing to cycle to when every group is collapsed', () => {
expect(
resolveCycledWorktreeId({ worktreeIds: [], activeWorktreeId: 'a', direction: 'down' })
).toBe(null)
})
})
describe('getCyclableWorktreeIds', () => {
const repo = {
id: 'repo-1',
@@ -107,67 +61,6 @@ describe('getCyclableWorktreeIds', () => {
}
}
it('keeps a pinned worktree cyclable when only its natural group is collapsed', () => {
const rows: HostSectionRow[] = [worktree('pinned-a', true), worktree('plain-b')]
expect(getCyclableWorktreeIds(rows, 'single-location')).toEqual(['pinned-a', 'plain-b'])
})
it('counts a duplicated pinned worktree once', () => {
const rows: HostSectionRow[] = [
worktree('dup', true),
{ ...worktree('dup'), rowKey: 'row:dup-natural' },
worktree('plain-b')
]
expect(getCyclableWorktreeIds(rows, 'duplicate-in-groups')).toEqual(['dup', 'plain-b'])
})
it('keeps same-id rows on different hosts independently cyclable', () => {
const rows: HostSectionRow[] = [
{
...worktree('shared'),
worktree: { id: 'shared', repoId: repo.id, hostId: 'local' } as never
},
{
...worktree('shared'),
rowKey: 'row:shared:ssh',
worktree: { id: 'shared', repoId: repo.id, hostId: 'ssh:host-b' } as never
}
]
expect(getCyclableWorktrees(rows, 'single-location').map((item) => item.hostId)).toEqual([
'local',
'ssh:host-b'
])
})
it('includes folder workspaces between git worktrees in visible order', () => {
const rows = [worktree('a'), folderRow, worktree('b')]
expect(getCyclableWorktreeIds(rows, 'single-location')).toEqual(['a', 'folder:folder-1', 'b'])
})
it('anchors both directions on the active folder workspace', () => {
const rows = getCyclableWorktreeRows(
[worktree('a'), folderRow, worktree('b')],
'single-location'
)
const activeWorktreeId = resolveActiveCycleIdentity({
rows,
activeWorktreeId: 'folder:folder-1',
activeWorkspaceExecutionHostId: 'local'
})
const worktreeIds = rows.map(getCyclableRowIdentity)
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId, direction: 'up' })).toBe(
getCyclableRowIdentity(rows[0])
)
expect(resolveCycledWorktreeId({ worktreeIds, activeWorktreeId, direction: 'down' })).toBe(
getCyclableRowIdentity(rows[2])
)
})
it('keeps folder placement while preferring a pinned worktree natural row', () => {
const rows = [
worktree('dup', true),
@@ -199,23 +92,4 @@ describe('getCyclableWorktreeIds', () => {
'ssh:host-b|folder:folder-1'
])
})
it('drops worktrees the sidebar elided inside a collapsed host section', () => {
const rows: HostSectionRow[] = [
{
type: 'host-header',
key: 'host:local',
hostId: 'local' as never,
kind: 'local',
label: 'This computer',
detail: '',
health: 'local',
collapsed: true,
count: 1
},
worktree('visible-after-host')
]
expect(getCyclableWorktreeIds(rows, 'single-location')).toEqual(['visible-after-host'])
})
})

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