// @vitest-environment happy-dom import { queued, detailTransport, events, mocks, actual, repo, onSelect, pickerStore, resetPickerStore, LinearPicker, prepareDefaultPicker, installRuntimeBridge, aliasStatus, aliasRow } from './smart-workspace-linear-read-test-fixtures' import { LinearIntegrationCard } from '../../src/renderer/src/components/integrations/task-trackers/linear-integration-card' import { TooltipProvider } from '../../src/renderer/src/components/ui/tooltip' import { getIssue } from '../../src/main/linear/linear-issue-lookups' import React from 'react' import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { clearLinearRequestMaps } from '@/store/slices/linear/linear-slice-request-state' import { buildTaskSourceContextFromRepo, getTaskSourceCacheScope } from '../../src/shared/task-source-context' import { linearSearchCacheKey } from '@/store/slices/linear/linear-cache' import { RESULT_LIMIT } from '../../src/renderer/src/components/new-workspace/smart-workspace-name-field-model' import { getLinearSourceScope } from '@/store/slices/linear/linear-slice-scope' import { credentialDecryptionMessage } from '../../src/shared/integration-credential-errors' import type { LinearIssue } from '../../src/shared/linear/issue-types' import type { LinearConnectionStatus } from '../../src/shared/linear/workspace-types' import { referenceLinearIssue } from '../../src/renderer/src/components/sidebar/workspace-card/review/workspace-reference-fixtures.test-support' vi.mock('../../src/renderer/src/components/skills/setup/linear-agent-skill-install-cta', () => ({ LinearAgentSkillInstallCta: () => null })) vi.mock('../../src/renderer/src/components/linear-api-key-dialog', () => ({ LinearApiKeyDialog: () => null })) describe('composer Linear read errors', () => { beforeEach(() => { mocks.state.linearStatus = { connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' } clearLinearRequestMaps() resetPickerStore() const { searchLinearIssues: _search, getCachedLinearIssues: _cache, ...initial } = mocks.state pickerStore.setState(initial) onSelect.mockClear() mocks.state.searchLinearIssues.mockReset() mocks.state.getCachedLinearIssues.mockReset() mocks.state.getCachedLinearIssues.mockReturnValue(null) }) afterEach(() => { cleanup() clearLinearRequestMaps() }) it.each([ { cache: 'cold', attachment: false }, { cache: 'warm', attachment: false }, { cache: 'cold', attachment: true }, { cache: 'warm', attachment: true } ])( '$cache network failure is safe with attachment=$attachment and successful retry clears it', async ({ cache, attachment }) => { if (cache === 'warm') { const context = buildTaskSourceContextFromRepo({ provider: 'linear', projectId: repo.id, repo })! const key = `${getTaskSourceCacheScope(context)}::${linearSearchCacheKey('workspace-1', 'launch', RESULT_LIMIT)}` pickerStore.setState({ linearSearchCache: { [key]: { data: [referenceLinearIssue()], fetchedAt: 1 } } }) } mocks.state.searchLinearIssues.mockRejectedValue(new Error('private network diagnostic')) const { rerender } = render() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) const message = 'Unable to load Linear issues. Try again.' await waitFor(() => expect(screen.getAllByText(message)).toHaveLength(1)) expect(screen.queryByText('Reconnect Linear')).toBeNull() if (cache === 'warm') { fireEvent.click(screen.getByText(/Task/)) expect(onSelect).toHaveBeenCalledTimes(1) } fireEvent.click(screen.getByRole('button', { name: 'Smart tab' })) expect(screen.queryByText(message)).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) mocks.state.searchLinearIssues.mockResolvedValue([referenceLinearIssue()]) rerender() await waitFor(() => expect(screen.getByText(/Task/)).toBeTruthy()) await waitFor(() => expect(screen.queryByText(message)).toBeNull()) } ) it('shows the read credential error on the Linear tab only, and a successful search clears it', async () => { mocks.state.searchLinearIssues.mockImplementation(async (query) => { if (query === 'broken') { throw new Error(credentialDecryptionMessage('Linear')) } return [referenceLinearIssue()] }) const message = 'Reconnect Linear' const { rerender } = render() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) await waitFor(() => expect(screen.getByRole('status').textContent).toBe( `${message}Unable to read saved Linear login. Reconnect it in Settings.` ) ) // The status refresh after the failure re-runs the same read; the error must stay up. mocks.state.linearStatus = { ...mocks.state.linearStatus, credentialError: credentialDecryptionMessage('Linear') } rerender() expect(screen.getByText(message)).toBeTruthy() await waitFor(() => expect(mocks.state.searchLinearIssues).toHaveBeenCalled()) expect(screen.getByText(message)).toBeTruthy() fireEvent.click(screen.getByRole('button', { name: 'Smart tab' })) expect(screen.queryByText(message)).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) rerender() await waitFor(() => expect(screen.getByText(/Task/)).toBeTruthy()) expect(screen.queryByText(message)).toBeNull() }) }) it.each( ['list', 'search'].flatMap((kind) => ['success', 'error', 'queued'].map((outcome) => ({ kind, outcome })) ) )( 'runtime refresh retires default New Workspace $kind pre-event $outcome', async ({ kind, outcome }) => { prepareDefaultPicker({ connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' }) cleanup() queued.enabled = false queued.updates.length = 0 const old = Promise.withResolvers() const fresh = Promise.withResolvers() const runtime = kind === 'search' ? mocks.state.searchLinearIssues : mocks.state.list const data = (title: string) => { const items = [{ ...referenceLinearIssue(), title }] return kind === 'search' ? items : { items } } runtime.mockReset().mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise) const control = Promise.withResolvers() mocks.state.detail.mockReset() mocks.state.detail.mockImplementation((context) => context.kind === 'task-source' || context.environmentId === 'other' ? control.promise : Promise.resolve(referenceLinearIssue()) ) const explicit = buildTaskSourceContextFromRepo({ provider: 'linear', projectId: repo.id, repo })! const controls = [ getLinearSourceScope(explicit), getLinearSourceScope({ ...explicit, accountLabel: 'Other full source' }), getLinearSourceScope({ kind: 'environment', environmentId: 'other' }) ].map((scope) => pickerStore.getState().fetchLinearIssue('ISSUE-1', 'workspace-1', { scope }).acquire() ) expect(mocks.state.detail).toHaveBeenCalledTimes(3) installRuntimeBridge() const mounted = render() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) await waitFor(() => expect(runtime).toHaveBeenCalledTimes(1)) const joined = Array.from({ length: 5 }, () => kind === 'search' ? pickerStore.getState().searchLinearIssues('launch', RESULT_LIMIT).acquire() : pickerStore .getState() .listLinearIssues({ kind: 'list', filter: 'assigned', limit: RESULT_LIMIT }) .acquire() ) expect(runtime).toHaveBeenCalledTimes(1) queued.enabled = true if (outcome === 'queued') { await act(async () => { old.resolve(data('Pre-event')) await old.promise }) } const beforeEdge = queued.updates.splice(0) act(() => events.emit({ type: 'linearLinkedIssueUpdated', worktreeId: 'fixture-worktree', identifier: 'ISSUE-1', workspaceId: 'workspace-1' }) ) const replacementUpdates = queued.updates.splice(0) queued.enabled = false expect(runtime).toHaveBeenCalledTimes(2) expect(mocks.state.detail).toHaveBeenCalledTimes(4) act(() => beforeEdge.forEach((update) => update())) expect(screen.queryByText('Pre-event')).toBeNull() act(() => replacementUpdates.forEach((update) => update())) if (outcome !== 'queued') { await act(async () => { if (outcome === 'success') { old.resolve(data('Pre-event')) } else { old.reject(new Error('old private diagnostic')) } await old.promise.catch(() => {}) }) expect(screen.queryByText('Pre-event')).toBeNull() } await act(async () => { fresh.reject(new Error('private diagnostic')) await fresh.promise.catch(() => {}) }) await waitFor(() => expect(screen.getAllByText('Unable to load Linear issues. Try again.')).toHaveLength(1) ) expect(runtime).toHaveBeenCalledTimes(2) const forced = Promise.withResolvers() const external = Promise.withResolvers() runtime.mockReturnValueOnce(forced.promise).mockReturnValueOnce(external.promise) const force = () => kind === 'search' ? pickerStore .getState() .searchLinearIssues('launch', RESULT_LIMIT, { force: true }) .acquire() : pickerStore .getState() .listLinearIssues( { kind: 'list', filter: 'assigned', limit: RESULT_LIMIT }, { force: true } ) .acquire() let A!: ReturnType, B!: ReturnType act(() => { A = force() B = force() }) expect(runtime).toHaveBeenCalledTimes(4) await act(async () => { forced.resolve(data('Superseded force')) external.resolve(data('External')) await external.promise }) await waitFor(() => expect(screen.getByText(/External/)).toBeTruthy()) expect(screen.queryByText(/Superseded force/)).toBeNull() expect(screen.queryByText('Unable to load Linear issues. Try again.')).toBeNull() A.release() B.release() control.resolve(referenceLinearIssue()) await Promise.all(controls.map((lease) => lease.waitForCurrentTerminal())) expect(controls.every((lease) => lease.currentView().kind === 'data')).toBe(true) controls.forEach((lease) => lease.release()) mounted.unmount() joined.forEach((lease) => lease.release()) act(() => events.emit({ type: 'linearLinkedIssueUpdated', worktreeId: 'fixture-worktree', identifier: 'ISSUE-1', workspaceId: 'workspace-1' }) ) expect(runtime).toHaveBeenCalledTimes(4) cleanup() clearLinearRequestMaps() } ) it('attachment Retry reacquires terminal cached missing without another provider read', async () => { resetPickerStore() const status: LinearConnectionStatus = { connected: true, viewer: null, selectedWorkspaceId: 'L', workspaces: [ { displayName: 'Test', email: null, id: 'L', organizationId: 'org', organizationName: 'Acme', organizationUrlKey: 'acme' } ] } mocks.state.linearStatus = status pickerStore.setState({ linearStatus: status, linearStatusChecked: true, linearStatusContextKey: 'local#0', preflightStatusChecked: true, preflightStatusContextKey: 'test', refreshPreflightStatus: vi.fn() }) mocks.state.detail.mockReset().mockResolvedValue(null) function Attachment() { const [attempt, retry] = React.useReducer((value: number) => value + 1, 0) return ( ) } render() const missing = 'Couldn’t load this Linear issue. Check the URL and connected workspace.' await waitFor(() => expect(screen.getByText(missing)).toBeTruthy()) expect(mocks.state.detail).toHaveBeenCalledTimes(1) expect(pickerStore.getState().linearIssueCache['L::ENG-1']?.data).toBeNull() fireEvent.click(screen.getByRole('button', { name: 'Retry' })) await waitFor(() => expect(screen.getByText(missing)).toBeTruthy()) expect(screen.queryByText('Loading Linear issue…')).toBeNull() expect(mocks.state.detail).toHaveBeenCalledTimes(1) }) it.each(['success', 'error', 'queued'])( 'runtime event retires a held same-literal URL alias observation: %s', async (outcome) => { const status = aliasStatus() const row = aliasRow prepareDefaultPicker(status) const old = Promise.withResolvers() const fresh = Promise.withResolvers() const control = Promise.withResolvers() let aliasLoads = 0 mocks.state.detail.mockReset().mockImplementation((context, _id, workspaceId) => { if ( context.kind === 'task-source' || context.environmentId === 'other' || workspaceId === 'F' ) { return control.promise } if (workspaceId === 'L') { return ++aliasLoads === 1 ? old.promise : fresh.promise } return Promise.resolve(row('Exact W', 'W')) }) pickerStore.setState({ linearIssueCache: { 'L::ENG-1': { data: row('Warm'), fetchedAt: 1 } } }) const explicit = buildTaskSourceContextFromRepo({ provider: 'linear', projectId: repo.id, repo })! const controls = [ pickerStore .getState() .fetchLinearIssue('ENG-1', 'F', { seed: { ...row('Foreign', 'F'), id: 'other-U' } }) .acquire(), pickerStore .getState() .fetchLinearIssue('ENG-1', 'L', { scope: getLinearSourceScope(explicit), seed: row('Explicit') }) .acquire(), pickerStore .getState() .fetchLinearIssue('ENG-1', 'L', { scope: getLinearSourceScope({ kind: 'environment', environmentId: 'other' }), seed: row('Other runtime') }) .acquire() ] const controlViews = controls.map((lease) => lease.currentView()) installRuntimeBridge() const mounted = render( ) fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) await waitFor(() => expect(aliasLoads).toBe(1)) queued.enabled = true if (outcome === 'queued') { await act(async () => { old.resolve(row('Pre-event')) await old.promise }) } const oldUpdates = queued.updates.splice(0) const emit = () => events.emit({ type: 'linearLinkedIssueUpdated', worktreeId: 'fixture-worktree', identifier: 'ENG-1', workspaceId: 'W' }) act(emit) const currentUpdates = queued.updates.splice(0) queued.enabled = false expect(aliasLoads).toBe(2) expect( mocks.state.detail.mock.calls.filter(([, , workspaceId]) => workspaceId === 'W') ).toHaveLength(1) expect(controls.map((lease) => lease.currentView())).toEqual(controlViews) act(() => oldUpdates.forEach((update) => update())) expect(screen.queryByText(/Pre-event/)).toBeNull() act(() => currentUpdates.forEach((update) => update())) if (outcome !== 'queued') { await act(async () => { if (outcome === 'success') { old.resolve(row('Pre-event')) } else { old.reject(new Error('retired private diagnostic')) } await old.promise.catch(() => {}) }) } if (outcome === 'queued') { await act(async () => { fresh.reject(new Error('current private diagnostic')) await fresh.promise.catch(() => {}) }) await waitFor(() => expect(screen.getAllByText('Unable to load Linear issues. Try again.')).toHaveLength(1) ) } else { expect(screen.queryByText(/Pre-event/)).toBeNull() await act(async () => { fresh.resolve(row('External')) await fresh.promise }) await waitFor(() => expect(screen.getByText(/External/)).toBeTruthy()) expect(screen.queryByText(/Pre-event/)).toBeNull() } expect( mocks.state.detail.mock.calls.filter(([, , workspaceId]) => workspaceId === 'W') ).toHaveLength(1) expect(aliasLoads).toBe(2) control.resolve(row('Control')) await Promise.all(controls.map((lease) => lease.waitForCurrentTerminal())) controls.forEach((lease) => lease.release()) mounted.unmount() act(emit) expect(aliasLoads).toBe(2) } ) it.each(['success', 'fallback', 'legacy', 'release'] as const)( 'cold URL alias actual provider replacement: %s', async (outcome) => { const status = aliasStatus(outcome === 'legacy') prepareDefaultPicker(status) expect(pickerStore.getState().linearIssueCache).toEqual({}) const old = Promise.withResolvers() const fresh = Promise.withResolvers() const control = Promise.withResolvers() let aliasLoads = 0 detailTransport.search .mockReset() .mockImplementation(async (_query, _variables, workspaceId) => { const issue = workspaceId === 'L' ? await (++aliasLoads === 1 ? old.promise : fresh.promise) : aliasRow('Exact W', 'W') if (!issue) { throw new Error('missing issue') } // Only provider I/O is synthetic; actual main concurrency/mapping/detail settlement runs. return { data: { issue: { ...issue, labels: { nodes: [] } } } } }) detailTransport.rpc.mockReset().mockImplementation((target, method, params) => { expect(target).toEqual({ kind: 'environment', environmentId: 'env-1' }) expect(method).toBe('linear.getIssue') return getIssue(params.id, params.workspaceId) }) mocks.state.detail.mockReset().mockImplementation((context, id, workspaceId) => { if ( context.kind === 'task-source' || context.environmentId === 'other' || workspaceId === 'F' || id === 'ENG-2' ) { return control.promise } return actual.linearGetIssue(context, id, workspaceId) }) const explicit = buildTaskSourceContextFromRepo({ provider: 'linear', projectId: repo.id, repo })! const controls = outcome === 'success' ? [ pickerStore.getState().fetchLinearIssue('ENG-2', 'L').acquire(), pickerStore.getState().fetchLinearIssue('ENG-1', 'F').acquire(), pickerStore .getState() .fetchLinearIssue('ENG-1', 'L', { scope: getLinearSourceScope(explicit) }) .acquire(), pickerStore .getState() .fetchLinearIssue('ENG-1', 'L', { scope: getLinearSourceScope({ kind: 'environment', environmentId: 'other' }) }) .acquire() ] : [] const controlViews = controls.map((lease) => lease.currentView()) expect(controlViews.every((view) => view.kind === 'loading' && view.data === null)).toBe(true) installRuntimeBridge() const mounted = render( ) const emit = () => events.emit({ type: 'linearLinkedIssueUpdated', worktreeId: 'fixture-worktree', identifier: 'ENG-1', workspaceId: 'W' }) const loads = (workspaceId: string) => detailTransport.search.mock.calls.filter(([, , workspace]) => workspace === workspaceId) .length try { fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) await waitFor(() => expect(loads('L')).toBe(1)) expect(screen.getByText('Loading Linear issue…')).toBeTruthy() act(emit) await waitFor(() => expect(loads('L')).toBe(2)) await waitFor(() => expect(loads('W')).toBe(1)) expect(controls.map((lease) => lease.currentView())).toEqual(controlViews) expect(mocks.state.detail).toHaveBeenCalledTimes(controls.length + 3) if (outcome === 'release') { mounted.unmount() } await act(async () => { if (outcome === 'fallback') { old.reject(new Error('retired provider diagnostic')) } else { old.resolve(aliasRow('Pre-event In Review')) } await old.promise.catch(() => {}) }) expect(screen.queryByText(/Pre-event/)).toBeNull() expect(screen.queryByText(/Exact W/)).toBeNull() if (outcome !== 'release') { expect(screen.getByText('Loading Linear issue…')).toBeTruthy() expect(screen.queryByText(/Couldn’t load this Linear issue/)).toBeNull() } await act(async () => { if (outcome === 'fallback') { // The existing status probe supplies a newly discovered URL candidate W. mocks.state.linearStatus = { ...status, workspaces: status.workspaces!.toReversed() } fresh.reject(new Error('current provider diagnostic')) } else { fresh.resolve(aliasRow('Current External')) } await fresh.promise.catch(() => {}) }) if (outcome !== 'release') { await waitFor(() => expect( screen.getByText(outcome === 'fallback' ? /Exact W/ : /Current External/) ).toBeTruthy() ) expect(screen.queryByText('Loading Linear issue…')).toBeNull() expect(screen.queryByText(/Couldn’t load this Linear issue/)).toBeNull() expect(screen.queryByText(/provider diagnostic|Pre-event/)).toBeNull() } expect(loads('L')).toBe(2) expect(loads('W')).toBe(1) expect(controls.map((lease) => lease.currentView())).toEqual(controlViews) expect(mocks.state.detail).toHaveBeenCalledTimes(controls.length + 3) controls.forEach((lease) => lease.release()) mounted.unmount() act(emit) await waitFor(() => expect(loads('W')).toBe(2)) expect(loads('L')).toBe(2) expect(mocks.state.detail).toHaveBeenCalledTimes(controls.length + 4) } finally { mounted.unmount() controls.forEach((lease) => lease.release()) await act(async () => { old.resolve(aliasRow('Cleanup')) fresh.resolve(aliasRow('Cleanup')) control.resolve(aliasRow('Control')) await Promise.allSettled([old.promise, fresh.promise, control.promise]) }) } } ) it.each([false, true])( 'warm default launch/12 keeps Task and one error with status outage=%s', async (outage) => { prepareDefaultPicker({ connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' }) mocks.state.searchLinearIssues.mockReset().mockResolvedValue([referenceLinearIssue()]) render() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) await waitFor(() => expect(screen.getByText(/Task/)).toBeTruthy()) if (outage) { mocks.statusReads.mockRejectedValue(new Error('remote status transport unavailable')) } mocks.state.searchLinearIssues.mockRejectedValue( new Error('remote search transport unavailable') ) const read = pickerStore .getState() .searchLinearIssues('launch', 12, { force: true, workspaceId: 'workspace-1' }) .acquire() await act(async () => { await read.waitForCurrentTerminal() }) expect(pickerStore.getState().linearStatus.connected).toBe(true) expect(screen.getByText(/Task/)).toBeTruthy() expect(screen.getAllByText('Unable to load Linear issues. Try again.')).toHaveLength(1) expect(mocks.statusReads).not.toHaveBeenCalled() read.release() } ) it('Settings keeps the confirmed account during failed checks, then displays actual disconnected status', async () => { prepareDefaultPicker({ connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' }) render( ) expect(screen.getByText('Connected')).toBeTruthy() mocks.statusReads.mockRejectedValue(new Error('status transport unavailable')) await act(async () => { await pickerStore.getState().checkLinearConnection(true) }) expect(screen.getByText('Connected')).toBeTruthy() expect(screen.queryByText('Not connected')).toBeNull() mocks.statusReads.mockResolvedValue({ connected: false, viewer: null }) await act(async () => { await pickerStore.getState().checkLinearConnection(true) }) expect(screen.getByText('Not connected')).toBeTruthy() }) it('a cold default host cannot import the previously focused host account or rows', async () => { prepareDefaultPicker({ connected: true, viewer: null, selectedWorkspaceId: 'workspace-1' }) mocks.state.searchLinearIssues.mockReset().mockResolvedValue([referenceLinearIssue()]) render() fireEvent.click(screen.getByRole('button', { name: 'Linear tab' })) await waitFor(() => expect(screen.getByText(/Task/)).toBeTruthy()) mocks.statusReads.mockRejectedValue(new Error('new host unavailable')) const settings = pickerStore.getState().settings if (!settings) { throw new Error('fixture requires captured host settings') } act(() => pickerStore.setState({ settings: { ...settings, activeRuntimeEnvironmentId: 'other' } }) ) expect(screen.queryByText(/Task/)).toBeNull() await waitFor(() => expect(pickerStore.getState().linearStatusContextKey).toBe('runtime:other#0')) expect(pickerStore.getState().linearStatusObserved).toBe(false) expect(pickerStore.getState().linearStatusChecked).toBe(true) })