mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove 48 low-value presentation cases and four suites (#34386)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -8713,7 +8713,6 @@
|
||||
"src/renderer/src/components/settings/PluginsSettingsSection.lifecycle.test.tsx": 889,
|
||||
"src/renderer/src/components/settings/ProjectWindowsRuntimeSetting.test.tsx": 729,
|
||||
"src/renderer/src/components/settings/QuickCommandsList.test.tsx": 617,
|
||||
"src/renderer/src/components/settings/QuickCommandsPane.test.ts": 604,
|
||||
"src/renderer/src/components/settings/ReleaseBuildPicker.test.tsx": 714,
|
||||
"src/renderer/src/components/settings/ReleaseChannelSection.test.tsx": 2427,
|
||||
"src/renderer/src/components/settings/RemoteServerUpdateDialog.test.tsx": 333,
|
||||
@@ -8735,7 +8734,6 @@
|
||||
"src/renderer/src/components/settings/SettingsSidebar.test.tsx": 2169,
|
||||
"src/renderer/src/components/settings/ShareSkillsSettingsPane.test.tsx": 601,
|
||||
"src/renderer/src/components/settings/ShortcutFilterRail.test.ts": 157,
|
||||
"src/renderer/src/components/settings/SourceControlActionRepoOverrideNote.test.tsx": 415,
|
||||
"src/renderer/src/components/settings/SparsePresetSettingsSection.test.tsx": 785,
|
||||
"src/renderer/src/components/settings/SshHostDeltaMoveDialog.test.tsx": 467,
|
||||
"src/renderer/src/components/settings/SshHostMoveAnywayAction.test.tsx": 509,
|
||||
@@ -8822,7 +8820,6 @@
|
||||
"src/renderer/src/components/settings/settings-project-list.test.ts": 71,
|
||||
"src/renderer/src/components/settings/settings-project-regrouping.test.ts": 467,
|
||||
"src/renderer/src/components/settings/settings-search.test.ts": 49,
|
||||
"src/renderer/src/components/settings/settings-setup-guide-progress.test.ts": 408,
|
||||
"src/renderer/src/components/settings/shortcut-groups.test.ts": 152,
|
||||
"src/renderer/src/components/settings/shortcut-row-visibility.test.ts": 186,
|
||||
"src/renderer/src/components/settings/skill-setup-blocked-plan.test.tsx": 5751,
|
||||
@@ -11478,7 +11475,6 @@
|
||||
"src/renderer/src/ssh/ssh-connect-ui-timeout.test.ts": 31,
|
||||
"src/renderer/src/ssh/ssh-connect-verb.test.ts": 44,
|
||||
"src/renderer/src/ssh/ssh-host-node-download-copy.test.ts": 1426,
|
||||
"src/renderer/src/ssh/ssh-host-server-status-copy.test.ts": 1157,
|
||||
"src/renderer/src/startup/active-workspace-ssh-targets.test.ts": 58,
|
||||
"src/renderer/src/startup/ssh-startup-reconnect.test.ts": 29,
|
||||
"src/renderer/src/startup/startup-degraded-recovery.test.ts": 427,
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
import { describe, expect, it, afterEach, vi } from 'vitest'
|
||||
import {
|
||||
getAvailableQuickCommandHostId,
|
||||
isQuickCommandEditorHostCurrent,
|
||||
shouldOpenQuickCommandAddIntent,
|
||||
shouldShowQuickCommandsRefreshError
|
||||
} from './QuickCommandsPane'
|
||||
|
||||
describe('QuickCommandsPane add-command intent', () => {
|
||||
it('opens the add flow once for each new intent signal', () => {
|
||||
expect(shouldOpenQuickCommandAddIntent(undefined, 0)).toBe(false)
|
||||
expect(shouldOpenQuickCommandAddIntent(0, 0)).toBe(false)
|
||||
expect(shouldOpenQuickCommandAddIntent(1, 0)).toBe(true)
|
||||
expect(shouldOpenQuickCommandAddIntent(1, 1)).toBe(false)
|
||||
expect(shouldOpenQuickCommandAddIntent(2, 1)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('QuickCommandsPane host state', () => {
|
||||
it('falls back to the local host when the selected remote host disappears', () => {
|
||||
expect(
|
||||
getAvailableQuickCommandHostId('runtime:removed', [
|
||||
{ id: 'local' },
|
||||
{ id: 'runtime:available' }
|
||||
])
|
||||
).toBe('local')
|
||||
})
|
||||
|
||||
it('keeps the selected host while it remains available', () => {
|
||||
expect(
|
||||
getAvailableQuickCommandHostId('runtime:available', [
|
||||
{ id: 'local' },
|
||||
{ id: 'runtime:available' }
|
||||
])
|
||||
).toBe('runtime:available')
|
||||
})
|
||||
|
||||
it('retires a remote editor when the same host reconnects with a new generation', () => {
|
||||
expect(
|
||||
isQuickCommandEditorHostCurrent(
|
||||
'runtime:build',
|
||||
3,
|
||||
[{ id: 'local' }, { id: 'runtime:build' }],
|
||||
new Map([['build', { connectionGeneration: 4 }]])
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('surfaces refresh failures only when cached commands remain usable', () => {
|
||||
expect(shouldShowQuickCommandsRefreshError(true, { error: 'offline', ready: true })).toBe(true)
|
||||
expect(shouldShowQuickCommandsRefreshError(true, { error: 'offline', ready: false })).toBe(
|
||||
false
|
||||
)
|
||||
expect(shouldShowQuickCommandsRefreshError(false, { error: 'offline', ready: true })).toBe(
|
||||
false
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const { rejectAppStoreAccess } = vi.hoisted(() => ({
|
||||
rejectAppStoreAccess: vi.fn(() => {
|
||||
throw new Error('Pure helper tests must not access the app store')
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(rejectAppStoreAccess, {
|
||||
getState: rejectAppStoreAccess,
|
||||
getInitialState: rejectAppStoreAccess,
|
||||
setState: rejectAppStoreAccess,
|
||||
subscribe: rejectAppStoreAccess
|
||||
})
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
expect(rejectAppStoreAccess).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,105 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TooltipProvider } from '../ui/tooltip'
|
||||
import { SourceControlActionRepoOverrideNote } from './SourceControlActionRepoOverrideNote'
|
||||
import type { SourceControlActionRecipeOverrideSummary } from '../../../../shared/source-control-launch-agent-selection'
|
||||
|
||||
let container: HTMLDivElement
|
||||
let root: Root
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
root = createRoot(container)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount())
|
||||
container.remove()
|
||||
})
|
||||
|
||||
function renderNote(
|
||||
summary: SourceControlActionRecipeOverrideSummary,
|
||||
onReviewRepo = vi.fn()
|
||||
): void {
|
||||
act(() => {
|
||||
root.render(
|
||||
<TooltipProvider>
|
||||
<SourceControlActionRepoOverrideNote summary={summary} onReviewRepo={onReviewRepo} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
describe('SourceControlActionRepoOverrideNote', () => {
|
||||
it('renders nothing when no repo overrides the action', () => {
|
||||
renderNote({ count: 0, overrides: [] })
|
||||
expect(container.textContent).toBe('')
|
||||
})
|
||||
|
||||
it('shows the singular count when one repo overrides the action', () => {
|
||||
renderNote({
|
||||
count: 1,
|
||||
overrides: [{ repoId: 'r1', repoName: 'App', fields: ['commandTemplate'] }]
|
||||
})
|
||||
expect(container.textContent).toContain("Global saves won't change 1 repository")
|
||||
expect(container.textContent).toContain('Review')
|
||||
})
|
||||
|
||||
it('shows the plural count when several repos override the action', () => {
|
||||
renderNote({
|
||||
count: 2,
|
||||
overrides: [
|
||||
{ repoId: 'r1', repoName: 'App', fields: ['agent'] },
|
||||
{ repoId: 'r2', repoName: 'Web', fields: ['agentArgs'] }
|
||||
]
|
||||
})
|
||||
expect(container.textContent).toContain("Global saves won't change 2 repositories")
|
||||
expect(container.textContent).toContain('Review first')
|
||||
})
|
||||
|
||||
it('shows a "+N more" note when overrides exceed the visible limit', async () => {
|
||||
renderNote({
|
||||
count: 6,
|
||||
overrides: Array.from({ length: 6 }, (_, index) => ({
|
||||
repoId: `r${index}`,
|
||||
repoName: `Repo ${index}`,
|
||||
fields: ['commandTemplate']
|
||||
}))
|
||||
})
|
||||
|
||||
// Open Radix’s pointer-move tooltip and flush its timer before inspecting the portal-only overflow note.
|
||||
const trigger = container.querySelector('[data-slot="tooltip-trigger"]')
|
||||
await act(async () => {
|
||||
trigger?.dispatchEvent(
|
||||
new PointerEvent('pointermove', { bubbles: true, pointerType: 'mouse' })
|
||||
)
|
||||
await new Promise((resolve) => setTimeout(resolve, 0))
|
||||
})
|
||||
|
||||
expect(document.body.textContent).toContain('+1 more')
|
||||
})
|
||||
|
||||
it('opens the first repo override from the review action', () => {
|
||||
const onReviewRepo = vi.fn()
|
||||
renderNote(
|
||||
{
|
||||
count: 2,
|
||||
overrides: [
|
||||
{ repoId: 'r1', repoName: 'App', fields: ['agent'] },
|
||||
{ repoId: 'r2', repoName: 'Web', fields: ['agentArgs'] }
|
||||
]
|
||||
},
|
||||
onReviewRepo
|
||||
)
|
||||
|
||||
act(() => {
|
||||
container.querySelector('button')?.click()
|
||||
})
|
||||
|
||||
expect(onReviewRepo).toHaveBeenCalledWith('r1')
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { TaskSourceProviderCard } from './TaskSourceProviderCard'
|
||||
|
||||
@@ -29,51 +28,6 @@ afterEach(async () => {
|
||||
})
|
||||
|
||||
describe('TaskSourceProviderCard', () => {
|
||||
it('shows the visibility action in only one place when expanded', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<TaskSourceProviderCard
|
||||
icon={<span />}
|
||||
name="Linear"
|
||||
description="Linear setup"
|
||||
readiness={readiness}
|
||||
visible
|
||||
canHide
|
||||
defaultExpanded
|
||||
onToggleVisible={vi.fn()}
|
||||
>
|
||||
<button aria-label="Hide Linear from Tasks">Shown</button>
|
||||
</TaskSourceProviderCard>
|
||||
)
|
||||
|
||||
expect(markup.match(/aria-label="Hide Linear from Tasks"/g)).toHaveLength(1)
|
||||
expect(markup).toContain('aria-label="Collapse Linear setup steps"')
|
||||
expect(markup).toContain('aria-expanded="true"')
|
||||
})
|
||||
|
||||
it('keeps visibility available while setup steps are collapsed', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<TaskSourceProviderCard
|
||||
icon={<span />}
|
||||
name="Linear"
|
||||
description="Linear setup"
|
||||
readiness={readiness}
|
||||
visible
|
||||
canHide
|
||||
defaultExpanded={false}
|
||||
onToggleVisible={vi.fn()}
|
||||
>
|
||||
<span>Expanded content</span>
|
||||
</TaskSourceProviderCard>
|
||||
)
|
||||
|
||||
expect(markup).toContain('aria-label="Hide Linear from Tasks"')
|
||||
expect(markup).not.toContain('aria-pressed')
|
||||
expect(markup).toContain('>Hide</button>')
|
||||
expect(markup).not.toContain('>Shown</button>')
|
||||
expect(markup).toContain('aria-label="Show Linear setup steps"')
|
||||
expect(markup).not.toContain('Expanded content')
|
||||
})
|
||||
|
||||
it('stays expanded when the auto-expand target moves to another provider', async () => {
|
||||
container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
@@ -105,47 +59,4 @@ describe('TaskSourceProviderCard', () => {
|
||||
|
||||
expect(container.textContent).toContain('Install terminal')
|
||||
})
|
||||
|
||||
it('labels the locked final provider as shown instead of offering a hide action', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<TaskSourceProviderCard
|
||||
icon={<span />}
|
||||
name="Linear"
|
||||
description="Linear setup"
|
||||
readiness={readiness}
|
||||
visible
|
||||
canHide={false}
|
||||
defaultExpanded={false}
|
||||
onToggleVisible={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
// aria-disabled, not disabled: the explanation must stay keyboard reachable.
|
||||
expect(markup).toContain('aria-disabled="true"')
|
||||
expect(markup).not.toContain('disabled=""')
|
||||
expect(markup).toContain(
|
||||
'aria-label="Linear is shown in Tasks. At least one provider must stay visible."'
|
||||
)
|
||||
expect(markup).toContain('>Shown</button>')
|
||||
expect(markup).not.toContain('>Hide</button>')
|
||||
})
|
||||
|
||||
it('labels an unverifiable skill scan as unknown while keeping the confirmed count', () => {
|
||||
const markup = renderToStaticMarkup(
|
||||
<TaskSourceProviderCard
|
||||
icon={<span />}
|
||||
name="Linear"
|
||||
description="Linear setup"
|
||||
readiness={{ ...readiness, connected: true, skillUnverifiable: true }}
|
||||
visible
|
||||
canHide
|
||||
defaultExpanded={false}
|
||||
onToggleVisible={vi.fn()}
|
||||
/>
|
||||
)
|
||||
|
||||
expect(markup).toContain('Cannot verify')
|
||||
expect(markup).toContain('2/3')
|
||||
expect(markup).not.toContain('Skill required')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
MANAGE_SESSIONS_SECTION_ID,
|
||||
TerminalTccAttributionNotice
|
||||
} from './TerminalTccAttributionNotice'
|
||||
import { TerminalTccAttributionNotice } from './TerminalTccAttributionNotice'
|
||||
|
||||
const openSettingsTarget = vi.fn()
|
||||
const openSettingsPage = vi.fn()
|
||||
@@ -48,52 +45,6 @@ afterEach(async () => {
|
||||
Reflect.deleteProperty(window, 'api')
|
||||
})
|
||||
|
||||
it('renders the remedy banner only while attribution is severed', async () => {
|
||||
stubAttributionHealth('severed')
|
||||
await act(async () => {
|
||||
root.render(<TerminalTccAttributionNotice />)
|
||||
})
|
||||
const alert = container.querySelector('[role="alert"]')
|
||||
expect(alert?.textContent).toContain('macOS permission grants aren’t reaching terminals')
|
||||
expect(alert?.textContent).toContain('-25211')
|
||||
|
||||
stubAttributionHealth('intact')
|
||||
await act(async () => {
|
||||
root.render(<TerminalTccAttributionNotice key="fresh" />)
|
||||
})
|
||||
expect(container.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('navigates to Manage Sessions from the banner action', async () => {
|
||||
stubAttributionHealth('severed')
|
||||
await act(async () => {
|
||||
root.render(<TerminalTccAttributionNotice />)
|
||||
})
|
||||
|
||||
const button = container.querySelector('button')
|
||||
expect(button?.textContent).toContain('Open Manage Sessions')
|
||||
await act(async () => {
|
||||
button?.click()
|
||||
})
|
||||
|
||||
expect(setSettingsSearchQuery).toHaveBeenCalledWith('')
|
||||
expect(openSettingsTarget).toHaveBeenCalledWith({
|
||||
pane: 'terminal',
|
||||
repoId: null,
|
||||
sectionId: MANAGE_SESSIONS_SECTION_ID
|
||||
})
|
||||
expect(openSettingsPage).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('hides the navigation button on the Manage Sessions surface itself', async () => {
|
||||
stubAttributionHealth('severed')
|
||||
await act(async () => {
|
||||
root.render(<TerminalTccAttributionNotice showManageSessionsButton={false} />)
|
||||
})
|
||||
expect(container.querySelector('[role="alert"]')).not.toBeNull()
|
||||
expect(container.querySelector('button')).toBeNull()
|
||||
})
|
||||
|
||||
it('refreshes the warning after the daemon restart remedy settles', async () => {
|
||||
stubAttributionHealth('severed')
|
||||
await act(async () => {
|
||||
@@ -107,11 +58,3 @@ it('refreshes the warning after the daemon restart remedy settles', async () =>
|
||||
})
|
||||
expect(container.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
it('fails closed when the attribution probe is unavailable', async () => {
|
||||
Object.assign(window, { api: { pty: {} } })
|
||||
await act(async () => {
|
||||
root.render(<TerminalTccAttributionNotice />)
|
||||
})
|
||||
expect(container.querySelector('[role="alert"]')).toBeNull()
|
||||
})
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { describe, expect, it, afterEach, vi } from 'vitest'
|
||||
import {
|
||||
FEATURE_WALL_SETUP_STEPS,
|
||||
type FeatureWallSetupStepId
|
||||
} from '../../../../shared/feature-wall-setup-steps'
|
||||
import { getSettingsSetupGuideProgress } from './settings-setup-guide-progress'
|
||||
|
||||
describe('settings setup guide progress', () => {
|
||||
function makePreBrowserDoneStepState(): Partial<Record<FeatureWallSetupStepId, boolean>> {
|
||||
return Object.fromEntries(
|
||||
FEATURE_WALL_SETUP_STEPS.map((step) => [step.id, step.id !== 'browser'])
|
||||
) as Partial<Record<FeatureWallSetupStepId, boolean>>
|
||||
}
|
||||
|
||||
it('tracks the full setup checklist total', () => {
|
||||
const progress = getSettingsSetupGuideProgress({
|
||||
ready: true,
|
||||
stepDone: {}
|
||||
})
|
||||
|
||||
expect(progress).toEqual({
|
||||
ready: true,
|
||||
doneCount: 0,
|
||||
total: FEATURE_WALL_SETUP_STEPS.length,
|
||||
firstIncompleteStepId: 'notifications'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not mark Settings complete when only the old setup subset is done', () => {
|
||||
const stepDone = {
|
||||
'two-worktrees': true,
|
||||
notifications: true,
|
||||
'default-agent': true,
|
||||
'task-sources': true
|
||||
} satisfies Partial<Record<FeatureWallSetupStepId, boolean>>
|
||||
|
||||
expect(getSettingsSetupGuideProgress({ ready: true, stepDone })).toEqual({
|
||||
ready: true,
|
||||
doneCount: 4,
|
||||
total: FEATURE_WALL_SETUP_STEPS.length,
|
||||
firstIncompleteStepId: 'agent-capabilities'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not mark Settings complete for fresh users when only the browser step is incomplete', () => {
|
||||
expect(
|
||||
getSettingsSetupGuideProgress({
|
||||
ready: true,
|
||||
stepDone: makePreBrowserDoneStepState()
|
||||
})
|
||||
).toEqual({
|
||||
ready: true,
|
||||
doneCount: FEATURE_WALL_SETUP_STEPS.length - 1,
|
||||
total: FEATURE_WALL_SETUP_STEPS.length,
|
||||
firstIncompleteStepId: 'browser'
|
||||
})
|
||||
})
|
||||
|
||||
it('marks Settings complete when every setup guide step is done', () => {
|
||||
const stepDone = Object.fromEntries(
|
||||
FEATURE_WALL_SETUP_STEPS.map((step) => [step.id, true])
|
||||
) as Record<FeatureWallSetupStepId, boolean>
|
||||
|
||||
expect(getSettingsSetupGuideProgress({ ready: true, stepDone })).toEqual({
|
||||
ready: true,
|
||||
doneCount: FEATURE_WALL_SETUP_STEPS.length,
|
||||
total: FEATURE_WALL_SETUP_STEPS.length,
|
||||
firstIncompleteStepId: null
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves progress readiness for the sidebar row gate', () => {
|
||||
const progress = getSettingsSetupGuideProgress({
|
||||
ready: false,
|
||||
stepDone: {
|
||||
'two-worktrees': true
|
||||
}
|
||||
})
|
||||
|
||||
expect(progress.ready).toBe(false)
|
||||
expect(progress.doneCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
const { rejectAppStoreAccess } = vi.hoisted(() => ({
|
||||
rejectAppStoreAccess: vi.fn(() => {
|
||||
throw new Error('Pure helper tests must not access the app store')
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(rejectAppStoreAccess, {
|
||||
getState: rejectAppStoreAccess,
|
||||
getInitialState: rejectAppStoreAccess,
|
||||
setState: rejectAppStoreAccess,
|
||||
subscribe: rejectAppStoreAccess
|
||||
})
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
expect(rejectAppStoreAccess).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,398 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { SshManagedServerUpdateNote } from '../../../shared/ssh-types'
|
||||
import { sshConnectFailureText, sshHostServerStatusLine } from './ssh-host-server-status-copy'
|
||||
|
||||
const plain = { label: 'Box' }
|
||||
|
||||
describe('SSH host server status line', () => {
|
||||
it('says nothing for a plain host before any decision', () => {
|
||||
expect(sshHostServerStatusLine(plain, undefined)).toBeNull()
|
||||
})
|
||||
|
||||
it('names the host while setting up and says the code stays where it is', () => {
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, { managedServer: { kind: 'setting-up', phase: 'converting' } })
|
||||
).toEqual({
|
||||
tone: 'muted',
|
||||
text: 'Moving to Orca on Box…',
|
||||
hint: 'Runs on Box, reached over your own SSH connection. Your code and files stay where they are. Only Orca’s list of projects, tabs and terminals moves to Box.'
|
||||
})
|
||||
// A reconnect of a host that already moved starts its server; nothing moves then.
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, { managedServer: { kind: 'setting-up', phase: 'starting' } })
|
||||
).toEqual({ tone: 'muted', text: 'Starting Orca on Box…' })
|
||||
})
|
||||
|
||||
it('shows startup progress and preserves the cause of unverifiable server contact', () => {
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, { managedServer: { kind: 'setting-up', phase: 'starting' } })
|
||||
?.text
|
||||
).toBe('Starting Orca on Box…')
|
||||
const detail = 'orcad did not become ready.\nLast lines of orcad.log:\nboom'
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'managed',
|
||||
environmentId: 'e',
|
||||
serving: { state: 'unverifiable', detail }
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
tone: 'warning',
|
||||
text: 'Orca couldn’t confirm that its server on Box is answering.',
|
||||
detail
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'This host’s Orca server process is live but is not answering.',
|
||||
'SSH operation was cancelled',
|
||||
'SSH transport closed before the server answered'
|
||||
])('does not infer process exit from unverifiable contact: %s', (detail) => {
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'managed',
|
||||
environmentId: 'e',
|
||||
serving: { state: 'unverifiable', detail },
|
||||
update: { state: 'deferred', detail: 'Terminals are running.' }
|
||||
}
|
||||
})
|
||||
).toEqual({
|
||||
tone: 'warning',
|
||||
text: 'Orca couldn’t confirm that its server on Box is answering.',
|
||||
detail
|
||||
})
|
||||
})
|
||||
|
||||
it('warns when the SSH session that started a macOS host’s server ended', () => {
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'managed', environmentId: 'e', launchSessionEnded: true }
|
||||
})
|
||||
).toEqual({
|
||||
tone: 'warning',
|
||||
text: 'Terminals on this Mac may have lost local network access because the SSH session that started this host’s Orca server ended. Restart Orca’s server to restore it.'
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a managed server being updated, and why it kept its version', () => {
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, { managedServer: { kind: 'setting-up', phase: 'updating' } })
|
||||
?.text
|
||||
).toBe('Updating Orca on Box…')
|
||||
const managed = (update: SshManagedServerUpdateNote) =>
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'managed', environmentId: 'e', update }
|
||||
})
|
||||
expect(managed({ state: 'host-newer' })?.text).toContain('newer version of Orca’s server')
|
||||
expect(managed({ state: 'deferred', detail: '2 terminals are running.' })).toMatchObject({
|
||||
tone: 'muted',
|
||||
detail: '2 terminals are running.'
|
||||
})
|
||||
expect(managed({ state: 'failed', detail: 'readiness timed out' })).toMatchObject({
|
||||
tone: 'warning',
|
||||
detail: 'readiness timed out'
|
||||
})
|
||||
})
|
||||
|
||||
it('says a managed server may be behind this app, whatever kept it there', () => {
|
||||
const line = (update: SshManagedServerUpdateNote) =>
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'managed', environmentId: 'e', update }
|
||||
})
|
||||
expect(line({ state: 'rolled-back' })).toMatchObject({ tone: 'warning' })
|
||||
expect(line({ state: 'rolled-back' })?.text).toContain('Update server')
|
||||
expect(line({ state: 'no-template' })?.text).toContain('can’t update')
|
||||
expect(line({ state: 'migrating' })?.text).toContain('project move')
|
||||
expect(line({ state: 'check-failed', detail: 'ssh dropped' })).toMatchObject({
|
||||
tone: 'warning',
|
||||
detail: 'ssh dropped'
|
||||
})
|
||||
})
|
||||
|
||||
it('says which projects the server already had, and what of this desktop’s stayed behind', () => {
|
||||
const linked = (automations: number) =>
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'managed',
|
||||
environmentId: 'e',
|
||||
linked: [
|
||||
{ path: '/root/app', automations },
|
||||
{ path: '/root/tool', automations: 0 }
|
||||
]
|
||||
}
|
||||
})
|
||||
expect(linked(2)).toEqual({
|
||||
tone: 'warning',
|
||||
text: 'This host’s Orca server already had /root/app, /root/tool, so this desktop now uses the server’s copy. This desktop’s tabs and 2 automations there were not moved, and worktrees it created there show as external worktrees.'
|
||||
})
|
||||
expect(linked(0)?.text).not.toContain('automation')
|
||||
})
|
||||
|
||||
it('keeps durable reasons visible without a live state', () => {
|
||||
expect(
|
||||
sshHostServerStatusLine({ label: 'Box', orcadFence: { environmentId: 'e' } }, undefined)
|
||||
).toBeNull()
|
||||
expect(
|
||||
sshHostServerStatusLine(
|
||||
{
|
||||
label: 'Box',
|
||||
orcadFence: { environmentId: 'e', sourceChangedAt: '2026-10-05T00:00:00Z' }
|
||||
},
|
||||
{ managedServer: { kind: 'managed', environmentId: 'e' } }
|
||||
)
|
||||
).toMatchObject({ tone: 'warning' })
|
||||
expect(
|
||||
sshHostServerStatusLine(
|
||||
{
|
||||
label: 'Box',
|
||||
managedServerUnavailable: { reason: 'native_preflight', appVersion: '1.5.0' }
|
||||
},
|
||||
undefined
|
||||
)?.text
|
||||
).toContain('missing system libraries')
|
||||
expect(
|
||||
sshHostServerStatusLine(
|
||||
{
|
||||
label: 'Box',
|
||||
managedServerUnavailable: { reason: 'future_reason', appVersion: '1.5.0' }
|
||||
},
|
||||
undefined
|
||||
)
|
||||
).toMatchObject({ text: expect.not.stringContaining('future_reason'), detail: 'future_reason' })
|
||||
})
|
||||
|
||||
it('tells an unsupported host to install an older Orca, never to fall back', () => {
|
||||
const line = sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'orcad_unavailable', detail: 'native_preflight' }
|
||||
})
|
||||
expect(line).toEqual({
|
||||
tone: 'destructive',
|
||||
text: 'This host isn’t supported by this version of Orca (the host is missing system libraries the server needs). To keep using it, install an older version of Orca.'
|
||||
})
|
||||
expect(line?.text).not.toContain('relay')
|
||||
})
|
||||
|
||||
it('tells a noexec host what to change, not to install an older Orca', () => {
|
||||
const line = sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'orcad_unavailable', detail: 'runtime_noexec' }
|
||||
})
|
||||
expect(line).toMatchObject({ tone: 'destructive', text: expect.stringContaining('noexec') })
|
||||
expect(line?.text).toContain('connect again')
|
||||
expect(line?.text).not.toContain('older version')
|
||||
})
|
||||
|
||||
it('names the Windows host’s blocker instead of advising a retry or an older Orca', () => {
|
||||
const line = (detail: string) =>
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'orcad_unavailable', detail }
|
||||
})
|
||||
expect(line('windows_build_unsupported')).toMatchObject({
|
||||
tone: 'destructive',
|
||||
text: expect.stringContaining('older than Windows 10 and Windows Server 2016')
|
||||
})
|
||||
expect(line('windows_no_extractor')).toMatchObject({
|
||||
tone: 'destructive',
|
||||
text: expect.stringContaining('Install Windows PowerShell 5.1 on the host')
|
||||
})
|
||||
for (const detail of ['windows_build_unsupported', 'windows_no_extractor']) {
|
||||
expect(line(detail)?.text).not.toMatch(/older version of Orca|Try connecting again/)
|
||||
}
|
||||
})
|
||||
|
||||
it('says plainly when neither port forwarding nor the SSH session reaches the server', () => {
|
||||
const live = sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'relay',
|
||||
reason: 'orcad_unavailable',
|
||||
detail: 'ssh_tunnel_unavailable'
|
||||
}
|
||||
})
|
||||
const recorded = sshHostServerStatusLine(
|
||||
{
|
||||
label: 'Box',
|
||||
managedServerUnavailable: { reason: 'ssh_tunnel_unavailable', appVersion: '1.5.0' }
|
||||
},
|
||||
undefined
|
||||
)
|
||||
for (const line of [live, recorded]) {
|
||||
expect(line).toMatchObject({
|
||||
tone: 'destructive',
|
||||
text: expect.stringContaining('install an older version of Orca')
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('names the relay terminals that keep a bridged host unmoved, and how to clear them', () => {
|
||||
const line = sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'relay',
|
||||
reason: 'relay_terminals_live',
|
||||
terminals: 1,
|
||||
bridged: true,
|
||||
detail: 'Terminals an older version of Orca started in /root/repo are still running…'
|
||||
}
|
||||
})
|
||||
expect(line).toMatchObject({ tone: 'warning' })
|
||||
expect(line?.text).toContain('They keep working here')
|
||||
expect(line?.text).toContain('Close them or end them on the host, then reconnect')
|
||||
expect(line?.detail).toContain('/root/repo')
|
||||
const unverifiable = sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'relay_terminals_unverifiable', bridged: true }
|
||||
})
|
||||
expect(unverifiable?.text).toContain('couldn’t confirm')
|
||||
expect(unverifiable?.text).not.toContain('They keep working here')
|
||||
})
|
||||
|
||||
it('asks for a reconnect while an older Orca’s terminals still run, counting them', () => {
|
||||
const live = (terminals?: number) =>
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'relay_terminals_live', terminals }
|
||||
})?.text
|
||||
expect(live(1)).toBe(
|
||||
'1 terminal started by an older version of Orca is still running on this host, and it doesn’t stop on its own. Close it in that version of Orca or end it on the host, then reconnect.'
|
||||
)
|
||||
expect(live(2)).toContain('2 terminals started by an older version of Orca')
|
||||
// Why: an absent count is unreported, never zero open terminals.
|
||||
expect(live(undefined)).not.toMatch(/\d/)
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'relay_terminals_unverifiable' }
|
||||
})?.text
|
||||
).toContain('Reconnect to try again')
|
||||
})
|
||||
|
||||
it('offers a failed setup’s log tail beside a try-again line', () => {
|
||||
const detail = 'No readiness line.\nLast lines of orcad.log:\nError: EADDRINUSE'
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, {
|
||||
managedServer: { kind: 'relay', reason: 'deferred', detail }
|
||||
})
|
||||
).toMatchObject({
|
||||
tone: 'warning',
|
||||
text: expect.stringContaining('Try connecting again'),
|
||||
detail
|
||||
})
|
||||
})
|
||||
|
||||
it('says contact was lost, not that the server failed, when setup lost the host', () => {
|
||||
const line = sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'relay',
|
||||
reason: 'failed',
|
||||
contactLost: true,
|
||||
detail: 'The SSH connection closed before the transfer finished'
|
||||
}
|
||||
})
|
||||
expect(line).toMatchObject({ tone: 'warning', text: expect.stringContaining('lost contact') })
|
||||
expect(line?.text).not.toContain('couldn’t start')
|
||||
})
|
||||
|
||||
it('says a move that stopped short is paused and kept, not a plain try-again', () => {
|
||||
const detail = 'Could not connect to the remote Orca runtime.'
|
||||
const state = {
|
||||
managedServer: {
|
||||
kind: 'relay' as const,
|
||||
reason: 'failed' as const,
|
||||
detail,
|
||||
held: true as const
|
||||
}
|
||||
}
|
||||
expect(sshHostServerStatusLine(plain, state)).toMatchObject({
|
||||
tone: 'warning',
|
||||
text: expect.stringContaining('Nothing is lost'),
|
||||
detail
|
||||
})
|
||||
expect(sshConnectFailureText(state, 'raw')).toContain('Settings > Orca on your SSH hosts')
|
||||
})
|
||||
|
||||
it('puts an unserved host’s translated reason in the connect toast', () => {
|
||||
const state = {
|
||||
managedServer: { kind: 'relay' as const, reason: 'orcad_unavailable' as const }
|
||||
}
|
||||
expect(sshConnectFailureText(state, 'raw')).toContain('install an older version of Orca')
|
||||
expect(sshConnectFailureText(undefined, 'raw')).toBe('raw')
|
||||
})
|
||||
|
||||
it('names what refuses the move, in the status line and the connect toast', () => {
|
||||
const detail =
|
||||
'This host’s Orca server already holds different saved state for something this desktop is moving, so nothing was moved and this desktop keeps its projects. Retrying won’t clear it. (orcad_migration_client_state_conflict:ui:workspaceHostScope)'
|
||||
const state = {
|
||||
managedServer: { kind: 'relay' as const, reason: 'refused' as const, detail }
|
||||
}
|
||||
const line = sshHostServerStatusLine(plain, state)
|
||||
expect(line).toMatchObject({ tone: 'destructive', detail })
|
||||
expect(line?.text).toContain('workspaceHostScope')
|
||||
expect(line?.text).not.toContain('something on the host stops the move')
|
||||
expect(sshConnectFailureText(state, 'raw')).toContain('already holds different saved state')
|
||||
})
|
||||
|
||||
it('caps a long refusal in the Settings row, but not where there is no Details', () => {
|
||||
const detail = `This SSH host cannot move yet: ${'saved tabs and panes (a); '.repeat(60)}then connect again.`
|
||||
const state = { managedServer: { kind: 'relay' as const, reason: 'refused' as const, detail } }
|
||||
const line = sshHostServerStatusLine(plain, state)
|
||||
expect(line?.detail).toBe(detail)
|
||||
expect(line?.text.length).toBeLessThan(400)
|
||||
expect(line?.text).toContain('This SSH host cannot move yet')
|
||||
// The terminal pane and toast keep the way out, which comes last.
|
||||
expect(sshConnectFailureText(state, 'raw')).toContain('then connect again.')
|
||||
})
|
||||
|
||||
it('says what stays on this computer when Move anyway clears the refusal (#29083)', () => {
|
||||
const state = {
|
||||
managedServer: {
|
||||
kind: 'relay' as const,
|
||||
reason: 'refused' as const,
|
||||
canLeaveBehind: true as const,
|
||||
detail: 'This SSH host cannot move yet: automations (1), automation runs (4).'
|
||||
}
|
||||
}
|
||||
const line = sshHostServerStatusLine(plain, state)
|
||||
expect(line).toMatchObject({ tone: 'warning' })
|
||||
expect(line?.text).toBe(
|
||||
'Some saved items for this host can’t move to its Orca server: automations (1), automation runs (4). Move anyway connects the host and keeps them on this computer.'
|
||||
)
|
||||
expect(sshConnectFailureText(state, 'raw')).toBe(line?.text)
|
||||
})
|
||||
|
||||
it('keeps main’s reason on a web client, which can’t move the host', () => {
|
||||
vi.stubGlobal('window', { __ORCA_WEB_CLIENT__: true })
|
||||
try {
|
||||
const line = sshHostServerStatusLine(plain, {
|
||||
managedServer: {
|
||||
kind: 'relay',
|
||||
reason: 'refused',
|
||||
canLeaveBehind: true,
|
||||
detail: 'This SSH host cannot move yet: automations (1).'
|
||||
}
|
||||
})
|
||||
expect(line?.text).toBe(
|
||||
'Orca isn’t set up on this host yet. This SSH host cannot move yet: automations (1).'
|
||||
)
|
||||
} finally {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the plain refusal line when main named no reason', () => {
|
||||
expect(
|
||||
sshHostServerStatusLine(plain, { managedServer: { kind: 'relay', reason: 'refused' } })?.text
|
||||
).toContain('Orca isn’t set up on this host yet')
|
||||
})
|
||||
|
||||
it('says a host an older Orca changed is blocked until moved, not served by the relay', () => {
|
||||
const changed = sshHostServerStatusLine(
|
||||
{ label: 'Box', orcadFence: { environmentId: 'e', sourceChangedAt: '2026-10-05T00:00:00Z' } },
|
||||
undefined
|
||||
)
|
||||
const unserved = sshConnectFailureText(
|
||||
{ managedServer: { kind: 'relay', reason: 'source_changed' } },
|
||||
'raw'
|
||||
)
|
||||
for (const text of [changed?.text, unserved]) {
|
||||
expect(text).toContain('to connect again')
|
||||
expect(text).not.toContain('relay')
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user