mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 18:07:45 +00:00
test: remove 136 low-value preview and workspace action tests (#37504)
* test: remove low-value preview and workspace action tests * test: remove low-value preview and workspace action tests
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -10631,27 +10631,16 @@
|
||||
"src/renderer/src/lib/feedback-image-attachments.test.ts": 557,
|
||||
"src/renderer/src/lib/file-owner-runtime-target.test.ts": 278,
|
||||
"src/renderer/src/lib/file-preview-link-open.test.ts": 2311,
|
||||
"src/renderer/src/lib/file-preview.test.ts": 1468,
|
||||
"src/renderer/src/lib/file-search-result-owner.test.ts": 48,
|
||||
"src/renderer/src/lib/file-search-selection.test.ts": 51,
|
||||
"src/renderer/src/lib/file-search-shortcut-policy.test.ts": 236,
|
||||
"src/renderer/src/lib/file-type-icons.test.ts": 147,
|
||||
"src/renderer/src/lib/find-query-bounds.test.ts": 45,
|
||||
"src/renderer/src/lib/finished-agent-resume-resurrection.test.ts": 1872,
|
||||
"src/renderer/src/lib/fix-checks-agent-launch.test.ts": 730,
|
||||
"src/renderer/src/lib/flatten-retained-slice.test.ts": 389,
|
||||
"src/renderer/src/lib/floating-workspace-panel-reveal.test.ts": 240,
|
||||
"src/renderer/src/lib/floating-workspace-terminal-actions.test.ts": 613,
|
||||
"src/renderer/src/lib/floating-workspace-tour-interaction-snapshot.test.ts": 56,
|
||||
"src/renderer/src/lib/focus-terminal-tab-surface.split-focus.test.ts": 331,
|
||||
"src/renderer/src/lib/folder-workspace-path-status.test.ts": 213,
|
||||
"src/renderer/src/lib/foreground-terminal-tabs.test.ts": 55,
|
||||
"src/renderer/src/lib/frame-drawn-chat-tab-placement.test.ts": 203,
|
||||
"src/renderer/src/lib/github-links.test.ts": 76,
|
||||
"src/renderer/src/lib/github-pr-start-point.test.ts": 326,
|
||||
"src/renderer/src/lib/github-source-runtime-context.test.ts": 62,
|
||||
"src/renderer/src/lib/github-work-item-source-lookup.test.ts": 350,
|
||||
"src/renderer/src/lib/github-work-item-workspace-attachment.test.ts": 77,
|
||||
"src/renderer/src/lib/gitlab-links.test.ts": 56,
|
||||
"src/renderer/src/lib/hook-command-delayed-delivery.test.ts": 1904,
|
||||
"src/renderer/src/lib/host-chat-program.test.ts": 2419,
|
||||
|
||||
@@ -55,11 +55,6 @@ describe('getRuntimeTargetForWorktreeOwner', () => {
|
||||
expect(getRuntimeTargetForWorktreeOwner(legacy, 'legacy-repo::wt')).toEqual({ kind: 'local' })
|
||||
})
|
||||
|
||||
it('stays on this app for no workspace or a workspace with no row yet', () => {
|
||||
expect(getRuntimeTargetForWorktreeOwner(state, null)).toEqual({ kind: 'local' })
|
||||
expect(getRuntimeTargetForWorktreeOwner(state, 'unknown::wt')).toEqual({ kind: 'local' })
|
||||
})
|
||||
|
||||
it('has no transport when rows disagree, and a caller with an error path refuses', () => {
|
||||
const ambiguous: WorktreeOperationRouteState = {
|
||||
settings,
|
||||
|
||||
@@ -1,606 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
canShowWorkspaceFileBrowserAction,
|
||||
convertBrowserPageToWorkspaceDoc,
|
||||
getWorkspaceFilePreviewPlan,
|
||||
openFileInBrowserTab,
|
||||
openFilePreviewToSide
|
||||
} from './file-preview'
|
||||
import { TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE } from './workspace-file-browser-open-target'
|
||||
import { localAndSshTwinRows } from './workspace-twin-rows.test-fixture'
|
||||
import { folderWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { useAppStore } from '@/store'
|
||||
|
||||
function browserActionState(connectionId: string | null = null): never {
|
||||
return {
|
||||
repos: [{ id: 'repo-1', connectionId }],
|
||||
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] },
|
||||
getKnownWorktreeById: () => ({ id: 'wt-1', path: '/repo' })
|
||||
} as never
|
||||
}
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
browserAvailability: {
|
||||
state: 'enabled' as const,
|
||||
provider: 'local-client' as 'local-client' | 'paired-runtime'
|
||||
} as
|
||||
| { state: 'enabled'; provider: 'local-client' | 'paired-runtime' }
|
||||
| { state: 'hidden'; reason: string },
|
||||
createBrowserTab: vi.fn(),
|
||||
createEmptySplitGroup: vi.fn(() => 'group-2'),
|
||||
setActiveBrowserTab: vi.fn(),
|
||||
setActiveBrowserPage: vi.fn(),
|
||||
setActiveWorktree: vi.fn(),
|
||||
activeWorktreeId: 'wt-1',
|
||||
focusGroup: vi.fn(),
|
||||
activateTab: vi.fn(),
|
||||
unifiedTabsByWorktree: {} as Record<string, unknown[]>,
|
||||
browserTabsByWorktree: {} as Record<string, unknown[]>,
|
||||
browserPagesByWorkspace: {} as Record<string, unknown[]>,
|
||||
convertBrowserPage: vi.fn(),
|
||||
environmentId: null as string | null,
|
||||
connectionId: null as string | null,
|
||||
twin: false,
|
||||
layoutByWorktree: {} as Record<string, unknown>,
|
||||
toastError: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { error: mocks.toastError } }))
|
||||
|
||||
vi.mock('@/lib/client-creation-action-policy', () => ({
|
||||
getWorkspaceClientCreationActionPolicy: () => ({ 'managed-browser': mocks.browserAvailability })
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/worktree-runtime-owner', () => ({
|
||||
runtimeEnvironmentIdOrLocalUnlessTwin: () => mocks.environmentId
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: {
|
||||
getState: () => ({
|
||||
createBrowserTab: mocks.createBrowserTab,
|
||||
createEmptySplitGroup: mocks.createEmptySplitGroup,
|
||||
setActiveBrowserTab: mocks.setActiveBrowserTab,
|
||||
setActiveBrowserPage: mocks.setActiveBrowserPage,
|
||||
setActiveWorktree: mocks.setActiveWorktree,
|
||||
activeWorktreeId: mocks.activeWorktreeId,
|
||||
focusGroup: mocks.focusGroup,
|
||||
activateTab: mocks.activateTab,
|
||||
unifiedTabsByWorktree: mocks.unifiedTabsByWorktree,
|
||||
browserTabsByWorktree: mocks.browserTabsByWorktree,
|
||||
browserPagesByWorkspace: mocks.browserPagesByWorkspace,
|
||||
convertBrowserPage: mocks.convertBrowserPage,
|
||||
getKnownWorktreeById: () => ({ id: 'wt-1', path: '/srv/repo' }),
|
||||
groupsByWorktree: {},
|
||||
layoutByWorktree: mocks.layoutByWorktree,
|
||||
...(mocks.twin
|
||||
? localAndSshTwinRows('repo-1', 'wt-1')
|
||||
: {
|
||||
repos: [{ id: 'repo-1', connectionId: mocks.connectionId }],
|
||||
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }
|
||||
})
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mocks.browserAvailability = { state: 'enabled', provider: 'local-client' }
|
||||
mocks.environmentId = null
|
||||
mocks.connectionId = null
|
||||
mocks.layoutByWorktree = {}
|
||||
mocks.browserTabsByWorktree = {}
|
||||
mocks.browserPagesByWorkspace = {}
|
||||
mocks.unifiedTabsByWorktree = {}
|
||||
mocks.activeWorktreeId = 'wt-1'
|
||||
mocks.twin = false
|
||||
})
|
||||
|
||||
// Preview tabs identify documents rather than URLs; callers choose foreground opening versus side/background placement.
|
||||
function docPreviewCall(filePath: string, extra: Record<string, unknown> = {}): unknown[] {
|
||||
return [
|
||||
'wt-1',
|
||||
'data:text/html,',
|
||||
{
|
||||
docLocation: { kind: 'workspace-doc', worktreeId: 'wt-1', filePath },
|
||||
title: filePath.slice(filePath.lastIndexOf('/') + 1),
|
||||
targetGroupId: undefined,
|
||||
browserRuntimeEnvironmentId: null,
|
||||
activate: false,
|
||||
...extra
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function mockOpenDocument(filePath: string): void {
|
||||
const docLocation = { kind: 'workspace-doc' as const, worktreeId: 'wt-1', filePath }
|
||||
mocks.browserTabsByWorktree = { 'wt-1': [{ id: 'browser-9', docLocation }] }
|
||||
mocks.browserPagesByWorkspace = {
|
||||
'browser-9': [{ id: 'page-doc', worktreeId: 'wt-1', docLocation }]
|
||||
}
|
||||
}
|
||||
|
||||
describe('openFileInBrowserTab', () => {
|
||||
it('reuses a document on an inactive page instead of creating a duplicate preview', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mocks.browserTabsByWorktree = { 'wt-1': [{ id: 'browser-9', docLocation: null }] }
|
||||
mocks.browserPagesByWorkspace = {
|
||||
'browser-9': [
|
||||
{ id: 'page-web', worktreeId: 'wt-1' },
|
||||
{
|
||||
id: 'page-doc',
|
||||
worktreeId: 'wt-1',
|
||||
docLocation: {
|
||||
kind: 'workspace-doc',
|
||||
worktreeId: 'wt-1',
|
||||
filePath: '/home/alice/report.html'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
openFileInBrowserTab({ filePath: '/home/alice/report.html', worktreeId: 'wt-1' })
|
||||
|
||||
expect(mocks.setActiveBrowserPage).toHaveBeenCalledWith('browser-9', 'page-doc')
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
})
|
||||
it('keeps a remote document preview in the requested group', () => {
|
||||
mocks.environmentId = 'env-1'
|
||||
openFileInBrowserTab({
|
||||
filePath: '/srv/repo/docs/report.html',
|
||||
worktreeId: 'wt-1',
|
||||
targetGroupId: 'group-2'
|
||||
})
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall('/srv/repo/docs/report.html', { activate: true, targetGroupId: 'group-2' })
|
||||
)
|
||||
})
|
||||
it('opens a local file URL in the Orca browser with the filename as title', () => {
|
||||
openFileInBrowserTab({
|
||||
filePath: '/tmp/example file.html',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith('wt-1', 'file:///tmp/example%20file.html', {
|
||||
title: 'example file.html',
|
||||
activate: true
|
||||
})
|
||||
})
|
||||
|
||||
it('renders a paired-runtime file as a local doc preview instead of a runtime browser tab', () => {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
mocks.browserAvailability = { state: 'enabled', provider: 'paired-runtime' }
|
||||
|
||||
const plan = openFileInBrowserTab({
|
||||
filePath: '/srv/repo/docs/example.html',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
|
||||
expect(plan).toEqual({ status: 'doc-preview' })
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall('/srv/repo/docs/example.html', { activate: true })
|
||||
)
|
||||
})
|
||||
|
||||
// Reuse one document tab or duplicate tabs would mint separate grants for the same file.
|
||||
it('activates the tab a document is already open in', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mockOpenDocument('/home/alice/report.html')
|
||||
|
||||
openFileInBrowserTab({ filePath: '/home/alice/report.html', worktreeId: 'wt-1' })
|
||||
|
||||
expect(mocks.setActiveBrowserTab).toHaveBeenCalledWith('browser-9')
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Activate the unified tab, not browser state alone, or the terminal remains in front of the preview.
|
||||
it('brings an already-open document to the front of its group', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mockOpenDocument('/home/alice/report.html')
|
||||
mocks.unifiedTabsByWorktree = {
|
||||
'wt-1': [
|
||||
{
|
||||
id: 'tab-terminal',
|
||||
worktreeId: 'wt-1',
|
||||
contentType: 'terminal',
|
||||
entityId: 'term-1',
|
||||
groupId: 'group-1'
|
||||
},
|
||||
{
|
||||
id: 'tab-doc',
|
||||
worktreeId: 'wt-1',
|
||||
contentType: 'browser',
|
||||
entityId: 'browser-9',
|
||||
groupId: 'group-1'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
openFileInBrowserTab({ filePath: '/home/alice/report.html', worktreeId: 'wt-1' })
|
||||
|
||||
expect(mocks.focusGroup).toHaveBeenCalledWith('wt-1', 'group-1')
|
||||
expect(mocks.activateTab).toHaveBeenCalledWith('tab-doc', { worktreeId: 'wt-1' })
|
||||
expect(mocks.setActiveBrowserTab).toHaveBeenCalledWith('browser-9')
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Side previews must leave the reader working in the source, including when reusing a tab.
|
||||
it('leaves the reader in place when a side preview reuses an open document', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mockOpenDocument('/home/alice/report.html')
|
||||
mocks.unifiedTabsByWorktree = {
|
||||
'wt-1': [{ id: 'tab-doc', contentType: 'browser', entityId: 'browser-9', groupId: 'group-1' }]
|
||||
}
|
||||
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/home/alice/report.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.activateTab).not.toHaveBeenCalled()
|
||||
expect(mocks.focusGroup).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Why: the split is part of the tab create, so reusing an open document must leave no empty group.
|
||||
it('creates no split when a side preview reuses an open document', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mockOpenDocument('/home/alice/report.html')
|
||||
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/home/alice/report.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.createEmptySplitGroup).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
expect(mocks.setActiveBrowserTab).toHaveBeenCalledWith('browser-9')
|
||||
})
|
||||
|
||||
it('opens a second document in its own tab', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mockOpenDocument('/home/alice/a.html')
|
||||
|
||||
openFileInBrowserTab({ filePath: '/home/alice/b.html', worktreeId: 'wt-1' })
|
||||
|
||||
expect(mocks.setActiveBrowserTab).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall('/home/alice/b.html', { activate: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('renders an SSH file as a local doc preview', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
|
||||
const plan = openFileInBrowserTab({
|
||||
filePath: '/home/alice/report.html',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
|
||||
expect(plan).toEqual({ status: 'doc-preview' })
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall('/home/alice/report.html', { activate: true })
|
||||
)
|
||||
})
|
||||
|
||||
it('previews a paired runtime whose managed browser is unavailable', () => {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
mocks.browserAvailability = { state: 'hidden', reason: 'streaming unavailable' }
|
||||
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/srv/repo/example.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
// Keep the background split unfocused; host snapshots interpret activated empty groups as terminals.
|
||||
it('creates paired-runtime side previews in an unfocused right-hand split', () => {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
mocks.browserAvailability = { state: 'enabled', provider: 'paired-runtime' }
|
||||
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/srv/repo/example.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.createEmptySplitGroup).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall('/srv/repo/example.html', {
|
||||
split: { sourceGroupId: 'group-1', splitDirection: 'right', activate: false }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('creates local side previews in an activated right-hand split', () => {
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/tmp/example.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.createEmptySplitGroup).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith('wt-1', 'file:///tmp/example.html', {
|
||||
title: 'example.html',
|
||||
split: { sourceGroupId: 'group-1', splitDirection: 'right', activate: true },
|
||||
activate: true
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects a local workspace whose managed browser is unavailable before creating a split', () => {
|
||||
mocks.browserAvailability = { state: 'hidden', reason: 'browser unavailable' }
|
||||
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/tmp/example.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.toastError).toHaveBeenCalledWith('browser unavailable')
|
||||
expect(mocks.createEmptySplitGroup).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('previews a paired document outside the workspace without a server browser', () => {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
mocks.browserAvailability = { state: 'hidden', reason: 'streaming unavailable' }
|
||||
|
||||
openFilePreviewToSide({
|
||||
language: 'html',
|
||||
filePath: '/tmp/agent-scratch/report.html',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall('/tmp/agent-scratch/report.html', {
|
||||
split: { sourceGroupId: 'group-1', splitDirection: 'right', activate: false }
|
||||
})
|
||||
)
|
||||
expect(mocks.createEmptySplitGroup).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps previewing an SSH document outside the worktree root', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
|
||||
const plan = openFileInBrowserTab({
|
||||
filePath: '/tmp/agent-scratch/report.html',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
|
||||
expect(plan).toEqual({ status: 'doc-preview' })
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledOnce()
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('ignores languages that have no preview surface', () => {
|
||||
openFilePreviewToSide({
|
||||
language: 'typescript',
|
||||
filePath: '/tmp/example.ts',
|
||||
worktreeId: 'wt-1',
|
||||
sourceGroupId: 'group-1'
|
||||
})
|
||||
|
||||
expect(mocks.createEmptySplitGroup).not.toHaveBeenCalled()
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('canShowWorkspaceFileBrowserAction', () => {
|
||||
it('permits paired runtimes regardless of managed-browser capability', () => {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
mocks.browserAvailability = { state: 'hidden', reason: 'streaming unavailable' }
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(browserActionState(), 'wt-1', '/repo/report.html')
|
||||
).toBe(true)
|
||||
|
||||
mocks.browserAvailability = { state: 'enabled', provider: 'paired-runtime' }
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(browserActionState(), 'wt-1', '/repo/report.html')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// Keep the action visible so activation can explain the workspace boundary.
|
||||
it('keeps the action visible for a paired document outside the worktree', () => {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(browserActionState(), 'wt-1', '/tmp/outside/report.html')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('permits both local and SSH files', () => {
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(browserActionState(), 'wt-1', '/repo/report.html')
|
||||
).toBe(true)
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(browserActionState('ssh-1'), 'wt-1', '/repo/report.html')
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('hides the action while a file has no resolved owner', () => {
|
||||
const workspaceId = folderWorkspaceKey('folder-1')
|
||||
const state = {
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'folder-1',
|
||||
projectGroupId: 'group-1',
|
||||
folderPath: '/workspace'
|
||||
}
|
||||
],
|
||||
projectGroups: [{ id: 'group-1', parentGroupId: null }],
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-local',
|
||||
path: '/workspace/local',
|
||||
projectGroupId: 'group-1'
|
||||
},
|
||||
{
|
||||
id: 'repo-ssh',
|
||||
path: '/workspace/remote',
|
||||
projectGroupId: 'group-1',
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
],
|
||||
worktreesByRepo: {}
|
||||
} as never
|
||||
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(state, workspaceId, '/workspace/local/report.html')
|
||||
).toBe(true)
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(state, workspaceId, '/workspace/remote/report.html')
|
||||
).toBe(true)
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(state, workspaceId, '/workspace/unknown/report.html')
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getWorkspaceFilePreviewPlan', () => {
|
||||
it('plans no local browser tab for a project two hosts publish', () => {
|
||||
mocks.twin = true
|
||||
expect(
|
||||
getWorkspaceFilePreviewPlan(useAppStore.getState(), 'wt-1', '/srv/repo/report.html')
|
||||
).toEqual({
|
||||
status: 'unsupported',
|
||||
message: TWIN_FILE_BROWSER_UNSUPPORTED_MESSAGE,
|
||||
reason: 'no-channel'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Address-bar conversion must also reuse the document’s existing grant/tab.
|
||||
describe('convertBrowserPageToWorkspaceDoc', () => {
|
||||
const DOC_LOCATION = {
|
||||
kind: 'workspace-doc' as const,
|
||||
worktreeId: 'wt-1',
|
||||
filePath: '/home/alice/report.html'
|
||||
}
|
||||
|
||||
// Inspect page docLocation: a mixed workspace’s mirror is null when its document page is inactive.
|
||||
it('activates the tab already showing the document, even as an inactive page', () => {
|
||||
mocks.browserTabsByWorktree = {
|
||||
'wt-1': [{ id: 'browser-9', docLocation: null }]
|
||||
}
|
||||
mocks.browserPagesByWorkspace = {
|
||||
'browser-9': [
|
||||
{ id: 'page-web', worktreeId: 'wt-1' },
|
||||
{ id: 'page-doc', worktreeId: 'wt-1', docLocation: DOC_LOCATION }
|
||||
]
|
||||
}
|
||||
|
||||
const outcome = convertBrowserPageToWorkspaceDoc('page-1', DOC_LOCATION)
|
||||
|
||||
expect(outcome).toBe('activated-existing')
|
||||
expect(mocks.convertBrowserPage).not.toHaveBeenCalled()
|
||||
expect(mocks.setActiveBrowserTab).toHaveBeenCalledWith('browser-9')
|
||||
expect(mocks.setActiveBrowserPage).toHaveBeenCalledWith('browser-9', 'page-doc')
|
||||
})
|
||||
|
||||
it('converts the page in place when no tab shows the document', () => {
|
||||
mocks.browserPagesByWorkspace = {
|
||||
'browser-1': [{ id: 'page-1', worktreeId: 'wt-1' }]
|
||||
}
|
||||
mocks.convertBrowserPage.mockReturnValue({ id: 'new-page' })
|
||||
|
||||
const outcome = convertBrowserPageToWorkspaceDoc('page-1', DOC_LOCATION)
|
||||
|
||||
expect(outcome).toBe('converted')
|
||||
expect(mocks.convertBrowserPage).toHaveBeenCalledWith(
|
||||
'page-1',
|
||||
{ kind: 'workspace-doc', docLocation: DOC_LOCATION },
|
||||
undefined
|
||||
)
|
||||
})
|
||||
|
||||
// Open another worktree’s document there; converting this row would leave its guest/links unable to take focus.
|
||||
it('opens a document from another worktree in that worktree instead of converting', () => {
|
||||
mocks.connectionId = 'ssh-1'
|
||||
mocks.activeWorktreeId = 'wt-other'
|
||||
mocks.browserPagesByWorkspace = {
|
||||
'browser-1': [{ id: 'page-1', worktreeId: 'wt-other' }]
|
||||
}
|
||||
|
||||
const outcome = convertBrowserPageToWorkspaceDoc('page-1', DOC_LOCATION)
|
||||
|
||||
expect(outcome).toBe('opened-in-owning-worktree')
|
||||
expect(mocks.convertBrowserPage).not.toHaveBeenCalled()
|
||||
// Activate the document’s worktree so opening cannot succeed invisibly.
|
||||
expect(mocks.setActiveWorktree).toHaveBeenCalledWith('wt-1')
|
||||
expect(mocks.createBrowserTab).toHaveBeenCalledWith(
|
||||
...docPreviewCall(DOC_LOCATION.filePath, { activate: true })
|
||||
)
|
||||
})
|
||||
|
||||
// History converts this tab in place; reusing another document tab would leave this tab’s history looping there.
|
||||
it.each(['history-return', 'history-advance'] as const)(
|
||||
'skips reuse on the %s leg and converts in place',
|
||||
(leg) => {
|
||||
mocks.browserTabsByWorktree = {
|
||||
'wt-1': [{ id: 'browser-9', docLocation: null }]
|
||||
}
|
||||
mocks.browserPagesByWorkspace = {
|
||||
'browser-9': [{ id: 'page-doc', worktreeId: 'wt-1', docLocation: DOC_LOCATION }],
|
||||
'browser-1': [{ id: 'page-1', worktreeId: 'wt-1' }]
|
||||
}
|
||||
mocks.convertBrowserPage.mockReturnValue({ id: 'new-page' })
|
||||
|
||||
const outcome = convertBrowserPageToWorkspaceDoc('page-1', DOC_LOCATION, { leg })
|
||||
|
||||
expect(outcome).toBe('converted')
|
||||
expect(mocks.setActiveBrowserTab).not.toHaveBeenCalled()
|
||||
expect(mocks.convertBrowserPage).toHaveBeenCalledWith(
|
||||
'page-1',
|
||||
{ kind: 'workspace-doc', docLocation: DOC_LOCATION },
|
||||
{ leg }
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
// Web clients cannot host desktop preview webviews; hide the action instead of creating an unusable tab (#22618).
|
||||
describe('document previews in a paired web client', () => {
|
||||
it('hides Open in Orca Browser and refuses with a reason instead of a silent no-op', () => {
|
||||
vi.stubGlobal('__ORCA_WEB_CLIENT__', true)
|
||||
try {
|
||||
mocks.environmentId = 'runtime-1'
|
||||
mocks.browserAvailability = { state: 'enabled', provider: 'paired-runtime' }
|
||||
|
||||
expect(
|
||||
canShowWorkspaceFileBrowserAction(browserActionState(), 'wt-1', '/repo/report.html')
|
||||
).toBe(false)
|
||||
const plan = openFileInBrowserTab({
|
||||
filePath: '/srv/repo/docs/example.html',
|
||||
worktreeId: 'wt-1'
|
||||
})
|
||||
expect(plan).toMatchObject({ status: 'unsupported', reason: 'no-channel' })
|
||||
expect(mocks.createBrowserTab).not.toHaveBeenCalled()
|
||||
|
||||
mocks.browserPagesByWorkspace = { 'browser-1': [{ id: 'page-1', worktreeId: 'wt-1' }] }
|
||||
expect(
|
||||
convertBrowserPageToWorkspaceDoc('page-1', {
|
||||
kind: 'workspace-doc',
|
||||
worktreeId: 'wt-1',
|
||||
filePath: '/srv/repo/docs/example.html'
|
||||
})
|
||||
).toBe('failed')
|
||||
expect(mocks.convertBrowserPage).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,20 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createFileSearchResultOwner } from './file-search-result-owner'
|
||||
|
||||
describe('createFileSearchResultOwner', () => {
|
||||
it('captures the exact runtime used by the completed search', () => {
|
||||
const owner = createFileSearchResultOwner('worktree-a', ' runtime-owner-a ')
|
||||
|
||||
expect(owner).toEqual({
|
||||
worktreeId: 'worktree-a',
|
||||
runtimeEnvironmentId: 'runtime-owner-a'
|
||||
})
|
||||
})
|
||||
|
||||
it.each([null, '', ' '])('records %j as an explicit non-runtime owner', (environmentId) => {
|
||||
expect(createFileSearchResultOwner('local-or-ssh-worktree', environmentId)).toEqual({
|
||||
worktreeId: 'local-or-ssh-worktree',
|
||||
runtimeEnvironmentId: null
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,6 @@ afterEach(() => {
|
||||
unregisterCallbacks.pop()?.()
|
||||
}
|
||||
vi.restoreAllMocks()
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function registerForTest(provider: () => string | null | undefined): void {
|
||||
@@ -20,19 +19,6 @@ function registerForTest(provider: () => string | null | undefined): void {
|
||||
}
|
||||
|
||||
describe('normalizeSelectedTextForFileSearch', () => {
|
||||
it('trims and collapses multi-line selections into the single-line search box shape', () => {
|
||||
expect(normalizeSelectedTextForFileSearch(' foo\r\n bar\n\n baz ')).toBe('foo bar baz')
|
||||
})
|
||||
|
||||
it('handles CR-only selected text line breaks', () => {
|
||||
expect(normalizeSelectedTextForFileSearch(' foo\r bar\r\r baz ')).toBe('foo bar baz')
|
||||
})
|
||||
|
||||
it('returns null for empty selections', () => {
|
||||
expect(normalizeSelectedTextForFileSearch(' \n\t ')).toBeNull()
|
||||
expect(normalizeSelectedTextForFileSearch(null)).toBeNull()
|
||||
})
|
||||
|
||||
it('bounds newline-heavy selected text without splitting it into arrays', () => {
|
||||
const split = vi.spyOn(String.prototype, 'split')
|
||||
const selectedText = Array.from({ length: 2000 }, (_, index) => `term-${index + 1}`).join('\n')
|
||||
@@ -46,20 +32,6 @@ describe('normalizeSelectedTextForFileSearch', () => {
|
||||
})
|
||||
|
||||
describe('getSelectedTextForFileSearch', () => {
|
||||
it('uses the most recently registered non-empty provider', () => {
|
||||
registerForTest(() => 'older')
|
||||
registerForTest(() => 'newer')
|
||||
|
||||
expect(getSelectedTextForFileSearch()).toBe('newer')
|
||||
})
|
||||
|
||||
it('falls back through empty providers', () => {
|
||||
registerForTest(() => 'needle')
|
||||
registerForTest(() => ' ')
|
||||
|
||||
expect(getSelectedTextForFileSearch()).toBe('needle')
|
||||
})
|
||||
|
||||
it('unregisters providers so closed editors do not accumulate stale selection readers', () => {
|
||||
registerForTest(() => 'active')
|
||||
const unregister = registerFileSearchSelectedTextProvider(() => 'stale')
|
||||
@@ -69,14 +41,4 @@ describe('getSelectedTextForFileSearch', () => {
|
||||
|
||||
expect(getSelectedTextForFileSearch()).toBe('active')
|
||||
})
|
||||
|
||||
it('falls back to the DOM selection when no provider has selected text', () => {
|
||||
vi.stubGlobal('window', {
|
||||
getSelection: () => ({
|
||||
toString: () => 'dom selection'
|
||||
})
|
||||
})
|
||||
|
||||
expect(getSelectedTextForFileSearch()).toBe('dom selection')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { keybindingMatchesAction } from '../../../shared/keybindings'
|
||||
import { fileSearchClaimsTextKey } from './file-search-shortcut-policy'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('file search shortcut ownership', () => {
|
||||
it.each(['Macintosh', 'Windows', 'Linux'])(
|
||||
'preserves editing and releases app chords on %s',
|
||||
(platform) => {
|
||||
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue(platform)
|
||||
const target = document.createElement('input')
|
||||
target.setAttribute('data-file-search-input', 'true')
|
||||
const modifiers = platform === 'Macintosh' ? { metaKey: true } : { ctrlKey: true }
|
||||
for (const key of ['p', 'j', 'b', 'g', '+', '-', 'ArrowUp', 'ArrowDown']) {
|
||||
expect(fileSearchClaimsTextKey({ target, key, shiftKey: true, ...modifiers })).toBe(false)
|
||||
}
|
||||
for (const key of [
|
||||
'a',
|
||||
'c',
|
||||
'v',
|
||||
'x',
|
||||
'z',
|
||||
'y',
|
||||
'ArrowLeft',
|
||||
'ArrowRight',
|
||||
'Home',
|
||||
'End',
|
||||
'Backspace',
|
||||
'Delete'
|
||||
]) {
|
||||
expect(fileSearchClaimsTextKey({ target, key, ...modifiers })).toBe(true)
|
||||
}
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'b' })).toBe(true)
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'с', code: 'KeyC', ...modifiers })).toBe(true)
|
||||
if (platform !== 'Macintosh') {
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'Insert', ...modifiers })).toBe(true)
|
||||
}
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'p', isComposing: true, ...modifiers })).toBe(
|
||||
true
|
||||
)
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'p', altKey: true, ...modifiers })).toBe(false)
|
||||
expect(
|
||||
fileSearchClaimsTextKey({ target, key: 'a', code: 'KeyA', altKey: true, ...modifiers })
|
||||
).toBe(false)
|
||||
expect(
|
||||
fileSearchClaimsTextKey({ target, key: 'r', code: 'KeyR', altKey: true, ...modifiers })
|
||||
).toBe(false)
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'a', altKey: true })).toBe(true)
|
||||
expect(
|
||||
fileSearchClaimsTextKey({
|
||||
target,
|
||||
key: 'a',
|
||||
code: 'KeyA',
|
||||
altKey: true,
|
||||
altGraph: true,
|
||||
...modifiers
|
||||
})
|
||||
).toBe(true)
|
||||
expect(
|
||||
fileSearchClaimsTextKey({
|
||||
target,
|
||||
key: 'a',
|
||||
altKey: true,
|
||||
getModifierState: () => true,
|
||||
...modifiers
|
||||
})
|
||||
).toBe(true)
|
||||
if (platform !== 'Macintosh') {
|
||||
expect(
|
||||
keybindingMatchesAction(
|
||||
'floatingTerminal.toggle',
|
||||
{ key: 'a', code: 'KeyA', ctrlKey: true, altKey: true },
|
||||
platform === 'Windows' ? 'win32' : 'linux'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
fileSearchClaimsTextKey({ target, key: '@', code: 'KeyQ', altKey: true, ...modifiers })
|
||||
).toBe(true)
|
||||
}
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'Delete', altKey: true, ...modifiers })).toBe(
|
||||
true
|
||||
)
|
||||
expect(fileSearchClaimsTextKey({ target, key: 'p', ctrlKey: true, metaKey: true })).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it('retains other text surface authority', () => {
|
||||
for (const tag of ['input', 'textarea', 'select']) {
|
||||
expect(
|
||||
fileSearchClaimsTextKey({ target: document.createElement(tag), key: 'b', metaKey: true })
|
||||
).toBe(true)
|
||||
}
|
||||
expect(fileSearchClaimsTextKey({ target: document.createElement('button'), key: 'b' })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('releases shipped Command+Option actions without reclaiming physical clipboard letters', () => {
|
||||
vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Macintosh')
|
||||
const target = document.createElement('input')
|
||||
target.setAttribute('data-file-search-input', 'true')
|
||||
for (const [action, key, code] of [
|
||||
['workspace.rename', 'r', 'KeyR'],
|
||||
['floatingTerminal.toggle', 'å', 'KeyA']
|
||||
] as const) {
|
||||
const input = { target, key, code, metaKey: true, altKey: true }
|
||||
expect(keybindingMatchesAction(action, input, 'darwin')).toBe(true)
|
||||
expect(fileSearchClaimsTextKey(input)).toBe(false)
|
||||
}
|
||||
})
|
||||
@@ -3,10 +3,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { FIND_QUERY_MAX_BYTES, getFindRequestQuery, isFindQueryTooLarge } from './find-query-bounds'
|
||||
|
||||
describe('find query bounds', () => {
|
||||
it('keeps find queries within the byte budget', () => {
|
||||
expect(getFindRequestQuery('needle')).toBe('needle')
|
||||
})
|
||||
|
||||
it('rejects oversized ASCII find queries', () => {
|
||||
const query = 'x'.repeat(FIND_QUERY_MAX_BYTES + 1)
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ const mocks = vi.hoisted(() => {
|
||||
activateAndRevealWorktree: vi.fn(),
|
||||
findGithubPrWorkspaceAttachment: vi.fn(),
|
||||
focusTerminalTabSurface: vi.fn(),
|
||||
getConnectionId: vi.fn(),
|
||||
launchAgentInNewTab: vi.fn(),
|
||||
launchWorkItemDirect: vi.fn(),
|
||||
pickSourceControlLaunchAgent: vi.fn(),
|
||||
@@ -93,10 +92,6 @@ vi.mock('sonner', () => ({
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/connection-context', () => ({
|
||||
getConnectionId: mocks.getConnectionId
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/focus-terminal-tab-surface', () => ({
|
||||
focusTerminalTabSurface: mocks.focusTerminalTabSurface
|
||||
}))
|
||||
@@ -170,7 +165,6 @@ describe('startFixChecksAgent', () => {
|
||||
mocks.store.ensureRemoteDetectedAgents.mockResolvedValue(['codex'])
|
||||
mocks.activateAndRevealWorktree.mockReturnValue(true)
|
||||
mocks.findGithubPrWorkspaceAttachment.mockReturnValue(null)
|
||||
mocks.getConnectionId.mockReturnValue(null)
|
||||
mocks.launchAgentInNewTab.mockImplementation(
|
||||
(args: { beforeSurfaceOpen?: (surface: { kind: 'local-terminal' }) => boolean | void }) => {
|
||||
args.beforeSurfaceOpen?.({ kind: 'local-terminal' })
|
||||
@@ -188,24 +182,6 @@ describe('startFixChecksAgent', () => {
|
||||
mocks.resolveRepoWorktreeLaunchPlatform.mockReturnValue('darwin')
|
||||
})
|
||||
|
||||
it('activates the attached workspace as a surface-providing caller', async () => {
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
// The launcher owns creation; activation must not seed a sibling shell after the last terminal closed.
|
||||
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1', {
|
||||
providesInitialSurface: true
|
||||
})
|
||||
})
|
||||
|
||||
it('carries the actual use-default literal from Fix Checks into final startup and its recipe', async () => {
|
||||
const settings = {
|
||||
...getDefaultSettings('/test'),
|
||||
@@ -259,157 +235,6 @@ describe('startFixChecksAgent', () => {
|
||||
expect(plans[0]?.launchCommand).not.toContain('high')
|
||||
})
|
||||
|
||||
it('fails without launching when the requested worktree is missing', async () => {
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'missing-worktree',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
|
||||
expect(mocks.store.ensureDetectedAgents).not.toHaveBeenCalled()
|
||||
expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('fails without launching when agent detection finds no enabled agent', async () => {
|
||||
mocks.store.ensureDetectedAgents.mockResolvedValue([])
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
|
||||
expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects without launching when remote agent detection fails', async () => {
|
||||
mocks.store.repos = [{ ...mocks.store.repos[0], connectionId: 'conn-1' }]
|
||||
mocks.getConnectionId.mockReturnValue('conn-1')
|
||||
mocks.store.ensureRemoteDetectedAgents.mockRejectedValue(new Error('detection failed'))
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).rejects.toThrow('detection failed')
|
||||
|
||||
expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("resolves the attached workspace's own execution host", async () => {
|
||||
mocks.store.repos = [
|
||||
{
|
||||
...mocks.store.repos[0],
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
]
|
||||
mocks.getConnectionId.mockReturnValue(undefined)
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(mocks.store.ensureRemoteDetectedAgents).toHaveBeenCalledWith('ssh-1')
|
||||
expect(mocks.resolveRepoWorktreeLaunchPlatform).toHaveBeenCalledWith(
|
||||
mocks.store,
|
||||
expect.objectContaining({ id: 'repo-1', connectionId: 'ssh-1' }),
|
||||
expect.objectContaining({ id: 'wt-1', path: '/repo/wt-1' })
|
||||
)
|
||||
})
|
||||
|
||||
it("plans a Linux server's launch for that server from a Windows client (#27971)", async () => {
|
||||
mocks.resolveRepoWorktreeLaunchPlatform.mockReturnValue('linux')
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(mocks.launchAgentInNewTab).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ launchPlatform: 'linux' })
|
||||
)
|
||||
})
|
||||
|
||||
it('fails without launching when the execution host has not reported its OS', async () => {
|
||||
mocks.resolveRepoWorktreeLaunchPlatform.mockReturnValue(undefined)
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(false)
|
||||
|
||||
expect(mocks.launchAgentInNewTab).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves hidden CLI validation to the selected launch route', async () => {
|
||||
mocks.resolveSourceControlActionRecipe.mockReturnValue({
|
||||
commandInputTemplate: '{basePrompt}',
|
||||
agentArgs: '--model "unterminated'
|
||||
})
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(mocks.launchAgentInNewTab).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ agentArgs: '--model "unterminated' })
|
||||
)
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects without focusing a terminal when agent launch throws', async () => {
|
||||
mocks.launchAgentInNewTab.mockImplementation(() => {
|
||||
throw new Error('launch failed')
|
||||
})
|
||||
const { startFixChecksAgent } = await import('./fix-checks-agent-launch')
|
||||
|
||||
await expect(
|
||||
startFixChecksAgent({
|
||||
repoId: 'repo-1',
|
||||
worktreeId: 'wt-1',
|
||||
basePrompt: 'Fix checks',
|
||||
launchSource: 'task_page'
|
||||
})
|
||||
).rejects.toThrow('launch failed')
|
||||
|
||||
expect(mocks.focusTerminalTabSurface).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// Why: a saved chat stays implicit so a host that stops running chats falls back to a terminal.
|
||||
it('replays a saved terminal explicitly and leaves a saved chat to the settings route', async () => {
|
||||
Object.assign(mocks.store.settings, { disabledTuiAgents: ['codex'] })
|
||||
|
||||
@@ -4,8 +4,6 @@ import { runProcess } from '@orca/process-host'
|
||||
import { z } from 'zod'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { flattenRetainedSlice } from './flatten-retained-slice'
|
||||
|
||||
// Use 1MB parents above V8’s SlicedString threshold so retained-parent versus copied-tail heap differences are measurable.
|
||||
const PARENT_CHARS = 1024 * 1024
|
||||
const TAIL_CHARS = 512
|
||||
@@ -89,13 +87,6 @@ async function measureTails(filler: string) {
|
||||
}
|
||||
|
||||
describe('flattenRetainedSlice', () => {
|
||||
it('preserves content exactly', () => {
|
||||
expect(flattenRetainedSlice('')).toBe('')
|
||||
expect(flattenRetainedSlice('a')).toBe('a')
|
||||
const source = 'hello world, 漢字, [0m, \u{1f600}'
|
||||
expect(flattenRetainedSlice(source.slice(3))).toBe(source.slice(3))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['one-byte', 'a'],
|
||||
['two-byte', '漢']
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getDefaultSettings } from '../../../shared/constants'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { TOGGLE_FLOATING_TERMINAL_EVENT } from './floating-terminal'
|
||||
import { revealFloatingWorkspacePanel } from './floating-workspace-panel-reveal'
|
||||
|
||||
function panelState({ enabled, open }: { enabled: boolean; open: boolean }) {
|
||||
return {
|
||||
settings: { ...getDefaultSettings('/home/test'), floatingTerminalEnabled: enabled },
|
||||
floatingWorkspacePanelOpen: open,
|
||||
updateSettings: vi.fn<AppState['updateSettings']>(async () => {})
|
||||
}
|
||||
}
|
||||
|
||||
async function settle(): Promise<void> {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
}
|
||||
|
||||
function stubWindow() {
|
||||
const dispatchEvent = vi.fn()
|
||||
const frames: FrameRequestCallback[] = []
|
||||
vi.stubGlobal('window', { dispatchEvent })
|
||||
vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => frames.push(callback))
|
||||
// A DOM a frame behind the store must not decide: it still shows the panel as it was.
|
||||
vi.stubGlobal('document', { querySelector: vi.fn().mockReturnValue({}) })
|
||||
return { dispatchEvent, runFrames: () => frames.splice(0).forEach((frame) => frame(0)) }
|
||||
}
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals())
|
||||
|
||||
describe('revealFloatingWorkspacePanel', () => {
|
||||
it('toggles an enabled panel open only while the store says it is closed', () => {
|
||||
const { dispatchEvent } = stubWindow()
|
||||
|
||||
revealFloatingWorkspacePanel(panelState({ enabled: true, open: true }))
|
||||
expect(dispatchEvent).not.toHaveBeenCalled()
|
||||
|
||||
revealFloatingWorkspacePanel(panelState({ enabled: true, open: false }))
|
||||
expect(dispatchEvent).toHaveBeenCalledOnce()
|
||||
expect(dispatchEvent.mock.calls[0][0]).toMatchObject({ type: TOGGLE_FLOATING_TERMINAL_EVENT })
|
||||
})
|
||||
|
||||
it('enables a disabled panel, then opens it a frame later unless it was left open', async () => {
|
||||
const { dispatchEvent, runFrames } = stubWindow()
|
||||
const leftOpen = panelState({ enabled: false, open: true })
|
||||
const leftClosed = panelState({ enabled: false, open: false })
|
||||
|
||||
revealFloatingWorkspacePanel(leftOpen)
|
||||
revealFloatingWorkspacePanel(leftClosed)
|
||||
await settle()
|
||||
expect(leftOpen.updateSettings).toHaveBeenCalledWith({ floatingTerminalEnabled: true })
|
||||
expect(leftClosed.updateSettings).toHaveBeenCalledWith({ floatingTerminalEnabled: true })
|
||||
expect(dispatchEvent).not.toHaveBeenCalled()
|
||||
|
||||
runFrames()
|
||||
expect(dispatchEvent).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -1,832 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultSettings } from '../../../shared/constants'
|
||||
import type { Tab } from '../../../shared/tab-types'
|
||||
import type { TerminalTab } from '../../../shared/terminal-tab-types'
|
||||
import {
|
||||
countVisibleFloatingWorkspaceItems,
|
||||
createFloatingWorkspaceBrowserTab,
|
||||
createFloatingWorkspaceMarkdownTab,
|
||||
createFloatingWorkspaceTerminalTab,
|
||||
handleEmptyFloatingWorkspacePanelCloseShortcut,
|
||||
isFloatingWorkspacePanelFocused,
|
||||
isFloatingWorkspacePanelShortcut,
|
||||
isFloatingWorkspacePanelShortcutTarget,
|
||||
isFloatingWorkspaceTerminalInputTarget,
|
||||
matchFloatingWorkspacePanelChord,
|
||||
shouldMinimizeFloatingWorkspacePanelOnCloseShortcut,
|
||||
switchFloatingWorkspaceRecentTab,
|
||||
switchFloatingWorkspaceTab
|
||||
} from './floating-workspace-terminal-actions'
|
||||
import {
|
||||
matchFloatingWorkspacePanelOwnedAction,
|
||||
FLOATING_WORKSPACE_SHORTCUT_SURFACE_SELECTOR
|
||||
} from './floating-workspace-shortcut-policy'
|
||||
|
||||
const activateWebRuntimeSessionTabMock = vi.hoisted(() => vi.fn())
|
||||
const createWebRuntimeSessionBrowserTabMock = vi.hoisted(() => vi.fn())
|
||||
const createWebRuntimeSessionTerminalMock = vi.hoisted(() => vi.fn())
|
||||
const createUntitledMarkdownFileWithTemplateSelectionMock = vi.hoisted(() => vi.fn())
|
||||
const focusTerminalTabSurfaceMock = vi.hoisted(() => vi.fn())
|
||||
const isWebRuntimeSessionActiveMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/runtime/web-runtime-session-tab-lifecycle', () => ({
|
||||
activateWebRuntimeSessionTab: activateWebRuntimeSessionTabMock
|
||||
}))
|
||||
vi.mock('@/runtime/web-runtime-browser-creation', () => ({
|
||||
createWebRuntimeSessionBrowserTab: createWebRuntimeSessionBrowserTabMock
|
||||
}))
|
||||
vi.mock('@/runtime/web-runtime-terminal-creation', () => ({
|
||||
createWebRuntimeSessionTerminal: createWebRuntimeSessionTerminalMock
|
||||
}))
|
||||
vi.mock('@/runtime/web-runtime-session-environment', () => ({
|
||||
isWebRuntimeSessionActive: isWebRuntimeSessionActiveMock
|
||||
}))
|
||||
|
||||
vi.mock('./create-untitled-markdown', () => ({
|
||||
createUntitledMarkdownFileWithTemplateSelection:
|
||||
createUntitledMarkdownFileWithTemplateSelectionMock
|
||||
}))
|
||||
|
||||
vi.mock('./connection-context', () => ({
|
||||
getConnectionId: vi.fn(() => null)
|
||||
}))
|
||||
|
||||
vi.mock('./focus-terminal-tab-surface', () => ({
|
||||
focusTerminalTabSurface: focusTerminalTabSurfaceMock
|
||||
}))
|
||||
|
||||
function shortcutEvent(overrides: Partial<KeyboardEvent>): KeyboardEvent {
|
||||
return {
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
key: 't',
|
||||
metaKey: false,
|
||||
shiftKey: false,
|
||||
...overrides
|
||||
} as KeyboardEvent
|
||||
}
|
||||
|
||||
function shortcutSurfaceEvent(overrides: Partial<KeyboardEvent>): KeyboardEvent {
|
||||
return shortcutEvent({
|
||||
target: makeElement({
|
||||
closestSelectors: [FLOATING_WORKSPACE_SHORTCUT_SURFACE_SELECTOR]
|
||||
}),
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
function installFakeHTMLElement(): void {
|
||||
vi.stubGlobal('HTMLElement', class {})
|
||||
}
|
||||
|
||||
function makeElement({
|
||||
attributes = [],
|
||||
classNames = [],
|
||||
closestSelectors = []
|
||||
}: {
|
||||
attributes?: string[]
|
||||
classNames?: string[]
|
||||
closestSelectors?: string[]
|
||||
}): HTMLElement {
|
||||
const element = {
|
||||
classList: {
|
||||
contains: vi.fn((token: string) => classNames.includes(token))
|
||||
},
|
||||
getAttribute: vi.fn((attribute: string) => (attributes.includes(attribute) ? '' : null)),
|
||||
closest: vi.fn((selector: string) => (closestSelectors.includes(selector) ? {} : null))
|
||||
}
|
||||
Object.setPrototypeOf(element, HTMLElement.prototype)
|
||||
return element as unknown as HTMLElement
|
||||
}
|
||||
|
||||
function makeTab(id: string): TerminalTab {
|
||||
return {
|
||||
id,
|
||||
ptyId: null,
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
title: 'Terminal',
|
||||
customTitle: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
function makeUnifiedTerminalTab(id: string, groupId = 'floating-group'): Tab {
|
||||
return {
|
||||
id,
|
||||
entityId: id,
|
||||
groupId,
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
contentType: 'terminal',
|
||||
label: 'Terminal',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
function floatingPanelState({
|
||||
enabled = true,
|
||||
open = true,
|
||||
tabs = []
|
||||
}: { enabled?: boolean; open?: boolean; tabs?: Tab[] } = {}) {
|
||||
return {
|
||||
settings: { ...getDefaultSettings('/home/me'), floatingTerminalEnabled: enabled },
|
||||
floatingWorkspacePanelOpen: open,
|
||||
browserTabsByWorktree: {},
|
||||
openFiles: [],
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: tabs }
|
||||
}
|
||||
}
|
||||
|
||||
const FLOATING_CHAT_TAB = {
|
||||
id: 'floating-chat-1',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
groupId: 'floating-group',
|
||||
contentType: 'agent-session',
|
||||
entityId: 'session-1',
|
||||
label: 'Chat',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 0,
|
||||
createdAt: 0
|
||||
} satisfies Tab
|
||||
|
||||
it('counts tabs in every floating split group when deciding whether a close emptied it', () => {
|
||||
expect(
|
||||
countVisibleFloatingWorkspaceItems(
|
||||
floatingPanelState({
|
||||
tabs: [FLOATING_CHAT_TAB, { ...FLOATING_CHAT_TAB, id: 'chat-2', groupId: 'other-group' }]
|
||||
})
|
||||
)
|
||||
).toBe(2)
|
||||
})
|
||||
|
||||
describe('isFloatingWorkspacePanelFocused', () => {
|
||||
it('detects focus inside the floating workspace panel', () => {
|
||||
installFakeHTMLElement()
|
||||
const activeElement = makeElement({
|
||||
closestSelectors: ['[data-floating-terminal-panel]']
|
||||
})
|
||||
|
||||
expect(isFloatingWorkspacePanelFocused({ activeElement } as never)).toBe(true)
|
||||
expect(activeElement.closest).toHaveBeenCalledWith('[data-floating-terminal-panel]')
|
||||
})
|
||||
|
||||
it('ignores focus outside the floating workspace panel', () => {
|
||||
installFakeHTMLElement()
|
||||
const activeElement = makeElement({})
|
||||
|
||||
expect(isFloatingWorkspacePanelFocused({ activeElement } as never)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFloatingWorkspaceTerminalInputTarget', () => {
|
||||
it('detects the xterm helper textarea inside the floating panel', () => {
|
||||
installFakeHTMLElement()
|
||||
const target = makeElement({
|
||||
classNames: ['xterm-helper-textarea'],
|
||||
closestSelectors: ['[data-floating-terminal-panel]']
|
||||
})
|
||||
|
||||
expect(isFloatingWorkspaceTerminalInputTarget(target)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects targets inside xterm DOM inside the floating panel', () => {
|
||||
installFakeHTMLElement()
|
||||
const target = makeElement({
|
||||
closestSelectors: ['[data-floating-terminal-panel]', '.xterm']
|
||||
})
|
||||
|
||||
expect(isFloatingWorkspaceTerminalInputTarget(target)).toBe(true)
|
||||
})
|
||||
|
||||
it('ignores terminal input outside the floating panel', () => {
|
||||
installFakeHTMLElement()
|
||||
const target = makeElement({
|
||||
classNames: ['xterm-helper-textarea']
|
||||
})
|
||||
|
||||
expect(isFloatingWorkspaceTerminalInputTarget(target)).toBe(false)
|
||||
})
|
||||
|
||||
it('ignores non-terminal targets inside the floating panel', () => {
|
||||
installFakeHTMLElement()
|
||||
const target = makeElement({
|
||||
closestSelectors: ['[data-floating-terminal-panel]']
|
||||
})
|
||||
|
||||
expect(isFloatingWorkspaceTerminalInputTarget(target)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFloatingWorkspacePanelShortcut', () => {
|
||||
beforeEach(() => {
|
||||
installFakeHTMLElement()
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Cmd+T', true, { key: 't', metaKey: true }],
|
||||
['Ctrl+T', false, { key: 't', ctrlKey: true }],
|
||||
['Cmd+W', true, { key: 'w', metaKey: true }],
|
||||
['Ctrl+W', false, { key: 'w', ctrlKey: true }],
|
||||
['Cmd+Shift+B', true, { key: 'b', metaKey: true, shiftKey: true }],
|
||||
['Ctrl+Shift+B', false, { key: 'b', ctrlKey: true, shiftKey: true }],
|
||||
['Cmd+Shift+M', true, { key: 'm', metaKey: true, shiftKey: true }],
|
||||
['Ctrl+Shift+M', false, { key: 'm', ctrlKey: true, shiftKey: true }],
|
||||
['Cmd+Shift+O', true, { key: 'o', metaKey: true, shiftKey: true }],
|
||||
['Ctrl+Shift+O', false, { key: 'o', ctrlKey: true, shiftKey: true }]
|
||||
])('claims %s', (_label, isMacPlatform, overrides) => {
|
||||
expect(isFloatingWorkspacePanelShortcut(shortcutSurfaceEvent(overrides), isMacPlatform)).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['Cmd+B', true, { key: 'b', metaKey: true }],
|
||||
['Ctrl+B', false, { key: 'b', ctrlKey: true }]
|
||||
])('does not claim bare %s', (_label, isMacPlatform, overrides) => {
|
||||
expect(isFloatingWorkspacePanelShortcut(shortcutSurfaceEvent(overrides), isMacPlatform)).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
|
||||
it('claims shortcuts by produced logical key rather than physical key', () => {
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutSurfaceEvent({ key: 'w', code: 'Comma', metaKey: true }),
|
||||
'darwin'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutSurfaceEvent({ key: ',', code: 'KeyW', metaKey: true }),
|
||||
'darwin'
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('honors customized tab shortcuts for the floating panel surface', () => {
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutSurfaceEvent({ key: 'n', code: 'KeyN', ctrlKey: true }),
|
||||
'linux',
|
||||
null,
|
||||
{ 'tab.newTerminal': ['Ctrl+N'] }
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutSurfaceEvent({ key: 't', code: 'KeyT', ctrlKey: true }),
|
||||
'linux',
|
||||
null,
|
||||
{ 'tab.newTerminal': ['Ctrl+N'] }
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('claims customized double-tap shortcuts for the floating panel surface', () => {
|
||||
const event = shortcutSurfaceEvent({}) as KeyboardEvent & { doubleTapModifier: 'Shift' }
|
||||
event.doubleTapModifier = 'Shift'
|
||||
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(event, 'linux', null, {
|
||||
'tab.newTerminal': ['DoubleTap+Shift']
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('does not claim shortcuts with Alt or the wrong platform modifier', () => {
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(shortcutSurfaceEvent({ key: 't', metaKey: true }), false)
|
||||
).toBe(false)
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(shortcutSurfaceEvent({ key: 't', ctrlKey: true }), true)
|
||||
).toBe(false)
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutSurfaceEvent({ key: 't', ctrlKey: true, altKey: true }),
|
||||
false
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('only claims shortcuts from the panel root or shortcut surface', () => {
|
||||
const panelRoot = makeElement({
|
||||
attributes: ['data-floating-terminal-panel'],
|
||||
closestSelectors: ['[data-floating-terminal-panel]']
|
||||
})
|
||||
const panelContent = makeElement({
|
||||
closestSelectors: ['[data-floating-terminal-panel]']
|
||||
})
|
||||
|
||||
expect(isFloatingWorkspacePanelShortcutTarget(panelRoot, panelRoot)).toBe(true)
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutEvent({ key: 't', ctrlKey: true, target: panelRoot }),
|
||||
false
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isFloatingWorkspacePanelShortcut(
|
||||
shortcutEvent({ key: 't', ctrlKey: true, target: panelContent }),
|
||||
false,
|
||||
panelRoot
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('matchFloatingWorkspacePanelOwnedAction', () => {
|
||||
beforeEach(() => {
|
||||
installFakeHTMLElement()
|
||||
})
|
||||
|
||||
it('returns the matched action so one pass serves the claim check and the dispatch branch', () => {
|
||||
expect(
|
||||
matchFloatingWorkspacePanelOwnedAction(
|
||||
shortcutEvent({ key: 't', code: 'KeyT', ctrlKey: true, target: makeElement({}) }),
|
||||
'linux',
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toBe('tab.newTerminal')
|
||||
expect(
|
||||
matchFloatingWorkspacePanelOwnedAction(
|
||||
shortcutEvent({ key: 'w', code: 'KeyW', ctrlKey: true, target: makeElement({}) }),
|
||||
'linux',
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toBe('tab.close')
|
||||
})
|
||||
|
||||
it('does not gate on the event target — the panel owns these chords from any pane it hosts', () => {
|
||||
expect(
|
||||
matchFloatingWorkspacePanelOwnedAction(
|
||||
shortcutEvent({ key: 'b', code: 'KeyB', target: makeElement({}) }),
|
||||
'linux',
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('matchFloatingWorkspacePanelChord', () => {
|
||||
beforeEach(() => {
|
||||
installFakeHTMLElement()
|
||||
})
|
||||
|
||||
it('claims a creation chord from the shortcut surface', () => {
|
||||
expect(
|
||||
matchFloatingWorkspacePanelChord(
|
||||
shortcutSurfaceEvent({ key: 't', code: 'KeyT', ctrlKey: true }),
|
||||
'linux',
|
||||
null,
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toEqual({ kind: 'action', action: 'tab.newTerminal' })
|
||||
})
|
||||
|
||||
it('keeps creation chords target-gated', () => {
|
||||
expect(
|
||||
matchFloatingWorkspacePanelChord(
|
||||
shortcutEvent({ key: 't', code: 'KeyT', ctrlKey: true, target: makeElement({}) }),
|
||||
'linux',
|
||||
null,
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('claims indexed switching and rename regardless of the event target', () => {
|
||||
expect(
|
||||
matchFloatingWorkspacePanelChord(
|
||||
shortcutEvent({ key: '2', code: 'Digit2', ctrlKey: true, target: makeElement({}) }),
|
||||
'linux',
|
||||
null,
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toEqual({ kind: 'index', index: 1 })
|
||||
expect(
|
||||
matchFloatingWorkspacePanelChord(
|
||||
shortcutEvent({ key: 'r', code: 'KeyR', metaKey: true, target: makeElement({}) }),
|
||||
'darwin',
|
||||
null,
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toEqual({ kind: 'action', action: 'tab.rename' })
|
||||
})
|
||||
|
||||
it('does not claim chords the panel has no shortcut for', () => {
|
||||
expect(
|
||||
matchFloatingWorkspacePanelChord(
|
||||
shortcutSurfaceEvent({ key: 'b', code: 'KeyB', ctrlKey: true }),
|
||||
'linux',
|
||||
null,
|
||||
undefined,
|
||||
{ context: 'app' }
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('createFloatingWorkspaceTerminalTab', () => {
|
||||
beforeEach(() => {
|
||||
activateWebRuntimeSessionTabMock.mockReset()
|
||||
createWebRuntimeSessionTerminalMock.mockReset()
|
||||
focusTerminalTabSurfaceMock.mockReset()
|
||||
isWebRuntimeSessionActiveMock.mockReset()
|
||||
})
|
||||
|
||||
it('creates and focuses a local floating workspace terminal in the active floating group', async () => {
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
createTab: vi.fn(),
|
||||
activateTab: vi.fn()
|
||||
}
|
||||
createWebRuntimeSessionTerminalMock.mockResolvedValue(false)
|
||||
|
||||
await createFloatingWorkspaceTerminalTab(store)
|
||||
|
||||
expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled()
|
||||
expect(store.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
undefined,
|
||||
{ id: expect.any(String) }
|
||||
)
|
||||
expect(store.activateTab).not.toHaveBeenCalled()
|
||||
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith(store.createTab.mock.calls[0][3].id)
|
||||
})
|
||||
|
||||
it('ignores the active runtime and keeps floating workspace terminals local', async () => {
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' },
|
||||
createTab: vi.fn(),
|
||||
activateTab: vi.fn()
|
||||
}
|
||||
createWebRuntimeSessionTerminalMock.mockResolvedValue(true)
|
||||
|
||||
await createFloatingWorkspaceTerminalTab(store, 'pwsh')
|
||||
|
||||
expect(createWebRuntimeSessionTerminalMock).not.toHaveBeenCalled()
|
||||
expect(store.createTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'floating-group',
|
||||
'pwsh',
|
||||
{ id: expect.any(String) }
|
||||
)
|
||||
expect(store.activateTab).not.toHaveBeenCalled()
|
||||
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith(store.createTab.mock.calls[0][3].id)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createFloatingWorkspaceBrowserTab', () => {
|
||||
beforeEach(() => {
|
||||
createWebRuntimeSessionBrowserTabMock.mockReset()
|
||||
})
|
||||
|
||||
it('creates floating browser tabs in the active floating group', async () => {
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
browserDefaultUrl: 'about:blank',
|
||||
settings: { activeRuntimeEnvironmentId: null },
|
||||
createBrowserTab: vi.fn()
|
||||
}
|
||||
createWebRuntimeSessionBrowserTabMock.mockResolvedValue(false)
|
||||
|
||||
await createFloatingWorkspaceBrowserTab(store)
|
||||
|
||||
expect(createWebRuntimeSessionBrowserTabMock).not.toHaveBeenCalled()
|
||||
expect(store.createBrowserTab).toHaveBeenCalledWith(
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
'about:blank',
|
||||
{
|
||||
title: 'New Browser Tab',
|
||||
focusAddressBar: true,
|
||||
targetGroupId: 'floating-group',
|
||||
browserRuntimeEnvironmentId: null
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createFloatingWorkspaceMarkdownTab', () => {
|
||||
beforeEach(() => {
|
||||
createUntitledMarkdownFileWithTemplateSelectionMock.mockReset()
|
||||
})
|
||||
|
||||
it('creates floating markdown tabs without activating the main workspace', async () => {
|
||||
const fileInfo = {
|
||||
filePath: '/tmp/orca/floating-workspace/untitled.md',
|
||||
relativePath: 'untitled.md',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
language: 'markdown',
|
||||
isUntitled: true,
|
||||
mode: 'edit'
|
||||
}
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
openFile: vi.fn()
|
||||
}
|
||||
createUntitledMarkdownFileWithTemplateSelectionMock.mockResolvedValue(fileInfo)
|
||||
|
||||
await createFloatingWorkspaceMarkdownTab(store as never, '/tmp/orca/floating-workspace')
|
||||
|
||||
expect(createUntitledMarkdownFileWithTemplateSelectionMock).toHaveBeenCalledWith(
|
||||
'/tmp/orca/floating-workspace',
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
undefined,
|
||||
{ kind: 'local' }
|
||||
)
|
||||
expect(store.openFile).toHaveBeenCalledWith(fileInfo, {
|
||||
preview: false,
|
||||
targetGroupId: 'floating-group',
|
||||
suppressActiveRuntimeFallback: true
|
||||
})
|
||||
})
|
||||
|
||||
it('does not open a floating markdown tab when template selection is cancelled', async () => {
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
openFile: vi.fn()
|
||||
}
|
||||
createUntitledMarkdownFileWithTemplateSelectionMock.mockResolvedValue(null)
|
||||
|
||||
await createFloatingWorkspaceMarkdownTab(store as never, '/tmp/orca/floating-workspace')
|
||||
|
||||
expect(createUntitledMarkdownFileWithTemplateSelectionMock).toHaveBeenCalledWith(
|
||||
'/tmp/orca/floating-workspace',
|
||||
FLOATING_TERMINAL_WORKTREE_ID,
|
||||
undefined,
|
||||
{ kind: 'local' }
|
||||
)
|
||||
expect(store.openFile).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('switchFloatingWorkspaceTab', () => {
|
||||
beforeEach(() => {
|
||||
activateWebRuntimeSessionTabMock.mockReset()
|
||||
focusTerminalTabSurfaceMock.mockReset()
|
||||
isWebRuntimeSessionActiveMock.mockReset()
|
||||
isWebRuntimeSessionActiveMock.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('cycles terminal tabs inside the floating workspace active group', () => {
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
activateTab: vi.fn(),
|
||||
browserPagesByWorkspace: {},
|
||||
browserTabsByWorktree: {},
|
||||
groupsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeTabId: 'tab-1',
|
||||
tabOrder: ['tab-1', 'tab-2'],
|
||||
recentTabIds: ['tab-1']
|
||||
}
|
||||
]
|
||||
},
|
||||
openFiles: [],
|
||||
setActiveTab: vi.fn(),
|
||||
tabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [makeTab('tab-1'), makeTab('tab-2')]
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
makeUnifiedTerminalTab('tab-1'),
|
||||
makeUnifiedTerminalTab('tab-2')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
expect(switchFloatingWorkspaceTab(store as never, 1, 'same-type')).toBe(true)
|
||||
|
||||
expect(store.activateTab).toHaveBeenCalledWith('tab-2')
|
||||
expect(store.setActiveTab).toHaveBeenCalledWith('tab-2')
|
||||
expect(focusTerminalTabSurfaceMock).toHaveBeenCalledWith('tab-2')
|
||||
expect(activateWebRuntimeSessionTabMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('cycles browser tabs locally while a web runtime is active', () => {
|
||||
const notifyActiveTabChanged = vi.fn()
|
||||
vi.stubGlobal('window', { api: { browser: { notifyActiveTabChanged } } })
|
||||
isWebRuntimeSessionActiveMock.mockReturnValue(true)
|
||||
const browserTab = {
|
||||
id: 'browser-2',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
url: 'https://example.com',
|
||||
title: 'Browser',
|
||||
loading: false,
|
||||
faviconUrl: null,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
loadError: null,
|
||||
createdAt: 0,
|
||||
activePageId: 'page-2'
|
||||
}
|
||||
const store = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
activateTab: vi.fn(),
|
||||
browserPagesByWorkspace: {},
|
||||
browserTabsByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: [browserTab] },
|
||||
groupsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeTabId: 'tab-1',
|
||||
tabOrder: ['tab-1', 'tab-browser-2'],
|
||||
recentTabIds: ['tab-1']
|
||||
}
|
||||
]
|
||||
},
|
||||
openFiles: [],
|
||||
setActiveTab: vi.fn(),
|
||||
tabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [makeTab('tab-1')]
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
makeUnifiedTerminalTab('tab-1'),
|
||||
{
|
||||
id: 'tab-browser-2',
|
||||
entityId: 'browser-2',
|
||||
groupId: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
contentType: 'browser',
|
||||
label: 'Browser',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: 1,
|
||||
createdAt: 1
|
||||
} satisfies Tab
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
expect(switchFloatingWorkspaceTab(store as never, 1, 'all-types')).toBe(true)
|
||||
|
||||
expect(store.activateTab).toHaveBeenCalledWith('tab-browser-2')
|
||||
expect(activateWebRuntimeSessionTabMock).not.toHaveBeenCalled()
|
||||
expect(notifyActiveTabChanged).toHaveBeenCalledWith({ browserPageId: 'page-2' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('switchFloatingWorkspaceRecentTab', () => {
|
||||
it('switches to the previous recent tab in the floating workspace', () => {
|
||||
const activateTab = vi.fn()
|
||||
const setActiveTab = vi.fn()
|
||||
const store: Parameters<typeof switchFloatingWorkspaceRecentTab>[0] = {
|
||||
activeGroupIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-group' },
|
||||
activateTab,
|
||||
browserTabsByWorktree: {},
|
||||
groupsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeTabId: 'tab-1',
|
||||
tabOrder: ['tab-1', 'tab-2'],
|
||||
recentTabIds: ['tab-2', 'tab-1']
|
||||
}
|
||||
]
|
||||
},
|
||||
openFiles: [],
|
||||
setActiveTab,
|
||||
tabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [makeTab('tab-1'), makeTab('tab-2')]
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
makeUnifiedTerminalTab('tab-1'),
|
||||
makeUnifiedTerminalTab('tab-2')
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
expect(switchFloatingWorkspaceRecentTab(store)).toBe(true)
|
||||
expect(activateTab).toHaveBeenCalledWith('tab-2')
|
||||
expect(setActiveTab).toHaveBeenCalledWith('tab-2')
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldMinimizeFloatingWorkspacePanelOnCloseShortcut', () => {
|
||||
const base = {
|
||||
floatingTerminalOpen: true,
|
||||
floatingVisibleTabCount: 0
|
||||
}
|
||||
|
||||
it('allows Cmd/Ctrl+W to minimize any empty floating panel', () => {
|
||||
expect(shouldMinimizeFloatingWorkspacePanelOnCloseShortcut(base)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not minimize when the floating panel is hidden or has tabs', () => {
|
||||
expect(
|
||||
shouldMinimizeFloatingWorkspacePanelOnCloseShortcut({
|
||||
...base,
|
||||
floatingVisibleTabCount: 1
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
shouldMinimizeFloatingWorkspacePanelOnCloseShortcut({
|
||||
...base,
|
||||
floatingTerminalOpen: false
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('handleEmptyFloatingWorkspacePanelCloseShortcut', () => {
|
||||
it('minimizes the visible empty floating workspace on Cmd/Ctrl+W', () => {
|
||||
installFakeHTMLElement()
|
||||
const dispatchEvent = vi.fn()
|
||||
vi.stubGlobal('window', { dispatchEvent })
|
||||
vi.stubGlobal('document', { querySelector: vi.fn().mockReturnValue({}) })
|
||||
const event = {
|
||||
altKey: false,
|
||||
code: 'KeyW',
|
||||
ctrlKey: false,
|
||||
key: 'w',
|
||||
metaKey: true,
|
||||
preventDefault: vi.fn(),
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
stopImmediatePropagation: vi.fn(),
|
||||
stopPropagation: vi.fn()
|
||||
} as unknown as KeyboardEvent
|
||||
|
||||
expect(
|
||||
handleEmptyFloatingWorkspacePanelCloseShortcut(floatingPanelState(), event, 'darwin')
|
||||
).toBe(true)
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalledWith()
|
||||
expect(event.stopPropagation).toHaveBeenCalledWith()
|
||||
expect(event.stopImmediatePropagation).toHaveBeenCalledWith()
|
||||
expect(dispatchEvent).toHaveBeenCalledWith(expect.any(Event))
|
||||
const dispatchedEvent = dispatchEvent.mock.calls[0][0] as Event
|
||||
expect(dispatchedEvent.type).toBe('orca-toggle-floating-terminal')
|
||||
})
|
||||
|
||||
it('ignores non-close shortcuts and non-empty floating workspaces', () => {
|
||||
vi.stubGlobal('window', { dispatchEvent: vi.fn() })
|
||||
vi.stubGlobal('document', { querySelector: vi.fn().mockReturnValue({}) })
|
||||
const nonCloseEvent = {
|
||||
altKey: false,
|
||||
code: 'KeyT',
|
||||
ctrlKey: false,
|
||||
key: 't',
|
||||
metaKey: true,
|
||||
preventDefault: vi.fn(),
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
stopImmediatePropagation: vi.fn(),
|
||||
stopPropagation: vi.fn()
|
||||
} as unknown as KeyboardEvent
|
||||
|
||||
expect(
|
||||
handleEmptyFloatingWorkspacePanelCloseShortcut(floatingPanelState(), nonCloseEvent, 'darwin')
|
||||
).toBe(false)
|
||||
expect(nonCloseEvent.preventDefault).not.toHaveBeenCalled()
|
||||
|
||||
const event = {
|
||||
altKey: false,
|
||||
code: 'KeyW',
|
||||
ctrlKey: false,
|
||||
key: 'w',
|
||||
metaKey: true,
|
||||
preventDefault: vi.fn(),
|
||||
repeat: false,
|
||||
shiftKey: false,
|
||||
stopImmediatePropagation: vi.fn(),
|
||||
stopPropagation: vi.fn()
|
||||
} as unknown as KeyboardEvent
|
||||
|
||||
for (const state of [
|
||||
floatingPanelState({ tabs: [FLOATING_CHAT_TAB] }),
|
||||
floatingPanelState({ open: false }),
|
||||
floatingPanelState({ enabled: false })
|
||||
]) {
|
||||
expect(handleEmptyFloatingWorkspacePanelCloseShortcut(state, event, 'darwin')).toBe(false)
|
||||
}
|
||||
expect(event.preventDefault).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createFloatingWorkspaceTourInteractionSnapshot } from './floating-workspace-tour-interaction-snapshot'
|
||||
import type { FeatureInteractionState } from '../../../shared/feature-interactions'
|
||||
|
||||
describe('createFloatingWorkspaceTourInteractionSnapshot', () => {
|
||||
it('captures first-open state before recording the floating workspace interaction', () => {
|
||||
const featureInteractions: FeatureInteractionState = {}
|
||||
const persisted = Promise.resolve()
|
||||
const recordFeatureInteraction = vi.fn(() => {
|
||||
featureInteractions['floating-workspace'] = {
|
||||
firstInteractedAt: 100,
|
||||
interactionCount: 1
|
||||
}
|
||||
return persisted
|
||||
})
|
||||
|
||||
const snapshot = createFloatingWorkspaceTourInteractionSnapshot({
|
||||
featureInteractions,
|
||||
persistedUIReady: true,
|
||||
recordFeatureInteraction
|
||||
})
|
||||
|
||||
expect(recordFeatureInteraction).toHaveBeenCalledOnce()
|
||||
expect(recordFeatureInteraction).toHaveBeenCalledWith('floating-workspace')
|
||||
expect(snapshot.wasPreviouslyInteracted).toBe(false)
|
||||
expect(snapshot.persisted).toBe(persisted)
|
||||
expect(snapshot.recordFeatureInteractionForTour).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { i18n } from '@/i18n/i18n'
|
||||
import type { FolderWorkspacePathStatus } from '../../../shared/folder-workspace-path-status'
|
||||
import {
|
||||
formatFolderWorkspaceCreateError,
|
||||
humanizeFolderWorkspacePathError,
|
||||
getFolderWorkspacePathErrorCopy,
|
||||
getFolderWorkspacePathStatusDescription,
|
||||
getFolderWorkspacePathStatusTitle
|
||||
} from './folder-workspace-path-status'
|
||||
|
||||
// RPC returns z.unknown and casts it; tests must cover newer-host wire values outside this build’s union.
|
||||
function wireStatus(reason: unknown): FolderWorkspacePathStatus {
|
||||
return { path: '/srv/scans', exists: false, reason } as unknown as FolderWorkspacePathStatus
|
||||
}
|
||||
|
||||
describe('getFolderWorkspacePathStatusTitle', () => {
|
||||
it('still titles a broken folder when a newer host sends an undeclared reason', () => {
|
||||
const title = getFolderWorkspacePathStatusTitle(wireStatus('permission-denied'))
|
||||
|
||||
expect(typeof title).toBe('string')
|
||||
expect(title).not.toBe('')
|
||||
})
|
||||
|
||||
// hasOwn coerces [missing] into a valid key; reject arrays before the switch (#15002).
|
||||
it('does not admit a non-string reason through the membership guard', () => {
|
||||
const title = getFolderWorkspacePathStatusTitle(wireStatus(['missing']))
|
||||
|
||||
expect(typeof title).toBe('string')
|
||||
expect(title).not.toBe('')
|
||||
expect(title).not.toBe('Folder not found')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFolderWorkspacePathStatusDescription', () => {
|
||||
it('still describes a broken folder when a newer host sends an undeclared reason', () => {
|
||||
const description = getFolderWorkspacePathStatusDescription(wireStatus('permission-denied'))
|
||||
|
||||
expect(typeof description).toBe('string')
|
||||
expect(description).not.toBe('')
|
||||
expect(description).toContain('/srv/scans')
|
||||
})
|
||||
|
||||
it('does not admit a non-string reason through the membership guard', () => {
|
||||
const description = getFolderWorkspacePathStatusDescription(wireStatus(['missing']))
|
||||
|
||||
expect(typeof description).toBe('string')
|
||||
expect(description).not.toBe('')
|
||||
expect(description).not.toBe(
|
||||
'Orca cannot find /srv/scans. Remove and re-import this folder workspace.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getFolderWorkspacePathErrorCopy', () => {
|
||||
it('maps the ambiguous-connection code the runtime throws without a path', () => {
|
||||
expect(
|
||||
getFolderWorkspacePathErrorCopy(
|
||||
"Error invoking remote method 'pty:spawn': Error: folder_workspace_connection_ambiguous"
|
||||
)?.title
|
||||
).toBe('Cannot determine connection')
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['/srv/project: folder', 'C:\\work\\My Project:backup', '/tmp/$& folder'])(
|
||||
'preserves a literal path %s and IPC wrapper',
|
||||
(path) => {
|
||||
const prefix = "Error invoking remote method 'pty:spawn': Error: "
|
||||
expect(humanizeFolderWorkspacePathError(`${prefix}folder_workspace_path_missing:${path}`)).toBe(
|
||||
`${prefix}Orca cannot find ${path}. Remove and re-import the folder.`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
'not_folder_workspace_path_missing:/x',
|
||||
'folder_workspace_path_missing_extra:/x',
|
||||
'Failed to read /tmp/folder_workspace_path_missing:/x',
|
||||
'folder_workspace_path_missing-more',
|
||||
'disk full'
|
||||
])('passes through unknown error %s', (error) => {
|
||||
expect(humanizeFolderWorkspacePathError(error)).toBe(error)
|
||||
expect(getFolderWorkspacePathErrorCopy(error)).toBeNull()
|
||||
})
|
||||
|
||||
it('preserves multiline paths in the existing create-folder formatter', () => {
|
||||
const path = '/tmp/folder\nwith\rline breaks'
|
||||
expect(
|
||||
formatFolderWorkspaceCreateError(`folder_workspace_path_missing:${path}`).description
|
||||
).toBe(`Orca cannot find ${path}. Remove and re-import the folder.`)
|
||||
})
|
||||
|
||||
it('uses the existing English fallback when a language pack has no folder-path translation', async () => {
|
||||
await i18n.changeLanguage('test-missing-folder-copy')
|
||||
try {
|
||||
expect(humanizeFolderWorkspacePathError('folder_workspace_path_missing:/tmp/project')).toBe(
|
||||
'Orca cannot find /tmp/project. Remove and re-import the folder.'
|
||||
)
|
||||
} finally {
|
||||
await i18n.changeLanguage('en')
|
||||
}
|
||||
})
|
||||
@@ -13,16 +13,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('foreground terminal tabs', () => {
|
||||
it('returns the union of explicit foreground ids and visible terminal claims', () => {
|
||||
setForegroundTerminalTabIds(['tab-explicit', null, '', undefined])
|
||||
const unregister = registerVisibleTerminalTab('tab-visible')
|
||||
|
||||
expect(getForegroundTerminalTabIds().sort()).toEqual(['tab-explicit', 'tab-visible'])
|
||||
|
||||
unregister()
|
||||
expect(getForegroundTerminalTabIds()).toEqual(['tab-explicit'])
|
||||
})
|
||||
|
||||
it('keeps duplicate visible terminal tab claims until every token unregisters', () => {
|
||||
const unregisterFirst = registerVisibleTerminalTab('tab-visible')
|
||||
const unregisterSecond = registerVisibleTerminalTab('tab-visible')
|
||||
@@ -36,18 +26,6 @@ describe('foreground terminal tabs', () => {
|
||||
expect(getForegroundTerminalTabIds()).toEqual([])
|
||||
})
|
||||
|
||||
it('records last-seen timestamps for explicit foreground entries and clears them in tests', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(1_000)
|
||||
|
||||
setForegroundTerminalTabIds(['tab-explicit', null, '', undefined])
|
||||
|
||||
expect(getForegroundTerminalTabLastSeenAtById()).toEqual({ 'tab-explicit': 1_000 })
|
||||
|
||||
resetForegroundTerminalTabIdsForTests()
|
||||
expect(getForegroundTerminalTabLastSeenAtById()).toEqual({})
|
||||
})
|
||||
|
||||
it('refreshes last-seen when explicit foreground ids leave the combined foreground set', () => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(1_000)
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const groupsByWorktree: Record<string, { id: string; tabOrder: string[] }[]> = {}
|
||||
const noted: string[][] = []
|
||||
const flags: { frameDrawn: boolean; routed: boolean; supported: boolean | null } = {
|
||||
frameDrawn: true,
|
||||
routed: true,
|
||||
supported: true
|
||||
}
|
||||
return {
|
||||
state: { groupsByWorktree },
|
||||
...flags,
|
||||
/** Tabs the server's frames hold, by desktop id → server id. */
|
||||
serverIds: new Map<string, string>(),
|
||||
noted
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: () => mocks.state } }))
|
||||
vi.mock('@/runtime/runtime-rpc-client', () => ({
|
||||
readCachedRuntimeEnvironmentCapability: () => mocks.supported
|
||||
}))
|
||||
vi.mock('@/runtime/web-session-tabs-sync/frame-drawn-server-workspaces', () => ({
|
||||
isFrameDrawnServerWorkspace: () => mocks.frameDrawn
|
||||
}))
|
||||
vi.mock('@/runtime/server-layout-apply-routes', () => ({
|
||||
serverLayoutApplyRoute: () =>
|
||||
mocks.routed ? { serverTabId: (id: string) => mocks.serverIds.get(id) ?? null } : null
|
||||
}))
|
||||
vi.mock('./desktop-created-server-tabs', () => ({
|
||||
noteDesktopCreatedServerTab: (...args: string[]) => mocks.noted.push(args)
|
||||
}))
|
||||
|
||||
import type { StructuredAgentSessionCreateParams } from '../../../shared/agent-session/structured-agent-session-create'
|
||||
import { withFrameDrawnChatTabPlacement } from './frame-drawn-chat-tab-placement'
|
||||
|
||||
const TAB = 'structured-agent-session-claude_1'
|
||||
const params: StructuredAgentSessionCreateParams = {
|
||||
envelope: {
|
||||
sessionId: 'claude_1',
|
||||
clientOperationId: 'op-1',
|
||||
expectedRuntimeFence: null,
|
||||
payloadFingerprint: 'fp'
|
||||
},
|
||||
worktree: 'id:wt',
|
||||
agent: 'claude'
|
||||
}
|
||||
const intent = {
|
||||
target: { kind: 'environment', environmentId: 'env-1' } as const,
|
||||
worktreeId: 'wt',
|
||||
sessionId: 'claude_1',
|
||||
params
|
||||
}
|
||||
|
||||
describe('a chat opened in a frame-drawn server workspace (L270)', () => {
|
||||
beforeEach(() => {
|
||||
mocks.frameDrawn = true
|
||||
mocks.supported = true
|
||||
mocks.routed = true
|
||||
mocks.serverIds = new Map([
|
||||
['web-terminal-pty-1', 'host-tab-1'],
|
||||
['editor-1', 'editor-1'],
|
||||
['editor-2', 'editor-2']
|
||||
])
|
||||
mocks.noted = []
|
||||
mocks.state.groupsByWorktree = {
|
||||
wt: [
|
||||
{ id: 'g-1', tabOrder: ['editor-0'] },
|
||||
{ id: 'g-2', tabOrder: ['editor-1', 'sim-1', TAB, 'editor-2'] }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
it("names the tab's group and the server tab before it, skipping tabs the server lacks", () => {
|
||||
expect(withFrameDrawnChatTabPlacement(intent)).toEqual({
|
||||
...params,
|
||||
tabPlacement: { groupId: 'g-2', afterTabId: 'editor-1' }
|
||||
})
|
||||
})
|
||||
|
||||
it("names the server's id for a terminal tab before the chat", () => {
|
||||
mocks.state.groupsByWorktree.wt[1].tabOrder = ['web-terminal-pty-1', TAB]
|
||||
expect(withFrameDrawnChatTabPlacement(intent)).toEqual({
|
||||
...params,
|
||||
tabPlacement: { groupId: 'g-2', afterTabId: 'host-tab-1' }
|
||||
})
|
||||
})
|
||||
|
||||
it('names the group alone while the server takes no layout intents for the workspace', () => {
|
||||
mocks.routed = false
|
||||
expect(withFrameDrawnChatTabPlacement(intent)).toEqual({
|
||||
...params,
|
||||
tabPlacement: { groupId: 'g-2' }
|
||||
})
|
||||
})
|
||||
|
||||
it('names the group alone when no server tab comes before the chat', () => {
|
||||
mocks.state.groupsByWorktree.wt[1].tabOrder = [TAB, 'editor-2']
|
||||
expect(withFrameDrawnChatTabPlacement(intent)).toEqual({
|
||||
...params,
|
||||
tabPlacement: { groupId: 'g-2' }
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,177 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildGitHubRepoUrl,
|
||||
normalizeGitHubLinkQuery,
|
||||
parseGitHubIssueOrPRLink,
|
||||
parseGitHubIssueOrPRNumber
|
||||
} from './github-links'
|
||||
import { normalizeGitHubLinkQuery } from './github-links'
|
||||
import { WORK_ITEM_LINK_QUERY_MAX_BYTES } from './work-item-link-query-bounds'
|
||||
|
||||
describe('buildGitHubRepoUrl', () => {
|
||||
it('builds a GitHub repository URL from an owner/repo slug', () => {
|
||||
expect(buildGitHubRepoUrl({ owner: 'stablyai', repo: 'orca' })).toBe(
|
||||
'https://github.com/stablyai/orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('encodes path segments', () => {
|
||||
expect(buildGitHubRepoUrl({ owner: 'stably ai', repo: 'orca/tools' })).toBe(
|
||||
'https://github.com/stably%20ai/orca%2Ftools'
|
||||
)
|
||||
})
|
||||
|
||||
it('links hosted slugs to their GitHub Enterprise server', () => {
|
||||
expect(buildGitHubRepoUrl({ owner: 'team', repo: 'orca', host: 'github.acme-corp.com' })).toBe(
|
||||
'https://github.acme-corp.com/team/orca'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseGitHubIssueOrPRNumber', () => {
|
||||
it('parses plain issue numbers and GitHub pull request URLs', () => {
|
||||
expect(parseGitHubIssueOrPRNumber('42')).toBe(42)
|
||||
expect(parseGitHubIssueOrPRNumber('#42')).toBe(42)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/stablyai/orca/pull/123')).toBe(123)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/stablyai/orca/issues/923')).toBe(923)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.my-company.net/MyOrg/my_repo/pull/395')).toBe(
|
||||
395
|
||||
)
|
||||
})
|
||||
|
||||
it('parses GitHub item URLs with trailing page segments', () => {
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/1965/changes')).toBe(1965)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/1965/files')).toBe(1965)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/1965/commits')).toBe(1965)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/issues/923/comments')).toBe(923)
|
||||
})
|
||||
|
||||
it('parses trailing segments with query, fragment, and repeated slashes', () => {
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/1965/changes?diff=split')).toBe(
|
||||
1965
|
||||
)
|
||||
expect(
|
||||
parseGitHubIssueOrPRNumber('https://github.com/o/r/issues/923/comments#issuecomment-1')
|
||||
).toBe(923)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/1965//changes///')).toBe(1965)
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/issues/923///')).toBe(923)
|
||||
})
|
||||
|
||||
it('rejects invalid GitHub item URLs', () => {
|
||||
expect(parseGitHubIssueOrPRNumber('0')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRNumber('#0')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/0')).toBeNull()
|
||||
expect(
|
||||
parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/not-a-number/changes')
|
||||
).toBeNull()
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/pull/')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/o/r/issues/123abc')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRNumber('https://github.com/owner/repo/pulls/123')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseGitHubIssueOrPRLink', () => {
|
||||
it('parses slug, number, and type for direct item URLs', () => {
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/stablyai/orca/pull/123')).toEqual({
|
||||
slug: { owner: 'stablyai', repo: 'orca', host: 'github.com' },
|
||||
number: 123,
|
||||
type: 'pr'
|
||||
})
|
||||
|
||||
expect(
|
||||
parseGitHubIssueOrPRLink('https://github.my-company.net/MyOrg/my_repo/pull/395')
|
||||
).toEqual({
|
||||
slug: { owner: 'MyOrg', repo: 'my_repo', host: 'github.my-company.net' },
|
||||
number: 395,
|
||||
type: 'pr'
|
||||
})
|
||||
|
||||
expect(parseGitHubIssueOrPRLink('https://git.corp.com/MyOrg/my_repo/pull/395')).toEqual({
|
||||
slug: { owner: 'MyOrg', repo: 'my_repo', host: 'git.corp.com' },
|
||||
number: 395,
|
||||
type: 'pr'
|
||||
})
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/stablyai/orca/issues/923')).toEqual({
|
||||
slug: { owner: 'stablyai', repo: 'orca', host: 'github.com' },
|
||||
number: 923,
|
||||
type: 'issue'
|
||||
})
|
||||
})
|
||||
|
||||
it('derives item type from the route segment when trailing segments are present', () => {
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/pull/1965/changes')).toEqual({
|
||||
slug: { owner: 'o', repo: 'r', host: 'github.com' },
|
||||
number: 1965,
|
||||
type: 'pr'
|
||||
})
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/issues/923/comments')).toEqual({
|
||||
slug: { owner: 'o', repo: 'r', host: 'github.com' },
|
||||
number: 923,
|
||||
type: 'issue'
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts query, fragment, and repeated trailing slashes', () => {
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/pull/1965/files?plain=1#diff')).toEqual(
|
||||
{
|
||||
slug: { owner: 'o', repo: 'r', host: 'github.com' },
|
||||
number: 1965,
|
||||
type: 'pr'
|
||||
}
|
||||
)
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/issues/923/comments///')).toEqual({
|
||||
slug: { owner: 'o', repo: 'r', host: 'github.com' },
|
||||
number: 923,
|
||||
type: 'issue'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects non-GitHub and malformed item URLs', () => {
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/pull/0')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/issues/0')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/pull/not-a-number/changes')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/pull/')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/o/r/issues/123abc')).toBeNull()
|
||||
expect(parseGitHubIssueOrPRLink('https://github.com/owner/repo/pulls/123')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeGitHubLinkQuery', () => {
|
||||
it('accepts full GitHub URLs whose slug differs from the selected repo slug', () => {
|
||||
expect(normalizeGitHubLinkQuery('https://github.com/stablyai/orca/issues/923')).toEqual({
|
||||
query: 'https://github.com/stablyai/orca/issues/923',
|
||||
directNumber: 923,
|
||||
directLink: {
|
||||
slug: { owner: 'stablyai', repo: 'orca', host: 'github.com' },
|
||||
number: 923,
|
||||
type: 'issue'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves PR route intent for full GitHub URLs', () => {
|
||||
expect(normalizeGitHubLinkQuery('https://github.com/stablyai/orca/pull/6934')).toEqual({
|
||||
query: 'https://github.com/stablyai/orca/pull/6934',
|
||||
directNumber: 6934,
|
||||
directLink: {
|
||||
slug: { owner: 'stablyai', repo: 'orca', host: 'github.com' },
|
||||
number: 6934,
|
||||
type: 'pr'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves route intent for URLs with uppercase schemes', () => {
|
||||
expect(normalizeGitHubLinkQuery('HTTPS://github.com/stablyai/orca/pull/6934')).toEqual({
|
||||
query: 'HTTPS://github.com/stablyai/orca/pull/6934',
|
||||
directNumber: 6934,
|
||||
directLink: {
|
||||
slug: { owner: 'stablyai', repo: 'orca', host: 'github.com' },
|
||||
number: 6934,
|
||||
type: 'pr'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects oversized pasted link queries without echoing their content', () => {
|
||||
const secret = 'github-link-secret'
|
||||
const result = normalizeGitHubLinkQuery(secret + 'x'.repeat(WORK_ITEM_LINK_QUERY_MAX_BYTES))
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
|
||||
import type * as RuntimeRpcClient from '@/runtime/runtime-rpc-client'
|
||||
import { resolveGitHubPrStartPointForRepo } from './github-pr-start-point'
|
||||
|
||||
vi.mock('@/runtime/runtime-rpc-client', async () => {
|
||||
const actual = await vi.importActual<typeof RuntimeRpcClient>('@/runtime/runtime-rpc-client')
|
||||
return {
|
||||
...actual,
|
||||
callRuntimeRpc: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
describe('resolveGitHubPrStartPointForRepo', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(callRuntimeRpc).mockReset()
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
worktrees: {
|
||||
resolvePrBase: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('throws resolver errors so submit can abort before creating from the wrong base', async () => {
|
||||
vi.mocked(window.api.worktrees.resolvePrBase).mockResolvedValue({
|
||||
error: 'Could not resolve PR head.'
|
||||
})
|
||||
|
||||
await expect(
|
||||
resolveGitHubPrStartPointForRepo({
|
||||
repoId: 'repo-1',
|
||||
prNumber: 99,
|
||||
target: { kind: 'local' }
|
||||
})
|
||||
).rejects.toThrow('Could not resolve PR head.')
|
||||
})
|
||||
|
||||
it('marks creating from a PR as a user act, locally and on a paired host (E9)', async () => {
|
||||
const startPoint = { baseBranch: 'origin/main' }
|
||||
vi.mocked(window.api.worktrees.resolvePrBase).mockResolvedValue(startPoint)
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue(startPoint)
|
||||
|
||||
await resolveGitHubPrStartPointForRepo({
|
||||
repoId: 'repo-1',
|
||||
prNumber: 7,
|
||||
target: { kind: 'local' }
|
||||
})
|
||||
await resolveGitHubPrStartPointForRepo({
|
||||
repoId: 'repo-1',
|
||||
prNumber: 7,
|
||||
target: { kind: 'environment', environmentId: 'env-1' }
|
||||
})
|
||||
|
||||
expect(window.api.worktrees.resolvePrBase).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ wakeWslDistro: true })
|
||||
)
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'worktree.resolvePrBase',
|
||||
expect.objectContaining({ wakeWslDistro: true }),
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -37,13 +37,6 @@ describe('getGitHubRepoRoutingTarget', () => {
|
||||
).toEqual({ kind: 'environment', environmentId: 'owner-runtime' })
|
||||
})
|
||||
|
||||
it('routes a runtime-owned repo to its owner runtime when there is no source context', () => {
|
||||
expect(resolveTarget(runtimeOwnedRepo, 'repo-1', null)).toEqual({
|
||||
kind: 'environment',
|
||||
environmentId: 'owner-runtime'
|
||||
})
|
||||
})
|
||||
|
||||
it('lets a runtime source override the repo owner when both name runtimes', () => {
|
||||
expect(
|
||||
resolveTarget(runtimeOwnedRepo, 'repo-1', {
|
||||
@@ -122,16 +115,6 @@ describe('getGitHubRepoRoutingTarget', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores runtime hosts on non-GitHub sources', () => {
|
||||
expect(
|
||||
resolveTarget(runtimeOwnedRepo, 'repo-1', {
|
||||
...runtimeSourceContext,
|
||||
provider: 'gitlab',
|
||||
hostId: 'runtime:source-runtime'
|
||||
})
|
||||
).toEqual({ kind: 'environment', environmentId: 'owner-runtime' })
|
||||
})
|
||||
|
||||
it('never routes a repo without an explicit owner to the globally focused runtime', () => {
|
||||
const noOwnerRepo: RepoRuntimeOwnerState = {
|
||||
settings: { activeRuntimeEnvironmentId: 'focused-runtime' },
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
|
||||
import type * as RuntimeRpcClient from '@/runtime/runtime-rpc-client'
|
||||
import {
|
||||
lookupGitHubWorkItemDetailsForSource,
|
||||
lookupGitHubWorkItemByOwnerRepoForSource
|
||||
} from './github-work-item-source-lookup'
|
||||
import type { TaskSourceContext } from '../../../shared/task-source-context'
|
||||
|
||||
vi.mock('@/runtime/runtime-rpc-client', async () => {
|
||||
const actual = await vi.importActual<typeof RuntimeRpcClient>('@/runtime/runtime-rpc-client')
|
||||
return {
|
||||
...actual,
|
||||
callRuntimeRpc: vi.fn(),
|
||||
runtimeEnvironmentSupportsCapability: vi.fn().mockResolvedValue(false)
|
||||
}
|
||||
})
|
||||
|
||||
const runtimeSourceContext: TaskSourceContext = {
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'project-1',
|
||||
hostId: 'runtime:env-1',
|
||||
repoId: 'runtime-repo'
|
||||
}
|
||||
|
||||
describe('GitHub source lookup routing', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(callRuntimeRpc).mockReset()
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
gh: {
|
||||
workItemDetails: vi.fn(),
|
||||
workItemByOwnerRepo: vi.fn().mockResolvedValue(null)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['local', 'ssh:build-box'] as const)(
|
||||
'forwards %s source identity to exact repository IPC',
|
||||
async (hostId) => {
|
||||
const sourceContext: TaskSourceContext = { ...runtimeSourceContext, hostId, repoId: 'repo' }
|
||||
await lookupGitHubWorkItemByOwnerRepoForSource({
|
||||
repoPath: '/repo',
|
||||
repoId: 'repo',
|
||||
sourceContext,
|
||||
owner: 'acme',
|
||||
repo: 'orca',
|
||||
host: 'github.com',
|
||||
number: 42,
|
||||
type: 'pr'
|
||||
})
|
||||
expect(window.api.gh.workItemByOwnerRepo).toHaveBeenCalledWith({
|
||||
repoPath: '/repo',
|
||||
repoId: 'repo',
|
||||
sourceContext,
|
||||
owner: 'acme',
|
||||
repo: 'orca',
|
||||
host: 'github.com',
|
||||
number: 42,
|
||||
type: 'pr'
|
||||
})
|
||||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('routes exact repository lookup to its source runtime instead of local IPC', async () => {
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue(null)
|
||||
await lookupGitHubWorkItemByOwnerRepoForSource({
|
||||
repoPath: '/repo',
|
||||
repoId: 'renderer-repo',
|
||||
sourceContext: runtimeSourceContext,
|
||||
owner: 'acme',
|
||||
repo: 'orca',
|
||||
host: 'github.com',
|
||||
number: 42,
|
||||
type: 'pr'
|
||||
})
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'env-1' },
|
||||
'github.workItemByOwnerRepo',
|
||||
{
|
||||
repo: 'runtime-repo',
|
||||
owner: 'acme',
|
||||
ownerRepo: 'orca',
|
||||
host: 'github.com',
|
||||
number: 42,
|
||||
type: 'pr'
|
||||
},
|
||||
expect.objectContaining({ timeoutMs: 30_000 })
|
||||
)
|
||||
expect(window.api.gh.workItemByOwnerRepo).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes runtime-owned GitHub details through runtime RPC', async () => {
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue(null)
|
||||
|
||||
await expect(
|
||||
lookupGitHubWorkItemDetailsForSource({
|
||||
repoPath: '/home/runtime/app',
|
||||
repoId: 'renderer-repo',
|
||||
sourceContext: runtimeSourceContext,
|
||||
number: 42,
|
||||
type: 'issue',
|
||||
ownerRepo: { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'env-1' },
|
||||
'github.workItemDetails',
|
||||
{ repo: 'runtime-repo', number: 42, type: 'issue' },
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
expect(window.api.gh.workItemDetails).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses the renderer repo id for runtime details when the source has no repo id', async () => {
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue(null)
|
||||
|
||||
await lookupGitHubWorkItemDetailsForSource({
|
||||
repoPath: '/home/runtime/app',
|
||||
repoId: 'renderer-repo',
|
||||
sourceContext: { ...runtimeSourceContext, repoId: null },
|
||||
number: 42,
|
||||
type: 'pr'
|
||||
})
|
||||
|
||||
expect(callRuntimeRpc).toHaveBeenCalledWith(
|
||||
{ kind: 'environment', environmentId: 'env-1' },
|
||||
'github.workItemDetails',
|
||||
{ repo: 'renderer-repo', number: 42, type: 'pr' },
|
||||
{ timeoutMs: 30_000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps local GitHub details on Electron IPC with source context', async () => {
|
||||
vi.mocked(window.api.gh.workItemDetails).mockResolvedValue(null)
|
||||
const sourceContext: TaskSourceContext = {
|
||||
...runtimeSourceContext,
|
||||
hostId: 'local',
|
||||
repoId: 'local-repo'
|
||||
}
|
||||
|
||||
await expect(
|
||||
lookupGitHubWorkItemDetailsForSource({
|
||||
repoPath: 'C:\\workspace\\app',
|
||||
repoId: 'local-repo',
|
||||
sourceContext,
|
||||
number: 42,
|
||||
type: 'issue'
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
|
||||
expect(window.api.gh.workItemDetails).toHaveBeenCalledWith({
|
||||
repoPath: 'C:\\workspace\\app',
|
||||
repoId: 'local-repo',
|
||||
sourceContext,
|
||||
number: 42,
|
||||
type: 'issue'
|
||||
})
|
||||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends a local issue row repository to the repo-scoped details route', async () => {
|
||||
const ownerRepo = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
|
||||
vi.mocked(window.api.gh.workItemDetails).mockResolvedValue(null)
|
||||
|
||||
await lookupGitHubWorkItemDetailsForSource({
|
||||
repoPath: '/home/fixture/widgets',
|
||||
repoId: 'local-repo',
|
||||
number: 12,
|
||||
type: 'issue',
|
||||
ownerRepo
|
||||
})
|
||||
|
||||
expect(window.api.gh.workItemDetails).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ repoPath: '/home/fixture/widgets', ownerRepo })
|
||||
)
|
||||
expect(callRuntimeRpc).not.toHaveBeenCalled()
|
||||
})
|
||||
it('keeps the old runtime details path and refuses only unsupported canonical refresh', async () => {
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue(null)
|
||||
await expect(
|
||||
lookupGitHubWorkItemDetailsForSource({
|
||||
repoPath: '/remote',
|
||||
repoId: 'renderer',
|
||||
sourceContext: runtimeSourceContext,
|
||||
number: 42,
|
||||
type: 'issue'
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
expect(callRuntimeRpc).toHaveBeenCalledTimes(1)
|
||||
await expect(
|
||||
lookupGitHubWorkItemDetailsForSource({
|
||||
repoPath: '/remote',
|
||||
repoId: 'renderer',
|
||||
sourceContext: runtimeSourceContext,
|
||||
number: 42,
|
||||
type: 'issue',
|
||||
canonical: true,
|
||||
ownerRepo: { owner: 'acme', repo: 'widgets' }
|
||||
})
|
||||
).resolves.toBeNull()
|
||||
expect(callRuntimeRpc).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,107 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
findGithubIssueWorkspaceAttachment,
|
||||
findGithubPrWorkspaceAttachment
|
||||
} from './github-work-item-workspace-attachment'
|
||||
import type { Worktree } from '../../../shared/worktree/types'
|
||||
|
||||
function worktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
return {
|
||||
id: overrides.id ?? 'wt-1',
|
||||
repoId: overrides.repoId ?? 'repo-1',
|
||||
path: overrides.path ?? '/tmp/repo-1/wt-1',
|
||||
head: 'abc123',
|
||||
branch: overrides.branch ?? 'refs/heads/feature/workspace-attachment',
|
||||
isBare: false,
|
||||
isMainWorktree: false,
|
||||
displayName: overrides.displayName ?? 'GitHub workspace',
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('GitHub work-item workspace attachment', () => {
|
||||
it('finds the first non-archived workspace linked to the repo PR', () => {
|
||||
const first = worktree({ id: 'first', linkedPR: 42 })
|
||||
const second = worktree({ id: 'second', linkedPR: 42 })
|
||||
|
||||
expect(findGithubPrWorkspaceAttachment([first, second], 'repo-1', 42)).toBe(first)
|
||||
})
|
||||
|
||||
it('does not match workspaces from a different repo', () => {
|
||||
const attachedElsewhere = worktree({ repoId: 'repo-2', linkedIssue: 42, linkedPR: 42 })
|
||||
|
||||
expect(findGithubPrWorkspaceAttachment([attachedElsewhere], 'repo-1', 42)).toBeNull()
|
||||
expect(findGithubIssueWorkspaceAttachment([attachedElsewhere], 'repo-1', 42)).toBeNull()
|
||||
})
|
||||
|
||||
it('does not match archived workspaces', () => {
|
||||
const archived = worktree({ linkedIssue: 42, linkedPR: 42, isArchived: true })
|
||||
|
||||
expect(findGithubPrWorkspaceAttachment([archived], 'repo-1', 42)).toBeNull()
|
||||
expect(findGithubIssueWorkspaceAttachment([archived], 'repo-1', 42)).toBeNull()
|
||||
})
|
||||
|
||||
it('does not cross-match PR and issue slots with the same number', () => {
|
||||
const prOnly = worktree({ id: 'pr', linkedPR: 42 })
|
||||
const issueOnly = worktree({ id: 'issue', linkedIssue: 42 })
|
||||
|
||||
expect(findGithubIssueWorkspaceAttachment([prOnly], 'repo-1', 42)).toBeNull()
|
||||
expect(findGithubPrWorkspaceAttachment([issueOnly], 'repo-1', 42)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiple GitHub attachments', () => {
|
||||
it('finds secondary reviews and issues', () => {
|
||||
const attached = worktree({
|
||||
linkedPR: 1,
|
||||
linkedIssue: 2,
|
||||
linkedItems: [
|
||||
{ provider: 'github', type: 'pr', number: 3 },
|
||||
{ provider: 'github', type: 'issue', number: 4 }
|
||||
]
|
||||
})
|
||||
expect(findGithubPrWorkspaceAttachment([attached], attached.repoId, 3)).toBe(attached)
|
||||
expect(findGithubIssueWorkspaceAttachment([attached], attached.repoId, 4)).toBe(attached)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GitHub attachment repository scope', () => {
|
||||
it('matches a foreign task by URL across workspace repositories', () => {
|
||||
const attached = worktree({
|
||||
linkedItems: [
|
||||
{
|
||||
provider: 'github',
|
||||
type: 'issue',
|
||||
number: 42,
|
||||
url: 'https://github.com/foreign/project/issues/42'
|
||||
}
|
||||
]
|
||||
})
|
||||
expect(
|
||||
findGithubIssueWorkspaceAttachment(
|
||||
[attached],
|
||||
'foreign-repo',
|
||||
42,
|
||||
'https://github.com/foreign/project/issues/42'
|
||||
)
|
||||
).toBe(attached)
|
||||
expect(
|
||||
findGithubIssueWorkspaceAttachment(
|
||||
[attached],
|
||||
'repo-1',
|
||||
42,
|
||||
'https://github.com/current/project/issues/42'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,54 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { normalizeGitLabLinkQuery, parseGitLabIssueOrMRLink } from './gitlab-links'
|
||||
import { normalizeGitLabLinkQuery } from './gitlab-links'
|
||||
import { WORK_ITEM_LINK_QUERY_MAX_BYTES } from './work-item-link-query-bounds'
|
||||
|
||||
describe('parseGitLabIssueOrMRLink', () => {
|
||||
it('preserves explicit ports on self-hosted GitLab hosts', () => {
|
||||
expect(parseGitLabIssueOrMRLink('https://gitlab.example.com:8443/team/api/-/issues/7')).toEqual(
|
||||
{
|
||||
slug: { host: 'gitlab.example.com:8443', path: 'team/api' },
|
||||
number: 7,
|
||||
type: 'issue'
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
it('treats /-/work_items/<iid> as an issue work item', () => {
|
||||
expect(parseGitLabIssueOrMRLink('https://gitlab.com/stablyai/orca/-/work_items/923')).toEqual({
|
||||
slug: { host: 'gitlab.com', path: 'stablyai/orca' },
|
||||
number: 923,
|
||||
type: 'issue'
|
||||
})
|
||||
expect(
|
||||
parseGitLabIssueOrMRLink('https://gitlab.example.com:8443/team/api/-/work_items/7')
|
||||
).toEqual({
|
||||
slug: { host: 'gitlab.example.com:8443', path: 'team/api' },
|
||||
number: 7,
|
||||
type: 'issue'
|
||||
})
|
||||
})
|
||||
|
||||
it('extracts slug, number, and type from URLs with trailing page segments', () => {
|
||||
expect(parseGitLabIssueOrMRLink('https://gitlab.com/g/p/-/merge_requests/77/diffs')).toEqual({
|
||||
slug: { host: 'gitlab.com', path: 'g/p' },
|
||||
number: 77,
|
||||
type: 'mr'
|
||||
})
|
||||
expect(
|
||||
parseGitLabIssueOrMRLink('https://gitlab.com/g/p/-/merge_requests/77/diffs.diff?diff_id=1')
|
||||
).toEqual({
|
||||
slug: { host: 'gitlab.com', path: 'g/p' },
|
||||
number: 77,
|
||||
type: 'mr'
|
||||
})
|
||||
expect(parseGitLabIssueOrMRLink('https://gitlab.com/g/p/-/issues/923/designs')).toEqual({
|
||||
slug: { host: 'gitlab.com', path: 'g/p' },
|
||||
number: 923,
|
||||
type: 'issue'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeGitLabLinkQuery', () => {
|
||||
it('rejects oversized pasted link queries without echoing their content', () => {
|
||||
const secret = 'gitlab-link-secret'
|
||||
|
||||
@@ -81,31 +81,4 @@ describe('handle-gap verdict, landed-handle eviction', () => {
|
||||
vi.advanceTimersByTime(HOST_MIRROR_HANDLE_GAP_DEADLINE_MS + 1)
|
||||
expect(replay).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('a wait re-parked under a new worktree is released by that worktree, not the old one', () => {
|
||||
// Adopting an orphaned terminal re-keys `tabsByWorktree` without re-keying the record, so the
|
||||
// re-park hands the live wait a new worktree. Retraction evidence about the OLD one says
|
||||
// nothing about the wait that is actually running.
|
||||
useAppStore.setState({
|
||||
tabsByWorktree: {
|
||||
'wt-old': [makeTerminalTab({ id: TAB_ID, worktreeId: 'wt-old', title: 't' })],
|
||||
'wt-new': [makeTerminalTab({ id: TAB_ID, worktreeId: 'wt-new', title: 't' })]
|
||||
},
|
||||
ptyIdsByTabId: {}
|
||||
})
|
||||
parkUntilHostMirrorHandleLands(ENV_ID, 'wt-old', TAB_ID, vi.fn())
|
||||
const replayAfterAdoption = vi.fn()
|
||||
parkUntilHostMirrorHandleLands(ENV_ID, 'wt-new', TAB_ID, replayAfterAdoption)
|
||||
|
||||
useAppStore.setState({
|
||||
tabsByWorktree: {
|
||||
'wt-new': [makeTerminalTab({ id: TAB_ID, worktreeId: 'wt-new', title: 't' })]
|
||||
}
|
||||
})
|
||||
expect(replayAfterAdoption).not.toHaveBeenCalled()
|
||||
expect(countParkedHostMirrorHandleGapPanesForTests()).toBe(1)
|
||||
|
||||
useAppStore.setState({ tabsByWorktree: {} })
|
||||
expect(replayAfterAdoption).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -410,24 +410,6 @@ describe('resume across the mirror handle gap', () => {
|
||||
expect(Object.keys(useAppStore.getState().automaticAgentResumeClaimsByTabId)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('re-arms the wait after a reconnect instead of inheriting the expired verdict', () => {
|
||||
const worktree = makeRuntimeOwnedWorktree()
|
||||
seedMirroredWorkspace(worktree)
|
||||
seedActiveSleepingRecord(worktree.id)
|
||||
markHostSessionMirrorHydrated(RUNTIME_ENV_ID)
|
||||
expect(resumeSleepingAgentSessionsForWorktree(worktree.id)).toBe(0)
|
||||
vi.advanceTimersByTime(HOST_MIRROR_HANDLE_GAP_DEADLINE_MS)
|
||||
expect(Object.keys(useAppStore.getState().automaticAgentResumeClaimsByTabId)).toHaveLength(0)
|
||||
|
||||
seedMirroredWorkspace(worktree)
|
||||
const paneKey = seedActiveSleepingRecord(worktree.id)
|
||||
setRuntimeEnvironmentConnectionGenerationForTests(RUNTIME_ENV_ID, 1)
|
||||
markHostSessionMirrorHydrated(RUNTIME_ENV_ID)
|
||||
|
||||
expect(resumeSleepingAgentSessionsForWorktree(worktree.id)).toBe(0)
|
||||
expect(useAppStore.getState().sleepingAgentSessionsByPaneKey[paneKey]).toBeDefined()
|
||||
})
|
||||
|
||||
// Reconnect during an armed wait must invalidate its pending deadline, independently of already-recorded verdicts.
|
||||
it('does not let a wait armed on the previous connection decide the new one', () => {
|
||||
const worktree = makeRuntimeOwnedWorktree()
|
||||
|
||||
Reference in New Issue
Block a user