mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 02:17:31 +00:00
test: remove 11 low-value suites and 157 individual cases (#36703)
* test: remove low-value suites and cases * test: preserve historical reliability gate evidence
This commit is contained in:
@@ -27,7 +27,6 @@
|
||||
1 test-no-in-test-import-actual:src/main/local-worktree-removal-recovery.test.ts
|
||||
2 test-no-in-test-import-actual:src/main/managed-server/activation/orcad-activation-crash-recovery.test.ts
|
||||
1 test-no-in-test-import-actual:src/main/managed-server/host-state/orcad-state-mutation-timeout.test.ts
|
||||
1 test-no-in-test-import-actual:src/main/node-runtime/managed-runtime-installation.test.ts
|
||||
1 test-no-in-test-import-actual:src/main/node-runtime/pinned-runtime-materializer.test.ts
|
||||
1 test-no-in-test-import-actual:src/main/omp/omp-session-cwd-lookup.test.ts
|
||||
6 test-no-in-test-import-actual:src/main/opencode/hook-service-legacy-acl.test.ts
|
||||
@@ -340,7 +339,6 @@
|
||||
1 test-no-real-time-wait:src/main/ipc/pty/pty-agent-launch-pane-attach.test.ts
|
||||
1 test-no-real-time-wait:src/main/ipc/pty/pty-daemon-controller-teardown.test.ts
|
||||
1 test-no-real-time-wait:src/main/ipc/pty/pty-launch-parity.test.ts
|
||||
2 test-no-real-time-wait:src/main/ipc/pty/pty-spawn-cwd-fallback-hung-folder.test.ts
|
||||
1 test-no-real-time-wait:src/main/ipc/rebinding-file-watch-noise.test.ts
|
||||
1 test-no-real-time-wait:src/main/ipc/rebinding-file-watch.test.ts
|
||||
3 test-no-real-time-wait:src/main/ipc/runtime-environments/runtime-environment-preference.test.ts
|
||||
@@ -382,7 +380,6 @@
|
||||
1 test-no-real-time-wait:src/main/native-chat/transcript-watch-error.test.ts
|
||||
2 test-no-real-time-wait:src/main/native-chat/transcript-watch-liveness.test.ts
|
||||
11 test-no-real-time-wait:src/main/native-chat/transcript-watch.test.ts
|
||||
1 test-no-real-time-wait:src/main/node-runtime/managed-runtime-installation.test.ts
|
||||
1 test-no-real-time-wait:src/main/opencode/hook-plugin-legacy-tui-ownership.test.ts
|
||||
6 test-no-real-time-wait:src/main/opencode/hook-plugin-module-contract.test.ts
|
||||
1 test-no-real-time-wait:src/main/opencode/hook-plugin-opencode2-tui-ownership.test.ts
|
||||
@@ -497,7 +494,6 @@
|
||||
1 test-no-real-time-wait:src/renderer/src/components/native-chat/use-structured-agent-session-legacy-recovery.test.tsx
|
||||
1 test-no-real-time-wait:src/renderer/src/components/native-chat/use-structured-agent-session-options.learned-default.test.tsx
|
||||
1 test-no-real-time-wait:src/renderer/src/components/native-chat/use-structured-agent-session-read.test.tsx
|
||||
1 test-no-real-time-wait:src/renderer/src/components/repo/CreateFromPicker.test.tsx
|
||||
1 test-no-real-time-wait:src/renderer/src/components/right-sidebar/checks-panel/use-checks-panel-generation.test.tsx
|
||||
1 test-no-real-time-wait:src/renderer/src/components/right-sidebar/file-explorer-watch-refresh-scheduler.test.ts
|
||||
1 test-no-real-time-wait:src/renderer/src/components/right-sidebar/parent-pr-checks-refresh.test.ts
|
||||
@@ -1107,7 +1103,6 @@
|
||||
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
|
||||
3 test-no-wall-clock:src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/status-bar/workspace-space-worktree-row.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/tab-bar/TabBarCreateEntry.history.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/tab-bar/cli-chat-resume-run.test.ts
|
||||
|
||||
@@ -9051,17 +9051,16 @@
|
||||
"providers": ["local", "daemon", "ssh", "wsl", "remote-runtime", "mobile-relay"],
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["local", "daemon", "ssh"],
|
||||
"coverageNotes": "Local macOS deterministic evidence covers the renderer snapshot/coordinator, exact local and SSH-shaped PTY request settlement, active-last and pinned terminal-tab routing, component-unmount continuation, and the existing current/legacy daemon management contract. Windows Electron process absence, live SSH/WSL behavior, and remote-runtime/mobile host completion remain explicit gaps.",
|
||||
"coverageNotes": "Local macOS deterministic evidence covers the renderer snapshot/coordinator, exact local and SSH-shaped PTY request settlement, active-last and pinned terminal-tab routing, component-unmount continuation, and the existing current/legacy daemon management contract. Windows Electron process absence, live SSH/WSL behavior, and remote-runtime/mobile host completion remain explicit gaps. The audit removed the isolated terminal-tab close routing suite. Forced-pin close-call shape, editor-before-browser fallback, browser fallback, empty-worktree deactivation and no replacement-terminal routing lose their supplied-store spy assertions. Remaining lifecycle/coordinator suites are not claimed to reproduce those isolated routing alarms.",
|
||||
"motivatingLinks": ["https://github.com/stablyai/orca/issues/8001"],
|
||||
"invariant": "Every terminal surface confirmed in the invoking renderer is force-closed exactly once after daemon management settles, later-created surfaces and non-terminal tabs survive, and exact shutdown requests are limited to deduplicated current non-runtime PTY bindings of the confirmed surfaces.",
|
||||
"oracle": "Snapshot terminal entity IDs before the first await; mutate ownership, active selection, bindings, and tab presence while daemon management is pending and between bounded close batches; then assert only the immutable targets disappear from both terminal stores, active targets close last with valid editor/browser/deactivated post-state, every captured exact PTY promise settles before callbacks, and no provider inventory sweep or late-tab kill occurs.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts src/renderer/src/components/shared/useDaemonActions.test.tsx src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts src/main/ipc/pty/pty-management.test.ts"
|
||||
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts src/renderer/src/components/shared/useDaemonActions.test.tsx src/main/ipc/pty/pty-management.test.ts"
|
||||
],
|
||||
"testFiles": [
|
||||
"src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts",
|
||||
"src/renderer/src/components/shared/useDaemonActions.test.tsx",
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts",
|
||||
"src/main/ipc/pty/pty-management.test.ts"
|
||||
],
|
||||
"assertionRefs": [
|
||||
@@ -9085,13 +9084,6 @@
|
||||
"closed terminal tabs report success instead of the no-sessions informational state when daemon management reports zero"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts",
|
||||
"assertions": [
|
||||
"force closes pinned terminals without a second confirmation",
|
||||
"closing the last active terminal preserves and activates editor or browser content, otherwise deactivates without auto-spawn"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/main/ipc/pty/pty-management.test.ts",
|
||||
"assertions": [
|
||||
@@ -9107,10 +9099,10 @@
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts src/renderer/src/components/shared/useDaemonActions.test.tsx src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts src/main/ipc/pty-management.test.ts",
|
||||
"currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts src/renderer/src/components/shared/useDaemonActions.test.tsx src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts src/main/ipc/pty/pty-management.test.ts",
|
||||
"currentCommand": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/shared/kill-all-terminal-surfaces.test.ts src/renderer/src/components/shared/useDaemonActions.test.tsx src/main/ipc/pty/pty-management.test.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 1.18,
|
||||
"summary": "4 test files and 27 tests passed locally, including the existing daemon handler contract and a real 100-terminal Zustand cleanup fixture."
|
||||
"summary": "4 test files and 27 tests passed locally, including the existing daemon handler contract and a real 100-terminal Zustand cleanup fixture. Historical four-file record only: its date, result, duration and 27-test count describe the original command. currentCommand maps the three surviving roots; that three-file invocation was not rerun here, and this record does not restore the removed routing alarms."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
@@ -9139,7 +9131,8 @@
|
||||
"Windows Electron screenshots and live process/xterm absence evidence were not produced by this local macOS run.",
|
||||
"Live SSH and WSL process absence, Linux local/daemon behavior, and mobile/relay shutdown remain unproved.",
|
||||
"Runtime-host terminal close is best-effort because closeTerminalTab still discards the existing async host result and its close-intent lifetime is shorter than the possible RPC flow.",
|
||||
"Daemon adapter listing failures remain suppressed by the existing management API, so reported daemon counts are not authoritative verification of every process."
|
||||
"Daemon adapter listing failures remain suppressed by the existing management API, so reported daemon counts are not authoritative verification of every process.",
|
||||
"The audit removed the isolated terminal-tab close routing suite. Forced-pin close-call shape, editor-before-browser fallback, browser fallback, empty-worktree deactivation and no replacement-terminal routing lose their supplied-store spy assertions. Remaining lifecycle/coordinator suites are not claimed to reproduce those isolated routing alarms."
|
||||
],
|
||||
"demotionRule": "Keep experimental or demote to protection none if the gate flakes, permits a late-created tab or unrelated PTY to close, duplicates provider shutdown, or performs more than one ownership replan per bounded yield."
|
||||
},
|
||||
|
||||
@@ -2654,7 +2654,6 @@
|
||||
"src/main/github/work-item-search-semantics.test.ts": 473,
|
||||
"src/main/gitlab/client-mr-auth-rate-limit.test.ts": 355,
|
||||
"src/main/gitlab/client-mr-branch-lookup.test.ts": 435,
|
||||
"src/main/gitlab/client-mr-job-ci.test.ts": 420,
|
||||
"src/main/gitlab/client-mr-listing.test.ts": 388,
|
||||
"src/main/gitlab/client-mr-review-actions.test.ts": 404,
|
||||
"src/main/gitlab/client-work-items.test.ts": 539,
|
||||
@@ -6170,7 +6169,6 @@
|
||||
"src/main/ssh/orcad-retained-fence-takeover.test.ts": 211,
|
||||
"src/main/ssh/orcad-retained-source-changes.test.ts": 1858,
|
||||
"src/main/ssh/orcad-retained-source-lists.test.ts": 566,
|
||||
"src/main/ssh/orcad-running-version-adoption.test.ts": 564,
|
||||
"src/main/ssh/orcad-runtime-maintenance.test.ts": 595,
|
||||
"src/main/ssh/orcad-server-project-groups.test.ts": 18,
|
||||
"src/main/ssh/orcad-snapshot-room.test.ts": 312,
|
||||
@@ -7054,9 +7052,7 @@
|
||||
"src/renderer/src/components/contextual-tours/contextual-tour-floating-position.test.ts": 2004,
|
||||
"src/renderer/src/components/contextual-tours/contextual-tour-gate.test.ts": 47,
|
||||
"src/renderer/src/components/contextual-tours/contextual-tour-overlay-measurement.test.ts": 258,
|
||||
"src/renderer/src/components/contextual-tours/contextual-tour-step-actions.test.ts": 55,
|
||||
"src/renderer/src/components/contextual-tours/request-contextual-tour-when-ready.test.ts": 1380,
|
||||
"src/renderer/src/components/contextual-tours/workspace-creation-tour-handoff.test.ts": 1390,
|
||||
"src/renderer/src/components/crash-report/crash-report-submit-notice.test.ts": 124,
|
||||
"src/renderer/src/components/crash-report/use-crash-report-copy.test.tsx": 358,
|
||||
"src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx": 740,
|
||||
@@ -8014,7 +8010,6 @@
|
||||
"src/renderer/src/components/quick-open-recent-validation.test.ts": 152,
|
||||
"src/renderer/src/components/quick-open-search.test.ts": 163,
|
||||
"src/renderer/src/components/quick-open-selection-owner.react.test.tsx": 1686,
|
||||
"src/renderer/src/components/repo/CreateFromPicker.test.tsx": 642,
|
||||
"src/renderer/src/components/repo/NestedRepoChecklist.test.tsx": 182,
|
||||
"src/renderer/src/components/repo/NestedRepoScanLimitNotice.test.ts": 192,
|
||||
"src/renderer/src/components/repo/repo-icon.test.tsx": 303,
|
||||
@@ -8774,7 +8769,6 @@
|
||||
"src/renderer/src/components/sidebar/worktree-list/rows/host-header-detail.test.ts": 128,
|
||||
"src/renderer/src/components/sidebar/worktree-list/rows/option-dom-host-collision.test.ts": 125,
|
||||
"src/renderer/src/components/sidebar/worktree-list/rows/project-header-host-context.test.ts": 103,
|
||||
"src/renderer/src/components/sidebar/worktree-list/rows/use-project-group-dialogs-owner-host.test.tsx": 732,
|
||||
"src/renderer/src/components/sidebar/worktree-list/viewport/hard-scroll-up.test.ts": 48,
|
||||
"src/renderer/src/components/sidebar/worktree-list/viewport/scroll-adjustment.test.ts": 1810,
|
||||
"src/renderer/src/components/sidebar/worktree-list/viewport/sticky-headers.test.ts": 195,
|
||||
@@ -8876,7 +8870,6 @@
|
||||
"src/renderer/src/components/status-bar/status-bar-copy-localization.test.tsx": 885,
|
||||
"src/renderer/src/components/status-bar/status-bar-density.test.ts": 57,
|
||||
"src/renderer/src/components/status-bar/status-bar-pool-countdown.test.tsx": 389,
|
||||
"src/renderer/src/components/status-bar/status-bar-provider-menu-focus.test.tsx": 941,
|
||||
"src/renderer/src/components/status-bar/status-bar-provider-visibility.test.ts": 42,
|
||||
"src/renderer/src/components/status-bar/status-bar-runtime-groups.test.ts": 237,
|
||||
"src/renderer/src/components/status-bar/status-bar-usage-collapse.test.ts": 159,
|
||||
@@ -9599,7 +9592,6 @@
|
||||
"src/renderer/src/components/terminal-pane/use-terminal-scroll-visibility-memory.test.ts": 56,
|
||||
"src/renderer/src/components/terminal-pane/use-terminal-tab-cold-parking.test.ts": 461,
|
||||
"src/renderer/src/components/terminal-pane/use-terminal-window-wake-recovery.test.ts": 333,
|
||||
"src/renderer/src/components/terminal-pane/useTerminalFontZoom.test.ts": 197,
|
||||
"src/renderer/src/components/terminal-pane/wrapped-terminal-http-link-limits.test.ts": 166,
|
||||
"src/renderer/src/components/terminal-pane/wrapped-terminal-http-link-provider.test.ts": 396,
|
||||
"src/renderer/src/components/terminal-pane/wrapped-terminal-link-ranges.test.ts": 42,
|
||||
@@ -9636,7 +9628,6 @@
|
||||
"src/renderer/src/components/terminal/terminal-provider-snapshot-bound-pty-ids.test.ts": 1570,
|
||||
"src/renderer/src/components/terminal/terminal-provider-snapshot-capability-resettlement.test.ts": 377,
|
||||
"src/renderer/src/components/terminal/terminal-provider-snapshot-capability.test.ts": 43,
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions-kill-all.test.ts": 602,
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions-pinned-server-close.test.ts": 269,
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions-unified-close.test.ts": 1809,
|
||||
"src/renderer/src/components/terminal/terminal-tab-actions-unrouted-close.test.ts": 474,
|
||||
@@ -10273,7 +10264,6 @@
|
||||
"src/renderer/src/lib/pane-manager/terminal-kitty-file-reader.test.ts": 68,
|
||||
"src/renderer/src/lib/pane-manager/terminal-ligatures-addon.test.ts": 38,
|
||||
"src/renderer/src/lib/pane-manager/terminal-link-provider-guard.test.ts": 52,
|
||||
"src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-mouseleave.test.ts": 34,
|
||||
"src/renderer/src/lib/pane-manager/terminal-linkifier-hover-reset-on-write.test.ts": 57,
|
||||
"src/renderer/src/lib/pane-manager/terminal-mouse-encoding-tracker.test.ts": 71,
|
||||
"src/renderer/src/lib/pane-manager/terminal-render-pause-release-parked-resize.test.ts": 50,
|
||||
@@ -11384,7 +11374,6 @@
|
||||
"src/renderer/src/web/preload-api/web-ui-compact-notice.test.ts": 569,
|
||||
"src/renderer/src/web/preload-api/web-ui-explorer-root.test.ts": 614,
|
||||
"src/renderer/src/web/preload-api/web-worktree-attachment-mutations.test.ts": 130,
|
||||
"src/renderer/src/web/web-clipboard-copy-fallback.test.ts": 34,
|
||||
"src/renderer/src/web/web-clipboard-copy-terminal-selection.test.ts": 152,
|
||||
"src/renderer/src/web/web-clipboard-image-presence.test.ts": 933,
|
||||
"src/renderer/src/web/web-device-key.test.ts": 86,
|
||||
|
||||
@@ -107,41 +107,4 @@ describe('the WSL hook relay idle stop', () => {
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
expect(states.has('ubuntu')).toBe(false)
|
||||
})
|
||||
|
||||
it('brings a stopped relay back with its config, and a quit forgets it', async () => {
|
||||
const state = running()
|
||||
states.set('ubuntu', state)
|
||||
idle.update(state)
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
|
||||
idle.restoreParked('Ubuntu')
|
||||
expect(states.get('ubuntu')).toMatchObject({
|
||||
phase: 'failed',
|
||||
cooldownUntil: 0,
|
||||
codexHomePath: '/home/u/.codex'
|
||||
})
|
||||
|
||||
states.clear()
|
||||
const again = running()
|
||||
states.set('ubuntu', again)
|
||||
idle.update(again)
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
idle.dispose(true)
|
||||
idle.restoreParked('Ubuntu')
|
||||
expect(states.has('ubuntu')).toBe(false)
|
||||
})
|
||||
|
||||
it('never stops a relay that is not running, or one a newer state replaced', async () => {
|
||||
const starting = { ...running(), phase: 'starting' as const }
|
||||
states.set('ubuntu', starting)
|
||||
idle.update(starting)
|
||||
await vi.advanceTimersByTimeAsync(5_000)
|
||||
expect(states.get('ubuntu')).toBe(starting)
|
||||
|
||||
const old = running()
|
||||
idle.update(old)
|
||||
await vi.advanceTimersByTimeAsync(1_000)
|
||||
expect(states.get('ubuntu')).toBe(starting)
|
||||
expect(old.child?.kill).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -98,19 +98,6 @@ describe('BrowserNetworkTunnelSession stream-scoped failures', () => {
|
||||
expect(tunnel.sockets[0]!.destroyed).toBe(false)
|
||||
})
|
||||
|
||||
it('fails only the pending stream when the destination ends before connecting', async () => {
|
||||
const tunnel = createPairedTunnel()
|
||||
const healthy = await openConnectedStream(tunnel, 'healthy.internal')
|
||||
|
||||
const pending = tunnel.client.open({ host: 'never-connects.internal', port: 443 })
|
||||
tunnel.sockets[1]!.emit('end')
|
||||
|
||||
await expect(pending).rejects.toThrow('destination_closed_before_connect')
|
||||
expect(tunnel.closures).toEqual([])
|
||||
expect(healthy.destroyed).toBe(false)
|
||||
expect(tunnel.sockets[0]!.destroyed).toBe(false)
|
||||
})
|
||||
|
||||
it('fails only the receiving stream when destination bytes exhaust the retained budget', async () => {
|
||||
const tunnel = createPairedTunnel(() => null)
|
||||
const healthy = await openConnectedStream(tunnel, 'healthy.internal')
|
||||
|
||||
@@ -44,8 +44,7 @@ vi.mock('./browser-extension-window-model', () => ({
|
||||
getBrowserExtensionMainWindow: () => ({ id: 1 })
|
||||
}))
|
||||
|
||||
const { openBrowserExtensionTab, recentWorktreeIdForBrowserProfile, recordActiveBrowserPage } =
|
||||
await import('./browser-extension-tab-model')
|
||||
const { openBrowserExtensionTab } = await import('./browser-extension-tab-model')
|
||||
const { watchBrowserMainWindowReadiness } = await import('../browser-main-window-readiness')
|
||||
|
||||
/** Orca's main window; its renderer reports readiness on the per-window IPC channel. */
|
||||
@@ -66,43 +65,6 @@ beforeEach(() => {
|
||||
mainWindow().rendererReady()
|
||||
})
|
||||
|
||||
describe('where an extension tab goes when no workspace is active', () => {
|
||||
it("prefers the active page's workspace when that page is in the profile", () => {
|
||||
mocks.pages.set('a', { id: 11, session: mocks.workSession, worktreeId: 'wt-a' })
|
||||
mocks.pages.set('b', { id: 12, session: mocks.workSession, worktreeId: 'wt-b' })
|
||||
recordActiveBrowserPage('b')
|
||||
expect(recentWorktreeIdForBrowserProfile('work')).toBe('wt-b')
|
||||
})
|
||||
|
||||
it("otherwise uses any open tab in the profile, never another profile's workspace", () => {
|
||||
mocks.pages.set('a', { id: 11, session: mocks.otherSession, worktreeId: 'wt-other' })
|
||||
mocks.pages.set('b', { id: 12, session: mocks.workSession, worktreeId: 'wt-work' })
|
||||
recordActiveBrowserPage('a')
|
||||
expect(recentWorktreeIdForBrowserProfile('work')).toBe('wt-work')
|
||||
mocks.pages.delete('b')
|
||||
expect(recentWorktreeIdForBrowserProfile('work')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('passes that workspace to the renderer as the fallback', async () => {
|
||||
mocks.pages.set('a', { id: 11, session: mocks.workSession, worktreeId: 'wt-a' })
|
||||
mocks.createTab.mockImplementation(async () => {
|
||||
mocks.pages.set('new', { id: 13, session: mocks.workSession, worktreeId: 'wt-a' })
|
||||
return { browserPageId: 'new' }
|
||||
})
|
||||
expect(await openBrowserExtensionTab('work', 'https://welcome.test', true)).toBe(13)
|
||||
expect(mocks.createTab).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'https://welcome.test',
|
||||
undefined,
|
||||
'work',
|
||||
'persist:work',
|
||||
true,
|
||||
undefined,
|
||||
'wt-a'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('an extension tab opened while Orca starts', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
@@ -128,14 +90,4 @@ describe('an extension tab opened while Orca starts', () => {
|
||||
window.rendererReady()
|
||||
await expect(reopening).resolves.toBe(21)
|
||||
})
|
||||
|
||||
it('fails cleanly when the window never becomes ready', async () => {
|
||||
vi.useFakeTimers()
|
||||
mainWindow()
|
||||
const opening = openBrowserExtensionTab('work', 'https://welcome.test', true)
|
||||
const failed = expect(opening).rejects.toThrow('Orca has no window to open a tab in')
|
||||
await vi.advanceTimersByTimeAsync(30_000)
|
||||
await failed
|
||||
expect(mocks.createTab).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { existsSync, mkdtempSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtempSync, readdirSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
@@ -67,31 +68,6 @@ describe('runClaudeLoginSession', () => {
|
||||
expect(openWslSignInLink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves a local login to open its own browser', async () => {
|
||||
const config = folder('host')
|
||||
const openWslSignInLink = vi.fn()
|
||||
const runCommand = vi.fn<RunCommand>(async (_args, _config, _ms, options) => {
|
||||
expect(options?.browser).toBeUndefined()
|
||||
expect(existsSync(join(config.windowsPath, '.orca-sign-in-browser'))).toBe(false)
|
||||
return ''
|
||||
})
|
||||
await runClaudeLoginSession(config, { runCommand, setCancel: vi.fn(), openWslSignInLink })
|
||||
expect(runCommand).toHaveBeenCalledTimes(1)
|
||||
expect(openWslSignInLink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('changes nothing for a WSL login where nothing can open a link', async () => {
|
||||
const config = folder('wsl')
|
||||
const runCommand = vi.fn<RunCommand>(async () => '')
|
||||
await runClaudeLoginSession(config, { runCommand, setCancel: vi.fn() })
|
||||
expect(runCommand.mock.calls[0]).toEqual([
|
||||
['auth', 'login', '--claudeai'],
|
||||
config,
|
||||
expect.any(Number),
|
||||
expect.objectContaining({ browser: undefined })
|
||||
])
|
||||
})
|
||||
|
||||
it('never opens a link that is not a Claude sign-in, and keeps the login going', async () => {
|
||||
const config = folder('wsl')
|
||||
const openWslSignInLink = vi.fn()
|
||||
@@ -129,19 +105,4 @@ describe('runClaudeLoginSession', () => {
|
||||
).rejects.toThrow('cancelled')
|
||||
expect(openWslSignInLink).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('runs a WSL login unchanged when the stand-in cannot be written', async () => {
|
||||
const config = { ...folder('wsl'), windowsPath: join(tmpdir(), 'missing-claude-folder', 'x') }
|
||||
const openWslSignInLink = vi.fn()
|
||||
const runCommand = vi.fn<RunCommand>(async () => '')
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
await runClaudeLoginSession(config, { runCommand, setCancel: vi.fn(), openWslSignInLink })
|
||||
warn.mockRestore()
|
||||
expect(runCommand.mock.calls[0]).toEqual([
|
||||
['auth', 'login', '--claudeai'],
|
||||
config,
|
||||
expect.any(Number),
|
||||
expect.objectContaining({ browser: undefined })
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,19 +91,6 @@ describe('DaemonServer attachment lifecycle', () => {
|
||||
expect(daemon.attachments.tokenBySessionId.has('transport-owned-session')).toBe(false)
|
||||
})
|
||||
|
||||
it('parks only the requesting client attachment on explicit detach', async () => {
|
||||
await createSession('explicitly-detached-session')
|
||||
const daemon = server as unknown as DaemonAttachmentPrivate
|
||||
const detach = vi.spyOn(daemon.host, 'detach')
|
||||
|
||||
await client.request('detach', { sessionId: 'explicitly-detached-session' })
|
||||
|
||||
expect(detach).toHaveBeenCalledWith('explicitly-detached-session', expect.any(Symbol))
|
||||
expect(daemon.host.sessions.get('explicitly-detached-session')?.hasAttachedClients).toBe(false)
|
||||
expect(daemon.attachments.clientIdBySessionId.has('explicitly-detached-session')).toBe(false)
|
||||
expect(daemon.attachments.tokenBySessionId.has('explicitly-detached-session')).toBe(false)
|
||||
})
|
||||
|
||||
it('parks an attachment completed after its transport already closed', async () => {
|
||||
const daemon = server as unknown as DaemonAttachmentPrivate
|
||||
const originalCreateOrAttach = daemon.host.createOrAttach.bind(daemon.host)
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
_resetRunningWslDistroCacheForTests,
|
||||
resolveRunningWslDistros
|
||||
} from '../wsl-running-distro-cache'
|
||||
import { checkWslBridgeDistro, classifyWslBridgeExit } from './daemon-wsl-bridge-exit'
|
||||
import { checkWslBridgeDistro } from './daemon-wsl-bridge-exit'
|
||||
|
||||
afterEach(() => {
|
||||
_resetRunningWslDistroCacheForTests()
|
||||
@@ -37,52 +37,6 @@ describe('checkWslBridgeDistro', () => {
|
||||
expect(probe).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('matches the distro case-insensitively, and a running distro is not lost', async () => {
|
||||
await expect(
|
||||
checkWslBridgeDistro('ubuntu', 0, async () => ({ status: 'confirmed', distros: ['Ubuntu'] }))
|
||||
).resolves.toBe('running')
|
||||
})
|
||||
|
||||
it('treats a hung check as unanswered, so the exit stays an exit', async () => {
|
||||
await expect(
|
||||
checkWslBridgeDistro('Ubuntu', 0, () => new Promise(() => {}), { firstMs: 10, totalMs: 30 })
|
||||
).resolves.toBe('unavailable')
|
||||
})
|
||||
|
||||
// 5e hardening (b): a slow or failing first read is asked once more before the exit stays an exit.
|
||||
it('retries an unanswered read once, past the failure backoff, within the total budget', async () => {
|
||||
const reads: { bypassFailureBackoff?: boolean }[] = []
|
||||
const answers: WslRunningDistrosRead[] = [
|
||||
{ status: 'unavailable' },
|
||||
{ status: 'confirmed', distros: [] }
|
||||
]
|
||||
await expect(
|
||||
checkWslBridgeDistro('Ubuntu', 0, async (options) => {
|
||||
reads.push({ bypassFailureBackoff: options.bypassFailureBackoff })
|
||||
return answers.shift() ?? { status: 'unavailable' }
|
||||
})
|
||||
).resolves.toBe('lost')
|
||||
expect(reads).toEqual([{ bypassFailureBackoff: undefined }, { bypassFailureBackoff: true }])
|
||||
})
|
||||
|
||||
it('gives the retry only what is left of the total budget after a hung first read', async () => {
|
||||
const reads = vi.fn(() => new Promise<WslRunningDistrosRead>(() => {}))
|
||||
const startedAt = performance.now()
|
||||
await expect(
|
||||
checkWslBridgeDistro('Ubuntu', 0, reads, { firstMs: 20, totalMs: 60 })
|
||||
).resolves.toBe('unavailable')
|
||||
expect(reads).toHaveBeenCalledTimes(2)
|
||||
expect(performance.now() - startedAt).toBeLessThan(500)
|
||||
})
|
||||
|
||||
it('treats a failed check as unanswered', async () => {
|
||||
await expect(
|
||||
checkWslBridgeDistro('Ubuntu', 0, async () => {
|
||||
throw new Error('wsl.exe missing')
|
||||
})
|
||||
).resolves.toBe('unavailable')
|
||||
})
|
||||
|
||||
it('never trusts a probe already in flight before the exit: one fresh probe follows it', async () => {
|
||||
// A background probe started before the pane's shell booted the distro, so it lacks it.
|
||||
const answers: ((distros: string[]) => void)[] = []
|
||||
@@ -105,35 +59,3 @@ describe('checkWslBridgeDistro', () => {
|
||||
await expect(Promise.all(checks)).resolves.toEqual(['running', 'running'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('classifyWslBridgeExit', () => {
|
||||
const running = async (): Promise<WslRunningDistrosRead> => ({
|
||||
status: 'confirmed',
|
||||
distros: ['Ubuntu']
|
||||
})
|
||||
|
||||
it('calls a running distro that booted again since the spawn rebooted, not an exit', async () => {
|
||||
await expect(classifyWslBridgeExit('Ubuntu', 0, running, async () => true)).resolves.toBe(
|
||||
'rebooted'
|
||||
)
|
||||
await expect(classifyWslBridgeExit('Ubuntu', 0, running, async () => false)).resolves.toBe(
|
||||
'running'
|
||||
)
|
||||
})
|
||||
|
||||
it('asks about the boot id only when the running list says running', async () => {
|
||||
const rebooted = vi.fn(async () => true)
|
||||
await expect(
|
||||
classifyWslBridgeExit(
|
||||
'Ubuntu',
|
||||
0,
|
||||
async () => ({ status: 'confirmed', distros: [] }),
|
||||
rebooted
|
||||
)
|
||||
).resolves.toBe('lost')
|
||||
await expect(
|
||||
classifyWslBridgeExit('Ubuntu', 0, async () => ({ status: 'unavailable' }), rebooted)
|
||||
).resolves.toBe('unavailable')
|
||||
expect(rebooted).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as GlUtils from './gl-utils'
|
||||
|
||||
const {
|
||||
glabExecFileAsyncMock,
|
||||
glabApiWithHeadersMock,
|
||||
getGlabKnownHostsMock,
|
||||
getProjectRefMock,
|
||||
resolveIssueSourceMock,
|
||||
acquireMock,
|
||||
releaseMock,
|
||||
gitExecFileAsyncMock
|
||||
} = vi.hoisted(() => ({
|
||||
glabExecFileAsyncMock: vi.fn(),
|
||||
glabApiWithHeadersMock: vi.fn(),
|
||||
getGlabKnownHostsMock: vi.fn(),
|
||||
getProjectRefMock: vi.fn(),
|
||||
resolveIssueSourceMock: vi.fn(),
|
||||
acquireMock: vi.fn(),
|
||||
releaseMock: vi.fn(),
|
||||
gitExecFileAsyncMock: vi.fn()
|
||||
}))
|
||||
|
||||
// Why: the #9171 default-branch guard resolves the repo default branch via
|
||||
// git; keep those probes hermetic instead of spawning real git processes.
|
||||
vi.mock('../git/runner', () => ({
|
||||
gitExecFileAsync: gitExecFileAsyncMock
|
||||
}))
|
||||
|
||||
vi.mock('./gl-utils', async () => {
|
||||
const actual = await vi.importActual<typeof GlUtils>('./gl-utils')
|
||||
return {
|
||||
...actual,
|
||||
glabExecFileAsync: glabExecFileAsyncMock,
|
||||
glabApiWithHeaders: glabApiWithHeadersMock,
|
||||
getGlabKnownHosts: getGlabKnownHostsMock,
|
||||
getProjectRef: getProjectRefMock,
|
||||
resolveIssueSource: resolveIssueSourceMock,
|
||||
acquire: acquireMock,
|
||||
release: releaseMock
|
||||
}
|
||||
})
|
||||
|
||||
import { getJobTrace, retryJob } from './client'
|
||||
import { resetGitLabMrMocks } from './client-mr-test-harness'
|
||||
|
||||
describe('gitlab client — MR operations', () => {
|
||||
beforeEach(() => {
|
||||
resetGitLabMrMocks({
|
||||
glabExecFileAsyncMock,
|
||||
glabApiWithHeadersMock,
|
||||
getGlabKnownHostsMock,
|
||||
getProjectRefMock,
|
||||
resolveIssueSourceMock,
|
||||
acquireMock,
|
||||
releaseMock,
|
||||
gitExecFileAsyncMock
|
||||
})
|
||||
})
|
||||
|
||||
describe('job CI operations', () => {
|
||||
beforeEach(() => {
|
||||
resolveIssueSourceMock.mockImplementation(async () => ({
|
||||
source: { host: 'git.internal', path: 'g/p' },
|
||||
fellBack: false
|
||||
}))
|
||||
})
|
||||
|
||||
it('fetches a job trace through the selected SSH GitLab host', async () => {
|
||||
glabExecFileAsyncMock.mockResolvedValueOnce({ stdout: 'trace output' })
|
||||
|
||||
await expect(getJobTrace('/repo', 99, 'upstream', 'conn-1')).resolves.toEqual({
|
||||
ok: true,
|
||||
trace: 'trace output'
|
||||
})
|
||||
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
['api', '--hostname', 'git.internal', 'projects/g%2Fp/jobs/99/trace'],
|
||||
{ timeout: 60_000 }
|
||||
)
|
||||
})
|
||||
|
||||
// Canceled-before-start jobs have no trace; show an empty log rather than issue-edit error copy.
|
||||
it('reports a 404 trace as an empty log rather than an error', async () => {
|
||||
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 404 Not Found'))
|
||||
|
||||
await expect(getJobTrace('/repo', 99, 'upstream', 'conn-1')).resolves.toEqual({
|
||||
ok: true,
|
||||
trace: ''
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a missing project and a scope failure as errors, in job-log wording', async () => {
|
||||
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 404: Project Not Found'))
|
||||
await expect(getJobTrace('/repo', 99, 'upstream', 'conn-1')).resolves.toMatchObject({
|
||||
ok: false,
|
||||
error: "Could not find this job's GitLab project."
|
||||
})
|
||||
|
||||
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('HTTP 403 insufficient_scope'))
|
||||
const forbidden = await getJobTrace('/repo', 99, 'upstream', 'conn-1')
|
||||
expect(forbidden).toEqual({
|
||||
ok: false,
|
||||
error: "You don't have permission to read this job's log. Check your GitLab token scopes.",
|
||||
errorCode: 'gitlab.jobLogPermissionDenied'
|
||||
})
|
||||
})
|
||||
|
||||
it('retries a job through the selected SSH GitLab host', async () => {
|
||||
glabExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: JSON.stringify({
|
||||
id: 100,
|
||||
pipeline: { id: 50 },
|
||||
name: 'test',
|
||||
stage: 'test',
|
||||
status: 'pending',
|
||||
web_url: 'https://git.internal/g/p/-/jobs/100',
|
||||
duration: null
|
||||
})
|
||||
})
|
||||
|
||||
await expect(retryJob('/repo', 99, 'upstream', 'conn-1')).resolves.toEqual({
|
||||
ok: true,
|
||||
job: {
|
||||
id: 100,
|
||||
pipelineId: 50,
|
||||
name: 'test',
|
||||
stage: 'test',
|
||||
status: 'pending',
|
||||
webUrl: 'https://git.internal/g/p/-/jobs/100',
|
||||
duration: null
|
||||
}
|
||||
})
|
||||
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledWith(
|
||||
['api', '--hostname', 'git.internal', '-X', 'POST', 'projects/g%2Fp/jobs/99/retry'],
|
||||
{}
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -64,12 +64,6 @@ describe('getLocalRepoForRegisteredWorktree', () => {
|
||||
expect(counter.splitCalls).toBe(rowCount)
|
||||
})
|
||||
|
||||
it('never touches the meta table when a repo path matches directly', () => {
|
||||
const { store, metaScans } = makeStore([{ id: 'repo-a', path: '/repos/a' }], [])
|
||||
expect(getLocalRepoForRegisteredWorktree(store, '/repos/a', '/repos/a')?.id).toBe('repo-a')
|
||||
expect(metaScans()).toBe(0)
|
||||
})
|
||||
|
||||
describe('equivalence with the per-repo scan', () => {
|
||||
const repos: TestRepo[] = [
|
||||
{ id: 'first', path: '/repos/first' },
|
||||
@@ -77,21 +71,6 @@ describe('getLocalRepoForRegisteredWorktree', () => {
|
||||
{ id: 'last', path: '/repos/last' }
|
||||
]
|
||||
|
||||
it('finds a worktree owned by the first repo', () => {
|
||||
const { store } = makeStore(repos, [worktreeId('first', '/wt/one')])
|
||||
expect(getLocalRepoForRegisteredWorktree(store, '/wt/one', '/wt/one')?.id).toBe('first')
|
||||
})
|
||||
|
||||
it('finds a worktree owned by the last repo', () => {
|
||||
const { store } = makeStore(repos, [worktreeId('last', '/wt/one')])
|
||||
expect(getLocalRepoForRegisteredWorktree(store, '/wt/one', '/wt/one')?.id).toBe('last')
|
||||
})
|
||||
|
||||
it('returns undefined when no repo owns the worktree', () => {
|
||||
const { store } = makeStore(repos, [worktreeId('other', '/wt/elsewhere')])
|
||||
expect(getLocalRepoForRegisteredWorktree(store, '/wt/one', '/wt/one')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps getRepos precedence when two repos both own the path', () => {
|
||||
const { store } = makeStore(repos, [
|
||||
worktreeId('last', '/wt/shared'),
|
||||
@@ -123,19 +102,5 @@ describe('getLocalRepoForRegisteredWorktree', () => {
|
||||
const { store } = makeStore(repos, [worktreeId('middle', '/wt/one')])
|
||||
expect(getLocalRepoForRegisteredWorktree(store, '/wt/other', '/wt/one')?.id).toBe('middle')
|
||||
})
|
||||
|
||||
it('returns undefined for a folder workspace that is not a registered worktree', () => {
|
||||
const { store } = makeStore(repos, [worktreeId('middle', '/wt/one')])
|
||||
expect(
|
||||
getLocalRepoForRegisteredWorktree(store, '/folders/notes', '/folders/notes')
|
||||
).toBeUndefined()
|
||||
})
|
||||
|
||||
it('tolerates a store without getRepos or getAllWorktreeMeta', () => {
|
||||
expect(getLocalRepoForRegisteredWorktree({} as never, '/wt/one', '/wt/one')).toBeUndefined()
|
||||
expect(
|
||||
getLocalRepoForRegisteredWorktree({ getRepos: () => repos } as never, '/wt/one', '/wt/one')
|
||||
).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type * as FsPromises from 'node:fs/promises'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { statSyncMock, spawnMock } from './pty-ipc-mock-registry'
|
||||
import { spawnMock } from './pty-ipc-mock-registry'
|
||||
|
||||
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
|
||||
import { registerPtyHandlers } from './register-handlers'
|
||||
|
||||
@@ -57,13 +58,8 @@ vi.mock('../../codex/codex-state-db-backfill-recovery', () =>
|
||||
)
|
||||
|
||||
const WORKTREE = '/Volumes/nas/repo'
|
||||
const SAVED_CWD = `${WORKTREE}/packages/app`
|
||||
const directoryStats = { isDirectory: () => true }
|
||||
|
||||
function statAnsweringAfter(ms: number): Promise<DirectoryStats> {
|
||||
return new Promise((resolve) => setTimeout(() => resolve(directoryStats), ms))
|
||||
}
|
||||
|
||||
function spawnWithWorktreeFallback(cwd: string): Promise<unknown> {
|
||||
return Promise.resolve(
|
||||
handlers.get('pty:spawn')!(null, {
|
||||
@@ -85,31 +81,6 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('pty:spawn with the worktree-folder fallback on a mount that stopped answering', () => {
|
||||
it('waits on a hung saved folder without blocking main or calling statSync', async () => {
|
||||
statMock.mockReturnValue(new Promise<DirectoryStats>(() => {}))
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
|
||||
const settled = vi.fn()
|
||||
void spawnWithWorktreeFallback(SAVED_CWD).then(settled, settled)
|
||||
const timerFired = await new Promise<boolean>((resolve) => setTimeout(() => resolve(true), 50))
|
||||
|
||||
expect(timerFired).toBe(true)
|
||||
expect(settled).not.toHaveBeenCalled()
|
||||
expect(spawnMock).not.toHaveBeenCalled()
|
||||
expect(statSyncMock).not.toHaveBeenCalledWith(SAVED_CWD)
|
||||
})
|
||||
|
||||
it('starts in a slow saved folder once it answers', async () => {
|
||||
statMock.mockImplementation(() => statAnsweringAfter(50))
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
|
||||
const result = await spawnWithWorktreeFallback(SAVED_CWD)
|
||||
|
||||
const [, , options] = spawnMock.mock.calls.at(-1) as [string, string[], { cwd: string }]
|
||||
expect(options.cwd).toBe(SAVED_CWD)
|
||||
expect(result).toEqual(expect.not.objectContaining({ startupCwdFallback: expect.anything() }))
|
||||
})
|
||||
|
||||
it('never asks the worktree folder once the saved folder answers', async () => {
|
||||
const outsideCwd = '/Users/alice/scratch'
|
||||
statMock.mockImplementation(async (path) =>
|
||||
@@ -123,19 +94,4 @@ describe('pty:spawn with the worktree-folder fallback on a mount that stopped an
|
||||
const [, , options] = spawnMock.mock.calls.at(-1) as [string, string[], { cwd: string }]
|
||||
expect(options.cwd).toBe(outsideCwd)
|
||||
})
|
||||
|
||||
it('still falls back to the worktree folder when the saved one is gone', async () => {
|
||||
statMock.mockImplementation(async (path) => {
|
||||
if (path === SAVED_CWD) {
|
||||
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
|
||||
}
|
||||
return directoryStats
|
||||
})
|
||||
registerPtyHandlers(mainWindow as never)
|
||||
|
||||
const result = await spawnWithWorktreeFallback(SAVED_CWD)
|
||||
|
||||
expect(statMock.mock.calls.map(([path]) => path)).toEqual([SAVED_CWD, WORKTREE])
|
||||
expect(result).toMatchObject({ startupCwdFallback: { kind: 'worktree', cwd: WORKTREE } })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ const WSL_RUNTIME = {
|
||||
|
||||
// E9: a restore the user did not ask for must not start a stopped distro, whatever provider runs.
|
||||
describe('a passive WSL restore with a stopped distro', () => {
|
||||
const { handlers, mainWindow, installDaemonTestProvider } = setupPtyIpcSuite()
|
||||
const { handlers, mainWindow } = setupPtyIpcSuite()
|
||||
const originalPlatform = process.platform
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -108,15 +108,6 @@ describe('a passive WSL restore with a stopped distro', () => {
|
||||
expect(spawnMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('holds with the daemon provider too, before its cwd probe', async () => {
|
||||
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-daemon' })
|
||||
installDaemonTestProvider({ spawn: providerSpawn })
|
||||
register()
|
||||
await expect(restore()).rejects.toThrow()
|
||||
expect(wslUncDirectoryExistsAsyncMock).not.toHaveBeenCalled()
|
||||
expect(providerSpawn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('restores once the distro runs, and never holds a restore the user asked for', async () => {
|
||||
register()
|
||||
runningStateMock.mockResolvedValue('running')
|
||||
@@ -129,18 +120,4 @@ describe('a passive WSL restore with a stopped distro', () => {
|
||||
expect(runningStateMock).not.toHaveBeenCalled()
|
||||
expect(spawnMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it("hands the provider this spawn's read anchor, so its hold reuses only that read", async () => {
|
||||
const providerSpawn = vi.fn().mockResolvedValue({ id: 'pty-daemon' })
|
||||
installDaemonTestProvider({ spawn: providerSpawn })
|
||||
register()
|
||||
runningStateMock.mockResolvedValue('running')
|
||||
await restore()
|
||||
|
||||
const anchor = runningStateMock.mock.calls[0]?.[1]?.startedAfter
|
||||
expect(anchor).toEqual(expect.any(Number))
|
||||
expect(providerSpawn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ wslHoldReadAnchor: anchor })
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -51,21 +51,6 @@ describe('required Jira transition fields', () => {
|
||||
getClients.mockReturnValue([entry])
|
||||
})
|
||||
|
||||
it('loads required resolution metadata rather than hiding it from the form', async () => {
|
||||
jiraRequest.mockResolvedValue({ transitions: [transition] })
|
||||
const { listTransitions } = await import('./jira-transition-queries')
|
||||
expect(await listTransitions('APP-1', 'site-1')).toMatchObject([
|
||||
{
|
||||
id: '31',
|
||||
fields: [{ key: 'resolution', required: true, allowedValues: [{ id: '1', name: 'Fixed' }] }]
|
||||
}
|
||||
])
|
||||
expect(jiraRequest).toHaveBeenCalledWith(
|
||||
entry,
|
||||
'/rest/api/3/issue/APP-1/transitions?expand=transitions.fields'
|
||||
)
|
||||
})
|
||||
|
||||
it('posts resolution and comment in the same request Jira validates', async () => {
|
||||
jiraRequest.mockImplementation(async (_entry, _path, options) => {
|
||||
const payload = JSON.parse(options.body)
|
||||
@@ -107,23 +92,6 @@ describe('required Jira transition fields', () => {
|
||||
expect(clearToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('preserves the legacy empty list on operational failures without strict reads', async () => {
|
||||
jiraRequest.mockRejectedValue(new JiraApiError('Service unavailable', 503))
|
||||
const { listTransitions } = await import('./jira-transition-queries')
|
||||
expect(await listTransitions('APP-1', 'site-1')).toEqual([])
|
||||
expect(jiraRequest).toHaveBeenCalledTimes(1)
|
||||
expect(release).toHaveBeenCalledOnce()
|
||||
expect(clearToken).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not retry expired credentials', async () => {
|
||||
jiraRequest.mockRejectedValue(new JiraApiError('Unauthorized', 401))
|
||||
const { listTransitions } = await import('./jira-transition-queries')
|
||||
await expect(listTransitions('APP-1')).rejects.toThrow('Unauthorized')
|
||||
expect(jiraRequest).toHaveBeenCalledTimes(1)
|
||||
expect(clearToken).toHaveBeenCalledWith('site-1')
|
||||
})
|
||||
|
||||
it('keeps old-client bare transitions and Server comments compatible', async () => {
|
||||
jiraRequest.mockResolvedValue(undefined)
|
||||
const { updateIssue } = await import('./jira-issue-mutations')
|
||||
|
||||
@@ -131,16 +131,4 @@ describe("a managed tunnel follows another desktop's restart of the server", ()
|
||||
})
|
||||
expect([...state.forwardedTo.values()]).toEqual([6_768])
|
||||
})
|
||||
|
||||
it('keeps the tunnel when the host still names the forwarded port', async () => {
|
||||
const state = setup()
|
||||
const environment = createEnvironment()
|
||||
await state.manager.ensure(environment)
|
||||
state.ensureServing.mockResolvedValueOnce({ state: 'unverifiable', detail: 'not answering' })
|
||||
|
||||
await expect(state.manager.verifyServing(environment)).resolves.toMatchObject({
|
||||
state: 'unverifiable'
|
||||
})
|
||||
expect([...state.forwardedTo.values()]).toEqual([6_768])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,12 +9,9 @@ vi.mock('../ipc/runtime-environments/runtime-environment-registry-root', () => (
|
||||
}))
|
||||
|
||||
import {
|
||||
REMOTE_PREVIEW_UPDATE_REQUIRED_MESSAGE,
|
||||
RemotePreviewPairingChangedError,
|
||||
assertRemoteChunkReadsSupported,
|
||||
readRemotePreviewRange,
|
||||
readRemotePreviewChunk,
|
||||
statRemotePreviewFile
|
||||
readRemotePreviewChunk
|
||||
} from './remote-file-preview-reader'
|
||||
|
||||
const address = {
|
||||
@@ -24,38 +21,11 @@ const address = {
|
||||
expectedEnvironmentPairingRevision: 7
|
||||
}
|
||||
const ok = (result: unknown) => ({ ok: true, result })
|
||||
const fail = (code: string, message: string) => ({ ok: false, error: { code, message } })
|
||||
|
||||
describe('remote file preview reader', () => {
|
||||
const signal = new AbortController().signal
|
||||
beforeEach(() => callRuntimeEnvironment.mockReset())
|
||||
|
||||
it('stats the file through its pinned pairing', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(
|
||||
ok({ size: 6, isDirectory: false, mtime: 1, ctime: 2 })
|
||||
)
|
||||
await expect(statRemotePreviewFile(address, signal)).resolves.toEqual({
|
||||
size: 6,
|
||||
mtime: 1,
|
||||
ctime: 2
|
||||
})
|
||||
expect(callRuntimeEnvironment).toHaveBeenCalledWith(
|
||||
'/user-data',
|
||||
'env-1',
|
||||
'files.stat',
|
||||
{ worktree: 'id:wt-1', relativePath: 'out/clip.mp4' },
|
||||
15_000,
|
||||
7,
|
||||
undefined,
|
||||
{ signal }
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses a directory', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(ok({ size: 0, isDirectory: true, mtime: 1 }))
|
||||
await expect(statRemotePreviewFile(address, signal)).rejects.toThrow('directory')
|
||||
})
|
||||
|
||||
it('decodes a chunk and refuses one whose length disagrees', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValueOnce(
|
||||
ok({ contentBase64: 'AAECAw==', bytesRead: 4, eof: false })
|
||||
@@ -69,20 +39,6 @@ describe('remote file preview reader', () => {
|
||||
await expect(readRemotePreviewChunk(address, 0, 4, signal)).rejects.toThrow('corrupted')
|
||||
})
|
||||
|
||||
it('tells the user to update a host without range reads', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(fail('method_not_found', 'x'))
|
||||
await expect(statRemotePreviewFile(address, signal)).rejects.toThrow(
|
||||
REMOTE_PREVIEW_UPDATE_REQUIRED_MESSAGE
|
||||
)
|
||||
})
|
||||
|
||||
it('reports a re-paired environment as a dead pairing, not a transient failure', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(fail('runtime_environment_changed', 'changed'))
|
||||
await expect(readRemotePreviewChunk(address, 0, 1, signal)).rejects.toBeInstanceOf(
|
||||
RemotePreviewPairingChangedError
|
||||
)
|
||||
})
|
||||
|
||||
it('proves chunk reads with one byte once per pairing, and re-proves after a re-pair', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(
|
||||
ok({ contentBase64: 'AA==', bytesRead: 1, eof: false })
|
||||
@@ -108,23 +64,6 @@ describe('remote file preview reader', () => {
|
||||
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('never caches a verdict no pairing revision pins', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(
|
||||
ok({ contentBase64: 'AA==', bytesRead: 1, eof: false })
|
||||
)
|
||||
const unpinned = { environmentId: 'env-unpinned', worktree: 'id:wt-1', relativePath: 'a.mp4' }
|
||||
await assertRemoteChunkReadsSupported(unpinned, signal)
|
||||
await assertRemoteChunkReadsSupported(unpinned, signal)
|
||||
expect(callRuntimeEnvironment).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('tells the user to update a host that answers files.stat but not files.readChunk', async () => {
|
||||
callRuntimeEnvironment.mockResolvedValue(fail('method_not_found', 'x'))
|
||||
await expect(
|
||||
assertRemoteChunkReadsSupported({ ...address, environmentId: 'env-old' }, signal)
|
||||
).rejects.toThrow(REMOTE_PREVIEW_UPDATE_REQUIRED_MESSAGE)
|
||||
})
|
||||
|
||||
it('reads a range in host-sized chunks, clipped to the file and stopping at EOF', async () => {
|
||||
callRuntimeEnvironment
|
||||
.mockResolvedValueOnce(
|
||||
|
||||
+1
-23
@@ -5,11 +5,7 @@ import type { AgentSessionAccountHome } from '../../../shared/agent-session-acco
|
||||
import type { AgentSessionModelOption } from '../../../shared/agent-session-wire'
|
||||
import { agentModelCatalogFingerprintForRecord } from './agent-model-catalog-fingerprint'
|
||||
import { createAgentModelCatalogService } from './agent-model-catalog-service'
|
||||
import {
|
||||
AGENT_MODEL_CATALOG_FRESH_MS,
|
||||
AgentModelCatalogStore,
|
||||
type AgentModelCatalogSuccess
|
||||
} from './agent-model-catalog-store'
|
||||
import { AgentModelCatalogStore, type AgentModelCatalogSuccess } from './agent-model-catalog-store'
|
||||
|
||||
const HOME: AgentSessionAccountHome = { variable: 'GROK_HOME', path: '/homes/grok' }
|
||||
|
||||
@@ -105,15 +101,6 @@ describe('a live listing beside the account-level discovery', () => {
|
||||
expect(read?.models[0]?.defaultEffort).toBe('high')
|
||||
})
|
||||
|
||||
it('names the default on a cold account whose first listing came from a live chat', async () => {
|
||||
const run = harness(async () => discovery('grok-4'))
|
||||
run.saveLive()
|
||||
// A live listing alone names no default and does not count as fresh, so explicit demand lists the account.
|
||||
await run.catalog.demand({ agent: 'grok' })
|
||||
await vi.waitFor(() => expect(run.probes).toHaveBeenCalledTimes(1))
|
||||
await vi.waitFor(async () => expect(await run.defaultOf()).toBe('grok-4'))
|
||||
})
|
||||
|
||||
it('still refreshes the account listing after ten minutes of live saves', async () => {
|
||||
let configured = 'grok-4'
|
||||
const run = harness(async () => discovery(configured))
|
||||
@@ -130,13 +117,4 @@ describe('a live listing beside the account-level discovery', () => {
|
||||
await vi.waitFor(() => expect(run.probes).toHaveBeenCalledTimes(2))
|
||||
await vi.waitFor(async () => expect(await run.defaultOf()).toBe('grok-3-mini'))
|
||||
})
|
||||
|
||||
it('does not refresh while the discovery is younger than its freshness window', async () => {
|
||||
const run = harness(async () => discovery('grok-4'))
|
||||
await run.read()
|
||||
run.advance(AGENT_MODEL_CATALOG_FRESH_MS - 1)
|
||||
run.saveLive()
|
||||
await run.read()
|
||||
expect(run.probes).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
-11
@@ -97,17 +97,6 @@ describe('resolved revisions', () => {
|
||||
expect(rowText()).toBe('abc')
|
||||
})
|
||||
|
||||
it('skips a revision that resolves to nothing', async () => {
|
||||
const deferred = createDeferredStructuredAgentSessionEventSink(testEventSinkLogging())
|
||||
deferred.sink.tryReviseResolvedItem?.(1_000, () => null, {
|
||||
turnScope: AGENT_JOURNAL_THREAD_SCOPE
|
||||
})
|
||||
const before = journal.cursor()
|
||||
deferred.bind(journalTarget())
|
||||
await expect(deferred.drained()).resolves.toEqual({ ok: true })
|
||||
expect(journal.cursor()).toEqual(before)
|
||||
})
|
||||
|
||||
it('publishes a revision in the operation that writes it, within the same reservation', async () => {
|
||||
const deferred = createDeferredStructuredAgentSessionEventSink(testEventSinkLogging())
|
||||
const bytes = estimateStructuredAgentSessionItemBytes(ROW, text('a'))
|
||||
|
||||
@@ -35,30 +35,6 @@ function createSession() {
|
||||
describe('default-session proxy request guard', () => {
|
||||
beforeEach(() => vi.clearAllMocks())
|
||||
|
||||
it('holds renderer requests until a delayed proxy transition settles', async () => {
|
||||
const { proxySession, request } = createSession()
|
||||
let finishWrite: (() => void) | undefined
|
||||
proxySession.setProxy.mockImplementationOnce(
|
||||
() => new Promise<void>((resolve) => (finishWrite = resolve))
|
||||
)
|
||||
resetSessionProxyApplicationForTests(proxySession)
|
||||
installElectronProxyRequestGuard(proxySession as never)
|
||||
|
||||
const applying = applyProxySettingsToSession(
|
||||
proxySession,
|
||||
{ httpProxyUrl: 'http://proxy.example:8080' },
|
||||
{ env: {} }
|
||||
)
|
||||
const callback = vi.fn()
|
||||
request(callback)
|
||||
expect(callback).not.toHaveBeenCalled()
|
||||
|
||||
await vi.waitFor(() => expect(proxySession.setProxy).toHaveBeenCalledOnce())
|
||||
finishWrite?.()
|
||||
await applying
|
||||
await vi.waitFor(() => expect(callback).toHaveBeenCalledWith({}))
|
||||
})
|
||||
|
||||
it('keeps renderer requests blocked across a transient retry', async () => {
|
||||
const { proxySession, request } = createSession()
|
||||
let finishRetry: (() => void) | undefined
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { mkdir, mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
import { mkdtemp, readdir, rm, writeFile } from 'node:fs/promises'
|
||||
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { strToU8, zipSync } from 'fflate'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import type * as ReleaseModule from './managed-runtime-release'
|
||||
import { reclaimRuntimeReleases, RUNTIME_RELEASE_RECEIPT } from './managed-runtime-release'
|
||||
import { RUNTIME_RELEASE_RECEIPT } from './managed-runtime-release'
|
||||
import { ManagedRuntimeInstallation } from './managed-runtime-installation'
|
||||
|
||||
vi.mock('./managed-runtime-release', async (importOriginal) => {
|
||||
@@ -90,34 +91,6 @@ it('installs only when asked, with progress, cancel, retry, and a status read fr
|
||||
expect(await installation.status()).toMatchObject({ installedCurrent: true })
|
||||
})
|
||||
|
||||
it('publishes an update only once the old releases were reclaimed', async () => {
|
||||
const { root, host, phase } = await rig(['whole'])
|
||||
for (const version of ['0.8', '0.9']) {
|
||||
await mkdir(join(root, version), { recursive: true })
|
||||
await writeFile(join(root, version, 'server'), 'old server', { mode: 0o755 })
|
||||
const members = [{ path: 'server', bytes: 10 }]
|
||||
const receipt = { releaseId: sha256(version), version, members }
|
||||
await writeFile(
|
||||
join(root, version, RUNTIME_RELEASE_RECEIPT),
|
||||
JSON.stringify({ ...receipt, platform: process.platform, arch: process.arch })
|
||||
)
|
||||
}
|
||||
let resume = (): void => {}
|
||||
const reclaim = vi.mocked(reclaimRuntimeReleases)
|
||||
const paused = new Promise<void>((resolve) => (resume = resolve))
|
||||
const { reclaimRuntimeReleases: reclaimNow } = await vi.importActual<typeof ReleaseModule>(
|
||||
'./managed-runtime-release'
|
||||
)
|
||||
reclaim.mockImplementationOnce(async (...args) => paused.then(() => reclaimNow(...args)))
|
||||
const installation = host()
|
||||
installation.install()
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
expect((await installation.status()).operation).toMatchObject({ downloadedBytes: 0 })
|
||||
resume()
|
||||
await vi.waitFor(async () => expect(await phase(installation)).toBe('succeeded'))
|
||||
expect((await readdir(root)).sort()).toEqual(['0.9', '1.0'])
|
||||
})
|
||||
|
||||
it('repairs a damaged copy when Install is clicked again', async () => {
|
||||
const { root, host, phase } = await rig(['whole', 'whole'])
|
||||
const installation = host()
|
||||
|
||||
@@ -43,9 +43,7 @@ vi.mock('./profile-cloud-client', () => ({
|
||||
|
||||
import {
|
||||
connectCurrentOrcaProfile,
|
||||
createCloudLinkedOrcaProfile,
|
||||
getCurrentOrcaProfileAuthStatus,
|
||||
selectCurrentOrcaProfileOrg,
|
||||
signOutCurrentOrcaProfile
|
||||
} from './profile-cloud-service'
|
||||
|
||||
@@ -73,59 +71,6 @@ describe('Orca cloud dev auth service', () => {
|
||||
vi.unstubAllEnvs()
|
||||
})
|
||||
|
||||
it('connects the active profile without PKCE or cloud endpoints', async () => {
|
||||
expect(getCurrentOrcaProfileAuthStatus(userDataPath)).toMatchObject({
|
||||
configured: true,
|
||||
state: 'local'
|
||||
})
|
||||
|
||||
const result = await connectCurrentOrcaProfile(userDataPath)
|
||||
|
||||
expect(result.status).toBe('connected')
|
||||
expect(beginOrcaCloudPkceFlowMock).not.toHaveBeenCalled()
|
||||
expect(exchangeOrcaCloudAuthCodeMock).not.toHaveBeenCalled()
|
||||
expect(getCurrentOrcaProfileAuthStatus(userDataPath)).toMatchObject({
|
||||
configured: true,
|
||||
state: 'connected',
|
||||
persistence: 'encrypted',
|
||||
cloud: {
|
||||
cloudProfileId: 'dev-cloud-local-default',
|
||||
email: 'dev@orca.local'
|
||||
},
|
||||
capabilities: {
|
||||
flags: expect.objectContaining({ 'share.create': true })
|
||||
}
|
||||
})
|
||||
expect(getCurrentOrcaProfileAuthStatus(userDataPath).organizations).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('selects dev organizations and creates org-scoped cloud profiles locally', async () => {
|
||||
await connectCurrentOrcaProfile(userDataPath)
|
||||
|
||||
const selected = await selectCurrentOrcaProfileOrg(userDataPath, 'dev-acme')
|
||||
const created = await createCloudLinkedOrcaProfile(userDataPath, {
|
||||
orgId: 'dev-acme',
|
||||
name: 'Acme Dev'
|
||||
})
|
||||
|
||||
expect(selected.status).toBe('selected')
|
||||
expect(getCurrentOrcaProfileAuthStatus(userDataPath).cloud).toMatchObject({
|
||||
activeOrgId: 'dev-acme',
|
||||
activeOrgName: 'Acme Dev'
|
||||
})
|
||||
expect(created.status).toBe('created')
|
||||
if (created.status === 'created') {
|
||||
expect(created.profile).toMatchObject({
|
||||
name: 'Acme Dev',
|
||||
kind: 'cloud-linked',
|
||||
cloud: expect.objectContaining({
|
||||
activeOrgId: 'dev-acme',
|
||||
activeOrgName: 'Acme Dev'
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('signs out locally without calling the cloud logout endpoint', async () => {
|
||||
await connectCurrentOrcaProfile(userDataPath)
|
||||
const regionCache = join(userDataPath, 'orca-relay-region-preference.json')
|
||||
|
||||
@@ -92,20 +92,6 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('resolveOrcadBrowserProvider Electron preference', () => {
|
||||
it('uses the installed Electron app even when a Chromium executable is configured', async () => {
|
||||
provider = await resolveOrcadBrowserProvider({
|
||||
userDataPath: join(harnessRoot, 'state'),
|
||||
environment: { ORCA_BROWSER_EXECUTABLE: chromiumExecutable },
|
||||
resolveInstalledElectronExecutable: async () => INSTALLED_EXECUTABLE,
|
||||
resolveAgentBrowserBinary: () => '/agent-browser'
|
||||
})
|
||||
|
||||
expect(provider?.kind).toBe('electron')
|
||||
expect(provider?.isAvailable()).toBe(true)
|
||||
expect(spawnProcessMock.mock.calls[0][0].program).toBe(INSTALLED_EXECUTABLE)
|
||||
expect(runProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('falls back to the operator Chromium when the installed Electron app cannot start', async () => {
|
||||
sidecarMode = 'exit-before-ready'
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
@@ -124,19 +110,4 @@ describe('resolveOrcadBrowserProvider Electron preference', () => {
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
|
||||
it('returns no provider when Electron fails and no Chromium is configured', async () => {
|
||||
sidecarMode = 'exit-before-ready'
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
|
||||
provider = await resolveOrcadBrowserProvider({
|
||||
userDataPath: join(harnessRoot, 'state'),
|
||||
environment: {},
|
||||
resolveInstalledElectronExecutable: async () => INSTALLED_EXECUTABLE,
|
||||
resolveAgentBrowserBinary: () => '/agent-browser'
|
||||
})
|
||||
|
||||
expect(provider).toBeNull()
|
||||
expect(runProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,16 +76,6 @@ describe('daemon descendant walk under Windows pid reuse', () => {
|
||||
expect(await teardown(root, [daemon, ...system], system)).toBeNull()
|
||||
})
|
||||
|
||||
it('still fails on a child the daemon started and left running', async ({ onTestFinished }) => {
|
||||
const daemonPid = await exitedPid()
|
||||
const daemon = { pid: daemonPid, ppid: 1, name: 'node.exe', creationTimeMs: 5_000 }
|
||||
const child = { pid: 77, ppid: daemonPid, name: 'OpenConsole.exe', creationTimeMs: 6_000 }
|
||||
const root = profile(daemonPid, onTestFinished)
|
||||
expect(await teardown(root, [daemon, child], [child])).toMatchObject(
|
||||
leaked(`OpenConsole.exe 77 (ppid ${daemonPid}, created 6000)`)
|
||||
)
|
||||
})
|
||||
|
||||
it('still fails on a grandchild chain created after the daemon', async ({ onTestFinished }) => {
|
||||
const daemonPid = await exitedPid()
|
||||
const daemon = { pid: daemonPid, ppid: 1, name: 'node.exe', creationTimeMs: 5_000 }
|
||||
@@ -97,17 +87,6 @@ describe('daemon descendant walk under Windows pid reuse', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('ignores an older process whose reused parent pid belongs to an exited daemon child', async ({
|
||||
onTestFinished
|
||||
}) => {
|
||||
const daemonPid = await exitedPid()
|
||||
const daemon = { pid: daemonPid, ppid: 1, name: 'node.exe', creationTimeMs: 5_000 }
|
||||
const child = { pid: 2_000, ppid: daemonPid, name: 'OpenConsole.exe', creationTimeMs: 6_000 }
|
||||
const telemetry = { pid: 2_904, ppid: 2_000, name: 'vctip.exe', creationTimeMs: 1_000 }
|
||||
const root = profile(daemonPid, onTestFinished)
|
||||
expect(await teardown(root, [daemon, child, telemetry], [telemetry])).toBeNull()
|
||||
})
|
||||
|
||||
it('ignores children of a newer process that reused the exited daemon pid', async ({
|
||||
onTestFinished
|
||||
}) => {
|
||||
@@ -121,25 +100,4 @@ describe('daemon descendant walk under Windows pid reuse', () => {
|
||||
leaked(`OpenConsole.exe 77 (ppid ${daemonPid}, created 6000)`)
|
||||
)
|
||||
})
|
||||
|
||||
it.for([
|
||||
{ daemonCreated: undefined, childCreated: 10 },
|
||||
{ daemonCreated: 5_000, childCreated: undefined }
|
||||
])(
|
||||
'keeps a child when a creation time is unknown (%o)',
|
||||
async ({ daemonCreated, childCreated }, { onTestFinished }) => {
|
||||
const daemonPid = await exitedPid()
|
||||
const daemon = { pid: daemonPid, ppid: 1, name: 'node.exe', creationTimeMs: daemonCreated }
|
||||
const child = {
|
||||
pid: 77,
|
||||
ppid: daemonPid,
|
||||
name: 'OpenConsole.exe',
|
||||
creationTimeMs: childCreated
|
||||
}
|
||||
const root = profile(daemonPid, onTestFinished)
|
||||
expect(await teardown(root, [daemon, child], [child])).toMatchObject(
|
||||
leaked(`OpenConsole.exe 77 (ppid ${daemonPid}, created ${childCreated})`)
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -45,16 +45,6 @@ function failRegistration(): void {
|
||||
}
|
||||
|
||||
describe('PluginServiceHousekeeping', () => {
|
||||
it('checks bindings and reaps healthy workers without refreshing or restarting watchers', async () => {
|
||||
const { reapIdle, refresh } = sync()
|
||||
await vi.advanceTimersByTimeAsync(180_000)
|
||||
expect(reapIdle).toHaveBeenCalledTimes(3)
|
||||
expect(refresh).not.toHaveBeenCalled()
|
||||
expect(watcher().start).toHaveBeenCalledOnce()
|
||||
expect(watcher().checkRootBindings).toHaveBeenCalledTimes(3)
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('retries failed registration using the existing maintenance interval', async () => {
|
||||
const { reapIdle, refresh } = sync()
|
||||
failRegistration()
|
||||
@@ -70,15 +60,6 @@ describe('PluginServiceHousekeeping', () => {
|
||||
expect(vi.getTimerCount()).toBe(1)
|
||||
})
|
||||
|
||||
it('retains a synchronous startup failure for the next bounded retry', async () => {
|
||||
vi.mocked(watcher().start).mockImplementation((_paths, _refresh, onWatcherError) => {
|
||||
onWatcherError?.()
|
||||
})
|
||||
const { refresh } = sync()
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
expect(refresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps unchanged missing roots idle and refreshes when their binding appears', async () => {
|
||||
const { refresh, reapIdle } = sync()
|
||||
const onWatcherError = vi.mocked(watcher().start).mock.calls[0]?.[2]
|
||||
@@ -125,20 +106,4 @@ describe('PluginServiceHousekeeping', () => {
|
||||
await Promise.resolve()
|
||||
expect(refresh).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('stops retries and releases subscriptions when disabled or disposed', () => {
|
||||
const { reapIdle, refresh } = sync()
|
||||
failRegistration()
|
||||
housekeeping.sync({ enabled: false, devPaths: ['plugin'], reapIdle, refresh })
|
||||
vi.advanceTimersByTime(120_000)
|
||||
expect(reapIdle).not.toHaveBeenCalled()
|
||||
expect(refresh).not.toHaveBeenCalled()
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
sync()
|
||||
failRegistration()
|
||||
housekeeping.dispose()
|
||||
vi.advanceTimersByTime(120_000)
|
||||
expect(refresh).not.toHaveBeenCalled()
|
||||
expect(vi.getTimerCount()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,19 +123,4 @@ describe('codex rate-limit spawn runtime pairing', () => {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves PATH alone when the CLI resolves to a bare command name', async () => {
|
||||
resolveCodexCommandMock.mockReturnValue('codex')
|
||||
const rpcChild = makeRpcChild()
|
||||
childSpawnMock.mockReturnValue(rpcChild)
|
||||
|
||||
const resultPromise = fetchCodexRateLimits()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
const spawnEnv = childSpawnMock.mock.calls[0]?.[2]?.env as NodeJS.ProcessEnv
|
||||
expect(spawnEnv.PATH).toBe(process.env.PATH)
|
||||
|
||||
rpcChild.emit('close')
|
||||
await resultPromise
|
||||
})
|
||||
})
|
||||
|
||||
@@ -49,19 +49,6 @@ describe('RuntimeFileCommands', () => {
|
||||
return dir
|
||||
}
|
||||
|
||||
it('stays inside the transport ceiling once base64-inflated', () => {
|
||||
const result = {
|
||||
content: Buffer.alloc(RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES).toString('base64'),
|
||||
isBinary: true,
|
||||
isImage: true,
|
||||
mimeType: 'image/png'
|
||||
}
|
||||
|
||||
expect(Buffer.byteLength(JSON.stringify(result), 'utf8')).toBeLessThanOrEqual(
|
||||
REMOTE_RPC_MAX_CONTENT_BYTES
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects a previewable image one byte above the cap', async () => {
|
||||
const dir = await previewFixture(RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES + 1)
|
||||
const { commands } = createRuntimeFileCommands({ path: dir })
|
||||
@@ -72,21 +59,6 @@ describe('RuntimeFileCommands', () => {
|
||||
).rejects.toThrow('file_too_large')
|
||||
})
|
||||
|
||||
it('returns full base64 for a previewable image at the cap', async () => {
|
||||
const dir = await previewFixture(RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES)
|
||||
const { commands } = createRuntimeFileCommands({ path: dir })
|
||||
resolveAuthorizedPathMock.mockImplementation(async (p: string) => p)
|
||||
|
||||
await expect(
|
||||
commands.readFileExplorerPreview('id:wt-1', 'logo.png', REMOTE_RPC_MAX_CONTENT_BYTES)
|
||||
).resolves.toEqual({
|
||||
content: Buffer.alloc(RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES, 0x61).toString('base64'),
|
||||
isBinary: true,
|
||||
isImage: true,
|
||||
mimeType: 'image/png'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps local previews above the remote cap available without a request budget', async () => {
|
||||
const size = RUNTIME_PREVIEWABLE_BINARY_MAX_BYTES + 1
|
||||
const dir = await previewFixture(size)
|
||||
|
||||
@@ -85,60 +85,4 @@ describe('a moved shell history that waited on live shells', () => {
|
||||
expect(options.worktreeHasLiveShells(TEST_WORKTREE_ID)).toBe(true)
|
||||
expect(options.worktreeHasLiveShells('repo-1::/srv/other')).toBe(false)
|
||||
})
|
||||
|
||||
it('waits when the terminal inventory does not answer', async () => {
|
||||
const installPending = vi.fn()
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...store,
|
||||
installPendingOrcadMigrationReceivedBlobs: installPending,
|
||||
hasPendingOrcadMigrationShellHistory: () => true
|
||||
})
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses: vi.fn(async () => {
|
||||
throw new Error('daemon unreachable')
|
||||
})
|
||||
})
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
await runtime.installPendingOrcadMigrationShellHistory()
|
||||
|
||||
expect(installPending).not.toHaveBeenCalled()
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('costs a start with nothing pending no inventory and keeps restored receipts', async () => {
|
||||
const installPending = vi.fn()
|
||||
const runtime = new OrcaRuntimeService({
|
||||
...store,
|
||||
installPendingOrcadMigrationReceivedBlobs: installPending,
|
||||
hasPendingOrcadMigrationShellHistory: () => false
|
||||
})
|
||||
const listProcesses = vi.fn(async () => [{ id: 'daemon-pty', cwd: '/elsewhere', title: 'zsh' }])
|
||||
runtime.setPtyController({
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null,
|
||||
listProcesses
|
||||
})
|
||||
const receipt = {
|
||||
ptyId: 'daemon-pty',
|
||||
worktreeId: TEST_WORKTREE_ID,
|
||||
terminalHandle: 'term_daemon',
|
||||
paneKey: 'tab-1:1',
|
||||
processIncarnation: 'daemon-pty:inc-1',
|
||||
hostScope: { kind: 'local' }
|
||||
}
|
||||
internalsOf(runtime).restoredOrchestrationAuthorityByPtyId.set('daemon-pty', receipt)
|
||||
|
||||
await runtime.installPendingOrcadMigrationShellHistory()
|
||||
|
||||
expect(listProcesses).not.toHaveBeenCalled()
|
||||
expect(installPending).not.toHaveBeenCalled()
|
||||
expect(internalsOf(runtime).restoredOrchestrationAuthorityByPtyId.get('daemon-pty')).toBe(
|
||||
receipt
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -47,54 +47,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('startup fork-upstream backfill', () => {
|
||||
it('keeps a renamed fork own owner avatar while recording the upstream', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const updateRepo = attachStore(runtime, [makeRepo()])
|
||||
getRepoUpstream.mockResolvedValue({ owner: 'upstream-org', repo: 'rocket' })
|
||||
getRepoSlug.mockResolvedValue({ owner: 'acme', repo: 'rocket-pro' })
|
||||
|
||||
await (runtime as unknown as BackfillInternals).repositoryForkBackfill.run()
|
||||
|
||||
expect(updateRepo).toHaveBeenCalledExactlyOnceWith(repoTarget('repo-1'), {
|
||||
upstream: { owner: 'upstream-org', repo: 'rocket' },
|
||||
repoIcon: {
|
||||
type: 'image',
|
||||
src: 'https://github.com/acme.png?size=64',
|
||||
source: 'github',
|
||||
label: 'acme/rocket-pro'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('migrates a same-name fork to the upstream owner avatar', async () => {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const updateRepo = attachStore(runtime, [
|
||||
makeRepo({
|
||||
path: '/workspace/rocket',
|
||||
repoIcon: {
|
||||
type: 'image',
|
||||
src: 'https://github.com/acme.png?size=64',
|
||||
source: 'github',
|
||||
label: 'acme/rocket'
|
||||
}
|
||||
})
|
||||
])
|
||||
getRepoUpstream.mockResolvedValue({ owner: 'upstream-org', repo: 'rocket' })
|
||||
getRepoSlug.mockResolvedValue({ owner: 'acme', repo: 'rocket' })
|
||||
|
||||
await (runtime as unknown as BackfillInternals).repositoryForkBackfill.run()
|
||||
|
||||
expect(updateRepo).toHaveBeenCalledExactlyOnceWith(repoTarget('repo-1'), {
|
||||
upstream: { owner: 'upstream-org', repo: 'rocket' },
|
||||
repoIcon: {
|
||||
type: 'image',
|
||||
src: 'https://github.com/upstream-org.png?size=64',
|
||||
source: 'github',
|
||||
label: 'upstream-org/rocket'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('skips every row whose files sit on an SSH host', async () => {
|
||||
// Unified remote ownership, including nested runtime targets, must not read this client's same-path files.
|
||||
const runtime = new OrcaRuntimeService()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { OrcaRuntimeService } from '../../../../orca-runtime'
|
||||
import type { OrchestrationDb } from '../../../../orchestration/db'
|
||||
import { exposeWorker, inspectWorkerTerminal } from './worker-observation'
|
||||
import { inspectWorkerTerminal } from './worker-observation'
|
||||
|
||||
import { exposeDispatchContext } from './dispatch-context-exposure'
|
||||
import type { DispatchContextRow, WorkerDispatchRow } from '../../../../orchestration/types'
|
||||
import type { DispatchContextRow } from '../../../../orchestration/types'
|
||||
|
||||
const DISPATCH_ID = 'ctx-worker'
|
||||
const TERMINAL_HANDLE = 'term-worker'
|
||||
@@ -28,18 +29,6 @@ function createHarness(args: {
|
||||
}
|
||||
|
||||
describe('inspectWorkerTerminal missing liveness verdict', () => {
|
||||
it('keeps a connected local worker live', async () => {
|
||||
const { runtime, db } = createHarness({
|
||||
connected: true,
|
||||
hostScope: { kind: 'local', hostId: 'local' }
|
||||
})
|
||||
|
||||
await expect(inspectWorkerTerminal(runtime, db, DISPATCH_ID)).resolves.toMatchObject({
|
||||
exact: true,
|
||||
status: 'live'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps a disconnected local worker exited', async () => {
|
||||
const { runtime, db } = createHarness({
|
||||
connected: false,
|
||||
@@ -67,40 +56,6 @@ describe('inspectWorkerTerminal missing liveness verdict', () => {
|
||||
})
|
||||
|
||||
describe('worker-show receipt shape', () => {
|
||||
it('parses the JSON columns once and emits one casing', () => {
|
||||
const exposed = exposeWorker({
|
||||
dispatch_id: DISPATCH_ID,
|
||||
runtime_epoch: 'epoch-1',
|
||||
state: 'ready',
|
||||
stage: 'input_accepted',
|
||||
worktree_id: 'repo::/tmp/wt',
|
||||
agent_terminal_handle: TERMINAL_HANDLE,
|
||||
setup_state: 'ran',
|
||||
effects: '[{"kind":"setup"}]',
|
||||
residual_resources: '["res-1"]',
|
||||
start_options: '{"agent":"codex"}',
|
||||
last_error: null,
|
||||
created_at: 'now',
|
||||
updated_at: 'now'
|
||||
} as WorkerDispatchRow)
|
||||
|
||||
expect(exposed).toEqual({
|
||||
dispatchId: DISPATCH_ID,
|
||||
runtimeEpoch: 'epoch-1',
|
||||
state: 'ready',
|
||||
stage: 'input_accepted',
|
||||
worktreeId: 'repo::/tmp/wt',
|
||||
agentTerminalHandle: TERMINAL_HANDLE,
|
||||
setupState: 'ran',
|
||||
effects: [{ kind: 'setup' }],
|
||||
residualResources: ['res-1'],
|
||||
startOptions: { agent: 'codex' },
|
||||
lastError: null,
|
||||
createdAt: 'now',
|
||||
updatedAt: 'now'
|
||||
})
|
||||
})
|
||||
|
||||
it('parses host_scope and withholds authority hashes from the dispatch row', () => {
|
||||
const exposed = exposeDispatchContext({
|
||||
id: DISPATCH_ID,
|
||||
@@ -123,25 +78,4 @@ describe('worker-show receipt shape', () => {
|
||||
// Retain only the legacy dispatch spelling older paired CLIs still print beside camelCase worker.
|
||||
expect(Object.keys(exposed).filter((key) => key.includes('_'))).toEqual(['task_id'])
|
||||
})
|
||||
|
||||
// Mixed-version CLIs still print these legacy fields (cli/handlers/orchestration/worker-observation-handlers.ts).
|
||||
it('keeps every field an older paired CLI prints', () => {
|
||||
const dispatch = exposeDispatchContext({
|
||||
id: DISPATCH_ID,
|
||||
run_id: 'run-1',
|
||||
task_id: 'task-1',
|
||||
status: 'dispatched'
|
||||
} as DispatchContextRow)
|
||||
|
||||
expect(dispatch).toMatchObject({ id: DISPATCH_ID, task_id: 'task-1', status: 'dispatched' })
|
||||
expect(
|
||||
exposeWorker({
|
||||
state: 'ready',
|
||||
stage: 'input_accepted',
|
||||
effects: '[]',
|
||||
residual_resources: '[]',
|
||||
start_options: '{}'
|
||||
} as WorkerDispatchRow)
|
||||
).toMatchObject({ state: 'ready', stage: 'input_accepted' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,67 +3,6 @@ import { eraseRpcMethods, type RpcContext } from '../core'
|
||||
import { TERMINAL_METHODS } from './terminal'
|
||||
|
||||
describe('terminal.create RPC idempotency', () => {
|
||||
it('scopes the mutation key to the authenticated paired device and worktree', async () => {
|
||||
const terminal = { handle: 'terminal-1', worktreeId: 'worktree-1', title: null }
|
||||
const createTerminal = vi.fn(async () => terminal)
|
||||
const dedupeTerminalCreate = vi.fn(
|
||||
async (
|
||||
_clientIdentity: string,
|
||||
_worktree: string | undefined,
|
||||
_mutationId: string | undefined,
|
||||
_reconcileExisting: boolean,
|
||||
run: (worktree: string | undefined, handle: string | undefined) => Promise<typeof terminal>
|
||||
) => run('id:worktree-1', 'term_stable')
|
||||
)
|
||||
const method = eraseRpcMethods(TERMINAL_METHODS).find(
|
||||
(candidate) => candidate.name === 'terminal.create'
|
||||
)
|
||||
if (!method) {
|
||||
throw new Error('terminal.create method missing')
|
||||
}
|
||||
|
||||
const result = await method.handler(
|
||||
{
|
||||
worktree: 'id:worktree-1',
|
||||
clientMutationId: 'mutation-1',
|
||||
command: 'pwsh',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-1',
|
||||
transcriptPath: 'C:\\Users\\example\\.codex\\sessions\\rollout.jsonl'
|
||||
}
|
||||
},
|
||||
{
|
||||
runtime: { createTerminal, dedupeTerminalCreate },
|
||||
pairedDeviceId: 'device-a',
|
||||
clientId: 'bearer-token'
|
||||
} as unknown as RpcContext,
|
||||
vi.fn()
|
||||
)
|
||||
|
||||
expect(dedupeTerminalCreate).toHaveBeenCalledWith(
|
||||
'device-a',
|
||||
'id:worktree-1',
|
||||
'mutation-1',
|
||||
false,
|
||||
expect.any(Function),
|
||||
{ wslUserAct: true }
|
||||
)
|
||||
expect(createTerminal).toHaveBeenCalledWith(
|
||||
'id:worktree-1',
|
||||
expect.objectContaining({
|
||||
command: 'pwsh',
|
||||
preAllocatedHandle: 'term_stable',
|
||||
resumeProviderSession: {
|
||||
key: 'session_id',
|
||||
id: 'session-1',
|
||||
transcriptPath: 'C:\\Users\\example\\.codex\\sessions\\rollout.jsonl'
|
||||
}
|
||||
})
|
||||
)
|
||||
expect(result).toEqual({ terminal })
|
||||
})
|
||||
|
||||
it('does not let a paired focused create navigate the host by default', async () => {
|
||||
const terminal = { handle: 'terminal-focused', worktreeId: 'worktree-1', title: null }
|
||||
const createTerminal = vi.fn(async () => terminal)
|
||||
|
||||
@@ -55,22 +55,6 @@ const notFound = (): never => {
|
||||
|
||||
// Why: only the desktop that owns a run can verify its dispatch token (#34433).
|
||||
describe('worktree.create for another owner’s automation run', () => {
|
||||
it('creates a plain workspace for a dispatch token it cannot verify', async () => {
|
||||
const { response, createManagedWorktree } = createOnHost(notFound, {
|
||||
automationProvenanceRequest: {
|
||||
automationId: 'desktop-automation',
|
||||
automationRunId: 'desktop-run',
|
||||
dispatchToken: 'desktop-token',
|
||||
createRequestId: 'create-request-desktop'
|
||||
}
|
||||
})
|
||||
|
||||
await expect(response).resolves.toMatchObject({ ok: true })
|
||||
expect(createManagedWorktree).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ automationProvenance: expect.anything() })
|
||||
)
|
||||
})
|
||||
|
||||
// Invariant: a client-origin label never grants ownership, sleep authority or create policy here.
|
||||
it('records the owner’s run as a client-origin label without automation create policy', async () => {
|
||||
const { response, createManagedWorktree } = createOnHost(notFound, {
|
||||
@@ -103,32 +87,6 @@ describe('worktree.create for another owner’s automation run', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('drops an oversized label and still creates the workspace', async () => {
|
||||
// Why fresh modules: the warning is once per process, so this create must be the first to drop one.
|
||||
vi.resetModules()
|
||||
const { WORKTREE_METHODS: freshMethods } = await import('./worktree')
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const { response, createManagedWorktree } = createOnHost(
|
||||
notFound,
|
||||
{
|
||||
clientAutomationProvenance: {
|
||||
...CLIENT_PROVENANCE,
|
||||
automationNameSnapshot: 'x'.repeat(1025)
|
||||
}
|
||||
},
|
||||
freshMethods
|
||||
)
|
||||
|
||||
await expect(response).resolves.toMatchObject({ ok: true })
|
||||
expect(createManagedWorktree).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({ automationProvenance: expect.anything() })
|
||||
)
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining('dropped a malformed client automation label')
|
||||
)
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('surfaces an automation store failure', async () => {
|
||||
const { response, createManagedWorktree } = createOnHost(
|
||||
() => {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
TERMINAL_MULTIPLEX_MAX_PENDING_PTY_WAITS_PER_CONNECTION,
|
||||
TERMINAL_MULTIPLEX_MAX_QUEUED_PTY_WAITS_PER_CONNECTION
|
||||
} from '../../../shared/terminal-multiplex-flow-control'
|
||||
import { createMultiplexPtyWaitAdmission } from './methods/terminal/terminal-multiplex-pty-wait-admission'
|
||||
|
||||
import { startDesktopMultiplexSubscribe } from './terminal-multiplex-test-harness'
|
||||
|
||||
function startPendingPtyHarness() {
|
||||
@@ -122,20 +122,3 @@ describe('terminal multiplex PTY wait queue', () => {
|
||||
await harness.dispatchPromise
|
||||
})
|
||||
})
|
||||
|
||||
describe('createMultiplexPtyWaitAdmission', () => {
|
||||
it('admits FIFO, skips aborted waiters, and reports room against both bounds', async () => {
|
||||
const admission = createMultiplexPtyWaitAdmission(1, 2)
|
||||
const first = await admission.acquire(new AbortController().signal)
|
||||
expect(first).toBe(true)
|
||||
const aborted = new AbortController()
|
||||
const second = admission.acquire(aborted.signal)
|
||||
const third = admission.acquire(new AbortController().signal)
|
||||
expect(admission.hasRoom()).toBe(false)
|
||||
aborted.abort()
|
||||
await expect(second).resolves.toBe(false)
|
||||
expect(admission.hasRoom()).toBe(true)
|
||||
admission.release()
|
||||
await expect(third).resolves.toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -96,22 +96,6 @@ describe.skipIf(process.platform === 'win32')(
|
||||
expect((await launchWith(settings)).command).toBe(join(privateBin, 'opencode'))
|
||||
})
|
||||
|
||||
it('keeps a refused release first on PATH on the terminal chat when nothing points elsewhere', async () => {
|
||||
const settings = { ...base, agentDefaultEnv: {} }
|
||||
expect(
|
||||
await structuredAgentSupportsLaunch('opencode', workspace, runtimeWith(settings))
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('runs a stable 2.x first on PATH in the structured chat', async () => {
|
||||
await fakeOpencode(shellBin, '2.0.21')
|
||||
const settings = { ...base, agentDefaultEnv: {} }
|
||||
expect(
|
||||
await structuredAgentSupportsLaunch('opencode', workspace, runtimeWith(settings))
|
||||
).toBe(true)
|
||||
expect((await launchWith(settings)).command).toBe(join(shellBin, 'opencode'))
|
||||
})
|
||||
|
||||
it('checks and launches the binary the Command setting names', async () => {
|
||||
const settings = {
|
||||
...base,
|
||||
|
||||
@@ -62,25 +62,6 @@ it('shares one root read across six concurrent name filters', async () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('reuses the raw snapshot for a new name and invalidates it on mutation', async () => {
|
||||
await fixture(async (root) => {
|
||||
const args = { homeDir: root, repos: [], includeCwd: false, sourceKinds: ['home' as const] }
|
||||
observed.opens = 0
|
||||
await repair.discoverSkills({ ...args, names: ['skill-0'] })
|
||||
const next = await repair.discoverSkills({ ...args, names: ['skill-47'] })
|
||||
expect(next.skills.map((skill) => skill.name)).toEqual(['skill-47'])
|
||||
expect(observed.opens).toBe(48)
|
||||
await writeFile(
|
||||
join(root, '.agents', 'skills', 'skill-47', 'SKILL.md'),
|
||||
'---\nname: renamed\n---\n'
|
||||
)
|
||||
repair.clearSkillRootScanCache()
|
||||
const updated = await repair.discoverSkills({ ...args, names: ['renamed'] })
|
||||
expect(updated.skills.map((skill) => skill.name)).toEqual(['renamed'])
|
||||
expect(observed.opens).toBe(96)
|
||||
})
|
||||
})
|
||||
|
||||
it('retains a newly requested name when its previously observed root becomes unavailable', async () => {
|
||||
await fixture(async (root) => {
|
||||
const args = { homeDir: root, repos: [], includeCwd: false, sourceKinds: ['home' as const] }
|
||||
@@ -108,21 +89,6 @@ it('retains a newly requested name when its previously observed root becomes una
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps simultaneous forced refreshes independent', async () => {
|
||||
await fixture(async (root) => {
|
||||
const args = { homeDir: root, repos: [], includeCwd: false, sourceKinds: ['home' as const] }
|
||||
await repair.discoverSkills({ ...args, names: ['skill-0'] })
|
||||
observed.opens = 0
|
||||
const results = await Promise.all(
|
||||
[0, 1].map((index) =>
|
||||
repair.discoverSkills({ ...args, names: [`skill-${index}`], refresh: true })
|
||||
)
|
||||
)
|
||||
expect(results.map((result) => result.skills[0]?.name)).toEqual(['skill-0', 'skill-1'])
|
||||
expect(observed.opens).toBe(96)
|
||||
})
|
||||
})
|
||||
|
||||
it('filters aliases before deduplication so excluded bundled roots cannot own home results', async () => {
|
||||
await fixture(async (root) => {
|
||||
const bundled = join(root, '.codex', 'skills', '.system', 'bundle')
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { OrcadActivationRecord } from '../managed-server/activation/orcad-activation-record'
|
||||
import type { OrcadSlotOptions } from './orcad-recovery-slot'
|
||||
import { getRemoteHostPlatform } from './ssh-remote-platform'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
wake: vi.fn(),
|
||||
readRecord: vi.fn(),
|
||||
probe: vi.fn(),
|
||||
launch: vi.fn(),
|
||||
fenceRefusal: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../managed-server/activation/orcad-activation-fence-hold', () => ({
|
||||
orcadActivationFenceRefusal: mocks.fenceRefusal
|
||||
}))
|
||||
|
||||
vi.mock('../managed-server/lifecycle/orcad-managed-wake', () => ({
|
||||
wakeStoppedManagedOrcad: mocks.wake
|
||||
}))
|
||||
vi.mock('../managed-server/activation/orcad-activation-record-store', () => ({
|
||||
readOrcadActivationRecord: mocks.readRecord
|
||||
}))
|
||||
vi.mock('../managed-server/install/orcad-remote-build-hash', () => ({
|
||||
readRemoteOrcadBuildHash: async () => 'hash'
|
||||
}))
|
||||
vi.mock('./orcad-active-readiness', async (importOriginal) => ({
|
||||
...(await importOriginal<Record<string, unknown>>()),
|
||||
probeActiveOrcadReadiness: mocks.probe,
|
||||
launchOrcadSlotAndAwaitReadiness: mocks.launch
|
||||
}))
|
||||
|
||||
const { adoptRunningOrcadVersion } = await import('./orcad-running-version-adoption')
|
||||
|
||||
const LOCAL = '1.5.0+new'
|
||||
const RUNNING = '1.4.0+old'
|
||||
const READINESS = { runtimeId: 'r' }
|
||||
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: every remote step is mocked, so only the paths are read.
|
||||
const slot = {
|
||||
host: getRemoteHostPlatform('linux-x64'),
|
||||
remoteHome: '/home/u',
|
||||
userDataDir: '/home/u/.orca'
|
||||
} as OrcadSlotOptions
|
||||
|
||||
const record = (active: string | null): OrcadActivationRecord => ({
|
||||
schemaVersion: 1,
|
||||
active,
|
||||
previous: null,
|
||||
activatedAt: null,
|
||||
snapshot: null
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks()
|
||||
mocks.readRecord.mockResolvedValue(record(RUNNING))
|
||||
mocks.probe.mockResolvedValue(READINESS)
|
||||
})
|
||||
|
||||
describe('adoptRunningOrcadVersion', () => {
|
||||
it('leaves an empty host, or one on this build, to the deploy', async () => {
|
||||
await expect(adoptRunningOrcadVersion(slot, record(null), LOCAL)).resolves.toBeNull()
|
||||
await expect(adoptRunningOrcadVersion(slot, record(LOCAL), LOCAL)).resolves.toBeNull()
|
||||
expect(mocks.wake).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('adopts the serving build, proven against its own installed bytes', async () => {
|
||||
mocks.wake.mockResolvedValue({ outcome: 'serving' })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toMatchObject({
|
||||
version: RUNNING,
|
||||
readiness: READINESS
|
||||
})
|
||||
expect(mocks.probe).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ remoteInstallDir: expect.stringContaining(RUNNING) }),
|
||||
{ buildHash: 'hash', fullVersion: RUNNING }
|
||||
)
|
||||
expect(mocks.launch).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('adopts a stopped build the wake restarted', async () => {
|
||||
mocks.wake.mockResolvedValue({ outcome: 'started', readiness: READINESS })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toMatchObject({
|
||||
version: RUNNING
|
||||
})
|
||||
})
|
||||
|
||||
it('defers to the deploy when the host was stopped, or moved to this build meanwhile', async () => {
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'not-activated' })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toBeNull()
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'serving' })
|
||||
mocks.readRecord.mockResolvedValueOnce(record(LOCAL))
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toBeNull()
|
||||
expect(mocks.probe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// #28833: peer startup must defer adoption; an unpaired deploy cannot census the running host.
|
||||
it('waits on a fence another run holds instead of deploying over an uncounted host', async () => {
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'fenced' })
|
||||
mocks.fenceRefusal.mockResolvedValueOnce({
|
||||
code: 'orcad_activation_fence_busy',
|
||||
reason: 'busy'
|
||||
})
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toEqual({
|
||||
outcome: 'deferred',
|
||||
candidateVersion: LOCAL,
|
||||
code: 'orcad_activation_fence_busy',
|
||||
reason: 'busy',
|
||||
forceable: false
|
||||
})
|
||||
expect(mocks.probe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('retries the wake once after clearing an abandoned fence', async () => {
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'fenced' })
|
||||
mocks.fenceRefusal.mockResolvedValueOnce({ code: 'c', reason: 'cleared', cleared: true })
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'started', readiness: READINESS })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toMatchObject({
|
||||
version: RUNNING
|
||||
})
|
||||
mocks.wake.mockResolvedValue({ outcome: 'fenced' })
|
||||
mocks.fenceRefusal.mockResolvedValue({ code: 'c', reason: 'cleared', cleared: true })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).resolves.toMatchObject({
|
||||
outcome: 'deferred'
|
||||
})
|
||||
expect(mocks.wake).toHaveBeenCalledTimes(4)
|
||||
})
|
||||
|
||||
it('reports an unverifiable or unrecoverable host without touching it', async () => {
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'unverifiable' })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).rejects.toMatchObject({
|
||||
verdict: 'unverifiable'
|
||||
})
|
||||
mocks.wake.mockResolvedValueOnce({ outcome: 'recovery-refused', reason: 'Recover it first.' })
|
||||
await expect(adoptRunningOrcadVersion(slot, record(RUNNING), LOCAL)).rejects.toThrow(
|
||||
'Recover it first.'
|
||||
)
|
||||
expect(mocks.launch).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -31,23 +31,6 @@ function fakeBase(width: number, height: number): FakeImage {
|
||||
const AMBER = { b: 0x0b, g: 0x9e, r: 0xf5 }
|
||||
|
||||
describe('composeTrayAttentionIcon', () => {
|
||||
it('returns the base unchanged when it has no pixels', () => {
|
||||
const base = { getSize: () => ({ width: 0, height: 0 }), toBitmap: () => Buffer.alloc(0) }
|
||||
|
||||
expect(composeTrayAttentionIcon(base as never)).toBe(base)
|
||||
expect(createFromBitmapMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('builds a new image of the same size', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const result = composeTrayAttentionIcon(fakeBase(16, 16) as never)
|
||||
|
||||
expect(createFromBitmapMock).toHaveBeenCalledTimes(1)
|
||||
const [, options] = createFromBitmapMock.mock.calls[0]
|
||||
expect(options).toEqual({ width: 16, height: 16 })
|
||||
expect(result).toMatchObject({ __image: true, width: 16, height: 16 })
|
||||
})
|
||||
|
||||
it('paints an amber dot in the top-right corner and leaves the rest untouched', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const width = 16
|
||||
@@ -86,33 +69,6 @@ describe('composeTrayAttentionIcon', () => {
|
||||
})
|
||||
|
||||
describe('tintTrayTemplateForAttention', () => {
|
||||
it('preserves alpha while selecting literal black pixels for a light menu bar', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const bitmap = Buffer.from([0x22, 0x33, 0x44, 0x80, 0xaa, 0xbb, 0xcc, 0x00])
|
||||
const base = {
|
||||
getSize: () => ({ width: 2, height: 1 }),
|
||||
toBitmap: () => bitmap
|
||||
}
|
||||
|
||||
tintTrayTemplateForAttention(base as never, false)
|
||||
|
||||
expect(createFromBitmapMock.mock.calls[0][0]).toEqual(
|
||||
Buffer.from([0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00])
|
||||
)
|
||||
})
|
||||
|
||||
it('selects literal white pixels for a dark menu bar', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const base = {
|
||||
getSize: () => ({ width: 1, height: 1 }),
|
||||
toBitmap: () => Buffer.from([0x00, 0x00, 0x00, 0xff])
|
||||
}
|
||||
|
||||
tintTrayTemplateForAttention(base as never, true)
|
||||
|
||||
expect(createFromBitmapMock.mock.calls[0][0]).toEqual(Buffer.from([0xff, 0xff, 0xff, 0xff]))
|
||||
})
|
||||
|
||||
it('keeps white edge pixels premultiplied-valid at partial alpha', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const base = {
|
||||
@@ -125,18 +81,4 @@ describe('tintTrayTemplateForAttention', () => {
|
||||
// Premultiplied alpha requires 0x80 channels for half-covered white, not 0xff.
|
||||
expect(createFromBitmapMock.mock.calls[0][0]).toEqual(Buffer.from([0x80, 0x80, 0x80, 0x80]))
|
||||
})
|
||||
|
||||
it('reads and sizes the requested scale factor', () => {
|
||||
createFromBitmapMock.mockClear()
|
||||
const toBitmap = vi.fn(() => Buffer.from([0x00, 0x00, 0x00, 0xff]))
|
||||
const base = {
|
||||
getSize: () => ({ width: 1, height: 1 }),
|
||||
toBitmap
|
||||
}
|
||||
|
||||
tintTrayTemplateForAttention(base as never, true, 2)
|
||||
|
||||
expect(toBitmap).toHaveBeenCalledWith({ scaleFactor: 2 })
|
||||
expect(createFromBitmapMock.mock.calls[0][1]).toEqual({ width: 2, height: 2 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,7 +8,6 @@ const {
|
||||
autoUpdaterMock,
|
||||
getLinuxRootPackageTypeMock,
|
||||
isExternallyManagedLinuxInstallMock,
|
||||
recordUpdaterLifecycleMock,
|
||||
fetchNewerReleaseTagsMock,
|
||||
moduleFactories,
|
||||
resetUpdaterMocks
|
||||
@@ -64,29 +63,6 @@ describe('updater externally managed Linux installs', () => {
|
||||
return send.mock.calls.findLast(([channel]) => channel === 'updater:status')?.[1]
|
||||
}
|
||||
|
||||
it('still reports the available release so the user can update through their distribution', async () => {
|
||||
const { send, updater } = await startUpdater({ packageType: 'deb', externallyManaged: true })
|
||||
updater.checkForUpdatesFromMenu()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(lastStatus(send)).toEqual({
|
||||
state: 'available',
|
||||
version: '1.0.61',
|
||||
changelog: null,
|
||||
externallyManaged: true
|
||||
})
|
||||
})
|
||||
|
||||
it('does not flag a real deb host', async () => {
|
||||
const { send, updater } = await startUpdater({ packageType: 'deb', externallyManaged: false })
|
||||
updater.checkForUpdatesFromMenu()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
const status = lastStatus(send)
|
||||
expect(status).toEqual({ state: 'available', version: '1.0.61', changelog: null })
|
||||
expect(status && 'externallyManaged' in status).toBe(false)
|
||||
})
|
||||
|
||||
it('refuses the download instead of spending it on a package it can never install', async () => {
|
||||
const { send, updater } = await startUpdater({ packageType: 'deb', externallyManaged: true })
|
||||
updater.checkForUpdatesFromMenu()
|
||||
@@ -105,19 +81,6 @@ describe('updater externally managed Linux installs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('records the blocked download for field diagnosis', async () => {
|
||||
const { updater } = await startUpdater({ packageType: 'deb', externallyManaged: true })
|
||||
updater.checkForUpdatesFromMenu()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
updater.downloadUpdate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(recordUpdaterLifecycleMock).toHaveBeenCalledWith(
|
||||
'linux_package_externally_managed_download_blocked',
|
||||
{ version: '1.0.61' }
|
||||
)
|
||||
})
|
||||
|
||||
it('leaves an ordinary deb host able to download', async () => {
|
||||
const { updater } = await startUpdater({ packageType: 'deb', externallyManaged: false })
|
||||
updater.checkForUpdatesFromMenu()
|
||||
@@ -129,16 +92,4 @@ describe('updater externally managed Linux installs', () => {
|
||||
|
||||
expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves an AppImage host able to download', async () => {
|
||||
const { updater } = await startUpdater({ packageType: null, externallyManaged: false })
|
||||
updater.checkForUpdatesFromMenu()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
autoUpdaterMock.downloadUpdate.mockResolvedValue([])
|
||||
|
||||
updater.downloadUpdate()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
|
||||
expect(autoUpdaterMock.downloadUpdate).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -60,25 +60,6 @@ describe('requestTerminalTabCloseFromRenderer', () => {
|
||||
await expect(pending).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('propagates renderer cancellation instead of reporting success', async () => {
|
||||
const { requestTerminalTabCloseFromRenderer } =
|
||||
await import('./terminal-tab-close-request-relay')
|
||||
const webContents = { isDestroyed: () => false, send: vi.fn() }
|
||||
const pending = requestTerminalTabCloseFromRenderer(
|
||||
{ isDestroyed: () => false, webContents } as never,
|
||||
'tab-pinned'
|
||||
)
|
||||
const request = webContents.send.mock.calls[0]?.[1] as { requestId: string }
|
||||
|
||||
ipcEmitter.emit(
|
||||
'ui:terminalTabCloseResponse',
|
||||
{ sender: webContents },
|
||||
{ requestId: request.requestId, error: 'terminal_tab_pinned' }
|
||||
)
|
||||
|
||||
await expect(pending).rejects.toThrow('terminal_tab_pinned')
|
||||
})
|
||||
|
||||
it('rejects and cleans up when the BrowserWindow closes and webContents becomes unavailable', async () => {
|
||||
const { requestTerminalTabCloseFromRenderer } =
|
||||
await import('./terminal-tab-close-request-relay')
|
||||
|
||||
@@ -73,35 +73,6 @@ describe('the guest process table', () => {
|
||||
expect(readWslGuestPaneShellCwd(twoRoots ?? [], KEY)).toBeNull()
|
||||
})
|
||||
|
||||
it('never starts a distro that is not running', async () => {
|
||||
isWslDistroRunning.mockResolvedValue(false)
|
||||
expect(await readWslGuestPaneProcesses('Ubuntu', 3_000)).toBeNull()
|
||||
expect(runWslProcess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips the running check for a live pane whose distro was last seen running, settled', async () => {
|
||||
wasWslDistroLastSeenRunningSettled.mockReturnValue(true)
|
||||
runWslProcess.mockResolvedValue({
|
||||
environmentResolved: true,
|
||||
code: 0,
|
||||
stdout: fenced(),
|
||||
stderr: '',
|
||||
timedOut: false
|
||||
})
|
||||
expect(await readWslGuestPaneProcesses('Ubuntu', 3_000, { running: true })).toEqual([])
|
||||
expect(wasWslDistroLastSeenRunningSettled).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(isWslDistroRunning).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Review of #33398: a session the client still holds as live may be in a distro terminated since.
|
||||
it('still checks a live pane whose distro was not last seen running after any stop settled', async () => {
|
||||
wasWslDistroLastSeenRunningSettled.mockReturnValue(false)
|
||||
isWslDistroRunning.mockResolvedValue(false)
|
||||
expect(await readWslGuestPaneProcesses('Ubuntu', 3_000, { running: true })).toBeNull()
|
||||
expect(isWslDistroRunning).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(runWslProcess).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('runs one fixed script with no login shell, shared by callers asking at once', async () => {
|
||||
runWslProcess.mockResolvedValue({
|
||||
environmentResolved: true,
|
||||
@@ -125,17 +96,4 @@ describe('the guest process table', () => {
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('answers null when the read fails or times out', async () => {
|
||||
runWslProcess.mockResolvedValue({
|
||||
environmentResolved: true,
|
||||
code: null,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
timedOut: true
|
||||
})
|
||||
expect(await readWslGuestPaneProcesses('Ubuntu', 3_000)).toBeNull()
|
||||
runWslProcess.mockRejectedValue(new Error('wsl.exe missing'))
|
||||
expect(await readWslGuestPaneProcesses('Ubuntu', 3_000)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ORCAD_HOST_NEWER_DEFERRAL_CODE } from '../../shared/wsl-host-types'
|
||||
|
||||
import type { OrcadActivationRecord } from '../managed-server/activation/orcad-activation-record'
|
||||
import { recordOrcadUpdateFailedThisLaunch } from '../managed-server/lifecycle/orcad-managed-update-failure-memory'
|
||||
import type { ServeReadiness } from '../server/serve-readiness'
|
||||
import { ORCAD_ACTIVATION_POLICY_REFUSED_CODE } from '../ssh/orcad-installed-activation'
|
||||
|
||||
import type { OrcadSlotOptions } from '../ssh/orcad-recovery-slot'
|
||||
import { getRemoteHostPlatform } from '../ssh/ssh-remote-platform'
|
||||
import { admitWslOrcadUpdate, wslOrcadDeferralCode } from './wsl-orcad-update-admission'
|
||||
import { admitWslOrcadUpdate } from './wsl-orcad-update-admission'
|
||||
|
||||
const { adoptRunningOrcadVersion, readOrcadActivationRecord } = vi.hoisted(() => ({
|
||||
adoptRunningOrcadVersion: vi.fn(),
|
||||
@@ -86,38 +86,11 @@ describe('WSL orcad admission across desktop and host versions', () => {
|
||||
}
|
||||
expect(adoptRunningOrcadVersion).toHaveBeenCalledWith(slot, running(NEWER, '1.6.0'), OLDER)
|
||||
})
|
||||
|
||||
it('deploys over it only when the user forces the update', async () => {
|
||||
expect(await admit(running(NEWER, '1.6.0'), olderDesktop, { intent: 'force' })).toEqual({
|
||||
action: 'deploy',
|
||||
record: running(NEWER, '1.6.0')
|
||||
})
|
||||
expect(adoptRunningOrcadVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('names a policy refusal over the newer host, which Update cannot pass', async () => {
|
||||
readOrcadActivationRecord.mockResolvedValue(running(NEWER, '1.6.0'))
|
||||
expect(
|
||||
await wslOrcadDeferralCode(
|
||||
slot,
|
||||
{ code: ORCAD_ACTIVATION_POLICY_REFUSED_CODE, fullVersion: OLDER },
|
||||
'1.5.0'
|
||||
)
|
||||
).toBe(ORCAD_HOST_NEWER_DEFERRAL_CODE)
|
||||
})
|
||||
})
|
||||
|
||||
describe('a newer desktop meets the orcad an older desktop deployed', () => {
|
||||
const newerDesktop = { candidateVersion: NEWER, appVersion: '1.6.0' }
|
||||
|
||||
it('updates it on connect', async () => {
|
||||
expect(await admit(running(OLDER, '1.5.0'), newerDesktop)).toEqual({
|
||||
action: 'deploy',
|
||||
record: running(OLDER, '1.5.0')
|
||||
})
|
||||
expect(adoptRunningOrcadVersion).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the older orcad serving after a failed update, and offers Force', async () => {
|
||||
const host = { kind: 'wsl' as const, id: 'Ubuntu-failed' }
|
||||
recordOrcadUpdateFailedThisLaunch(host, { code: 'orcad_activation_failed', reason: 'boom' })
|
||||
@@ -134,10 +107,5 @@ describe('WSL orcad admission across desktop and host versions', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('treats an orcad from before app provenance as updatable, not as newer', async () => {
|
||||
const { activeAppVersion: _omitted, ...legacy } = running(OLDER, '1.5.0')
|
||||
expect(await admit(legacy, newerDesktop)).toEqual({ action: 'deploy', record: legacy })
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -64,77 +64,6 @@ describe('installBrowserExtensionInertApis', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves storage alone when the extension has no storage permission', () => {
|
||||
const realm = { chrome: { runtime: { getManifest: () => ({}) } } }
|
||||
runInNewContext(`(${installBrowserExtensionInertApis.toString()})('page')`, {
|
||||
globalThis: realm,
|
||||
Promise
|
||||
})
|
||||
expect(member(realm.chrome, 'storage')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('gives content scripts chrome.extension.inIncognitoContext and dom.openOrClosedShadowRoot', () => {
|
||||
const { chrome } = install('content-script')
|
||||
expect(member(chrome, 'extension.inIncognitoContext')).toBe(false)
|
||||
const shadowRoot = { mode: 'open' }
|
||||
expect(call(chrome, 'dom.openOrClosedShadowRoot', { shadowRoot })).toBe(shadowRoot)
|
||||
expect(call(chrome, 'dom.openOrClosedShadowRoot', { shadowRoot: null })).toBeNull()
|
||||
expect(member(chrome, 'extension.getViews')).toBeUndefined()
|
||||
expect(member(chrome, 'runtime.onPerformanceWarning')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('adds getViews to pages only, and inert worker and page APIs on chrome and browser', async () => {
|
||||
const page = install('page', {}, { extension: { getBackgroundPage: () => null } })
|
||||
expect(call(page.chrome, 'extension.getViews', { type: 'popup' })).toEqual([])
|
||||
expect(typeof member(page.chrome, 'extension.getBackgroundPage')).toBe('function')
|
||||
|
||||
const { chrome, browser } = install('worker', {}, { permissions: {} })
|
||||
expect(member(chrome, 'extension.getViews')).toBeUndefined()
|
||||
expect(member(chrome, 'dom')).toBeUndefined()
|
||||
for (const api of [chrome, browser]) {
|
||||
const onWarning = member(api, 'runtime.onPerformanceWarning')
|
||||
const listener = vi.fn()
|
||||
call(api, 'runtime.onPerformanceWarning.addListener', listener)
|
||||
expect(call(api, 'runtime.onPerformanceWarning.hasListener', listener)).toBe(true)
|
||||
expect(onWarning).toBe(member(chrome, 'runtime.onPerformanceWarning'))
|
||||
await expect(call(api, 'permissions.addHostAccessRequest', { tabId: 1 })).resolves.toBe(
|
||||
undefined
|
||||
)
|
||||
await expect(call(api, 'permissions.removeHostAccessRequest', { tabId: 1 })).resolves.toBe(
|
||||
undefined
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('installs omnibox for a manifest omnibox key and fontSettings for its permission', async () => {
|
||||
const without = install('worker')
|
||||
expect(without.chrome.omnibox).toBeUndefined()
|
||||
expect(without.chrome.fontSettings).toBeUndefined()
|
||||
|
||||
const { chrome } = install('page', {
|
||||
omnibox: { keyword: 'lp' },
|
||||
permissions: ['fontSettings']
|
||||
})
|
||||
expect(typeof member(chrome, 'omnibox.onInputEntered.addListener')).toBe('function')
|
||||
expect(member(chrome, 'omnibox.OnInputEnteredDisposition.NEW_FOREGROUND_TAB')).toBe(
|
||||
'newForegroundTab'
|
||||
)
|
||||
const suggested = await new Promise((resolve) =>
|
||||
call(chrome, 'omnibox.setDefaultSuggestion', { description: 'Search' }, () =>
|
||||
resolve('called')
|
||||
)
|
||||
)
|
||||
expect(suggested).toBe('called')
|
||||
const fonts = await new Promise((resolve) => call(chrome, 'fontSettings.getFontList', resolve))
|
||||
expect(fonts).toContainEqual({ fontId: 'sans-serif', displayName: 'sans-serif' })
|
||||
await expect(
|
||||
call(chrome, 'fontSettings.getFont', { genericFamily: 'standard' })
|
||||
).resolves.toEqual({ fontId: '', levelOfControl: 'not_controllable' })
|
||||
await expect(
|
||||
call(chrome, 'fontSettings.setFont', { genericFamily: 'standard', fontId: 'serif' })
|
||||
).resolves.toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps members Electron already provides', () => {
|
||||
const native = vi.fn()
|
||||
const { chrome } = install('page', {}, { extension: { getViews: native } })
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { performContextualTourStepAction } from './contextual-tour-step-actions'
|
||||
|
||||
describe('performContextualTourStepAction', () => {
|
||||
it('opens Tasks after detaching the terminal-owned tour source', () => {
|
||||
const finishTour = vi.fn()
|
||||
const advanceContextualTour = vi.fn()
|
||||
const detachContextualTourSource = vi.fn()
|
||||
const openTaskPage = vi.fn()
|
||||
|
||||
performContextualTourStepAction({
|
||||
action: { kind: 'open-tasks', label: 'Show tasks' },
|
||||
activeTabId: 'tab-1',
|
||||
isLastStep: false,
|
||||
finishTour,
|
||||
advanceContextualTour,
|
||||
detachContextualTourSource,
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage,
|
||||
openModal: vi.fn(),
|
||||
openClientHostedBrowserSettings: vi.fn(),
|
||||
openWorkspaceComposer: vi.fn(),
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: vi.fn()
|
||||
})
|
||||
|
||||
expect(detachContextualTourSource).toHaveBeenCalledTimes(1)
|
||||
expect(openTaskPage).toHaveBeenCalledTimes(1)
|
||||
expect(advanceContextualTour).toHaveBeenCalledTimes(1)
|
||||
expect(finishTour).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('dispatches the terminal-pane split action against the active tab', () => {
|
||||
const dispatchTerminalPaneSplit = vi.fn()
|
||||
|
||||
performContextualTourStepAction({
|
||||
action: { kind: 'split-terminal-pane', label: 'Split terminal' },
|
||||
activeTabId: 'tab-1',
|
||||
isLastStep: false,
|
||||
finishTour: vi.fn(),
|
||||
advanceContextualTour: vi.fn(),
|
||||
detachContextualTourSource: vi.fn(),
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
openClientHostedBrowserSettings: vi.fn(),
|
||||
openWorkspaceComposer: vi.fn(),
|
||||
dispatchTerminalPaneSplit,
|
||||
schedule: vi.fn()
|
||||
})
|
||||
|
||||
expect(dispatchTerminalPaneSplit).toHaveBeenCalledWith({
|
||||
tabId: 'tab-1',
|
||||
direction: 'vertical'
|
||||
})
|
||||
})
|
||||
|
||||
it('opens the workspace composer after detaching, without advancing the tour itself', () => {
|
||||
const detachContextualTourSource = vi.fn()
|
||||
const openWorkspaceComposer = vi.fn()
|
||||
const advanceContextualTour = vi.fn()
|
||||
const finishTour = vi.fn()
|
||||
|
||||
performContextualTourStepAction({
|
||||
action: { kind: 'create-worktree', label: 'Create worktree' },
|
||||
activeTabId: 'tab-1',
|
||||
isLastStep: true,
|
||||
finishTour,
|
||||
advanceContextualTour,
|
||||
detachContextualTourSource,
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
openClientHostedBrowserSettings: vi.fn(),
|
||||
openWorkspaceComposer,
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: vi.fn()
|
||||
})
|
||||
|
||||
// Opening the composer cancels this tour and hands off, so we neither
|
||||
// advance nor finish it here.
|
||||
expect(detachContextualTourSource).toHaveBeenCalledTimes(1)
|
||||
expect(openWorkspaceComposer).toHaveBeenCalledTimes(1)
|
||||
expect(advanceContextualTour).not.toHaveBeenCalled()
|
||||
expect(finishTour).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens the workspace composer with no projects so the first one can be added there', () => {
|
||||
const detachContextualTourSource = vi.fn()
|
||||
const openWorkspaceComposer = vi.fn()
|
||||
|
||||
performContextualTourStepAction({
|
||||
action: { kind: 'create-worktree', label: 'Create worktree' },
|
||||
activeTabId: 'tab-1',
|
||||
isLastStep: true,
|
||||
finishTour: vi.fn(),
|
||||
advanceContextualTour: vi.fn(),
|
||||
detachContextualTourSource,
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
openClientHostedBrowserSettings: vi.fn(),
|
||||
openWorkspaceComposer,
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: vi.fn()
|
||||
})
|
||||
|
||||
expect(detachContextualTourSource).toHaveBeenCalledTimes(1)
|
||||
expect(openWorkspaceComposer).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('finishes the tour before opening the client-hosted browser settings', () => {
|
||||
const finishTour = vi.fn()
|
||||
const openClientHostedBrowserSettings = vi.fn()
|
||||
const scheduled: (() => void)[] = []
|
||||
|
||||
performContextualTourStepAction({
|
||||
action: { kind: 'open-client-hosted-browser-settings', label: 'Browser settings' },
|
||||
activeTabId: 'tab-1',
|
||||
isLastStep: true,
|
||||
finishTour,
|
||||
advanceContextualTour: vi.fn(),
|
||||
detachContextualTourSource: vi.fn(),
|
||||
setSidebarOpen: vi.fn(),
|
||||
openTaskPage: vi.fn(),
|
||||
openModal: vi.fn(),
|
||||
openClientHostedBrowserSettings,
|
||||
openWorkspaceComposer: vi.fn(),
|
||||
dispatchTerminalPaneSplit: vi.fn(),
|
||||
schedule: (callback) => {
|
||||
scheduled.push(callback)
|
||||
}
|
||||
})
|
||||
|
||||
expect(finishTour).toHaveBeenCalledTimes(1)
|
||||
expect(openClientHostedBrowserSettings).not.toHaveBeenCalled()
|
||||
for (const callback of scheduled) {
|
||||
callback()
|
||||
}
|
||||
expect(openClientHostedBrowserSettings).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,146 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import { openWorkspaceCreationComposerWithTourHandoff } from './workspace-creation-tour-handoff'
|
||||
import { requestContextualTourWhenReady } from './request-contextual-tour-when-ready'
|
||||
|
||||
vi.mock('./request-contextual-tour-when-ready', () => ({
|
||||
requestContextualTourWhenReady: vi.fn()
|
||||
}))
|
||||
|
||||
describe('openWorkspaceCreationComposerWithTourHandoff', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('opens the workspace composer without a tour handoff outside the agent sessions tour', () => {
|
||||
const openModal = vi.fn()
|
||||
const detachContextualTourSource = vi.fn()
|
||||
vi.spyOn(useAppStore, 'getState').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
activeContextualTourId: null,
|
||||
activeContextualTourStepIndex: 0,
|
||||
activeContextualTourSource: null,
|
||||
activeModal: 'none',
|
||||
contextualToursSeenIds: [],
|
||||
repos: [{ id: 'repo-1' }],
|
||||
detachContextualTourSource,
|
||||
openModal
|
||||
}) as unknown as ReturnType<typeof useAppStore.getState>
|
||||
)
|
||||
|
||||
openWorkspaceCreationComposerWithTourHandoff()
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith('new-workspace-composer', {
|
||||
telemetrySource: 'sidebar'
|
||||
})
|
||||
expect(detachContextualTourSource).not.toHaveBeenCalled()
|
||||
expect(requestContextualTourWhenReady).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hands off from the agent sessions create-worktree step to the workspace-creation tour', () => {
|
||||
const openModal = vi.fn()
|
||||
const detachContextualTourSource = vi.fn()
|
||||
const completeContextualTour = vi.fn()
|
||||
let activeModal: ReturnType<typeof useAppStore.getState>['activeModal'] = 'none'
|
||||
vi.spyOn(useAppStore, 'getState').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
activeContextualTourId: 'workspace-agent-sessions',
|
||||
activeContextualTourStepIndex: 1,
|
||||
activeContextualTourSource: 'setup_guide_parallel_work',
|
||||
activeModal,
|
||||
contextualToursSeenIds: [],
|
||||
repos: [{ id: 'repo-1' }],
|
||||
detachContextualTourSource,
|
||||
completeContextualTour,
|
||||
openModal
|
||||
}) as unknown as ReturnType<typeof useAppStore.getState>
|
||||
)
|
||||
|
||||
openWorkspaceCreationComposerWithTourHandoff()
|
||||
|
||||
expect(detachContextualTourSource).toHaveBeenCalledWith(
|
||||
'workspace-agent-sessions',
|
||||
'setup_guide_parallel_work'
|
||||
)
|
||||
expect(completeContextualTour).toHaveBeenCalledWith('workspace-agent-sessions')
|
||||
expect(openModal).toHaveBeenCalledWith('new-workspace-composer', {
|
||||
telemetrySource: 'sidebar',
|
||||
contextualTourSource: 'workspace_creation_modal'
|
||||
})
|
||||
expect(requestContextualTourWhenReady).toHaveBeenCalledWith({
|
||||
id: 'workspace-creation',
|
||||
source: 'workspace_creation_modal',
|
||||
wasFeaturePreviouslyInteracted: false,
|
||||
waitForActiveTourToClear: true,
|
||||
shouldContinue: expect.any(Function)
|
||||
})
|
||||
|
||||
const [{ shouldContinue }] = vi.mocked(requestContextualTourWhenReady).mock.calls[0]
|
||||
activeModal = 'new-workspace-composer'
|
||||
expect(shouldContinue?.()).toBe(true)
|
||||
activeModal = 'none'
|
||||
expect(shouldContinue?.()).toBe(false)
|
||||
})
|
||||
|
||||
it('opens the composer without re-showing workspace creation when that tour was already seen', () => {
|
||||
const openModal = vi.fn()
|
||||
const detachContextualTourSource = vi.fn()
|
||||
const completeContextualTour = vi.fn()
|
||||
vi.spyOn(useAppStore, 'getState').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
activeContextualTourId: 'workspace-agent-sessions',
|
||||
activeContextualTourStepIndex: 1,
|
||||
activeContextualTourSource: 'setup_guide_parallel_work',
|
||||
activeModal: 'none',
|
||||
contextualToursSeenIds: ['workspace-creation'],
|
||||
repos: [{ id: 'repo-1' }],
|
||||
detachContextualTourSource,
|
||||
completeContextualTour,
|
||||
openModal
|
||||
}) as unknown as ReturnType<typeof useAppStore.getState>
|
||||
)
|
||||
|
||||
openWorkspaceCreationComposerWithTourHandoff()
|
||||
|
||||
expect(detachContextualTourSource).toHaveBeenCalledWith(
|
||||
'workspace-agent-sessions',
|
||||
'setup_guide_parallel_work'
|
||||
)
|
||||
expect(completeContextualTour).toHaveBeenCalledWith('workspace-agent-sessions')
|
||||
expect(openModal).toHaveBeenCalledWith('new-workspace-composer', {
|
||||
telemetrySource: 'sidebar',
|
||||
contextualTourSource: 'workspace_creation_modal'
|
||||
})
|
||||
expect(requestContextualTourWhenReady).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens the composer without a project so the empty form can guide setup', () => {
|
||||
const openModal = vi.fn()
|
||||
const detachContextualTourSource = vi.fn()
|
||||
vi.spyOn(useAppStore, 'getState').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
activeContextualTourId: 'workspace-agent-sessions',
|
||||
activeContextualTourStepIndex: 1,
|
||||
activeContextualTourSource: 'setup_guide_parallel_work',
|
||||
activeModal: 'none',
|
||||
contextualToursSeenIds: [],
|
||||
repos: [],
|
||||
detachContextualTourSource,
|
||||
openModal
|
||||
}) as unknown as ReturnType<typeof useAppStore.getState>
|
||||
)
|
||||
|
||||
openWorkspaceCreationComposerWithTourHandoff()
|
||||
|
||||
expect(detachContextualTourSource).not.toHaveBeenCalled()
|
||||
expect(openModal).toHaveBeenCalledWith('new-workspace-composer', {
|
||||
telemetrySource: 'sidebar'
|
||||
})
|
||||
expect(requestContextualTourWhenReady).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
+1
-13
@@ -3,7 +3,7 @@
|
||||
import React, { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { VIRTUALIZED_LIST_MIN_ROWS } from '@/components/virtualized-list'
|
||||
|
||||
import type { GitBranchChangeEntry } from '../../../../../../shared/git-diff-compare-types'
|
||||
import type { CombinedDiffFileTreeRow as CombinedDiffFileTreeRowComponent } from './combined-diff-file-tree-row'
|
||||
|
||||
@@ -117,18 +117,6 @@ function renderTree(entries: readonly GitBranchChangeEntry[]): void {
|
||||
}
|
||||
|
||||
describe('combined diff file tree row windowing', () => {
|
||||
it('mounts every row below the virtualize threshold', () => {
|
||||
const fileCount = VIRTUALIZED_LIST_MIN_ROWS - 10
|
||||
const directoryCount = 4
|
||||
renderTree(buildEntries(fileCount, directoryCount))
|
||||
|
||||
const totalRows = 1 + directoryCount + fileCount
|
||||
expect(totalRows).toBeLessThan(VIRTUALIZED_LIST_MIN_ROWS)
|
||||
expect(mountedRows.count).toBe(totalRows)
|
||||
expect(host.querySelector('[data-testid="virtualized-list"]')).toBeNull()
|
||||
expect(host.querySelectorAll('[data-index]').length).toBe(0)
|
||||
})
|
||||
|
||||
it('mounts only a window of rows for a large review', () => {
|
||||
const fileCount = 900
|
||||
const directoryCount = 30
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createRichMarkdownExtensions } from './rich-markdown-extensions'
|
||||
import { createRichMarkdownEditorCodec } from './rich-markdown-source-transport'
|
||||
import { useLocalImagePick } from './useLocalImagePick'
|
||||
import { handleRichMarkdownImagePaste } from './rich-markdown-paste-image'
|
||||
import { runSlashCommand, slashCommands } from './rich-markdown-slash-commands'
|
||||
|
||||
vi.mock('@/runtime/runtime-file-import-client', () => ({
|
||||
importExternalPathsToRuntime: vi.fn().mockResolvedValue({
|
||||
@@ -102,20 +101,6 @@ describe('inserting an image while the cursor is inside a fenced code block', ()
|
||||
expectFencedSplit(editor)
|
||||
})
|
||||
|
||||
it('keeps both halves fenced when the slash command inserts the image', async () => {
|
||||
const imageCommand = slashCommands.find((command) => command.id === 'image')
|
||||
expect(imageCommand).toBeDefined()
|
||||
const { result } = renderHook(() => useLocalImagePick(editor as never, '/repo/note.md', 'wt-1'))
|
||||
const from = editor.state.selection.from
|
||||
|
||||
runSlashCommand(editor as never, { from, to: from }, imageCommand!, () => {
|
||||
void result.current()
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
expectFencedSplit(editor)
|
||||
})
|
||||
|
||||
it('keeps both halves fenced when a clipboard screenshot is pasted', async () => {
|
||||
const handled = handleRichMarkdownImagePaste({
|
||||
editor: editor as never,
|
||||
@@ -131,16 +116,4 @@ describe('inserting an image while the cursor is inside a fenced code block', ()
|
||||
expect(handled).toBe(true)
|
||||
expectFencedSplit(editor)
|
||||
})
|
||||
|
||||
it('still inserts the image inline when the cursor is in ordinary prose', async () => {
|
||||
editor.destroy()
|
||||
editor = mountRichMarkdownEditor('Install the extension\n')
|
||||
editor.commands.setTextSelection(13)
|
||||
const { result } = renderHook(() => useLocalImagePick(editor as never, '/repo/note.md', 'wt-1'))
|
||||
|
||||
await result.current()
|
||||
await flushPromises()
|
||||
|
||||
expect(editor.getMarkdown().trimEnd()).toBe('Install the extension')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -52,54 +52,6 @@ function diffEditorFixture(
|
||||
}
|
||||
|
||||
describe('useDiffViewerLargeDiffLifecycle', () => {
|
||||
it('keeps repeated content rotations bounded to the current Monaco models', async () => {
|
||||
const modelKey = 'diff-tab'
|
||||
const originalModelKey = 'original-v1'
|
||||
const onEnterFallback = vi.fn()
|
||||
const paths = ['modified-v1', 'modified-v2', 'modified-v3'].map((modifiedModelKey) =>
|
||||
getDiffViewerMonacoModelPaths({
|
||||
modelKey,
|
||||
originalModelKey,
|
||||
modifiedModelKey,
|
||||
generationSuffix: ''
|
||||
})
|
||||
)
|
||||
const firstModel = detachedModel()
|
||||
const secondModel = detachedModel()
|
||||
const currentModel = detachedModel()
|
||||
const originalModel = detachedModel()
|
||||
monacoFixture.models.set(paths[0].originalModelPath, originalModel)
|
||||
monacoFixture.models.set(paths[0].modifiedModelPath, firstModel)
|
||||
monacoFixture.models.set(paths[1].modifiedModelPath, secondModel)
|
||||
monacoFixture.models.set(paths[2].modifiedModelPath, currentModel)
|
||||
const retainedEditor = diffEditorFixture(originalModel, firstModel)
|
||||
|
||||
const hook = renderHook(
|
||||
({ modifiedModelKey }) =>
|
||||
useDiffViewerLargeDiffLifecycle({
|
||||
limited: false,
|
||||
modelKey,
|
||||
originalModelKey,
|
||||
modifiedModelKey,
|
||||
diffEditorRef: retainedEditor,
|
||||
onEnterFallback
|
||||
}),
|
||||
{ initialProps: { modifiedModelKey: 'modified-v1' } }
|
||||
)
|
||||
|
||||
hook.rerender({ modifiedModelKey: 'modified-v2' })
|
||||
await act(() => Promise.resolve())
|
||||
hook.rerender({ modifiedModelKey: 'modified-v3' })
|
||||
await act(() => Promise.resolve())
|
||||
|
||||
expect(firstModel.dispose).toHaveBeenCalledOnce()
|
||||
expect(secondModel.dispose).toHaveBeenCalledOnce()
|
||||
expect(currentModel.dispose).not.toHaveBeenCalled()
|
||||
expect(retainedEditor.setModel).toHaveBeenCalledTimes(2)
|
||||
expect(hook.result.current).toEqual(paths[2])
|
||||
expect(onEnterFallback).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('resets the owning diff widget before disposing a superseded model', async () => {
|
||||
const modelKey = 'diff-tab'
|
||||
const paths = ['modified-v1', 'modified-v2'].map((modifiedModelKey) =>
|
||||
|
||||
@@ -61,24 +61,6 @@ describe('native chat launch-default adoption', () => {
|
||||
expect(resolveNativeChatSessionOptionDefaults(defaults.read(), 'claude')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not adopt an unlisted launch-flag model before discovery either', async () => {
|
||||
// A tracked flag proves emission, not that a catalog recognizes the model.
|
||||
seedNativeChatAppliedSessionOptions('pty-1', 'claude', { model: 'claude-opus-5' })
|
||||
const defaults = claudeLaunchDefaults()
|
||||
const surface = createNativeChatPtySessionOptions({
|
||||
agent: 'claude',
|
||||
scopeKey: 'pty-1',
|
||||
mode: 'live',
|
||||
modelLabel: 'Model',
|
||||
dispatchCommand: vi.fn().mockResolvedValue({ outcome: 'applied' }),
|
||||
persistSelection: defaults.persistSelection
|
||||
})!
|
||||
|
||||
await surface.setOption('model', 'claude-opus-5')
|
||||
|
||||
expect(defaults.read().claude?.model).toBeUndefined()
|
||||
})
|
||||
|
||||
it('still adopts a model only the host catalog lists', async () => {
|
||||
seedNativeChatAppliedSessionOptions('pty-1', 'claude', { model: 'sonnet' })
|
||||
const defaults = claudeLaunchDefaults()
|
||||
@@ -119,21 +101,4 @@ describe('native chat launch-default adoption', () => {
|
||||
effort: 'high'
|
||||
})
|
||||
})
|
||||
|
||||
it('still adopts a tracked seed model before any discovery', async () => {
|
||||
seedNativeChatAppliedSessionOptions('pty-1', 'claude', { model: 'opus', effort: 'xhigh' })
|
||||
const defaults = claudeLaunchDefaults()
|
||||
const surface = createNativeChatPtySessionOptions({
|
||||
agent: 'claude',
|
||||
scopeKey: 'pty-1',
|
||||
mode: 'live',
|
||||
modelLabel: 'Model',
|
||||
dispatchCommand: vi.fn(),
|
||||
persistSelection: defaults.persistSelection
|
||||
})!
|
||||
|
||||
await surface.setOption('effort', 'high')
|
||||
|
||||
expect(defaults.read().claude?.model).toBe('opus')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,29 +26,6 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
describe('a mounted structured chat', () => {
|
||||
it('holds the session while it is on screen and releases it on unmount', async () => {
|
||||
const { unmount } = renderHook(() =>
|
||||
useStructuredAgentSessionHold({
|
||||
sessionId: 'session-alpha',
|
||||
target: LOCAL_TARGET,
|
||||
surface: 'desktop-chat'
|
||||
})
|
||||
)
|
||||
|
||||
await waitFor(() => expect(callsTo('agentSession.hold')).toHaveLength(1))
|
||||
const held = callsTo('agentSession.hold')[0] as { sessionId: string; holderId: string }
|
||||
expect(held.sessionId).toBe('session-alpha')
|
||||
expect(callsTo('agentSession.release')).toHaveLength(0)
|
||||
|
||||
unmount()
|
||||
|
||||
await waitFor(() =>
|
||||
expect(callsTo('agentSession.release')).toEqual([
|
||||
{ sessionId: 'session-alpha', holderId: held.holderId }
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('does not release a hold that has not landed yet', async () => {
|
||||
let settleHold = (): void => {}
|
||||
mocks.call.mockImplementation((_target: unknown, method: string) =>
|
||||
@@ -76,26 +53,6 @@ describe('a mounted structured chat', () => {
|
||||
await waitFor(() => expect(callsTo('agentSession.release')).toHaveLength(1))
|
||||
})
|
||||
|
||||
it('reports a hold the host refused and still releases on unmount', async () => {
|
||||
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
|
||||
const refused = new Error('structured_agent_session_unsupported')
|
||||
mocks.call.mockImplementation((_target: unknown, method: string) =>
|
||||
method === 'agentSession.hold' ? Promise.reject(refused) : Promise.resolve()
|
||||
)
|
||||
const { unmount } = renderHook(() =>
|
||||
useStructuredAgentSessionHold({
|
||||
sessionId: 'session-alpha',
|
||||
target: LOCAL_TARGET,
|
||||
surface: 'desktop-chat'
|
||||
})
|
||||
)
|
||||
|
||||
await waitFor(() => expect(warn).toHaveBeenCalledWith(expect.any(String), refused))
|
||||
unmount()
|
||||
await waitFor(() => expect(callsTo('agentSession.release')).toHaveLength(1))
|
||||
warn.mockRestore()
|
||||
})
|
||||
|
||||
it('keeps one hold across re-renders that rebuild the target object', async () => {
|
||||
const { rerender, unmount } = renderHook(
|
||||
(props: { sessionId: string }) =>
|
||||
|
||||
-40
@@ -12,46 +12,6 @@ vi.mock('@/runtime/runtime-rpc-client', () => ({
|
||||
callRuntimeRpc: permission.settings
|
||||
}))
|
||||
|
||||
it('reveals before launch Retry, but waits for a successful queue Resume', async () => {
|
||||
const order: string[] = []
|
||||
const controller = {
|
||||
capturePresentation: () => () => true,
|
||||
respond: vi.fn(async () => null),
|
||||
queuedMessages: {
|
||||
queueCapable: true,
|
||||
clearCapable: true,
|
||||
cards: [],
|
||||
pause: null,
|
||||
clear: vi.fn(async () => true),
|
||||
queuePending: null,
|
||||
pending: () => null,
|
||||
resume: vi.fn(async () => {
|
||||
order.push('resume')
|
||||
return true
|
||||
}),
|
||||
steer: vi.fn(async () => {}),
|
||||
remove: vi.fn(async () => {}),
|
||||
edit: vi.fn(async () => {}),
|
||||
editCapable: false,
|
||||
editor: undefined,
|
||||
steerHead: vi.fn(() => false),
|
||||
isWorking: false
|
||||
}
|
||||
}
|
||||
const retryLaunch = (): number => order.push('launch')
|
||||
const { result } = renderHook(() => useStructuredNativeChatSubmitReveal(controller, retryLaunch))
|
||||
result.current.messageListRef.current = {
|
||||
revealLatest: () => order.push('reveal'),
|
||||
revealFindMatch: () => {},
|
||||
messageCopyText: () => null
|
||||
}
|
||||
|
||||
act(() => result.current.retryLaunch())
|
||||
await act(() => result.current.queuedMessages.resume())
|
||||
|
||||
expect(order).toEqual(['reveal', 'launch', 'resume', 'reveal'])
|
||||
})
|
||||
|
||||
// STA-10671: an older host held for its permission mode would run a queued card unprompted.
|
||||
it('sends no queued card and retries no start on a host held for its permission mode', async () => {
|
||||
const queuedMessages = {
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import React, { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { CreateFromPicker } from './CreateFromPicker'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import {
|
||||
getRuntimeRepoBaseRefDefault,
|
||||
searchRuntimeRepoBaseRefs
|
||||
} from '@/runtime/runtime-repo-client'
|
||||
|
||||
vi.mock('@/components/ui/popover', () => ({
|
||||
// Why: the real popover owns open state; expose onOpenChange so a test can open the list.
|
||||
Popover: ({
|
||||
children,
|
||||
onOpenChange
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}) => (
|
||||
<div>
|
||||
<button type="button" data-testid="open-popover" onClick={() => onOpenChange?.(true)} />
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
PopoverContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
PopoverTrigger: ({ children }: { children: React.ReactNode }) => <>{children}</>
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/command', () => ({
|
||||
Command: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CommandEmpty: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
CommandInput: () => <input />,
|
||||
CommandItem: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
|
||||
CommandList: ({ children }: { children: React.ReactNode }) => <div>{children}</div>
|
||||
}))
|
||||
|
||||
const storeState = {
|
||||
settings: { activeRuntimeEnvironmentId: 'focused-runtime' },
|
||||
repos: [] as Pick<Repo, 'id' | 'connectionId' | 'executionHostId'>[]
|
||||
}
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: typeof storeState) => unknown) => selector(storeState)
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/runtime-repo-client', () => ({
|
||||
getRuntimeRepoBaseRefDefault: vi.fn().mockResolvedValue({
|
||||
defaultBaseRef: 'main',
|
||||
remoteCount: 1
|
||||
}),
|
||||
searchRuntimeRepoBaseRefs: vi.fn().mockResolvedValue([])
|
||||
}))
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
function repoMapFor(repo: Repo): Map<string, Repo> {
|
||||
return new Map([[repo.id, repo]])
|
||||
}
|
||||
|
||||
function makeRepo(overrides: Partial<Repo>): Repo {
|
||||
return {
|
||||
id: 'repo-1',
|
||||
path: '/repo',
|
||||
displayName: 'Repo',
|
||||
badgeColor: '#000000',
|
||||
addedAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPicker(repo: Repo): Promise<void> {
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<TooltipProvider>
|
||||
<CreateFromPicker
|
||||
repoId={repo.id}
|
||||
repoMap={repoMapFor(repo)}
|
||||
worktrees={[]}
|
||||
value=""
|
||||
onValueChange={vi.fn()}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
describe('CreateFromPicker host routing', () => {
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount()
|
||||
})
|
||||
container.remove()
|
||||
storeState.repos = []
|
||||
})
|
||||
|
||||
it('uses the selected runtime-owned repo host instead of the focused runtime', async () => {
|
||||
const repo = makeRepo({ executionHostId: 'runtime:owner-runtime' })
|
||||
storeState.repos = [repo]
|
||||
|
||||
await renderPicker(repo)
|
||||
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith('runtime:owner-runtime', repo.id)
|
||||
})
|
||||
|
||||
// Empty queries must list all repo branches instead of only defaults/worktree branches.
|
||||
it('lists the repo branches as soon as it opens, with no query typed', async () => {
|
||||
const repo = makeRepo({ executionHostId: 'local' })
|
||||
storeState.repos = [repo]
|
||||
|
||||
await renderPicker(repo)
|
||||
await act(async () => {
|
||||
container.querySelector<HTMLButtonElement>('[data-testid="open-popover"]')?.click()
|
||||
})
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
})
|
||||
|
||||
expect(searchRuntimeRepoBaseRefs).toHaveBeenCalledWith('local', repo.id, '', 30)
|
||||
})
|
||||
|
||||
it('keeps an explicit local repo on the local client even when a runtime is focused', async () => {
|
||||
const repo = makeRepo({ executionHostId: 'local' })
|
||||
storeState.repos = [repo]
|
||||
|
||||
await renderPicker(repo)
|
||||
|
||||
expect(getRuntimeRepoBaseRefDefault).toHaveBeenCalledWith('local', repo.id)
|
||||
expect(searchRuntimeRepoBaseRefs).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -7,7 +7,6 @@ import type { Repo } from '../../../../shared/repo-types'
|
||||
import { makeWorktree } from '@/store/slices/worktrees-slice-test-fixtures'
|
||||
import { useAppStore } from '@/store'
|
||||
import { McpConfigSection } from './McpConfigSection'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const runtimeFiles = vi.hoisted(() => ({
|
||||
readRuntimeDirectory: vi.fn(),
|
||||
@@ -105,24 +104,6 @@ it('inspects and creates a managed-host repo config on that host, never the desk
|
||||
expect(desktopFs.writeFile).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps a desktop repo on the desktop while a managed host is focused', async () => {
|
||||
const desktopRepo: Repo = { ...repo, executionHostId: 'local' }
|
||||
useAppStore.setState({
|
||||
settings: { ...getDefaultSettings('/tmp'), activeRuntimeEnvironmentId: 'host-a' },
|
||||
repos: [desktopRepo],
|
||||
worktreesByRepo: {
|
||||
repo: [makeWorktree({ id: worktreeId, repoId: 'repo', path: root, isMainWorktree: true })]
|
||||
}
|
||||
})
|
||||
render(<McpConfigSection repo={desktopRepo} />)
|
||||
|
||||
await waitFor(() => expect(runtimeFiles.readRuntimeDirectory).toHaveBeenCalled())
|
||||
expect(runtimeFiles.readRuntimeDirectory).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ target: { kind: 'local' } }),
|
||||
root
|
||||
)
|
||||
})
|
||||
|
||||
it('does not offer the starter over a listed config the host refuses to read', async () => {
|
||||
runtimeFiles.readRuntimeDirectory.mockResolvedValue([{ name: '.mcp.json', isDirectory: false }])
|
||||
runtimeFiles.readRuntimeFileContent.mockRejectedValue(new Error('Remote file is too large'))
|
||||
@@ -131,14 +112,3 @@ it('does not offer the starter over a listed config the host refuses to read', a
|
||||
await screen.findByText('Remote file is too large')
|
||||
expect(screen.queryByRole('button', { name: /Add MCP config/ })).toBeNull()
|
||||
})
|
||||
|
||||
it('does not replace a config another process created after the listing', async () => {
|
||||
runtimeFiles.writeRuntimeFile.mockResolvedValue({ ok: false, diskConflict: 'changed' })
|
||||
render(<McpConfigSection repo={repo} />)
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Add MCP config/ }))
|
||||
fireEvent.click(await screen.findByRole('button', { name: /Create empty config/ }))
|
||||
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalled())
|
||||
expect(toast.success).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -70,23 +70,6 @@ describe('settings orca.yaml inspection', () => {
|
||||
// An unmounted hook must drop its retry listeners, or they fire into the next case.
|
||||
afterEach(cleanup)
|
||||
|
||||
it('asks in the background and keeps a stopped distro checking, not "missing"', async () => {
|
||||
checkRuntimeHooksMock.mockResolvedValue({
|
||||
status: 'error',
|
||||
hasHooks: false,
|
||||
hooks: null,
|
||||
mayNeedUpdate: false,
|
||||
unavailable: 'wsl-distro-stopped'
|
||||
})
|
||||
const { map } = renderEffects()
|
||||
|
||||
await waitFor(() => expect(checkRuntimeHooksMock).toHaveBeenCalled())
|
||||
// Let the resolved check settle into state before asserting nothing was recorded.
|
||||
await act(async () => {})
|
||||
expect(checkRuntimeHooksMock).toHaveBeenCalledWith('local', 'repo-1', { background: true })
|
||||
expect(map()).toEqual({})
|
||||
})
|
||||
|
||||
it('re-asks when the window comes back and records the distro once it runs', async () => {
|
||||
const hooks = { scripts: { setup: 'pnpm i' } }
|
||||
checkRuntimeHooksMock
|
||||
@@ -134,11 +117,4 @@ describe('settings orca.yaml inspection', () => {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('records the inspection once the distro answers', async () => {
|
||||
checkRuntimeHooksMock.mockResolvedValue({ hasHooks: true, hooks: null, mayNeedUpdate: false })
|
||||
const { map } = renderEffects()
|
||||
|
||||
await waitFor(() => expect(Object.keys(map())).toHaveLength(1))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,25 +36,6 @@ function makeSession(
|
||||
}
|
||||
|
||||
describe('commitProjectHeaderDragDrop', () => {
|
||||
it('commits whole-repo reordering when project groups are absent', () => {
|
||||
const onCommitRepoOrder = vi.fn()
|
||||
const repos = [makeRepo('a'), makeRepo('b'), makeRepo('c')]
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession(makeRepo('c'), ['a', 'b', 'c']),
|
||||
sidebarDropIndex: 0,
|
||||
orderedRepoIds: ['a', 'b', 'c'],
|
||||
repoById,
|
||||
liveRepos: repos,
|
||||
usesProjectGroupOrdering: false,
|
||||
onCommitRepoOrder,
|
||||
onCommitProjectGroupOrder: vi.fn()
|
||||
})
|
||||
|
||||
expect(onCommitRepoOrder).toHaveBeenCalledWith(['c', 'a', 'b'])
|
||||
})
|
||||
|
||||
it('moves a merged paired-host header upward as one stable block', () => {
|
||||
const onCommitRepoOrder = vi.fn()
|
||||
const repos = [makeRepo('b'), makeRepo('same'), makeRepo('c')]
|
||||
@@ -74,25 +55,6 @@ describe('commitProjectHeaderDragDrop', () => {
|
||||
expect(onCommitRepoOrder).toHaveBeenCalledWith(['same', 'same', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('does not reorder host occurrences when a merged header stays in place', () => {
|
||||
const onCommitRepoOrder = vi.fn()
|
||||
const repos = [makeRepo('b'), makeRepo('same'), makeRepo('c')]
|
||||
const repoById = new Map(repos.map((repo) => [repo.id, repo]))
|
||||
|
||||
commitProjectHeaderDragDrop({
|
||||
session: makeSession(makeRepo('same'), ['b', 'same', 'c']),
|
||||
sidebarDropIndex: 2,
|
||||
orderedRepoIds: ['b', 'same', 'c', 'same'],
|
||||
repoById,
|
||||
liveRepos: repos,
|
||||
usesProjectGroupOrdering: false,
|
||||
onCommitRepoOrder,
|
||||
onCommitProjectGroupOrder: vi.fn()
|
||||
})
|
||||
|
||||
expect(onCommitRepoOrder).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('commits projectGroupOrder for the dragged row, not the id-keyed twin on another host', () => {
|
||||
const onCommitProjectGroupOrder = vi.fn()
|
||||
const repos = [
|
||||
|
||||
-142
@@ -1,142 +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 { useProjectGroupDialogs, type ProjectGroupDialogs } from './use-project-group-dialogs'
|
||||
import type { ProjectGroup } from '../../../../../../shared/project-group-types'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
moveProjectToGroup: vi.fn(),
|
||||
createProjectGroup: vi.fn(),
|
||||
updateProjectGroup: vi.fn(),
|
||||
deleteProjectGroupWithContainedProjects: vi.fn(),
|
||||
confirmUnsavedDiscard: vi.fn(),
|
||||
toastError: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
moveProjectToGroup: mocks.moveProjectToGroup,
|
||||
createProjectGroup: mocks.createProjectGroup,
|
||||
updateProjectGroup: mocks.updateProjectGroup,
|
||||
deleteProjectGroupWithContainedProjects: mocks.deleteProjectGroupWithContainedProjects
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store/slices/worktrees/teardown/removed-workspace-unsaved-files', () => ({
|
||||
confirmUnsavedDiscard: mocks.confirmUnsavedDiscard
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({
|
||||
toast: { error: mocks.toastError }
|
||||
}))
|
||||
|
||||
const remoteGroup: ProjectGroup = {
|
||||
id: 'group-1',
|
||||
name: 'Projects',
|
||||
parentPath: '/srv/projects',
|
||||
parentGroupId: null,
|
||||
createdFrom: 'folder-scan',
|
||||
executionHostId: 'runtime:env-1',
|
||||
tabOrder: 0,
|
||||
isCollapsed: false,
|
||||
color: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
|
||||
let latest: ProjectGroupDialogs | null = null
|
||||
const roots: Root[] = []
|
||||
|
||||
function HookProbe(): null {
|
||||
latest = useProjectGroupDialogs({
|
||||
repos: [],
|
||||
repoMap: new Map(),
|
||||
projectGroups: [remoteGroup]
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
async function renderHookProbe(): Promise<void> {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
roots.push(root)
|
||||
await act(async () => {
|
||||
root.render(<HookProbe />)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.updateProjectGroup.mockResolvedValue(true)
|
||||
mocks.deleteProjectGroupWithContainedProjects.mockResolvedValue({
|
||||
status: 'deleted-group',
|
||||
groupId: remoteGroup.id,
|
||||
requestedProjectIds: [],
|
||||
removedProjectIds: [],
|
||||
failedProjectRemovals: []
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await act(async () => {
|
||||
for (const root of roots.splice(0)) {
|
||||
root.unmount()
|
||||
}
|
||||
})
|
||||
latest = null
|
||||
})
|
||||
|
||||
describe('project group dialogs carry the owner host', () => {
|
||||
it('renames through the host that owns the group row', async () => {
|
||||
await renderHookProbe()
|
||||
await act(async () => {
|
||||
latest!.handleRenameProjectGroup(remoteGroup.id, remoteGroup.name, 'runtime:env-1')
|
||||
})
|
||||
await act(async () => {
|
||||
await latest!.handleSubmitProjectGroupName('Renamed')
|
||||
})
|
||||
|
||||
expect(mocks.updateProjectGroup).toHaveBeenCalledWith(
|
||||
remoteGroup.id,
|
||||
{ name: 'Renamed' },
|
||||
{ hostId: 'runtime:env-1' }
|
||||
)
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('surfaces an unconfirmed-rename toast when the owner host does not answer', async () => {
|
||||
mocks.updateProjectGroup.mockResolvedValue(false)
|
||||
await renderHookProbe()
|
||||
await act(async () => {
|
||||
latest!.handleRenameProjectGroup(remoteGroup.id, remoteGroup.name, 'runtime:env-1')
|
||||
})
|
||||
await act(async () => {
|
||||
await latest!.handleSubmitProjectGroupName('Renamed')
|
||||
})
|
||||
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('Failed to rename group', {
|
||||
description:
|
||||
"Orca could not confirm the new name with the group's host. Recheck the group after reconnecting."
|
||||
})
|
||||
})
|
||||
|
||||
it('deletes through the host that owns the group row', async () => {
|
||||
await renderHookProbe()
|
||||
await act(async () => {
|
||||
latest!.handleDeleteProjectGroup(remoteGroup.id, remoteGroup.name, 'runtime:env-1')
|
||||
})
|
||||
await act(async () => {
|
||||
await latest!.handleConfirmDeleteProjectGroup()
|
||||
})
|
||||
|
||||
expect(mocks.deleteProjectGroupWithContainedProjects).toHaveBeenCalledWith(remoteGroup.id, {
|
||||
removeContainedProjects: false,
|
||||
hostId: 'runtime:env-1'
|
||||
})
|
||||
expect(mocks.confirmUnsavedDiscard).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,42 +1,9 @@
|
||||
import type { VirtualItem } from '@tanstack/react-virtual'
|
||||
import type { WorktreeItemRow } from '../listing/renderable-rows'
|
||||
import { makePRRefreshWorktree } from '@/store/slices/github-slice-test-harness'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
installWorktreeVisibleRefreshVisibilityListener,
|
||||
visibleReviewCardIds,
|
||||
installVisibleReviewCardScrollListener
|
||||
} from './use-visible-review-refresh'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
describe('installWorktreeVisibleRefreshVisibilityListener', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('subscribes to document visibility changes so visible PR refresh can rerun on return', () => {
|
||||
const listeners = new Map<string, () => void>()
|
||||
const onChange = vi.fn()
|
||||
const addEventListener = vi.fn((event: string, listener: () => void) => {
|
||||
listeners.set(event, listener)
|
||||
})
|
||||
const removeEventListener = vi.fn()
|
||||
|
||||
vi.stubGlobal('document', {
|
||||
addEventListener,
|
||||
removeEventListener
|
||||
})
|
||||
|
||||
const cleanup = installWorktreeVisibleRefreshVisibilityListener(onChange)
|
||||
|
||||
expect(addEventListener).toHaveBeenCalledWith('visibilitychange', onChange)
|
||||
listeners.get('visibilitychange')?.()
|
||||
expect(onChange).toHaveBeenCalledTimes(1)
|
||||
|
||||
cleanup()
|
||||
expect(removeEventListener).toHaveBeenCalledWith('visibilitychange', onChange)
|
||||
})
|
||||
})
|
||||
import { visibleReviewCardIds } from './use-visible-review-refresh'
|
||||
|
||||
function row(id: string): WorktreeItemRow {
|
||||
return {
|
||||
@@ -74,65 +41,4 @@ describe('actual visible review cards', () => {
|
||||
})
|
||||
).toEqual(['visible'])
|
||||
})
|
||||
it('includes visible lineage children but excludes offscreen members', () => {
|
||||
expect(
|
||||
visibleReviewCardIds({
|
||||
enabled: true,
|
||||
renderRows: [{ type: 'lineage-group', key: 'family', rows: [row('parent'), row('child')] }],
|
||||
virtualItems: [virtualItem(0)],
|
||||
viewportTop: 0,
|
||||
viewportHeight: 100,
|
||||
isOnScreen: (id) => id === 'child'
|
||||
})
|
||||
).toEqual(['child'])
|
||||
})
|
||||
it('excludes rows when their review decoration is disabled', () => {
|
||||
expect(
|
||||
visibleReviewCardIds({
|
||||
enabled: false,
|
||||
renderRows: [row('visible')],
|
||||
virtualItems: [virtualItem(0)],
|
||||
viewportTop: 0,
|
||||
viewportHeight: 100
|
||||
})
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('scrolling within one virtual lineage row', () => {
|
||||
it('updates child visibility on scroll without changing virtual indexes and cancels pending frames', () => {
|
||||
const listeners = new Map<string, EventListenerOrEventListenerObject>()
|
||||
let frame: FrameRequestCallback = () => {}
|
||||
const update = vi.fn()
|
||||
const scroll = {
|
||||
addEventListener: vi.fn((type: string, callback: EventListenerOrEventListenerObject) => {
|
||||
listeners.set(type, callback)
|
||||
}),
|
||||
removeEventListener: vi.fn()
|
||||
}
|
||||
vi.stubGlobal(
|
||||
'requestAnimationFrame',
|
||||
vi.fn((callback: FrameRequestCallback) => {
|
||||
frame = callback
|
||||
return 1
|
||||
})
|
||||
)
|
||||
const cancel = vi.fn()
|
||||
vi.stubGlobal('cancelAnimationFrame', cancel)
|
||||
const cleanup = installVisibleReviewCardScrollListener(scroll, update)
|
||||
const listener = listeners.get('scroll')
|
||||
if (typeof listener === 'function') {
|
||||
listener(new Event('scroll'))
|
||||
}
|
||||
expect(update).not.toHaveBeenCalled()
|
||||
frame(0)
|
||||
expect(update).toHaveBeenCalledOnce()
|
||||
if (typeof listener === 'function') {
|
||||
listener(new Event('scroll'))
|
||||
}
|
||||
cleanup()
|
||||
expect(cancel).toHaveBeenCalledWith(1)
|
||||
expect(scroll.removeEventListener).toHaveBeenCalledWith('scroll', listener)
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -118,24 +118,4 @@ describe('sidebar manual-order drop', () => {
|
||||
expect(updateMeta).not.toHaveBeenCalled()
|
||||
expect(toastInfo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reorders silently when Manual is already the sort', async () => {
|
||||
const worktrees = seedManualOrderedRows(3)
|
||||
useAppStore.setState({ sortBy: 'manual' })
|
||||
const ids = worktrees.map((worktree) => worktree.id)
|
||||
const keys = worktrees.map(getWorktreeHostIdentity)
|
||||
const reorder = renderReorder(worktrees)
|
||||
|
||||
await act(async () => {
|
||||
reorder.current.reorderWorktrees({
|
||||
groups: [{ key: GROUP_KEY, worktreeIds: keys }],
|
||||
sourceGroupKey: GROUP_KEY,
|
||||
draggedKeys: [keys[0]!],
|
||||
dropIndex: 2
|
||||
})
|
||||
})
|
||||
|
||||
expect(manualOrderedIds()).toEqual([ids[1], ids[0], ids[2]])
|
||||
expect(toastInfo).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const recordFeatureInteractionMock = vi.fn()
|
||||
const usagePercentageDisplayMock = 'used'
|
||||
|
||||
vi.mock(import('react'), async (importOriginal) => {
|
||||
const actual = await importOriginal()
|
||||
return {
|
||||
...actual,
|
||||
memo: function memo<T>(component: T): T {
|
||||
return component
|
||||
},
|
||||
useRef: function useRef<T>(current: T): { current: T } {
|
||||
return { current }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../store', () => ({
|
||||
useAppStore: (
|
||||
selector: (state: {
|
||||
recordFeatureInteraction: typeof recordFeatureInteractionMock
|
||||
usagePercentageDisplay: 'used'
|
||||
}) => unknown
|
||||
) =>
|
||||
selector({
|
||||
recordFeatureInteraction: recordFeatureInteractionMock,
|
||||
usagePercentageDisplay: usagePercentageDisplayMock
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('./tooltip', () => ({
|
||||
ProviderIcon: function ProviderIcon(props: Record<string, unknown>) {
|
||||
return { type: 'ProviderIcon', props }
|
||||
},
|
||||
ProviderPanel: function ProviderPanel(props: Record<string, unknown>) {
|
||||
return { type: 'ProviderPanel', props }
|
||||
},
|
||||
barColor: () => 'bg-green-500',
|
||||
clampUsedPercent: (n: number) => Math.max(0, Math.min(100, Math.round(n)))
|
||||
}))
|
||||
|
||||
vi.mock('@/components/ui/dropdown-menu', () => ({
|
||||
DropdownMenu: function DropdownMenu(props: Record<string, unknown>) {
|
||||
return { type: 'DropdownMenu', props }
|
||||
},
|
||||
DropdownMenuContent: function DropdownMenuContent(props: Record<string, unknown>) {
|
||||
return { type: 'DropdownMenuContent', props }
|
||||
},
|
||||
DropdownMenuSeparator: function DropdownMenuSeparator() {
|
||||
return { type: 'DropdownMenuSeparator', props: {} }
|
||||
},
|
||||
DropdownMenuTrigger: function DropdownMenuTrigger(props: Record<string, unknown>) {
|
||||
return { type: 'DropdownMenuTrigger', props }
|
||||
}
|
||||
}))
|
||||
|
||||
type ReactElementLike = {
|
||||
type: unknown
|
||||
props: Record<string, unknown>
|
||||
}
|
||||
|
||||
function findChildByType(node: unknown, typeName: string): ReactElementLike {
|
||||
const stack = [node]
|
||||
while (stack.length > 0) {
|
||||
const current = stack.pop()
|
||||
if (current == null || typeof current === 'string' || typeof current === 'number') {
|
||||
continue
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
stack.push(...current)
|
||||
continue
|
||||
}
|
||||
const el = current as ReactElementLike
|
||||
const type = el.type as { name?: string } | string | undefined
|
||||
const matchedName = typeof type === 'string' ? type : type?.name
|
||||
if (matchedName === typeName) {
|
||||
return el
|
||||
}
|
||||
if (el.props && 'children' in el.props) {
|
||||
stack.push(el.props.children)
|
||||
}
|
||||
}
|
||||
throw new Error(`Could not find ${typeName}`)
|
||||
}
|
||||
|
||||
async function renderProviderDetailsMenu(): Promise<unknown> {
|
||||
const { ProviderDetailsMenu } = await import('./StatusBar')
|
||||
return ProviderDetailsMenu({
|
||||
provider: {
|
||||
provider: 'codex',
|
||||
status: 'ok',
|
||||
error: null,
|
||||
updatedAt: Date.now(),
|
||||
session: {
|
||||
usedPercent: 1,
|
||||
resetsAt: Date.now() + 1_000,
|
||||
resetDescription: '5h',
|
||||
windowMinutes: 300
|
||||
},
|
||||
weekly: {
|
||||
usedPercent: 3,
|
||||
resetsAt: Date.now() + 1_000,
|
||||
resetDescription: 'wk',
|
||||
windowMinutes: 10_080
|
||||
}
|
||||
},
|
||||
compact: false,
|
||||
iconOnly: false,
|
||||
ariaLabel: 'Open Codex usage details'
|
||||
})
|
||||
}
|
||||
|
||||
describe('ProviderDetailsMenu focus handoff', () => {
|
||||
it('lets pointer-outside closes keep focus on the clicked surface', async () => {
|
||||
const element = await renderProviderDetailsMenu()
|
||||
const dropdown = findChildByType(element, 'DropdownMenu')
|
||||
const content = findChildByType(element, 'DropdownMenuContent')
|
||||
const providerPanel = findChildByType(element, 'ProviderPanel')
|
||||
|
||||
expect(dropdown.props.modal).toBe(false)
|
||||
expect(providerPanel.props.usagePercentageDisplay).toBe('used')
|
||||
|
||||
const preventDefault = vi.fn()
|
||||
;(content.props.onCloseAutoFocus as (event: { preventDefault: () => void }) => void)({
|
||||
preventDefault
|
||||
})
|
||||
expect(preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
;(content.props.onPointerDownOutside as () => void)()
|
||||
;(content.props.onCloseAutoFocus as (event: { preventDefault: () => void }) => void)({
|
||||
preventDefault
|
||||
})
|
||||
expect(preventDefault).toHaveBeenCalledOnce()
|
||||
|
||||
preventDefault.mockClear()
|
||||
;(content.props.onCloseAutoFocus as (event: { preventDefault: () => void }) => void)({
|
||||
preventDefault
|
||||
})
|
||||
expect(preventDefault).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
-18
@@ -89,24 +89,6 @@ async function createGatedTransport(options: Record<string, unknown> = {}) {
|
||||
|
||||
// The user is told when keys typed into a starting remote pane were dropped for safety.
|
||||
describe('remote pre-bind typing discard notice', () => {
|
||||
it('tells the pane when startup work made it drop typing from before the terminal bound', async () => {
|
||||
const onTypingDiscardedBeforeBind = vi.fn()
|
||||
const { transport, created, waitForCreate } = await createGatedTransport({
|
||||
keepTypeAheadForNewTerminal: () => true,
|
||||
releasePreconnectInput: () => false,
|
||||
onTypingDiscardedBeforeBind
|
||||
})
|
||||
expect(transport.sendInput('make the tests pass\r', 'driving')).toBe(true)
|
||||
const connecting = transport.connect({ url: '', callbacks: {} })
|
||||
await waitForCreate()
|
||||
created.resolve({ handle: 'terminal-new' })
|
||||
await connecting
|
||||
await completeAttach()
|
||||
|
||||
expect(onTypingDiscardedBeforeBind).toHaveBeenCalledTimes(1)
|
||||
transport.destroy?.()
|
||||
})
|
||||
|
||||
it('says nothing when the typing reaches the terminal it was typed for', async () => {
|
||||
const onTypingDiscardedBeforeBind = vi.fn()
|
||||
const { transport, created, waitForCreate } = await createGatedTransport({
|
||||
|
||||
+11
-55
@@ -20,23 +20,17 @@ afterEach(() => {
|
||||
let subscriptionCallbacks: MultiplexSubscriptionCallbacks = null
|
||||
let resolvedPaneHandle = 'terminal-1'
|
||||
|
||||
const {
|
||||
emitSnapshot,
|
||||
runtimeCall,
|
||||
latestSubscribePayload,
|
||||
subscribeFrameCount,
|
||||
subscribedTerminalHandles,
|
||||
resetRemoteRuntimeTransport
|
||||
} = createRemoteRuntimeTransportMocks({
|
||||
getCallbacks: () => subscriptionCallbacks,
|
||||
setCallbacks: (callbacks) => {
|
||||
subscriptionCallbacks = callbacks
|
||||
},
|
||||
getResolvedPaneHandle: () => resolvedPaneHandle,
|
||||
setResolvedPaneHandle: (handle) => {
|
||||
resolvedPaneHandle = handle
|
||||
}
|
||||
})
|
||||
const { latestSubscribePayload, subscribeFrameCount, resetRemoteRuntimeTransport } =
|
||||
createRemoteRuntimeTransportMocks({
|
||||
getCallbacks: () => subscriptionCallbacks,
|
||||
setCallbacks: (callbacks) => {
|
||||
subscriptionCallbacks = callbacks
|
||||
},
|
||||
getResolvedPaneHandle: () => resolvedPaneHandle,
|
||||
setResolvedPaneHandle: (handle) => {
|
||||
resolvedPaneHandle = handle
|
||||
}
|
||||
})
|
||||
|
||||
// What a host sends when its window did not mount the pane within its 10 s wait.
|
||||
function answerUnmountedHostPane(streamId: number): void {
|
||||
@@ -65,44 +59,6 @@ describe('remote runtime PTY whose host pane is not mounted yet', () => {
|
||||
resetRemoteRuntimeTransport()
|
||||
})
|
||||
|
||||
it('keeps the pane and reattaches once the host mounts it', async () => {
|
||||
const onPtyExit = vi.fn()
|
||||
const { transport, onExit } = await connectPane(onPtyExit)
|
||||
|
||||
answerUnmountedHostPane(latestSubscribePayload().streamId)
|
||||
|
||||
expect(onPtyExit).not.toHaveBeenCalled()
|
||||
expect(onExit).not.toHaveBeenCalled()
|
||||
await vi.waitFor(() => expect(subscribeFrameCount()).toBe(2))
|
||||
expect(subscribedTerminalHandles()).toEqual(['terminal-1', 'terminal-1'])
|
||||
emitSnapshot(latestSubscribePayload().streamId, 'host mounted the pane')
|
||||
await vi.waitFor(() => expect(transport.getRecoveryState?.().phase).toBe('connected'))
|
||||
expect(transport.getPtyId()).toBe('remote:env-1@@terminal-1')
|
||||
expect(onPtyExit).not.toHaveBeenCalled()
|
||||
transport.destroy?.()
|
||||
})
|
||||
|
||||
it('retires the pane as lost, not exited, when the host stops listing it', async () => {
|
||||
const onPtyExit = vi.fn()
|
||||
const { transport, onExit } = await connectPane(onPtyExit)
|
||||
const defaultCall = runtimeCall.getMockImplementation()
|
||||
runtimeCall.mockImplementation(async (args: { method: string }) => {
|
||||
if (args.method === 'terminal.resolvePane') {
|
||||
return { ok: false, error: { code: 'runtime_error', message: 'terminal_not_found' } }
|
||||
}
|
||||
return defaultCall?.(args)
|
||||
})
|
||||
|
||||
answerUnmountedHostPane(latestSubscribePayload().streamId)
|
||||
|
||||
// Why -1: the host-loss sentinel; a pane the host stopped listing is not proof of exit.
|
||||
await vi.waitFor(() => expect(onPtyExit).toHaveBeenCalledWith('remote:env-1@@terminal-1', -1))
|
||||
expect(onPtyExit).toHaveBeenCalledOnce()
|
||||
expect(onExit).not.toHaveBeenCalled()
|
||||
expect(subscribeFrameCount()).toBe(1)
|
||||
transport.destroy?.()
|
||||
})
|
||||
|
||||
it('backs off and settles disconnected, not exited, when the host never mounts it', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
||||
@@ -17,30 +17,6 @@ describe('refreshTerminalImeInputContext', () => {
|
||||
return helper
|
||||
}
|
||||
|
||||
it('blurs then refocuses the helper on macOS', () => {
|
||||
const helper = appendHelper()
|
||||
const blur = vi.spyOn(helper, 'blur')
|
||||
const focus = vi.spyOn(helper, 'focus')
|
||||
helper.focus()
|
||||
focus.mockClear()
|
||||
const scheduled: (() => void)[] = []
|
||||
|
||||
const refreshed = refreshTerminalImeInputContext(helper, {
|
||||
isMac: true,
|
||||
scheduleRefocus: (callback) => scheduled.push(callback)
|
||||
})
|
||||
|
||||
expect(refreshed).toBe(true)
|
||||
expect(blur).toHaveBeenCalledOnce()
|
||||
expect(focus).not.toHaveBeenCalled()
|
||||
|
||||
for (const run of scheduled) {
|
||||
run()
|
||||
}
|
||||
expect(focus).toHaveBeenCalledOnce()
|
||||
expect(document.activeElement).toBe(helper)
|
||||
})
|
||||
|
||||
it('marks only the synchronous refresh blur so focus ownership can stay latched', () => {
|
||||
const helper = appendHelper()
|
||||
let refreshingDuringBlur = false
|
||||
@@ -80,27 +56,6 @@ describe('refreshTerminalImeInputContext', () => {
|
||||
expect(document.activeElement).toBe(outside)
|
||||
})
|
||||
|
||||
it('reports when a newer focus owner wins the refocus guard', () => {
|
||||
const helper = appendHelper()
|
||||
const outside = document.createElement('input')
|
||||
document.body.appendChild(outside)
|
||||
const onRefocusSkipped = vi.fn()
|
||||
const scheduled: (() => void)[] = []
|
||||
helper.focus()
|
||||
|
||||
refreshTerminalImeInputContext(helper, {
|
||||
isMac: true,
|
||||
onRefocusSkipped,
|
||||
scheduleRefocus: (callback) => scheduled.push(callback)
|
||||
})
|
||||
|
||||
outside.focus()
|
||||
for (const run of scheduled) {
|
||||
run()
|
||||
}
|
||||
expect(onRefocusSkipped).toHaveBeenCalledWith(outside)
|
||||
})
|
||||
|
||||
it('reports when a connected helper cannot accept the scheduled focus', () => {
|
||||
const helper = appendHelper()
|
||||
const onRefocusSkipped = vi.fn()
|
||||
@@ -120,19 +75,4 @@ describe('refreshTerminalImeInputContext', () => {
|
||||
expect(document.activeElement).toBe(document.body)
|
||||
expect(onRefocusSkipped).toHaveBeenCalledWith(document.body)
|
||||
})
|
||||
|
||||
it('skips non-macOS platforms', () => {
|
||||
const helper = appendHelper()
|
||||
const blur = vi.spyOn(helper, 'blur')
|
||||
|
||||
const refreshed = refreshTerminalImeInputContext(helper, {
|
||||
isMac: false,
|
||||
scheduleRefocus: () => {
|
||||
throw new Error('should not schedule')
|
||||
}
|
||||
})
|
||||
|
||||
expect(refreshed).toBe(false)
|
||||
expect(blur).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
// useShallow skips renders, not selectors; memoize host resolution to avoid per-tab traversal and discarded objects on every publication.
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as ConnectionContext from '@/lib/connection-context'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import type { AppState } from '@/store/types'
|
||||
import {
|
||||
resetTerminalPaneHostStateMemoForTests,
|
||||
selectTerminalPaneHostState
|
||||
} from './terminal-pane-host-state'
|
||||
|
||||
const connectionIdCalls = vi.hoisted(() => ({ count: 0 }))
|
||||
|
||||
vi.mock('@/lib/connection-context', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ConnectionContext>()
|
||||
return {
|
||||
...actual,
|
||||
getConnectionIdFromState: (state: AppState, worktreeId: string | null) => {
|
||||
connectionIdCalls.count += 1
|
||||
return actual.getConnectionIdFromState(state, worktreeId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const LOCAL_WORKTREE = 'repo-1::/repo/worktrees/local'
|
||||
const OTHER_WORKTREE = 'repo-2::/repo/worktrees/other'
|
||||
|
||||
/** Only the fields the resolver touches; the memo key is the whole object. */
|
||||
function makeState(overrides: Partial<Record<string, unknown>> = {}): AppState {
|
||||
@@ -46,7 +31,6 @@ function makeState(overrides: Partial<Record<string, unknown>> = {}): AppState {
|
||||
|
||||
beforeEach(() => {
|
||||
resetTerminalPaneHostStateMemoForTests()
|
||||
connectionIdCalls.count = 0
|
||||
})
|
||||
|
||||
describe('selectTerminalPaneHostState memo', () => {
|
||||
@@ -77,21 +61,6 @@ describe('selectTerminalPaneHostState memo', () => {
|
||||
expect(after).toBe(before)
|
||||
})
|
||||
|
||||
it('resolves the host once per published state, not once per mounted tab', () => {
|
||||
const state = makeState()
|
||||
selectTerminalPaneHostState(state, LOCAL_WORKTREE)
|
||||
const afterFirstTab = connectionIdCalls.count
|
||||
expect(afterFirstTab).toBe(1)
|
||||
|
||||
for (let index = 0; index < 4; index += 1) {
|
||||
selectTerminalPaneHostState(state, LOCAL_WORKTREE)
|
||||
}
|
||||
expect(connectionIdCalls.count).toBe(afterFirstTab)
|
||||
|
||||
selectTerminalPaneHostState(state, OTHER_WORKTREE)
|
||||
expect(connectionIdCalls.count).toBe(afterFirstTab + 1)
|
||||
})
|
||||
|
||||
it('re-resolves when the published state changes, so a host change is never stale', () => {
|
||||
const local = makeState()
|
||||
const before = selectTerminalPaneHostState(local, LOCAL_WORKTREE)
|
||||
@@ -108,31 +77,4 @@ describe('selectTerminalPaneHostState memo', () => {
|
||||
expect(after).not.toBe(before)
|
||||
expect(after.sshReconnectTargetId).toBe('ssh-host-a')
|
||||
})
|
||||
|
||||
it('allocates nothing across 1,000 publications at 6 worktrees x 5 tabs', () => {
|
||||
const worktreeIds = Array.from({ length: 6 }, (_, index) => `repo-${index}::/repo/wt-${index}`)
|
||||
const firstState = makeState()
|
||||
const firstByWorktree = new Map(
|
||||
worktreeIds.map((worktreeId) => [
|
||||
worktreeId,
|
||||
selectTerminalPaneHostState(firstState, worktreeId)
|
||||
])
|
||||
)
|
||||
connectionIdCalls.count = 0
|
||||
let allocations = 0
|
||||
|
||||
for (let publication = 0; publication < 1_000; publication += 1) {
|
||||
const state = makeState({ agentStatusEpoch: publication })
|
||||
for (const worktreeId of worktreeIds) {
|
||||
for (let tab = 0; tab < 5; tab += 1) {
|
||||
if (selectTerminalPaneHostState(state, worktreeId) !== firstByWorktree.get(worktreeId)) {
|
||||
allocations += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(allocations).toBe(0)
|
||||
expect(connectionIdCalls.count).toBe(6 * 1_000)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,7 +6,6 @@ import { BACKGROUND_WORKTREE_MEASURE_WINDOW_MS } from '../terminal/background-te
|
||||
import {
|
||||
_resetTerminalPaneSplitRequestRoutingForTests,
|
||||
cancelQueuedTerminalPaneSplitRequests,
|
||||
dispatchTerminalPaneSplitRequest,
|
||||
hasTerminalPaneSplitMountLease,
|
||||
queueTerminalPaneSplitRequest,
|
||||
registerTerminalPaneSplitRequestHandler,
|
||||
@@ -37,16 +36,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('parked terminal split request routing', () => {
|
||||
it('demonstrates that the legacy fire-and-forget event is lost before a parked pane mounts', () => {
|
||||
const handler = vi.fn()
|
||||
|
||||
dispatchTerminalPaneSplitRequest(splitRequest('tab-parked'))
|
||||
const unregister = registerTerminalPaneSplitRequestHandler('tab-parked', undefined, handler)
|
||||
|
||||
expect(handler).not.toHaveBeenCalled()
|
||||
unregister()
|
||||
})
|
||||
|
||||
it('replays a parked-tab request as soon as that exact pane lifecycle registers', () => {
|
||||
const request = splitRequest('tab-parked', 91)
|
||||
const splitPane = vi.fn()
|
||||
@@ -74,10 +63,6 @@ describe('parked terminal split request routing', () => {
|
||||
unregister()
|
||||
})
|
||||
|
||||
it('fails closed when a remount no longer contains the stable source leaf', () => {
|
||||
expect(resolveTerminalPaneSplitSourceId(splitRequest('tab-parked', 91), () => null)).toBe(-1)
|
||||
})
|
||||
|
||||
it('keeps the mount lease through replay, then releases it at the existing measure bound', () => {
|
||||
queueTerminalPaneSplitRequest(splitRequest('tab-parked'))
|
||||
takeQueuedTerminalPaneSplitRequests('tab-parked')
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import type * as ReactModule from 'react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useTerminalFontZoom } from './useTerminalFontZoom'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
captureScrollState: vi.fn(() => ({ wasAtBottom: true })),
|
||||
restoreScrollState: vi.fn(),
|
||||
safeFit: vi.fn(),
|
||||
dispatchZoomLevelChanged: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('react', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ReactModule>()
|
||||
return {
|
||||
...actual,
|
||||
useEffect: (effect: () => void | (() => void)) => effect()
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/lib/pane-manager/pane-tree-ops', () => ({
|
||||
captureScrollState: mocks.captureScrollState,
|
||||
restoreScrollState: mocks.restoreScrollState,
|
||||
safeFit: mocks.safeFit
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/zoom-events', () => ({
|
||||
dispatchZoomLevelChanged: mocks.dispatchZoomLevelChanged
|
||||
}))
|
||||
|
||||
describe('useTerminalFontZoom', () => {
|
||||
let terminalZoomListeners: ((direction: 'in' | 'out' | 'reset') => void)[]
|
||||
|
||||
beforeEach(() => {
|
||||
terminalZoomListeners = []
|
||||
document.body.replaceChildren()
|
||||
vi.clearAllMocks()
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
ui: {
|
||||
onTerminalZoom: vi.fn((listener: (direction: 'in' | 'out' | 'reset') => void) => {
|
||||
terminalZoomListeners.push(listener)
|
||||
return () => {}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function useMountedTerminalFontZoom(activeElement: HTMLElement): {
|
||||
terminal: { options: { fontSize?: number } }
|
||||
listener: (direction: 'in' | 'out' | 'reset') => void
|
||||
} {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
container.appendChild(activeElement)
|
||||
activeElement.focus()
|
||||
const terminal = { options: { fontSize: 14 } }
|
||||
useTerminalFontZoom({
|
||||
isActive: true,
|
||||
containerRef: { current: container },
|
||||
managerRef: {
|
||||
current: {
|
||||
getActivePane: () => ({ id: 1, terminal })
|
||||
}
|
||||
} as never,
|
||||
paneFontSizesRef: { current: new Map() },
|
||||
settingsRef: { current: { terminalFontSize: 14 } }
|
||||
})
|
||||
const listener = terminalZoomListeners.at(-1)
|
||||
expect(listener).toBeTypeOf('function')
|
||||
return { terminal, listener: listener as (direction: 'in' | 'out' | 'reset') => void }
|
||||
}
|
||||
|
||||
it('ignores zoom events when terminal input no longer owns focus', () => {
|
||||
const button = document.createElement('button')
|
||||
const { listener, terminal } = useMountedTerminalFontZoom(button)
|
||||
|
||||
listener('in')
|
||||
|
||||
expect(terminal.options.fontSize).toBe(14)
|
||||
expect(mocks.safeFit).not.toHaveBeenCalled()
|
||||
expect(mocks.dispatchZoomLevelChanged).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('applies terminal font zoom while the xterm helper textarea owns focus', () => {
|
||||
const helper = document.createElement('textarea')
|
||||
helper.className = 'xterm-helper-textarea'
|
||||
const { listener, terminal } = useMountedTerminalFontZoom(helper)
|
||||
|
||||
listener('in')
|
||||
|
||||
expect(terminal.options.fontSize).toBe(15)
|
||||
expect(mocks.safeFit).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.dispatchZoomLevelChanged).toHaveBeenCalledWith('terminal', 107)
|
||||
})
|
||||
|
||||
it('only lets the pane owning the focused helper apply terminal font zoom', () => {
|
||||
const inactiveContainer = document.createElement('div')
|
||||
const activeContainer = document.createElement('div')
|
||||
const focusedHelper = document.createElement('textarea')
|
||||
focusedHelper.className = 'xterm-helper-textarea'
|
||||
document.body.append(inactiveContainer, activeContainer)
|
||||
activeContainer.appendChild(focusedHelper)
|
||||
focusedHelper.focus()
|
||||
|
||||
const inactiveTerminal = { options: { fontSize: 14 } }
|
||||
const activeTerminal = { options: { fontSize: 14 } }
|
||||
useTerminalFontZoom({
|
||||
isActive: true,
|
||||
containerRef: { current: inactiveContainer },
|
||||
managerRef: {
|
||||
current: {
|
||||
getActivePane: () => ({ id: 1, terminal: inactiveTerminal })
|
||||
}
|
||||
} as never,
|
||||
paneFontSizesRef: { current: new Map() },
|
||||
settingsRef: { current: { terminalFontSize: 14 } }
|
||||
})
|
||||
useTerminalFontZoom({
|
||||
isActive: true,
|
||||
containerRef: { current: activeContainer },
|
||||
managerRef: {
|
||||
current: {
|
||||
getActivePane: () => ({ id: 2, terminal: activeTerminal })
|
||||
}
|
||||
} as never,
|
||||
paneFontSizesRef: { current: new Map() },
|
||||
settingsRef: { current: { terminalFontSize: 14 } }
|
||||
})
|
||||
|
||||
for (const listener of terminalZoomListeners) {
|
||||
listener('in')
|
||||
}
|
||||
|
||||
expect(inactiveTerminal.options.fontSize).toBe(14)
|
||||
expect(activeTerminal.options.fontSize).toBe(15)
|
||||
expect(mocks.safeFit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,141 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { getStateMock, closeWebRuntimeSessionTabMock, isWebRuntimeSessionActiveMock } = vi.hoisted(
|
||||
() => ({
|
||||
getStateMock: vi.fn(),
|
||||
closeWebRuntimeSessionTabMock: vi.fn(),
|
||||
isWebRuntimeSessionActiveMock: vi.fn(() => false)
|
||||
})
|
||||
)
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: { getState: getStateMock }
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/web-runtime-session-tab-lifecycle', () => ({
|
||||
activateWebRuntimeSessionTab: vi.fn(),
|
||||
closeWebRuntimeSessionTab: closeWebRuntimeSessionTabMock
|
||||
}))
|
||||
vi.mock('@/runtime/web-runtime-terminal-creation', () => ({
|
||||
createWebRuntimeSessionTerminal: vi.fn()
|
||||
}))
|
||||
vi.mock('@/runtime/web-runtime-session-environment', () => ({
|
||||
isWebRuntimeSessionActive: isWebRuntimeSessionActiveMock
|
||||
}))
|
||||
vi.mock('../../../../shared/terminal-surface-id', () => ({
|
||||
toHostSessionTabId: vi.fn((tabId: string) => tabId)
|
||||
}))
|
||||
|
||||
vi.mock('@/runtime/web-session-tabs-sync/tracking-mappings', () => ({
|
||||
resolveHostSessionTabIdForWebSessionTab: vi.fn(() => null)
|
||||
}))
|
||||
|
||||
import { closeTerminalTab } from './terminal-tab-actions'
|
||||
|
||||
function baseState(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
settings: { activeRuntimeEnvironmentId: null, confirmClosePinnedTab: true },
|
||||
repos: [{ id: 'repo', executionHostId: 'local', connectionId: null }],
|
||||
worktreesByRepo: { repo: [{ id: 'wt', repoId: 'repo' }] },
|
||||
tabsByWorktree: { wt: [{ id: 'terminal-1' }] },
|
||||
unifiedTabsByWorktree: {},
|
||||
activeWorktreeId: 'wt',
|
||||
activeTabId: 'terminal-1',
|
||||
openFiles: [],
|
||||
browserTabsByWorktree: {},
|
||||
reconcileWorktreeTabModel: vi.fn(() => ({ renderableTabCount: 0 })),
|
||||
closeTab: vi.fn(),
|
||||
closeUnifiedTab: vi.fn(),
|
||||
setActiveFile: vi.fn(),
|
||||
setActiveBrowserTab: vi.fn(),
|
||||
setActiveTabType: vi.fn(),
|
||||
setActiveTab: vi.fn(),
|
||||
setActiveWorktree: vi.fn(),
|
||||
requestPinnedTabCloseConfirm: vi.fn(),
|
||||
createTab: vi.fn(),
|
||||
closeFile: vi.fn(),
|
||||
closeBrowserTab: vi.fn(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('closeTerminalTab kill-all routing', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
isWebRuntimeSessionActiveMock.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('force-closes a pinned terminal without opening a second confirmation', () => {
|
||||
const closeTab = vi.fn()
|
||||
const closeUnifiedTab = vi.fn()
|
||||
const requestPinnedTabCloseConfirm = vi.fn()
|
||||
getStateMock.mockReturnValue(
|
||||
baseState({
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {
|
||||
wt: [
|
||||
{
|
||||
id: 'visible-pinned',
|
||||
entityId: 'terminal-1',
|
||||
contentType: 'terminal',
|
||||
isPinned: true
|
||||
}
|
||||
]
|
||||
},
|
||||
closeTab,
|
||||
closeUnifiedTab,
|
||||
requestPinnedTabCloseConfirm
|
||||
})
|
||||
)
|
||||
|
||||
closeTerminalTab('terminal-1', { force: true })
|
||||
|
||||
expect(requestPinnedTabCloseConfirm).not.toHaveBeenCalled()
|
||||
expect(closeTab).toHaveBeenCalledWith('terminal-1', { reason: undefined })
|
||||
expect(closeUnifiedTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes the last active terminal to an existing editor without closing it', () => {
|
||||
const state = baseState({
|
||||
openFiles: [{ id: 'editor-1', worktreeId: 'wt' }]
|
||||
})
|
||||
getStateMock.mockReturnValue(state)
|
||||
|
||||
closeTerminalTab('terminal-1', { force: true })
|
||||
|
||||
expect(state.closeTab).toHaveBeenCalledWith('terminal-1')
|
||||
expect(state.setActiveFile).toHaveBeenCalledWith('editor-1')
|
||||
expect(state.setActiveTabType).toHaveBeenCalledWith('editor', 'wt')
|
||||
expect(state.closeFile).not.toHaveBeenCalled()
|
||||
expect(state.closeBrowserTab).not.toHaveBeenCalled()
|
||||
expect(state.setActiveWorktree).not.toHaveBeenCalled()
|
||||
expect(state.createTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes the last active terminal to an existing browser when no editor exists', () => {
|
||||
const state = baseState({
|
||||
browserTabsByWorktree: { wt: [{ id: 'browser-1' }] }
|
||||
})
|
||||
getStateMock.mockReturnValue(state)
|
||||
|
||||
closeTerminalTab('terminal-1', { force: true })
|
||||
|
||||
expect(state.setActiveBrowserTab).toHaveBeenCalledWith('browser-1')
|
||||
expect(state.setActiveTabType).toHaveBeenCalledWith('browser', 'wt')
|
||||
expect(state.closeBrowserTab).not.toHaveBeenCalled()
|
||||
expect(state.setActiveWorktree).not.toHaveBeenCalled()
|
||||
expect(state.createTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('deactivates after the last active terminal when no other content exists', () => {
|
||||
const state = baseState()
|
||||
getStateMock.mockReturnValue(state)
|
||||
|
||||
closeTerminalTab('terminal-1', { force: true })
|
||||
|
||||
expect(state.setActiveWorktree).toHaveBeenCalledWith(null)
|
||||
expect(state.setActiveFile).not.toHaveBeenCalled()
|
||||
expect(state.setActiveBrowserTab).not.toHaveBeenCalled()
|
||||
expect(state.createTab).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -84,40 +84,6 @@ describe('unsaved-changes prompt for a closing file', () => {
|
||||
})
|
||||
afterEach(() => cleanup())
|
||||
|
||||
it('reveals a floating note in the floating panel without moving the main window', () => {
|
||||
store.openFiles = [dirtyFile('note', FLOATING_TERMINAL_WORKTREE_ID)]
|
||||
const { result } = mountCloseQueue()
|
||||
|
||||
act(() => requestEditorFileClose('note'))
|
||||
|
||||
expect(result.current.saveDialogFileId).toBe('note')
|
||||
expect(revealFloatingWorkspacePanel).toHaveBeenCalledOnce()
|
||||
expect(setActiveWorktree).not.toHaveBeenCalled()
|
||||
expect(setActiveTabType).toHaveBeenCalledWith('editor', FLOATING_TERMINAL_WORKTREE_ID)
|
||||
})
|
||||
|
||||
it('still switches to a background worktree to show its file', () => {
|
||||
store.openFiles = [dirtyFile('readme', 'wt-other')]
|
||||
mountCloseQueue()
|
||||
|
||||
act(() => requestEditorFileClose('readme'))
|
||||
|
||||
expect(setActiveWorktree).toHaveBeenCalledWith('wt-other')
|
||||
expect(revealFloatingWorkspacePanel).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('runs the close reaction once the file is discarded', async () => {
|
||||
store.openFiles = [dirtyFile('note', FLOATING_TERMINAL_WORKTREE_ID)]
|
||||
const onClosed = vi.fn()
|
||||
const { result } = mountCloseQueue()
|
||||
act(() => requestEditorFileClose('note', { onClosed }))
|
||||
|
||||
await act(() => result.current.handleSaveDialogDiscard())
|
||||
|
||||
expect(closeFile).toHaveBeenCalledWith('note')
|
||||
expect(onClosed).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
// Why a later close of the same file: a cancelled reaction left behind would fire on it.
|
||||
it('drops the close reaction when the prompt is cancelled', async () => {
|
||||
vi.useFakeTimers()
|
||||
|
||||
@@ -73,19 +73,6 @@ describe('createClientWorkspaceViewReporter', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('refreshes the host’s copy at most every ten seconds of activity', () => {
|
||||
const { sent, report } = reporter()
|
||||
report.update(STATE, ['local-wt'], [])
|
||||
vi.setSystemTime(Date.now() + 9_000)
|
||||
report.renewPresence()
|
||||
expect(sent).toHaveLength(1)
|
||||
|
||||
vi.setSystemTime(Date.now() + 1_000)
|
||||
report.renewPresence()
|
||||
expect(sent).toHaveLength(2)
|
||||
expect(sent[1].view.presentForMs).toBe(190_000)
|
||||
})
|
||||
|
||||
it('reports an away user’s screen with no presence, and renews as soon as input returns', () => {
|
||||
const { sent, report } = reporter()
|
||||
presence.remaining = 0
|
||||
@@ -98,16 +85,6 @@ describe('createClientWorkspaceViewReporter', () => {
|
||||
expect(sent).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('refreshes where presence is not tracked too, so a reconnected host learns the view', () => {
|
||||
const { sent, report } = reporter()
|
||||
presence.remaining = null
|
||||
report.update(STATE, ['local-wt'], [])
|
||||
vi.setSystemTime(Date.now() + 10_000)
|
||||
report.renewPresence()
|
||||
expect(sent).toHaveLength(2)
|
||||
expect(sent[1].view.presentForMs).toBeNull()
|
||||
})
|
||||
|
||||
it('retries a failed send, empty or not, no sooner than a refresh', async () => {
|
||||
const sent: ClientWorkspaceView[] = []
|
||||
let fail = true
|
||||
|
||||
@@ -121,24 +121,4 @@ describe('live agent-status replay notifications', () => {
|
||||
1_700_000_004_000
|
||||
)
|
||||
})
|
||||
|
||||
it('seeds a stamped working replay when this renderer holds no row', () => {
|
||||
const apply = createApplicator()
|
||||
|
||||
apply(status({ state: 'working', isReplay: true, turnCompletedAt: 1_700_000_004_000 }))
|
||||
|
||||
expect(mocks.observe.mock.calls[0][0]).toHaveProperty('seedOnly', true)
|
||||
})
|
||||
|
||||
it('seeds a replayed working row, and a later real done still notifies', () => {
|
||||
const apply = createApplicator()
|
||||
|
||||
apply(status({ state: 'working', isReplay: true }))
|
||||
apply(status({ state: 'done', receivedAt: 1_700_000_006_000 }))
|
||||
|
||||
expect(mocks.observe).toHaveBeenCalledTimes(2)
|
||||
expect(mocks.observe.mock.calls[0][0]).toHaveProperty('seedOnly', true)
|
||||
expect(mocks.observe.mock.calls[1][0]).not.toHaveProperty('seedOnly')
|
||||
expect(mocks.observe.mock.calls[1][0].payload).toHaveProperty('state', 'done')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,15 +123,6 @@ describe('useReadOnScreenAttention — clock-skewed execution host', () => {
|
||||
expect(calls).toEqual([])
|
||||
})
|
||||
|
||||
it('still acks a normally stamped turn once', () => {
|
||||
seedFutureStampedTurn(NOW - 5_000)
|
||||
const calls = instrumentAcknowledgeAgents()
|
||||
|
||||
renderHook(() => useReadOnScreenAttention())
|
||||
|
||||
expect(calls).toEqual([[PANE_KEY]])
|
||||
})
|
||||
|
||||
it('keeps an explicitly marked-unread visible turn unread', () => {
|
||||
seedFutureStampedTurn(NOW - 5_000)
|
||||
useAppStore.getState().acknowledgeAgents([PANE_KEY])
|
||||
|
||||
-141
@@ -1,141 +0,0 @@
|
||||
import type { Terminal } from '@xterm/xterm'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
installTerminalLinkifierHoverResetOnMouseLeave,
|
||||
installTerminalLinkifierHoverResetOnWindowBlur
|
||||
} from './terminal-linkifier-hover-reset-on-mouseleave'
|
||||
|
||||
type FakeLinkifier = {
|
||||
_lastBufferCell?: unknown
|
||||
_activeLine?: number
|
||||
_clearCurrentLink?: () => void
|
||||
_currentLink?: unknown
|
||||
}
|
||||
|
||||
function createHarness(hasScreen = true) {
|
||||
let mouseLeaveHandler: (() => void) | null = null
|
||||
let blurHandler: (() => void) | null = null
|
||||
const addEventListener = vi.fn((_event: string, handler: () => void) => {
|
||||
if (_event === 'mouseleave') {
|
||||
mouseLeaveHandler = handler
|
||||
}
|
||||
})
|
||||
const removeEventListener = vi.fn((_event: string, handler: () => void) => {
|
||||
if (_event === 'mouseleave' && mouseLeaveHandler === handler) {
|
||||
mouseLeaveHandler = null
|
||||
}
|
||||
})
|
||||
const screen = { addEventListener, removeEventListener, classList: { remove: vi.fn() } }
|
||||
const querySelector = vi.fn(() => (hasScreen ? screen : null))
|
||||
const rendererWindow = {
|
||||
addEventListener: vi.fn((_event: string, handler: () => void) => {
|
||||
blurHandler = handler
|
||||
}),
|
||||
removeEventListener: vi.fn((_event: string, handler: () => void) => {
|
||||
if (blurHandler === handler) {
|
||||
blurHandler = null
|
||||
}
|
||||
})
|
||||
}
|
||||
const linkifier: FakeLinkifier = {
|
||||
_lastBufferCell: { x: 2, y: 3 },
|
||||
_activeLine: 3,
|
||||
_clearCurrentLink: vi.fn(),
|
||||
_currentLink: { link: 'https://example.com' }
|
||||
}
|
||||
const terminal = {
|
||||
element: { querySelector, ownerDocument: { defaultView: rendererWindow } },
|
||||
_core: { linkifier }
|
||||
} as unknown as Terminal
|
||||
const linkTooltip = {
|
||||
ownerDocument: { defaultView: rendererWindow },
|
||||
style: { display: '' }
|
||||
} as unknown as HTMLElement
|
||||
return {
|
||||
terminal,
|
||||
linkifier,
|
||||
linkTooltip,
|
||||
rendererWindow,
|
||||
querySelector,
|
||||
addEventListener,
|
||||
removeEventListener,
|
||||
dispatchMouseLeave: () => mouseLeaveHandler?.(),
|
||||
dispatchBlur: () => blurHandler?.()
|
||||
}
|
||||
}
|
||||
|
||||
describe('installTerminalLinkifierHoverResetOnMouseLeave', () => {
|
||||
it('resets the hover cache when the terminal surface loses the pointer', () => {
|
||||
const harness = createHarness()
|
||||
installTerminalLinkifierHoverResetOnMouseLeave(harness.terminal, harness.linkTooltip)
|
||||
|
||||
expect(harness.querySelector).toHaveBeenCalledWith('.xterm-screen')
|
||||
expect(harness.addEventListener).toHaveBeenCalledWith('mouseleave', expect.any(Function))
|
||||
harness.dispatchMouseLeave()
|
||||
|
||||
expect(harness.linkifier._lastBufferCell).toBeUndefined()
|
||||
expect(harness.linkifier._activeLine).toBe(-1)
|
||||
expect(harness.linkifier._clearCurrentLink).toHaveBeenCalledTimes(1)
|
||||
expect(harness.linkifier._currentLink).toBeUndefined()
|
||||
expect(harness.linkTooltip.style.display).toBe('none')
|
||||
})
|
||||
|
||||
it('removes the listener on dispose', () => {
|
||||
const harness = createHarness()
|
||||
const disposable = installTerminalLinkifierHoverResetOnMouseLeave(harness.terminal)
|
||||
const mouseLeaveHandler = harness.addEventListener.mock.calls.find(
|
||||
([eventName]) => eventName === 'mouseleave'
|
||||
)?.[1]
|
||||
expect(mouseLeaveHandler).toBeTypeOf('function')
|
||||
|
||||
disposable.dispose()
|
||||
expect(harness.removeEventListener).toHaveBeenCalledWith('mouseleave', mouseLeaveHandler)
|
||||
harness.dispatchMouseLeave()
|
||||
|
||||
expect(harness.linkifier._lastBufferCell).toEqual({ x: 2, y: 3 })
|
||||
expect(harness.linkifier._activeLine).toBe(3)
|
||||
})
|
||||
|
||||
it('degrades to a no-op when the screen is unavailable', () => {
|
||||
const harness = createHarness(false)
|
||||
|
||||
expect(() =>
|
||||
installTerminalLinkifierHoverResetOnMouseLeave(harness.terminal).dispose()
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not throw when xterm linkifier internals are unavailable', () => {
|
||||
const harness = createHarness()
|
||||
const terminal = harness.terminal as unknown as { _core?: unknown }
|
||||
terminal._core = undefined
|
||||
installTerminalLinkifierHoverResetOnMouseLeave(harness.terminal)
|
||||
|
||||
expect(harness.dispatchMouseLeave).not.toThrow()
|
||||
})
|
||||
|
||||
it('clears the active link and tooltip when the renderer window blurs', () => {
|
||||
const harness = createHarness()
|
||||
const disposable = installTerminalLinkifierHoverResetOnWindowBlur(
|
||||
harness.terminal,
|
||||
harness.linkTooltip
|
||||
)
|
||||
|
||||
harness.dispatchBlur()
|
||||
|
||||
expect(harness.rendererWindow.addEventListener).toHaveBeenCalledWith(
|
||||
'blur',
|
||||
expect.any(Function)
|
||||
)
|
||||
expect(harness.linkifier._clearCurrentLink).toHaveBeenCalledTimes(1)
|
||||
expect(harness.linkifier._currentLink).toBeUndefined()
|
||||
expect(harness.linkifier._lastBufferCell).toBeUndefined()
|
||||
expect(harness.linkifier._activeLine).toBe(-1)
|
||||
expect(harness.linkTooltip.style.display).toBe('none')
|
||||
|
||||
disposable.dispose()
|
||||
expect(harness.rendererWindow.removeEventListener).toHaveBeenCalledWith(
|
||||
'blur',
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -33,10 +33,7 @@ vi.mock('@/lib/ensure-hooks-confirmed', () => ({
|
||||
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
|
||||
|
||||
import { ensureHooksConfirmed } from './ensure-hooks-confirmed'
|
||||
import {
|
||||
runBackgroundWorktreeCreation,
|
||||
retryBackgroundWorktreeCreation
|
||||
} from './worktree-creation-flow'
|
||||
import { runBackgroundWorktreeCreation } from './worktree-creation-flow'
|
||||
|
||||
function makeRequest(overrides: Partial<WorktreeCreationRequest> = {}): WorktreeCreationRequest {
|
||||
return {
|
||||
@@ -106,34 +103,4 @@ describe('background script preparation', () => {
|
||||
await flushAsyncWorktreeCreation()
|
||||
expect(store.createWorktree).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps refused scripts disabled when creating the worktree', async () => {
|
||||
vi.mocked(ensureHooksConfirmed).mockResolvedValueOnce('skip')
|
||||
runBackgroundWorktreeCreation(
|
||||
makeRequest({
|
||||
setupDecision: 'run',
|
||||
hookPreparation: { executionHostId: 'local' }
|
||||
})
|
||||
)
|
||||
await vi.waitFor(() => expect(store.createWorktree).toHaveBeenCalledOnce())
|
||||
expect(store.pendingWorktreeCreations['creation-1'].request.setupDecision).toBe('skip')
|
||||
})
|
||||
|
||||
it('retries failed preparation on the captured host', async () => {
|
||||
vi.mocked(ensureHooksConfirmed).mockRejectedValueOnce(new Error('inspection failed'))
|
||||
runBackgroundWorktreeCreation(
|
||||
makeRequest({ hookPreparation: { executionHostId: 'runtime:host-1' } })
|
||||
)
|
||||
await vi.waitFor(() =>
|
||||
expect(store.pendingWorktreeCreations['creation-1'].status).toBe('error')
|
||||
)
|
||||
expect(store.createWorktree).not.toHaveBeenCalled()
|
||||
|
||||
const trust = Promise.withResolvers<'run' | 'skip'>()
|
||||
vi.mocked(ensureHooksConfirmed).mockReturnValueOnce(trust.promise)
|
||||
retryBackgroundWorktreeCreation('creation-1')
|
||||
expect(store.pendingWorktreeCreations['creation-1'].phase).toBe('preparing')
|
||||
trust.resolve('run')
|
||||
await vi.waitFor(() => expect(store.createWorktree).toHaveBeenCalledOnce())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-types'
|
||||
import {
|
||||
getWebSessionTerminalHandleSubscriberCountForTests,
|
||||
queueAcceptedWebSessionTerminalSnapshot,
|
||||
subscribeAcceptedWebSessionTerminalHandle
|
||||
} from './web-session-terminal-handle-events'
|
||||
@@ -22,42 +21,6 @@ function snapshot(
|
||||
}
|
||||
|
||||
describe('accepted web-session terminal handle events', () => {
|
||||
it('notifies only the matching runtime/worktree/pane and releases the listener', async () => {
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = subscribeAcceptedWebSessionTerminalHandle(
|
||||
{ environmentId: 'env-1', worktreeId: 'wt-1', hostTabId: 'tab-1', leafId: 'leaf-1' },
|
||||
listener
|
||||
)
|
||||
|
||||
queueAcceptedWebSessionTerminalSnapshot(snapshot([]), 'env-2')
|
||||
queueAcceptedWebSessionTerminalSnapshot(snapshot([], 'wt-2'), 'env-1')
|
||||
queueAcceptedWebSessionTerminalSnapshot(
|
||||
snapshot([
|
||||
{
|
||||
type: 'terminal',
|
||||
id: 'tab-1::leaf-1',
|
||||
parentTabId: 'tab-1',
|
||||
leafId: 'leaf-1',
|
||||
title: 'Claude Code',
|
||||
isActive: true,
|
||||
status: 'ready',
|
||||
terminal: 'terminal-replacement'
|
||||
}
|
||||
]),
|
||||
'env-1'
|
||||
)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(listener).toHaveBeenCalledOnce()
|
||||
expect(listener).toHaveBeenCalledWith({
|
||||
surfacePresent: true,
|
||||
terminalHandle: 'terminal-replacement'
|
||||
})
|
||||
expect(getWebSessionTerminalHandleSubscriberCountForTests()).toBe(1)
|
||||
unsubscribe()
|
||||
expect(getWebSessionTerminalHandleSubscriberCountForTests()).toBe(0)
|
||||
})
|
||||
|
||||
it('distinguishes a pending handle from an explicitly removed surface', async () => {
|
||||
const listener = vi.fn()
|
||||
const unsubscribe = subscribeAcceptedWebSessionTerminalHandle(
|
||||
|
||||
@@ -91,50 +91,3 @@ it('drops the previous icons when the next selected theme fails to load', async
|
||||
await waitFor(() => expect(document.getElementById('orca-plugin-icon-theme')).toBeNull())
|
||||
expect(store.useActivePluginIconTheme.getState().theme).toBeNull()
|
||||
})
|
||||
|
||||
it('reloads the active icon theme when a same-metadata update changes its content', async () => {
|
||||
const metadata = { id: 'plugin:samples.icons/minimal', pluginKey: 'samples.icons', label: 'A' }
|
||||
let listed: { id: string; pluginKey: string; label: string; revision?: string }[] = [
|
||||
{ ...metadata, revision: 'r1' }
|
||||
]
|
||||
const listeners: ((event: PluginChangeEvent) => void)[] = []
|
||||
const loadIconTheme = vi.fn(async () => ({
|
||||
...listed[0],
|
||||
svgs: ['<svg xmlns="http://www.w3.org/2000/svg"><path fill="currentColor"/></svg>'],
|
||||
icons: { file: 0 },
|
||||
fileNames: {},
|
||||
fileExtensions: {},
|
||||
folderNames: {},
|
||||
folderNamesExpanded: {}
|
||||
}))
|
||||
vi.stubGlobal('api', {
|
||||
plugins: {
|
||||
listIconThemes: async () => listed,
|
||||
loadIconTheme,
|
||||
onChanged: (listener: (event: PluginChangeEvent) => void) => {
|
||||
listeners.push(listener)
|
||||
return () => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const store = await import('./plugin-icon-themes')
|
||||
renderHook(() => store.usePluginIconThemeActivation(metadata.id))
|
||||
await waitFor(() => expect(loadIconTheme).toHaveBeenCalledTimes(1))
|
||||
|
||||
const refresh = async () => {
|
||||
act(() => listeners.forEach((listener) => listener({ contentPacksChanged: true })))
|
||||
await act(async () => {
|
||||
await store.usePluginIconThemeStore.getState().fetchEntries()
|
||||
})
|
||||
}
|
||||
listed = [
|
||||
{ ...metadata, revision: 'r1' },
|
||||
{ ...metadata, id: 'plugin:samples.icons/other' }
|
||||
]
|
||||
await refresh()
|
||||
expect(loadIconTheme).toHaveBeenCalledTimes(1)
|
||||
|
||||
listed = [{ ...metadata, revision: 'r2' }]
|
||||
await refresh()
|
||||
await waitFor(() => expect(loadIconTheme).toHaveBeenCalledTimes(2))
|
||||
})
|
||||
|
||||
@@ -97,24 +97,6 @@ describe('a fetched catalog that drops a repo registration', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('drops the pre-host rows too once no host registers the repo id', async () => {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
repos: [serverRepo('old')],
|
||||
worktreesByRepo: {
|
||||
old: [
|
||||
serverRow('old'),
|
||||
makeWorktree({ id: 'old::/legacy', repoId: 'old', path: '/legacy' })
|
||||
]
|
||||
}
|
||||
})
|
||||
serverRepos = [serverRepo('copied')]
|
||||
|
||||
await store.getState().fetchRuntimeEnvironmentRepos('env-1')
|
||||
|
||||
expect(store.getState().worktreesByRepo.old).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the rows of a repo added while the catalog was in flight', async () => {
|
||||
const store = createTestStore()
|
||||
seedStore(store, {
|
||||
|
||||
@@ -68,27 +68,6 @@ describe('tab view mode', () => {
|
||||
seedTwoLeafTabs(store)
|
||||
})
|
||||
|
||||
it('toggles a tab to chat and back to terminal', async () => {
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBeUndefined()
|
||||
|
||||
store.getState().toggleTabViewMode('left-tab')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('chat')
|
||||
|
||||
store.getState().toggleTabViewMode('left-tab')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('terminal')
|
||||
})
|
||||
|
||||
it('setTabViewMode sets an explicit mode', async () => {
|
||||
store.getState().setTabViewMode('left-tab', 'chat')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('chat')
|
||||
store.getState().setTabViewMode('left-tab', 'terminal')
|
||||
await layoutApplyTestTransport.settle()
|
||||
expect(findTab(store, 'left-tab')?.viewMode).toBe('terminal')
|
||||
})
|
||||
|
||||
it('mutates only the targeted tab, leaving a split sibling unchanged', async () => {
|
||||
store.getState().toggleTabViewMode('left-tab')
|
||||
await layoutApplyTestTransport.settle()
|
||||
@@ -97,12 +76,6 @@ describe('tab view mode', () => {
|
||||
expect(findTab(store, 'right-tab')?.viewMode).toBeUndefined()
|
||||
})
|
||||
|
||||
it('is a no-op for an unknown tab id', () => {
|
||||
const before = store.getState().unifiedTabsByWorktree[WT]
|
||||
store.getState().toggleTabViewMode('missing-tab')
|
||||
expect(store.getState().unifiedTabsByWorktree[WT]).toBe(before)
|
||||
})
|
||||
|
||||
// Local toggles must mirror viewMode onto terminal rows used by recovery, matching host sync.
|
||||
describe('mirrors onto the terminal row', () => {
|
||||
function terminalRow(tabId: string) {
|
||||
|
||||
@@ -52,24 +52,6 @@ describe('row writers touch only the copy on their own host', () => {
|
||||
expect(rows).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('a server create leaves the local twin alone', () => {
|
||||
const store = createTestStore()
|
||||
const { local } = seedTwins(store)
|
||||
applyCreatedWorktree(
|
||||
store.setState,
|
||||
REPO_ID,
|
||||
{
|
||||
worktree: makeWorktree({ id: WORKTREE_ID, repoId: REPO_ID, displayName: 'fresh remote' })
|
||||
} satisfies CreateWorktreeResult,
|
||||
'runtime:hub'
|
||||
)
|
||||
const rows = store.getState().worktreesByRepo[REPO_ID]!
|
||||
expect(rows.find((row) => !row.runtimeOwnerEnvironmentId)).toEqual(local)
|
||||
expect(rows.find((row) => row.runtimeOwnerEnvironmentId === 'hub')?.displayName).toBe(
|
||||
'fresh remote'
|
||||
)
|
||||
})
|
||||
|
||||
it("a server's lineage reply updates its own copy, never the focused local twin", () => {
|
||||
const store = createTestStore()
|
||||
const { local } = seedTwins(store)
|
||||
|
||||
@@ -114,14 +114,6 @@ it("keeps only a moved project's chat tab through hydration and persistence", ()
|
||||
expect(persisted.unifiedTabs?.[MOVED]?.map((entry) => entry.id)).toEqual([CHAT])
|
||||
})
|
||||
|
||||
it("never treats a paired host's partition as the moved local workspace", () => {
|
||||
const session = movedSession()
|
||||
expect(
|
||||
collectWslMovedWorkspaceKeysFromSession(session, projects, { [MOVED]: 'runtime:wsl-ubuntu' })
|
||||
).toEqual([])
|
||||
expect(collectWslMovedWorkspaceKeysFromSession(session, [])).toEqual([])
|
||||
})
|
||||
|
||||
it("lists the moved chats under their project, and never the host's listed worktree", () => {
|
||||
const store = createTestStore()
|
||||
hydrate(store, movedSession())
|
||||
|
||||
@@ -2,10 +2,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type * as AgentStatusModule from '@/lib/agent-status'
|
||||
import { createTestStore, makeWorktree, seedStore } from '../slices/store-test-helpers'
|
||||
import { createStoreCascadesMockApi } from '../slices/store-cascades-test-harness'
|
||||
import {
|
||||
_resetLayoutOwnerPaneIntentsForTests,
|
||||
isTabPaneTreeHeld
|
||||
} from '@/runtime/layout-owner-pane-intents'
|
||||
import { _resetLayoutOwnerPaneIntentsForTests } from '@/runtime/layout-owner-pane-intents'
|
||||
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import { LayoutApplyParams } from '../../../../shared/rpc-contract/workspace-layout-params'
|
||||
import { applyLayoutCommand } from '../../../../shared/workspace-layout/workspace-layout-commands'
|
||||
@@ -116,24 +114,4 @@ describe('the chat pane in a layout-owner build', () => {
|
||||
applySent()
|
||||
expect(mainTab(tabId).panes.chatLeafId).toBeUndefined()
|
||||
})
|
||||
|
||||
it('holds frames off the tab until main answers, so a frame cannot undo the pick', () => {
|
||||
const tabId = heldTab()
|
||||
mocks.callRuntimeRpc.mockImplementationOnce(() => new Promise(() => {}))
|
||||
|
||||
setChatLeaf(tabId, LEAF)
|
||||
|
||||
expect(isTabPaneTreeHeld(tabId)).toBe(true)
|
||||
})
|
||||
|
||||
it('sends nothing for a write that keeps the chat pane, or with the switch off', () => {
|
||||
const tabId = heldTab()
|
||||
const layout = store.getState().terminalLayoutsByTabId[tabId]!
|
||||
store.getState().setTabLayout(tabId, { ...layout, expandedLeafId: LEAF })
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
|
||||
mocks.layoutOwnerIsMain = false
|
||||
setChatLeaf(tabId, LEAF)
|
||||
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -8,32 +8,6 @@ import { createTestStore, makeLayout } from '../slices/store-test-helpers'
|
||||
afterEach(resetTerminalInputActivityCoalescingForTests)
|
||||
|
||||
describe('terminal no-op subscriber budget', () => {
|
||||
it('does not publish missing-entry cleanup and restart actions', () => {
|
||||
const store = createTestStore()
|
||||
const before = store.getState()
|
||||
const listener = vi.fn()
|
||||
store.subscribe(listener)
|
||||
|
||||
for (let i = 0; i < 25; i += 1) {
|
||||
const s = store.getState()
|
||||
s.replaceTerminalLayoutPanePtyId('missing', 'leaf', 'pty')
|
||||
expect(s.consumeSuppressedPtyExit('missing')).toBe(false)
|
||||
expect(s.consumePendingPaneAccountRestart('missing')).toBeNull()
|
||||
s.clearCodexRestartNotice('missing')
|
||||
s.dismissCodexRestartNotices(['missing'])
|
||||
s.reopenCodexRestartPrompt('missing')
|
||||
s.markNativeChatLaunchPromptFailed('missing')
|
||||
s.clearNativeChatLaunchPrompt('missing')
|
||||
s.markNativeChatLaunchDraftAdopted('missing')
|
||||
s.resolveNativeChatLaunchDraft('missing', { text: 'draft', createdAt: 1 })
|
||||
s.clearNativeChatLaunchDraft('missing')
|
||||
s.removeDeferredSshSessionId('missing')
|
||||
}
|
||||
|
||||
expect(listener).not.toHaveBeenCalled()
|
||||
expect(store.getState()).toBe(before)
|
||||
})
|
||||
|
||||
it('publishes real mutations once and keeps repeated actions silent', () => {
|
||||
const store = createTestStore()
|
||||
const draft = { tabId: 'tab', agent: 'codex', text: 'draft', createdAt: 1 } as const
|
||||
@@ -81,23 +55,6 @@ describe('terminal no-op subscriber budget', () => {
|
||||
expect(store.getState().nativeChatLaunchDraftByTabId.tab).toBe(draft)
|
||||
})
|
||||
|
||||
it('consumes real restart entries and leaves repeated consumes silent', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().suppressPtyExit('pty')
|
||||
store.getState().queuePaneAccountRestarts(['pty'], 'codex')
|
||||
const listener = vi.fn()
|
||||
store.subscribe(listener)
|
||||
|
||||
expect(store.getState().consumeSuppressedPtyExit('pty')).toBe(true)
|
||||
expect(store.getState().consumePendingPaneAccountRestart('pty')).toBe('codex')
|
||||
expect(listener).toHaveBeenCalledTimes(2)
|
||||
const before = store.getState()
|
||||
expect(store.getState().consumeSuppressedPtyExit('pty')).toBe(false)
|
||||
expect(store.getState().consumePendingPaneAccountRestart('pty')).toBeNull()
|
||||
expect(listener).toHaveBeenCalledTimes(2)
|
||||
expect(store.getState()).toBe(before)
|
||||
})
|
||||
|
||||
it('drops a trailing input flush after pane teardown without publishing', () => {
|
||||
const store = createTestStore()
|
||||
store.getState().recordTerminalInput('tab:leaf', 1000)
|
||||
@@ -113,34 +70,4 @@ describe('terminal no-op subscriber budget', () => {
|
||||
expect(store.getState()).toBe(before)
|
||||
expect(store.getState().lastTerminalInputAtByPaneKey['tab:leaf']).toBeUndefined()
|
||||
})
|
||||
|
||||
it('dismisses, reopens and clears restart notices without replaying no-op notifications', () => {
|
||||
const store = createTestStore()
|
||||
store
|
||||
.getState()
|
||||
.markCodexRestartNotices([
|
||||
{ ptyId: 'pty', previousAccountLabel: 'old', nextAccountLabel: 'new' }
|
||||
])
|
||||
const listener = vi.fn()
|
||||
store.subscribe(listener)
|
||||
const actions = [
|
||||
() => store.getState().dismissCodexRestartNotices(['pty']),
|
||||
() => store.getState().reopenCodexRestartPrompt('pty'),
|
||||
() => store.getState().clearCodexRestartNotice('pty')
|
||||
]
|
||||
for (const [index, action] of actions.entries()) {
|
||||
if (index === 1) {
|
||||
store.getState().queuePaneAccountRestarts(['pty'], 'codex')
|
||||
}
|
||||
listener.mockClear()
|
||||
action()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
const before = store.getState()
|
||||
action()
|
||||
expect(listener).toHaveBeenCalledTimes(1)
|
||||
expect(store.getState()).toBe(before)
|
||||
}
|
||||
expect(store.getState().codexRestartNoticeByPtyId.pty).toBeUndefined()
|
||||
expect(store.getState().pendingPaneAccountRestarts.pty).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -50,11 +50,6 @@ vi.mock('../../runtime/runtime-rpc-client', () => ({
|
||||
assertRuntimeEnvironmentCapability: vi.fn(),
|
||||
callRuntimeRpc: rpc
|
||||
}))
|
||||
|
||||
const savedState = {
|
||||
filters: [{ id: 'mine', name: 'My work', jql: 'project = APP' }],
|
||||
activeFilterId: 'mine'
|
||||
}
|
||||
const quickState = { statuses: ['In Progress'] }
|
||||
const BOTH = [JIRA_SAVED_FILTER_RUNTIME_CAPABILITY, JIRA_STATUS_FILTER_RUNTIME_CAPABILITY]
|
||||
|
||||
@@ -69,26 +64,6 @@ beforeEach(() => {
|
||||
})
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
it('reads and writes Jira filter state on the paired host from the local target', async () => {
|
||||
runtime.status.mockResolvedValue({ capabilities: BOTH })
|
||||
runtime.call.mockImplementation(async (method: string) =>
|
||||
method.includes('Saved') ? savedState : quickState
|
||||
)
|
||||
|
||||
expect(await readJiraSavedFilterState({ kind: 'local' })).toEqual(savedState)
|
||||
await mutateJiraSavedFilters({ kind: 'local' }, { kind: 'select', id: 'mine' })
|
||||
await readJiraQuickFilterState({ kind: 'local' })
|
||||
await writeJiraQuickFilterState({ kind: 'local' }, quickState)
|
||||
|
||||
expect(runtime.call.mock.calls).toEqual([
|
||||
['ui.getJiraSavedFilters', undefined, 15_000],
|
||||
['ui.mutateJiraSavedFilters', { kind: 'select', id: 'mine' }, 15_000],
|
||||
['ui.getJiraQuickFilters', undefined, 15_000],
|
||||
['ui.setJiraQuickFilters', quickState, 15_000]
|
||||
])
|
||||
expect(rpc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports update-required for an older host and never sends the new methods', async () => {
|
||||
runtime.status.mockResolvedValue({ capabilities: [] })
|
||||
|
||||
@@ -107,17 +82,6 @@ it('reports update-required for an older host and never sends the new methods',
|
||||
expect(runtime.call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects host failures instead of returning empty or unsaved state', async () => {
|
||||
runtime.status.mockRejectedValueOnce(new Error('Connection lost'))
|
||||
await expect(readJiraSavedFilterState({ kind: 'local' })).rejects.toThrow('Connection lost')
|
||||
|
||||
runtime.status.mockResolvedValue({ capabilities: BOTH })
|
||||
runtime.call.mockRejectedValue(new Error('Host write failed'))
|
||||
await expect(writeJiraQuickFilterState({ kind: 'local' }, quickState)).rejects.toThrow(
|
||||
'Host write failed'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not send to a host paired in while the capability probe was in flight', async () => {
|
||||
runtime.status.mockResolvedValue({ capabilities: BOTH })
|
||||
runtime.idAfterProbe = 'host-b'
|
||||
@@ -127,17 +91,3 @@ it('does not send to a host paired in while the capability probe was in flight',
|
||||
).rejects.toThrow('Pairing changed')
|
||||
expect(runtime.call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps explicit environment targets on the environment RPC path', async () => {
|
||||
rpc.mockResolvedValue(savedState)
|
||||
|
||||
await readJiraSavedFilterState({ kind: 'environment', environmentId: 'host-c' })
|
||||
|
||||
expect(rpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'host-c' },
|
||||
'ui.getJiraSavedFilters',
|
||||
undefined,
|
||||
{ timeoutMs: 15_000 }
|
||||
)
|
||||
expect(runtime.call).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -1,140 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { copyClipboardTextViaExecCommand } from './web-clipboard-copy-fallback'
|
||||
|
||||
type FakeDocOptions = {
|
||||
dispatchesCopyEvent?: boolean
|
||||
execCommandResult?: boolean
|
||||
execCommandThrows?: boolean
|
||||
withClipboardData?: boolean
|
||||
}
|
||||
|
||||
function createFakeDocument(options?: FakeDocOptions) {
|
||||
const listeners: ((event: unknown) => void)[] = []
|
||||
const clipboardData = { setData: vi.fn() }
|
||||
const stopImmediatePropagation = vi.fn()
|
||||
const createElement = vi.fn()
|
||||
const appendChild = vi.fn()
|
||||
const execCommand = vi.fn((command: string) => {
|
||||
if (options?.execCommandThrows) {
|
||||
throw new Error('execCommand denied')
|
||||
}
|
||||
if (command !== 'copy') {
|
||||
return false
|
||||
}
|
||||
if (options?.dispatchesCopyEvent ?? true) {
|
||||
for (const listener of listeners.slice()) {
|
||||
listener({
|
||||
clipboardData: (options?.withClipboardData ?? true) ? clipboardData : undefined,
|
||||
preventDefault: vi.fn(),
|
||||
stopImmediatePropagation
|
||||
})
|
||||
}
|
||||
}
|
||||
return options?.execCommandResult ?? true
|
||||
})
|
||||
const doc = {
|
||||
activeElement: { focus: vi.fn() },
|
||||
createElement,
|
||||
execCommand,
|
||||
addEventListener: vi.fn((type: string, listener: (event: unknown) => void) => {
|
||||
if (type === 'copy') {
|
||||
listeners.push(listener)
|
||||
}
|
||||
}),
|
||||
removeEventListener: vi.fn((type: string, listener: (event: unknown) => void) => {
|
||||
if (type === 'copy') {
|
||||
const index = listeners.indexOf(listener)
|
||||
if (index !== -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}),
|
||||
body: { appendChild }
|
||||
} as unknown as Document
|
||||
|
||||
return {
|
||||
doc,
|
||||
clipboardData,
|
||||
stopImmediatePropagation,
|
||||
createElement,
|
||||
appendChild,
|
||||
execCommand,
|
||||
listeners
|
||||
}
|
||||
}
|
||||
|
||||
describe('copyClipboardTextViaExecCommand', () => {
|
||||
it('serves the text from the copy event without touching the DOM', () => {
|
||||
const { doc, clipboardData, createElement, appendChild, execCommand } = createFakeDocument()
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('terminal selection', doc)).toBe(true)
|
||||
expect(clipboardData.setData).toHaveBeenCalledWith('text/plain', 'terminal selection')
|
||||
expect(execCommand).toHaveBeenCalledWith('copy')
|
||||
expect(createElement).not.toHaveBeenCalled()
|
||||
expect(appendChild).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('removes the copy listener after a successful copy', () => {
|
||||
const { doc, listeners } = createFakeDocument()
|
||||
|
||||
copyClipboardTextViaExecCommand('copy me', doc)
|
||||
|
||||
expect(listeners).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('removes the copy listener when execCommand throws', () => {
|
||||
const { doc, listeners } = createFakeDocument({ execCommandThrows: true })
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(false)
|
||||
expect(listeners).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('reports failure when no copy event is dispatched', () => {
|
||||
const { doc, createElement, appendChild } = createFakeDocument({ dispatchesCopyEvent: false })
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(false)
|
||||
expect(createElement).not.toHaveBeenCalled()
|
||||
expect(appendChild).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports failure when the copy event has no clipboardData', () => {
|
||||
const { doc } = createFakeDocument({ withClipboardData: false })
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(false)
|
||||
})
|
||||
|
||||
it('reports failure when execCommand reports failure', () => {
|
||||
const { doc } = createFakeDocument({ execCommandResult: false })
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when the document has no execCommand', () => {
|
||||
const doc = { addEventListener: vi.fn() } as unknown as Document
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns false when the document has no event listener API', () => {
|
||||
const doc = { execCommand: vi.fn() } as unknown as Document
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves focus and the page selection untouched', () => {
|
||||
const { doc } = createFakeDocument()
|
||||
const focus = vi.fn()
|
||||
const selection = {
|
||||
rangeCount: 1,
|
||||
getRangeAt: vi.fn(),
|
||||
removeAllRanges: vi.fn(),
|
||||
addRange: vi.fn()
|
||||
}
|
||||
;(doc as unknown as { activeElement: unknown }).activeElement = { focus }
|
||||
;(doc as unknown as { getSelection: () => unknown }).getSelection = () => selection
|
||||
|
||||
expect(copyClipboardTextViaExecCommand('copy me', doc)).toBe(true)
|
||||
expect(selection.removeAllRanges).not.toHaveBeenCalled()
|
||||
expect(focus).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -5,23 +5,10 @@ import {
|
||||
} from '../../agent-session-journal-producer'
|
||||
|
||||
describe('isRootAgentJournalItem', () => {
|
||||
it("reads a row carrying no agent id as the session's own", () => {
|
||||
expect(isRootAgentJournalItem({})).toBe(true)
|
||||
})
|
||||
|
||||
it('reads a row carrying one as a subagent’s', () => {
|
||||
expect(isRootAgentJournalItem({ agentId: 'task-1' })).toBe(false)
|
||||
})
|
||||
|
||||
it('reads an id that failed to resolve as a subagent’s, not as root', () => {
|
||||
// Presence, not truthiness: an unresolved child id must never attribute its content to the parent.
|
||||
expect(isRootAgentJournalItem({ agentId: '' })).toBe(false)
|
||||
})
|
||||
|
||||
it('reads a missing item as root rather than throwing', () => {
|
||||
// Every caller walks a list backwards with an index that can fall off it.
|
||||
expect(isRootAgentJournalItem(undefined)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('agentJournalLinkageFields', () => {
|
||||
@@ -31,15 +18,4 @@ describe('agentJournalLinkageFields', () => {
|
||||
expect(Object.keys(agentJournalLinkageFields({ agentId: 'task-1' }))).toEqual(['agentId'])
|
||||
expect(agentJournalLinkageFields(undefined)).toEqual({})
|
||||
})
|
||||
|
||||
it('carries every member of the bundle through', () => {
|
||||
const linkage = {
|
||||
agentId: 'task-1',
|
||||
parentAgentId: 'task-parent',
|
||||
providerParentRef: 'toolu_1',
|
||||
producerKind: 'background' as const,
|
||||
attempt: 3
|
||||
}
|
||||
expect(agentJournalLinkageFields(linkage)).toEqual(linkage)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,46 +29,6 @@ describe('RelayDispatcher silent-client reaper', () => {
|
||||
expect(detachListener).toHaveBeenCalledWith(clientId, 'local')
|
||||
})
|
||||
|
||||
it('keeps a quiet but answering client attached', () => {
|
||||
const detachListener = vi.fn()
|
||||
dispatcher = new RelayDispatcher(() => true)
|
||||
dispatcher.onClientDetached(detachListener)
|
||||
const clientId = dispatcher.attachClient(() => true)
|
||||
|
||||
for (let tick = 0; tick < 10; tick += 1) {
|
||||
vi.advanceTimersByTime(KEEPALIVE_SEND_MS)
|
||||
dispatcher.feedClient(clientId, encodeKeepAliveFrame(tick + 1, 0))
|
||||
}
|
||||
|
||||
// Assert this client specifically; the unfed primary in this harness proves nothing about the socket under test.
|
||||
expect(detachListener).not.toHaveBeenCalledWith(clientId, expect.anything())
|
||||
})
|
||||
|
||||
it('does not reap every client on the first tick after the host slept', () => {
|
||||
const detachListener = vi.fn()
|
||||
dispatcher = new RelayDispatcher(() => true)
|
||||
dispatcher.onClientDetached(detachListener)
|
||||
dispatcher.attachClient(() => true)
|
||||
|
||||
// One tick fires far late because the process was paused, not because the peers went away.
|
||||
vi.setSystemTime(10 * 60_000)
|
||||
vi.advanceTimersByTime(KEEPALIVE_SEND_MS)
|
||||
|
||||
expect(detachListener).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not judge a client that has not spoken yet by the silence window', () => {
|
||||
// A slow initial handshake can outlast the silence window; give never-speaking clients a wider bound.
|
||||
const detachListener = vi.fn()
|
||||
dispatcher = new RelayDispatcher(() => true)
|
||||
dispatcher.onClientDetached(detachListener)
|
||||
const clientId = dispatcher.attachClient(() => true)
|
||||
|
||||
vi.advanceTimersByTime(TIMEOUT_MS * 5)
|
||||
|
||||
expect(detachListener).not.toHaveBeenCalledWith(clientId, expect.anything())
|
||||
})
|
||||
|
||||
it('still bounds a client that never speaks at all', () => {
|
||||
// Bound attach age because a never-speaking client has no lastReceivedAt to expire.
|
||||
const detachListener = vi.fn()
|
||||
|
||||
Reference in New Issue
Block a user