diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 06d0649b18..6a394c158b 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -594,8 +594,15 @@ declare module 'vscode' { /** * The initial option selections for the session, provided with the first request. * Contains the options the user selected (or defaults) before the session was created. + * + * @deprecated Use `inputState` instead */ readonly initialSessionOptions?: ReadonlyArray<{ optionId: string; value: string | ChatSessionProviderOptionItem }>; + + /** + * The current input state of the chat session. + */ + readonly inputState: ChatSessionInputState; } export interface ChatSessionCapabilities { @@ -692,6 +699,8 @@ declare module 'vscode' { * * These commands will be displayed at the bottom of the group. * + * For extensions using the legacy `commands` API, these commands are passed the sessionResource as the first argument. + * * For extensions that use the new `provideChatSessionInputState` API, these commands are passed a context object * `{ inputState: ChatSessionInputState; sessionResource: Uri | undefined }` that they can use to determine which session and options they are being invoked for. */ diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 724f9e46a1..dbe093e402 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -1916,9 +1916,13 @@ export class GitHubRepository extends Disposable { const statusByContext = new Map(); for (const status of statuses) { - const existing = statusByContext.get(status.context); + // Include event and workflowName in the key so that checks from different + // workflow events (e.g. "push" vs "pull_request") or different workflows + // are not incorrectly merged during deduplication. + const key = `${status.context}\0${status.event ?? ''}\0${status.workflowName ?? ''}`; + const existing = statusByContext.get(key); if (!existing) { - statusByContext.set(status.context, status); + statusByContext.set(key, status); continue; } @@ -1928,7 +1932,7 @@ export class GitHubRepository extends Disposable { if (currentIsPending && !existingIsPending) { // Current is pending, existing is completed - prefer current - statusByContext.set(status.context, status); + statusByContext.set(key, status); } else if (!currentIsPending && existingIsPending) { // Current is completed, existing is pending - keep existing continue; @@ -1936,7 +1940,7 @@ export class GitHubRepository extends Disposable { // Both are same type (both pending or both completed) // Prefer the one with a higher ID (more recent), as GitHub IDs are monotonically increasing if (status.id > existing.id) { - statusByContext.set(status.context, status); + statusByContext.set(key, status); } } } diff --git a/src/test/github/githubRepository.test.ts b/src/test/github/githubRepository.test.ts index 6d41977abf..0e5111026c 100644 --- a/src/test/github/githubRepository.test.ts +++ b/src/test/github/githubRepository.test.ts @@ -15,6 +15,7 @@ import { Uri } from 'vscode'; import { MockExtensionContext } from '../mocks/mockExtensionContext'; import { GitHubManager } from '../../authentication/githubServer'; import { GitHubServerType } from '../../common/authentication'; +import { CheckState, PullRequestCheckStatus } from '../../github/interface'; describe('GitHubRepository', function () { let sinon: SinonSandbox; @@ -52,4 +53,82 @@ describe('GitHubRepository', function () { // assert(! dotcomRepository.isGitHubDotCom); }); }); + + describe('deduplicateStatusChecks', function () { + function createStatus(overrides: Partial & { id: string; context: string }): PullRequestCheckStatus { + return { + databaseId: undefined, + url: undefined, + avatarUrl: undefined, + state: CheckState.Success, + description: null, + targetUrl: null, + workflowName: undefined, + event: undefined, + isRequired: false, + isCheckRun: true, + ...overrides, + }; + } + + function callDeduplicateStatusChecks(repo: GitHubRepository, statuses: PullRequestCheckStatus[]): PullRequestCheckStatus[] { + return (repo as any).deduplicateStatusChecks(statuses); + } + + let repo: GitHubRepository; + + beforeEach(function () { + const url = 'https://github.com/some/repo'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const rootUri = Uri.file('C:\\users\\test\\repo'); + repo = new GitHubRepository(1, remote, rootUri, credentialStore, telemetry); + }); + + it('keeps checks with different events as separate entries', function () { + const statuses = [ + createStatus({ id: '1', context: 'Build Linux / x86-64', event: 'push', workflowName: 'Build Linux' }), + createStatus({ id: '2', context: 'Build Linux / x86-64', event: 'pull_request', workflowName: 'Build Linux' }), + ]; + const result = callDeduplicateStatusChecks(repo, statuses); + assert.strictEqual(result.length, 2); + }); + + it('deduplicates checks with the same name, event, and workflow', function () { + const statuses = [ + createStatus({ id: '1', context: 'Build Linux / x86-64', event: 'push', workflowName: 'Build Linux', state: CheckState.Success }), + createStatus({ id: '2', context: 'Build Linux / x86-64', event: 'push', workflowName: 'Build Linux', state: CheckState.Success }), + ]; + const result = callDeduplicateStatusChecks(repo, statuses); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].id, '2'); // higher ID preferred + }); + + it('keeps checks from different workflows as separate entries', function () { + const statuses = [ + createStatus({ id: '1', context: 'build', event: 'push', workflowName: 'CI' }), + createStatus({ id: '2', context: 'build', event: 'push', workflowName: 'Nightly' }), + ]; + const result = callDeduplicateStatusChecks(repo, statuses); + assert.strictEqual(result.length, 2); + }); + + it('prefers pending checks over completed ones during deduplication', function () { + const statuses = [ + createStatus({ id: '1', context: 'test', event: 'push', workflowName: 'CI', state: CheckState.Success }), + createStatus({ id: '2', context: 'test', event: 'push', workflowName: 'CI', state: CheckState.Pending }), + ]; + const result = callDeduplicateStatusChecks(repo, statuses); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].state, CheckState.Pending); + }); + + it('handles status contexts without event or workflowName', function () { + const statuses = [ + createStatus({ id: '1', context: 'ci/jenkins', isCheckRun: false }), + createStatus({ id: '2', context: 'ci/travis', isCheckRun: false }), + ]; + const result = callDeduplicateStatusChecks(repo, statuses); + assert.strictEqual(result.length, 2); + }); + }); });