From 319a01aacda463e1bbbc92977901da690740b2df Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 4 Nov 2026 22:07:26 -0800 Subject: [PATCH] test: trim project view inventories and duplicate dialog cases (#38399) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- config/scripts/ci-shard-timings.json | 1 - .../github-item-dialog.task-record.test.tsx | 39 +- .../components/github-pr-merge-state.test.ts | 288 ----------- .../github-project/ProjectRoadmap.test.tsx | 221 +-------- .../github-project/group-sort.test.ts | 223 --------- .../project-row-filtering.test.ts | 178 +------ .../linear-integration-card.test.tsx | 115 +---- .../jira-issue-workspace-image-owner.test.tsx | 29 -- ...-issue-workspace-request-lifetime.test.tsx | 26 - ...-issue-attribute-filter-dropdowns.test.tsx | 452 +----------------- .../native-chat-resume-bulk-progress.test.tsx | 49 +- 11 files changed, 9 insertions(+), 1612 deletions(-) delete mode 100644 src/renderer/src/components/github-pr-merge-state.test.ts diff --git a/config/scripts/ci-shard-timings.json b/config/scripts/ci-shard-timings.json index 035297f2833..e0b04bde0a1 100644 --- a/config/scripts/ci-shard-timings.json +++ b/config/scripts/ci-shard-timings.json @@ -7227,7 +7227,6 @@ "src/renderer/src/components/github-item-dialog/load-item-details/use-github-task-refresh.test.tsx": 1070, "src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog.task-record.test.tsx": 1391, "src/renderer/src/components/github-link-copy-state.test.ts": 41, - "src/renderer/src/components/github-pr-merge-state.test.ts": 243, "src/renderer/src/components/github-project/GhAuthErrorHelp.test.ts": 479, "src/renderer/src/components/github-project/ProjectBoard.test.tsx": 988, "src/renderer/src/components/github-project/ProjectRoadmap.test.tsx": 2369, diff --git a/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog.task-record.test.tsx b/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog.task-record.test.tsx index f973d03bc05..44d44b31b8d 100644 --- a/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog.task-record.test.tsx +++ b/src/renderer/src/components/github-item-dialog/open-dialog/github-item-dialog.task-record.test.tsx @@ -13,7 +13,7 @@ import type { AppState } from '@/store/types' import type { Repo } from '../../../../../shared/repo-types' import type { GitHubWorkItem } from '../../../../../shared/github/work-item-types' import { buildTaskSourceContextFromRepo } from '../../../../../shared/task-source-context' -import { useGitHubTaskItems } from '@/hooks/useGitHubTaskItems' + import { useTaskPageGitHubWorkItemMutation } from '@/hooks/useTaskPageGitHubWorkItemMutation' import { parseTaskQuery } from '../../../../../shared/task-query' import type { GitHubProjectTable } from '../../../../../shared/github/project-types' @@ -220,10 +220,7 @@ function renderDialog() { ) }) } -function TasksRow({ rows }: { rows: GitHubWorkItem[] }) { - const [projected] = useGitHubTaskItems(rows) - return {projected.assignees?.map((u) => u.login).join(',')} -} + const text = (field: string) => node.querySelector(`[data-field="${field}"]`)?.textContent async function click(label: string) { const button = [...node.querySelectorAll('button')].find((b) => b.textContent === label) @@ -262,38 +259,6 @@ describe('issue dialog edits reach the Tasks row through the shared record', () node.remove() }) - it('assigns in the dialog, keeps it through a stale search, and starts with the item', async () => { - const rows = answered(await mocks.store!.getState().fetchWorkItems(repo.id, repo.path, 20, '')) - await act(async () => { - root.render( - <> - - {}} - /> - - ) - }) - await click('Assign me') - await act(async () => { - await Promise.resolve() - }) - expect(mocks.update).toHaveBeenCalledOnce() - expect(text('tasks')).toBe('alice') - expect(text('dialog')).toBe('alice') - const stale = answered( - await mocks.store!.getState().fetchWorkItems(repo.id, repo.path, 20, '', { force: true }) - ) - expect(stale[0].assignees?.map((u) => u.login)).toEqual(['alice']) - await click('Start') - expect(mocks.onUse).toHaveBeenCalledWith(expect.objectContaining({ id: row.id })) - }) - it.each([ ['tasks', false], ['tasks', true], diff --git a/src/renderer/src/components/github-pr-merge-state.test.ts b/src/renderer/src/components/github-pr-merge-state.test.ts deleted file mode 100644 index dfc8a6be064..00000000000 --- a/src/renderer/src/components/github-pr-merge-state.test.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { summarizeProviderChecks } from '../../../shared/source-control/checks/provider-check-summary' -import { describe, expect, it } from 'vitest' -import type { GitHubPRMergeStateInput } from '../../../shared/github/pull-request-merge-state' -import { presentGitHubPRMergeState } from './github-pr-merge-state' - -function pr(overrides: Partial = {}): GitHubPRMergeStateInput { - return { - state: 'open', - mergeable: 'MERGEABLE', - mergeStateStatus: 'CLEAN', - checksSummary: { state: 'success', total: 1, passed: 1, failed: 0, pending: 0, neutral: 0 }, - autoMergeAllowed: true, - ...overrides - } -} - -describe('presentGitHubPRMergeState', () => { - it('blocks direct merge when approval is required or changes are requested', () => { - expect(presentGitHubPRMergeState(pr({ reviewDecision: 'REVIEW_REQUIRED' }))).toMatchObject({ - label: 'Approval required', - directMergeAvailable: false - }) - expect(presentGitHubPRMergeState(pr({ reviewDecision: 'CHANGES_REQUESTED' }))).toMatchObject({ - label: 'Changes requested', - directMergeAvailable: false - }) - }) - - it('uses the merge-queue label for auto-merge when a queue is required', () => { - expect(presentGitHubPRMergeState(pr({ mergeQueueRequired: true }))).toMatchObject({ - label: 'Merge when ready', - directMergeAvailable: false, - autoMergeAction: { kind: 'enable', label: 'Merge when ready' } - }) - }) - - it('blocks conflicts on merge-queue branches', () => { - expect( - presentGitHubPRMergeState( - pr({ mergeQueueRequired: true, mergeable: 'UNKNOWN', mergeStateStatus: 'DIRTY' }) - ) - ).toMatchObject({ - label: 'Conflicts', - directMergeAvailable: false, - autoMergeAction: null - }) - }) - - it('offers enable auto-merge for open PRs that are waiting on requirements', () => { - expect(presentGitHubPRMergeState(pr({ mergeQueueRequired: null })).autoMergeAction).toBeNull() - expect( - presentGitHubPRMergeState( - pr({ reviewDecision: 'REVIEW_REQUIRED', mergeStateStatus: 'BLOCKED' }) - ).autoMergeAction - ).toMatchObject({ kind: 'enable', label: 'Enable auto-merge' }) - expect( - presentGitHubPRMergeState( - pr({ - mergeStateStatus: 'BLOCKED', - checksSummary: { - state: 'pending', - total: 1, - passed: 0, - failed: 0, - pending: 1, - neutral: 0 - } - }) - ).autoMergeAction - ).toMatchObject({ kind: 'enable', label: 'Enable auto-merge' }) - }) - - it('does not offer enable auto-merge when only optional checks are pending', () => { - expect( - presentGitHubPRMergeState( - pr({ - checksSummary: { - state: 'pending', - total: 1, - passed: 0, - failed: 0, - pending: 1, - neutral: 0 - } - }) - ) - ).toMatchObject({ - label: 'Checks pending', - directMergeAvailable: true, - autoMergeAction: null - }) - }) - - it.each(['UNSTABLE', 'HAS_HOOKS'])( - 'allows a direct merge for %s even before conflict detection finishes', - (mergeStateStatus) => { - expect( - presentGitHubPRMergeState( - pr({ - mergeable: 'UNKNOWN', - mergeStateStatus, - checksSummary: { - state: 'failure', - total: 1, - passed: 0, - failed: 1, - pending: 0, - neutral: 0 - } - }) - ) - ).toMatchObject({ - label: 'Checks failed', - directMergeAvailable: true, - autoMergeAction: null - }) - } - ) - - it('does not offer enable auto-merge on conflicting PRs (GitHub would reject it)', () => { - expect( - presentGitHubPRMergeState(pr({ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' })) - .autoMergeAction - ).toBeNull() - expect( - presentGitHubPRMergeState(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'DIRTY' })) - .autoMergeAction - ).toBeNull() - }) - - it('does not offer enable auto-merge when GitHub reports the repository disallows it', () => { - expect( - presentGitHubPRMergeState( - pr({ autoMergeAllowed: false, mergeable: 'UNKNOWN', mergeStateStatus: 'BLOCKED' }) - ).autoMergeAction - ).toBeNull() - expect( - presentGitHubPRMergeState( - pr({ autoMergeAllowed: undefined, mergeable: 'UNKNOWN', mergeStateStatus: 'BLOCKED' }) - ).autoMergeAction - ).toMatchObject({ kind: 'enable', label: 'Enable auto-merge' }) - }) - - it('offers disable auto-merge when GitHub reports auto-merge is already enabled', () => { - expect(presentGitHubPRMergeState(pr({ autoMergeEnabled: true }))).toMatchObject({ - autoMergeAction: { kind: 'disable', label: 'Disable auto-merge' } - }) - }) - - it('suppresses enable auto-merge but keeps disable when direct merge is available', () => { - // GitHub rejects enabling auto-merge on directly mergeable PRs with a clean-status error. - expect( - presentGitHubPRMergeState({ - state: 'open', - checksSummary: { state: 'success', total: 1, passed: 1, failed: 0, pending: 0, neutral: 0 } - }) - ).toMatchObject({ label: 'Checks passed', directMergeAvailable: true, autoMergeAction: null }) - expect(presentGitHubPRMergeState(pr())).toMatchObject({ - directMergeAvailable: true, - autoMergeAction: null - }) - expect(presentGitHubPRMergeState(pr({ autoMergeEnabled: true })).autoMergeAction).toMatchObject( - { kind: 'disable' } - ) - }) - - it('blocks conflicts and behind branches, but not optional aggregate check failures', () => { - expect( - presentGitHubPRMergeState(pr({ mergeable: 'CONFLICTING', mergeStateStatus: 'DIRTY' })) - ).toMatchObject({ label: 'Conflicts', directMergeAvailable: false }) - expect( - presentGitHubPRMergeState(pr({ mergeable: 'UNKNOWN', mergeStateStatus: 'DIRTY' })) - ).toMatchObject({ label: 'Conflicts', directMergeAvailable: false }) - expect(presentGitHubPRMergeState(pr({ mergeStateStatus: 'BEHIND' }))).toMatchObject({ - label: 'Behind', - directMergeAvailable: false - }) - expect( - presentGitHubPRMergeState( - pr({ - checksSummary: { - state: 'pending', - total: 1, - passed: 0, - failed: 0, - pending: 1, - neutral: 0 - } - }) - ) - ).toMatchObject({ label: 'Checks pending', directMergeAvailable: true }) - expect( - presentGitHubPRMergeState( - pr({ - checksSummary: { - state: 'failure', - total: 1, - passed: 0, - failed: 1, - pending: 0, - neutral: 0 - } - }) - ) - ).toMatchObject({ label: 'Checks failed', directMergeAvailable: true }) - expect(presentGitHubPRMergeState(pr())).toMatchObject({ - label: 'Able to merge', - directMergeAvailable: true - }) - }) - - it('labels unresolved GitHub mergeability as checking', () => { - expect( - presentGitHubPRMergeState( - pr({ - mergeable: 'UNKNOWN', - mergeStateStatus: null, - checksSummary: { - state: 'pending', - total: 1, - passed: 0, - failed: 0, - pending: 1, - neutral: 0 - } - }) - ) - ).toMatchObject({ - label: 'Checking', - directMergeAvailable: false - }) - }) - - it('allows direct merge when GitHub mergeability is unavailable but checks have passed', () => { - expect( - presentGitHubPRMergeState({ - state: 'open', - checksSummary: { state: 'success', total: 3, passed: 3, failed: 0, pending: 0, neutral: 0 } - }) - ).toMatchObject({ - label: 'Checks passed', - directMergeAvailable: true - }) - expect( - presentGitHubPRMergeState( - pr({ - mergeable: 'UNKNOWN', - mergeStateStatus: null, - checksSummary: { - state: 'success', - total: 3, - passed: 3, - failed: 0, - pending: 0, - neutral: 0 - } - }) - ) - ).toMatchObject({ - label: 'Checks passed', - directMergeAvailable: true - }) - }) - - it('suppresses auto-merge actions for non-open PR states', () => { - expect( - presentGitHubPRMergeState(pr({ state: 'closed', mergeQueueRequired: true })).autoMergeAction - ).toBeNull() - expect( - presentGitHubPRMergeState(pr({ state: 'merged', autoMergeEnabled: true })).autoMergeAction - ).toBeNull() - expect( - presentGitHubPRMergeState(pr({ state: 'draft', mergeQueueRequired: true })).autoMergeAction - ).toBeNull() - }) - it('names cancelled checks accurately without changing the provider merge permission', () => { - const checksSummary = summarizeProviderChecks([ - { status: 'completed', conclusion: 'success' }, - { status: 'completed', conclusion: 'cancelled' } - ]) - expect(presentGitHubPRMergeState(pr({ checksStatus: 'failure', checksSummary }))).toMatchObject( - { - label: 'Cancelled', - directMergeAvailable: true - } - ) - }) -}) diff --git a/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx b/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx index 2a7585f33c6..f4b04c435f4 100644 --- a/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx +++ b/src/renderer/src/components/github-project/ProjectRoadmap.test.tsx @@ -1,12 +1,12 @@ // @vitest-environment happy-dom import type { ReactNode } from 'react' -import { act, cleanup, fireEvent, render, screen } from '@testing-library/react' +import { act, cleanup, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import ProjectRoadmap from './ProjectRoadmap' import { i18n, loadRendererUiLanguage, setRendererPluginLanguagePacks } from '@/i18n/i18n' import { DEFAULT_LOCALE } from '@/i18n/supported-languages' -import { pluginLanguageResourceId } from '../../../../shared/plugins/artifacts/plugin-language-pack-artifact' + import type { GitHubProjectField, GitHubProjectFieldValue, @@ -32,12 +32,6 @@ const TARGET_FIELD: GitHubProjectField = { name: 'Target date', dataType: 'DATE' } -const TITLE_FIELD: GitHubProjectField = { - kind: 'field', - id: 'f_title', - name: 'Title', - dataType: 'TITLE' -} function row(id: string, title: string, values: GitHubProjectFieldValue[]): GitHubProjectRow { const fieldValuesByFieldId: Record = {} @@ -106,56 +100,6 @@ afterEach(async () => { }) describe('ProjectRoadmap', () => { - it.each([ - ['pt-BR', 'pt-BR'], - ['en-US-US', DEFAULT_LOCALE] - ])('renders date ticks and bars with plugin locale %s', async (packLocale, dateLocale) => { - const packId = 'plugin:stablyai.orca-portuguese/pt-BR' as const - const resourceLanguage = pluginLanguageResourceId(packId) - setRendererPluginLanguagePacks([ - { - id: packId, - resourceLanguage, - pluginKey: 'stablyai.orca-portuguese', - locale: packLocale, - catalog: { common: { cancel: 'Cancelar' } }, - coverage: 100 - } - ]) - await i18n.changeLanguage(await loadRendererUiLanguage(packId)) - expect(i18n.resolvedLanguage).toBe(resourceLanguage) - - render( - list} - /> - ) - - const month = new Intl.DateTimeFormat(dateLocale, { - month: 'short', - timeZone: 'UTC' - }).format(new Date(Date.UTC(2026, 8, 1))) - expect(screen.getAllByText(month, { exact: false }).length).toBeGreaterThan(0) - const dateFormat = new Intl.DateTimeFormat(dateLocale, { - year: 'numeric', - month: 'short', - day: 'numeric', - timeZone: 'UTC' - }) - const start = dateFormat.format(new Date(Date.UTC(2026, 8, 1))) - const end = dateFormat.format(new Date(Date.UTC(2026, 9, 15))) - expect(screen.getByRole('button', { name: `Scheduled — ${start} – ${end}` })).toBeTruthy() - }) - it('moves the today marker across local midnight without resetting scroll and cleans up its timer', () => { vi.useFakeTimers() vi.setSystemTime(new Date(2026, 8, 5, 23, 59, 59)) @@ -179,165 +123,4 @@ describe('ProjectRoadmap', () => { unmount() expect(vi.getTimerCount()).toBe(0) }) - - it.each([false, true])( - 'centers when an initially empty view gains dated rows (fields hidden: %s)', - (hidden) => { - vi.useFakeTimers() - vi.setSystemTime(new Date(2026, 8, 5, 12)) - const fields = hidden ? [TITLE_FIELD] : [TITLE_FIELD, START_FIELD, TARGET_FIELD] - const { rerender } = render( - list} /> - ) - const populated = table(fields, [ - row('one', 'Arrived', [ - { kind: 'date', fieldId: 'f_start', date: '2026-01-01' }, - { kind: 'date', fieldId: 'f_end', date: '2026-09-10' } - ]) - ]) - rerender(list} />) - const scroller = screen.getByTestId('project-roadmap-scroller') - expect(scroller.scrollLeft).toBeGreaterThan(1000) - scroller.scrollLeft = 123 - rerender( - list} - /> - ) - expect(scroller.scrollLeft).toBe(123) - fireEvent.click(screen.getByRole('button', { name: 'Year' })) - expect(scroller.scrollLeft).not.toBe(123) - expect(window.localStorage.getItem('orca.githubProject.roadmapZoom')).toBe('year') - } - ) - - it('places a dated row on the timeline and names the fields driving it', () => { - render( - list} - /> - ) - expect(screen.getByText('Placed by Start date → Target date')).toBeTruthy() - expect(screen.getByLabelText(/^Ship the thing — /)).toBeTruthy() - expect(screen.queryByText('list')).toBeNull() - }) - - it('keeps an undated row in place and flags it rather than hiding it', () => { - render( - list} - /> - ) - expect(screen.getByText('No dates')).toBeTruthy() - expect(screen.getByText('1 without dates')).toBeTruthy() - expect(screen.queryByLabelText(/^Undated — /)).toBeNull() - }) - - it('opens the row dialog when a bar is clicked', () => { - const onOpenDialog = vi.fn() - render( - list} - /> - ) - fireEvent.click(screen.getByLabelText(/^Ship the thing — /)) - expect(onOpenDialog).toHaveBeenCalledTimes(1) - expect(onOpenDialog.mock.calls[0]?.[0]).toMatchObject({ id: 'PVTI_1' }) - }) - - it('places items from row-carried dates when the view hides its date fields', () => { - render( - list} - /> - ) - expect(screen.getByText('Placed by Start date → Target date')).toBeTruthy() - expect(screen.getByLabelText(/^Hidden-field item — /)).toBeTruthy() - expect(screen.queryByText('list')).toBeNull() - }) - - it('announces restricted items by name in the bar label', () => { - const redacted: GitHubProjectRow = { - ...row('PVTI_9', '', [ - { kind: 'date', fieldId: 'f_start', date: '2026-03-02' }, - { kind: 'date', fieldId: 'f_end', date: '2026-03-05' } - ]), - itemType: 'REDACTED' - } - render( - list} - /> - ) - expect(screen.getByLabelText(/^Restricted item — /)).toBeTruthy() - }) - - it('falls back to the caller-supplied list when no field can place items', () => { - render(list} />) - expect(screen.getByText('list')).toBeTruthy() - expect( - screen.getByText( - 'This roadmap view has no date or iteration field to place items on, so Orca is listing them instead.' - ) - ).toBeTruthy() - }) - - it('reports an empty filter result instead of drawing an empty grid', () => { - render( - list} - /> - ) - expect(screen.getByText("No items match this view's filter.")).toBeTruthy() - expect(screen.queryByText('list')).toBeNull() - }) - - it('does not blame a filter an unfiltered roadmap does not have', () => { - render( - list} - /> - ) - expect(screen.getByText('This view has no items yet.')).toBeTruthy() - expect(screen.queryByText("No items match this view's filter.")).toBeNull() - }) }) diff --git a/src/renderer/src/components/github-project/group-sort.test.ts b/src/renderer/src/components/github-project/group-sort.test.ts index 96d2c6c6c3f..1bd86e19e60 100644 --- a/src/renderer/src/components/github-project/group-sort.test.ts +++ b/src/renderer/src/components/github-project/group-sort.test.ts @@ -19,17 +19,6 @@ const singleSelectField: GitHubProjectField = { ] } -const iterationField: GitHubProjectField = { - kind: 'iteration', - id: 'F_iter', - name: 'Iteration', - dataType: 'ITERATION', - iterations: [ - { id: 'iter_1', title: 'Sprint 1', startDate: '2026-01-01', duration: 14, completed: false }, - { id: 'iter_2', title: 'Sprint 2', startDate: '2026-01-15', duration: 14, completed: false } - ] -} - const assigneesField: GitHubProjectField = { kind: 'field', id: 'F_assignees', @@ -37,20 +26,6 @@ const assigneesField: GitHubProjectField = { dataType: 'ASSIGNEES' } -const labelsField: GitHubProjectField = { - kind: 'field', - id: 'F_labels', - name: 'Labels', - dataType: 'LABELS' -} - -const textField: GitHubProjectField = { - kind: 'field', - id: 'F_text', - name: 'Notes', - dataType: 'TEXT' -} - const numberField: GitHubProjectField = { kind: 'field', id: 'F_points', @@ -117,35 +92,6 @@ function makeTable(view: GitHubProjectView, rows: GitHubProjectRow[]): GitHubPro } describe('sortRows', () => { - it('orders rows by single-select option order', () => { - const view = makeView(singleSelectField, { - direction: 'ASC', - field: singleSelectField - }) - const rows = [ - makeRow('r2', 1, { - F_status: { - kind: 'single-select', - fieldId: 'F_status', - optionId: 'opt_b', - name: 'In Progress', - color: 'YELLOW' - } - }), - makeRow('r1', 0, { - F_status: { - kind: 'single-select', - fieldId: 'F_status', - optionId: 'opt_a', - name: 'Todo', - color: 'GRAY' - } - }) - ] - const sorted = sortRows(makeTable(view, rows), rows) - expect(sorted.map((r) => r.id)).toEqual(['r1', 'r2']) - }) - it('does not produce NaN when both rows reference unknown single-select options', () => { // Avoid Infinity - Infinity producing NaN and bypassing the position tie-break for orphaned options. const view = makeView(singleSelectField, { @@ -176,58 +122,6 @@ describe('sortRows', () => { expect(sorted.map((r) => r.id)).toEqual(['rA', 'rB']) }) - it('does not produce NaN when both rows reference unknown iteration ids', () => { - const view = makeView(iterationField, { - direction: 'ASC', - field: iterationField - }) - const rows = [ - makeRow('rB', 5, { - F_iter: { - kind: 'iteration', - fieldId: 'F_iter', - iterationId: 'gone_b', - title: 'Gone B', - startDate: '2025-01-01', - duration: 14 - } - }), - makeRow('rA', 1, { - F_iter: { - kind: 'iteration', - fieldId: 'F_iter', - iterationId: 'gone_a', - title: 'Gone A', - startDate: '2025-01-01', - duration: 14 - } - }) - ] - const sorted = sortRows(makeTable(view, rows), rows) - expect(sorted.map((r) => r.id)).toEqual(['rA', 'rB']) - }) - - it('places rows missing the sort field after rows that have it', () => { - const view = makeView(singleSelectField, { - direction: 'ASC', - field: singleSelectField - }) - const rows = [ - makeRow('rEmpty', 0, {}), - makeRow('rHas', 1, { - F_status: { - kind: 'single-select', - fieldId: 'F_status', - optionId: 'opt_a', - name: 'Todo', - color: 'GRAY' - } - }) - ] - const sorted = sortRows(makeTable(view, rows), rows) - expect(sorted.map((r) => r.id)).toEqual(['rHas', 'rEmpty']) - }) - it('sorts an empty user list last in both directions, like a missing value', () => { const rows = [ makeRow('empty-list', 0, { @@ -249,55 +143,6 @@ describe('sortRows', () => { expect(sorted.map((r) => r.id)).toEqual(['alice', 'empty-list', 'no-value']) } }) - - it('sorts an empty label list last in both directions, like a missing value', () => { - const rows = [ - makeRow('empty-list', 0, { - F_labels: { kind: 'labels', fieldId: 'F_labels', labels: [] } - }), - makeRow('bug', 1, { - F_labels: { - kind: 'labels', - fieldId: 'F_labels', - labels: [{ name: 'bug', color: 'ff0000' }] - } - }), - makeRow('no-value', 2, {}) - ] - - for (const direction of ['ASC', 'DESC'] as const) { - const view = makeView(labelsField, { direction, field: labelsField }) - const sorted = sortRows(makeTable(view, rows), rows) - expect(sorted.map((r) => r.id)).toEqual(['bug', 'empty-list', 'no-value']) - } - }) - - it('sorts a blank text value last in both directions, like a missing value', () => { - // Why reachable: the normalizer turns a null GitHub text/date into ''. - const rows = [ - makeRow('blank', 0, { F_text: { kind: 'text', fieldId: 'F_text', text: '' } }), - makeRow('alpha', 1, { F_text: { kind: 'text', fieldId: 'F_text', text: 'alpha' } }), - makeRow('no-value', 2, {}) - ] - - for (const direction of ['ASC', 'DESC'] as const) { - const view = makeView(textField, { direction, field: textField }) - const sorted = sortRows(makeTable(view, rows), rows) - expect(sorted.map((r) => r.id)).toEqual(['alpha', 'blank', 'no-value']) - } - }) - - it('keeps sort fallback finite when row positions are absent', () => { - const view = makeView(singleSelectField) - const rows = [ - { ...makeRow('rA', 0, {}), position: undefined as unknown as number }, - { ...makeRow('rB', 0, {}), position: undefined as unknown as number } - ] - - const sorted = sortRows(makeTable(view, rows), rows) - - expect(sorted.map((r) => r.id)).toEqual(['rA', 'rB']) - }) }) describe('groupRows', () => { @@ -337,74 +182,6 @@ describe('groupRows', () => { expect(groups.map((group) => group.label)).toEqual(['-10', '-2', '1', '2', '8', '10']) }) - - it('places the empty group last', () => { - const view = { - ...makeView(singleSelectField), - groupByFields: [singleSelectField] - } - const rows = [ - makeRow('rNone', 0, {}), - makeRow('rA', 1, { - F_status: { - kind: 'single-select', - fieldId: 'F_status', - optionId: 'opt_a', - name: 'Todo', - color: 'GRAY' - } - }) - ] - const groups = groupRows(makeTable(view, rows), rows) - expect(groups.map((g) => g.key)).toEqual(['opt_a', '__empty__']) - }) -}) - -it('indexes field ordering once for grouping and sorting a large project table', () => { - let reads = 0 - const field: GitHubProjectField = { - kind: 'single-select', - id: 'field', - name: 'Status', - dataType: 'SINGLE_SELECT', - options: Array.from({ length: 1000 }, (_, i) => ({ - get id() { - reads++ - return `option-${i}` - }, - name: String(i), - color: 'GRAY' - })) - } - const rows = Array.from({ length: 1000 }, (_, i) => - makeRow(String(i), i, { - field: { - kind: 'single-select', - fieldId: 'field', - optionId: `option-${(i * 173) % 1000}`, - name: String((i * 173) % 1000), - color: 'GRAY' - } - }) - ) - const view = { ...makeView(field, { field, direction: 'ASC' }), groupByFields: [field] } - const table = makeTable(view, rows) - const sorted = sortRows(table, rows) - expect(reads).toBe(1000) - expect( - sorted.map((row) => - Number( - row.fieldValuesByFieldId.field.kind === 'single-select' && - row.fieldValuesByFieldId.field.name - ) - ) - ).toEqual(Array.from({ length: 1000 }, (_, i) => i)) - reads = 0 - const groups = groupRows(table, rows) - expect(reads).toBe(1000) - expect(groups.map((group) => group.key)).toEqual( - Array.from({ length: 1000 }, (_, i) => `option-${i}`) - ) }) it('uses the first iteration ordering and metadata when legacy field IDs repeat', () => { diff --git a/src/renderer/src/components/github-project/project-row-filtering.test.ts b/src/renderer/src/components/github-project/project-row-filtering.test.ts index 790fe544e5f..9bbdcf1f61f 100644 --- a/src/renderer/src/components/github-project/project-row-filtering.test.ts +++ b/src/renderer/src/components/github-project/project-row-filtering.test.ts @@ -1,10 +1,7 @@ import { describe, expect, it, vi } from 'vitest' import type { GitHubProjectRow, GitHubProjectTable } from '../../../../shared/github/project-types' import type { Repo } from '../../../../shared/repo-types' -import { - filterProjectTableRowsBySelectedRepos, - resolveSelectedProjectRowRepo -} from './project-row-filtering' +import { filterProjectTableRowsBySelectedRepos } from './project-row-filtering' function repo(id: string): Repo { return { @@ -125,177 +122,4 @@ describe('filterProjectTableRowsBySelectedRepos', () => { expect(filtered.rows).toEqual([...drafts, ...restricted, issue, pullRequest]) expect(filtered.totalCount).toBe(71) }) - - it('keeps a row when at least one slug match is selected', () => { - const rows = [row('visible', 'acme/orca'), row('hidden', 'acme/tool')] - const filtered = filterProjectTableRowsBySelectedRepos( - table(rows), - (slug) => - originOnly(slug?.toLowerCase() === 'acme/orca' ? [repo('repo-1')] : [repo('repo-2')]), - true, - new Set(['repo-1']) - ) - - expect(filtered.rows.map((r) => r.id)).toEqual(['visible']) - expect(filtered.totalCount).toBe(1) - }) - - it('filters a row when only unselected repos match', () => { - const rows = [row('hidden', 'acme/orca')] - const filtered = filterProjectTableRowsBySelectedRepos( - table(rows), - () => originOnly([repo('repo-2')]), - true, - new Set(['repo-1']) - ) - - expect(filtered.rows).toEqual([]) - expect(filtered.totalCount).toBe(0) - }) - - it('keeps a row whose only selected match is a fork of the row s repo', () => { - const rows = [row('fork-only', 'acme/orca')] - const filtered = filterProjectTableRowsBySelectedRepos( - table(rows), - () => ({ origin: [], upstream: [repo('fork')] }), - true, - new Set(['fork']) - ) - - expect(filtered.rows.map((r) => r.id)).toEqual(['fork-only']) - expect(filtered.totalCount).toBe(1) - }) - - it('keeps a row with multiple selected matches for action ambiguity handling', () => { - const rows = [row('ambiguous', 'acme/orca')] - const filtered = filterProjectTableRowsBySelectedRepos( - table(rows), - () => originOnly([repo('repo-1'), repo('repo-2'), repo('repo-3')]), - true, - new Set(['repo-1', 'repo-2']) - ) - - expect(filtered.rows.map((r) => r.id)).toEqual(['ambiguous']) - }) -}) - -describe('resolveSelectedProjectRowRepo', () => { - it('reports loading without reading stale slug matches', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('loading', 'acme/orca'), - lookupSlugMatches: () => { - throw new Error('should not read stale matches') - }, - slugIndexReady: false, - selectedRepoIds: new Set(['repo-1']) - }) - - expect(resolution.status).toBe('loading') - }) - - it('reports invalid slug for rows without a repository', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('missing-slug', null), - lookupSlugMatches: () => originOnly([repo('repo-1')]), - slugIndexReady: true, - selectedRepoIds: new Set(['repo-1']) - }) - - expect(resolution.status).toBe('invalid_slug') - }) - - it('reports no global match when Orca has no repo for the slug', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('missing', 'acme/orca'), - lookupSlugMatches: () => originOnly([]), - slugIndexReady: true, - selectedRepoIds: new Set(['repo-1']) - }) - - expect(resolution.status).toBe('no_global_match') - }) - - it('reports global-only matches when the repo is not selected', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('unselected', 'acme/orca'), - lookupSlugMatches: () => originOnly([repo('repo-2')]), - slugIndexReady: true, - selectedRepoIds: new Set(['repo-1']) - }) - - expect(resolution.status).toBe('unselected_match') - }) - - it('returns the selected match when exactly one matching repo is selected', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('selected', 'acme/orca'), - lookupSlugMatches: () => originOnly([repo('repo-1'), repo('repo-2')]), - slugIndexReady: true, - selectedRepoIds: new Set(['repo-2']) - }) - - expect(resolution).toMatchObject({ status: 'selected_match', repo: { id: 'repo-2' } }) - }) - - it('passes the project host into repository matching', () => { - const lookupSlugMatches = vi.fn(() => originOnly([repo('repo-1')])) - - expect( - resolveSelectedProjectRowRepo({ - row: row('enterprise', 'acme/orca'), - lookupSlugMatches, - host: 'ghe.example:8443', - slugIndexReady: true, - selectedRepoIds: new Set(['repo-1']) - }) - ).toMatchObject({ status: 'selected_match' }) - expect(lookupSlugMatches).toHaveBeenCalledWith('acme/orca', 'ghe.example:8443') - }) - - it('reports ambiguity when multiple matching repos are selected', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('ambiguous', 'acme/orca'), - lookupSlugMatches: () => originOnly([repo('repo-1'), repo('repo-2')]), - slugIndexReady: true, - selectedRepoIds: new Set(['repo-1', 'repo-2']) - }) - - expect(resolution.status).toBe('ambiguous_selected_match') - }) - - it('falls through to a selected fork when the upstream clone is unselected', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('fork-selected', 'acme/orca'), - lookupSlugMatches: () => ({ origin: [repo('upstream')], upstream: [repo('fork')] }), - slugIndexReady: true, - selectedRepoIds: new Set(['fork']) - }) - - expect(resolution).toMatchObject({ status: 'selected_match', repo: { id: 'fork' } }) - }) - - it('prefers the selected upstream clone over a selected fork of it', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('both-selected', 'acme/orca'), - lookupSlugMatches: () => ({ origin: [repo('upstream')], upstream: [repo('fork')] }), - slugIndexReady: true, - selectedRepoIds: new Set(['upstream', 'fork']) - }) - - expect(resolution).toMatchObject({ status: 'selected_match', repo: { id: 'upstream' } }) - }) - - it('still reports no selection when neither the upstream clone nor the fork is selected', () => { - const resolution = resolveSelectedProjectRowRepo({ - row: row('neither', 'acme/orca'), - lookupSlugMatches: () => ({ origin: [repo('upstream')], upstream: [repo('fork')] }), - slugIndexReady: true, - selectedRepoIds: new Set(['other']) - }) - - expect(resolution).toMatchObject({ - status: 'unselected_match', - globalMatches: [{ id: 'upstream' }, { id: 'fork' }] - }) - }) }) diff --git a/src/renderer/src/components/integrations/task-trackers/linear-integration-card.test.tsx b/src/renderer/src/components/integrations/task-trackers/linear-integration-card.test.tsx index 088da40ceb1..204abd49804 100644 --- a/src/renderer/src/components/integrations/task-trackers/linear-integration-card.test.tsx +++ b/src/renderer/src/components/integrations/task-trackers/linear-integration-card.test.tsx @@ -3,15 +3,12 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { getExecutionHostLabel } from '../../../../../shared/execution-host' -import es from '@/i18n/locales/es.json' -import { i18n, loadRendererUiLanguage, translate } from '@/i18n/i18n' + +import { i18n, loadRendererUiLanguage } from '@/i18n/i18n' import { getProviderRuntimeContextKey } from '@/lib/provider-runtime-context' import { TooltipProvider } from '../../ui/tooltip' import { LinearIntegrationCard } from './linear-integration-card' -const LOCAL_HOST_LABEL = getExecutionHostLabel('local') - type StoreState = { linearStatus: { connected: boolean @@ -126,114 +123,6 @@ describe('LinearIntegrationCard account scope', () => { Reflect.deleteProperty(window, 'api') }) - it.each(['en', 'ko'] as const)( - 'renders complete %s connection counts without an English suffix', - async (language) => { - await i18n.changeLanguage(await loadRendererUiLanguage(language)) - for (const count of [0, 1, 2]) { - const state = installStore(true) - state.linearStatus.workspaces = Array.from({ length: count }, (_, index) => ({ - id: `workspace-${index}`, - organizationName: 'Fixture', - displayName: 'Fixture workspace' - })) - const rendered = await renderCard() - const description = - rendered.querySelector('[data-settings-section="integrations-linear"]')?.textContent ?? '' - const expected = - language === 'ko' - ? `워크스페이스 ${count}개 연결됨` - : `${count} workspace${count === 1 ? '' : 's'} connected` - expect(description).toContain(expected) - expect(description).not.toMatch(/개(?: 사이트| 워크스페이스)?s/) - await act(async () => { - root?.unmount() - }) - root = null - container?.remove() - container = null - } - } - ) - - it('translates a coded connection failure at the display site', async () => { - await i18n.changeLanguage(await loadRendererUiLanguage('es')) - const state = installStore(true) - state.testLinearConnection = vi.fn(async () => ({ - ok: false, - error: 'Wire detail from the host', - errorCode: 'linear.noApiKey' - })) - const rendered = await renderCard() - const label = translate( - 'auto.components.settings.task.tracker.integration.cards.c24e56c532', - 'Test' - ) - const button = Array.from(rendered.querySelectorAll('button')).find( - (button) => button.textContent === label - ) - expect(button).toBeDefined() - await act(async () => { - button?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - expect(rendered.textContent).toContain(es.taskProviders.errors.linear.noApiKey) - expect(rendered.textContent).not.toContain('Wire detail from the host') - }) - - it('shows local-client account ownership when Linear is disconnected', async () => { - const state = installStore(false) - - const rendered = await renderCard() - - expect(rendered.querySelector('[data-settings-section="integrations-linear"]')).not.toBeNull() - expect(rendered.textContent).toContain(`Account scope: ${LOCAL_HOST_LABEL}`) - expect(rendered.textContent).toContain( - 'Credentials and account checks for this provider are owned by this desktop client. To use a server, change Settings > Remote Orca Servers > New projects open on.' - ) - expect(rendered.textContent).toContain('Open Remote Servers') - expect(rendered.textContent).toContain('Add access with a Personal API key') - - await act(async () => { - Array.from(rendered.querySelectorAll('button')) - .find((button) => button.textContent === 'Re-check') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(state.checkLinearConnection).toHaveBeenCalledWith(true) - }) - - it('shows remote-server account ownership and connected workspace rows', async () => { - const state = installStore(true, { activeRuntimeEnvironmentId: 'runtime-1' }) - - const rendered = await renderCard() - - expect(rendered.textContent).toContain('Account scope: Remote server: runtime-1') - expect(rendered.textContent).toContain( - 'Credentials and account checks for this provider are owned by this remote server. To use another host, change Settings > Remote Orca Servers > New projects open on.' - ) - await act(async () => { - Array.from(rendered.querySelectorAll('button')) - .find((button) => button.textContent === 'Open Remote Servers') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - expect(state.openSettingsPage).toHaveBeenCalledTimes(1) - expect(state.openSettingsTarget).toHaveBeenCalledWith({ - pane: 'servers', - repoId: null, - sectionId: 'default-runtime' - }) - expect(rendered.textContent).toContain('Acme') - expect(rendered.textContent).toContain('Acme workspace · linear@example.test') - - await act(async () => { - Array.from(rendered.querySelectorAll('button')) - .find((button) => button.textContent === 'Test') - ?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(state.testLinearConnection).toHaveBeenCalledWith('workspace-1') - }) - it('clears verification state after adding another Linear workspace', async () => { installStore(true) const rendered = await renderCard() diff --git a/src/renderer/src/components/jira-issue-workspace-image-owner.test.tsx b/src/renderer/src/components/jira-issue-workspace-image-owner.test.tsx index 67dca8d9d12..58376048888 100644 --- a/src/renderer/src/components/jira-issue-workspace-image-owner.test.tsx +++ b/src/renderer/src/components/jira-issue-workspace-image-owner.test.tsx @@ -228,19 +228,6 @@ afterEach(async () => { }) describe('Jira issue workspace image ownership', () => { - it('keeps explicit ownership without duplicate reads when the default host changes', async () => { - const selected = issue() - const owner: RuntimeClientTarget = { kind: 'environment', environmentId: 'environment-1' } - await render(selected) - boundary.settings = { activeRuntimeEnvironmentId: 'environment-2' } - await render(selected) - for (const reads of [boundary.issues, boundary.comments, boundary.transitions]) { - expect(reads).toHaveLength(1) - expect(reads[0]?.target).toEqual(owner) - } - expect(text('image-owner')).toBe(JSON.stringify(owner)) - }) - it('fences late detail, comments and transitions when the same issue switches owners', async () => { const selected = issue() await render(selected) @@ -272,20 +259,4 @@ describe('Jira issue workspace image ownership', () => { expect(text('comments')).toBe('new') expect(text('transitions')).toBe('new') }) - - it('uses one account owner for row-less detail, comments, transitions and images', async () => { - await render(issue(), null) - const detail = take(boundary.issues) - const comments = take(boundary.comments) - const transitions = take(boundary.transitions) - expect(detail.target).toEqual({ kind: 'environment', environmentId: 'environment-1' }) - expect(comments.target).toBe(detail.target) - expect(transitions.target).toBe(detail.target) - expect(text('image-owner')).toBe(JSON.stringify(detail.target)) - await act(async () => { - detail.resolve(null) - comments.resolve([]) - transitions.resolve([]) - }) - }) }) diff --git a/src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx b/src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx index 3fdb1aea828..f7f3d4d8ad1 100644 --- a/src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx +++ b/src/renderer/src/components/jira-issue-workspace-request-lifetime.test.tsx @@ -392,32 +392,6 @@ describe('Jira issue workspace request lifetime', () => { expect(text('comments-loading')).toBe('false') }) - it('discards late detail and comments after closing the mounted workspace', async () => { - await render(issue()) - await render(null) - await act(async () => { - take(boundary.issues).resolve({ ...issue(), description: 'Screenshot data'.repeat(32_768) }) - take(boundary.comments).resolve([{ ...comment('late'), body: 'Image data'.repeat(32_768) }]) - }) - expect(container.querySelector('[data-open]')?.getAttribute('data-open')).toBe('false') - expect(container.querySelector('[data-testid="detail"]')).toBeNull() - expect(boundary.composer).toBeNull() - }) - - it('clears an already hydrated issue and comments when it closes', async () => { - await render(issue()) - await act(async () => { - take(boundary.issues).resolve(issue('JIR-1', 'Hydrated')) - take(boundary.comments).resolve([comment('loaded')]) - }) - expect(text('title')).toBe('Hydrated') - expect(text('comments')).toBe('loaded') - expect(text('issue-loading')).toBe('false') - expect(text('comments-loading')).toBe('false') - await render(null) - expect(container.querySelector('[data-testid="detail"]')).toBeNull() - }) - it('ignores old replies and accepts the reopened issue replies', async () => { await render(issue()) const oldIssue = take(boundary.issues) diff --git a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx index ad2cfb30571..768062ad3ca 100644 --- a/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx +++ b/src/renderer/src/components/linear-issue-attribute-filter-dropdowns.test.tsx @@ -4,13 +4,8 @@ import { act } from 'react' import { createRoot, type Root } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { LinearTeam } from '../../../shared/linear/workspace-types' + import { - clearLinearIssueAttributeFacet, - countLinearIssueAttributeFilters, - linearIssueAttributeFilterPillLabels -} from './linear-issue-attribute-filter-pills' -import { - LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_LABEL_IDS, LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS, type LinearIssueAttributeFilter } from '../../../shared/linear/issue-attribute-filter' @@ -61,208 +56,6 @@ afterEach(() => { metadataMocks.useTeamsMembers.mockReset() }) -const sample: LinearIssueAttributeFilter = { - stateIds: ['s1', 's2'], - priorities: [0, 1], - assignee: { kind: 'unassigned' }, - labelIds: ['l1'] -} - -describe('linear-issue-attribute-filter helpers', () => { - it('counts active facets and clears individual facets', () => { - expect(countLinearIssueAttributeFilters(sample)).toBe(4) - expect(clearLinearIssueAttributeFacet(sample, 'status').stateIds).toEqual([]) - expect(clearLinearIssueAttributeFacet(sample, 'priority').priorities).toEqual([]) - expect(clearLinearIssueAttributeFacet(sample, 'assignee').assignee).toBeNull() - expect(clearLinearIssueAttributeFacet(sample, 'labels').labelIds).toEqual([]) - }) - - it('collapses same-named status ids into one pill label', () => { - const pills = linearIssueAttributeFilterPillLabels({ - value: { stateIds: ['be-todo', 'fe-todo'], priorities: [], assignee: null, labelIds: [] }, - stateNamesById: new Map([ - ['be-todo', 'Todo'], - ['fe-todo', 'Todo'] - ]), - memberNamesById: new Map(), - labelNamesById: new Map(), - statusOptions: [{ key: 'be-todo', primary: 'Todo', ids: ['be-todo', 'fe-todo'] }], - labelOptions: [], - statusTruncated: false, - labelsTruncated: false - }) - expect(pills[0]?.value).toBe('Todo') - }) - - it('builds pill labels from metadata maps', () => { - const pills = linearIssueAttributeFilterPillLabels({ - value: sample, - stateNamesById: new Map([ - ['s1', 'Todo'], - ['s2', 'In Progress'] - ]), - memberNamesById: new Map(), - labelNamesById: new Map([['l1', 'Bug']]), - statusOptions: [], - labelOptions: [], - statusTruncated: false, - labelsTruncated: false - }) - expect(pills.map((p) => p.key)).toEqual(['status', 'priority', 'assignee', 'labels']) - expect(pills[0]?.value).toContain('Todo') - expect(pills[2]?.value).toMatch(/Unassigned/i) - expect(pills[3]?.value).toBe('Bug') - }) -}) - -describe('LinearIssueAttributeFilterDropdowns', () => { - it('keeps metadata lazy while closed with only static filter labels', () => { - const team: LinearTeam = { id: 'team-1', name: 'Engineering', key: 'ENG' } - const container = document.createElement('div') - document.body.appendChild(container) - const root = createRoot(container) - roots.push(root) - - act(() => { - root.render( - undefined} - workspaceId="workspace-1" - primaryTeam={team} - selectedTeamIds={[]} - availableTeams={[team]} - teamsSettled - /> - ) - }) - - expect(metadataMocks.useTeamsStates).toHaveBeenCalledWith([], undefined, null) - expect(metadataMocks.useTeamsLabels).toHaveBeenCalledWith([], undefined, null) - expect(metadataMocks.useTeamsMembers).toHaveBeenCalledWith([], undefined, null) - }) - - it('keeps readable metadata names available after the popover closes', () => { - const value: LinearIssueAttributeFilter = { - stateIds: ['state-1'], - priorities: [], - assignee: { kind: 'user', id: 'member-1' }, - labelIds: ['label-1'] - } - const team: LinearTeam = { id: 'team-1', name: 'Engineering', key: 'ENG' } - const container = document.createElement('div') - document.body.appendChild(container) - const root = createRoot(container) - roots.push(root) - - act(() => { - root.render( - undefined} - workspaceId="workspace-1" - primaryTeam={team} - selectedTeamIds={[]} - availableTeams={[team]} - teamsSettled - /> - ) - }) - - expect(container.textContent).toContain('Todo') - expect(container.textContent).toContain('Ada Lovelace') - expect(container.textContent).toContain('Bug') - expect(container.textContent).not.toContain('state-1') - expect(metadataMocks.useTeamsStates).toHaveBeenCalledWith(['team-1'], undefined, 'workspace-1') - expect(metadataMocks.useTeamsLabels).toHaveBeenCalledWith(['team-1'], undefined, 'workspace-1') - expect(metadataMocks.useTeamsMembers).toHaveBeenCalledWith(['team-1'], undefined, 'workspace-1') - }) - - it.each([['all'], [null]])( - 'offers the workspace picker hint instead of filter sections for workspaceId %s', - (workspaceId) => { - const team: LinearTeam = { id: 'team-1', name: 'Engineering', key: 'ENG' } - const container = document.createElement('div') - document.body.appendChild(container) - const root = createRoot(container) - roots.push(root) - - act(() => { - root.render( - undefined} - workspaceId={workspaceId} - primaryTeam={team} - selectedTeamIds={[]} - availableTeams={[team]} - teamsSettled - /> - ) - }) - - const trigger = container.querySelector('button') - act(() => { - trigger?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(document.body.textContent).toContain('Select one workspace') - expect(metadataMocks.useTeamsStates).toHaveBeenCalledWith([], undefined, null) - } - ) - - // Wait for the full team fetch; pruning against issue-scraped teams would permanently discard restored facets. - it('prunes unknown facet ids only once teams settle on the mounted component', () => { - const team: LinearTeam = { id: 'team-1', name: 'Engineering', key: 'ENG' } - const onChange = vi.fn() - const container = document.createElement('div') - document.body.appendChild(container) - const root = createRoot(container) - roots.push(root) - - const renderWith = (teamsSettled: boolean): void => { - act(() => { - root.render( - - ) - }) - } - - renderWith(false) - expect(onChange).not.toHaveBeenCalled() - - renderWith(true) - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ stateIds: [], labelIds: [], assignee: null }) - ) - }) -}) - -const multiTeamStates = [ - { id: 'be-backlog', name: 'Backlog', type: 'backlog' }, - { id: 'be-todo', name: 'Todo', type: 'unstarted' }, - { id: 'fe-backlog', name: 'Backlog', type: 'backlog' }, - { id: 'fe-todo', name: 'Todo', type: 'unstarted' } -] - const teamBe: LinearTeam = { id: 'team-be', name: 'Backend', key: 'BE' } const teamFe: LinearTeam = { id: 'team-fe', name: 'Frontend', key: 'FE' } @@ -326,159 +119,7 @@ function pickerRowsNamed(name: string): HTMLElement[] { ) } -describe('LinearIssueAttributeFilterDropdowns status options across teams', () => { - it('lists one row per status name instead of one per team state id', () => { - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: multiTeamStates, - loading: false, - error: null - })) - - renderDropdowns(emptyFilter) - openSectionNamed('Status') - - expect(pickerRowsNamed('Todo')).toHaveLength(1) - expect(pickerRowsNamed('Backlog')).toHaveLength(1) - }) - - it('selects every team state id behind the picked status name', () => { - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: multiTeamStates, - loading: false, - error: null - })) - const { onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Status') - act(() => { - pickerRowsNamed('Todo')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(onChange).toHaveBeenCalledWith( - expect.objectContaining({ stateIds: ['be-todo', 'fe-todo'] }) - ) - }) - - it('keeps the expanded status selection within the transport id cap', () => { - const overCap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS + 20 - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: Array.from({ length: overCap }, (_unused, index) => ({ - id: `team-${index}-todo`, - name: 'Todo', - type: 'unstarted' - })), - loading: false, - error: null - })) - const { onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Status') - act(() => { - pickerRowsNamed('Todo')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - expect(onChange.mock.calls[0]?.[0].stateIds).toHaveLength( - LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS - ) - }) -}) - -function sameNamedMetadata(name: string, count: number): { id: string; name: string }[] { - return Array.from({ length: count }, (_unused, index) => ({ id: `team-${index}-${name}`, name })) -} - describe('LinearIssueAttributeFilterDropdowns transport-cap coverage notice', () => { - it('reports the team statuses left out once a picked status exceeds the id cap', () => { - const overCap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS + 20 - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: sameNamedMetadata('todo', overCap), - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Status') - act(() => { - pickerRowsNamed('todo')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - const bounded = onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter - expect(bounded.stateIds).toHaveLength(LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS) - rerender(bounded) - - expect(pickerRowsNamed('todo')).toHaveLength(1) - expect(document.body.textContent).toContain( - `Filtering ${LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS} of ${overCap} team statuses` - ) - }) - - it('reports the shrunken coverage of a picked status when a second status is added', () => { - const perStatus = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS + 20 - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: [...sameNamedMetadata('todo', perStatus), ...sameNamedMetadata('doing', perStatus)], - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Status') - act(() => { - pickerRowsNamed('todo')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - rerender(onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter) - act(() => { - pickerRowsNamed('doing')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - rerender(onChange.mock.calls[1]?.[0] as LinearIssueAttributeFilter) - - expect(document.body.textContent).toContain( - `Filtering ${LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS} of ${perStatus * 2} team statuses` - ) - }) - - it('stays silent while every team id behind the picked status is applied', () => { - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: multiTeamStates, - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Status') - act(() => { - pickerRowsNamed('Todo')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - rerender(onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter) - - expect(document.body.textContent).not.toContain('team statuses') - openSectionNamed('Back') - expect(document.body.textContent).not.toContain('partial') - }) - - it('flags the trimmed status filter in the section menu and the pill', () => { - const overCap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS + 20 - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: sameNamedMetadata('todo', overCap), - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Status') - act(() => { - pickerRowsNamed('todo')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - rerender(onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter) - openSectionNamed('Back') - - expect(document.body.textContent).toContain('1 selected') - expect(document.body.textContent).toContain('· partial') - // Use a keyboard-reachable, named marker; a title alone cannot provide accessible coverage feedback. - const pillMarkers = [...document.body.querySelectorAll('button')].filter( - (button) => button.textContent === 'partial' - ) - expect(pillMarkers).toHaveLength(1) - expect(pillMarkers[0]?.getAttribute('data-slot')).toBe('tooltip-trigger') - }) - it('keeps and counts a picked status whose ids all sort past the cap', () => { const cap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS metadataMocks.useTeamsStates.mockImplementation(() => ({ @@ -509,64 +150,6 @@ describe('LinearIssueAttributeFilterDropdowns transport-cap coverage notice', () expect(bounded.stateIds).toContain('z-000') expect(document.body.textContent).toContain(`Filtering ${cap} of ${cap + 1} team statuses`) }) - - it('says the status filter is full instead of claiming coverage it cannot have', () => { - const cap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_STATE_IDS - const states = Array.from({ length: cap + 1 }, (_unused, index) => ({ - id: `s-${String(index).padStart(3, '0')}`, - name: `Status ${String(index).padStart(3, '0')}` - })) - metadataMocks.useTeamsStates.mockImplementation(() => ({ - data: states, - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns({ - ...emptyFilter, - stateIds: states.slice(0, cap).map((state) => state.id) - }) - openSectionNamed('Status') - act(() => { - pickerRowsNamed(`Status ${String(cap).padStart(3, '0')}`)[0]?.dispatchEvent( - new MouseEvent('click', { bubbles: true }) - ) - }) - - const bounded = onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter - expect(bounded.stateIds).toHaveLength(cap) - rerender(bounded) - - expect(document.body.textContent).toContain( - `Filtering the most this can carry: ${cap} team statuses` - ) - openSectionNamed('Back') - expect(document.body.textContent).toContain(`${cap} selected`) - expect(document.body.textContent).toContain('· partial') - }) - - it('reports the team labels left out once a picked label exceeds the id cap', () => { - const overCap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_LABEL_IDS + 20 - metadataMocks.useTeamsLabels.mockImplementation(() => ({ - data: sameNamedMetadata('bug', overCap), - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns(emptyFilter) - openSectionNamed('Labels') - act(() => { - pickerRowsNamed('bug')[0]?.dispatchEvent(new MouseEvent('click', { bubbles: true })) - }) - - const bounded = onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter - expect(bounded.labelIds).toHaveLength(LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_LABEL_IDS) - rerender(bounded) - - expect(document.body.textContent).toContain( - `Filtering ${LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_LABEL_IDS} of ${overCap} team labels` - ) - }) }) // Exactly the cap is complete here; bounded ids alone cannot prove that truncation happened. @@ -678,39 +261,6 @@ describe('LinearIssueAttributeFilterDropdowns coverage at exactly the transport expect(document.body.textContent).toContain('Filtering the most this can carry') }) - it('keeps the labels truncation when an unrelated facet is picked', () => { - const labelCap = LINEAR_ISSUE_ATTRIBUTE_FILTER_MAX_LABEL_IDS - const labels = Array.from({ length: labelCap + 1 }, (_unused, index) => ({ - id: `l-${String(index).padStart(3, '0')}`, - name: rowNamed(index) - })) - metadataMocks.useTeamsLabels.mockImplementation(() => ({ - data: labels, - loading: false, - error: null - })) - - const { rerender, onChange } = renderDropdowns({ - ...emptyFilter, - labelIds: labels.slice(0, labelCap).map((label) => label.id) - }) - openSectionNamed('Labels') - clickRow(rowNamed(labelCap)) - rerender(onChange.mock.calls[0]?.[0] as LinearIssueAttributeFilter) - expect(document.body.textContent).toContain('Filtering the most this can carry') - - openSectionNamed('Back') - openSectionNamed('Priority') - clickRow('Urgent') - const withPriority = onChange.mock.calls[1]?.[0] as LinearIssueAttributeFilter - rerender(withPriority) - - expect(withPriority.labelIds).toHaveLength(labelCap) - openSectionNamed('Back') - openSectionNamed('Labels') - expect(document.body.textContent).toContain('Filtering the most this can carry') - }) - it('drops the recorded truncation when the facet is cleared', () => { const states = Array.from({ length: cap + 1 }, (_unused, index) => ({ id: `s-${String(index).padStart(3, '0')}`, diff --git a/src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx b/src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx index 0afd02fb518..87f590750ae 100644 --- a/src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx +++ b/src/renderer/src/components/native-chat-resume-bulk-progress.test.tsx @@ -9,7 +9,7 @@ import { import { act } from 'react' import { expect, it, vi, type Mock } from 'vitest' import { NativeChatResumeOnRestartModal } from './NativeChatResumeRuntimeModal' -import { chatBox } from './native-chat-resume-on-restart-modal.test-support' + import { NativeChatResumeStatusSegment } from './status-bar/NativeChatResumeStatusSegment' import type { ResumeCandidate, ResumeFailure } from './native-chat-resume-on-restart-grouping' import type { AgentSessionStatusSummary } from '../../../shared/agent-session/agent-session-wire' @@ -66,29 +66,6 @@ function emitPhase( }) } -it('resumes a 21-chat selection with one action and no redundant listing', async () => { - const candidates = Array.from({ length: 21 }, (_, index) => ({ - ...offered[0]!, - sessionId: `chat-${index}` - })) - rpc.mockImplementation(async (_target, method) => - method === 'agentSession.restartResumable' - ? { sessions: candidates, failed: [] } - : { - sessions: [], - failed: [], - continued: candidates.map(({ sessionId }) => ({ sessionId, outcome: 'continued' })) - } - ) - await refreshNativeChatRestartOffer() - await continueNativeChatRestartOffer(candidates.map(({ sessionId }) => sessionId)) - expect(rpc.mock.calls.map((call) => [call[1], call[2]])).toEqual([ - ['agentSession.restartResumable', undefined], - ['agentSession.restartContinue', { sessionIds: candidates.map(({ sessionId }) => sessionId) }] - ]) - expect(toasts()).toEqual([['Resumed 21 chats']]) -}) - it('uses the host unconfirmed result after a lost bulk reply without reporting a failure', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) let reads = 0 @@ -119,30 +96,6 @@ it('uses the host unconfirmed result after a lost bulk reply without reporting a } }) -it('publishes a refusal reply without needing a second host read', async () => { - let reads = 0 - rpc.mockImplementation(async (_target, method) => { - if (method === 'agentSession.restartResumable') { - if (reads++ > 0) { - throw new Error('listing unavailable') - } - return { sessions: offered, failed: [] } - } - return { - sessions: [offered[1]], - failed: [failure('a')], - continued: [{ sessionId: 'a', outcome: 'refused' }] - } - }) - await mount() - await act(async () => chatBox('b').click()) - await act(async () => button('Resume 1 chat').click()) - expect(reads).toBe(1) - expect(offerIds()).toEqual(['b']) - expect(getNativeChatRestartOffer().failed.map((row) => row.sessionId)).toEqual(['a']) - expect(toasts()).toEqual([['1 chat couldn’t be resumed']]) -}) - it('keeps the run in flight after every feed verdict until the fallback list is published', async () => { const answer = Promise.withResolvers<{ continued: { sessionId: string; outcome: 'continued' }[]