mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove 49 low-value cases and four suites (#34407)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1120,7 +1120,6 @@
|
||||
1 test-no-wall-clock:src/renderer/src/components/sidebar/SidebarToolbar.test.tsx
|
||||
5 test-no-wall-clock:src/renderer/src/components/sidebar/use-workspace-reference-details.test.tsx
|
||||
3 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-agent-rows-subscriptions.bench.test.tsx
|
||||
1 test-no-wall-clock:src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts
|
||||
4 test-no-wall-clock:src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
|
||||
13 test-no-wall-clock:src/renderer/src/components/sidebar/WorktreeCard.pr-display.test.tsx
|
||||
2 test-no-wall-clock:src/renderer/src/components/sidebar/WorktreeCard.single-reference-details.test.tsx
|
||||
|
||||
@@ -7718,7 +7718,6 @@
|
||||
"src/renderer/src/components/editor/rich-markdown-terminal-path-paste.test.ts": 146,
|
||||
"src/renderer/src/components/editor/rich-markdown-toc-heading-target.test.ts": 130,
|
||||
"src/renderer/src/components/editor/rich-markdown-trailing-paragraph.test.ts": 2127,
|
||||
"src/renderer/src/components/editor/selection-copy.test.ts": 27,
|
||||
"src/renderer/src/components/editor/setup-contextual-copy.test.ts": 116,
|
||||
"src/renderer/src/components/editor/untitled-file-rename-path.test.ts": 31,
|
||||
"src/renderer/src/components/editor/use-editor-content-change-handler.test.tsx": 1528,
|
||||
@@ -9137,7 +9136,6 @@
|
||||
"src/renderer/src/components/sidebar/worktree-lineage-projection.test.ts": 58,
|
||||
"src/renderer/src/components/sidebar/worktree-lineage-toggle-handler-cache.test.ts": 32,
|
||||
"src/renderer/src/components/sidebar/worktree-list-groups-host-collision.test.ts": 205,
|
||||
"src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts": 204,
|
||||
"src/renderer/src/components/sidebar/worktree-list-groups-host-name-collisions.test.ts": 194,
|
||||
"src/renderer/src/components/sidebar/worktree-list-groups-imported-worktrees.test.ts": 185,
|
||||
"src/renderer/src/components/sidebar/worktree-list-groups-lineage-nesting.test.ts": 234,
|
||||
@@ -9502,7 +9500,6 @@
|
||||
"src/renderer/src/components/task-page/task-page-default-repo-selection.test.ts": 42,
|
||||
"src/renderer/src/components/task-page/task-page-initial-selection-scaling.test.tsx": 1703,
|
||||
"src/renderer/src/components/task-page/task-page-pagination-page-numbers.test.ts": 54,
|
||||
"src/renderer/src/components/task-source-context-summary.test.ts": 138,
|
||||
"src/renderer/src/components/task-source-provider-availability.test.ts": 43,
|
||||
"src/renderer/src/components/terminal-agent-tab-shortcut.test.ts": 6185,
|
||||
"src/renderer/src/components/terminal-pane/CloseTerminalDialog.test.tsx": 460,
|
||||
@@ -10016,7 +10013,6 @@
|
||||
"src/renderer/src/components/terminal-pane/terminal-retention-churn-pins.test.tsx": 522,
|
||||
"src/renderer/src/components/terminal-pane/terminal-retention-exempt-growth.test.ts": 361,
|
||||
"src/renderer/src/components/terminal-pane/terminal-runtime-host-link-routing.test.ts": 1940,
|
||||
"src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts": 1225,
|
||||
"src/renderer/src/components/terminal-pane/terminal-selection-fit-guard.test.ts": 31,
|
||||
"src/renderer/src/components/terminal-pane/terminal-shortcut-ctrl-arrow.test.ts": 54,
|
||||
"src/renderer/src/components/terminal-pane/terminal-shortcut-forward-delete.test.ts": 82,
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
canCopySelectionWithContext,
|
||||
formatCopiedSelectionWithContext,
|
||||
getContextualCopyLineRange
|
||||
} from './selection-copy'
|
||||
|
||||
describe('contextual copy eligibility', () => {
|
||||
it('matches formatting eligibility for line boundaries and whitespace-only selections', () => {
|
||||
for (const startLineNumber of [1, 2, 44]) {
|
||||
for (const endLineNumber of [1, 2, 3, 44, 45]) {
|
||||
for (const endColumn of [1, 2, 15]) {
|
||||
const selection = { startLineNumber, startColumn: 1, endLineNumber, endColumn }
|
||||
const expected =
|
||||
endLineNumber !== startLineNumber &&
|
||||
(endColumn === 1 ? endLineNumber - 1 : endLineNumber) >= startLineNumber
|
||||
expect(canCopySelectionWithContext(selection)).toBe(expected)
|
||||
expect(
|
||||
formatCopiedSelectionWithContext({
|
||||
relativePath: '',
|
||||
language: 'plaintext',
|
||||
selection,
|
||||
selectedText: ' \n'
|
||||
}) !== null
|
||||
).toBe(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCopiedSelectionWithContext', () => {
|
||||
it('formats multi-line selections with file and line context', () => {
|
||||
expect(
|
||||
formatCopiedSelectionWithContext({
|
||||
relativePath: 'src/main/git/status.ts',
|
||||
language: 'typescript',
|
||||
selection: {
|
||||
startLineNumber: 44,
|
||||
startColumn: 1,
|
||||
endLineNumber: 47,
|
||||
endColumn: 9
|
||||
},
|
||||
selectedText: "if (line.startsWith('1 ')) {\n const parts = line.split(' ')\n}"
|
||||
})
|
||||
).toBe(
|
||||
[
|
||||
'File: src/main/git/status.ts',
|
||||
'Lines: 44-47',
|
||||
'',
|
||||
'```ts',
|
||||
"if (line.startsWith('1 ')) {\n const parts = line.split(' ')\n}",
|
||||
'```'
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
|
||||
it('treats column-1 end positions as the previous line for full-line selections', () => {
|
||||
const selection = {
|
||||
startLineNumber: 44,
|
||||
startColumn: 1,
|
||||
endLineNumber: 48,
|
||||
endColumn: 1
|
||||
}
|
||||
|
||||
expect(getContextualCopyLineRange(selection)).toEqual({
|
||||
startLine: 44,
|
||||
endLine: 47
|
||||
})
|
||||
|
||||
expect(
|
||||
formatCopiedSelectionWithContext({
|
||||
relativePath: 'src/main/git/status.ts',
|
||||
language: 'typescript',
|
||||
selection,
|
||||
selectedText: 'line 44\nline 45\nline 46\nline 47\n'
|
||||
})
|
||||
).toContain('Lines: 44-47')
|
||||
})
|
||||
|
||||
it('keeps full-line single-line selections copyable', () => {
|
||||
expect(
|
||||
formatCopiedSelectionWithContext({
|
||||
relativePath: 'src/main/git/status.ts',
|
||||
language: 'typescript',
|
||||
selection: {
|
||||
startLineNumber: 44,
|
||||
startColumn: 1,
|
||||
endLineNumber: 45,
|
||||
endColumn: 1
|
||||
},
|
||||
selectedText: 'line 44\n'
|
||||
})
|
||||
).toContain('Line: 44')
|
||||
})
|
||||
|
||||
it('leaves single-line selections alone', () => {
|
||||
expect(
|
||||
formatCopiedSelectionWithContext({
|
||||
relativePath: 'src/main/git/status.ts',
|
||||
language: 'typescript',
|
||||
selection: {
|
||||
startLineNumber: 44,
|
||||
startColumn: 4,
|
||||
endLineNumber: 44,
|
||||
endColumn: 15
|
||||
},
|
||||
selectedText: 'line.startsWith'
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -10,43 +10,6 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('commit failure summary', () => {
|
||||
it('collapses lint-staged, husky, and oxlint failures to a lint summary', () => {
|
||||
const raw = [
|
||||
'npm warn Unknown env config "python". This will stop working.',
|
||||
'husky - pre-commit hook exited with code 1',
|
||||
'lint-staged failed',
|
||||
'oxlint found 3 errors'
|
||||
].join('\n')
|
||||
|
||||
expect(summarizeCommitFailure(raw)).toBe('Lint failed during commit.')
|
||||
})
|
||||
|
||||
it('collapses pre-commit hook failures without lint output to a hook summary', () => {
|
||||
expect(summarizeCommitFailure('pre-commit hook failed: secret scan blocked commit')).toBe(
|
||||
'Pre-commit hook failed.'
|
||||
)
|
||||
})
|
||||
|
||||
it('does not treat generic non-lint error counts as lint failures', () => {
|
||||
expect(summarizeCommitFailure('tsc --noEmit\nFound 5 errors in 3 files.')).toBe('tsc --noEmit')
|
||||
expect(summarizeCommitFailure('pre-commit hook failed\ntsc found 5 errors')).toBe(
|
||||
'Pre-commit hook failed.'
|
||||
)
|
||||
})
|
||||
|
||||
it('falls back to the first meaningful line for generic failures', () => {
|
||||
expect(summarizeCommitFailure('\n fatal: unable to auto-detect email address\nmore')).toBe(
|
||||
'fatal: unable to auto-detect email address'
|
||||
)
|
||||
})
|
||||
|
||||
it('strips ANSI/control sequences and handles empty input', () => {
|
||||
expect(summarizeCommitFailure('\u001b[31meslint found 2 errors\u001b[0m')).toBe(
|
||||
'Lint failed during commit.'
|
||||
)
|
||||
expect(summarizeCommitFailure(' \n\t ')).toBe('Commit failed.')
|
||||
})
|
||||
|
||||
it('summarizes newline-heavy commit output without splitting the full log', () => {
|
||||
const split = vi.spyOn(String.prototype, 'split')
|
||||
const raw = `${'npm notice noisy line\n'.repeat(1000)}husky - pre-commit hook\noxlint failed`
|
||||
@@ -63,27 +26,4 @@ describe('commit failure summary', () => {
|
||||
expect(hasExpandedCommitFailureDetails(raw, 'Commit failed.')).toBe(true)
|
||||
expect(split).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('reports whether expanded details add information beyond the summary', () => {
|
||||
expect(hasExpandedCommitFailureDetails('nothing to commit', 'nothing to commit')).toBe(false)
|
||||
expect(
|
||||
hasExpandedCommitFailureDetails(
|
||||
'husky - pre-commit hook\neslint found 2 errors\nfull output',
|
||||
'Lint failed during commit.'
|
||||
)
|
||||
).toBe(true)
|
||||
expect(hasExpandedCommitFailureDetails('', 'Commit failed.')).toBe(false)
|
||||
})
|
||||
|
||||
it('compares expanded details without whitespace regex replacement', () => {
|
||||
const replace = vi.spyOn(String.prototype, 'replace')
|
||||
const raw = ['nothing', String.fromCharCode(160), ' to\tcommit\n'].join('')
|
||||
|
||||
expect(hasExpandedCommitFailureDetails(raw, 'nothing to commit')).toBe(false)
|
||||
expect(
|
||||
replace.mock.calls.filter(
|
||||
([pattern]) => pattern instanceof RegExp && pattern.source === '\\s+'
|
||||
)
|
||||
).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { createElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData'
|
||||
import { CompactAgentRow, getCompactAgentSecondary } from './worktree-card-compact-agent-row'
|
||||
import {
|
||||
buildSummaryAgentGroups,
|
||||
getAgentDotState,
|
||||
summarizeAgents
|
||||
} from './worktree-card-agent-summary'
|
||||
import { getAgentDotState } from './worktree-card-agent-summary'
|
||||
import { buildSubagentChildRows } from './worktree-subagent-child-rows'
|
||||
|
||||
function monitoringAgent(): DashboardAgentRowData {
|
||||
@@ -39,176 +31,7 @@ function monitoringAgent(): DashboardAgentRowData {
|
||||
}
|
||||
}
|
||||
|
||||
function orderedTitles(markup: string): string[] {
|
||||
return [...markup.matchAll(/\stitle="([^"]*)"/g)].map((match) => match[1])
|
||||
}
|
||||
|
||||
function renderCompactAgentRow(props: React.ComponentProps<typeof CompactAgentRow>): string {
|
||||
return renderToStaticMarkup(
|
||||
createElement(TooltipProvider, null, createElement(CompactAgentRow, props))
|
||||
)
|
||||
}
|
||||
|
||||
describe('worktree card agent summary', () => {
|
||||
it("counts a running child by its own row's state", () => {
|
||||
const running: DashboardAgentRowData = {
|
||||
...monitoringAgent(),
|
||||
paneKey: 'tab-1:leaf-1\u0000subagent:child',
|
||||
rowSource: 'subagent',
|
||||
state: 'working',
|
||||
childRow: {
|
||||
id: 'child',
|
||||
kind: 'agent',
|
||||
displayState: 'waiting',
|
||||
name: 'Fuzz the tokenizer',
|
||||
detail: null,
|
||||
firstObservedAt: 1,
|
||||
recencyAt: 1,
|
||||
canStop: false,
|
||||
settled: false,
|
||||
owned: []
|
||||
}
|
||||
}
|
||||
expect(getAgentDotState(running)).toBe('waiting')
|
||||
expect(buildSummaryAgentGroups([running]).map((group) => group.state)).toEqual(['waiting'])
|
||||
})
|
||||
|
||||
it('presents passive working as monitoring', () => {
|
||||
const agent = monitoringAgent()
|
||||
|
||||
expect(getAgentDotState(agent)).toBe('monitoring')
|
||||
expect(getCompactAgentSecondary(agent, Date.now())).toBe('Monitoring background tasks')
|
||||
expect(summarizeAgents([agent], 'Agent')).toBe('Agent monitoring')
|
||||
})
|
||||
|
||||
it('keeps monitoring visible before a compact row prompt', () => {
|
||||
const agent = monitoringAgent()
|
||||
agent.entry.prompt = 'Run background checks'
|
||||
|
||||
const markup = renderCompactAgentRow({ agent, now: 2000, onActivate: vi.fn() })
|
||||
|
||||
expect(markup).toContain('title="Monitoring background tasks - Run background checks"')
|
||||
expect(markup).toMatch(
|
||||
/Monitoring background tasks<\/span><span[^>]*> - Run background checks<\/span>/
|
||||
)
|
||||
})
|
||||
|
||||
it('hands the whole row to the send-target reason, and only then', () => {
|
||||
const agent = monitoringAgent()
|
||||
agent.entry.prompt = 'Run background checks'
|
||||
|
||||
const disabled = renderCompactAgentRow({
|
||||
agent,
|
||||
now: 2000,
|
||||
onActivate: vi.fn(),
|
||||
sendTargetStatus: 'disabled',
|
||||
sendTargetDisabledReason: 'Agent needs permission'
|
||||
})
|
||||
|
||||
// The dot sits inside the row, so its own state title would shadow the reason on hover.
|
||||
expect(orderedTitles(disabled)).toEqual(['Agent needs permission', 'Claude'])
|
||||
|
||||
const eligible = renderCompactAgentRow({ agent, now: 2000, onActivate: vi.fn() })
|
||||
|
||||
expect(orderedTitles(eligible)).toEqual([
|
||||
'Claude',
|
||||
'Monitoring background tasks - Run background checks'
|
||||
])
|
||||
expect(eligible).toContain('data-slot="tooltip-trigger"')
|
||||
})
|
||||
|
||||
it("lists a crash-cut turn as failed and a user's Stop as interrupted, before clean completions", () => {
|
||||
const done = monitoringAgent()
|
||||
done.state = 'done'
|
||||
done.entry.state = 'done'
|
||||
done.entry.workingMode = undefined
|
||||
const interrupted = {
|
||||
...done,
|
||||
paneKey: 'tab-1:leaf-2',
|
||||
entry: {
|
||||
...done.entry,
|
||||
paneKey: 'tab-1:leaf-2',
|
||||
mainAgent: { state: 'done' as const, outcome: 'interruption' as const, stateStartedAt: 1 }
|
||||
}
|
||||
}
|
||||
const stopped = {
|
||||
...done,
|
||||
paneKey: 'tab-1:leaf-3',
|
||||
entry: { ...done.entry, paneKey: 'tab-1:leaf-3', interrupted: true }
|
||||
}
|
||||
|
||||
expect(getAgentDotState(interrupted)).toBe('failed')
|
||||
expect(getCompactAgentSecondary(interrupted, 0)).toBe('Failed')
|
||||
expect(getAgentDotState(stopped)).toBe('interrupted')
|
||||
expect(getCompactAgentSecondary(stopped, 0)).toBe('Interrupted by user')
|
||||
const replaced = {
|
||||
...done,
|
||||
entry: {
|
||||
...done.entry,
|
||||
mainAgent: { state: 'done' as const, outcome: 'superseded' as const, stateStartedAt: 1 }
|
||||
}
|
||||
}
|
||||
expect(getAgentDotState(replaced)).toBe('interrupted')
|
||||
expect(getCompactAgentSecondary(replaced, 0)).toBe('Interrupted')
|
||||
expect(summarizeAgents([done, interrupted, stopped], 'Agents')).toBe(
|
||||
'Agents: 1 failed, 1 interrupted, 1 done'
|
||||
)
|
||||
})
|
||||
|
||||
it("lists a failed turn as failed, not done, ahead of a user's Stop", () => {
|
||||
const done = monitoringAgent()
|
||||
done.state = 'done'
|
||||
done.entry.state = 'done'
|
||||
done.entry.workingMode = undefined
|
||||
const failed = {
|
||||
...done,
|
||||
paneKey: 'tab-1:leaf-3',
|
||||
entry: {
|
||||
...done.entry,
|
||||
paneKey: 'tab-1:leaf-3',
|
||||
mainAgent: { state: 'done' as const, outcome: 'failure' as const, stateStartedAt: 1 }
|
||||
}
|
||||
}
|
||||
const interrupted = {
|
||||
...done,
|
||||
paneKey: 'tab-1:leaf-2',
|
||||
entry: {
|
||||
...done.entry,
|
||||
paneKey: 'tab-1:leaf-2',
|
||||
mainAgent: { state: 'done' as const, outcome: 'cancellation' as const, stateStartedAt: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
expect(getAgentDotState(failed)).toBe('failed')
|
||||
expect(getCompactAgentSecondary(failed, 0)).toBe('Failed')
|
||||
expect(getCompactAgentSecondary(interrupted, 0)).toBe('Interrupted by user')
|
||||
expect(summarizeAgents([done, interrupted, failed], 'Agents')).toBe(
|
||||
'Agents: 1 failed, 1 interrupted, 1 done'
|
||||
)
|
||||
})
|
||||
|
||||
it('reads a main agent that failed while its subagent works as failed, ranked above working', () => {
|
||||
const held = (outcome: 'failure' | 'success' | 'cancellation'): DashboardAgentRowData => {
|
||||
const agent = monitoringAgent()
|
||||
agent.entry.workingMode = undefined
|
||||
agent.entry.mainAgent = { state: 'done', outcome, stateStartedAt: 1 }
|
||||
return agent
|
||||
}
|
||||
const working = monitoringAgent()
|
||||
working.paneKey = 'tab-1:leaf-2'
|
||||
working.entry = { ...working.entry, paneKey: 'tab-1:leaf-2', workingMode: undefined }
|
||||
|
||||
expect(getAgentDotState(held('failure'))).toBe('failed')
|
||||
expect(getCompactAgentSecondary(held('failure'), 0)).toBe('Failed')
|
||||
expect(summarizeAgents([working, held('failure')], 'Agents')).toBe(
|
||||
'Agents: 1 failed, 1 working'
|
||||
)
|
||||
// Only a failure outranks live work; a success or a stop with a live subagent reads working.
|
||||
expect(getAgentDotState(held('success'))).toBe('working')
|
||||
expect(getAgentDotState(held('cancellation'))).toBe('working')
|
||||
expect(getCompactAgentSecondary(held('cancellation'), 0)).not.toBe('Interrupted by user')
|
||||
})
|
||||
|
||||
it('keeps a subagent row on its own state while its failed main agent reads failed', () => {
|
||||
const parent = monitoringAgent()
|
||||
parent.entry = {
|
||||
|
||||
@@ -1,342 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildRows } from './worktree-list/grouping/build-rows.test-fixture'
|
||||
import { getGroupKeyForWorktree } from '../../../../shared/sidebar-list/worktree-group-keys'
|
||||
import {
|
||||
LOCAL_HOST_LABEL,
|
||||
repo,
|
||||
worktree,
|
||||
remoteRepo,
|
||||
remoteWorktree,
|
||||
project,
|
||||
projectHostSetups
|
||||
} from './worktree-list-groups-test-fixtures'
|
||||
import type { ProjectHostSetup } from '../../../../shared/project-types'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
|
||||
describe('buildRows with pinned worktrees', () => {
|
||||
it('groups Windows host and WSL setups on the same runtime host', () => {
|
||||
const runtimeHostId = 'runtime:g16'
|
||||
const windowsRepo: Repo = {
|
||||
...repo,
|
||||
id: 'repo-windows',
|
||||
path: String.raw`C:\Users\alice\git\orca`,
|
||||
displayName: 'orca',
|
||||
executionHostId: runtimeHostId
|
||||
}
|
||||
const wslRepo: Repo = {
|
||||
...repo,
|
||||
id: 'repo-wsl',
|
||||
path: String.raw`\\wsl.localhost\Ubuntu\home\alice\git\orca`,
|
||||
displayName: 'orca',
|
||||
executionHostId: runtimeHostId
|
||||
}
|
||||
const windowsWorktree: Worktree = {
|
||||
...worktree,
|
||||
id: 'wt-windows',
|
||||
repoId: windowsRepo.id,
|
||||
path: String.raw`C:\Users\alice\git\orca\feature`
|
||||
}
|
||||
const wslWorktree: Worktree = {
|
||||
...worktree,
|
||||
id: 'wt-wsl',
|
||||
repoId: wslRepo.id,
|
||||
path: String.raw`\\wsl.localhost\Ubuntu\home\alice\git\orca\feature`
|
||||
}
|
||||
const windowsSetup: ProjectHostSetup = {
|
||||
...projectHostSetups[0]!,
|
||||
id: windowsRepo.id,
|
||||
hostId: runtimeHostId,
|
||||
repoId: windowsRepo.id,
|
||||
path: windowsRepo.path,
|
||||
displayName: windowsRepo.displayName
|
||||
}
|
||||
const wslSetup: ProjectHostSetup = {
|
||||
...windowsSetup,
|
||||
id: wslRepo.id,
|
||||
repoId: wslRepo.id,
|
||||
path: wslRepo.path
|
||||
}
|
||||
const rows = buildRows(
|
||||
'repo',
|
||||
[windowsWorktree, wslWorktree],
|
||||
new Map([
|
||||
[windowsRepo.id, windowsRepo],
|
||||
[wslRepo.id, wslRepo]
|
||||
]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map([
|
||||
[windowsWorktree.id, windowsWorktree],
|
||||
[wslWorktree.id, wslWorktree]
|
||||
]),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
{
|
||||
projects: [{ ...project, sourceRepoIds: [windowsRepo.id, wslRepo.id] }],
|
||||
projectHostSetups: [windowsSetup, wslSetup]
|
||||
}
|
||||
)
|
||||
|
||||
expect(rows.filter((row) => row.type === 'header')).toMatchObject([
|
||||
{
|
||||
key: 'project:github:stablyai/orca',
|
||||
label: 'Orca',
|
||||
count: 2
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('uses saved host labels for mixed-host sidebar card badges', () => {
|
||||
const runtimeRepo: Repo = {
|
||||
...remoteRepo,
|
||||
id: 'repo-runtime',
|
||||
path: '/Users/alice/runtime-orca',
|
||||
connectionId: null,
|
||||
executionHostId: 'runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3'
|
||||
}
|
||||
const runtimeWorktree: Worktree = {
|
||||
...remoteWorktree,
|
||||
id: 'wt-runtime',
|
||||
repoId: runtimeRepo.id
|
||||
}
|
||||
const runtimeSetup: ProjectHostSetup = {
|
||||
...projectHostSetups[1]!,
|
||||
id: runtimeRepo.id,
|
||||
hostId: 'runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3',
|
||||
repoId: runtimeRepo.id,
|
||||
path: runtimeRepo.path
|
||||
}
|
||||
const rows = buildRows(
|
||||
'repo',
|
||||
[worktree, runtimeWorktree],
|
||||
new Map([
|
||||
[repo.id, repo],
|
||||
[runtimeRepo.id, runtimeRepo]
|
||||
]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map([
|
||||
[worktree.id, worktree],
|
||||
[runtimeWorktree.id, runtimeWorktree]
|
||||
]),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
{ projects: [project], projectHostSetups: [projectHostSetups[0]!, runtimeSetup] },
|
||||
[],
|
||||
new Map([
|
||||
['local', LOCAL_HOST_LABEL],
|
||||
['runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3', 'dev box']
|
||||
])
|
||||
)
|
||||
|
||||
expect(rows).toMatchObject([
|
||||
{ type: 'header', key: 'project:github:stablyai/orca', label: 'Orca', count: 2 },
|
||||
{ type: 'item', worktree: { id: worktree.id }, hostContextLabel: LOCAL_HOST_LABEL },
|
||||
{ type: 'item', worktree: { id: runtimeWorktree.id }, hostContextLabel: 'dev box' }
|
||||
])
|
||||
})
|
||||
|
||||
it('uses the registered SSH target label for openclaw rows', () => {
|
||||
const sshRepo: Repo = {
|
||||
...remoteRepo,
|
||||
id: 'repo-openclaw',
|
||||
connectionId: 'openclaw',
|
||||
executionHostId: 'ssh:openclaw'
|
||||
}
|
||||
const sshWorktree: Worktree = {
|
||||
...remoteWorktree,
|
||||
id: 'wt-openclaw',
|
||||
repoId: sshRepo.id
|
||||
}
|
||||
const rows = buildRows(
|
||||
'workspace-status',
|
||||
[worktree, sshWorktree],
|
||||
new Map([
|
||||
[repo.id, repo],
|
||||
[sshRepo.id, sshRepo]
|
||||
]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map([
|
||||
[worktree.id, worktree],
|
||||
[sshWorktree.id, sshWorktree]
|
||||
]),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
undefined,
|
||||
[],
|
||||
new Map([['ssh:openclaw', 'openclaw']])
|
||||
)
|
||||
|
||||
expect(rows.filter((row) => row.type === 'item')).toMatchObject([
|
||||
{ worktree: { id: worktree.id }, hostContextLabel: LOCAL_HOST_LABEL },
|
||||
{ worktree: { id: sshWorktree.id }, hostContextLabel: 'openclaw' }
|
||||
])
|
||||
})
|
||||
|
||||
it('shows distinct Orca server names when status grouping mixes runtime hosts', () => {
|
||||
const firstRepo: Repo = {
|
||||
...repo,
|
||||
id: 'repo-runtime-a',
|
||||
executionHostId: 'runtime:env-a'
|
||||
}
|
||||
const secondRepo: Repo = {
|
||||
...repo,
|
||||
id: 'repo-runtime-b',
|
||||
executionHostId: 'runtime:env-b'
|
||||
}
|
||||
const firstWorktree: Worktree = {
|
||||
...worktree,
|
||||
id: 'wt-runtime-a',
|
||||
repoId: firstRepo.id
|
||||
}
|
||||
const secondWorktree: Worktree = {
|
||||
...worktree,
|
||||
id: 'wt-runtime-b',
|
||||
repoId: secondRepo.id
|
||||
}
|
||||
const rows = buildRows(
|
||||
'workspace-status',
|
||||
[firstWorktree, secondWorktree],
|
||||
new Map([
|
||||
[firstRepo.id, firstRepo],
|
||||
[secondRepo.id, secondRepo]
|
||||
]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map([
|
||||
[firstWorktree.id, firstWorktree],
|
||||
[secondWorktree.id, secondWorktree]
|
||||
]),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
undefined,
|
||||
[],
|
||||
new Map([
|
||||
['runtime:env-a', 'Remote Mac'],
|
||||
['runtime:env-b', 'Build Linux']
|
||||
])
|
||||
)
|
||||
|
||||
expect(rows.filter((row) => row.type === 'item')).toMatchObject([
|
||||
{ worktree: { id: firstWorktree.id }, hostContextLabel: 'Remote Mac' },
|
||||
{ worktree: { id: secondWorktree.id }, hostContextLabel: 'Build Linux' }
|
||||
])
|
||||
})
|
||||
|
||||
it('omits host context labels when a project group only has one host', () => {
|
||||
const secondLocalWorktree: Worktree = {
|
||||
...worktree,
|
||||
id: 'wt-local-2',
|
||||
displayName: 'local-only'
|
||||
}
|
||||
const rows = buildRows(
|
||||
'repo',
|
||||
[worktree, secondLocalWorktree],
|
||||
new Map([[repo.id, repo]]),
|
||||
null,
|
||||
new Set(),
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{},
|
||||
new Map([
|
||||
[worktree.id, worktree],
|
||||
[secondLocalWorktree.id, secondLocalWorktree]
|
||||
]),
|
||||
false,
|
||||
undefined,
|
||||
[],
|
||||
new Set(),
|
||||
new Map(),
|
||||
new Map(),
|
||||
[],
|
||||
{
|
||||
projects: [{ ...project, sourceRepoIds: [repo.id] }],
|
||||
projectHostSetups: [projectHostSetups[0]]
|
||||
}
|
||||
)
|
||||
|
||||
expect(rows).toMatchObject([
|
||||
{ type: 'header', key: 'project:github:stablyai/orca', label: 'Orca', count: 2 },
|
||||
{ type: 'item', worktree: { id: worktree.id } },
|
||||
{ type: 'item', worktree: { id: secondLocalWorktree.id } }
|
||||
])
|
||||
for (const row of rows) {
|
||||
if (row.type === 'item') {
|
||||
expect(row.hostContextLabel).toBeUndefined()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps same-named repos separate without project setup identity', () => {
|
||||
const rows = buildRows(
|
||||
'repo',
|
||||
[worktree, remoteWorktree],
|
||||
new Map([
|
||||
[repo.id, { ...repo, displayName: 'orca' }],
|
||||
[remoteRepo.id, { ...remoteRepo, displayName: 'orca' }]
|
||||
]),
|
||||
null,
|
||||
new Set()
|
||||
)
|
||||
|
||||
expect(rows.filter((row) => row.type === 'header')).toMatchObject([
|
||||
{ key: 'repo:repo-1' },
|
||||
{ key: 'repo:repo-remote' }
|
||||
])
|
||||
})
|
||||
|
||||
it('returns project group keys for worktree reveal when project setup identity exists', () => {
|
||||
expect(
|
||||
getGroupKeyForWorktree(
|
||||
'repo',
|
||||
remoteWorktree,
|
||||
new Map([[remoteRepo.id, remoteRepo]]),
|
||||
null,
|
||||
undefined,
|
||||
{
|
||||
projects: [project],
|
||||
projectHostSetups
|
||||
}
|
||||
)
|
||||
).toBe('project:github:stablyai/orca')
|
||||
})
|
||||
})
|
||||
@@ -1,8 +1,6 @@
|
||||
/** Disambiguate discovery notices by distinct host IDs from the unfiltered project universe, even when host labels match. */
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildRows } from './worktree-list/grouping/build-rows.test-fixture'
|
||||
import { getNoticeHostContextLabels } from '../../../../shared/sidebar-list/host-labels'
|
||||
import { buildProjectGroupingIndex } from '../../../../shared/project-section-grouping'
|
||||
import { repo, worktree, project, projectHostSetups } from './worktree-list-groups-test-fixtures'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { ProjectHostSetup } from '../../../../shared/project-types'
|
||||
@@ -148,12 +146,6 @@ describe('discovery notice rows on a multi-host project', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('labels a lone eligible row on a project that spans hosts', () => {
|
||||
expect(summarize(noticeRows({ eligibleRepoIds: [envTwin.id] }))).toEqual([
|
||||
{ repoId: envTwin.id, label: 'openclaw', hostId: ENV_HOST_ID, count: 134 }
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps each row its own label and count under either host filter', () => {
|
||||
expect(summarize(noticeRows({ eligibleRepoIds: [sshTwin.id] }))).toEqual([
|
||||
{ repoId: sshTwin.id, label: 'openclaw', hostId: SSH_HOST_ID, count: 61 }
|
||||
@@ -163,22 +155,6 @@ describe('discovery notice rows on a multi-host project', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('returns labels for exactly the eligible records, never the filtered-out ones', () => {
|
||||
// Assert exact map membership because rows cannot reveal extra ineligible entries.
|
||||
const index = buildProjectGroupingIndex(TWIN_GROUPING)
|
||||
|
||||
for (const eligible of [[sshTwin.id], [envTwin.id], [sshTwin.id, envTwin.id]]) {
|
||||
const labels = getNoticeHostContextLabels(
|
||||
eligible,
|
||||
TWIN_REPO_MAP.keys(),
|
||||
TWIN_REPO_MAP,
|
||||
index,
|
||||
HOST_LABELS
|
||||
)
|
||||
expect([...(labels?.keys() ?? [])]).toEqual(eligible)
|
||||
}
|
||||
})
|
||||
|
||||
it('leaves a single-host project unlabelled even with several records on it', () => {
|
||||
const secondLocal: Repo = { ...repo, id: 'repo-local-2', path: '/tmp/orca-second' }
|
||||
const rows = noticeRows({
|
||||
|
||||
@@ -76,30 +76,6 @@ describe('pinned rows on a multi-host sidebar', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('labels pinned rows when the only other host is itself pinned', () => {
|
||||
// Natural rows alone contain one host; both pinned hosts must still establish a mixed sidebar.
|
||||
const pinnedLocal: Worktree = { ...worktree, isPinned: true }
|
||||
const pinnedRemote: Worktree = { ...remoteWorktree, isPinned: true }
|
||||
const localOnly: Worktree = { ...worktree, id: 'wt-local-2', displayName: 'local-2' }
|
||||
const rows = itemRows(buildPinnedRows([pinnedLocal, pinnedRemote, localOnly]))
|
||||
|
||||
expect(rows).toEqual([
|
||||
{ id: pinnedLocal.id, sectionKey: 'pinned', hostContextLabel: LOCAL_HOST_LABEL },
|
||||
{ id: pinnedRemote.id, sectionKey: 'pinned', hostContextLabel: 'gpu-vm' },
|
||||
{ id: localOnly.id, sectionKey: 'repo:repo-1', hostContextLabel: LOCAL_HOST_LABEL }
|
||||
])
|
||||
})
|
||||
|
||||
it('labels both copies when pinned worktrees also show in their groups', () => {
|
||||
const pinnedRemote: Worktree = { ...remoteWorktree, isPinned: true }
|
||||
const rows = itemRows(buildPinnedRows([worktree, pinnedRemote], 'repo', true))
|
||||
|
||||
expect(rows.filter((row) => row.id === pinnedRemote.id)).toEqual([
|
||||
{ id: pinnedRemote.id, sectionKey: 'pinned', hostContextLabel: 'gpu-vm' },
|
||||
{ id: pinnedRemote.id, sectionKey: 'repo:repo-remote', hostContextLabel: 'gpu-vm' }
|
||||
])
|
||||
})
|
||||
|
||||
it('draws no badge on a single-host sidebar even with a pinned row', () => {
|
||||
const pinnedLocal: Worktree = { ...worktree, isPinned: true }
|
||||
const localOnly: Worktree = { ...worktree, id: 'wt-local-2', displayName: 'local-2' }
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getTaskSourceAvailabilityNotice,
|
||||
getTaskSourceContextSummary
|
||||
} from './task-source-context-summary'
|
||||
import { getExecutionHostLabel } from '../../../shared/execution-host'
|
||||
|
||||
const LOCAL_HOST_LABEL = getExecutionHostLabel('local')
|
||||
|
||||
describe('task source context summary', () => {
|
||||
it('shows provider, host, and provider identity for a single repo-backed source', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 1,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'ssh:devbox',
|
||||
projectHostSetupId: 'setup-1',
|
||||
repoId: 'repo-1',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe('GitHub · devbox · stablyai/orca')
|
||||
expect(summary.title).toBe('GitHub · Host: devbox · Source: stablyai/orca')
|
||||
})
|
||||
|
||||
it('shows repo-backed provider account labels when accounts can differ by host', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 2,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'local',
|
||||
projectHostSetupId: 'setup-local',
|
||||
repoId: 'repo-local',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' },
|
||||
accountLabel: 'personal-gh'
|
||||
},
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'ssh:builder',
|
||||
projectHostSetupId: 'setup-builder',
|
||||
repoId: 'repo-builder',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' },
|
||||
accountLabel: 'work-gh'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe(`GitHub · ${LOCAL_HOST_LABEL}, builder · personal-gh, work-gh`)
|
||||
expect(summary.title).toBe(
|
||||
`GitHub · Host: ${LOCAL_HOST_LABEL}, builder · Account: personal-gh, work-gh · Source: stablyai/orca · 2 selected projects`
|
||||
)
|
||||
})
|
||||
|
||||
it('shows disconnected source-host availability for a single SSH repo source', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 1,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'ssh:devbox',
|
||||
repoId: 'repo-1',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
],
|
||||
hostAvailability: [{ hostId: 'ssh:devbox', status: 'disconnected' }]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe('GitHub · devbox · disconnected · stablyai/orca')
|
||||
expect(summary.title).toBe(
|
||||
'GitHub · Host: devbox · Availability: devbox disconnected · Source: stablyai/orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('summarizes multiple unavailable source hosts without cluttering the label', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 2,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'project-a',
|
||||
hostId: 'ssh:devbox',
|
||||
repoId: 'repo-a'
|
||||
},
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'project-b',
|
||||
hostId: 'ssh:buildbox',
|
||||
repoId: 'repo-b'
|
||||
}
|
||||
],
|
||||
hostAvailability: [
|
||||
{ hostId: 'ssh:devbox', status: 'auth-failed' },
|
||||
{ hostId: 'ssh:buildbox', status: 'reconnecting' }
|
||||
]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe('GitHub · devbox, buildbox · 2 unavailable · 2 projects')
|
||||
expect(summary.title).toBe(
|
||||
'GitHub · Host: devbox, buildbox · Availability: devbox auth needed, buildbox connecting · 2 selected projects'
|
||||
)
|
||||
})
|
||||
|
||||
it('summarizes multiple repo-backed hosts without hiding the selected count', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'gitlab',
|
||||
providerLabel: 'GitLab',
|
||||
selectedRepoCount: 3,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'gitlab',
|
||||
projectId: 'project-a',
|
||||
hostId: 'local',
|
||||
repoId: 'repo-a'
|
||||
},
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'gitlab',
|
||||
projectId: 'project-b',
|
||||
hostId: 'ssh:build',
|
||||
repoId: 'repo-b'
|
||||
},
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'gitlab',
|
||||
projectId: 'project-c',
|
||||
hostId: 'runtime:linux',
|
||||
repoId: 'repo-c'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe(`GitLab · ${LOCAL_HOST_LABEL} +2 · 3 projects`)
|
||||
expect(summary.title).toBe(
|
||||
`GitLab · Host: ${LOCAL_HOST_LABEL}, build, linux · 3 selected projects`
|
||||
)
|
||||
})
|
||||
|
||||
it('shows blocked remote-server source-host availability', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 1,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'project-a',
|
||||
hostId: 'runtime:old-server',
|
||||
repoId: 'repo-a',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
],
|
||||
hostAvailability: [{ hostId: 'runtime:old-server', health: 'blocked' }]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe('GitHub · old-server · server update needed · stablyai/orca')
|
||||
expect(summary.title).toBe(
|
||||
'GitHub · Host: old-server · Availability: old-server server update needed · Source: stablyai/orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows remote-server task-source capability checks', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 1,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'project-a',
|
||||
hostId: 'runtime:old-server',
|
||||
repoId: 'repo-a',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
],
|
||||
hostAvailability: [
|
||||
{ hostId: 'runtime:old-server', reason: 'checking-task-source-capability' }
|
||||
]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe('GitHub · old-server · checking server capabilities · stablyai/orca')
|
||||
expect(summary.title).toBe(
|
||||
'GitHub · Host: old-server · Availability: old-server checking server capabilities · Source: stablyai/orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses saved remote server labels in repo-backed source summaries and notices', () => {
|
||||
const hostLabelById = new Map([['runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3', 'dev box']])
|
||||
|
||||
expect(
|
||||
getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 1,
|
||||
hostLabelById,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3',
|
||||
repoId: 'repo-runtime',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
],
|
||||
hostAvailability: [
|
||||
{
|
||||
hostId: 'runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3',
|
||||
health: 'blocked'
|
||||
}
|
||||
]
|
||||
})
|
||||
).toEqual({
|
||||
label: 'GitHub · dev box · server update needed · stablyai/orca',
|
||||
title:
|
||||
'GitHub · Host: dev box · Availability: dev box server update needed · Source: stablyai/orca'
|
||||
})
|
||||
|
||||
expect(
|
||||
getTaskSourceAvailabilityNotice({
|
||||
providerLabel: 'GitHub',
|
||||
sourceCount: 1,
|
||||
hostLabelById,
|
||||
hostAvailability: [
|
||||
{
|
||||
hostId: 'runtime:03ef704c-b180-4b10-998d-e28fbd5de9a3',
|
||||
reason: 'missing-task-source-capability'
|
||||
}
|
||||
]
|
||||
})?.label
|
||||
).toBe('GitHub source unavailable: dev box server update needed for task sources')
|
||||
})
|
||||
|
||||
it('shows remote-server task-source capability version skew', () => {
|
||||
expect(
|
||||
getTaskSourceAvailabilityNotice({
|
||||
providerLabel: 'GitHub',
|
||||
sourceCount: 1,
|
||||
hostAvailability: [
|
||||
{ hostId: 'runtime:old-server', reason: 'missing-task-source-capability' }
|
||||
]
|
||||
})
|
||||
).toEqual({
|
||||
label: 'GitHub source unavailable: old-server server update needed for task sources',
|
||||
title:
|
||||
'Reconnect or update old-server server update needed for task sources to load this source.',
|
||||
blocking: true
|
||||
})
|
||||
})
|
||||
|
||||
it('shows account-backed Linear and Jira sources', () => {
|
||||
expect(
|
||||
getTaskSourceContextSummary({
|
||||
provider: 'linear',
|
||||
providerLabel: 'Linear',
|
||||
accountHostId: 'local',
|
||||
linearWorkspaceName: 'Stably'
|
||||
}).label
|
||||
).toBe(`Linear · ${LOCAL_HOST_LABEL} · Stably`)
|
||||
|
||||
expect(
|
||||
getTaskSourceContextSummary({
|
||||
provider: 'jira',
|
||||
providerLabel: 'Jira',
|
||||
accountHostId: 'runtime:server',
|
||||
jiraSiteName: 'Stably Jira'
|
||||
}).label
|
||||
).toBe('Jira · server · Stably Jira')
|
||||
})
|
||||
|
||||
it('shows account-backed source host availability', () => {
|
||||
const summary = getTaskSourceContextSummary({
|
||||
provider: 'linear',
|
||||
providerLabel: 'Linear',
|
||||
accountHostId: 'runtime:old-server',
|
||||
linearWorkspaceName: 'Stably',
|
||||
hostAvailability: [{ hostId: 'runtime:old-server', health: 'blocked' }]
|
||||
})
|
||||
|
||||
expect(summary.label).toBe('Linear · old-server · server update needed · Stably')
|
||||
expect(summary.title).toBe(
|
||||
'Linear source · Host: old-server · Availability: old-server server update needed · Account: Stably'
|
||||
)
|
||||
})
|
||||
|
||||
it('builds a visible unavailable-source notice from host availability', () => {
|
||||
expect(
|
||||
getTaskSourceAvailabilityNotice({
|
||||
providerLabel: 'GitHub',
|
||||
hostAvailability: [{ hostId: 'ssh:devbox', status: 'auth-failed' }]
|
||||
})
|
||||
).toEqual({
|
||||
label: 'GitHub source unavailable: devbox auth needed',
|
||||
title: 'Reconnect or update devbox auth needed to load this source.',
|
||||
blocking: true
|
||||
})
|
||||
|
||||
expect(
|
||||
getTaskSourceAvailabilityNotice({
|
||||
providerLabel: 'GitLab',
|
||||
sourceCount: 3,
|
||||
hostAvailability: [
|
||||
{ hostId: 'ssh:devbox', status: 'disconnected' },
|
||||
{ hostId: 'runtime:old-server', health: 'blocked' }
|
||||
]
|
||||
})?.label
|
||||
).toBe('Some GitLab source hosts unavailable: 2 source hosts')
|
||||
})
|
||||
|
||||
it('shows provider-specific source availability reasons', () => {
|
||||
expect(
|
||||
getTaskSourceContextSummary({
|
||||
provider: 'github',
|
||||
providerLabel: 'GitHub',
|
||||
selectedRepoCount: 1,
|
||||
repoContexts: [
|
||||
{
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'github:stablyai/orca',
|
||||
hostId: 'ssh:devbox',
|
||||
repoId: 'repo-1',
|
||||
providerIdentity: { provider: 'github', owner: 'stablyai', repo: 'orca' }
|
||||
}
|
||||
],
|
||||
hostAvailability: [{ hostId: 'ssh:devbox', reason: 'missing-provider-auth' }]
|
||||
})
|
||||
).toEqual({
|
||||
label: 'GitHub · devbox · provider auth needed · stablyai/orca',
|
||||
title:
|
||||
'GitHub · Host: devbox · Availability: devbox provider auth needed · Source: stablyai/orca'
|
||||
})
|
||||
|
||||
expect(
|
||||
getTaskSourceAvailabilityNotice({
|
||||
providerLabel: 'GitHub',
|
||||
sourceCount: 3,
|
||||
hostAvailability: [
|
||||
{ hostId: 'ssh:devbox', reason: 'unavailable-source-tool' },
|
||||
{ hostId: 'runtime:linux', reason: 'unsupported-provider' }
|
||||
]
|
||||
})
|
||||
).toEqual({
|
||||
label: 'Some GitHub source hosts unavailable: 2 source hosts',
|
||||
title:
|
||||
'Reconnect or update devbox source tool unavailable, linux provider unsupported on this host to load this source.',
|
||||
blocking: false
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,61 +5,6 @@ import { toRemoteRuntimePtyId } from '../../../../shared/remote-runtime/remote-r
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
describe('copyTerminalHandleForPane', () => {
|
||||
it('copies the runtime terminal handle for a pane key', async () => {
|
||||
const callRuntime = vi.fn().mockResolvedValue({
|
||||
id: 'req-1',
|
||||
ok: true,
|
||||
result: {
|
||||
terminal: {
|
||||
handle: 'term_worker',
|
||||
tabId: 'tab-1',
|
||||
leafId: LEAF_ID,
|
||||
ptyId: 'pty-1'
|
||||
}
|
||||
},
|
||||
_meta: { runtimeId: 'runtime-1' }
|
||||
})
|
||||
const writeClipboardText = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
await expect(
|
||||
copyTerminalHandleForPane({
|
||||
tabId: 'tab-1',
|
||||
leafId: LEAF_ID,
|
||||
callRuntime,
|
||||
writeClipboardText
|
||||
})
|
||||
).resolves.toBe('term_worker')
|
||||
|
||||
expect(callRuntime).toHaveBeenCalledWith({
|
||||
method: 'terminal.resolvePane',
|
||||
params: { paneKey: `tab-1:${LEAF_ID}` }
|
||||
})
|
||||
expect(writeClipboardText).toHaveBeenCalledWith('term_worker')
|
||||
})
|
||||
|
||||
it('surfaces runtime lookup failures without writing the clipboard', async () => {
|
||||
const callRuntime = vi.fn().mockResolvedValue({
|
||||
id: 'req-1',
|
||||
ok: false,
|
||||
error: {
|
||||
code: 'terminal_not_found',
|
||||
message: 'terminal not found'
|
||||
}
|
||||
})
|
||||
const writeClipboardText = vi.fn()
|
||||
|
||||
await expect(
|
||||
copyTerminalHandleForPane({
|
||||
tabId: 'tab-1',
|
||||
leafId: LEAF_ID,
|
||||
callRuntime,
|
||||
writeClipboardText
|
||||
})
|
||||
).rejects.toThrow('terminal not found')
|
||||
|
||||
expect(writeClipboardText).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('copies the owning server handle for a paired-server pane without asking this runtime', async () => {
|
||||
// Why: this desktop's runtime has no record of a server-owned pane (#11664, #15553).
|
||||
const callRuntime = vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { copyTerminalSelection } from './terminal-selection-copy'
|
||||
|
||||
function makeTerminal(selection: string) {
|
||||
return {
|
||||
getSelection: vi.fn(() => selection),
|
||||
clearSelection: vi.fn()
|
||||
}
|
||||
}
|
||||
|
||||
describe('copyTerminalSelection', () => {
|
||||
it('writes selected terminal text to the clipboard', async () => {
|
||||
const terminal = makeTerminal('copilot answer')
|
||||
const writeClipboardText = vi.fn<(text: string) => Promise<void>>().mockResolvedValue()
|
||||
|
||||
await expect(copyTerminalSelection({ terminal, writeClipboardText })).resolves.toBe(true)
|
||||
|
||||
expect(writeClipboardText).toHaveBeenCalledWith('copilot answer')
|
||||
expect(terminal.clearSelection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not claim success for an empty xterm selection', async () => {
|
||||
const terminal = makeTerminal('')
|
||||
const writeClipboardText = vi.fn<(text: string) => Promise<void>>().mockResolvedValue()
|
||||
|
||||
await expect(copyTerminalSelection({ terminal, writeClipboardText })).resolves.toBe(false)
|
||||
|
||||
expect(writeClipboardText).not.toHaveBeenCalled()
|
||||
expect(terminal.clearSelection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears xterm selection only after the clipboard write succeeds', async () => {
|
||||
const terminal = makeTerminal('copilot answer')
|
||||
const writeClipboardText = vi
|
||||
.fn<(text: string) => Promise<void>>()
|
||||
.mockRejectedValue(new Error('clipboard unchanged'))
|
||||
|
||||
await expect(
|
||||
copyTerminalSelection({ terminal, writeClipboardText, clearSelectionOnSuccess: true })
|
||||
).rejects.toThrow('clipboard unchanged')
|
||||
|
||||
expect(terminal.clearSelection).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('clears the xterm selection after a successful write when requested', async () => {
|
||||
const terminal = makeTerminal('copilot answer')
|
||||
const writeClipboardText = vi.fn<(text: string) => Promise<void>>().mockResolvedValue()
|
||||
|
||||
await expect(
|
||||
copyTerminalSelection({ terminal, writeClipboardText, clearSelectionOnSuccess: true })
|
||||
).resolves.toBe(true)
|
||||
|
||||
expect(terminal.clearSelection).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user