From bb7e74d2364fc883ef45b33fbb964dd5d6f84cf2 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Fri, 16 Oct 2026 12:55:32 -0700 Subject: [PATCH] fix(e2e): keep dedicated specs out of general CI shard plan (#28161) - Specs with their own CI jobs were also being assigned to general E2E shards, so they could run twice or in the wrong job. - planE2e now drops dedicated specs before balancing and fails if no general specs remain, instead of producing an empty plan. - The mobile subscribe spec builds a paired runtime connection from the pairing offer before sending requests, matching the updated remote runtime helper signatures. --- config/scripts/ci-e2e-shard-plan.mjs | 10 +++++- config/scripts/ci-shard-assignment.test.mjs | 35 ++++++++++++++++++- ...ile-subscribe-idle-hidden-pane-fit.spec.ts | 6 ++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/config/scripts/ci-e2e-shard-plan.mjs b/config/scripts/ci-e2e-shard-plan.mjs index ea6002451a3..183e279c9eb 100644 --- a/config/scripts/ci-e2e-shard-plan.mjs +++ b/config/scripts/ci-e2e-shard-plan.mjs @@ -1,6 +1,7 @@ import { readFileSync, writeFileSync } from 'node:fs' import { join } from 'node:path' import { pathToFileURL } from 'node:url' +import { isDedicatedE2eSpec } from './ci-e2e-job-selection.mjs' import { balanceFiles, compareIds, @@ -44,7 +45,14 @@ export function discoverE2eFiles(report) { } export function planE2e(report, count, baseline) { - const testsByFile = discoverE2eFiles(report) + const testsByFile = Object.fromEntries( + Object.entries(discoverE2eFiles(report)).filter( + ([file]) => !isDedicatedE2eSpec(`tests/e2e/${file}`) + ) + ) + if (!Object.keys(testsByFile).length) { + throw new Error('Playwright discovered no general E2E tests') + } const timings = Object.fromEntries( Object.entries(baseline.timings).map(([file, duration]) => [ file.replace(/^tests\/e2e\//, ''), diff --git a/config/scripts/ci-shard-assignment.test.mjs b/config/scripts/ci-shard-assignment.test.mjs index 44ccf5d4d4f..3baedc084e9 100644 --- a/config/scripts/ci-shard-assignment.test.mjs +++ b/config/scripts/ci-shard-assignment.test.mjs @@ -4,7 +4,7 @@ import { join, resolve } from 'node:path' import { afterEach, describe, expect, it, vi } from 'vitest' import RuntimeSequencer from './vitest-runtime-sequencer.mjs' import { balanceFiles } from './ci-shard-assignment.mjs' -import { discoverE2eFiles, planE2e } from './ci-e2e-shard-plan.mjs' +import { discoverE2eFiles, planE2e, verifyE2eSelection } from './ci-e2e-shard-plan.mjs' import { parseTimingLog } from './ci-shard-timing-import.mjs' import TimingSequencer from './ci-unit-sequencer.mjs' import { UNIT_INCLUDE, UNIT_EXCLUDE } from './ci-unit-files.mjs' @@ -145,6 +145,39 @@ describe('timing-weighted shard selection', () => { ).toThrow('Duplicate') }) + it('plans only general E2E specs and rejects unexpected dedicated selections', () => { + const spec = (id, file) => ({ id, file, tests: [{ projectName: 'electron-headless' }] }) + const report = { + suites: [ + { + specs: [ + spec('general', 'general.spec.ts'), + spec('layout', 'workspace-layout-oracle-headless.spec.ts'), + spec('ssh', 'ssh-orcad-terminal.spec.ts') + ] + } + ] + } + expect(Object.keys(discoverE2eFiles(report))).toEqual([ + 'general.spec.ts', + 'ssh-orcad-terminal.spec.ts', + 'workspace-layout-oracle-headless.spec.ts' + ]) + const assignment = { ...planE2e(report, 1, { timings: {} }), selectedShard: 1 } + expect(assignment.shards[0].files).toEqual(['general.spec.ts']) + expect(Object.keys(assignment.testsByFile)).toEqual(['general.spec.ts']) + const general = report.suites[0].specs[0] + expect(() => verifyE2eSelection(assignment, { suites: [{ specs: [general] }] })).not.toThrow() + for (const dedicated of report.suites[0].specs.slice(1)) { + expect(() => + verifyE2eSelection(assignment, { suites: [{ specs: [general, dedicated] }] }) + ).toThrow('differs') + } + expect(() => + planE2e({ suites: [{ specs: report.suites[0].specs.slice(1) }] }, 1, { timings: {} }) + ).toThrow('no general E2E tests') + }) + it('imports ANSI unit timings and E2E failures without counting headful reruns', () => { const parsed = parseTimingLog( [ diff --git a/tests/e2e/mobile-subscribe-idle-hidden-pane-fit.spec.ts b/tests/e2e/mobile-subscribe-idle-hidden-pane-fit.spec.ts index 6b57ba0dd38..0c5acb7ef35 100644 --- a/tests/e2e/mobile-subscribe-idle-hidden-pane-fit.spec.ts +++ b/tests/e2e/mobile-subscribe-idle-hidden-pane-fit.spec.ts @@ -23,6 +23,7 @@ import { import { ensureTerminalVisible, waitForSessionReady } from './helpers/store' import { createRuntimeDesktopPairingOffer } from './helpers/paired-electron-client' import { decodePairingOffer } from '../../src/shared/pairing' +import { pairedRuntimeConnectionFromOffer } from '../../src/shared/paired-runtime-connection' import { sendRemoteRuntimeRequest, subscribeRemoteRuntimeRequest @@ -132,6 +133,7 @@ test('phone subscribe to an idle relaunched terminal behind another tab fits the const offer = await createRuntimeDesktopPairingOffer(second.page) const pairing = decodePairingOffer(offer.pairingUrl) + const connection = pairedRuntimeConnectionFromOffer(pairing) let handle: string | undefined // The new tab's PTY spawn refuses terminal.list until its pane binds. await expect @@ -139,7 +141,7 @@ test('phone subscribe to an idle relaunched terminal behind another tab fits the async () => { const list = await sendRemoteRuntimeRequest<{ terminals: { handle: string; ptyId: string | null }[] - }>(pairing, 'terminal.list', {}, 15_000) + }>(connection, 'terminal.list', {}, 15_000) if (!list.ok) { return JSON.stringify(list) } @@ -152,7 +154,7 @@ test('phone subscribe to an idle relaunched terminal behind another tab fits the const seen: { snapshot: { cols: number; rows: number } | null } = { snapshot: null } const subscription = await subscribeRemoteRuntimeRequest( - pairing, + connection, 'terminal.subscribe', { terminal: handle,