diff --git a/src/issues/stateManager.ts b/src/issues/stateManager.ts index 14316c0818..d9d2b74ed1 100644 --- a/src/issues/stateManager.ts +++ b/src/issues/stateManager.ts @@ -345,20 +345,24 @@ export class StateManager { singleRepoState.maxIssueNumber = await folderManager.getMaxIssue(); singleRepoState.lastHead = folderManager.repository.state.HEAD?.commit; singleRepoState.lastBranch = folderManager.repository.state.HEAD?.name; + this._onDidChangeIssueData.fire(); } - private setIssues(folderManager: FolderRepositoryManager, query: string): Promise { - return new Promise(async resolve => { + private async setIssues(folderManager: FolderRepositoryManager, query: string): Promise { + try { const issues = await folderManager.getIssues(query, { fetchNextPage: false, fetchOnePagePerRepo: true }); + return issues?.items.map(item => { + const issueItem: IssueItem = item as IssueItem; + issueItem.uri = folderManager.repository.rootUri; + return issueItem; + }); + } catch { + // Errors from fetching issues are expected (e.g. network failures). + // Return undefined so the tree shows an empty state for this query. + return undefined; + } finally { this._onDidChangeIssueData.fire(); - resolve( - issues?.items.map(item => { - const issueItem: IssueItem = item as IssueItem; - issueItem.uri = folderManager.repository.rootUri; - return issueItem; - }), - ); - }); + } } private async setCurrentIssueFromBranch(singleRepoState: SingleRepoState, branchName: string, silent: boolean = false) { diff --git a/src/test/issues/stateManager.test.ts b/src/test/issues/stateManager.test.ts index 4f2549ac0f..64ad1ed274 100644 --- a/src/test/issues/stateManager.test.ts +++ b/src/test/issues/stateManager.test.ts @@ -169,4 +169,140 @@ describe('StateManager branch behavior with useBranchForIssues setting', functio vscode.workspace.getConfiguration = originalGetConfiguration; } }); + + it('should fire onDidChangeIssueData even when getIssues throws', async function () { + const mockUri = vscode.Uri.parse('file:///test'); + const mockFolderManager = { + repository: { rootUri: mockUri, state: { HEAD: { commit: 'abc123' }, remotes: [] } }, + getIssues: async () => { + throw new Error('Network error'); + }, + getMaxIssue: async () => 0, + }; + + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string, defaultValue?: any) => { + if (key === 'queries') { + return [{ label: 'Test', query: 'is:open assignee:@me repo:owner/repo', groupBy: [] }]; + } + return defaultValue; + }, + } as any; + } + return originalGetConfiguration(section); + }; + + try { + const sm = new StateManager(undefined as any, { + folderManagers: [mockFolderManager], + credentialStore: { isAnyAuthenticated: () => true, getCurrentUser: async () => ({ login: 'testuser' }) }, + } as any, mockContext); + + let changeEventCount = 0; + sm.onDidChangeIssueData(() => changeEventCount++); + + await (sm as any).setIssueData(mockFolderManager); + + // The event should have fired even though getIssues threw + assert.ok(changeEventCount > 0, 'onDidChangeIssueData should fire even when getIssues fails'); + + // The promise in the collection should resolve to undefined issues (not reject) + const collection = sm.getIssueCollection(mockUri); + const queryResult = await collection.get('Test'); + assert.strictEqual(queryResult?.issues, undefined, 'Issues should be undefined when getIssues fails'); + } finally { + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); + + it('should fire onDidChangeIssueData after setIssueData completes', async function () { + const mockUri = vscode.Uri.parse('file:///test'); + const mockFolderManager = { + repository: { rootUri: mockUri, state: { HEAD: { commit: 'abc123' }, remotes: [] } }, + getIssues: async () => { + return { items: [], hasMorePages: false, hasUnsearchedRepositories: false, totalCount: 0 }; + }, + getMaxIssue: async () => 0, + }; + + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string, defaultValue?: any) => { + if (key === 'queries') { + return [{ label: 'Test', query: 'is:open assignee:@me repo:owner/repo', groupBy: [] }]; + } + return defaultValue; + }, + } as any; + } + return originalGetConfiguration(section); + }; + + try { + const sm = new StateManager(undefined as any, { + folderManagers: [mockFolderManager], + credentialStore: { isAnyAuthenticated: () => true, getCurrentUser: async () => ({ login: 'testuser' }) }, + } as any, mockContext); + + let changeEventCount = 0; + sm.onDidChangeIssueData(() => changeEventCount++); + + await (sm as any).setIssueData(mockFolderManager); + + // The event should fire at least twice: once from setIssues (per-query) and once from setIssueData (final) + assert.strictEqual(changeEventCount, 2, `onDidChangeIssueData should fire exactly twice for a single query, but fired ${changeEventCount} times`); + } finally { + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); + + it('should not reject promises in issueCollection when getIssues throws', async function () { + const mockUri = vscode.Uri.parse('file:///test'); + const mockFolderManager = { + repository: { rootUri: mockUri, state: { HEAD: { commit: 'abc123' }, remotes: [] } }, + getIssues: async () => { + throw new Error('API error'); + }, + getMaxIssue: async () => 0, + }; + + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string, defaultValue?: any) => { + if (key === 'queries') { + return [{ label: 'Test', query: 'is:open repo:owner/repo', groupBy: [] }]; + } + return defaultValue; + }, + } as any; + } + return originalGetConfiguration(section); + }; + + try { + const sm = new StateManager(undefined as any, { + folderManagers: [mockFolderManager], + credentialStore: { isAnyAuthenticated: () => true, getCurrentUser: async () => ({ login: 'testuser' }) }, + } as any, mockContext); + + await (sm as any).setIssueData(mockFolderManager); + + // Verify that the promises in issueCollection resolve (not reject) + const collection = sm.getIssueCollection(mockUri); + for (const [, promise] of collection) { + const result = await promise; + assert.ok(result !== undefined, 'Promise should resolve, not reject'); + assert.strictEqual(result.issues, undefined, 'Issues should be undefined on error'); + } + } finally { + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); }); \ No newline at end of file