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.
This commit is contained in:
Jinjing
2026-10-16 12:55:32 -07:00
committed by GitHub
parent 33fd7d3991
commit bb7e74d236
3 changed files with 47 additions and 4 deletions
+9 -1
View File
@@ -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\//, ''),
+34 -1
View File
@@ -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(
[
@@ -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,