test: remove eight low-value suites and 198 individual cases (#36650)

* test: remove low-value suites and cases

* test: fix retained timestamps and prune flaky baseline

* test: document malformed persisted-provider fixture cast

* test: restore types used by retained divergence regressions
This commit is contained in:
OrcaWin
2026-10-31 10:40:53 +00:00
committed by GitHub
parent fbc48858f6
commit 1afbd1d559
80 changed files with 43 additions and 4950 deletions
+1 -4
View File
@@ -956,7 +956,7 @@
2 test-no-wall-clock:src/main/rate-limits/service-cursor-usage.test.ts
12 test-no-wall-clock:src/main/rate-limits/service-minimax-usage.test.ts
38 test-no-wall-clock:src/main/rate-limits/service-refresh-orchestration.test.ts
6 test-no-wall-clock:src/main/rate-limits/service-zcode-usage.test.ts
4 test-no-wall-clock:src/main/rate-limits/service-zcode-usage.test.ts
1 test-no-wall-clock:src/main/runtime/agent-session/ownership/agent-session-process-identity-probe.test.ts
1 test-no-wall-clock:src/main/runtime/agent-state-rules/agent-state-rules-engine.test.ts
1 test-no-wall-clock:src/main/runtime/antigravity-readiness-transcripts.test.ts
@@ -1106,7 +1106,6 @@
1 test-no-wall-clock:src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx
1 test-no-wall-clock:src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx
5 test-no-wall-clock:src/renderer/src/components/sidebar/use-workspace-reference-details.test.tsx
1 test-no-wall-clock:src/renderer/src/components/status-bar/RuntimeHostStatusRow.test.tsx
5 test-no-wall-clock:src/renderer/src/components/status-bar/inline-usage-bars.test.tsx
4 test-no-wall-clock:src/renderer/src/components/status-bar/provider-segment-balance.test.tsx
2 test-no-wall-clock:src/renderer/src/components/status-bar/status-bar-antigravity-segment.test.tsx
@@ -1127,7 +1126,6 @@
1 test-no-wall-clock:src/renderer/src/components/terminal/initial-terminal-structured-launch.test.tsx
2 test-no-wall-clock:src/renderer/src/components/workspace-cleanup/use-workspace-cleanup-removal.agent-stop.test.tsx
6 test-no-wall-clock:src/renderer/src/components/workspace-cleanup/workspace-cleanup-confirm-remove.test.tsx
2 test-no-wall-clock:src/renderer/src/components/worktree-creation/WorktreeCreationPanel.test.tsx
2 test-no-wall-clock:src/renderer/src/hooks/ipc-events/agent-status-terminal-title-tab-write.test.ts
3 test-no-wall-clock:src/renderer/src/hooks/useAutomationDispatchEvents.test.ts
1 test-no-wall-clock:src/renderer/src/hooks/useIpcEvents-agent-status-hook-titles.test.ts
@@ -1363,7 +1361,6 @@
4 test-timeout-needs-reason:src/packages/process-host/src/run-process.test.ts
5 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
2 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx
4 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx
7 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
1 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCardAgents.send-target.test.tsx
1 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCardAgents.test.tsx
+2 -7
View File
@@ -10811,7 +10811,7 @@
"providers": ["local", "remote-runtime", "ssh", "wsl"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "remote-runtime", "ssh", "wsl"],
"coverageNotes": "Platform-neutral deterministic tests separate the durable Active Server preference from per-client connection, selected-workspace, browser-session, Add Project, and execution-host routing. The composed regression models Local desktop -> connect/navigate Windows 2 -> reveal a local terminal -> restart. Multi-client browser host overrides, multi-server profile caches, generic settings IPC rejection, local and remote workspace ownership, Add Project eligibility/owner capture/stale-result fencing, host-qualified group/folder/worktree identity, and restart reset of transient routing are covered. A visible macOS Orca desktop server plus separate paired Electron and web clients prove selected-runtime Add Project operations and disabled creation when the only listed host is blocked or unavailable; headless-server parity also passed. Live Linux, Windows, WSL, and SSH Add Project journeys remain uncollected.",
"coverageNotes": "Platform-neutral deterministic tests separate the durable Active Server preference from per-client connection, selected-workspace, browser-session, Add Project, and execution-host routing. The composed regression models Local desktop -> connect/navigate Windows 2 -> reveal a local terminal -> restart. Multi-client browser host overrides, multi-server profile caches, generic settings IPC rejection, local and remote workspace ownership, Add Project eligibility/owner capture/stale-result fencing, host-qualified group/folder/worktree identity, and restart reset of transient routing are covered. A visible macOS Orca desktop server plus separate paired Electron and web clients prove selected-runtime Add Project operations and disabled creation when the only listed host is blocked or unavailable; headless-server parity also passed. Live Linux, Windows, WSL, and SSH Add Project journeys remain uncollected. The static wrapper-rendering suite was removed during the low-value-test audit; its supplied-prop host-browser and control-visibility output assertions are no longer checked by that suite. This removal supplies no new runtime-routing evidence.",
"motivatingLinks": [
"https://github.com/stablyai/orca/pull/9687",
"https://linear.app/stably/issue/STA-4024/p1-paired-web-add-project-enables-unavailable-host-fallback-after-pr"
@@ -10821,7 +10821,7 @@
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/settings.test.ts src/main/ipc/runtime-environments/runtime-environments-pairing.test.ts src/renderer/src/store/slices/settings.test.ts src/renderer/src/store/slices/browser.test.ts src/renderer/src/components/settings/browser-session-host-selection.test.ts src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/hooks/useIpcEvents-terminal-create-surfacing.test.ts src/renderer/src/web/web-preload-api-runtime-environment.test.ts --maxWorkers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts src/renderer/src/components/sidebar/project-added-default-checkout.test.ts src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/lib/worktree-runtime-owner.test.ts src/renderer/src/store/selectors.test.ts src/renderer/src/store/slices/repos-catalog-reconciliation.test.ts src/renderer/src/store/slices/repos-project-groups.test.ts src/renderer/src/store/slices/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts --maxWorkers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/store/slices/repos-paired-runtime-add.test.ts --maxWorkers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/use-add-repo-host-selection.test.ts src/renderer/src/components/sidebar/add-repo-browse-authority.test.ts src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/store/slices/repos-paired-runtime-add.test.ts --maxWorkers=1",
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts src/renderer/src/components/sidebar/project-added-default-checkout.test.ts src/renderer/src/components/sidebar/useAddRepoLocalFolderFlow.test.ts src/renderer/src/components/sidebar/useAddRepoNestedImportFlow.test.ts src/renderer/src/components/sidebar/useAddRepoServerPathFlow.test.ts src/renderer/src/components/sidebar/useAddRepoCloneFlow.test.ts src/renderer/src/components/sidebar/useCreateRepo.default-checkout.test.ts src/renderer/src/components/sidebar/worktree-list/listing/host-filtering.test.ts src/renderer/src/lib/resolved-worktree-execution-host.test.ts src/renderer/src/lib/worktree-runtime-owner.test.ts src/renderer/src/store/selectors.test.ts src/renderer/src/store/slices/repos-catalog-reconciliation.test.ts src/renderer/src/store/slices/repos-project-groups.test.ts src/renderer/src/store/slices/repos-selected-owner-routing.test.ts src/renderer/src/store/slices/selected-host-active-workspace-identity.test.ts src/renderer/src/store/slices/worktrees-fetch-owner-routing.test.ts src/renderer/src/runtime/web-runtime-session.test.ts --maxWorkers=1",
"pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headful --workers=1 --reporter=line",
"pnpm run ensure:electron-runtime && npx playwright test tests/e2e/pr11346-selected-runtime-add.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1 --reporter=line",
@@ -10842,7 +10842,6 @@
"src/renderer/src/components/sidebar/AddProjectFromFolderDialog.test.tsx",
"src/renderer/src/components/sidebar/NonGitFolderDialog.test.tsx",
"src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts",
"src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx",
"src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx",
"src/renderer/src/components/sidebar/AddRepoSteps.default-checkout.test.ts",
"src/renderer/src/components/sidebar/project-added-default-checkout.test.ts",
@@ -10879,10 +10878,6 @@
"paired runtime browsing opens the host browser without invoking native pickFolders while desktop local and SSH routes remain intact"
]
},
{
"file": "src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx",
"assertions": ["paired runtime browsing renders the in-app host filesystem browser"]
},
{
"file": "src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx",
"assertions": [
-8
View File
@@ -5469,7 +5469,6 @@
"src/main/runtime/rpc/terminal-agent-prompt-send.test.ts": 1761,
"src/main/runtime/rpc/terminal-agent-send-guard.test.ts": 1295,
"src/main/runtime/rpc/terminal-claude-agent-view.test.ts": 1058,
"src/main/runtime/rpc/terminal-geometry-stale-handle.test.ts": 1168,
"src/main/runtime/rpc/terminal-lease-stale-handle-survival.test.ts": 1201,
"src/main/runtime/rpc/terminal-multiplex-ack-output-budget.test.ts": 1505,
"src/main/runtime/rpc/terminal-multiplex-ack-overflow-recovery-history.test.ts": 1306,
@@ -7528,7 +7527,6 @@
"src/renderer/src/components/feature-tips/feature-tip-startup-gate.test.ts": 105,
"src/renderer/src/components/feature-tips/use-session-search-tip-setup.test.tsx": 520,
"src/renderer/src/components/feature-wall/AgentCapabilitiesSetupAction.test.ts": 1690,
"src/renderer/src/components/feature-wall/ConnectIntegrationsList.test.tsx": 663,
"src/renderer/src/components/feature-wall/FeatureWallSetupWorkflowActions.test.tsx": 1589,
"src/renderer/src/components/feature-wall/FullDiskAccessSetupPrompt.test.ts": 695,
"src/renderer/src/components/feature-wall/browser-animated-visual-sequence.test.ts": 42,
@@ -8390,7 +8388,6 @@
"src/renderer/src/components/settings/ReleaseChannelSection.test.tsx": 2427,
"src/renderer/src/components/settings/repository/RepositoryGitHubAccountSection.inventory-lifetime.test.tsx": 587,
"src/renderer/src/components/settings/repository/RepositoryGitHubAccountSection.test.tsx": 662,
"src/renderer/src/components/settings/repository/RepositoryPane.test.ts": 2778,
"src/renderer/src/components/settings/repository/source-control/RepositorySourceControlAiSection.test.ts": 365,
"src/renderer/src/components/settings/RuntimeEnvironmentsPane.test.ts": 201,
"src/renderer/src/components/settings/RuntimeHostAccessForm.test.tsx": 307,
@@ -8401,7 +8398,6 @@
"src/renderer/src/components/settings/SettingsConstants.test.ts": 97,
"src/renderer/src/components/settings/SettingsFormControls.font-autocomplete.test.tsx": 789,
"src/renderer/src/components/settings/SettingsFormControls.segmented-control.test.tsx": 548,
"src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2169,
"src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 601,
"src/renderer/src/components/settings/ShortcutFilterRail.test.ts": 157,
"src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx": 785,
@@ -8514,7 +8510,6 @@
"src/renderer/src/components/sidebar/AddRepoCreateStep.test.tsx": 532,
"src/renderer/src/components/sidebar/AddRepoDialog.default-checkout.test.ts": 42,
"src/renderer/src/components/sidebar/AddRepoDialogChrome.test.tsx": 495,
"src/renderer/src/components/sidebar/AddRepoDialogStepContent.test.tsx": 1988,
"src/renderer/src/components/sidebar/AddRepoHostSelector.test.tsx": 390,
"src/renderer/src/components/sidebar/AddRepoNestedImportStep.test.tsx": 817,
"src/renderer/src/components/sidebar/AddRepoStartSteps.test.tsx": 681,
@@ -8575,7 +8570,6 @@
"src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx": 1253,
"src/renderer/src/components/sidebar/WorktreeCard.hosted-review-refresh.test.tsx": 1647,
"src/renderer/src/components/sidebar/WorktreeCard.merged-pr-display.test.tsx": 1682,
"src/renderer/src/components/sidebar/WorktreeCard.pinned-repo-icon.test.tsx": 1604,
"src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx": 1246,
"src/renderer/src/components/sidebar/WorktreeCard.quick-actions.test.tsx": 1827,
"src/renderer/src/components/sidebar/WorktreeCard.single-reference-details.test.tsx": 1662,
@@ -8596,7 +8590,6 @@
"src/renderer/src/components/sidebar/WorktreeContextMenu.test.ts": 164,
"src/renderer/src/components/sidebar/WorktreeList.card-memo-stability.test.tsx": 819,
"src/renderer/src/components/sidebar/WorktreeList.folder-workspace-rows.test.ts": 2943,
"src/renderer/src/components/sidebar/WorktreeList.group-headers.test.ts": 1786,
"src/renderer/src/components/sidebar/WorktreeList.lineage-agent-expansion-coupling.test.tsx": 3116,
"src/renderer/src/components/sidebar/WorktreeList.lineage-child-card.test.ts": 4240,
"src/renderer/src/components/sidebar/WorktreeList.lineage-child-real-card.test.tsx": 3000,
@@ -9756,7 +9749,6 @@
"src/renderer/src/components/workspace-space/SpaceWindowStatePublisher.test.tsx": 618,
"src/renderer/src/components/workspace-space/space-window-state.test.ts": 162,
"src/renderer/src/components/workspace-surface-projection.test.ts": 48,
"src/renderer/src/components/worktree-creation/WorktreeCreationPanel.test.tsx": 509,
"src/renderer/src/components/worktree-jump-palette-browser-navigation.test.tsx": 2772,
"src/renderer/src/components/worktree-jump-palette-interleaved-sections.test.tsx": 2023,
"src/renderer/src/components/worktree-jump-palette-primitives.test.tsx": 384,
@@ -171,48 +171,4 @@ describe('native cookie import preserves destination cookies', () => {
expect(removedNames).not.toContain('SID')
expect(removedNames).not.toContain('orphan')
})
it('keeps extra destination paths and subdomains alongside imported values', async () => {
sessionFromPartitionMock.mockReturnValue({
cookies: {
get: vi.fn().mockResolvedValue([
{
name: 'gist-session',
value: 'stale',
domain: 'gist.github.com',
hostOnly: true,
path: '/',
secure: true,
httpOnly: true,
sameSite: 'lax'
},
{
name: 'lookalike',
value: 'live',
domain: 'notgithub.com',
hostOnly: true,
path: '/',
secure: true,
httpOnly: true,
sameSite: 'lax'
}
]),
set: vi.fn(async () => undefined),
remove: cookiesRemoveMock,
flushStore: vi.fn().mockResolvedValue(undefined)
},
clearData: vi.fn().mockResolvedValue(undefined),
setUserAgent: vi.fn(),
getStoragePath: () => join(tmpDir, 'userData', 'Partitions', 'test')
})
createChromiumCookieTestDatabase(sourceCookiesPath, [
{ domain: '.github.com', name: 'user_session', value: 'imported-github' }
]).close()
createChromiumCookieTestDatabase(targetCookiesPath, []).close()
const result = await importCookiesFromBrowser(chromeBrowser(sourceCookiesPath), 'persist:test')
expect(result.ok).toBe(true)
expect(cookiesRemoveMock).not.toHaveBeenCalled()
})
})
-86
View File
@@ -46,13 +46,6 @@ function node(
}
describe('buildSnapshot', () => {
it('returns empty snapshot for empty tree', async () => {
const result = await buildSnapshot(makeSender([]))
expect(result.snapshot).toBe('')
expect(result.refs).toEqual([])
expect(result.refMap.size).toBe(0)
})
it('assigns refs to interactive elements', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2', '3'] }),
@@ -68,48 +61,6 @@ describe('buildSnapshot', () => {
expect(result.snapshot).toContain('[@e2] link "Home"')
})
it('renders text inputs with friendly role name', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2'] }),
node('2', 'textbox', 'Email', { backendDOMNodeId: 10 })
]
const result = await buildSnapshot(makeSender(nodes))
expect(result.snapshot).toContain('text input "Email"')
})
it('renders landmarks without refs', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2'] }),
node('2', 'navigation', 'Main Nav', { childIds: ['3'] }),
node('3', 'link', 'About', { backendDOMNodeId: 10 })
]
const result = await buildSnapshot(makeSender(nodes))
expect(result.snapshot).toContain('[Main Nav]')
expect(result.refs).toHaveLength(1)
expect(result.refs[0].name).toBe('About')
})
it('renders headings without refs', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2'] }),
node('2', 'heading', 'Welcome')
]
const result = await buildSnapshot(makeSender(nodes))
expect(result.snapshot).toContain('heading "Welcome"')
expect(result.refs).toHaveLength(0)
})
it('renders static text without refs', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2'] }),
node('2', 'staticText', 'Hello world')
]
const result = await buildSnapshot(makeSender(nodes))
expect(result.snapshot).toContain('text "Hello world"')
expect(result.refs).toHaveLength(0)
})
it('skips generic/none/presentation roles', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2'] }),
@@ -156,41 +107,4 @@ describe('buildSnapshot', () => {
expect(entry!.role).toBe('checkbox')
expect(entry!.name).toBe('Agree')
})
it('indents children under landmarks', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2'] }),
node('2', 'main', '', { childIds: ['3'] }),
node('3', 'button', 'Action', { backendDOMNodeId: 10 })
]
const result = await buildSnapshot(makeSender(nodes))
const lines = result.snapshot.split('\n')
const mainLine = lines.find((l) => l.includes('[Main Content]'))
const buttonLine = lines.find((l) => l.includes('Action'))
expect(mainLine).toBeDefined()
expect(buttonLine).toBeDefined()
expect(buttonLine!.startsWith(' ')).toBe(true)
})
it('handles a realistic page structure', async () => {
const nodes: AXNode[] = [
node('1', 'WebArea', 'page', { childIds: ['2', '3', '4'] }),
node('2', 'banner', '', { childIds: ['5'] }),
node('3', 'main', '', { childIds: ['6', '7', '8'] }),
node('4', 'contentinfo', '', {}),
node('5', 'link', 'Logo', { backendDOMNodeId: 10 }),
node('6', 'heading', 'Dashboard'),
node('7', 'textbox', 'Search', { backendDOMNodeId: 20 }),
node('8', 'button', 'Go', { backendDOMNodeId: 21 })
]
const result = await buildSnapshot(makeSender(nodes))
expect(result.refs).toHaveLength(3)
expect(result.refs.map((r) => r.name)).toEqual(['Logo', 'Search', 'Go'])
expect(result.snapshot).toContain('[Header]')
expect(result.snapshot).toContain('[Main Content]')
expect(result.snapshot).toContain('[Footer]')
expect(result.snapshot).toContain('heading "Dashboard"')
})
})
@@ -170,35 +170,4 @@ describe('Claude permission callbacks', () => {
expect.objectContaining({ type: 'prompt-cancelled', promptKey: 'perm-4' })
)
})
it('settles every in-flight prompt with null when the registry is cleared', async () => {
const control = callbacksFor()
const first = control.canUseTool(
'Bash',
{ command: 'a' },
permissionOptions('perm-5', 'tool-5', new AbortController().signal)
)
const second = control.canUseTool(
'Bash',
{ command: 'b' },
permissionOptions('perm-6', 'tool-6', new AbortController().signal)
)
for (const prompt of control.prompts.clear()) {
prompt.settle(null)
}
await expect(first).resolves.toBeNull()
await expect(second).resolves.toBeNull()
})
it('answers a user dialog deny-safe', async () => {
const control = callbacksFor()
await expect(
control.onUserDialog(
{ dialogKind: 'refusal_fallback_prompt', payload: {} },
{ signal: new AbortController().signal, requestId: 'dialog-1' }
)
).resolves.toEqual({ behavior: 'cancelled' })
})
})
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from 'vitest'
import { CodexAppServerRequestError } from './app-server/codex-app-server-request-error'
import type { CodexSession } from './codex-structured-session-state'
import { recoverCodexRewind, rewindCodexSession } from './codex-structured-rewind'
import { openCodexThread } from './codex-structured-thread-open'
function fixture(turns = ['kept', 'drop'], reverted = false) {
let history = reverted ? turns.slice(0, turns.indexOf('drop')) : turns
@@ -174,15 +173,4 @@ describe('Codex rewind', () => {
})
expect(request).toHaveBeenCalledTimes(1)
})
it('captures history mode at both start and resume without changing defaults', async () => {
for (const resumeThreadId of [null, 'thread']) {
const request = vi.fn(async (_method: string, _params?: unknown) => ({
thread: { id: 'thread', historyMode: 'legacy' }
}))
expect(
await openCodexThread({ request }, { cwd: '/workspace', resumeThreadId }, 10)
).toMatchObject({ historyMode: 'legacy' })
expect(request.mock.calls[0]?.[1]).not.toHaveProperty('historyMode')
}
})
})
@@ -1,4 +1,5 @@
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -57,17 +58,6 @@ function mutate(
}
describe('real-home user hook trust rebasing', () => {
it('writes without touching config.toml when no hook moves', () => {
const user = command('user-hook')
mutate(
{ Stop: [{ hooks: [user] }] },
{ Stop: [{ hooks: [user] }, { hooks: [command('orca')] }] }
)
expect(existsSync(hooksPath)).toBe(true)
expect(existsSync(configPath)).toBe(false)
})
it('finds multiple shifted user hooks, including a handler from a mixed group', async () => {
const orca = command('orca-hook')
const first = command('first-user')
@@ -172,11 +172,4 @@ describe('repairing configs broken by #22592', () => {
expect(repairOrcaDuplicateTrustTables(broken)).toBe(broken)
expect(warn).toHaveBeenCalledTimes(1)
})
it('does not touch a valid config', () => {
const config = ['["projects"."/repo-f"]', '"trust_level" = "trusted"', ''].join('\n')
expect(repairOrcaDuplicateTrustTables(config)).toBe(config)
expect(warn).not.toHaveBeenCalled()
})
})
@@ -8,10 +8,7 @@ import { DaemonAutomaticWorkGuard } from './daemon-automatic-work-guard'
import { DaemonProcessGrowthMonitor } from './daemon-process-growth-monitor'
import type { SubprocessHandle } from './session-subprocess-handle'
import { TerminalHost, type TerminalHostOptions } from './terminal-host'
import {
resolveTerminalSpawnAdmissionMode,
TERMINAL_SPAWN_ADMISSION_ENV_VAR
} from './terminal-spawn-gate'
import { resolveTerminalSpawnAdmissionMode } from './terminal-spawn-gate'
type SpawnSubprocess = TerminalHostOptions['spawnSubprocess']
@@ -105,19 +102,6 @@ describe('desktop restore admission', () => {
})
}
it('enforces on desktop unless the rollback switch asks to observe', () => {
expect(resolveTerminalSpawnAdmissionMode({})).toBe('enforce')
expect(
resolveTerminalSpawnAdmissionMode({ [TERMINAL_SPAWN_ADMISSION_ENV_VAR]: 'enforce' })
).toBe('enforce')
expect(resolveTerminalSpawnAdmissionMode({ [TERMINAL_SPAWN_ADMISSION_ENV_VAR]: 'bogus' })).toBe(
'enforce'
)
expect(
resolveTerminalSpawnAdmissionMode({ [TERMINAL_SPAWN_ADMISSION_ENV_VAR]: ' Observe ' })
).toBe('observe')
})
it('restores the visible pane at once, at most 8 hidden agents, and keeps the rest sleeping', async () => {
startDesktopDaemon()
const hidden = Array.from({ length: 19 }, (_, i) =>
@@ -195,24 +195,4 @@ describe('createPtySubprocess', () => {
Object.assign(process.env, savedGitConfigEnv)
}
})
it('guards a trusted daemon agent whose launch command is wrapped', async () => {
const proc = mockPtyProcess()
spawnMock.mockReturnValue(proc)
await createPtySubprocess({
sessionId: 'trusted-wrapped-agent',
cols: 80,
rows: 24,
command: 'cd /repo && custom-agent-wrapper',
launchAgent: 'claude',
env: { SHELL: '/bin/bash' }
})
const spawnEnv = spawnMock.mock.calls.at(-1)?.[2]?.env as Record<string, string>
expect(spawnEnv.GIT_TERMINAL_PROMPT).toBe('0')
expect(spawnEnv.GCM_INTERACTIVE).toBe('never')
expect(Object.values(spawnEnv)).toContain('credential.interactive')
expect(Object.values(spawnEnv)).toContain('credential.guiPrompt')
})
})
@@ -35,50 +35,6 @@ describe('listSimulatorDevices', () => {
platformMock.mockReturnValue('darwin')
})
it('parses simctl devices on macOS', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
null,
JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [
{
name: 'iPhone 17 Pro',
udid: 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE',
state: 'Shutdown',
isAvailable: true
}
]
}
}),
''
)
})
await expect(listSimulatorDevices()).resolves.toEqual([
{
name: 'iPhone 17 Pro',
udid: 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE',
state: 'Shutdown',
runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0',
isAvailable: true
}
])
expect(execFileMock).toHaveBeenCalledWith(
'xcrun',
['simctl', 'list', 'devices', '-j'],
{ timeout: 15_000 },
expect.any(Function)
)
})
it('returns no devices on non-macOS hosts', async () => {
platformMock.mockReturnValue('linux')
await expect(listSimulatorDevices()).resolves.toEqual([])
expect(execFileMock).not.toHaveBeenCalled()
})
it('normalizes missing simctl errors for UI and CLI callers', async () => {
execFileMock.mockImplementation((_command, _args, _options, callback) => {
callback(
-30
View File
@@ -69,36 +69,6 @@ describe('addSparseWorktree', () => {
platformSpy.mockRestore()
})
it('enables long paths on the Windows commands that actually write the deep checkout', async () => {
platformSpy.mockReturnValue('win32')
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
await addSparseWorktree('C:\\repo', 'C:\\repo-feature', 'feature/test', ['packages/web'])
const calls = getGitCalls()
expect(calls).toEqual(
expect.arrayContaining([
'git -c core.longpaths=true sparse-checkout set -- packages/web',
'git -c core.longpaths=true checkout feature/test'
])
)
})
it('omits the long-path option on non-Windows hosts', async () => {
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
await addSparseWorktree('/repo', '/repo-feature', 'feature/test', ['packages/web'])
const calls = getGitCalls()
expect(calls).toEqual(
expect.arrayContaining([
'git sparse-checkout set -- packages/web',
'git checkout feature/test'
])
)
expect(calls.some((call) => call.includes('core.longpaths'))).toBe(false)
})
it('separates sparse checkout directory operands from options', async () => {
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
@@ -142,14 +142,6 @@ describe('reconcileDriveCheckoutSuspects', () => {
})
}
it('does nothing for checkouts outside the Windows-checkout config', async () => {
mocks.appliesTo = new Set()
await reconcileDriveCheckoutSuspects(repo, [], {})
changed(['a.ts'])
expect(await reconcileDriveCheckoutSuspects(repo, [], {})).toEqual([])
expect(mocks.gitStreamStdout).not.toHaveBeenCalled()
})
it('rechecks only suspects minimal status reported clean, keeping the dirty ones', async () => {
await touchFiles(['clean.ts', 'hidden edit.ts', 'already.ts'])
await reconcileDriveCheckoutSuspects(repo, [], {})
+2 -54
View File
@@ -4,14 +4,10 @@ const mocks = vi.hoisted(() => ({ gitExecFileAsync: vi.fn() }))
vi.mock('./runner', () => ({ gitExecFileAsync: mocks.gitExecFileAsync }))
import { GIT_READ_TIMEOUT_MS } from './command-runner/git-command-timeout'
import { GitAdmissionScheduler } from './command-runner/git-subprocess-admission'
import type { GitAdmissionTier } from './command-runner/git-exec-options'
import { WSL_GIT_READ_ENVIRONMENT_WAIT_MS } from './wsl-git-read-environment'
import {
measureRetargetDivergence,
RETARGET_DIVERGENCE_BUDGET_MS
} from './worktree-base-divergence'
import { measureRetargetDivergence } from './worktree-base-divergence'
type ExecOptions = {
cwd: string
@@ -21,10 +17,6 @@ type ExecOptions = {
admissionTier?: GitAdmissionTier
}
function callOptions(): ExecOptions[] {
return mocks.gitExecFileAsync.mock.calls.map((call) => call[1] as ExecOptions)
}
function subcommands(): string[] {
return mocks.gitExecFileAsync.mock.calls.map((call) => (call[0] as string[])[0]!)
}
@@ -73,32 +65,6 @@ describe('measureRetargetDivergence deadlines', () => {
}
})
it('puts every probe under one shared budget, not a budget each', async () => {
answerProbes('3\n')
await expect(
measureRetargetDivergence('/repo', 'refs/heads/main', 'refs/remotes/origin/main')
).resolves.toBe('within')
expect(subcommands()).toEqual(['rev-list', 'rev-list', 'merge-base'])
const signals = callOptions().map((options) => options.signal)
// One budget spans all staged probes; per-probe budgets would add together.
expect(new Set(signals).size).toBe(1)
expect(signals[0]).toBeInstanceOf(AbortSignal)
})
it('also gives each probe a command timeout well below git default read deadline', async () => {
answerProbes('3\n')
await measureRetargetDivergence('/repo', 'refs/heads/main', 'refs/remotes/origin/main')
// Signals cover pre-spawn waits; command timeouts still bound a hung spawn.
for (const options of callOptions()) {
expect(options.timeout).toBe(RETARGET_DIVERGENCE_BUDGET_MS)
}
expect(RETARGET_DIVERGENCE_BUDGET_MS).toBeLessThan(GIT_READ_TIMEOUT_MS)
})
it('really aborts the in-flight probes when the shared budget expires', async () => {
mocks.gitExecFileAsync.mockImplementation(
(_args: string[], options: ExecOptions) =>
@@ -121,12 +87,6 @@ describe('measureRetargetDivergence deadlines', () => {
).resolves.toBe('unknown')
})
it('clears the WSL read-environment wait, which starts before any command timeout', () => {
// Leave walk headroom after the first WSL environment probe.
expect(RETARGET_DIVERGENCE_BUDGET_MS).toBeGreaterThan(WSL_GIT_READ_ENVIRONMENT_WAIT_MS)
expect(RETARGET_DIVERGENCE_BUDGET_MS).toBeLessThan(GIT_READ_TIMEOUT_MS)
})
it('stops the probes when the create itself is cancelled, without waiting for the budget', async () => {
mocks.gitExecFileAsync.mockImplementation(
(_args: string[], options: ExecOptions) =>
@@ -194,16 +154,4 @@ describe('measureRetargetDivergence deadlines', () => {
).resolves.toBe('exceeded')
expect(subcommands()).not.toContain('merge-base')
})
it('routes every probe to the caller-named WSL distro', async () => {
answerProbes('1\n')
await measureRetargetDivergence('/repo', 'refs/heads/main', 'refs/remotes/origin/main', {
wslDistro: 'Ubuntu'
})
for (const options of callOptions()) {
expect(options).toMatchObject({ cwd: '/repo', wslDistro: 'Ubuntu' })
}
})
})
@@ -183,12 +183,4 @@ describe('finalizeView verticalGroupByFields normalization', () => {
])
}
})
it('omits the key entirely when the host never sent it (wire-compat shape)', () => {
const finalized = finalizeView(base, [])
expect(finalized.ok).toBe(true)
if (finalized.ok) {
expect('verticalGroupByFields' in finalized.view).toBe(false)
}
})
})
@@ -43,15 +43,6 @@ describe('guarded Grok hook config mutation', () => {
expect(readFileSync(configPath, 'utf8')).toBe(userEdit)
})
it('treats a newer user deletion as a generation mismatch', async () => {
const configPath = makeConfigPath()
const installed = '{"hooks":{"SessionStart":[]}}\n'
writeFileSync(configPath, installed)
rmSync(configPath)
await expect(removeGrokHookConfigIfUnchanged(configPath, installed)).resolves.toBe(false)
})
it('does not overwrite a newer user edit', async () => {
const configPath = makeConfigPath()
const installed = '{"hooks":{"SessionStart":[]}}\n'
@@ -152,36 +143,6 @@ describe('guarded Grok hook config mutation', () => {
expect(statSync(configPath).mode & 0o777).toBe(0o600)
})
// Grok rejects global hook files with nlink !=1; publishing through a hard link creates a session-failure window.
it('never publishes the config through a second hard link', async () => {
const configPath = makeConfigPath()
const installed = '{"hooks":{"SessionStart":[]}}\n'
writeFileSync(configPath, installed)
const linkCounts: number[] = []
const sampler = setInterval(() => {
try {
linkCounts.push(statSync(configPath).nlink)
} catch {
/* absent mid-swap is fine; only a second link is not */
}
}, 0)
try {
for (let i = 0; i < 40; i += 1) {
const next = `{"hooks":{"SessionStart":[],"n":${i}}}\n`
await expect(
writeGrokHookConfigIfUnchanged(configPath, readFileSync(configPath, 'utf8'), next)
).resolves.toBe(true)
}
} finally {
clearInterval(sampler)
}
expect(linkCounts.length).toBeGreaterThan(0)
expect(Math.max(...linkCounts)).toBe(1)
expect(statSync(configPath).nlink).toBe(1)
})
it('removes the config and reports the removal', async () => {
const configPath = makeConfigPath()
const installed = '{"hooks":{"SessionStart":[]}}\n'
@@ -202,16 +202,4 @@ describe('workspace cleanup execution-host routing', () => {
expect(result.candidates[0]?.blockers).toContain('git-status-error')
expect(result.candidates[0]?.executionHostId).toBe('ssh:alpha')
})
it('still routes a genuinely local repo to this machine', async () => {
const store = makeStore([makeRepo({ path: '/local/repo' })], {
[WORKTREE_ID]: makeMeta()
})
const result = await scanWorkspaceCleanup(store)
expect(listRepoWorktreesMock).toHaveBeenCalled()
expect(getStatusMock).toHaveBeenCalled()
expect(result.candidates[0]?.executionHostId).toBe('local')
})
})
@@ -1,7 +1,5 @@
import type * as ExecCommandModule from '../../ssh/remote-exec/remote-exec-command'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { ensureRemoteOrcadNodeRuntime } from './orcad-remote-node-runtime'
import {
@@ -11,14 +9,9 @@ import {
import { execCommand } from '../../ssh/remote-exec/remote-exec-command'
import { uploadRemoteInstallDirectory } from './remote-install-transfers'
import { getRemoteHostPlatform } from '../../ssh/ssh-remote-platform'
import { pinnedNodeRuntimeAsset } from '../../../shared/node-runtime-pin'
import { DesktopDownloadUnreachableError } from '../../node-runtime/pinned-runtime-materializer'
import { setNodeDistMirrorSettingReader } from '../../node-runtime/node-dist-mirror-setting'
import {
HOST_NODE_DOWNLOAD_FAILED,
HOST_NODE_DOWNLOAD_OK,
NodeRuntimeUnreachableError
} from './remote-node-runtime-host-download'
import { HOST_NODE_DOWNLOAD_OK } from './remote-node-runtime-host-download'
import { unusedOrcadHostTransport } from '../../ssh/unused-host-transport.test-fixture'
vi.mock('../../ssh/remote-exec/remote-exec-command', async (importOriginal) => ({
@@ -33,7 +26,6 @@ const conn = unusedOrcadHostTransport()
const host = getRemoteHostPlatform('linux-x64')
const TARGET = 'linux-x64-glibc'
const OFFLINE_PATH = '/Users/me/Library/Application Support/Orca/orcad-artifacts/node/archives/x'
const refused = (): Promise<string> => Promise.reject(new Error('net::ERR_CONNECTION_REFUSED'))
afterEach(() => {
vi.mocked(execCommand).mockReset()
@@ -72,18 +64,6 @@ function ensure(archivePath: () => Promise<string>, signal?: AbortSignal) {
}
describe('pinned Node when this computer cannot download it', () => {
it('has the host download it and promotes that copy, with nothing uploaded', async () => {
const commands = freshHost(HOST_NODE_DOWNLOAD_OK)
const result = await ensure(refused)
expect(result.transfer).toBe('host-downloaded')
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
const promote = commands.find((command) => command.includes('tar -xzf'))
expect(promote).toContain(pinnedNodeRuntimeAsset(TARGET).archive)
expect(promote).toContain(pinnedNodeRuntimeAsset(TARGET).executableSha256)
})
it('also tries the host when the desktop got a file that failed the pin', async () => {
freshHost(HOST_NODE_DOWNLOAD_OK)
const blockPage = () => Promise.reject(new Error('Node archive checksum mismatch'))
@@ -91,68 +71,6 @@ describe('pinned Node when this computer cannot download it', () => {
await expect(ensure(blockPage)).resolves.toMatchObject({ transfer: 'host-downloaded' })
})
it('names both attempts and the offline copy path when the host cannot download either', async () => {
freshHost(
`${HOST_NODE_DOWNLOAD_FAILED}\nORCA_RUNTIME_EXIT=7\ncurl: (7) Failed to connect to nodejs.org`
)
const failure = await ensure(refused).catch((error: unknown) => error)
expect(failure).toBeInstanceOf(NodeRuntimeUnreachableError)
const message = String(failure)
expect(message).toContain('net::ERR_CONNECTION_REFUSED')
expect(message).toContain('curl: (7) Failed to connect to nodejs.org')
expect(message).toContain(OFFLINE_PATH)
expect(message).toContain('Settings > Advanced > Network > Node.js download mirror')
expect(message).toContain("NODEJS_ORG_MIRROR in this computer's environment")
expect(failure).toMatchObject({ mirror: 'unset' })
expect(uploadRemoteInstallDirectory).not.toHaveBeenCalled()
})
it('records that both attempts went through the configured mirror', async () => {
freshHost(`${HOST_NODE_DOWNLOAD_FAILED}\nmirror: curl exit 7`)
setNodeDistMirrorSettingReader(() => 'https://npmmirror.com/mirrors/node')
try {
const failure = await ensure(refused).catch((error: unknown) => error)
expect(failure).toMatchObject({ name: 'NodeRuntimeUnreachableError', mirror: 'tried' })
expect(String(failure)).toContain('check the mirror of nodejs.org/dist in Settings')
} finally {
setNodeDistMirrorSettingReader(null)
}
})
it('offers no mirror for a build nodejs.org/dist does not carry', async () => {
freshHost(`${HOST_NODE_DOWNLOAD_FAILED}\nnodejs.org: curl exit 7`)
const failure = await ensureRemoteOrcadNodeRuntime({
conn,
host: getRemoteHostPlatform('linux-arm64'),
slotDir: '/home/me/.orca-remote/orcad-0.1.0+abc',
target: 'linux-arm64-musl',
archivePath: refused
}).catch((error: unknown) => error)
expect(failure).toMatchObject({ name: 'NodeRuntimeUnreachableError', mirror: 'unsupported' })
expect(String(failure)).not.toContain('Node.js download mirror')
})
it("quotes only this computer's short reason, not its standalone advice", async () => {
freshHost(`${HOST_NODE_DOWNLOAD_FAILED}\nnodejs.org: curl exit 7`)
const desktop = () =>
Promise.reject(
new DesktopDownloadUnreachableError(
"This computer couldn't download Node ... so the host doesn't need internet access",
'https://nodejs.org/dist/x: net::ERR_CONNECTION_REFUSED',
new Error('net::ERR_CONNECTION_REFUSED')
)
)
const message = String(await ensure(desktop).catch((error: unknown) => error))
expect(message).toContain(
'This computer: https://nodejs.org/dist/x: net::ERR_CONNECTION_REFUSED.'
)
expect(message).not.toContain("the host doesn't need internet access")
})
it('does not try the host once the connect was cancelled', async () => {
const commands = freshHost(HOST_NODE_DOWNLOAD_OK)
const controller = new AbortController()
@@ -164,18 +82,4 @@ describe('pinned Node when this computer cannot download it', () => {
await expect(ensure(cancelled, controller.signal)).rejects.toThrow('aborted')
expect(commands.some((command) => command.includes('command -v curl'))).toBe(false)
})
it('still uploads the desktop copy when there is one', async () => {
const commands = freshHost(HOST_NODE_DOWNLOAD_OK)
const cache = mkdtempSync(join(tmpdir(), 'orca-runtime-cache-'))
const archive = join(cache, pinnedNodeRuntimeAsset(TARGET).archive)
writeFileSync(archive, 'archive')
try {
await expect(ensure(async () => archive)).resolves.toMatchObject({ transfer: 'uploaded' })
expect(uploadRemoteInstallDirectory).toHaveBeenCalledTimes(1)
expect(commands.some((command) => command.includes('command -v curl'))).toBe(false)
} finally {
rmSync(cache, { recursive: true, force: true })
}
})
})
-40
View File
@@ -103,22 +103,6 @@ describe('orcad profile CLI launcher', () => {
}
)
it.skipIf(process.platform === 'win32')(
'refreshes the launcher when the host switches server slots',
async () => {
await writeCli()
await prepareOrcadCliLauncher()
roots.install = join(directory, 'updated server')
await writeCli()
await prepareOrcadCliLauncher()
const launcher = getOrcadCliLauncherPath()
if (!launcher) {
throw new Error('CLI launcher is missing')
}
expect(await readFile(launcher, 'utf8')).toContain(roots.install)
}
)
it('leaves old slots without a CLI usable and clears a previous launcher', async () => {
await writeCli()
await prepareOrcadCliLauncher()
@@ -127,24 +111,6 @@ describe('orcad profile CLI launcher', () => {
expect(getOrcadCliLauncherPath()).toBeNull()
})
describe('on a Linux host', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
})
it('refreshes both CLI names when the runtime switches server slots', async () => {
await writeCli()
await prepareOrcadCliLauncher()
roots.install = join(directory, 'updated server')
await writeCli()
await prepareOrcadCliLauncher()
const binDir = join(roots.profile, 'cli', 'bin')
const script = await readFile(join(binDir, 'orca-ide'), 'utf8')
expect(script).toContain(roots.install)
expect(script).toBe(await readFile(join(binDir, 'orca'), 'utf8'))
})
})
describe('on a Windows host', () => {
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
@@ -192,11 +158,5 @@ describe('orcad profile CLI launcher', () => {
expect(await readFile(join(binDir(), 'orca.exe'), 'utf8')).toBe('native launcher v2')
expect(await readConfig()).toContain(`cli=${join(roots.install, 'out', 'cli', 'index.js')}`)
})
it('leaves a slot without the native launcher with no orca command', async () => {
await writeCli()
await prepareOrcadCliLauncher()
expect(getOrcadCliLauncherPath()).toBeNull()
})
})
})
@@ -8,7 +8,6 @@ import { RateLimitService } from './service'
import { fetchClaudeRateLimits } from './claude-fetcher'
import { fetchCodexRateLimits } from './codex-fetcher'
import { fetchZcodeRateLimits } from './zcode-usage-fetcher'
import { hasZcodePlanApiKey } from '../zcode/zcode-plan-api-key-store'
import {
deferred,
okProvider,
@@ -88,39 +87,6 @@ describe('RateLimitService zcode plan credentials', () => {
vi.mocked(fetchCodexRateLimits).mockResolvedValue(okProvider('codex', 20))
})
it('fetches zcode with the site-resolved plan credential when a resolver is set', async () => {
const service = new RateLimitService()
service.setZcodePlanConfigResolver(() => ({ site: 'bigmodel', apiKey: 'glm-key' }))
vi.mocked(hasZcodePlanApiKey).mockReturnValue(true)
vi.mocked(fetchZcodeRateLimits).mockResolvedValueOnce(okProvider('zcode', 33, Date.now()))
await service.refresh()
expect(fetchZcodeRateLimits).toHaveBeenCalledTimes(1)
expect(fetchZcodeRateLimits).toHaveBeenCalledWith({
signal: expect.any(AbortSignal),
planCredential: { apiKey: 'glm-key', baseUrl: 'https://open.bigmodel.cn' }
})
const state = service.getState()
expect(state.zcode?.status).toBe('ok')
expect(state.zcode?.session?.usedPercent).toBe(33)
expect(state.zcodePlanApiKeyConfigured).toBe(true)
})
it('passes no plan credential while no key is saved and still fetches via the CLI config', async () => {
const service = new RateLimitService()
service.setZcodePlanConfigResolver(() => ({ site: 'zai', apiKey: '' }))
vi.mocked(fetchZcodeRateLimits).mockResolvedValueOnce(okProvider('zcode', 12, Date.now()))
await service.refresh()
expect(fetchZcodeRateLimits).toHaveBeenCalledWith({
signal: expect.any(AbortSignal),
planCredential: null
})
expect(service.getState().zcode?.session?.usedPercent).toBe(12)
})
it('surfaces a resolver failure as a zcode-only error without fetching', async () => {
const service = new RateLimitService()
service.setZcodePlanConfigResolver(() => {
@@ -2,7 +2,8 @@ import { describe, expect, it, vi } from 'vitest'
import { mkdtemp, realpath, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { enoent, resolveAuthorizedPathMock, statMock } from './orca-runtime-files-mock-registry'
import { resolveAuthorizedPathMock, statMock } from './orca-runtime-files-mock-registry'
import {
createRuntimeFileCommands,
useRuntimeFileCommandsLifecycle
@@ -38,34 +39,6 @@ describe('RuntimeFileCommands', () => {
describe('resolveTerminalPath', () => {
const { tempDirs, tempFile } = useTerminalArtifactTempFiles()
it('returns an absolute open target for an existing local temp path outside the worktree', async () => {
const artifactPath = await tempFile('result.json', '{}')
const { commands } = createRuntimeFileCommands({ path: '/repo' })
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
statMock.mockResolvedValue({ isDirectory: () => false, size: 12, dev: 1, ino: 2, mtimeMs: 3 })
const canonicalPath = await realpath(artifactPath)
const result = await resolveTerminalArtifactPath(commands, artifactPath)
expect(result).toMatchObject({
worktree: 'wt-1',
relativePath: null,
absolutePath: canonicalPath,
exists: true,
isDirectory: false,
openTarget: {
kind: 'absolute-file',
provider: 'local',
absolutePath: canonicalPath
}
})
expect(result.openTarget?.kind === 'absolute-file' ? result.openTarget.grantId : '').toMatch(
/\S/
)
expect(resolveAuthorizedPathMock).not.toHaveBeenCalledWith(canonicalPath, expect.anything())
expect(statMock).not.toHaveBeenCalled()
})
// Why: a chat link or typed path carries no terminal; the caller's terminal access is the authority.
it('grants a writable file outside every workspace with no terminal or chat context', async () => {
const artifactPath = await tempFile('handoff.md', '# handoff')
@@ -108,16 +81,6 @@ describe('RuntimeFileCommands', () => {
).resolves.toEqual({ ok: true })
})
it('reports a missing file outside every workspace as absent', async () => {
const { commands } = createRuntimeFileCommands({ path: '/repo' })
const missingPath = join(tmpdir(), `orca-missing-${process.pid}-${Date.now()}.md`)
const result = await commands.resolveTerminalPath('id:wt-1', missingPath, null, 'client-a')
expect(result).toMatchObject({ relativePath: null, exists: false })
expect(result.openTarget).toBeUndefined()
})
// Why: this machine's home is the Windows user's, not the distro user's.
it('refuses tilde paths when the workspace runs in WSL', async () => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
@@ -157,24 +120,6 @@ describe('RuntimeFileCommands', () => {
).rejects.toThrow('Not a regular file')
})
it('resolves a relative path against the terminal cwd, not a stale client cwd', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-terminal-artifact-'))
tempDirs.push(dir)
await writeFile(join(dir, 'result.json'), '{}')
const { commands } = createRuntimeFileCommands({ path: '/repo' })
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
statMock.mockRejectedValue(enoent())
const result = await resolveTerminalArtifactPath(commands, 'result.json', dir)
expect(result).toMatchObject({
relativePath: 'result.json',
absolutePath: '/repo/result.json',
exists: false
})
expect(result.openTarget).toBeUndefined()
})
it('uses the canonical local temp artifact path for the exact grant', async () => {
const dir = await mkdtemp(join(tmpdir(), 'orca-terminal-artifact-'))
tempDirs.push(dir)
@@ -87,13 +87,6 @@ async function restore(runtime: InstanceType<typeof OrcaRuntimeService>): Promis
}
describe('restoring a pane its host recorded an agent in', () => {
it('resumes the hook store’s conversation instead of replaying the launch command', async () => {
const { runtime, spawn } = makeRuntime([codexRow()], 'codex')
await restore(runtime)
expect(spawn).toHaveBeenCalledOnce()
expect(spawn.mock.calls[0]![0].command).toMatch(RESUMES)
})
it('resumes an agent typed into a shell, which no launch command names', async () => {
const { runtime, spawn } = makeRuntime([codexRow()])
await restore(runtime)
@@ -159,22 +152,6 @@ describe('restoring a pane its host recorded an agent in', () => {
expectFreshLaunch(spawn.mock.calls[0]![0].command)
})
it('launches the tab’s agent fresh when another pane holds the conversation live', async () => {
const otherPaneKey = 'other-tab:44444444-4444-4444-8444-444444444444'
const { runtime, spawn } = makeRuntime(
[codexRow(), codexRow({ paneKey: otherPaneKey })],
'codex',
{ presence: 'live' }
)
vi.spyOn(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the runtime's protected paneHoldsSession; the other pane stands in for a bound live terminal.
runtime as unknown as { paneHoldsSession: (paneKey: string) => boolean },
'paneHoldsSession'
).mockImplementation((paneKey) => paneKey === otherPaneKey)
await restore(runtime)
expectFreshLaunch(spawn.mock.calls[0]![0].command)
})
it('falls back to the tab’s agent launch when the newest row is a structured host’s', async () => {
const { runtime, spawn } = makeRuntime([codexRow({ structuredHost: 'owned' })], 'codex')
await restore(runtime)
-96
View File
@@ -29,60 +29,6 @@ function makeRequest(method: string, params?: unknown): RpcRequest {
}
describe('ssh RPC methods', () => {
it('returns the registered SSH target state', async () => {
const state = {
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0
}
getRegisteredSshStateMock.mockReturnValueOnce(state)
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
const response = await dispatcher.dispatch(makeRequest('ssh.getState', { targetId: 'ssh-1' }))
expect(getRegisteredSshStateMock).toHaveBeenCalledWith('ssh-1')
expect(response).toMatchObject({ ok: true, result: { state } })
})
it('connects through the registered desktop SSH lifecycle', async () => {
const state = {
targetId: 'ssh-1',
status: 'connected',
error: null,
reconnectAttempt: 0
}
connectRegisteredSshTargetMock.mockResolvedValueOnce(state)
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
const response = await dispatcher.dispatch(makeRequest('ssh.connect', { targetId: 'ssh-1' }))
expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1', { automatic: false })
expect(response).toMatchObject({ ok: true, result: { state } })
})
it("keeps a paired client's automatic connect from lifting a cancelled sign-in", async () => {
connectRegisteredSshTargetMock.mockResolvedValueOnce(null)
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
await dispatcher.dispatch(makeRequest('ssh.connect', { targetId: 'ssh-1', automatic: true }))
expect(connectRegisteredSshTargetMock).toHaveBeenCalledWith('ssh-1', { automatic: true })
})
it('returns null when the target has no registered state yet', async () => {
getRegisteredSshStateMock.mockReturnValueOnce(undefined)
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
const response = await dispatcher.dispatch(makeRequest('ssh.getState', { targetId: 'ssh-1' }))
expect(response).toMatchObject({ ok: true, result: { state: null } })
})
it('redacts HUB-private diagnostics from state and connect failures', async () => {
const privateMessage = 'identity /Users/hub/.ssh/private via bastion.internal failed'
getRegisteredSshStateMock.mockReturnValue({
@@ -152,37 +98,6 @@ describe('ssh RPC methods', () => {
expect(JSON.stringify(response)).not.toContain('bastion')
})
it('does not invent a platform before the SSH host has been detected', async () => {
listRegisteredSshTargetsMock.mockReturnValueOnce([{ id: 'ssh-1', label: 'Dev box' }])
getRegisteredSshStateMock.mockReturnValueOnce(undefined)
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
const response = await dispatcher.dispatch(makeRequest('ssh.listTargetSummaries'))
expect(response).toMatchObject({
ok: true,
result: { targets: [{ id: 'ssh-1', label: 'Dev box' }] }
})
expect(JSON.stringify(response)).not.toContain('remotePlatform')
})
it('reports disconnected lifecycle states without calling them connected', async () => {
listRegisteredSshTargetsMock.mockReturnValueOnce([{ id: 'ssh-1', label: 'Dev box' }])
getRegisteredSshStateMock.mockReturnValueOnce({ status: 'reconnecting' })
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
const response = await dispatcher.dispatch(makeRequest('ssh.listTargetSummaries'))
expect(response).toMatchObject({
ok: true,
result: {
targets: [{ id: 'ssh-1', connected: false, connectionStatus: 'reconnecting' }]
}
})
})
it('redacts the legacy target response for older clients', async () => {
const targets = [
{
@@ -209,15 +124,4 @@ describe('ssh RPC methods', () => {
expect(JSON.stringify(response)).not.toContain('/secret/key')
expect(JSON.stringify(response)).not.toContain('bastion')
})
it('lists removed-target labels for ghost-host display on paired clients', async () => {
const labels = { 'ssh-old': 'Dev box' }
listRegisteredRemovedSshTargetLabelsMock.mockReturnValueOnce(labels)
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: SSH_METHODS })
const response = await dispatcher.dispatch(makeRequest('ssh.listRemovedTargetLabels'))
expect(response).toMatchObject({ ok: true, result: { labels } })
})
})
@@ -1,213 +0,0 @@
import './unused-default-rpc-methods.test-fixture'
import { describe, expect, it, vi } from 'vitest'
import { RpcDispatcher } from './dispatcher'
import type { RpcRequest } from './core'
import type { OrcaRuntimeService } from '../orca-runtime'
import { TERMINAL_METHODS } from './methods/terminal'
// Geometry RPCs must use resolveLiveLeafForHandle: an unguarded stale handle resolves the replacement PTY and corrupts its display (#7718).
// Model stale resolution: unguarded lookup returns replacement pty-b; guarded lookup refuses.
const NEW_PTY_UNDER_PANE = 'pty-b'
function stubStaleHandleRuntime(overrides: Partial<OrcaRuntimeService> = {}): OrcaRuntimeService {
return {
getRuntimeId: () => 'test-runtime',
resolveLeafForHandle: vi.fn().mockReturnValue({ ptyId: NEW_PTY_UNDER_PANE }),
resolveLiveLeafForHandle: vi.fn(() => {
throw new Error('terminal_handle_stale')
}),
...overrides
} as unknown as OrcaRuntimeService
}
function makeRequest(method: string, params?: unknown): RpcRequest {
return { id: 'req-1', authToken: 'tok', method, params }
}
async function expectStale(method: string, params: unknown, mutators: string[]): Promise<void> {
const spies: Record<string, ReturnType<typeof vi.fn>> = {}
for (const name of mutators) {
spies[name] = vi.fn()
}
const runtime = stubStaleHandleRuntime(spies as Partial<OrcaRuntimeService>)
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
const response = await dispatcher.dispatch(makeRequest(method, params))
expect(response.ok).toBe(false)
if (response.ok) {
throw new Error(`expected ${method} to reject a stale handle`)
}
expect(response.error.message).toContain('terminal_handle_stale')
for (const name of mutators) {
expect(spies[name]).not.toHaveBeenCalled()
}
}
describe('terminal geometry family rejects stale handles instead of mutating the wrong PTY', () => {
it('terminal.resizeForClient fails with terminal_handle_stale', async () => {
await expectStale(
'terminal.resizeForClient',
{ terminal: 'stale-terminal', mode: 'restore', clientId: 'client-1' },
['resizeForClient']
)
})
it('terminal.setDisplayMode fails with terminal_handle_stale', async () => {
await expectStale(
'terminal.setDisplayMode',
{
terminal: 'stale-terminal',
mode: 'auto',
client: { id: 'client-1', type: 'mobile' },
viewport: { cols: 80, rows: 24 }
},
[
'setMobileDisplayMode',
'applyMobileDisplayMode',
'updateMobileSubscriberViewport',
'markMobileActor'
]
)
})
it('terminal.restoreFit fails with terminal_handle_stale', async () => {
await expectStale('terminal.restoreFit', { terminal: 'stale-terminal' }, [
'reclaimTerminalForDesktop'
])
})
it('terminal.updateViewport fails with terminal_handle_stale', async () => {
await expectStale(
'terminal.updateViewport',
{
terminal: 'stale-terminal',
client: { id: 'client-1', type: 'mobile' },
viewport: { cols: 80, rows: 24 }
},
['updateMobileViewport', 'updateDesktopViewport']
)
})
})
describe('terminal geometry family still mutates the live PTY for a fresh handle', () => {
it('terminal.restoreFit reclaims the resolved PTY when the handle is live', async () => {
const reclaimTerminalForDesktop = vi.fn().mockResolvedValue(true)
const runtime = {
getRuntimeId: () => 'test-runtime',
resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-a' }),
reclaimTerminalForDesktop
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
const response = await dispatcher.dispatch(
makeRequest('terminal.restoreFit', { terminal: 'live-terminal' })
)
expect(response.ok).toBe(true)
if (!response.ok) {
throw new Error(response.error.message)
}
expect(response.result).toEqual({ restored: true })
expect(reclaimTerminalForDesktop).toHaveBeenCalledWith('pty-a')
})
it('terminal.resizeForClient resizes the resolved PTY when the handle is live', async () => {
const resizeForClient = vi.fn().mockResolvedValue({ cols: 80, rows: 24 })
const runtime = {
getRuntimeId: () => 'test-runtime',
resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-a' }),
resizeForClient
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
const response = await dispatcher.dispatch(
makeRequest('terminal.resizeForClient', {
terminal: 'live-terminal',
mode: 'restore',
clientId: 'client-1'
})
)
expect(response.ok).toBe(true)
if (!response.ok) {
throw new Error(response.error.message)
}
expect(resizeForClient).toHaveBeenCalledWith(
'pty-a',
'restore',
'client-1',
undefined,
undefined
)
})
it('terminal.setDisplayMode mutates the resolved PTY when the handle is live', async () => {
const setMobileDisplayMode = vi.fn()
const applyMobileDisplayMode = vi.fn().mockResolvedValue(undefined)
const updateMobileSubscriberViewport = vi.fn()
const markMobileActor = vi.fn()
const runtime = {
getRuntimeId: () => 'test-runtime',
resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-a' }),
setMobileDisplayMode,
applyMobileDisplayMode,
updateMobileSubscriberViewport,
markMobileActor,
getLayout: vi.fn().mockReturnValue({ seq: 42 })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
const response = await dispatcher.dispatch(
makeRequest('terminal.setDisplayMode', {
terminal: 'live-terminal',
mode: 'auto',
client: { id: 'client-1', type: 'mobile' },
viewport: { cols: 80, rows: 24 }
})
)
expect(response.ok).toBe(true)
if (!response.ok) {
throw new Error(response.error.message)
}
expect(updateMobileSubscriberViewport).toHaveBeenCalledWith('pty-a', 'client-1', {
cols: 80,
rows: 24
})
expect(markMobileActor).toHaveBeenCalledWith('pty-a', 'client-1')
expect(setMobileDisplayMode).toHaveBeenCalledWith('pty-a', 'auto')
expect(applyMobileDisplayMode).toHaveBeenCalledWith('pty-a')
expect(response.result).toEqual({ mode: 'auto', seq: 42 })
})
it('terminal.updateViewport updates the resolved PTY when the handle is live', async () => {
const updateMobileViewport = vi.fn().mockResolvedValue({ updated: true, applied: true })
const runtime = {
getRuntimeId: () => 'test-runtime',
resolveLiveLeafForHandle: vi.fn().mockReturnValue({ ptyId: 'pty-a' }),
updateMobileViewport,
getLayout: vi.fn().mockReturnValue({ seq: 7 })
} as unknown as OrcaRuntimeService
const dispatcher = new RpcDispatcher({ runtime, methods: TERMINAL_METHODS })
const response = await dispatcher.dispatch(
makeRequest('terminal.updateViewport', {
terminal: 'live-terminal',
client: { id: 'client-1', type: 'mobile' },
viewport: { cols: 80, rows: 24 }
})
)
expect(response.ok).toBe(true)
if (!response.ok) {
throw new Error(response.error.message)
}
expect(updateMobileViewport).toHaveBeenCalledWith('pty-a', 'client-1', {
cols: 80,
rows: 24
})
expect(response.result).toEqual({ updated: true, applied: true, seq: 7 })
})
})
@@ -1,20 +1,17 @@
import { describe, expect, it, vi } from 'vitest'
import {
agentSessionAttentionKey,
type AgentSessionAttentionEdge,
type StructuredAttentionState
import type {
AgentSessionAttentionEdge,
StructuredAttentionState
} from '../../shared/agent-session-attention'
import type { AgentSessionStatusSummary } from '../../shared/agent-session-wire'
import type { NotificationSettings } from '../../shared/notification-settings-types'
import {
structuredAgentSessionPaneKey,
structuredAgentSessionTabId
} from '../../shared/structured-agent-session-projection'
import {
createStructuredAttentionMobileDelivery,
readStructuredAttentionWorkspaceLabels,
type StructuredAttentionMobileDeliveryDeps
} from './structured-agent-session-mobile-attention'
import type { MobileNotificationDispatchEvent } from './runtime-mobile-notification-controller'
const SCOPE = {
@@ -87,41 +84,6 @@ function harness(overrides: Partial<StructuredAttentionMobileDeliveryDeps> = {})
}
describe('structured attention mobile delivery', () => {
it('pushes a raised prompt at once as "needs input", keyed by the prompt', () => {
const h = harness()
h.delivery.deliver(PROMPT, SUMMARY)
const key = agentSessionAttentionKey(PROMPT)
expect(h.sent).toEqual([
{
type: 'notification',
emittedAt: 42,
source: 'agent-task-complete',
title: 'orca / feature - Claude needs input',
body: 'May I run the migration?',
worktreeId: SCOPE.workspaceId,
paneKey: structuredAgentSessionPaneKey(
structuredAgentSessionTabId('session-1'),
'session-1'
),
notificationId: key,
attentionKey: key,
agentState: 'blocked'
}
])
})
it('words a settled turn by its outcome, and a failure as a failure even while a prompt waits', () => {
const h = harness()
h.delivery.deliver(completion('success'), SUMMARY)
h.delivery.deliver(completion('success', true), SUMMARY)
h.delivery.deliver(completion('failure', true), SUMMARY)
expect(h.sent.map((event) => [event.agentState, event.title])).toEqual([
['done', 'orca / feature - Claude finished'],
['blocked', 'orca / feature - Claude needs input'],
['done', 'orca / feature - Claude failed']
])
})
it('sends nothing for a workspace whose notifications are muted', () => {
const isWorkspaceMuted = vi.fn(() => true)
const quiet = harness({ isWorkspaceMuted })
@@ -160,49 +122,4 @@ describe('structured attention mobile delivery', () => {
h.delivery.deliver(edge, SUMMARY)
expect(h.sent).toEqual([])
})
it('forwards current prompt state for reconciliation', () => {
const h = harness()
const state = { scope: SCOPE, sessionId: 'session-1', pendingPromptIds: ['a', 'b'] }
h.delivery.reconcile(state)
expect(h.reconciled).toEqual([state])
})
})
describe('readStructuredAttentionWorkspaceLabels', () => {
const store = {
getRepo: vi.fn((id: string) => (id === 'repo-1' ? { displayName: 'orca' } : undefined)),
getWorktreeMeta: vi.fn((id: string) =>
id === SCOPE.workspaceId ? { displayName: 'feature' } : undefined
),
getFolderWorkspaces: vi.fn(() => [{ id: 'folder-1', name: 'Notes' }])
}
it('reads a worktree and its repo from persisted metadata', () => {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the read fields are stubbed.
const labels = readStructuredAttentionWorkspaceLabels(store as never, SCOPE)
expect(labels).toEqual({ repoLabel: 'orca', worktreeLabel: 'feature' })
})
it('reads a folder workspace from the folder key a chat scope carries', () => {
const labels = readStructuredAttentionWorkspaceLabels(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the read fields are stubbed.
store as never,
{ ...SCOPE, workspaceId: 'folder:folder-1', workspaceKind: 'folder' }
)
expect(labels).toEqual({ worktreeLabel: 'Notes' })
})
})
it('preserves the missing notification settings failure before dispatch', () => {
let reads = 0
const h = harness({
readNotificationSettings() {
reads += 1
return undefined
}
})
expect(() => h.delivery.deliver(PROMPT, SUMMARY)).toThrow(TypeError)
expect(reads).toBe(1)
expect(h.sent).toEqual([])
})
@@ -122,12 +122,6 @@ describe('reading a structured worker through the terminal-read path', () => {
expect(refusal).not.toContain('worker-read')
})
it('reports dropped history as truncated rather than pretending the page is whole', async () => {
const handle = registerWorker()
installHost({ items: [message('i1', 'tail only')], hasOlder: true })
expect((await readStructuredWorkerTerminal({ handle, db: null }))?.truncated).toBe(true)
})
it('redacts dispatch capability tokens the same way the archive path does', async () => {
const handle = registerWorker()
const token = `dcap_${'a'.repeat(32)}`
+2 -119
View File
@@ -1,10 +1,10 @@
import { describe, expect, it, vi } from 'vitest'
import { describe, expect, it } from 'vitest'
import {
renderServeReadiness,
ServeReadinessPublisher,
type ServeReadiness
} from './serve-readiness'
import type { OrcadHealth } from '../orcad/orcad-health'
const ready: ServeReadiness = {
runtimeId: 'runtime-1',
@@ -22,81 +22,7 @@ const ready: ServeReadiness = {
}
}
const health: OrcadHealth = {
buildHash: 'abc123def4567890',
buildVersion: '1.4.0',
nodeVersion: '20.11.0',
nodeAbi: '115',
platform: 'linux',
arch: 'x64',
pid: 1234,
terminalDaemon: {
state: 'live',
ownsFreshSessions: true,
pid: 4242,
buildVersion: '1.4.0',
entryPath: '/opt/orcad/daemon-entry.js',
protocolVersion: 36,
cgroupUnit: null,
selfTest: { ok: true, coverage: 'pty-spawn', verdict: 'healthy', durationMs: 12 }
}
}
describe('ServeReadinessPublisher', () => {
it('writes one complete human-readable ready block', async () => {
const write = vi.fn(async () => {})
const publisher = new ServeReadinessPublisher(write)
await publisher.publish(ready, { mode: 'human' })
expect(write).toHaveBeenCalledOnce()
expect(write).toHaveBeenCalledWith(
expect.stringContaining(
'Orca server ready\nBound endpoint: ws://0.0.0.0:6768\nAdvertised endpoint: wss://orca.example.test/runtime'
)
)
expect(write).toHaveBeenCalledWith(
expect.stringContaining('Pairing URL: orca://pair?code=secret\n')
)
})
it('publishes a versioned JSON contract with explicit endpoints and pairing availability', () => {
expect(JSON.parse(renderServeReadiness(ready, { mode: 'json' }))).toEqual({
type: 'orca_server_ready',
schemaVersion: 1,
runtimeId: 'runtime-1',
endpoint: 'ws://0.0.0.0:6768',
boundEndpoint: 'ws://0.0.0.0:6768',
advertisedEndpoint: 'wss://orca.example.test/runtime',
managedWslCliReconciliation: 'settled',
pairing: ready.pairing
})
})
it('reports unavailable pairing as an explicit machine-readable object', () => {
const unavailable: ServeReadiness = {
...ready,
advertisedEndpoint: null,
pairing: {
available: false,
reason: 'invalid_advertised_endpoint',
guidance: 'Use a reachable address.'
}
}
const json = JSON.parse(renderServeReadiness(unavailable, { mode: 'json' }))
expect(json.pairing).toEqual(unavailable.pairing)
expect(renderServeReadiness(unavailable, { mode: 'human' })).toContain(
'Pairing unavailable: invalid_advertised_endpoint\nPairing guidance: Use a reachable address.'
)
})
it('preserves the recipe JSON contract', () => {
expect(renderServeReadiness(ready, { mode: 'recipe-json', projectRoot: '/workspace' })).toBe(
'{"schemaVersion":1,"pairingCode":"orca://pair?code=secret","projectRoot":"/workspace"}'
)
})
it('fails recipe output with the unavailable reason and guidance', () => {
const unavailable: ServeReadiness = {
...ready,
@@ -111,49 +37,6 @@ describe('ServeReadinessPublisher', () => {
).toThrow('websocket_unavailable. Choose an unused --port.')
})
it('omits the health block entirely when a host does not report one', () => {
const payload = JSON.parse(renderServeReadiness(ready, { mode: 'json' }))
// Absent health means unreported; it must not imply a green verdict.
expect('health' in payload).toBe(false)
expect(renderServeReadiness(ready, { mode: 'human' })).not.toContain('Terminal daemon')
})
it('carries build identity, Node ABI and the daemon self-test in the JSON contract', () => {
const payload = JSON.parse(renderServeReadiness({ ...ready, health }, { mode: 'json' }))
expect(payload.health.buildHash).toBe('abc123def4567890')
expect(payload.health.nodeAbi).toBe('115')
expect(payload.health.terminalDaemon.selfTest).toEqual({
ok: true,
coverage: 'pty-spawn',
verdict: 'healthy',
durationMs: 12
})
})
it('says out loud when the daemon self-test failed', () => {
const failed: ServeReadiness = {
...ready,
health: {
...health,
terminalDaemon: {
...health.terminalDaemon,
state: 'degraded',
ownsFreshSessions: false,
selfTest: {
ok: false,
coverage: 'pty-spawn',
verdict: 'pty-spawn-unhealthy',
durationMs: 3_000
}
}
}
}
const human = renderServeReadiness(failed, { mode: 'human' })
// Make failure explicit to operators rather than requiring inference from missing output.
expect(human).toContain('PTY self-test FAILED')
expect(human).toContain('terminals survive an orcad restart: NO')
})
it('rejects concurrent and later duplicate publications', async () => {
let finishWrite: (() => void) | undefined
const publisher = new ServeReadinessPublisher(
@@ -106,16 +106,6 @@ describe('executeSkillInstallRequest', () => {
).toEqual([])
})
it('rejects local paths at the remote request boundary', async () => {
const { request, dependencies, archive } = await fixture()
await expect(
executeSkillInstallRequest(
{ ...request, ingress: { kind: 'local-file', path: archive.archivePath } },
dependencies
)
).rejects.toThrow('skill-install-local-ingress-rejected')
})
it('rejects invalid shape, identity, scope, destination, and ingress policy before download', async () => {
const { request, dependencies, archive, home } = await fixture()
const cases: { input: unknown; error: string }[] = [
+2 -43
View File
@@ -35,12 +35,9 @@ import { writeAtomicOrcadRemoteRecord } from '../managed-server/install/orcad-re
import { deployOrcad } from './orcad-remote-deploy'
import { getRemoteHostPlatform } from './ssh-remote-platform'
import { ORCAD_SNAPSHOT_SPACE_INSUFFICIENT_CODE } from './orcad-snapshot-room'
import { isSnapshotCaptureCommand } from './orcad-snapshot-capture-command'
import { OrcadFenceLostError } from '../managed-server/activation/orcad-activation-fence-scope'
import {
emptyOrcadActivationRecord,
withActivatedVersion
} from '../managed-server/activation/orcad-activation-record'
import {
FakeOrcadHost,
NEW,
@@ -114,44 +111,6 @@ describe('an update on a host without room for its snapshot (#30491)', () => {
expect(host.fence).toBe(false)
})
it('counts free space and reclaimable snapshots apart in the refusal', async () => {
answerRoom(400_000, 300_000, 1_000)
expect(JSON.stringify(await deploy())).toMatch(
/has 1000 KB free \(plus 293\.0 MB Orca would reclaim from earlier snapshots\)/u
)
})
it('keeps the snapshot the activation record names through both the room check and the prune', async () => {
const recorded = 'pre-0.1.0+aa01-500'
host.record = JSON.stringify(
withActivatedVersion(
emptyOrcadActivationRecord(),
OLD,
{
dirName: recorded,
takenBeforeVersion: OLD,
readableByVersion: null,
takenAt: new Date(500).toISOString()
},
new Date(0)
)
)
answerRoom(400_000, 0, 10_000_000)
expect(await deploy()).toMatchObject({ outcome: 'installed-and-activated' })
const commands = vi.mocked(execCommand).mock.calls.map(([, command]) => command)
expect(commands.find((command) => command.includes('SNAPSHOT_SPACE'))).toContain(
`('${recorded}')`
)
expect(commands.find(isSnapshotCaptureCommand)).toMatch(
new RegExp(
`\\('pre-${NEW.replace('+', '\\+')}-\\d+'\\|'${recorded.replace('+', '\\+')}'\\)`,
'u'
)
)
})
it('measures room before the last terminal recount, so the scan cannot outdate it', async () => {
answerRoom(400_000, 0, 10_000_000)
const probedBeforeRecount: boolean[] = []
@@ -10,8 +10,6 @@ import { unservedHostServerMessage, unservedServerStatus } from './ssh-host-unse
import { admitSshManagedServerStatus } from '../../shared/ssh-managed-server-status-admission'
const target: SshTarget = { id: 'ssh-1', label: 'Box', host: 'box', port: 22, username: 'me' }
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the orchestrator reads only the environment id.
const environment = { id: 'env-1' } as never
function fencedHost(phase: 'source-fenced' | 'destination-staged', registered: boolean) {
const host: { phase: string | null; registered: boolean } = { phase, registered }
@@ -53,36 +51,6 @@ describe('a move to the managed server that stops short', () => {
warn.mockRestore()
})
it('resumes the same move on the next connect once the server answers', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const { host, deps } = fencedHost('destination-staged', true)
const failing = deps({
convert: vi.fn(async () => {
throw new Error('socket closed')
}),
abandonConversion: vi.fn(async () => {
throw new Error('socket closed')
})
})
await expect(resolveHostServerOnConnect(target, failing)).resolves.toMatchObject({
held: true
})
const convert = vi.fn(async () => {
host.phase = 'destination-committed'
return { outcome: 'converted' as const, environment, migrationId: 'm' }
})
const next = deps({ convert })
await expect(resolveHostServerOnConnect(target, next)).resolves.toEqual({
route: 'managed',
environmentId: 'env-1'
})
// Resumed through the registered server, without a fresh census or a second fence.
expect(next.ensureTunnel).toHaveBeenCalledWith('env-1')
expect(convert).toHaveBeenCalledWith(target, null)
expect(next.relayTerminals).not.toHaveBeenCalled()
warn.mockRestore()
})
it('gives the host back, with no lock to report, when the server proves it holds nothing', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const { host, deps } = fencedHost('destination-staged', true)
@@ -138,39 +106,6 @@ describe('a move to the managed server that stops short', () => {
warn.mockRestore()
})
it('finishes, rather than reports a lock, when the abort finds the move already committed', async () => {
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
const { host, deps } = fencedHost('destination-staged', true)
const lost = deps({
convert: vi.fn(async () => {
throw new Error('orcad_migration_commit_reply_lost')
}),
// The abort's re-read found the commit landed and recorded it.
abandonConversion: vi.fn(async () => {
host.phase = 'destination-committed'
})
})
await expect(resolveHostServerOnConnect(target, lost)).resolves.not.toHaveProperty('held')
const next = deps()
await expect(resolveHostServerOnConnect(target, next)).resolves.toEqual({
route: 'managed',
environmentId: 'env-1'
})
expect(next.convert).not.toHaveBeenCalled()
expect(next.retainCommittedSource).toHaveBeenCalledWith(target)
warn.mockRestore()
})
it('keeps a refusal’s own guidance even while the host stays locked', () => {
const decision = {
route: 'unserved' as const,
reason: 'refused' as const,
detail: 'remove that project from this desktop and connect again',
held: true as const
}
expect(unservedHostServerMessage(decision)).toBe(decision.detail)
})
it('carries the lock to the renderer, and drops anything but a literal true', () => {
expect(admitSshManagedServerStatus({ kind: 'relay', reason: 'failed', held: true })).toEqual({
managedServer: { kind: 'relay', reason: 'failed', held: true }
@@ -168,34 +168,10 @@ describe('clipboard image paste for a runtime-owned SSH workspace', () => {
expect(fsWriteFileMock).not.toHaveBeenCalled()
})
it('keeps a runtime-host paste (no SSH target) addressed to the runtime itself', async () => {
mockRuntimeUpload()
await expect(
saveImageHandler()(rendererEvent, { runtimeEnvironmentId: RUNTIME_ID })
).resolves.toBe('/tmp/on-runtime-target.png')
expect(runtimeCall('clipboard.startImageUpload')?.[3]).toEqual({
expectedBase64Length: PNG.toString('base64').length,
connectionId: null
})
})
it('reports the dropped-connection verdict for a client-dialed SSH target', async () => {
await expect(
saveImageHandler()(rendererEvent, { connectionId: CLIENT_SSH_TARGET })
).rejects.toThrow(SSH_HOST_NOT_SERVED_MESSAGE)
expect(callRuntimeEnvironmentMock).not.toHaveBeenCalled()
})
it('keeps a plain local paste on the local temp dir', async () => {
fsWriteFileMock.mockResolvedValue(undefined)
await expect(saveImageHandler()(rendererEvent, undefined)).resolves.toMatch(
/orca-paste-.*\.png$/
)
expect(fsWriteFileMock).toHaveBeenCalledTimes(1)
expect(callRuntimeEnvironmentMock).not.toHaveBeenCalled()
})
})
@@ -42,35 +42,6 @@ describe('retirement discovery on WSL', () => {
vi.restoreAllMocks()
})
it('admits every WSL UNC listing through the bounded filesystem gate', async () => {
await discoverRetiredWorktreeNames({
workspaceRoots: [DISTRO_ROOT],
home: '/nonexistent-home',
env: {},
resolveWslHome: async () => '\\\\wsl.localhost\\Ubuntu\\home\\ada'
})
// Both UNC listings and absence-proving ancestor probes must share scan admission.
const calls = runWslTranscriptFsTaskMock.mock.calls.map(([options]) => options)
expect(calls.every((options) => options.priority === 'scan')).toBe(true)
expect(calls.map((options) => options.path)).toEqual(
expect.arrayContaining([
DISTRO_ROOT,
join('\\\\wsl.localhost\\Ubuntu\\home\\ada', '.claude', 'projects')
])
)
})
it('leaves no listing ungated when the workspace root is on the Windows side', async () => {
await discoverRetiredWorktreeNames({
workspaceRoots: ['C:\\Users\\ada\\orca\\workspaces'],
home: '/nonexistent-home',
env: {}
})
expect(runWslTranscriptFsTaskMock).not.toHaveBeenCalled()
})
it('reports a gate refusal as incomplete but still returns what the host side could read', async () => {
runWslTranscriptFsTaskMock.mockRejectedValue(
new WslTranscriptFsError('timeout', 'filesystem access is taking too long')
@@ -140,17 +111,6 @@ describe('retirement discovery on WSL', () => {
expect(retired.complete).toBe(false)
})
it('stays retryable when the distro home cannot be resolved, rather than memoizing the hole', async () => {
const retired = await discoverRetiredWorktreeNames({
workspaceRoots: [DISTRO_ROOT],
home: '/nonexistent-home',
env: {},
resolveWslHome: async () => null
})
expect(retired.complete).toBe(false)
})
it('keeps a deleted WSL workspace name spent, so the next create cannot reuse its cwd', async () => {
// The distro bucket outlives the deleted workspace and must keep its name retired.
const distroHome = await mkdtemp(join(tmpdir(), 'orca-wsl-distro-home-'))
-54
View File
@@ -101,60 +101,6 @@ describe('stopWslHostServer', () => {
warn.mockRestore()
})
it('names each terminal that refused, and still asks the server to stop', async () => {
const { all } = steps({
endTerminals: vi.fn(async () => [
{ handle: 'term_1', worktreeId: 'gone::/home/me/old', reason: 'selector_not_found' }
]),
census: vi.fn(async () => census(1)),
decommission: vi.fn(async (): Promise<OrcadDecommissionResult> => ({
outcome: 'refused',
verdict: 'live',
code: 'orcad_decommission_terminals_running',
reason: '1 terminal is still running on this host.'
}))
})
const result = await stopWslHostServer(all)
expect(all.decommission).toHaveBeenCalled()
expect(result).toEqual({
stopped: false,
reason:
'Terminals that refused to close: term_1 (gone::/home/me/old): selector_not_found 1 terminal session kept running. Orca’s server kept running: 1 terminal is still running on this host.',
verdict: 'live'
})
})
it('never reports stopped when the decommission could not prove it stopped the serving orcad', async () => {
const { all } = steps({
decommission: vi.fn(async (): Promise<OrcadDecommissionResult> => ({
outcome: 'refused',
verdict: 'unverifiable',
code: 'orcad_wsl_host_serving_instance_unproven',
reason: 'the orcad this computer was connected to is not the one the distro records'
}))
})
await expect(stopWslHostServer(all)).resolves.toMatchObject({
stopped: false,
verdict: 'unverifiable'
})
})
it('calls a census that did not answer unverifiable, not live', async () => {
const { all } = steps({
census: vi.fn(async () => census(null)),
decommission: vi.fn(async (): Promise<OrcadDecommissionResult> => ({
outcome: 'refused',
verdict: 'unverifiable',
code: 'orcad_decommission_census_unavailable',
reason: 'The terminal daemon did not answer a session count.'
}))
})
await expect(stopWslHostServer(all)).resolves.toMatchObject({
stopped: false,
verdict: 'unverifiable'
})
})
it('reports a daemon that did not retire', async () => {
const { all } = steps({
decommission: vi.fn(async (): Promise<OrcadDecommissionResult> => ({
@@ -63,22 +63,6 @@ afterEach(() => {
})
describe.skipIf(process.platform === 'win32')('WSL connect state snapshot', () => {
it('reads a fresh host in one command', async () => {
const reader = createWslOrcadConnectStateReader()
await expect(reader.readActivationRecord(target())).resolves.toEqual(
emptyOrcadActivationRecord()
)
expect(mockExec).toHaveBeenCalledOnce()
expect(reader.snapshot()).toEqual({
activation: { state: 'absent' },
transactionAbsent: true,
instanceLock: { state: 'absent' },
fenceOpen: true
})
})
it('reads the record, a pending journal, the instance lock and a held fence together', async () => {
write('.orca-remote/orcad-active.json', serializeOrcadActivationRecord(ACTIVE))
write('.orca-remote/.orcad-activation-transaction/transaction.json', '{"partial"')
@@ -193,12 +177,6 @@ describe('wslOrcadConnectStateHints', () => {
fenceOpen: true
}
it('hints while the record the snapshot read is the one acted on', () => {
const hints = wslOrcadConnectStateHints(snapshot, ACTIVE)
expect(hints.instanceLock(ACTIVE)).toEqual({ state: 'absent' })
expect(hints.deployPreRead(ACTIVE)).toEqual({ preRead: { record: ACTIVE, fenceOpen: true } })
})
it('stops hinting once recovery, adoption or admission replaced the record', () => {
const hints = wslOrcadConnectStateHints(snapshot, ACTIVE)
const replaced = { ...ACTIVE }
+1 -66
View File
@@ -1,15 +1,11 @@
import { ORCAD_WSL_DISTRO_GUID_ENV } from '../orcad/orcad-migration-wsl-destination'
import { ORCAD_IDLE_TIMEOUT_ENV } from '../../shared/orcad-idle-exit'
import { EventEmitter } from 'node:events'
import { PassThrough, Readable } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import type { PipedChildProcess, PipedProcessSpec } from '@orca/process-host/process-spec'
import { execCommand } from '../ssh/remote-exec/remote-exec-command'
import { isWslOrcadHostTarget } from '../ssh/orcad-host-transport'
import { WSL_CHANNEL_CLOSE_GRACE_MS } from './wsl-command-channel'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import { openOrcadStdioBridgeChannel, resolveOrcadStdioBridge } from '../ssh/orcad-stdio-bridge'
import { orcadBashStdioBridgeCommand } from '../managed-server/lifecycle/orcad-stdio-bridge-script'
type FakeChild = EventEmitter & {
stdin: PassThrough
@@ -72,32 +68,6 @@ function transportWith(children: FakeChild[]) {
}
describe('createWslOrcadHostTransport', () => {
it('runs the command through wsl.exe --exec sh -c in the chosen distro', async () => {
const child = fakeChild()
const { transport, specs } = transportWith([child])
const channel = await transport.exec('echo $HOME')
expect(specs[0]?.program).toMatch(/wsl(\.exe)?$/i)
expect(specs[0]?.args).toEqual(['-d', 'Ubuntu-24.04', '--exec', 'sh', '-c', 'echo $HOME'])
expect(specs[0]?.env?.WSL_UTF8).toBe('1')
channel.close()
})
it('hands the bash bridge to sh through --exec, so wsl.exe never expands its $ words', async () => {
const child = fakeChild()
const { transport, specs } = transportWith([child])
const bridge = await resolveOrcadStdioBridge(transport, 6768)
const channel = await openOrcadStdioBridgeChannel(transport, bridge)
expect(specs[0]?.args).toEqual([
'-d',
'Ubuntu-24.04',
'--exec',
'sh',
'-c',
orcadBashStdioBridgeCommand(6768)
])
channel.close()
})
it('emits exit before close and reports the exit code on both', async () => {
const child = fakeChild()
const { transport } = transportWith([child])
@@ -189,39 +159,4 @@ describe('createWslOrcadHostTransport', () => {
await closed
expect(Buffer.concat(received).length).toBe(20 * 1024 * 1024)
})
it('reports a stable WSL identity and never claims the system SSH transport', () => {
const { transport } = transportWith([])
const target = transport.getTarget()
expect(target).toEqual({
kind: 'wsl',
id: 'env-1',
distro: 'Ubuntu-24.04',
label: 'My Ubuntu'
})
expect(isWslOrcadHostTarget(target)).toBe(true)
expect(transport.usesSystemSshTransport()).toBe(false)
expect(transport.getConnectGeneration()).toBe(transport.getConnectGeneration())
})
// Regression: `nohup … &` dies with its `wsl.exe --exec` on a distro without systemd.
it('asks orcad launches to start in their own session', () => {
expect(transportWith([]).transport.orcadLaunchDetach).toBe('setsid')
})
it('tells orcad which registration of the distro it runs in, when the registry knows', async () => {
const make = (id: string | null) =>
createWslOrcadHostTransport({
target: { kind: 'wsl', id: 'env-1', distro: 'Ubuntu-24.04', label: 'My Ubuntu' },
commandLoops: false,
readDistroRegistryId: async (distro) => (distro === 'Ubuntu-24.04' ? id : null)
})
await expect(make('1a2b')?.orcadLaunchEnv?.({ fullVersion: '0.1.0+abc' })).resolves.toEqual([
[ORCAD_WSL_DISTRO_GUID_ENV, '1a2b'],
[ORCAD_IDLE_TIMEOUT_ENV, '300000']
])
await expect(make(null)?.orcadLaunchEnv?.({ fullVersion: '0.1.0+abc' })).resolves.toEqual([
[ORCAD_IDLE_TIMEOUT_ENV, '300000']
])
})
})
@@ -3,12 +3,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getContextualTour } from '../../../../shared/contextual-tours'
import { i18n, loadRendererUiLanguage } from '@/i18n/i18n'
import {
getContextualTourDisplayProgress,
getContextualTourMeasurementAction,
measureContextualTourOverlayRenderState,
isContextualTourLastDisplayStep
} from './contextual-tour-overlay-measurement'
import { measureContextualTourOverlayRenderState } from './contextual-tour-overlay-measurement'
afterEach(async () => {
document.body.replaceChildren()
@@ -16,56 +11,6 @@ afterEach(async () => {
})
describe('contextual tour overlay measurement', () => {
it('renders the automation tour copy in Korean when the UI locale is Korean', async () => {
await i18n.changeLanguage(await loadRendererUiLanguage('ko'))
const target = document.createElement('button')
target.setAttribute('data-contextual-tour-target', 'automations-create')
target.getBoundingClientRect = () => new DOMRect(0, 0, 20, 20)
document.body.appendChild(target)
const result = measureContextualTourOverlayRenderState({
tour: getContextualTour('automations'),
activeStepIndex: 0,
sidebarOpen: true,
keybindings: undefined,
previousTelemetryTotalSteps: 0
})
expect(result.kind).toBe('render')
if (result.kind !== 'render') {
throw new Error(`Expected render result, received ${result.kind}`)
}
expect(result.renderState.title).toBe('자동화란 무엇인가요?')
expect(result.renderState.body).toBe(
'자동화는 일정에 따라 agent 작업을 실행합니다. 이 버튼을 눌러 자동화를 추가하세요.'
)
})
it('renders the automation results step in Korean when the UI locale is Korean', async () => {
await i18n.changeLanguage(await loadRendererUiLanguage('ko'))
const target = document.createElement('div')
target.setAttribute('data-contextual-tour-target', 'automations-runs')
target.getBoundingClientRect = () => new DOMRect(0, 0, 20, 20)
document.body.appendChild(target)
const result = measureContextualTourOverlayRenderState({
tour: getContextualTour('automations'),
activeStepIndex: 1,
sidebarOpen: true,
keybindings: undefined,
previousTelemetryTotalSteps: 0
})
expect(result.kind).toBe('render')
if (result.kind !== 'render') {
throw new Error(`Expected render result, received ${result.kind}`)
}
expect(result.renderState.title).toBe('결과 확인')
expect(result.renderState.body).toBe(
'실행 내역에서 자동화가 언제 실행되었는지, 어떤 일이 발생했는지, 출력을 어디서 확인할 수 있는지 볼 수 있습니다.'
)
})
it('localizes the title, shortcut body, and action label of non-automation steps', async () => {
await i18n.changeLanguage(await loadRendererUiLanguage('ko'))
const target = document.createElement('div')
@@ -127,43 +72,6 @@ describe('contextual tour overlay measurement', () => {
}
expect(result.renderState.title).toBe('자동화란 무엇인가요?')
})
it('shows all defined browser steps in progress even when step 3 is hidden', () => {
const tour = getContextualTour('browser')
expect(
getContextualTourDisplayProgress({
tour,
visibleStepIndexes: [0, 1],
stepIndex: 1,
activeStep: tour.steps[1]
})
).toEqual({ current: 2, total: 3 })
})
it('waits for the browser cookie step target instead of cancelling', () => {
const tour = getContextualTour('browser')
expect(
getContextualTourMeasurementAction({
tour,
visibleStepIndexes: [0, 1],
activeStepIndex: 2
})
).toEqual({ kind: 'wait' })
})
it('treats browser step 3 as the last display step', () => {
const tour = getContextualTour('browser')
expect(
isContextualTourLastDisplayStep({
tour,
activeStepIndex: 2,
progress: { current: 3, total: 3 }
})
).toBe(true)
})
})
const { rejectAppStoreAccess } = vi.hoisted(() => ({
@@ -76,18 +76,6 @@ describe('remoteRuntimeTerminalPreviewApi', () => {
harness.subscriptions.length = 0
})
it('subscribes on the host named by the pty id with a preview-only client', async () => {
const { sub, payloads } = await openPreview()
expect(sub.environmentId).toBe('env-a')
expect(sub.terminal).toBe('term-1')
expect(sub.clientId).toMatch(/^dashboard-preview:/)
sub.callbacks.onData('live', { seq: 14 })
expect(payloads).toEqual([{ type: 'data', ptyId: PTY, data: 'live', bytes: 4 }])
await expect(api.input(PTY, 'x')).resolves.toBe(true)
expect(sub.stream.sendInput).toHaveBeenCalledWith('x')
})
it('refuses an id without its owning host instead of guessing one', async () => {
await expect(api.connect('remote:term-1')).resolves.toEqual({ snapshot: null, replay: [] })
expect(harness.subscriptions).toHaveLength(0)
@@ -1,26 +1,11 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
// Stub lazy renderers because renderToStaticMarkup cannot resolve them; retain the banner branch structure.
vi.mock('@/lib/lazy-with-retry', () => ({
lazyWithRetry: () => () => null
}))
import { EditorContent, getMarkdownSourceLineOffset } from './EditorContent'
function createOpenFile(overrides: Partial<OpenFile> = {}): OpenFile {
return {
id: '/repo/notebook.ipynb',
filePath: '/repo/notebook.ipynb',
relativePath: 'notebook.ipynb',
worktreeId: 'repo::/repo',
language: 'notebook',
isDirty: false,
mode: 'edit',
...overrides
}
}
import { getMarkdownSourceLineOffset } from './EditorContent'
afterEach(() => {
vi.restoreAllMocks()
@@ -33,188 +18,4 @@ describe('EditorContent', () => {
expect(getMarkdownSourceLineOffset('---\r\ntitle: x\r\n---\r\n')).toBe(3)
expect(getMarkdownSourceLineOffset('---\rtitle: x\n---\r\n')).toBe(3)
})
it('counts newline-heavy front matter offsets without regex match', () => {
const matchSpy = vi.spyOn(String.prototype, 'match')
const frontMatterRaw = `---\n${'title: x\n'.repeat(12_000)}---\n`
expect(getMarkdownSourceLineOffset(frontMatterRaw)).toBe(12_002)
expect(matchSpy).not.toHaveBeenCalled()
})
it('surfaces file load errors before notebook content is parsed', () => {
const activeFile = createOpenFile()
const html = renderToStaticMarkup(
<EditorContent
activeFile={activeFile}
viewStateScopeId={activeFile.id}
fileContents={{
[activeFile.id]: {
content: '',
isBinary: false,
loadError: 'Access denied: path resolves outside allowed directories.'
}
}}
diffContents={{}}
editBuffers={{}}
openFiles={[activeFile]}
worktreeEntries={[]}
resolvedLanguage="notebook"
isMarkdown={false}
isMermaid={false}
isCsv={false}
isNotebook
mdViewMode="rich"
inlineMarkdownRenderState={null}
isChangesMode={false}
sideBySide={false}
pendingEditorReveal={null}
handleContentChange={vi.fn()}
handleContentChangeForFile={vi.fn()}
handleDirtyStateHint={vi.fn()}
handleSave={vi.fn()}
handleSaveForFile={vi.fn()}
reloadContent={vi.fn()}
/>
)
expect(html).toContain('Unable to load file')
expect(html).toContain('Access denied')
expect(html).not.toContain('Unable to render notebook')
})
it('shows the changed-on-disk banner above a dirty edit tab', () => {
const activeFile = createOpenFile({
id: '/repo/file.ts',
filePath: '/repo/file.ts',
relativePath: 'file.ts',
language: 'typescript',
isDirty: true,
externalMutation: 'changed'
})
const html = renderToStaticMarkup(
<EditorContent
activeFile={activeFile}
viewStateScopeId={activeFile.id}
fileContents={{ [activeFile.id]: { content: 'saved text', isBinary: false } }}
diffContents={{}}
editBuffers={{ [activeFile.id]: 'saved text plus edits' }}
openFiles={[activeFile]}
worktreeEntries={[]}
resolvedLanguage="typescript"
isMarkdown={false}
isMermaid={false}
isCsv={false}
isNotebook={false}
mdViewMode="rich"
inlineMarkdownRenderState={null}
isChangesMode={false}
sideBySide={false}
pendingEditorReveal={null}
handleContentChange={vi.fn()}
handleContentChangeForFile={vi.fn()}
handleDirtyStateHint={vi.fn()}
handleSave={vi.fn()}
handleSaveForFile={vi.fn()}
reloadContent={vi.fn()}
/>
)
expect(html).toContain('role="alert"')
expect(html).toContain('changed on disk')
expect(html).toContain('Reload from Disk')
})
it('shows the changed-on-disk banner above a dirty unstaged diff without collapsing it', () => {
const activeFile = createOpenFile({
id: 'diff:/repo/file.ts',
filePath: '/repo/file.ts',
relativePath: 'file.ts',
language: 'typescript',
mode: 'diff',
diffSource: 'unstaged',
isDirty: true,
externalMutation: 'changed'
})
const html = renderToStaticMarkup(
<EditorContent
activeFile={activeFile}
viewStateScopeId={activeFile.id}
fileContents={{}}
diffContents={{
[activeFile.id]: { kind: 'text', originalContent: 'old', modifiedContent: 'new' } as never
}}
editBuffers={{ [activeFile.id]: 'new plus edits' }}
openFiles={[activeFile]}
worktreeEntries={[]}
resolvedLanguage="typescript"
isMarkdown={false}
isMermaid={false}
isCsv={false}
isNotebook={false}
mdViewMode="rich"
inlineMarkdownRenderState={null}
isChangesMode={false}
sideBySide={false}
pendingEditorReveal={null}
handleContentChange={vi.fn()}
handleContentChangeForFile={vi.fn()}
handleDirtyStateHint={vi.fn()}
handleSave={vi.fn()}
handleSaveForFile={vi.fn()}
reloadContent={vi.fn()}
/>
)
expect(html).toContain('role="alert"')
expect(html).toContain('changed on disk')
// A flex-1-only wrapper collapsed the diff to zero height.
expect(html).toContain('flex h-full min-h-0 flex-col')
})
it('shows the changed-on-disk banner on a dirty markdown diff in preview mode', () => {
const activeFile = createOpenFile({
id: 'diff:/repo/notes.md',
filePath: '/repo/notes.md',
relativePath: 'notes.md',
language: 'markdown',
mode: 'diff',
diffSource: 'unstaged',
isDirty: true,
externalMutation: 'changed'
})
const html = renderToStaticMarkup(
<EditorContent
activeFile={activeFile}
viewStateScopeId={activeFile.id}
fileContents={{}}
diffContents={{
[activeFile.id]: { kind: 'text', originalContent: 'old', modifiedContent: 'new' } as never
}}
editBuffers={{ [activeFile.id]: 'new plus edits' }}
openFiles={[activeFile]}
worktreeEntries={[]}
resolvedLanguage="markdown"
isMarkdown
isMermaid={false}
isCsv={false}
isNotebook={false}
mdViewMode="preview"
inlineMarkdownRenderState={null}
isChangesMode={false}
sideBySide={false}
pendingEditorReveal={null}
handleContentChange={vi.fn()}
handleContentChangeForFile={vi.fn()}
handleDirtyStateHint={vi.fn()}
handleSave={vi.fn()}
handleSaveForFile={vi.fn()}
reloadContent={vi.fn()}
/>
)
expect(html).toContain('role="alert"')
expect(html).toContain('changed on disk')
expect(html).toContain('Previewing the modified version of this diff')
})
})
@@ -71,20 +71,6 @@ describe('MediaViewer', () => {
expect(video?.getAttribute('src')).toBeNull()
})
it('resumes from the last playback position after the tab remounts', async () => {
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {})
vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => {})
const first = await renderPlaybackViewer('clip.mp4:pdf')
first.video.currentTime = 42
await act(async () => first.root.unmount())
const second = await renderPlaybackViewer('clip.mp4:pdf')
second.video.currentTime = 0
await act(async () => second.video.dispatchEvent(new Event('loadedmetadata')))
expect(second.video.currentTime).toBe(42)
await act(async () => second.root.unmount())
})
it('retains the position when leaving before metadata and clamps it when the file shrinks', async () => {
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {})
vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => {})
@@ -131,29 +117,6 @@ describe('MediaViewer', () => {
await act(async () => otherRestored.root.unmount())
})
it('shows a persistent playback error and only offers the default app for local files', async () => {
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {})
vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => {})
const container = document.createElement('div')
const root = createRoot(container)
await act(async () =>
root.render(
<MediaViewer
src="orca-media://file/token"
mimeType="video/mp4"
filePath="remote.mp4"
canOpenLocally={false}
/>
)
)
await act(async () => container.querySelector('video')?.dispatchEvent(new Event('error')))
expect(container.querySelector('[role="alert"]')?.textContent).toContain(
'Unable to play this media file'
)
expect(container.querySelector('button')).toBeNull()
await act(async () => root.unmount())
})
it('uses native audio controls and releases the audio source when switching files', async () => {
const pause = vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {})
const load = vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => {})
@@ -113,21 +113,6 @@ async function renderFrame(props: {
}
describe('PluginCustomEditorFrame', () => {
it('renders the editor in a scripts-only sandbox inside the CSP shell', async () => {
const { iframe } = await renderFrame({ content: '{}' })
expect(readCustomEditorEntry).toHaveBeenCalledWith({
pluginKey: 'stablyai.excalidraw',
editorId: 'excalidraw'
})
// allow-same-origin would expose app DOM, storage and IPC to the plugin.
expect(iframe.getAttribute('sandbox')).toBe('allow-scripts')
expect(iframe.getAttribute('name')?.startsWith('orca-plugin-panel:editor:')).toBe(true)
// window.name is readable in the frame, so it must not name the file.
expect(iframe.getAttribute('name')).not.toContain('diagram')
expect(iframe.getAttribute('name')).not.toContain('/repo')
expect(iframe.getAttribute('srcdoc')).toContain('<main>editor</main>')
})
it('sends the buffer on ready and applies edits without saving', async () => {
const { posted, fromFrame, onContentChange, onSave } = await renderFrame({ content: 'scene' })
await fromFrame(READY)
@@ -107,61 +107,6 @@ describe('RichMarkdownErrorBoundary lazy chunk containment', () => {
})
})
it('records the degraded breadcrumb once across repeated Retry clicks', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const LazyRejectingImport = lazyWithRetry(
() => Promise.reject(new SyntaxError(CORRUPT_CHUNK_PARSE_ERROR)),
{ retries: 0 }
)
;({ container, root } = createContainer())
await act(async () => {
root?.render(
<BoundaryHarness>
<LazyRejectingImport />
</BoundaryHarness>
)
})
await flushReactWork()
await flushReactWork()
for (let click = 0; click < 3; click += 1) {
const retry = container?.querySelector('button')
expect(retry).not.toBeNull()
await act(async () => {
retry?.click()
})
await flushReactWork()
}
expect(container?.textContent).toContain('rich markdown editor')
expect(recordBreadcrumbMock).toHaveBeenCalledTimes(1)
})
it('records the degraded breadcrumb once across boundary remounts', async () => {
window.sessionStorage.setItem(RELOAD_GUARD_KEY, LANDED_RELOAD_GUARD_VALUE)
const LazyRejectingImport = lazyWithRetry(
() => Promise.reject(new SyntaxError("Unexpected token '<'")),
{ retries: 0 }
)
;({ container, root } = createContainer())
for (const boundaryKey of ['pane-a', 'pane-b', 'pane-c']) {
await act(async () => {
root?.render(
<BoundaryHarness boundaryKey={boundaryKey}>
<LazyRejectingImport />
</BoundaryHarness>
)
})
await flushReactWork()
await flushReactWork()
}
expect(container?.textContent).toContain('rich markdown editor')
expect(recordBreadcrumbMock).toHaveBeenCalledTimes(1)
})
it('still reports ordinary render errors', async () => {
const error = new Error('ordinary render failure')
function BrokenEditor(): ReactElement {
@@ -75,20 +75,6 @@ function createClipboardEventMock(options?: { failReadback?: boolean }): {
}
describe('rich markdown cut handler behavior', () => {
it('hard-wrapped document prose stays one paragraph after normalization', () => {
const editor = createEditor('Line one\nLine two\nLine three\n')
expect(countParagraphs(editor)).toBe(1)
expect(editor.state.doc.firstChild!.textContent).toContain('\n')
normalizeEmptyListItems(editor)
expect(countParagraphs(editor)).toBe(1)
expect(editor.state.doc.firstChild!.textContent).toBe('Line one\nLine two\nLine three')
editor.destroy()
})
it('soft-break normalization still creates visible paragraph breaks', () => {
const editor = createEditor('Line one\nLine two\nLine three\n')
normalizeSoftBreaks(editor)
@@ -201,28 +187,4 @@ describe('rich markdown cut handler behavior', () => {
editor.destroy()
}
})
it('normalizeEmptyListItems is idempotent on already-clean documents', () => {
const editor = createEditor('First.\n\nSecond.\n\nThird.\n')
const docBefore = editor.state.doc.toJSON()
normalizeEmptyListItems(editor)
const docAfter = editor.state.doc.toJSON()
expect(docAfter).toEqual(docBefore)
editor.destroy()
})
it('normalizeEmptyListItems does not modify populated list items or blockquotes', () => {
const editor = createEditor('- Item 1\n- Item 2\n')
const docBefore = editor.state.doc.toJSON()
normalizeEmptyListItems(editor)
const docAfter = editor.state.doc.toJSON()
expect(docAfter).toEqual(docBefore)
editor.destroy()
})
})
@@ -1,222 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { PreflightStatus } from '../../../../preload/api-types'
import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context'
import { ConnectIntegrationsList } from './ConnectIntegrationsList'
type StoreState = {
activeRepoId: string | null
activeWorktreeId: string | null
worktreesByRepo: Record<string, unknown[]>
repos: unknown[]
settings: { activeRuntimeEnvironmentId?: string | null }
preflightStatus: PreflightStatus | null
preflightStatusChecked: boolean
preflightStatusContextKey: string
preflightStatusError: string | null
preflightStatusLoading: boolean
refreshPreflightStatus: () => Promise<void>
linearStatus: { connected: boolean; workspaces?: unknown[] }
linearStatusChecked: boolean
linearStatusContextKey: string | null
checkLinearConnection: () => Promise<void>
testLinearConnection: () => Promise<{ ok: boolean; error?: string }>
disconnectLinear: () => Promise<void>
disconnectLinearWorkspace: () => Promise<void>
jiraStatus: { connected: boolean; sites?: unknown[] }
jiraStatusChecked: boolean
jiraStatusContextKey: string | null
checkJiraConnection: () => Promise<void>
testJiraConnection: () => Promise<{ ok: boolean; error?: string }>
disconnectJira: () => Promise<void>
}
const { storeState } = vi.hoisted(() => ({
storeState: { current: null as StoreState | null }
}))
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: StoreState) => unknown) => {
if (!storeState.current) {
throw new Error('Store state was not installed')
}
return selector(storeState.current)
},
{ getState: () => storeState.current }
)
}))
vi.mock('@/components/linear-api-key-dialog', () => ({
LinearApiKeyDialog: () => null
}))
vi.mock('@/components/jira-connect-dialog', () => ({
JiraConnectDialog: () => null
}))
function makePreflightStatus(overrides: Partial<PreflightStatus> = {}): PreflightStatus {
const status: PreflightStatus = {
git: { installed: true },
gh: { installed: true, authenticated: false },
glab: { installed: true, authenticated: false },
bitbucket: {
configured: false,
authenticated: false,
account: null
},
azureDevOps: {
configured: false,
authenticated: false,
account: null,
baseUrl: null,
tokenConfigured: false
},
gitea: {
configured: false,
authenticated: false,
account: null,
baseUrl: null,
tokenConfigured: false
}
}
return { ...status, ...overrides }
}
function installStore(preflightStatus: PreflightStatus): void {
const settings = { activeRuntimeEnvironmentId: null }
const providerContextKey = getProviderRuntimeContextKey(settings)
storeState.current = {
activeRepoId: null,
activeWorktreeId: null,
worktreesByRepo: {},
repos: [],
settings,
preflightStatus,
preflightStatusChecked: true,
preflightStatusContextKey: 'host',
preflightStatusError: null,
preflightStatusLoading: false,
refreshPreflightStatus: vi.fn(async () => {}),
linearStatus: { connected: false, workspaces: [] },
linearStatusChecked: true,
linearStatusContextKey: providerContextKey,
checkLinearConnection: vi.fn(async () => {}),
testLinearConnection: vi.fn(async () => ({ ok: true })),
disconnectLinear: vi.fn(async () => {}),
disconnectLinearWorkspace: vi.fn(async () => {}),
jiraStatus: { connected: false, sites: [] },
jiraStatusChecked: true,
jiraStatusContextKey: providerContextKey,
checkJiraConnection: vi.fn(async () => {}),
testJiraConnection: vi.fn(async () => ({ ok: true })),
disconnectJira: vi.fn(async () => {})
}
}
async function renderConnectIntegrationsList(): Promise<{
markup: string
}> {
return { markup: renderToStaticMarkup(<ConnectIntegrationsList />) }
}
describe('ConnectIntegrationsList', () => {
beforeEach(() => {
vi.stubGlobal('window', {
api: {
shell: {
openUrl: vi.fn()
}
}
})
})
afterEach(() => {
storeState.current = null
vi.unstubAllGlobals()
vi.restoreAllMocks()
})
it('renders the settings review cards in the review step without an inline auth terminal', async () => {
installStore(makePreflightStatus())
const { markup } = await renderConnectIntegrationsList()
for (const provider of ['GitHub', 'GitLab', 'Bitbucket', 'Azure DevOps', 'Gitea']) {
expect(markup).toContain(provider)
}
expect(markup).toContain('gh auth login')
expect(markup).toContain('glab auth login')
expect(markup).not.toContain('Run in terminal')
})
it('keeps the upcoming task step collapsed but openable, not inert', async () => {
installStore(makePreflightStatus())
const { markup } = await renderConnectIntegrationsList()
// Review is optional ordering: tracker-first users can expand the task step.
expect(markup).toContain('Open')
expect(markup).not.toContain('opacity-55')
expect(markup).not.toContain('Add Linear access')
})
it('collapses the task step to its summary when a tracker connects first', async () => {
installStore(makePreflightStatus())
if (!storeState.current) {
throw new Error('Store state was not installed')
}
storeState.current.linearStatus = { connected: true, workspaces: [] }
const { markup } = await renderConnectIntegrationsList()
expect(markup).toContain('connected for tasks')
expect(markup).not.toContain('Connect Jira')
})
it('auto-resolves the task step from a connected code host but keeps it open for trackers', async () => {
installStore(makePreflightStatus({ gh: { installed: true, authenticated: true } }))
const { markup } = await renderConnectIntegrationsList()
expect(markup).toContain('GitHub')
expect(markup).toContain('issues available as tasks')
expect(markup).toContain('add Linear or Jira if your team plans work there')
expect(markup).not.toContain('Use GitHub issues')
// Keep dedicated trackers discoverable even when code-host issues already satisfy tasks.
expect(markup).toContain('Add Linear access')
expect(markup).toContain('Connect Jira')
})
it('offers GitHub and GitLab as task sources when review came from a non-task provider', async () => {
installStore(
makePreflightStatus({
bitbucket: { configured: true, authenticated: true, account: 'acme' }
})
)
const { markup } = await renderConnectIntegrationsList()
expect(markup).toContain('issues also work as tasks.')
expect(markup).toContain('gh auth login')
expect(markup).toContain('glab auth login')
expect(markup).toContain('Linear')
expect(markup).toContain('Jira')
})
it('lists the code host alongside a connected tracker in the task summary', async () => {
installStore(makePreflightStatus({ gh: { installed: true, authenticated: true } }))
if (!storeState.current) {
throw new Error('Store state was not installed')
}
storeState.current.linearStatus = { connected: true, workspaces: [] }
const { markup } = await renderConnectIntegrationsList()
expect(markup).toContain('Linear')
expect(markup).toContain('GitHub')
expect(markup).toContain('connected for tasks')
expect(markup).toContain(' and ')
expect(markup).not.toContain('Connect Jira')
})
})
@@ -8,11 +8,8 @@ import type {
DeveloperPermissionRequestResult,
DeveloperPermissionState
} from '../../../../shared/developer-permissions-types'
import {
FullDiskAccessSetupPrompt,
isFullDiskAccessReady,
isFullDiskAccessSetupVisible
} from './FullDiskAccessSetupPrompt'
import { FullDiskAccessSetupPrompt } from './FullDiskAccessSetupPrompt'
import { toast } from 'sonner'
vi.mock('sonner', () => ({
@@ -75,24 +72,6 @@ describe('FullDiskAccessSetupPrompt state helpers', () => {
})
})
it('hides the setup prompt before status is known or when unsupported', () => {
expect(isFullDiskAccessSetupVisible(undefined)).toBe(false)
expect(isFullDiskAccessSetupVisible('unsupported')).toBe(false)
})
it('shows the setup prompt only for macOS statuses users can act on', () => {
expect(isFullDiskAccessSetupVisible('unknown')).toBe(true)
expect(isFullDiskAccessSetupVisible('denied')).toBe(true)
expect(isFullDiskAccessSetupVisible('granted')).toBe(false)
expect(isFullDiskAccessSetupVisible('ready')).toBe(false)
})
it('treats granted and entitled statuses as ready', () => {
expect(isFullDiskAccessReady('granted')).toBe(true)
expect(isFullDiskAccessReady('ready')).toBe(true)
expect(isFullDiskAccessReady('unknown')).toBe(false)
})
it('refreshes macOS Full Disk Access status when Orca regains focus', async () => {
setUserAgent('Macintosh')
const getStatus = vi
@@ -52,21 +52,6 @@ describe('host lists with a 13,692-host ssh config', () => {
expect(screen.getAllByRole('option')[0]?.textContent).toContain(lastName)
})
it('HostCombobox treats re-picking the current host as no change', async () => {
const onChange = vi.fn()
render(
<HostCombobox
value="ssh-0"
onChange={onChange}
ariaLabel="Host"
options={names.slice(0, 3).map((name, i) => ({ id: `ssh-${i}`, label: name }))}
/>
)
fireEvent.click(screen.getByRole('combobox', { name: 'Host' }))
fireEvent.click(await screen.findByRole('option', { name: names[0] }))
expect(onChange).not.toHaveBeenCalled()
})
it('Settings > Servers SSH host picker', async () => {
const onChange = vi.fn()
render(
@@ -159,15 +159,6 @@ describe('Jira transition control', () => {
expect(mutate).not.toHaveBeenCalled()
})
it('keeps failures visible and exposes Jira fallback', async () => {
await choose(
[close],
vi.fn().mockResolvedValue({ ok: false, error: 'Resolution is required.' })
)
expect((await screen.findByRole('alert')).textContent).toContain('Resolution is required.')
expect(screen.getByRole('button', { name: 'Open in Jira' })).toBeTruthy()
})
it('keeps a rejected mutation visible and offers Jira fallback', async () => {
await choose([close], vi.fn().mockRejectedValue(new Error('Disconnected from Jira host.')))
expect((await screen.findByRole('alert')).textContent).toContain('Disconnected from Jira host.')
@@ -5,11 +5,10 @@ import { NativeChatResumeOnRestartModal } from './NativeChatResumeRuntimeModal'
import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment'
import {
createResumeModalFixture,
failure,
offered,
type RestartRpc,
type ResumeStatusStream
} from './native-chat-resume-modal.test-support'
import { lastToastShow } from './native-chat-resume-toast.test-support'
import { chatBox, dontAskAgain } from './native-chat-resume-on-restart-modal.test-support'
import { useAppStore } from '../store'
@@ -47,22 +46,6 @@ async function mountSurfaces(): Promise<void> {
)
}
it('shows a finished resume as a result, in the past tense, once nothing is left to choose', async () => {
fakeHost()
await mountSurfaces()
await act(async () => button('Resume 2 chats').click())
await act(async () => lastToastShow()?.())
expect(dialog()?.textContent).toContain('Resumed 2 of 2 chats')
expect(dialog()?.textContent).toContain(
'Orca restored each one where it stopped, with its full context'
)
expect(dialog()?.textContent).not.toContain('Resuming restores')
expect(tree()?.querySelectorAll('[role="checkbox"]')).toHaveLength(0)
expect(tree()?.textContent).not.toContain('Select all')
expect(dialog()?.textContent).toContain("Don't ask again")
expect(dialog()?.textContent).not.toContain('Resumed automatically')
})
it('saves "Don\'t ask again" ticked on a finished resume the user started', async () => {
fakeHost()
await mountSurfaces()
@@ -151,32 +134,6 @@ it('reopens a collapsed result group with tree keys', async () => {
expect(document.activeElement).toBe(disclosure)
})
it('shows a settled nonretryable failure as a result with a Done action', async () => {
const failed = { ...failure('a'), outcome: 'refused' as const, retryable: false }
let acted = false
rpc.mockImplementation(async (_target, method) => {
if (method === 'agentSession.restartResumable') {
return { sessions: acted ? [] : [offered[0]], failed: acted ? [failed] : [] }
}
acted = true
return {
sessions: [],
failed: [failed],
continued: [{ sessionId: 'a', outcome: 'refused' }]
}
})
await mountSurfaces()
await act(async () => button('Resume 1 chat').click())
await act(async () => lastToastShow()?.())
expect(dialog()?.textContent).toContain('Resumed 0 of 1 chat')
expect(tree()?.textContent).not.toContain('Select all')
expect(tree()?.querySelectorAll('[role="checkbox"]')).toHaveLength(0)
expect(button('Open chat')).toBeTruthy()
expect(button('Dismiss "Prompt a" in workspace')).toBeTruthy()
expect(button('Done').disabled).toBe(false)
expect(dialog()?.textContent).not.toContain('Resume 0 chats')
})
it('does not place an untouched offer in the Need you filter', async () => {
fakeHost()
await mountSurfaces()
@@ -216,14 +173,3 @@ it('says a launch resumed on its own and links to the setting', async () => {
})
expect(dialog()).toBeNull()
})
it('keeps the checkboxes while a chat can still be chosen, whichever tab is shown', async () => {
fakeHost({}, (sessionId) => (sessionId === 'b' ? 'refused' : 'continued'))
await mountSurfaces()
await act(async () => button('Resume 2 chats').click())
await act(async () => button('1 chat failed to resume').click())
expect(dialog()?.textContent).toContain('The ones Orca resumed were restored')
expect(tree()?.textContent).toContain('Select all')
await act(async () => button('Resumed1').click())
expect(tree()?.textContent).toContain('Select all')
})
@@ -110,34 +110,6 @@ describe('native chat Mermaid fences', () => {
expect(container.querySelector('[data-code-language="mermaid"]')).toBeNull()
})
it('shows partial source while streaming and renders the diagram when the reply finishes', async () => {
const { container, rerender } = render(reply(partial, true))
expect(container.querySelector('[data-native-chat-code-content]')?.textContent).toContain(
partial
)
expect(container.querySelector('[data-code-language="mermaid"]')).toBeInTheDocument()
expect(container.querySelector('.mermaid-block')).toBeNull()
rerender(reply(complete, true))
expect(container.querySelector('pre')?.textContent).toContain(complete)
rerender(reply(complete, false))
await waitFor(() =>
expect(container.querySelector('svg')).toHaveTextContent('Sync with main once')
)
expect(mermaid.render).toHaveBeenCalledWith(expect.any(String), complete)
expect(container.querySelector('pre')).toBeNull()
expect(screen.queryByText(/Diagram error:/)).not.toBeInTheDocument()
})
it('still shows the existing error and source for an invalid finished reply', async () => {
mermaid.render.mockRejectedValue(new Error('Invalid mermaid syntax'))
const { container } = render(reply(partial, false))
await screen.findByText(/Diagram error: Invalid mermaid syntax/)
expect(container.querySelector('pre')?.textContent).toBe(partial.trimEnd())
expect(container.querySelector('svg')).toBeNull()
})
it('keeps the source mounted until a pending diagram fails', async () => {
const pending = holdNextRender()
const view = render(reply(partial, true))
@@ -20,28 +20,6 @@ afterEach(() => {
})
describe('checks panel concrete content', () => {
it('renders the no-workspace empty state with its guidance', () => {
const model = { activeWorktree: null } as ChecksPanelEmptyContentModel
render(<ChecksPanelEmptyContent model={model} />)
expect(screen.getByText('No workspace selected')).toBeTruthy()
expect(screen.getByText('Select a workspace to view checks')).toBeTruthy()
})
it('renders the folder-workspace empty state without review controls', () => {
const model = {
activeWorktree: { id: 'folder-1' },
isFolder: true
} as ChecksPanelEmptyContentModel
render(<ChecksPanelEmptyContent model={model} />)
expect(screen.getByText('Checks unavailable')).toBeTruthy()
expect(screen.getByText('Checks require a Git branch and hosted review context')).toBeTruthy()
expect(screen.queryByRole('button')).toBeNull()
})
it('renders a durable unlinked state that can relink before PR data refetches', () => {
const handleLinkSuppressedPullRequest = vi.fn<() => void>()
const model = {
@@ -64,16 +64,6 @@ function openDialog(): HTMLInputElement {
}
describe('Use a Node.js mirror', () => {
it('opens from the host row with the suggested mirror, focused and editable', () => {
render(<Host status={blocked} />)
const input = openDialog()
expect(screen.getByRole('dialog', { name: 'Use a Node.js mirror' })).toBeInTheDocument()
expect(input).toHaveValue('https://npmmirror.com/mirrors/node/')
expect(input).toHaveFocus()
expect(screen.getByText(/checked against the checksum Orca pins/)).toBeInTheDocument()
})
it('saves to the mirror setting, then retries the connect and closes', async () => {
render(<Host status={blocked} />)
openDialog()
@@ -147,16 +137,6 @@ describe('Use a Node.js mirror', () => {
expect(state.activeModal).toBe('none')
})
it('offers nothing in a paired browser, whose settings are not this computer’s', () => {
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
try {
render(<Host status={blocked} />)
expect(screen.queryByRole('button')).toBeNull()
} finally {
vi.unstubAllGlobals()
}
})
it('opens the setting instead when a mirror was set after the toast appeared', () => {
useAppStore.getState().setSshConnectionState('ssh-1', {
targetId: 'ssh-1',
@@ -1,226 +0,0 @@
// @vitest-environment happy-dom
import { renderToStaticMarkup } from 'react-dom/server'
import { Bot, GitBranch, Mic, Network, Puzzle } from 'lucide-react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../shared/constants'
import { buildSettingsNavigationMetadata } from '@/hooks/useSettingsNavigationMetadata'
import type * as ShortcutLabels from '@/hooks/useShortcutLabel'
import { SettingsSidebar } from './SettingsSidebar'
import { TooltipProvider } from '../ui/tooltip'
import type { SettingsSetupGuideProgress } from './settings-setup-guide-progress'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
const mocks = vi.hoisted(() => ({
useSettingsSetupGuideProgress: vi.fn()
}))
vi.mock('@/hooks/useShortcutLabel', async () => {
const actual = await vi.importActual<typeof ShortcutLabels>('@/hooks/useShortcutLabel')
return {
...actual,
useShortcutLabel: () => '⌘F',
useShortcutKeyComboDetails: () => [{ keys: ['⌘', 'F'], doubleTap: false }]
}
})
vi.mock('./settings-setup-guide-progress', () => ({
useSettingsSetupGuideProgress: mocks.useSettingsSetupGuideProgress
}))
function makeSetupGuideProgress(
overrides: Partial<SettingsSetupGuideProgress> = {}
): SettingsSetupGuideProgress {
return {
ready: true,
doneCount: 5,
total: 8,
firstIncompleteStepId: 'agent-capabilities',
...overrides
}
}
function renderSidebar(
activeSectionId = 'orchestration',
settings: GlobalSettings = getDefaultSettings('/tmp')
): string {
return renderToStaticMarkup(
<TooltipProvider>
<SettingsSidebar
activeSectionId={activeSectionId}
settings={settings}
generalGroups={[
{
id: 'capabilities',
title: 'AI Capabilities',
sections: [
{
id: 'agents',
title: 'Agents',
icon: Bot
},
{
id: 'orchestration',
title: 'Orchestration',
icon: Network,
installStatus: 'install'
},
{
id: 'voice',
title: 'Voice',
icon: Mic,
installStatus: 'installed'
},
{
id: 'computer-use',
title: 'Computer Use',
icon: Bot,
installStatus: 'up-to-date'
},
{
id: 'voice-loading',
title: 'Voice Loading',
icon: Mic,
installStatus: 'checking'
},
{
id: 'linear',
title: 'Linear',
icon: GitBranch,
installStatus: 'update-available'
},
{
id: 'ephemeral-vms',
title: 'Ephemeral VMs',
icon: Bot,
installStatus: 'needs-attention'
},
{
id: 'plugins',
title: 'Plugins',
icon: Puzzle
}
]
},
{
id: 'setup',
title: 'Set Up',
sections: [
{
id: 'accounts',
title: 'AI Provider Accounts',
icon: Bot,
badge: 'Optional'
}
]
}
]}
repoSections={[]}
hasRepos={false}
onBack={vi.fn()}
onSelectSection={vi.fn()}
/>
</TooltipProvider>
)
}
describe('SettingsSidebar', () => {
beforeEach(() => {
mocks.useSettingsSetupGuideProgress.mockReset()
mocks.useSettingsSetupGuideProgress.mockReturnValue(makeSetupGuideProgress())
})
afterEach(() => {
document.body.innerHTML = ''
})
it('renders Chat navigation', () => {
const settings = getDefaultSettings('/tmp')
const sections = buildSettingsNavigationMetadata({
isMac: false,
isWindows: false,
isWebClient: false,
repos: []
}).filter((section) => section.group === 'interface')
const container = document.createElement('div')
container.innerHTML = renderToStaticMarkup(
<TooltipProvider>
<SettingsSidebar
activeSectionId="appearance"
settings={settings}
generalGroups={[{ id: 'interface', title: 'Interface', sections }]}
repoSections={[]}
hasRepos={false}
onBack={vi.fn()}
onSelectSection={vi.fn()}
/>
</TooltipProvider>
)
const buttons = Array.from(container.querySelectorAll('button'))
expect(buttons.some((button) => button.textContent === 'Chat')).toBe(true)
})
it('applies left sidebar appearance styles to the settings navigation', () => {
const markup = renderSidebar('orchestration', {
...getDefaultSettings('/tmp'),
leftSidebarAppearanceMode: 'match-terminal',
terminalColorOverrides: {
background: '#101820',
foreground: '#f0f4f8'
}
})
expect(markup).toContain('--worktree-sidebar:#101820')
expect(markup).toContain('--worktree-sidebar-foreground:#f0f4f8')
})
it('reserves install state labels for actionable skill states', () => {
const markup = renderSidebar()
expect(markup).not.toContain('Not installed')
expect(markup).not.toContain('Installed')
expect(markup).not.toContain('Up to date')
expect(markup).not.toContain('Checking...')
expect(markup).toContain('Update available')
expect(markup).toContain('Review skill')
expect(markup).toContain('Optional')
})
it('does not render the setup guide row before progress readiness settles', () => {
mocks.useSettingsSetupGuideProgress.mockReturnValue(
makeSetupGuideProgress({
ready: false,
doneCount: 7,
firstIncompleteStepId: 'setup-script'
})
)
expect(renderSidebar()).not.toContain('Onboarding checklist')
})
it('renders incomplete setup progress with the full checklist total', () => {
const markup = renderSidebar()
expect(markup).toContain('Onboarding checklist')
expect(markup).toContain('Onboarding checklist, 5 of 8 done. Show setup guide.')
expect(markup).toContain('5 of 8 setup steps complete')
})
it('does not render the setup guide row after every checklist step is complete', () => {
mocks.useSettingsSetupGuideProgress.mockReturnValue(
makeSetupGuideProgress({
doneCount: 8,
firstIncompleteStepId: null
})
)
expect(renderSidebar()).not.toContain('Onboarding checklist')
})
it('keeps the setup guide row available from Settings when incomplete', () => {
const markup = renderSidebar('setup-guide')
expect(markup).toContain('aria-current="page"')
expect(markup).toContain('Onboarding checklist')
})
})
@@ -52,57 +52,6 @@ function button(label: string): HTMLButtonElement {
}
describe('SshTargetForm', () => {
it('opens add mode as a dialog with primary fields in the viewport', async () => {
const root = await renderForm({})
expect(document.body.textContent).toContain('Add SSH host')
expect(document.body.textContent).toContain(
'Add a persistent machine you can log into over SSH.'
)
expect(document.querySelector('#ssh-target-host')).not.toBeNull()
expect(document.querySelector('#ssh-target-label')).not.toBeNull()
expect(document.body.textContent).toContain('Add host')
expect(document.body.textContent).not.toContain('Editing')
act(() => root.unmount())
})
it('pins header and footer outside the scroll body', async () => {
const root = await renderForm({})
const content = document.querySelector('[data-slot="dialog-content"]')
const header = document.querySelector('[data-slot="dialog-header"]')
const footer = document.querySelector('[data-slot="dialog-footer"]')
if (!content || !header || !footer) {
throw new Error('missing dialog chrome')
}
expect(content.className).toContain('overflow-hidden')
expect(content.className).toContain('flex-col')
expect(header.className).toContain('shrink-0')
expect(footer.className).toContain('shrink-0')
const scrollBody = content.querySelector('.overflow-y-auto')
expect(scrollBody).not.toBeNull()
expect(scrollBody?.contains(header)).toBe(false)
expect(scrollBody?.contains(footer)).toBe(false)
act(() => root.unmount())
})
it('shows edit title and context chip for the target being edited', async () => {
const root = await renderForm({
editingId: 'target-1',
form: {
...EMPTY_FORM,
label: 'dev-box',
host: 'dev-box.lan',
username: 'alice',
port: '2222'
}
})
expect(document.body.textContent).toContain('Edit SSH host')
expect(document.body.textContent).toContain('Save Changes')
expect(document.body.textContent).toContain('Editing')
expect(document.body.textContent).toContain('dev-box')
expect(document.body.textContent).toContain('alice@dev-box.lan:2222')
act(() => root.unmount())
})
it('submits via the primary action and closes via Cancel', async () => {
const onSave = vi.fn()
const onOpenChange = vi.fn()
@@ -120,13 +69,6 @@ describe('SshTargetForm', () => {
act(() => root.unmount())
})
it('does not render dialog content when closed', async () => {
const root = await renderForm({ open: false })
expect(document.querySelector('#ssh-target-host')).toBeNull()
expect(document.body.textContent).not.toContain('Add SSH host')
act(() => root.unmount())
})
it('collapses Advanced again after cancel and reopen', async () => {
let form: EditingTarget = EMPTY_FORM
const onFormChange = vi.fn((updater: (prev: EditingTarget) => EditingTarget) => {
@@ -209,19 +151,4 @@ describe('SshTargetForm', () => {
expect(onOpenChange).not.toHaveBeenCalled()
act(() => root.unmount())
})
it('opens Advanced by default when the target already has advanced values', async () => {
const root = await renderForm({
editingId: 'target-1',
form: {
...EMPTY_FORM,
label: 'bastion',
host: 'bastion.example',
jumpHost: 'jump.example'
}
})
expect(button('Advanced').getAttribute('data-state')).toBe('open')
expect(document.body.textContent).toContain('Jump Host')
act(() => root.unmount())
})
})
@@ -1,219 +0,0 @@
// @vitest-environment happy-dom
import React, { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { getDefaultSettings } from '../../../../../shared/constants'
import type { Project } from '../../../../../shared/project-types'
import type { Repo } from '../../../../../shared/repo-types'
import type { TerminalTab } from '../../../../../shared/terminal-tab-types'
import type { AgentStatusEntry } from '../../../../../shared/agent-status-types'
import { useAppStore } from '@/store/index'
import { getRepositoryPaneSearchEntries } from './repository-search'
import { RepositoryPane } from './RepositoryPane'
import { matchesRepositoryIdentitySearch } from './identity/repository-identity-search'
import { matchesSettingsSearch } from '../settings-search'
import { TooltipProvider } from '../../ui/tooltip'
let container: HTMLDivElement
let root: Root
const repo: Repo = {
id: 'repo-1',
path: '/tmp/repo',
displayName: 'Example Repo',
badgeColor: '#000000',
addedAt: 1,
kind: 'git'
}
const project: Project = {
id: 'project-1',
displayName: 'Example Project',
badgeColor: '#000000',
sourceRepoIds: ['repo-1'],
createdAt: 1,
updatedAt: 1
}
function terminalTab(id: string, worktreeId: string): TerminalTab {
return {
id,
ptyId: null,
worktreeId,
title: 'Terminal',
customTitle: null,
color: null,
sortOrder: 0,
createdAt: 1
}
}
function activeAgent(paneKey: string, worktreeId: string): AgentStatusEntry {
return {
state: 'working',
prompt: 'Implement runtime switch',
updatedAt: 1,
stateStartedAt: 1,
agentType: 'codex',
paneKey,
tabId: paneKey.split(':')[0],
worktreeId,
stateHistory: []
}
}
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState(), true)
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
// Provide the fs bridge needed by mounted WorktreeSymlinksSection path suggestions.
;(window as unknown as { api: unknown }).api = {
preflight: { detectAgents: async () => [] },
fs: { readDir: () => Promise.resolve([]) }
}
})
afterEach(() => {
act(() => {
root.unmount()
})
container.remove()
delete (window as unknown as { api?: unknown }).api
useAppStore.setState(useAppStore.getInitialState(), true)
})
describe('RepositoryPane search entries', () => {
it('keeps renamed hook sections reachable through settings search', () => {
const entries = getRepositoryPaneSearchEntries(repo, { isLocalWindowsProject: true })
expect(matchesSettingsSearch('setup script', entries)).toBe(true)
expect(matchesSettingsSearch('archive script', entries)).toBe(true)
expect(matchesSettingsSearch('setup command', entries)).toBe(true)
expect(matchesSettingsSearch('archive command', entries)).toBe(true)
expect(matchesSettingsSearch('advanced', entries)).toBe(true)
expect(matchesSettingsSearch('runtime', entries)).toBe(true)
expect(matchesSettingsSearch('wsl', entries)).toBe(true)
expect(matchesSettingsSearch('windows host', entries)).toBe(true)
expect(matchesSettingsSearch('command source', entries)).toBe(true)
expect(matchesSettingsSearch('local settings scripts', entries)).toBe(true)
expect(matchesSettingsSearch('../worktrees', entries)).toBe(true)
expect(matchesSettingsSearch('worktree path', entries)).toBe(true)
})
it('includes each project search section once', () => {
const entries = getRepositoryPaneSearchEntries(repo)
expect(entries.filter(({ title }) => title === 'Project Icon')).toHaveLength(1)
expect(entries.filter(({ title }) => title === 'Default Worktree Base')).toHaveLength(1)
})
it('omits project runtime search for remote or unsupported repos', () => {
expect(matchesSettingsSearch('project runtime', getRepositoryPaneSearchEntries(repo))).toBe(
false
)
expect(
matchesSettingsSearch(
'project runtime',
getRepositoryPaneSearchEntries(
{
...repo,
connectionId: 'builder',
executionHostId: 'ssh:builder'
},
{ windowsRuntimeSupported: true }
)
)
).toBe(false)
})
it('matches project identity searches on display name and path only', () => {
expect(matchesRepositoryIdentitySearch('example repo', repo)).toBe(true)
expect(matchesRepositoryIdentitySearch('/tmp/repo', repo)).toBe(true)
expect(matchesRepositoryIdentitySearch('setup script', repo)).toBe(false)
})
it('renders full hook controls when search matches the project name', () => {
useAppStore.setState({
settingsSearchQuery: 'Example Repo',
settingsSearchInputQuery: 'Example Repo'
})
try {
const html = renderToStaticMarkup(
React.createElement(
TooltipProvider,
null,
React.createElement(RepositoryPane, {
repo,
yamlHooks: null,
hasHooksFile: false,
hooksInspectionReady: true,
mayNeedUpdate: false,
updateRepo: vi.fn(),
removeProject: vi.fn()
})
)
)
expect(html).toContain('Worktree Hooks')
expect(html).toContain('Setup Script')
expect(html).toContain('Archive Script')
expect(html).toContain('Custom GitHub Issue Command')
} finally {
useAppStore.setState({
settingsSearchQuery: '',
settingsSearchInputQuery: ''
})
}
})
it('warns about live terminals and active tasks before project runtime changes', () => {
const worktreeId = 'repo-1::/tmp/repo'
useAppStore.setState({
settingsSearchQuery: 'Example Repo',
settingsSearchInputQuery: 'Example Repo',
settings: getDefaultSettings('/tmp'),
tabsByWorktree: {
[worktreeId]: [terminalTab('tab-1', worktreeId), terminalTab('tab-2', worktreeId)]
},
ptyIdsByTabId: {
'tab-1': ['pty-1'],
'tab-2': ['pty-2']
},
agentStatusByPaneKey: {
'tab-2:0': activeAgent('tab-2:0', worktreeId)
}
})
act(() => {
root.render(
React.createElement(
TooltipProvider,
null,
React.createElement(RepositoryPane, {
repo,
project,
yamlHooks: null,
hasHooksFile: false,
hooksInspectionReady: true,
mayNeedUpdate: false,
updateRepo: vi.fn(),
removeProject: vi.fn(),
isLocalWindowsProject: true,
wslAvailable: true,
wslDistros: ['Ubuntu-24.04'],
wslCapabilitiesLoading: false,
updateProject: vi.fn()
})
)
)
})
expect(container.textContent).toContain('2 live terminals')
expect(container.textContent).toContain('1 active task')
expect(container.textContent).toContain('finish or restart')
})
})
@@ -158,32 +158,9 @@ describe('RepositoryWorktreeDefaultsSection — worktree path', () => {
expect(updateRepo).toHaveBeenCalledWith('repo-1', { worktreeBasePath: undefined })
})
it('calls updateRepo with undefined when the value is whitespace-only', () => {
const updateRepo = vi.fn()
render({ ...BASE_REPO, worktreeBasePath: '../worktrees' }, updateRepo)
const input = getWorktreePathInput()
typeText(input, ' ')
blurInput(input)
expect(updateRepo).toHaveBeenCalledWith('repo-1', { worktreeBasePath: undefined })
})
})
describe('RepositoryWorktreeDefaultsSection — external visibility', () => {
it('shows the inherited effective value without stamping the repo', () => {
render(BASE_REPO, vi.fn(), {
settings: {
workspaceDir: '/home/user/orca/workspaces',
worktreeVisibilityDefaults: { external: 'show' }
}
})
expect(container.textContent).toContain('Using global: Show')
expect(container.querySelector('select')?.value).toBe('global')
})
it('clears the override with the remote-safe null sentinel and refreshes classification', async () => {
const updateRepo = vi.fn().mockResolvedValue(true)
const refreshRepo = vi.fn().mockResolvedValue(true)
@@ -89,28 +89,7 @@ async function openPicker(): Promise<ReturnType<typeof userEvent.setup>> {
return user
}
async function openPickerWith(result: Record<string, unknown>): Promise<void> {
listConfigHosts.mockResolvedValue({ hasMore: false, ...result })
const user = userEvent.setup()
render(<AddRemoteHostDialog mode="ssh" onOpenChange={vi.fn()} />)
await user.click(screen.getByRole('button', { name: /Fill from/ }))
}
describe('SSH config picker host selection', () => {
it('drops a resolve that lands after the user backs out of the picker', async () => {
const pending = deferred<SshConfigHostResolution>()
resolveConfigHost.mockReturnValueOnce(pending.promise)
const user = await openPicker()
await user.click(screen.getByRole('button', { name: /alpha/ }))
await user.click(screen.getByRole('button', { name: 'Back' }))
pending.resolve(resolution('alpha', 'alpha.internal'))
await waitFor(() => expect(screen.getByLabelText('Host or alias')).toBeDefined())
expect((screen.getByLabelText('Host or alias') as HTMLInputElement).value).toBe('')
expect(toastMocks.success).not.toHaveBeenCalled()
})
it('keeps the host the user settled on when an earlier resolve lands late', async () => {
const slowAlpha = deferred<SshConfigHostResolution>()
resolveConfigHost.mockReturnValueOnce(slowAlpha.promise)
@@ -136,76 +115,4 @@ describe('SSH config picker host selection', () => {
'bravo.internal'
)
})
it('freezes the other rows while a pick is resolving', async () => {
const pending = deferred<SshConfigHostResolution>()
resolveConfigHost.mockReturnValueOnce(pending.promise)
const user = await openPicker()
await user.click(screen.getByRole('button', { name: /alpha/ }))
await waitFor(() =>
expect(screen.getByRole('button', { name: /bravo/ }).hasAttribute('disabled')).toBe(true)
)
await act(async () => {
pending.resolve(resolution('alpha', 'alpha.internal'))
await pending.promise
})
})
})
describe('SSH config picker tombstoned hosts', () => {
it('keeps a previously removed host pickable while an adopted one stays disabled', async () => {
await openPickerWith({
hosts: [
configHost('removed', { previouslyRemoved: true }),
configHost('kept', { alreadyInOrca: true })
],
totalHostCount: 2,
newHostCount: 0,
matchCount: 2
})
const removed = await screen.findByRole('button', { name: /removed/ })
expect(removed.hasAttribute('disabled')).toBe(false)
expect(removed.textContent).toContain('Removed from Orca')
const kept = screen.getByRole('button', { name: /kept/ })
expect(kept.hasAttribute('disabled')).toBe(true)
expect(kept.textContent).toContain('In Orca')
})
it('never claims the config is empty when every host is only tombstoned', async () => {
await openPickerWith({
hosts: [configHost('removed', { previouslyRemoved: true })],
totalHostCount: 1,
newHostCount: 0,
matchCount: 1
})
const addAll = await screen.findByRole('button', { name: 'No new hosts to add' })
expect(addAll.hasAttribute('disabled')).toBe(true)
expect(screen.queryByText('No hosts in ~/.ssh/config')).toBeNull()
})
it('still shows the empty state when the config really has no hosts', async () => {
await openPickerWith({ hosts: [], totalHostCount: 0, newHostCount: 0, matchCount: 0 })
expect(await screen.findByText('No hosts in ~/.ssh/config')).toBeDefined()
expect(screen.getByRole('button', { name: 'Add all to Orca' }).hasAttribute('disabled')).toBe(
true
)
})
})
describe('SSH config picker bulk add', () => {
it('adds only new hosts and never re-adopts deleted aliases', async () => {
const user = await openPicker()
await user.click(screen.getByRole('button', { name: /Add all 2 to Orca/ }))
await waitFor(() => expect(importConfig).toHaveBeenCalled())
expect(importConfig).toHaveBeenCalledWith()
expect(importConfig.mock.calls[0][0]).toBeUndefined()
})
})
@@ -1,198 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it, vi } from 'vitest'
import type { ComponentProps } from 'react'
import { Dialog } from '@/components/ui/dialog'
import { TooltipProvider } from '@/components/ui/tooltip'
import { AddRepoDialogStepContent } from './AddRepoDialogStepContent'
import type { NestedRepoScanResult } from '../../../../shared/project-group-types'
const nestedScan: NestedRepoScanResult = {
selectedPath: '/workspace/platform',
selectedPathKind: 'non_git_folder',
repos: [
{ path: '/workspace/platform/api', displayName: 'api', depth: 1 },
{ path: '/workspace/platform/cli', displayName: 'cli', depth: 1 }
],
truncated: false,
timedOut: false,
stopped: false,
durationMs: 5,
maxDepth: 3,
maxRepos: 100,
timeoutMs: null
}
type StepContentProps = ComponentProps<typeof AddRepoDialogStepContent>
function renderStepContent(overrides: Partial<StepContentProps>): string {
const props: StepContentProps = {
step: 'nested',
isRuntimeEnvironmentActive: false,
activeRuntimeEnvironmentId: null,
repoCount: 1,
isAdding: false,
addProjectBusyLabel: null,
nestedScanInProgress: false,
nestedScanId: null,
serverPath: '',
isAddingServerPath: false,
cloneUrl: '',
cloneDestination: '',
cloneError: null,
cloneProgress: null,
isCloning: false,
sshTargets: [],
selectedTargetId: null,
remotePath: '',
remoteError: null,
isAddingRemote: false,
nestedScan,
nestedSelectedPaths: new Set(nestedScan.repos.map((repo) => repo.path)),
nestedGroupName: 'platform',
createName: '',
createParent: '',
createError: null,
isCreating: false,
createDefaultParent: '',
createGitAvailability: 'unknown',
createRuntimeParentStatus: 'idle',
createParentDefaultPending: false,
onBrowse: vi.fn(),
onOpenCloneStep: vi.fn(),
onOpenCreateStep: vi.fn(),
onOpenRemoteStep: vi.fn(),
onStopNestedScan: vi.fn(),
onServerPathChange: vi.fn(),
onAddServerPath: vi.fn(),
onSelectTarget: vi.fn(),
onRemotePathChange: vi.fn(),
onAddRemoteRepo: vi.fn(),
onConnectTarget: vi.fn(),
onCloneUrlChange: vi.fn(),
onCloneDestinationChange: vi.fn(),
onPickCloneDestination: vi.fn(),
onClone: vi.fn(),
onNestedGroupNameChange: vi.fn(),
onNestedSelectedPathsChange: vi.fn(),
onImportNestedRepos: vi.fn(),
onOpenNestedRootFolder: vi.fn(),
onCreateNameChange: vi.fn(),
onCreateParentChange: vi.fn(),
onPickCreateParent: vi.fn(),
onCreate: vi.fn(),
...overrides
}
return renderToStaticMarkup(
<TooltipProvider>
<Dialog open>
<AddRepoDialogStepContent {...props} />
</Dialog>
</TooltipProvider>
)
}
function renderNestedStep(repoCount: number): string {
return renderStepContent({ repoCount })
}
describe('AddRepoDialogStepContent nested imports', () => {
it('asks the grouping question when no repos exist yet', () => {
const html = renderNestedStep(0)
expect(html).toContain('Group these repositories?')
expect(html).toContain('aria-label="Group name"')
expect(html).toContain('Yes, import as group')
expect(html).toContain('No, import separately')
expect(html).not.toContain('>Import</button>')
})
it('shows the same grouping import controls after a repo already exists', () => {
const html = renderNestedStep(1)
expect(html).toContain('Group these repositories?')
expect(html).toContain('aria-label="Group name"')
expect(html).toContain('Yes, import as group')
expect(html).toContain('No, import separately')
expect(html).not.toContain('>Import</button>')
})
it('offers opening the parent folder when nested import selection is empty', () => {
const html = renderStepContent({ nestedSelectedPaths: new Set() })
expect(html).toContain('No repositories are selected')
expect(html).toContain('Open as Folder')
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>No, import separately<\/button>/)
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>Yes, import as group<\/button>/)
})
it('offers host browsing for remote create project locations', () => {
const html = renderStepContent({
step: 'create',
isRuntimeEnvironmentActive: true,
activeRuntimeEnvironmentId: 'env-1'
})
expect(html).toContain('Create a new project')
expect(html).toContain('host folder not selected')
})
it('opens the host browser for a filled path that Windows did not hand over', () => {
const remote = {
step: 'server-path' as const,
isRuntimeEnvironmentActive: true,
activeRuntimeEnvironmentId: 'env-1',
serverPath: '/home/me/app'
}
const browser = 'Navigate to a directory and click Select to choose it.'
expect(renderStepContent(remote)).toContain(browser)
expect(renderStepContent({ ...remote, serverPathPrefilled: true })).not.toContain(browser)
})
it('offers host browsing for remote clone destinations', () => {
const html = renderStepContent({
step: 'clone',
isRuntimeEnvironmentActive: true,
activeRuntimeEnvironmentId: 'env-1'
})
expect(html).toContain('Clone from URL')
expect(html).toContain('aria-label="Browse host filesystem"')
})
it('falls back to the start step when no paired server reaches the SSH hosts', () => {
const html = renderStepContent({ step: 'remote', selectedTargetId: 'openclaw-2' })
expect(html).toContain('Browse folder')
expect(html).not.toContain('Host path')
})
it('uses the standard add step for remote Orca server hosts', () => {
const html = renderStepContent({
step: 'add',
isRuntimeEnvironmentActive: true,
activeRuntimeEnvironmentId: 'env-1',
browseHostKind: 'runtime'
})
expect(html).toContain('Browse folder')
expect(html).toContain('Existing Git repository or folder on this host')
expect(html).toContain('Clone from URL')
expect(html).toContain('Create new project')
expect(html).not.toContain('Browse host')
expect(html).not.toContain('Create on host')
expect(html).not.toContain('Want to import many repos at once?')
})
it('opens the in-app filesystem browser for a paired runtime', () => {
const html = renderStepContent({
step: 'server-path',
isRuntimeEnvironmentActive: true,
activeRuntimeEnvironmentId: 'paired-host'
})
expect(html).toContain('Browse host filesystem')
expect(html).toContain('Navigate to a directory and click Select to choose it.')
expect(html).toContain('Select folder')
})
})
@@ -1,8 +1,9 @@
// @vitest-environment happy-dom
import { act, useState, type ComponentProps } from 'react'
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { AddRepoNestedImportStep } from './AddRepoNestedImportStep'
import { TooltipProvider } from '@/components/ui/tooltip'
@@ -26,30 +27,6 @@ const scan: NestedRepoScanResult = {
timeoutMs: null
}
function renderStepMarkup(
overrides: Partial<ComponentProps<typeof AddRepoNestedImportStep>> = {}
): string {
return renderToStaticMarkup(
<TooltipProvider>
<Dialog open>
<AddRepoNestedImportStep
scan={scan}
groupName=""
selectedPaths={new Set(scan.repos.map((repo) => repo.path))}
isAdding={false}
scanInProgress={false}
onGroupNameChange={vi.fn()}
onSelectedPathsChange={vi.fn()}
onImport={vi.fn()}
onOpenAsFolder={vi.fn()}
onStopScan={vi.fn()}
{...overrides}
/>
</Dialog>
</TooltipProvider>
)
}
function findButton(container: HTMLElement, label: string): HTMLButtonElement {
const button = Array.from(container.querySelectorAll('button')).find((entry) =>
entry.textContent?.includes(label)
@@ -73,36 +50,6 @@ describe('AddRepoNestedImportStep', () => {
container = null
})
it('asks whether the selected folder should be grouped', () => {
const html = renderStepMarkup()
expect(html).toContain('Import repositories from folder')
expect(html).toContain('Found 3 repositories in')
expect(html).toContain('/workspace/platform')
expect(html).toContain('aria-label="Group name"')
expect(html).not.toContain('What is a')
expect(html).toContain('Group these repositories?')
expect(html).toContain('Choose this if these projects belong together')
expect(html).toContain('Orca will group them and let you work from the parent folder')
expect(html).toContain('No, import separately')
expect(html).toContain('Yes, import as group')
expect(html).toContain('payments/api')
expect(html).toContain('billing/api')
expect(html).not.toContain('disabled=""')
expect(html).not.toContain('>Back</button>')
expect(html).not.toContain('Project group')
})
it('disables both import actions while scanning', () => {
const html = renderStepMarkup({ scanInProgress: true })
expect(html).toContain('Group these repositories?')
expect(html).toContain('No, import separately')
expect(html).toContain('Yes, import as group')
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>No, import separately<\/button>/)
expect(html).toMatch(/<button[^>]*disabled=""[^>]*>Yes, import as group<\/button>/)
})
it('maps the group choice to grouped import and the separate choice to separate import', () => {
const onImport = vi.fn()
const host = document.createElement('div')
@@ -140,51 +87,6 @@ describe('AddRepoNestedImportStep', () => {
expect(onImport).toHaveBeenNthCalledWith(2, 'separate')
})
it('shows progress only on the clicked import action', () => {
const onImport = vi.fn()
const host = document.createElement('div')
container = host
document.body.appendChild(host)
root = createRoot(host)
function Harness(): React.JSX.Element {
const [isAdding, setIsAdding] = useState(false)
return (
<TooltipProvider>
<Dialog open>
<AddRepoNestedImportStep
scan={scan}
groupName=""
selectedPaths={new Set(scan.repos.map((repo) => repo.path))}
isAdding={isAdding}
scanInProgress={false}
onGroupNameChange={vi.fn()}
onSelectedPathsChange={vi.fn()}
onImport={(mode) => {
onImport(mode)
setIsAdding(true)
}}
onOpenAsFolder={vi.fn()}
onStopScan={vi.fn()}
/>
</Dialog>
</TooltipProvider>
)
}
act(() => {
root?.render(<Harness />)
})
act(() => {
findButton(host, 'Yes, import as group').click()
})
expect(onImport).toHaveBeenCalledWith('group')
expect(findButton(host, 'Yes, import as group').querySelector('.animate-spin')).not.toBeNull()
expect(findButton(host, 'No, import separately').querySelector('.animate-spin')).toBeNull()
})
it('offers opening the parent folder when no repositories are selected', () => {
const onOpenAsFolder = vi.fn()
const host = document.createElement('div')
@@ -1,6 +1,5 @@
// @vitest-environment happy-dom
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReactNode } from 'react'
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
@@ -83,17 +82,6 @@ function makeAllDoneProgress(
})
}
function makeOnlyBrowserIncompleteProgress(): FeatureWallSetupProgress {
return makeAllDoneProgress({
stepDone: {
...makeAllDoneProgress().stepDone,
browser: false
},
coreDoneCount: 7,
coreTotal: 8
})
}
const mountedRoots: Root[] = []
async function renderSetupGuideSidebarEntry(): Promise<{
@@ -132,41 +120,6 @@ describe('SetupGuideSidebarEntry', () => {
mocks.useSetupGuideProgress.mockReturnValue(makeProgress())
})
it('does not render before persisted UI hydration is ready', () => {
persistedUIReady = false
expect(renderToStaticMarkup(<SetupGuideSidebarEntry />)).not.toContain('Onboarding checklist')
})
it('does not render before setup progress readiness settles', () => {
mocks.useSetupGuideProgress.mockReturnValue(makeProgress({ ready: false }))
expect(renderToStaticMarkup(<SetupGuideSidebarEntry />)).not.toContain('Onboarding checklist')
})
it('does not render after setup is complete and progress is ready', () => {
mocks.useSetupGuideProgress.mockReturnValue(makeAllDoneProgress())
expect(renderToStaticMarkup(<SetupGuideSidebarEntry />)).not.toContain('Onboarding checklist')
})
it('renders for fresh active users when only the browser step is incomplete', () => {
mocks.useSetupGuideProgress.mockReturnValue(makeOnlyBrowserIncompleteProgress())
expect(renderToStaticMarkup(<SetupGuideSidebarEntry />)).toContain('Onboarding checklist')
})
it('does not render when the sidebar entry was dismissed with only browser incomplete', () => {
mocks.useSetupGuideProgress.mockReturnValue(makeOnlyBrowserIncompleteProgress())
setupGuideSidebarDismissed = true
expect(renderToStaticMarkup(<SetupGuideSidebarEntry />)).not.toContain('Onboarding checklist')
})
it('renders after persisted UI and setup progress are ready when setup is incomplete', () => {
expect(renderToStaticMarkup(<SetupGuideSidebarEntry />)).toContain('Onboarding checklist')
})
it('keeps the visible entry mounted during transient setup progress refreshes', async () => {
const { container, rerender } = await renderSetupGuideSidebarEntry()
@@ -3,8 +3,7 @@
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import WorkspaceKanbanSearchField, { overlayReserve } from './WorkspaceKanbanSearchField'
import WorkspaceKanbanSearchField from './WorkspaceKanbanSearchField'
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
let container: HTMLDivElement
@@ -97,15 +96,6 @@ describe('WorkspaceKanbanSearchField', () => {
expect(onClear).toHaveBeenCalledOnce()
})
it('hides the visual match count from assistive tech but keeps the clear button named', () => {
renderField({ query: 'orca', matchCount: 3, totalCount: 12 })
const count = container.querySelector('span[aria-hidden="true"]')
expect(count?.textContent).toBe('3 / 12')
expect(clearButton()?.getAttribute('aria-hidden')).toBeNull()
expect(clearButton()?.getAttribute('aria-label')).toBe('Clear search')
})
it('withholds counts for text that never narrows the board', () => {
renderField({ query: ' ', isFiltering: false, matchCount: 12, totalCount: 12 })
@@ -159,18 +149,6 @@ describe('WorkspaceKanbanSearchField', () => {
expect(onClose).toHaveBeenCalledOnce()
})
it('says so when a query was discarded for length instead of silently not filtering', () => {
renderField({ query: 'x'.repeat(3000), isFiltering: false, isTooLarge: true })
expect(container.textContent).toContain('Too long')
expect(input().getAttribute('aria-invalid')).toBe('true')
expect(liveRegion().textContent).toContain('too long')
renderField({ query: 'orca', matchCount: 3, totalCount: 12 })
expect(container.textContent).not.toContain('Too long')
expect(input().getAttribute('aria-invalid')).toBeNull()
})
it('leaves Escape to the IME while a composition is in progress', () => {
renderField({ query: '検索', matchCount: 1, totalCount: 12 })
@@ -188,26 +166,4 @@ describe('WorkspaceKanbanSearchField', () => {
expect(onClear).not.toHaveBeenCalled()
expect(onClose).not.toHaveBeenCalled()
})
it('keeps focus in the field after the clear button unmounts itself', () => {
renderField({ query: 'orca', matchCount: 3, totalCount: 12 })
act(() => {
input().focus()
clearButton()?.click()
})
expect(onClear).toHaveBeenCalledOnce()
expect(document.activeElement).toBe(input())
})
it('reserves overlay width in font-relative units, capped so text stays visible', () => {
// Multi-digit counters need font-relative width rather than a fixed reserve.
expect(overlayReserve('298 / 1024')).toContain('10ch')
expect(overlayReserve('3 / 9')).toContain('5ch')
// Cap reserve so wide counters cannot consume the typed-text area.
expect(overlayReserve('298 / 1024')).toContain('55%')
expect(overlayReserve(null)).toBe('32px')
})
})
@@ -1,207 +0,0 @@
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReactNode } from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { GlobalSettings } from '../../../../shared/global-settings-types'
import type { Repo } from '../../../../shared/repo-types'
import type { WorktreeCardProperty } from '../../../../shared/ui-chrome-types'
import type { Worktree } from '../../../../shared/worktree/types'
const fetchHostedReviewForBranch = vi.fn()
const fetchIssue = vi.fn()
const fetchLinearIssue = vi.fn()
const openModal = vi.fn()
const updateWorktreeMeta = vi.fn()
let worktreeCardProperties: WorktreeCardProperty[] = []
let settings: Partial<GlobalSettings> | null = null
const WORKTREE_CARD_IMPORT_TIMEOUT_MS = 15_000
vi.mock('@/store', () => {
const getState = () => ({
deleteStateByWorktreeId: {},
fetchHostedReviewForBranch,
fetchIssue,
fetchLinearIssue,
gitConflictOperationByWorktree: {},
hostedReviewCache: {},
issueCache: {},
linearIssueCache: {},
openModal,
projectGroups: [],
remoteBranchConflictByWorktreeId: {},
settings,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
updateWorktreeMeta,
workspacePortScan: null,
worktreeCardProperties
})
return {
useAppStore: Object.assign((selector: (state: unknown) => unknown) => selector(getState()), {
getState,
subscribe: () => () => {}
})
}
})
vi.mock('@/lib/worktree-activation', () => ({
activateAndRevealWorktree: vi.fn()
}))
vi.mock('@/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipContent: ({ children }: { children: ReactNode }) => <>{children}</>,
TooltipTrigger: ({ children }: { children: ReactNode }) => <>{children}</>
}))
vi.mock('./use-worktree-activity-status', () => ({
useWorktreeActivityStatus: () => 'idle'
}))
vi.mock('./use-worktree-sleep-state', () => ({
useIsSleepingWorktree: () => false
}))
vi.mock('./CacheTimer', () => ({
default: () => null,
usePromptCacheCountdownStartedAt: () => null
}))
vi.mock('./WorktreeCardAgents', () => ({
default: () => null
}))
vi.mock('./WorktreeContextMenu', () => ({
default: ({ children }: { children: ReactNode }) => <>{children}</>,
WORKTREE_NATIVE_CONTEXT_MENU_ATTR: 'data-worktree-native-context-menu',
WORKTREE_CONTEXT_MENU_SCOPE_ATTR: 'data-orca-context-menu-scope'
}))
function makeRepo(overrides: Partial<Repo> = {}): Repo {
return {
id: 'repo-1',
path: '/repo',
displayName: 'orca',
badgeColor: '#999999',
repoIcon: { type: 'emoji', emoji: '🦊' },
addedAt: 1,
...overrides
}
}
function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
return {
id: 'repo-1::/repo/worktrees/pinned',
repoId: 'repo-1',
path: '/repo/worktrees/pinned',
displayName: 'Pinned tree',
branch: 'feature/pinned',
head: 'abc123',
isBare: false,
isMainWorktree: false,
comment: '',
linkedIssue: null,
linkedPR: null,
linkedLinearIssue: null,
isArchived: false,
isUnread: false,
isPinned: true,
sortOrder: 0,
lastActivityAt: 1,
...overrides
}
}
describe('WorktreeCard pinned repo icon', () => {
beforeEach(() => {
vi.clearAllMocks()
worktreeCardProperties = []
settings = null
})
it(
'shows the repo icon and project name for pinned cards even when the repo badge is hidden',
async () => {
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree()}
repo={makeRepo()}
isActive={false}
inPinnedSection
hideRepoBadge
/>
)
expect(markup).toContain('🦊')
expect(markup).toContain('Project orca')
expect(markup).toContain('>orca</span>')
},
WORKTREE_CARD_IMPORT_TIMEOUT_MS
)
it(
'does not render the leading pinned repo icon for non-pinned cards',
async () => {
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({ isPinned: false })}
repo={makeRepo()}
isActive={false}
/>
)
expect(markup).not.toContain('🦊')
expect(markup).not.toContain('Project orca')
},
WORKTREE_CARD_IMPORT_TIMEOUT_MS
)
it(
'uses the pinned-style repo icon in new card style instead of a metadata-row badge',
async () => {
settings = { compactWorktreeCards: false, experimentalNewWorktreeCardStyle: true }
worktreeCardProperties = ['status']
const { default: WorktreeCard } = await import('./WorktreeCard')
const markup = renderToStaticMarkup(
<WorktreeCard
worktree={makeWorktree({ isPinned: false })}
repo={makeRepo()}
isActive={false}
/>
)
expect(markup).toContain('🦊')
expect(markup).toContain('Project orca')
expect(markup).toContain('>orca</span>')
expect(markup).not.toContain('data-worktree-card-meta-row=""')
},
WORKTREE_CARD_IMPORT_TIMEOUT_MS
)
it(
'shows the project name on ungrouped compact cards and hides it when grouped by project',
async () => {
settings = { compactWorktreeCards: true, experimentalNewWorktreeCardStyle: false }
const { default: WorktreeCard } = await import('./WorktreeCard')
const worktree = makeWorktree({ isPinned: false, displayName: 'main' })
const ungrouped = renderToStaticMarkup(
<WorktreeCard worktree={worktree} repo={makeRepo()} isActive={false} />
)
expect(ungrouped).toContain('>orca</span>')
expect(ungrouped).toContain('Project orca')
const groupedByProject = renderToStaticMarkup(
<WorktreeCard worktree={worktree} repo={makeRepo()} isActive={false} hideRepoBadge />
)
expect(groupedByProject).not.toContain('>orca</span>')
expect(groupedByProject).not.toContain('Project orca')
},
WORKTREE_CARD_IMPORT_TIMEOUT_MS
)
})
@@ -1,204 +0,0 @@
import { beforeAll, describe, expect, it, vi } from 'vitest'
import type { ProjectGroup } from '../../../../shared/project-group-types'
import type { Repo } from '../../../../shared/repo-types'
import {
createAppStoreModuleMock,
createDropdownMenuModuleMock,
createProjectHeaderDragModuleMock,
createReactVirtualModuleMock,
createTooltipModuleMock,
createVirtualizedScrollAnchorModuleMock,
createWorktreeCardAgentsModuleMock,
createWorktreeCardModuleMock,
createWorktreeContextMenuModuleMock,
createWorktreeTitleInlineRenameModuleMock,
loadWorktreeList,
mockStore,
renderWorktreeListMarkup
} from './worktree-list-lineage-card-test-harness'
import {
makeFolderWorkspacePathStatusMockState,
makeFolderWorkspacePathStatusState,
makeRepo
} from './worktree-list-lineage-card-test-fixtures'
import { setLineageFixtureState } from './worktree-list-lineage-store-state'
import { setPinnedFixtureState } from './worktree-list-pinned-store-state'
vi.mock('@/store', () => createAppStoreModuleMock())
vi.mock('@tanstack/react-virtual', () => createReactVirtualModuleMock())
vi.mock('@/hooks/useVirtualizedScrollAnchor', () => createVirtualizedScrollAnchorModuleMock())
vi.mock('./project-header-drag', () => createProjectHeaderDragModuleMock())
vi.mock('./WorktreeCard', () => createWorktreeCardModuleMock())
vi.mock('./WorktreeCardAgents', () => createWorktreeCardAgentsModuleMock())
vi.mock('./WorktreeTitleInlineRename', () => createWorktreeTitleInlineRenameModuleMock())
vi.mock('./WorktreeContextMenu', () => createWorktreeContextMenuModuleMock())
vi.mock('@/components/ui/tooltip', () => createTooltipModuleMock())
vi.mock('@/components/ui/dropdown-menu', () => createDropdownMenuModuleMock())
function setProjectGroupWithoutWorktreeRowsState(
filterRepoIds: string[] = [],
collapsedGroups = new Set<string>()
): void {
const group: ProjectGroup = {
id: 'group-1',
name: 'Imported Services',
parentPath: '/tmp/imported-services',
parentGroupId: null,
createdFrom: 'folder-scan',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 1,
updatedAt: 1
}
const repo: Repo = {
...makeRepo(),
projectGroupId: group.id
}
mockStore.state = {
...makeFolderWorkspacePathStatusMockState(),
startupWorktreeRefreshCompleted: true,
activeModal: '',
activeView: 'terminal',
activeWorktreeId: null,
agentStatusEpoch: 0,
agentStatusByPaneKey: {},
browserTabsByWorktree: {},
clearPendingRevealWorktreeId: vi.fn(),
collapsedGroups,
deleteStateByWorktreeId: {},
filterRepoIds,
...makeFolderWorkspacePathStatusState(),
groupBy: 'repo',
hideDefaultBranchWorkspace: false,
issueCache: {},
migrationUnsupportedByPtyId: {},
openModal: vi.fn(),
pendingRevealWorktree: null,
prCache: {},
prVisibleRefreshGeneration: 0,
projectGroups: [group],
ptyIdsByTabId: {},
reorderRepos: vi.fn(),
reportVisibleGitHubPRRefreshCandidates: vi.fn(),
retainedAgentsByPaneKey: {},
repos: [repo],
runtimePaneTitlesByTabId: {},
setFilterRepoIds: vi.fn(),
setHideDefaultBranchWorkspace: vi.fn(),
setRenamingWorktreeId: vi.fn(),
setShowSleepingWorkspaces: vi.fn(),
setSortBy: vi.fn(),
settings: null,
renamingWorktreeId: null,
showSleepingWorkspaces: true,
sortBy: 'recent',
sortEpoch: 0,
sshConnectedGeneration: 0,
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
tabsByWorktree: {},
terminalLayoutsByTabId: {},
toggleCollapsedGroup: vi.fn(),
updateWorktreeMeta: vi.fn(),
updateWorktreesMeta: vi.fn(),
workspaceHostScope: 'all',
workspaceStatuses: [],
worktreeCardProperties: ['status', 'inline-agents'],
worktreeLineageById: {},
worktreesByRepo: {
[repo.id]: []
}
}
}
describe('WorktreeList lineage child card renderer', () => {
beforeAll(async () => {
await loadWorktreeList()
}, 60_000)
it('renders project group headers when repos import before worktree rows load', async () => {
setProjectGroupWithoutWorktreeRowsState()
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('Imported Services')
expect(markup).not.toContain('No workspaces found')
})
it('renders a collapse chevron on project group headers with children', async () => {
setProjectGroupWithoutWorktreeRowsState()
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('data-repo-header-collapse-affordance=""')
expect(markup).toContain('aria-expanded="true"')
})
it('renders collapsed project group header affordance state', async () => {
setProjectGroupWithoutWorktreeRowsState([], new Set(['project-group:group-1']))
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('data-repo-header-collapse-affordance=""')
expect(markup).toContain('aria-expanded="false"')
expect(markup).toContain('-rotate-90')
})
it('does not render the project collapse affordance on flat section headers', async () => {
setLineageFixtureState('none')
const markup = await renderWorktreeListMarkup()
expect(markup).not.toContain('data-repo-header-collapse-affordance=""')
})
it('renders a collapse chevron on status group headers with worktrees', async () => {
setLineageFixtureState('workspace-status')
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('In progress')
expect(markup).toContain('data-workspace-status-drop-target=""')
expect(markup).toContain('data-repo-header-collapse-affordance=""')
expect(markup).toContain('aria-expanded="true"')
})
it('renders a collapse chevron on the pinned section header with worktrees', async () => {
setPinnedFixtureState()
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('Pinned')
expect(markup).toContain('data-workspace-pin-drop-target=""')
expect(markup).toContain('data-repo-header-collapse-affordance=""')
expect(markup).toContain('aria-expanded="true"')
})
it('renders collapsed pinned section header affordance state', async () => {
setPinnedFixtureState()
mockStore.state = {
...mockStore.state,
collapsedGroups: new Set(['pinned'])
}
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('data-workspace-pin-drop-target=""')
expect(markup).toContain('data-repo-header-collapse-affordance=""')
expect(markup).toContain('aria-expanded="false"')
expect(markup).toContain('-rotate-90')
})
it('renders a collapse chevron on grouped repo headers with worktrees', async () => {
setLineageFixtureState('repo')
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('data-repo-header-collapse-affordance=""')
expect(markup).toContain('data-repo-header-id="repo-1"')
expect(markup).toContain('aria-expanded="true"')
})
it('shows Clear Filters when filters exclude pre-worktree project groups', async () => {
setProjectGroupWithoutWorktreeRowsState(['another-repo'])
const markup = await renderWorktreeListMarkup()
expect(markup).toContain('No workspaces found')
expect(markup).toContain('Clear Filters')
expect(markup).not.toContain('Imported Services')
})
})
@@ -90,38 +90,6 @@ describe('showDeleteWorktreeFailureToast', () => {
expect(onForceDelete).toHaveBeenCalled()
})
it('keeps non-forceable failures destructive without a force action', () => {
const onViewChanges = vi.fn()
showDeleteWorktreeFailureToast({
error: 'permission denied',
canForceDelete: false,
forceDeleteReason: null,
onViewChanges,
onForceDelete: vi.fn(),
onDeleteAnyway: vi.fn(),
worktreeId: 'wt-2',
worktreeName: 'feature/bar'
})
expect(toast.error).toHaveBeenCalledWith(
'Failed to delete workspace feature/bar',
expect.objectContaining({
id: 'delete-worktree-failure:|wt-2',
dismissible: true,
duration: 10000
})
)
const body = renderToastBody('error')
expect(body.textContent).toContain('permission denied')
expect(body.textContent).not.toContain('Force Delete')
clickButton(body, 'View')
expect(toast.dismiss).toHaveBeenCalledWith('delete-worktree-failure:|wt-2')
expect(onViewChanges).toHaveBeenCalled()
})
it('offers Delete Anyway when the archive hook refused the removal', () => {
const onDeleteAnyway = vi.fn()
@@ -150,80 +118,4 @@ describe('showDeleteWorktreeFailureToast', () => {
expect(toast.dismiss).toHaveBeenCalledWith('delete-worktree-failure:|wt-archive')
expect(onDeleteAnyway).toHaveBeenCalled()
})
it('does not offer Delete Anyway for an ordinary failure', () => {
showDeleteWorktreeFailureToast({
error: 'permission denied',
canForceDelete: false,
forceDeleteReason: null,
onViewChanges: vi.fn(),
onForceDelete: vi.fn(),
onDeleteAnyway: vi.fn(),
worktreeId: 'wt-plain',
worktreeName: 'feature/plain'
})
expect(renderToastBody('error').textContent).not.toContain('Delete Anyway')
})
it('offers neither force delete nor View for a locked workspace', () => {
const onViewChanges = vi.fn()
showDeleteWorktreeFailureToast({
error: 'Worktree is locked by Git.',
canForceDelete: false,
forceDeleteReason: null,
onViewChanges,
onForceDelete: vi.fn(),
onDeleteAnyway: vi.fn(),
worktreeId: 'wt-locked',
worktreeName: 'feature/locked'
})
const body = renderToastBody('info')
expect(body.textContent).toContain('This workspace is locked by Git.')
expect(body.textContent).toContain(
'Run git worktree unlock <worktree-path> from its repository, then retry deletion'
)
expect(body.textContent).not.toContain('Force Delete')
expect(body.textContent).not.toContain('View')
expect(onViewChanges).not.toHaveBeenCalled()
})
it('keeps View for a locked workspace when changed files are known', () => {
showDeleteWorktreeFailureToast({
error: 'Worktree is locked by Git.',
canForceDelete: false,
forceDeleteReason: null,
lockReason: 'active agent session',
hasKnownChanges: true,
onViewChanges: vi.fn(),
onForceDelete: vi.fn(),
onDeleteAnyway: vi.fn(),
worktreeId: 'wt-locked-dirty',
worktreeName: 'feature/locked-dirty'
})
const body = renderToastBody('info')
expect(body.textContent).toContain('Git reported: active agent session')
expect(body.textContent).toContain('View')
})
it('offers a persistent nested deletion review for the exact failed target', () => {
Object.assign(window, { api: { worktrees: { previewNestedRemoval: vi.fn() } } })
showDeleteWorktreeFailureToast({
error:
'Refusing to delete worktree because it contains another registered worktree: /parent/child',
canForceDelete: false,
forceDeleteReason: null,
onViewChanges: vi.fn(),
onForceDelete: vi.fn(),
onDeleteAnyway: vi.fn(),
worktreeId: 'repo::/parent',
worktreeName: 'parent',
nestedRemovalTarget: { id: 'repo::/parent', executionHostId: 'local' }
})
const body = renderToastBody('error')
expect(body.textContent).toContain('Delete with nested worktrees…')
expect(vi.mocked(toast.error).mock.calls.at(-1)?.[1]?.duration).toBe(Infinity)
})
})
@@ -155,17 +155,6 @@ describe('showPreservedBranchBatchToast', () => {
})
})
it('keeps older-server branches visible without offering an unsafe delete', () => {
showPreservedBranchBatchToast(3, [{ worktreeId: 'repo-a::one', branchName: 'feature/one' }])
const body = renderToastBody()
expect(body.querySelector('button')).toBeNull()
expect(toast.warning).toHaveBeenCalledWith(
'3 workspaces removed, 1 branch kept',
expect.not.objectContaining({ duration: Infinity })
)
})
it('serializes branch deletion within one repository', async () => {
let resolveFirst: (result: { ok: true; deleted: true }) => void = () => {}
mocks.state.forceDeletePreservedBranch
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { makeDetectedResult } from '@/store/slices/worktrees-detected-listing-fixtures'
import { createVisibleHostedReviewRefreshScheduler } from '@/store/github/visible-hosted-review-refresh-scheduler'
import { getHostedReviewCacheKey } from '@/store/slices/hosted-review-cache-identity'
import { getWorkspaceReferenceRefreshTargets } from './workspace-reference-refresh-targets'
@@ -173,13 +173,3 @@ it('retains both explicit owners when equal visible workspace IDs exist on two h
getWorkspaceReferenceRefreshTargets(useAppStore.getState(), { selectedOnly: true })
).toHaveLength(1)
})
it('admits attached reviews on visible external detected worktrees', () => {
useAppStore.setState({
worktreesByRepo: {},
detectedWorktreesByRepo: {
repo: makeDetectedResult('repo', [workspace])
}
})
expect(update()).toHaveLength(1)
})
@@ -1,7 +1,7 @@
// @vitest-environment happy-dom
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
import { renderToStaticMarkup } from 'react-dom/server'
import type { ReactNode } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { RuntimeHostStatusRow } from './RuntimeHostStatusRow'
@@ -38,104 +38,6 @@ vi.mock('@/components/ui/dropdown-menu', () => ({
afterEach(cleanup)
describe('RuntimeHostStatusRow', () => {
it('renders reconnecting diagnostics for remote hosts', () => {
const markup = renderToStaticMarkup(
<RuntimeHostStatusRow label="Dev Box" state="reconnecting" detail="Attempt 3" />
)
expect(markup).toContain('Dev Box')
expect(markup).toContain('Reconnecting')
expect(markup).toContain('Attempt 3')
})
it('renders disconnected hosts with a connect action', () => {
const markup = renderToStaticMarkup(
<RuntimeHostStatusRow
label="Dev Box"
state="disconnected"
detail="Last closed: 1006"
onConnect={async () => {}}
/>
)
expect(markup).toContain('Dev Box')
expect(markup).toContain('Remote Server')
expect(markup).toContain('Disconnected')
expect(markup).toContain('Last closed: 1006')
expect(markup).toContain('Connect')
})
it('names the closed workspace window while keeping the disconnect action', () => {
const markup = renderToStaticMarkup(
<RuntimeHostStatusRow
label="Dev Box"
state="workspace-window-closed"
onConnect={async () => {}}
onDisconnect={async () => {}}
/>
)
expect(markup).toContain('Dev Box')
expect(markup).toContain('Workspace window closed')
expect(markup).toContain('Disconnect')
expect(markup).not.toContain('>Connect</button>')
// The host is still reachable — the row must not read as a lost connection.
expect(markup).not.toContain('Disconnected')
// Require the disconnect handler; a connect-only row must offer no action for an already reachable host.
expect(
renderToStaticMarkup(
<RuntimeHostStatusRow
label="Dev Box"
state="workspace-window-closed"
onConnect={async () => {}}
/>
)
).not.toContain('<button')
})
it('renders connected hosts with a disconnect action', () => {
const markup = renderToStaticMarkup(
<RuntimeHostStatusRow label="Dev Box" state="connected" onDisconnect={async () => {}} />
)
expect(markup).toContain('Dev Box')
expect(markup).toContain('Connected')
expect(markup).toContain('Disconnect')
})
it('renders transport-up runtime-unavailable hosts without implying process death', () => {
const { container } = render(
<RuntimeHostStatusRow
label="OpenClaw"
state="runtime-unavailable"
detail="status.get refused"
onDisconnect={async () => {}}
/>
)
expect(container.textContent).toContain('Orca unavailable')
expect(container.textContent).toContain('SSH transport is connected')
expect(container.textContent).toContain('may still be running')
expect(container.textContent).toContain('Disconnect')
expect(container.textContent).not.toContain('exited')
})
it('keeps healthy rows free of a submenu trigger', () => {
const { container } = render(<RuntimeHostStatusRow label="Dev Box" state="connected" />)
expect(container.querySelector('[data-slot="dropdown-menu-sub-trigger"]')).toBeNull()
})
it('renders failing rows as submenu triggers', () => {
const { container } = render(
<RuntimeHostStatusRow label="Dev Box" state="disconnected" detail="Connection refused" />
)
const trigger = container.querySelector('[data-slot="dropdown-menu-sub-trigger"]')
expect(trigger).not.toBeNull()
expect(trigger?.querySelector('button')).toBeNull()
})
it('keeps the row action invokable for failing hosts', async () => {
const onConnect = vi.fn(async () => {})
render(
@@ -151,51 +53,4 @@ describe('RuntimeHostStatusRow', () => {
await waitFor(() => expect(onConnect).toHaveBeenCalledOnce())
})
it('renders a long raw error in full inside the submenu', () => {
const longError = `Invalid remote endpoint: ${'wss://relay.example.test/path/'.repeat(8)}`
const { container } = render(
<RuntimeHostStatusRow
label="Dev Box"
state="disconnected"
detail={longError}
diagnostics={{
state: 'closed',
pendingRequestCount: 0,
subscriptionCount: 0,
reconnectAttempt: 3,
lastConnectedAt: null,
lastClose: null,
lastError: longError
}}
/>
)
const submenu = container.querySelector('[data-slot="dropdown-menu-sub-content"]')
expect(submenu).not.toBeNull()
expect(submenu?.textContent).toContain(longError)
})
it('shows the existing connection diagnostics in the submenu', () => {
const { container } = render(
<RuntimeHostStatusRow
label="Dev Box"
state="reconnecting"
detail="Connection refused"
diagnostics={{
state: 'reconnecting',
pendingRequestCount: 0,
subscriptionCount: 0,
reconnectAttempt: 2,
lastConnectedAt: Date.now() - 60_000,
lastClose: null,
lastError: 'Connection refused'
}}
/>
)
const submenu = container.querySelector('[data-slot="dropdown-menu-sub-content"]')
expect(submenu?.textContent).toContain('Last connected')
expect(submenu?.textContent).toContain('Attempt 3')
})
})
@@ -30,46 +30,7 @@ function windowOf(
return { usedPercent, windowMinutes, resetsAt, resetDescription: null }
}
// Grok unified-billing accounts surface a monthly window and nothing else.
function grokMonthlyLimits(status: ProviderRateLimits['status']): ProviderRateLimits {
return {
provider: 'grok',
session: null,
weekly: null,
monthly: windowOf(25, 43200),
updatedAt: Date.now(),
error: null,
status
}
}
describe('ProviderSegment monthly window', () => {
it('renders a monthly-only snapshot in the chip instead of a bare icon', async () => {
const { ProviderSegment } = await import('./StatusBar')
const markup = renderToStaticMarkup(
<ProviderSegment p={grokMonthlyLimits('ok')} compact={false} display="used" mode="compact" />
)
expect(markup).toContain('>25% 30d<')
})
it('shows monthly data while fetching instead of the loading placeholder', async () => {
const { ProviderSegment } = await import('./StatusBar')
const markup = renderToStaticMarkup(
<ProviderSegment
p={grokMonthlyLimits('fetching')}
compact={false}
display="used"
mode="compact"
/>
)
expect(markup).toContain('>25% 30d<')
expect(markup).not.toContain('···')
})
it('shows only the highest-used window when several windows exist', async () => {
const { ProviderSegment } = await import('./StatusBar')
@@ -78,7 +39,7 @@ describe('ProviderSegment monthly window', () => {
session: windowOf(10, 300),
weekly: windowOf(20, 10080),
monthly: windowOf(30, 43200),
updatedAt: Date.now(),
updatedAt: 0,
error: null,
status: 'ok'
}
@@ -101,7 +62,7 @@ describe('ProviderSegment monthly window', () => {
{ ...windowOf(25, 300), name: 'Flash' },
{ ...windowOf(80, 300), name: 'Pro' }
],
updatedAt: Date.now(),
updatedAt: 0,
error: null,
status: 'ok'
}
@@ -113,74 +74,6 @@ describe('ProviderSegment monthly window', () => {
expect(markup).toContain('>80% Pro<')
expect(markup).not.toContain('>25%')
})
// Keep the chip’s countdown consistent with the popup for the same resetsAt.
it('shows remaining session time on the chip when resetsAt is known (repro-8378)', async () => {
const { ProviderSegment } = await import('./StatusBar')
const now = 1_700_000_000_000
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(now)
const remainingMs = 2 * 60 * 60_000 + 33 * 60_000
try {
const limits: ProviderRateLimits = {
provider: 'codex',
session: windowOf(42, 300, now + remainingMs),
weekly: windowOf(10, 10080, now + 6 * 24 * 60 * 60_000),
updatedAt: now,
error: null,
status: 'ok'
}
const markup = renderToStaticMarkup(
<ProviderSegment p={limits} compact={false} display="used" mode="compact" />
)
expect(markup).toContain('>42% 2h 33m<')
expect(markup).not.toContain('5h')
// The consolidated footer intentionally renders only the tightest window.
expect(markup).not.toContain('>10%')
expect(markup).not.toContain('wk')
} finally {
dateNow.mockRestore()
}
})
it('shows the footer bar only in verbose mode', async () => {
const { ProviderSegment } = await import('./StatusBar')
const limits = grokMonthlyLimits('ok')
const verbose = renderToStaticMarkup(
<ProviderSegment p={limits} compact={false} display="used" mode="verbose" />
)
const compact = renderToStaticMarkup(
<ProviderSegment p={limits} compact={false} display="used" mode="compact" />
)
expect(verbose).toContain('data-usage-bar')
expect(compact).not.toContain('data-usage-bar')
})
it('restores every inline window in verbose mode', async () => {
const { ProviderSegment } = await import('./StatusBar')
const limits: ProviderRateLimits = {
provider: 'claude',
session: windowOf(10, 300),
weekly: windowOf(20, 10_080),
fableWeekly: windowOf(30, 10_080),
monthly: windowOf(40, 43_200),
updatedAt: Date.now(),
error: null,
status: 'ok'
}
const markup = renderToStaticMarkup(
<ProviderSegment p={limits} compact={false} display="used" mode="verbose" />
)
expect(markup).toContain('>10% 5h<')
expect(markup).toContain('>20% wk<')
expect(markup).toContain('>30% Fable<')
expect(markup).not.toContain('>40%')
})
})
describe('undefined provider window safety (crash d2c1da69 / bb74236c)', () => {
@@ -189,10 +82,11 @@ describe('undefined provider window safety (crash d2c1da69 / bb74236c)', () => {
// old `s.window !== null` filter let the undefined-window section through, so
// getTightestUsageSection's reduce read `.usedPercent` of undefined and crashed
// the status-bar overlay (TypeError in ProviderSegment).
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Deliberately omit session to reproduce malformed persisted provider data.
const partialProvider = {
provider: 'codex',
weekly: windowOf(42, 10080),
updatedAt: Date.now(),
updatedAt: 0,
error: null,
status: 'ok'
} as unknown as ProviderRateLimits // `session` omitted -> undefined at runtime
@@ -80,31 +80,6 @@ describe('resource session bindings', () => {
expect(index.boundPtyIds.has('pty-closed')).toBe(false)
})
it('counts only daemon sessions without live or restorable tab bindings', () => {
const count = countUnboundDaemonSessions(
[
{ id: 'pty-live', cwd: '/workspace', title: 'live', agentOwnership: 'absent' as const },
{
id: 'pty-restored',
cwd: '/workspace',
title: 'restored',
agentOwnership: 'absent' as const
},
{ id: 'pty-orphan', cwd: '/tmp', title: 'orphan', agentOwnership: 'absent' as const }
],
{
ptyIdsByTabId: { 'tab-live': ['pty-live'] },
tabsByWorktree: {
'repo::/workspace': [makeTab('tab-live'), makeTab('tab-restored', 'pty-restored')]
},
terminalLayoutsByTabId: {},
workspaceSessionReady: true
}
)
expect(count).toBe(1)
})
it('binds a session deferred for SSH reattach so it is never offered for cleanup', () => {
const inputs = {
ptyIdsByTabId: {},
@@ -158,32 +133,6 @@ describe('resource session bindings', () => {
expect(countUnboundDaemonSessions(sessions, inputs)).toBe(1)
})
it('keeps the advertised count and the selected set identical', () => {
const inputs = {
ptyIdsByTabId: { 'tab-live': ['pty-live'] },
tabsByWorktree: { 'repo::/workspace': [makeTab('tab-live'), makeTab('tab-ssh')] },
terminalLayoutsByTabId: {},
deferredSshSessionIdsByTabId: { 'tab-ssh': 'pty-deferred' },
workspaceSessionReady: true
}
const sessions = [
{ id: 'pty-live', cwd: '/workspace', title: 'live', agentOwnership: 'absent' as const },
{
id: 'pty-deferred',
cwd: '/workspace',
title: 'deferred',
agentOwnership: 'absent' as const
},
{ id: 'pty-agent', cwd: '/workspace', title: 'codex', agentOwnership: 'present' as const },
{ id: 'pty-orphan', cwd: '/tmp', title: 'orphan', agentOwnership: 'absent' as const }
]
expect(selectUnboundDaemonSessions(sessions, inputs).map((s) => s.id)).toEqual(['pty-orphan'])
expect(countUnboundDaemonSessions(sessions, inputs)).toBe(
selectUnboundDaemonSessions(sessions, inputs).length
)
})
it('never offers a session whose ownership could not be established', () => {
// Older daemons/relays may omit ownership claims; unknown cannot establish owner absence.
const inputs = {
@@ -75,29 +75,6 @@ describe('useOwnerUsageRateLimits', () => {
})
afterEach(() => cleanup())
it('opens no watcher and keeps local usage when this device owns accounts', () => {
const { result } = renderHook(() => useOwnerUsageRateLimits(local))
expect(mocks.watchers).toHaveLength(0)
expect(result.current.rateLimits).toBe(local)
expect(result.current.refreshRemote).toBeNull()
expect(result.current.remoteOwner).toBeNull()
})
it("reads usage from the remote owner's accounts stream", () => {
mocks.owner = { kind: 'environment', environmentId: 'mini' }
const { result } = renderHook(() => useOwnerUsageRateLimits(local))
expect(mocks.watchers[0].target).toEqual({ kind: 'environment', environmentId: 'mini' })
expect(result.current.remoteOwner).toEqual({ environmentId: 'mini', label: 'Mac Mini' })
expect(result.current.rateLimits.claude?.session).toBeNull()
act(() =>
mocks.watchers[0].handlers.onSnapshot(
snapshot(createEmptyRateLimitState({ claude: provider('claude', 64) }))
)
)
expect(result.current.rateLimits.claude?.session?.usedPercent).toBe(64)
})
it('reports an older host without published usage as unavailable, not loading', () => {
mocks.owner = { kind: 'environment', environmentId: 'mini' }
const { result } = renderHook(() => useOwnerUsageRateLimits(local))
@@ -107,19 +84,6 @@ describe('useOwnerUsageRateLimits', () => {
expect(result.current.rateLimits.claude?.error).toContain('Mac Mini does not report usage')
})
it('turns loss of contact into an unverifiable verdict that keeps known bars', () => {
mocks.owner = { kind: 'environment', environmentId: 'mini' }
const { result } = renderHook(() => useOwnerUsageRateLimits(local))
act(() =>
mocks.watchers[0].handlers.onSnapshot(
snapshot(createEmptyRateLimitState({ codex: provider('codex', 30) }))
)
)
act(() => mocks.watchers[0].handlers.onError(new Error('closed')))
expect(result.current.rateLimits.codex?.session).toBeNull()
expect(result.current.rateLimits.codex?.error).toContain('cannot reach Mac Mini')
})
it('never shows server A numbers under server B', () => {
mocks.owner = { kind: 'environment', environmentId: 'mini' }
const { result, rerender } = renderHook(() => useOwnerUsageRateLimits(local))
@@ -92,17 +92,6 @@ describe('workspace cleanup facet controls', () => {
container = null
})
it('shows a per-facet match count for every group', () => {
renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock())
const counts = [...(container?.querySelectorAll('[data-facet-count]') ?? [])].map(
(node) => node.textContent
)
expect(counts).toHaveLength(10)
expect(counts).toContain('2/8')
expect(counts).toContain('3/8')
})
it('takes a user-chosen idle threshold in days rather than a fixed enum', () => {
const onPatch = createPatchMock()
renderFacets(createDefaultWorkspaceCleanupFilterState(), onPatch)
@@ -112,27 +101,6 @@ describe('workspace cleanup facet controls', () => {
expect(onPatch).toHaveBeenCalledWith('activity', { idleMinDays: 17 })
})
it('keeps every numeric threshold off type=number so a wheel tick cannot set it', () => {
// Assert input type because happy-dom cannot reproduce Chromium’s wheel-driven numeric changes.
renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock())
const numericFacets = [
'Idle for at least',
'At least',
'At most',
'Commits ahead ≥',
'Commits behind ≥'
]
const found = numericFacets
.map((label) => control(label) as HTMLInputElement | null)
.filter((input): input is HTMLInputElement => input !== null)
expect(found).toHaveLength(numericFacets.length)
for (const input of found) {
expect(input.type).toBe('text')
expect(input.inputMode).toBe('numeric')
}
})
it('multi-selects git states', () => {
const onPatch = createPatchMock()
const filters = createDefaultWorkspaceCleanupFilterState()
@@ -144,14 +112,6 @@ describe('workspace cleanup facet controls', () => {
expect(onPatch).toHaveBeenCalledWith('git', { states: ['dirty', 'unpushed'] })
})
it('offers the hosts and providers discovered in the fleet, not a hardcoded list', () => {
renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock())
expect(control('GitLab')).not.toBeNull()
expect(container?.textContent).toContain('Alpha')
expect(container?.textContent).toContain('In progress')
})
it('cycles a tri-state safety facet', () => {
const onPatch = createPatchMock()
renderFacets(createDefaultWorkspaceCleanupFilterState(), onPatch)
@@ -160,15 +120,6 @@ describe('workspace cleanup facet controls', () => {
expect(onPatch).toHaveBeenCalledWith('safety', { dismissed: 'only' })
})
it('labels blocker matching without exposing model tokens', () => {
renderFacets(createDefaultWorkspaceCleanupFilterState(), createPatchMock())
expect(control('Blocker match: Has any')).not.toBeNull()
expect(control('Blocker match: Has none')).not.toBeNull()
expect(container?.textContent).not.toContain('any-of')
expect(container?.textContent).not.toContain('none-of')
})
it('toggles the unsized escape hatch for the opt-in space scan', () => {
const onPatch = createPatchMock()
renderFacets(createDefaultWorkspaceCleanupFilterState(), onPatch)
@@ -1,203 +0,0 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import WorktreeCreationPanel from './WorktreeCreationPanel'
import type { PendingWorktreeCreation } from '@/lib/pending-worktree-creation'
const mocks = vi.hoisted(() => ({
state: {
pendingWorktreeCreations: {
'create-1': {
creationId: 'create-1',
phase: 'creating',
status: 'creating',
startedAt: Date.now(),
indeterminate: false,
loaderVisible: true,
request: {
repoId: 'repo-1',
name: 'new-workspace',
displayName: 'New workspace',
setupDecision: 'skip',
agent: null,
pendingFirstAgentMessageRename: false,
note: '',
startupPlan: null,
quickPrompt: '',
quickTelemetry: null
}
}
} as Record<string, PendingWorktreeCreation>
}
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: typeof mocks.state) => unknown) => selector(mocks.state)
}))
vi.mock('@/lib/worktree-creation-flow', () => ({
retryBackgroundWorktreeCreation: vi.fn()
}))
const roots: Root[] = []
async function renderPanel(reserveCollapsedSidebarHeaderSpace: boolean): Promise<HTMLDivElement> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
roots.push(root)
await act(async () => {
root.render(
<WorktreeCreationPanel
creationId="create-1"
reserveCollapsedSidebarHeaderSpace={reserveCollapsedSidebarHeaderSpace}
/>
)
})
return container
}
describe('WorktreeCreationPanel', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
mocks.state.pendingWorktreeCreations['create-1'] = {
creationId: 'create-1',
phase: 'creating',
status: 'creating',
startedAt: Date.now(),
indeterminate: false,
loaderVisible: true,
request: {
repoId: 'repo-1',
name: 'new-workspace',
displayName: 'New workspace',
setupDecision: 'skip',
agent: null,
pendingFirstAgentMessageRename: false,
note: '',
startupPlan: null,
quickPrompt: '',
quickTelemetry: null
}
}
})
afterEach(() => {
roots.splice(0).forEach((root) => {
act(() => root.unmount())
})
document.body.replaceChildren()
})
it('keeps the faux creation tab visible', async () => {
const container = await renderPanel(false)
expect(container.textContent).toContain('New workspace')
expect(container.textContent).toContain('Creating worktree…')
const title = [...container.querySelectorAll('span')].find(
(node) => node.textContent === 'New workspace'
)
expect(title?.closest('div')?.className).toContain('border-r')
})
it('reserves collapsed left-titlebar space before the faux tab', async () => {
const container = await renderPanel(true)
const title = [...container.querySelectorAll('span')].find(
(node) => node.textContent === 'New workspace'
)
const spacer = title?.closest('div')?.previousElementSibling as HTMLElement | null
expect(spacer?.style.width).toBe('var(--collapsed-sidebar-header-width)')
})
it('does not reserve left-titlebar space when the header is not floating', async () => {
const container = await renderPanel(false)
const title = [...container.querySelectorAll('span')].find(
(node) => node.textContent === 'New workspace'
)
expect(title?.closest('div')?.previousElementSibling).toBeNull()
})
it('shows provisioning logs while a VM recipe is running', async () => {
mocks.state.pendingWorktreeCreations['create-1'] = {
...mocks.state.pendingWorktreeCreations['create-1'],
phase: 'provisioning-vm',
provisioningLog: 'creating sandbox\nstarting orca serve\n'
}
const container = await renderPanel(false)
expect(container.textContent).toContain('Provisioning VM')
expect(container.querySelector('pre')?.textContent).toBe(
'creating sandbox\nstarting orca serve\n'
)
expect(container.querySelector('pre')?.className).toContain('h-72')
const cancel = [...container.querySelectorAll('button')].find(
(button) => button.textContent === 'Cancel'
)
expect(cancel).toBeTruthy()
})
it('surfaces the captured recipe output when a VM recipe fails', async () => {
mocks.state.pendingWorktreeCreations['create-1'] = {
...mocks.state.pendingWorktreeCreations['create-1'],
phase: 'provisioning-vm',
status: 'error',
error: 'Recipe exited with code 1.',
provisioningLog: 'pulling image…\nERROR: no space left on device\n'
}
const container = await renderPanel(false)
expect(container.textContent).toContain('Couldn’t create worktree')
expect(container.textContent).toContain('Recipe exited with code 1.')
const log = container.querySelector('pre')
expect(log?.textContent).toBe('pulling image…\nERROR: no space left on device\n')
expect(log?.className).toContain('h-72')
expect(container.textContent).toContain('Retry')
})
it("shows git's checkout meter as a labelled bar while the checkout runs", async () => {
mocks.state.pendingWorktreeCreations['create-1'] = {
...mocks.state.pendingWorktreeCreations['create-1'],
checkoutProgress: { percent: 42, completed: 420, total: 1000 }
}
const container = await renderPanel(false)
const bar = container.querySelector('[role="progressbar"]')
expect(bar?.getAttribute('aria-valuenow')).toBe('42')
const label = document.getElementById(bar?.getAttribute('aria-labelledby') ?? '')
expect(label?.textContent).toBe('Checking out files… 42%')
})
it('shows no bar when git printed no checkout meter, or after the create failed', async () => {
expect((await renderPanel(false)).querySelector('[role="progressbar"]')).toBeNull()
mocks.state.pendingWorktreeCreations['create-1'] = {
...mocks.state.pendingWorktreeCreations['create-1'],
status: 'error',
error: 'git worktree add failed',
checkoutProgress: { percent: 42, completed: 420, total: 1000 }
}
expect((await renderPanel(false)).querySelector('[role="progressbar"]')).toBeNull()
})
it('omits the recipe output panel for a non-VM creation failure', async () => {
mocks.state.pendingWorktreeCreations['create-1'] = {
...mocks.state.pendingWorktreeCreations['create-1'],
status: 'error',
error: 'git worktree add failed'
}
const container = await renderPanel(false)
expect(container.textContent).toContain('git worktree add failed')
expect(container.querySelector('pre')).toBeNull()
})
})
@@ -83,123 +83,4 @@ describe('local composer drop failures', () => {
expect(attached).toHaveLength(9)
expect(attached).not.toContain('/drop/bad-1.png')
})
it('stays silent when every dropped path attaches', async () => {
const { result } = renderDropState(() => {})
await act(async () => {
await result.current.applyLocalComposerDrop(['/drop/ok-0.png', '/drop/ok-1.png'])
})
expect(mocks.toastError).not.toHaveBeenCalled()
})
it('says nothing once the composer that owned the drop is gone', async () => {
const { result } = renderDropState(() => {})
await act(async () => {
await result.current.applyLocalComposerDrop(dropPaths(12), () => false)
})
expect(mocks.toastError).not.toHaveBeenCalled()
})
})
// Why: the upload branch returns early unless a runtime environment or connection is resolved.
const RUNTIME_ENVIRONMENT_ID = 'env-1'
describe('composer upload failures', () => {
it('aggregates a mixed runtime import into one toast, and withholds a reason that is not shared', async () => {
mocks.importExternalPaths.mockResolvedValue({
results: [
{
sourcePath: '/a.png',
status: 'imported',
destPath: '/repo/.orca/drops/a.png',
kind: 'file',
renamed: false
},
{ sourcePath: '/b.png', status: 'skipped', reason: 'permission-denied' },
{ sourcePath: '/c.png', status: 'failed', reason: 'disk full' }
]
})
const { result } = renderDropState(() => {})
await act(async () => {
await result.current.uploadComposerPaths(
['/a.png', '/b.png', '/c.png'],
RUNTIME_ENVIRONMENT_ID,
null,
'/repo'
)
})
expect(mocks.toastError).toHaveBeenCalledTimes(1)
const [title, options] = mocks.toastError.mock.calls[0] ?? []
expect(title).toBe('2 of 3 items could not be attached.')
expect(options.description).toBeUndefined()
})
it('stays silent when every uploaded path imports', async () => {
mocks.importExternalPaths.mockResolvedValue({
results: [
{
sourcePath: '/a.png',
status: 'imported',
destPath: '/repo/.orca/drops/a.png',
kind: 'file',
renamed: false
}
]
})
const { result } = renderDropState(() => {})
await act(async () => {
await result.current.uploadComposerPaths(['/a.png'], RUNTIME_ENVIRONMENT_ID, null, '/repo')
})
expect(mocks.toastError).not.toHaveBeenCalled()
})
it('does not report after the composer that owned the upload is gone', async () => {
mocks.importExternalPaths.mockResolvedValue({
results: [{ sourcePath: '/b.png', status: 'skipped', reason: 'missing' }]
})
const { result } = renderDropState(() => {})
await act(async () => {
await result.current.uploadComposerPaths(
['/b.png'],
RUNTIME_ENVIRONMENT_ID,
null,
'/repo',
null,
() => false
)
})
expect(mocks.toastError).not.toHaveBeenCalled()
})
it('does give the shared reason when every uploaded path failed the same way', async () => {
mocks.importExternalPaths.mockResolvedValue({
results: [
{ sourcePath: '/b.png', status: 'skipped', reason: 'permission-denied' },
{ sourcePath: '/c.png', status: 'skipped', reason: 'permission-denied' }
]
})
const { result } = renderDropState(() => {})
await act(async () => {
await result.current.uploadComposerPaths(
['/b.png', '/c.png'],
RUNTIME_ENVIRONMENT_ID,
null,
'/repo'
)
})
const [, options] = mocks.toastError.mock.calls[0] ?? []
expect(options.description).toBe('Permission denied.')
})
})
@@ -159,26 +159,3 @@ describe('useReadOnScreenAttention — floating workspace panel visibility', ()
expect(selectFloatingWorkspaceHasUnread(state)).toBe(false)
})
})
it('does not acknowledge the regular workspace for a colliding floating tab', () => {
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
seedFloatingCompletion()
useAppStore.setState({
activeView: 'terminal',
activeTabId: FLOATING_TAB_ID,
activeWorktreeId: 'regular',
unreadAgentCompletionPanes: { [FLOATING_PANE_KEY]: true }
})
openFloatingPanel()
const read = vi.spyOn(useAppStore.getState(), 'readOnScreenAttention')
try {
renderHook(() => useReadOnScreenAttention())
expect(read).toHaveBeenCalled()
expect(
read.mock.calls.flatMap(([screen]) => screen.targets.map((target) => target.worktreeId))
).not.toContain('regular')
} finally {
cleanup()
vi.restoreAllMocks()
}
})
@@ -43,22 +43,6 @@ describe('app menu paste', () => {
expect(performNativePaste).not.toHaveBeenCalled()
})
it('falls back to native paste when no Orca-owned target is focused', async () => {
const button = document.createElement('button')
document.body.appendChild(button)
button.focus()
const performNativePaste = vi.fn()
const result = await handleAppMenuPasteRequest({
readClipboardText: vi.fn(async () => 'text'),
performNativePaste,
dispatchOwnedPasteEvent: () => false
})
expect(result).toEqual({ status: 'native-fallback', reason: 'no-owned-target' })
expect(performNativePaste).toHaveBeenCalledWith({ mode: 'paste' })
})
it('preserves paste-and-match-style when falling back to native paste', async () => {
const performNativePaste = vi.fn()
@@ -144,21 +128,6 @@ describe('app menu paste', () => {
expect(performNativePaste).toHaveBeenCalledWith({ mode: 'paste' })
})
it('native-pastes an image-only clipboard so the control can take the image', async () => {
const textarea = appendTextarea()
textarea.focus()
const performNativePaste = vi.fn()
const result = await handleAppMenuPasteRequest({
readClipboardText: vi.fn(async () => ''),
performNativePaste,
dispatchOwnedPasteEvent: () => false
})
expect(result).toEqual({ status: 'native-fallback', reason: 'no-clipboard-text' })
expect(performNativePaste).toHaveBeenCalledWith({ mode: 'paste' })
})
it('does not native-fallback when clipboard read fails after focus moves', async () => {
const textarea = appendTextarea('unchanged')
const other = appendTextarea('other')
@@ -186,33 +155,4 @@ describe('app menu paste', () => {
expect(other.value).toBe('other')
expect(performNativePaste).not.toHaveBeenCalled()
})
it('rejects oversized focused text-control paste without native fallback', async () => {
const textarea = appendTextarea('unchanged')
textarea.focus()
const performNativePaste = vi.fn()
const result = await handleAppMenuPasteRequest({
readClipboardText: vi.fn(async () => {
throw new Error('Clipboard text is too large for this paste target.')
}),
performNativePaste,
dispatchOwnedPasteEvent: () => false
})
expect(result).toMatchObject({
status: 'rejected',
target: 'text-control',
reason: 'too-large'
})
expect(result.status === 'rejected' ? result.redactedDiagnostic : '').toContain(
'reason=too-large'
)
expect(result.status === 'rejected' ? result.redactedDiagnostic : '').toContain('durationMs=')
expect(result.status === 'rejected' ? result.redactedDiagnostic : '').toContain(
'content=redacted'
)
expect(textarea.value).toBe('unchanged')
expect(performNativePaste).not.toHaveBeenCalled()
})
})
@@ -4,10 +4,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
import type { Tab } from '../../../shared/tab-types'
import { applyLocalStructuredSessionTabSnapshots } from './local-structured-session-tabs-sync/snapshot-apply'
import {
resetLocalStructuredSessionVersionForTests,
localStructuredSessionEpochHistoryByWorktree
} from './local-structured-session-tabs-sync/inventory-generation-fence'
import { resetLocalStructuredSessionVersionForTests } from './local-structured-session-tabs-sync/inventory-generation-fence'
import type { WebSessionTabsSyncState } from './web-session-tabs-sync/state'
import { resetWebSessionFocusIntentForTests } from './web-session-focus-intent'
@@ -124,14 +122,6 @@ function replayUntilDrop(
}
describe('retired-epoch repair for a returning publisher', () => {
it('POSITIVE CONTROL: the same frame lands when no epoch has been retired', () => {
const applied = applyLocalStructuredSessionTabSnapshots(stateWithCoordinatorTerminal(), [
frame(RENDERER_EPOCH, 7, 'claude-1')
])
expect(chatTabs(applied)).toEqual(['Claude Chat'])
})
it('drops the returning publisher and reports it to the repair lane', () => {
const onRetiredEpochDrop = vi.fn()
@@ -165,21 +155,4 @@ describe('retired-epoch repair for a returning publisher', () => {
expect(chatTabs(redelivered)).toEqual([])
})
it('revives only the epoch authority names, leaving other generations fenced', () => {
const dropped = replayUntilDrop()
applyLocalStructuredSessionTabSnapshots(
dropped,
[frame(RENDERER_EPOCH, 7, 'claude-1')],
undefined,
undefined,
{ authoritative: true }
)
// Authoritative census restored renderer epoch; delayed frames from displaced headless generation must remain retired.
const history = localStructuredSessionEpochHistoryByWorktree.get(WORKTREE)
expect(history?.current).toBe(RENDERER_EPOCH)
expect(history?.retired).toContain(HEADLESS_EPOCH)
expect(history?.retired).not.toContain(RENDERER_EPOCH)
})
})
@@ -117,49 +117,6 @@ describe('setWebRuntimeTabProps', () => {
})
})
it('maps mirrored browser/editor unified ids before setting host tab props', async () => {
vi.stubGlobal('__ORCA_WEB_CLIENT__', false)
mocks.getState.mockReturnValue(ownedBy(RUNTIME_ROW))
mocks.resolveHostSessionTabIdForWebSessionTab.mockImplementation(
(_state, args: { tabId: string }) =>
args.tabId === 'local-browser-unified' ? 'host-browser-unified' : null
)
const runtimeCall = vi.fn().mockResolvedValue({ id: 'p', ok: true, result: { updated: true } })
stubRuntimeEnvironmentBridge({ call: runtimeCall })
expect(
setWebRuntimeTabProps({
worktreeId: WORKTREE_ID,
tabId: 'local-browser-unified',
color: '#3b82f6'
})
).toBe(true)
await vi.waitFor(() => expect(runtimeCall).toHaveBeenCalledTimes(1))
expect(runtimeCall).toHaveBeenCalledWith({
selector: ENVIRONMENT_ID,
method: 'session.tabs.setTabProps',
params: {
worktree: `id:${WORKTREE_ID}`,
tabId: 'host-browser-unified',
color: '#3b82f6'
},
timeoutMs: 15_000
})
})
it('no-ops for a worktree with no runtime environment (local tab)', () => {
vi.stubGlobal('__ORCA_WEB_CLIENT__', false)
mocks.getState.mockReturnValue(ownedBy({ hostId: 'local' }))
const runtimeCall = vi.fn()
stubRuntimeEnvironmentBridge({ call: runtimeCall })
expect(
setWebRuntimeTabProps({ worktreeId: WORKTREE_ID, tabId: 'local-tab', color: '#fff' })
).toBe(false)
expect(runtimeCall).not.toHaveBeenCalled()
})
it('refuses a local twin of the paired server instead of pushing to either host', () => {
vi.stubGlobal('__ORCA_WEB_CLIENT__', false)
mocks.getState.mockReturnValue(ownedBy({ hostId: 'local' }, RUNTIME_ROW))
@@ -22,14 +22,6 @@ const remoteRepo: Repo = {
executionHostId: 'runtime:env-1'
}
const localRepo: Repo = {
id: 'app',
path: '/Users/me/app',
displayName: 'app',
badgeColor: '#000',
addedAt: 1
}
const hostCalls: string[] = []
let hostKnowsProject = true
let hostSelectorAmbiguous = false
@@ -172,20 +164,3 @@ describe('project removal after its server is gone', () => {
expect(store.getState().repos).toEqual([])
})
})
describe('project removal error toast', () => {
it('shows the host’s message without the IPC wrapper', async () => {
removeForHost.mockRejectedValue(
new Error("Error invoking remote method 'repos:removeForHost': Error: Project is busy")
)
const store = createTestStore()
store.setState({ repos: [localRepo] })
await store.getState().removeProject('app', { hostId: 'local', errorFeedback: 'toast' })
expect(toast.error).toHaveBeenCalledWith(
'Failed to remove project',
expect.objectContaining({ description: 'Project is busy' })
)
})
})
@@ -76,16 +76,6 @@ describe('store identity churn probe', () => {
expect(readStoreIdentityChurnReport()).toEqual([])
})
it('does not flag a refresh that returns the original reference', () => {
const store = createProbeStore()
const original = store.getState().rows
armStoreIdentityChurnProbe()
store.getState().refresh(original)
expect(readStoreIdentityChurnReport()).toEqual([])
})
it('names the write site when capture is requested', () => {
const store = createProbeStore()
armStoreIdentityChurnProbe({ captureSites: true })
@@ -118,15 +108,6 @@ describe('store identity churn probe', () => {
expect(churnFor('rows')).toBe(1)
})
it('ignores a write zustand itself drops as identical', () => {
const store = createProbeStore()
armStoreIdentityChurnProbe()
store.setState((state) => state)
expect(readStoreIdentityChurnReport()).toEqual([])
})
it('passes disarmed writes straight through without reading state', () => {
const innerSet = vi.fn()
const innerGet = vi.fn(() => ({ counter: 0 }))
@@ -190,14 +171,6 @@ describe('store identity churn probe', () => {
expect(churnFor('rows')).toBe(1)
})
it('records nothing while disarmed', () => {
const store = createProbeStore()
store.getState().refresh([{ id: 'a', label: 'A' }])
expect(readStoreIdentityChurnReport()).toEqual([])
})
it('treats distinct class instances as changed rather than equal', () => {
const store = create<{ value: unknown; put: (value: unknown) => void }>()(
withStoreIdentityChurnProbe((set) => ({
@@ -82,11 +82,6 @@ function withoutTab(frame: PublishedWorkspaceLayout, tabId: string): PublishedWo
}
describe('laying frame slices over the store', () => {
it('changes nothing for the frame the store already shows', async () => {
const { state, session, frame } = await restored()
expect(patchFor(state, session, frame)).toEqual({})
})
it('drops a terminal tab main closed, with its pane layout', async () => {
const { state, session, frame } = await restored()
const patch = patchFor(state, session, withoutTab(frame, 'tab-shell'))
@@ -114,21 +109,6 @@ describe('laying frame slices over the store', () => {
).not.toHaveProperty('tab-shell')
})
it('gives a row the frame creates the window’s own fields for it', async () => {
const { state, session, frame } = await restored()
const shown = patchFor(state, session, withoutTab(frame, 'tab-shell'))
const before = { ...state, ...shown }
const fields = vi.fn((id: string) =>
id === 'tab-shell' ? { pendingActivationSpawn: true } : null
)
const patch = patchFor(before, session, frame, undefined, fields)
const row = patch.tabsByWorktree?.[GIT_KEY]?.find((tab) => tab.id === 'tab-shell')
expect(row?.pendingActivationSpawn).toBe(true)
expect(fields).toHaveBeenCalledTimes(1)
})
it('keeps an open file’s editor state and place, and drops a file main closed', async () => {
const { state, session, frame } = await restored()
const file = state.openFiles.find((entry) => entry.worktreeId === GIT_KEY)!
+1 -55
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { GitBranchChangeEntry } from './git-diff-compare-types'
import type { GitStatusEntry } from './git-status-types'
import {
buildGitStatusSourceControlTree,
@@ -24,22 +24,6 @@ function labels(nodes: ReturnType<typeof flattenSourceControlTree>): string[] {
}
describe('buildSourceControlTree', () => {
it('groups changed files by path segments', () => {
const tree = buildSourceControlTree('unstaged', [
entry({ path: 'src/components/Button.tsx' }),
entry({ path: 'src/index.ts' }),
entry({ path: 'README.md' })
])
expect(labels(flattenSourceControlTree(tree, new Set()))).toEqual([
'directory:src',
'directory:src/components',
'file:src/components/Button.tsx',
'file:src/index.ts',
'file:README.md'
])
})
it('orders numbered sibling directories naturally, matching the file rows', () => {
const tree = buildSourceControlTree('unstaged', [
entry({ path: 'migrations/100 - b/a.sql' }),
@@ -104,23 +88,6 @@ describe('buildSourceControlTree', () => {
])
})
it('groups committed branch entries with branch-scoped directory keys', () => {
const entries: GitBranchChangeEntry[] = [
{ path: 'packages/app/src/index.ts', status: 'modified' },
{ path: 'packages/app/package.json', status: 'added' }
]
const tree = buildSourceControlTree('branch', entries)
expect(labels(flattenSourceControlTree(tree, new Set()))).toEqual([
'directory:packages',
'directory:packages/app',
'directory:packages/app/src',
'file:packages/app/src/index.ts',
'file:packages/app/package.json'
])
expect(tree[0].key).toBe('dir::branch::packages')
})
it('compacts single-child directory chains for visible source-control rows', () => {
const tree = buildSourceControlTree('branch', [
{ path: 'src/renderer/src/mockups.css', status: 'modified' },
@@ -160,27 +127,6 @@ describe('buildSourceControlTree', () => {
])
})
it('can namespace directory keys without changing file entry areas', () => {
const tree = compactSourceControlTree(
buildGitStatusSourceControlTree('unstaged', [
entry({
path: 'src/conflict.ts',
conflictKind: 'both_modified',
conflictStatus: 'unresolved'
})
])
)
const namespaced = namespaceSourceControlTreeDirectoryKeys(tree, 'conflicts')
const rows = flattenSourceControlTree(namespaced, new Set())
const directory = rows.find((node) => node.type === 'directory')
const file = rows.find((node) => node.type === 'file')
expect(directory?.key).toBe('dir::conflicts::src')
expect(file?.key).toBe('unstaged::src/conflict.ts')
expect(file?.area).toBe('unstaged')
})
it('can preserve file node areas from mixed conflict section entries', () => {
const tree = compactSourceControlTree(
buildGitStatusSourceControlTree('unstaged', [