diff --git a/config/flaky-test-pattern-baseline.txt b/config/flaky-test-pattern-baseline.txt index 9c9f69c01f6..8d78d5cd1df 100644 --- a/config/flaky-test-pattern-baseline.txt +++ b/config/flaky-test-pattern-baseline.txt @@ -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 diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index b3f13546cde..ea0f482d350 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -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, diff --git a/src/renderer/src/components/editor/selection-copy.test.ts b/src/renderer/src/components/editor/selection-copy.test.ts deleted file mode 100644 index d658cfa7980..00000000000 --- a/src/renderer/src/components/editor/selection-copy.test.ts +++ /dev/null @@ -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() - }) -}) diff --git a/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts b/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts index 8ff03642c5e..f3eef0e8afd 100644 --- a/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts +++ b/src/renderer/src/components/right-sidebar/commit-failure-summary.test.ts @@ -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) - }) }) diff --git a/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts b/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts index 2e3334b7e11..9a40cbd66eb 100644 --- a/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts +++ b/src/renderer/src/components/sidebar/worktree-card-agent-summary.test.ts @@ -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): 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>]*> - 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 = { diff --git a/src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts deleted file mode 100644 index 55aaa9f98a2..00000000000 --- a/src/renderer/src/components/sidebar/worktree-list-groups-host-labels.test.ts +++ /dev/null @@ -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') - }) -}) diff --git a/src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts index 857f61bd8da..9d9ac0c0a7d 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups-notice-host-labels.test.ts @@ -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({ diff --git a/src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts b/src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts index 1804f7679b6..408bfd975a5 100644 --- a/src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts +++ b/src/renderer/src/components/sidebar/worktree-list-groups-pinned-host-labels.test.ts @@ -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' } diff --git a/src/renderer/src/components/task-source-context-summary.test.ts b/src/renderer/src/components/task-source-context-summary.test.ts deleted file mode 100644 index 69cf53cbb8f..00000000000 --- a/src/renderer/src/components/task-source-context-summary.test.ts +++ /dev/null @@ -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 - }) - }) -}) diff --git a/src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts b/src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts index f687dd0c718..4c973d22a58 100644 --- a/src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts +++ b/src/renderer/src/components/terminal-pane/terminal-handle-copy.test.ts @@ -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({ diff --git a/src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts b/src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts deleted file mode 100644 index 91c561c6e76..00000000000 --- a/src/renderer/src/components/terminal-pane/terminal-selection-copy.test.ts +++ /dev/null @@ -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>().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>().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>() - .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>().mockResolvedValue() - - await expect( - copyTerminalSelection({ terminal, writeClipboardText, clearSelectionOnSuccess: true }) - ).resolves.toBe(true) - - expect(terminal.clearSelection).toHaveBeenCalledOnce() - }) -})