From 2d1a6ce22b141facaf7a8e80c3b806362fa17075 Mon Sep 17 00:00:00 2001 From: William Quintal Date: Tue, 7 Jul 2026 20:22:45 -0400 Subject: [PATCH] test(e2e): cover GitLab Checks panel job details Add a headless Electron E2E spec that drives the Checks panel against a synthetic GitLab MR + failed pipeline job (no live credentials, no private data). It re-registers the hostedReview/workItemDetails/jobTrace IPC handlers in the main process with fake data, links the MR to the active worktree, then: - expands the failed job row and asserts the inline details load (the "View full logs" affordance + log-tail hint) instead of "No inline details are available for this check"; - opens the full-details tab and asserts the job trace renders. Doubles as the reproducible screenshot source for the PR. Co-Authored-By: Claude Opus 4.8 --- tests/e2e/gitlab-checks-job-details.spec.ts | 83 +++++++++++++ tests/e2e/helpers/gitlab-checks-fixture.ts | 131 ++++++++++++++++++++ 2 files changed, 214 insertions(+) create mode 100644 tests/e2e/gitlab-checks-job-details.spec.ts create mode 100644 tests/e2e/helpers/gitlab-checks-fixture.ts diff --git a/tests/e2e/gitlab-checks-job-details.spec.ts b/tests/e2e/gitlab-checks-job-details.spec.ts new file mode 100644 index 00000000000..38272f4fd0c --- /dev/null +++ b/tests/e2e/gitlab-checks-job-details.spec.ts @@ -0,0 +1,83 @@ +import type { Page } from '@stablyai/playwright-test' +import { mkdirSync } from 'node:fs' +import path from 'node:path' +import { test, expect } from './helpers/orca-app' +import { waitForActiveWorktree, waitForSessionReady } from './helpers/store' +import { openChecks } from './helpers/source-control-ai-generation' +import { + GITLAB_CHECKS_FIXTURE, + installGitLabChecksBackend, + linkGitLabMRToWorktree +} from './helpers/gitlab-checks-fixture' + +async function resolveActiveWorktreeId(page: Page): Promise { + const worktreeId = await page.evaluate(() => window.__store?.getState().activeWorktreeId ?? null) + if (!worktreeId) { + throw new Error('E2E fixture did not expose an active worktree') + } + return worktreeId +} + +test.describe('GitLab Checks panel job details', () => { + test('expands a failed pipeline job to show its log excerpt', async ({ + orcaPage, + electronApp + }, testInfo) => { + await waitForSessionReady(orcaPage) + await waitForActiveWorktree(orcaPage) + + await installGitLabChecksBackend(electronApp) + const worktreeId = await resolveActiveWorktreeId(orcaPage) + await linkGitLabMRToWorktree(orcaPage, worktreeId, GITLAB_CHECKS_FIXTURE.mrNumber) + + await openChecks(orcaPage, worktreeId) + + // The pipeline job renders as a check row labelled ": ". + const rowLabel = `${GITLAB_CHECKS_FIXTURE.stage}: ${GITLAB_CHECKS_FIXTURE.jobName}` + const jobRow = orcaPage.getByText(rowLabel, { exact: true }) + await expect(jobRow).toBeVisible({ timeout: 10_000 }) + + const screenshotDir = path.join( + process.cwd(), + 'validation-screenshots', + `gitlab-checks-job-details-${Date.now()}` + ) + mkdirSync(screenshotDir, { recursive: true }) + await testInfo.attach('validation-screenshot-dir', { + body: screenshotDir, + contentType: 'text/plain' + }) + + await orcaPage.screenshot({ + path: path.join(screenshotDir, '01-gitlab-checks-job-collapsed.png') + }) + + // Expand the job row → the fix loads the trace instead of showing + // "No inline details are available for this check." The inline panel keeps + // the full log behind "View full logs" (matching GitHub), so assert the + // failed-jobs section and the log-tail affordance render. + await jobRow.click() + + await expect(orcaPage.getByText('No inline details are available for this check.')).toHaveCount( + 0 + ) + const viewFullLogs = orcaPage.getByRole('button', { name: 'View full logs' }) + await expect(viewFullLogs).toBeVisible({ timeout: 10_000 }) + await expect(orcaPage.getByText('Log tail available in full details.')).toBeVisible() + + await orcaPage.screenshot({ + path: path.join(screenshotDir, '02-gitlab-checks-job-expanded.png') + }) + + // Open the full-details tab → the actual trace renders in the editor. + await viewFullLogs.click() + await expect(orcaPage.getByText('AssertionError: expected refunded amount')).toBeVisible({ + timeout: 10_000 + }) + await expect(orcaPage.getByText('ERROR: Job failed: exit code 1')).toBeVisible() + + await orcaPage.screenshot({ + path: path.join(screenshotDir, '03-gitlab-checks-job-full-logs.png') + }) + }) +}) diff --git a/tests/e2e/helpers/gitlab-checks-fixture.ts b/tests/e2e/helpers/gitlab-checks-fixture.ts new file mode 100644 index 00000000000..11c02bd2649 --- /dev/null +++ b/tests/e2e/helpers/gitlab-checks-fixture.ts @@ -0,0 +1,131 @@ +import type { ElectronApplication, Page } from '@stablyai/playwright-test' +import { expect } from '@stablyai/playwright-test' + +/** + * Synthetic GitLab MR + failed pipeline job used to drive the Checks panel + * without a live GitLab project. All values are fake so evidence screenshots + * carry no private data. + */ +export const GITLAB_CHECKS_FIXTURE = { + mrNumber: 4242, + jobId: 987654, + jobName: 'Purchase API Component Tests', + stage: 'Component Tests', + webUrl: 'https://gitlab.example.test/acme/orca/-/jobs/987654', + mrUrl: 'https://gitlab.example.test/acme/orca/-/merge_requests/4242', + headSha: 'e2ee2ee2ee2ee2ee2ee2ee2ee2ee2ee2ee2ee2e', + trace: [ + '$ pnpm test:component --project purchase-api', + 'Running 128 component tests across 6 shards…', + 'PASS src/purchase/cart.spec.ts (42 tests)', + 'PASS src/purchase/checkout.spec.ts (37 tests)', + 'FAIL src/purchase/refund.spec.ts', + ' ● refund flow › issues a partial refund for a split payment', + ' AssertionError: expected refunded amount 4200 to equal 4250', + ' at Object. (src/purchase/refund.spec.ts:118:32)', + 'Tests: 1 failed, 127 passed, 128 total', + 'ERROR: Job failed: exit code 1' + ].join('\n') +} as const + +/** + * Re-registers the GitLab + hosted-review IPC handlers in the main process so + * the real renderer fetch path resolves to synthetic data. Returns nothing; the + * handlers stay installed for the lifetime of the app instance. + */ +export async function installGitLabChecksBackend( + electronApp: ElectronApplication, + fixture: typeof GITLAB_CHECKS_FIXTURE = GITLAB_CHECKS_FIXTURE +): Promise { + await electronApp.evaluate(({ ipcMain }, fx) => { + ipcMain.removeHandler('hostedReview:forBranch') + ipcMain.handle('hostedReview:forBranch', async () => ({ + provider: 'gitlab', + number: fx.mrNumber, + title: 'Add purchase API component test coverage', + state: 'open', + url: fx.mrUrl, + status: 'failure', + updatedAt: '2026-07-07T12:00:00.000Z', + mergeable: 'MERGEABLE', + headSha: fx.headSha + })) + + ipcMain.removeHandler('gitlab:workItemDetails') + ipcMain.handle('gitlab:workItemDetails', async () => ({ + item: { + id: `gitlab-mr-${fx.mrNumber}`, + type: 'mr', + number: fx.mrNumber, + title: 'Add purchase API component test coverage', + state: 'opened', + url: fx.mrUrl, + labels: [], + updatedAt: '2026-07-07T12:00:00.000Z', + author: 'e2e-bot' + }, + body: 'Synthetic MR used for Checks panel evidence.', + comments: [], + headSha: fx.headSha, + pipelineJobs: [ + { + id: fx.jobId, + pipelineId: 55, + name: fx.jobName, + stage: fx.stage, + status: 'failed', + webUrl: fx.webUrl, + duration: 87 + } + ], + reviewers: [] + })) + + ipcMain.removeHandler('gitlab:jobTrace') + ipcMain.handle('gitlab:jobTrace', async () => ({ ok: true, trace: fx.trace })) + }, fixture) +} + +/** + * Links the synthetic GitLab MR to the given worktree and clears its git status + * so the Checks panel renders the review header + pipeline jobs. + */ +export async function linkGitLabMRToWorktree( + page: Page, + worktreeId: string, + mrNumber: number +): Promise { + await page.evaluate( + ({ worktreeId, mrNumber }) => { + const store = window.__store + if (!store) { + throw new Error('window.__store is not available') + } + store.setState((current) => ({ + worktreesByRepo: Object.fromEntries( + Object.entries(current.worktreesByRepo).map(([repoId, worktrees]) => [ + repoId, + worktrees.map((worktree) => + worktree.id === worktreeId ? { ...worktree, linkedGitLabMR: mrNumber } : worktree + ) + ]) + ), + gitStatusByWorktree: { + ...current.gitStatusByWorktree, + [worktreeId]: [] + } + })) + }, + { worktreeId, mrNumber } + ) + await expect + .poll( + () => + page.evaluate((worktreeId) => { + const worktrees = Object.values(window.__store?.getState().worktreesByRepo ?? {}).flat() + return worktrees.find((entry) => entry.id === worktreeId)?.linkedGitLabMR ?? null + }, worktreeId), + { timeout: 5_000 } + ) + .toBe(mrNumber) +}