ci: the layout oracle gates merges (#27600)

* ci: the layout oracle gates merges

An oracle-only job joins verify's needs. It runs only the two layout oracle
specs through the existing e2e workflow, routed by layout writer paths; a PR
outside the route skips it and verify treats the skip as a pass. The route is
widened to the terminal store, terminal pane, graph sync, frozen mirror and
compatibility folders while the layout-holder stack is open. The oracle specs
leave the advisory e2e job so a PR does not run them twice.

* ci: the layout oracle tests the merge commit and routes its harness and stack writers

Review fixes: run on the merge commit so a stale-base PR cannot pass on an old
main; route the oracle's own e2e harness and runtime client (a change there broke
main's oracle without running it), and the window-side layout writers the stack
edits; move the route into its own module.

* ci: route the remaining layout writers the stack edits to the layout oracle
This commit is contained in:
Jinwoo Hong
2026-10-11 19:39:59 -04:00
committed by GitHub
parent a944a37c8c
commit 29dca8c031
11 changed files with 257 additions and 85 deletions
+17 -11
View File
@@ -27,6 +27,12 @@ on:
required: false
type: boolean
default: true
# Why: artifact names are unique per workflow run, and pr.yml calls this workflow twice.
artifact_suffix:
description: Appended to the build and trace artifact names so two calls in one run do not collide
required: false
type: string
default: ''
workflow_dispatch:
inputs:
ref:
@@ -91,7 +97,7 @@ jobs:
- name: Upload E2E build output
uses: actions/upload-artifact@v7
with:
name: e2e-build-out
name: e2e-build-out${{ inputs.artifact_suffix }}
path: out/
# Why: build-relay.mjs writes each relay's marker as `out/relay/<platform>/.version`,
# and upload-artifact drops dotfiles by default — consumers then fail SSH specs with
@@ -201,7 +207,7 @@ jobs:
- name: Download E2E build output
uses: actions/download-artifact@v8
with:
name: e2e-build-out
name: e2e-build-out${{ inputs.artifact_suffix }}
path: out/
# Why: the Electron suite is wall-clock constrained on OSS runners, but
@@ -291,7 +297,7 @@ jobs:
- name: Download E2E build output
uses: actions/download-artifact@v8
with:
name: e2e-build-out
name: e2e-build-out${{ inputs.artifact_suffix }}
path: out/
- name: Run changed E2E specs
@@ -345,7 +351,7 @@ jobs:
if: failure() || cancelled()
uses: actions/upload-artifact@v7
with:
name: playwright-traces-changed
name: playwright-traces-changed${{ inputs.artifact_suffix }}
path: test-results/
retention-days: 7
if-no-files-found: ignore
@@ -393,7 +399,7 @@ jobs:
native-runtime: electron
- uses: actions/download-artifact@v8
with:
name: e2e-build-out
name: e2e-build-out${{ inputs.artifact_suffix }}
path: out/
# The packaged orcad slot the T6-11 launcher runs: its own node-pty under the pinned Node.
# The template is what `orca serve`'s default selection materializes that slot from.
@@ -423,7 +429,7 @@ jobs:
- uses: actions/upload-artifact@v7
if: failure()
with:
name: orcad-serve-mode-switch-traces
name: orcad-serve-mode-switch-traces${{ inputs.artifact_suffix }}
path: test-results/
retention-days: 7
if-no-files-found: ignore
@@ -451,7 +457,7 @@ jobs:
native-runtime: electron
- uses: actions/download-artifact@v8
with:
name: e2e-build-out
name: e2e-build-out${{ inputs.artifact_suffix }}
path: out/
# Built to a path the app does not look at by default, so the relay phase has no template.
- name: Build the linux-x64-glibc orcad template
@@ -474,7 +480,7 @@ jobs:
- uses: actions/upload-artifact@v7
if: failure()
with:
name: orcad-auto-convert-docker-traces
name: orcad-auto-convert-docker-traces${{ inputs.artifact_suffix }}
path: test-results/
retention-days: 7
if-no-files-found: ignore
@@ -501,7 +507,7 @@ jobs:
native-runtime: electron
- uses: actions/download-artifact@v8
with:
name: e2e-build-out
name: e2e-build-out${{ inputs.artifact_suffix }}
path: out/
- name: Build the linux-x64-glibc orcad template
run: |
@@ -522,7 +528,7 @@ jobs:
- uses: actions/upload-artifact@v7
if: failure()
with:
name: orcad-journeys-docker-traces
name: orcad-journeys-docker-traces${{ inputs.artifact_suffix }}
path: test-results/
retention-days: 7
if-no-files-found: ignore
@@ -580,7 +586,7 @@ jobs:
- uses: actions/upload-artifact@v7
if: failure()
with:
name: orcad-serve-mode-switch-windows-traces
name: orcad-serve-mode-switch-windows-traces${{ inputs.artifact_suffix }}
path: test-results/
retention-days: 7
if-no-files-found: ignore
+1
View File
@@ -36,6 +36,7 @@ on:
- 'config/scripts/mobile-release-check-scope*'
- 'config/scripts/mobile-test-change-scope*'
- 'config/scripts/pr-code-change-scope.mjs'
- 'config/scripts/pr-layout-oracle-route.mjs'
# Why main too: two pull requests can each pass against their own base and disagree once both
# land, and `verify` never runs on main. The same source paths as above, less the CI inputs only
# `verify` reads.
+29
View File
@@ -50,6 +50,7 @@ jobs:
managed_hook_node18: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.managed_hook_node18 }}
package: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package }}
package_windows: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.package_windows }}
layout_oracle: ${{ steps.readiness.outputs.reused != 'true' && steps.filter.outputs.layout_oracle }}
e2e_should_run: ${{ steps.e2e_filter.outputs.should_run }}
test_files: ${{ steps.e2e_filter.outputs.test_files }}
e2e_run_changed: ${{ steps.e2e_filter.outputs.e2e_run_changed }}
@@ -75,6 +76,7 @@ jobs:
/.github/scripts/check-root-directory-entries.mjs
/config/scripts/check-readme-local-links.mjs
/config/scripts/pr-code-change-scope.mjs
/config/scripts/pr-layout-oracle-route.mjs
/config/scripts/pr-e2e-source-routing.mjs
/config/scripts/ci-e2e-job-selection.mjs
/config/scripts/pr-ready-check-reuse.mjs
@@ -1377,6 +1379,29 @@ jobs:
run_changed_e2e: ${{ needs.code_paths.outputs.e2e_run_changed != 'false' }}
needs_build: ${{ needs.code_paths.outputs.e2e_needs_build != 'false' }}
# Why its own required job, apart from `e2e`: the rest of the suite is red on main, but the
# layout oracle is green there, and its known-on-main list absorbs today's findings. Only the two
# oracle specs run, through the same reusable workflow: the window spec in the changed-spec lane,
# the headless spec in the orcad lane on both serve hosts.
layout_oracle:
name: layout oracle
needs: [code_paths, preflight]
if: >-
!cancelled() &&
needs.code_paths.result == 'success' &&
needs.code_paths.outputs.layout_oracle == 'true' &&
needs.preflight.result == 'success'
permissions:
contents: read
uses: ./.github/workflows/e2e.yml
with:
# Why the merge commit, unlike `e2e`: a gate must test what lands on main, not a stale head.
ref: ${{ github.sha }}
test_files: '["tests/e2e/workspace-layout-oracle-headless.spec.ts","tests/e2e/workspace-layout-oracle.spec.ts"]'
run_changed_e2e: true
needs_build: true
artifact_suffix: -layout-oracle
# Why this is not in verify's needs: it is the first PR-gate run of a harness whose reliability
# is only known from nightly main runs (20/20 green, 2026-08-09..2026-08-29, p50 3m25s). It
# reports a red X on the PR without blocking, exactly like `e2e` above. Deliberately no
@@ -1417,6 +1442,7 @@ jobs:
- managed_hook_node18
- package
- package_windows
- layout_oracle
# Evaluating job results only needs the lightweight container runner.
runs-on: ubuntu-slim
timeout-minutes: 5
@@ -1456,6 +1482,8 @@ jobs:
PACKAGE_SHOULD_RUN: ${{ needs.code_paths.outputs.package }}
PACKAGE_WINDOWS: ${{ needs.package_windows.result }}
PACKAGE_WINDOWS_SHOULD_RUN: ${{ needs.code_paths.outputs.package_windows }}
LAYOUT_ORACLE: ${{ needs.layout_oracle.result }}
LAYOUT_ORACLE_SHOULD_RUN: ${{ needs.code_paths.outputs.layout_oracle }}
run: |
if [ "$CODE_PATHS" != "success" ]; then
exit 1
@@ -1490,4 +1518,5 @@ jobs:
check_job managed_hook_node18 "$MANAGED_HOOK_NODE18" "$MANAGED_HOOK_NODE18_SHOULD_RUN"
check_job package "$PACKAGE" "$PACKAGE_SHOULD_RUN"
check_job package_windows "$PACKAGE_WINDOWS" "$PACKAGE_WINDOWS_SHOULD_RUN"
check_job layout_oracle "$LAYOUT_ORACLE" "$LAYOUT_ORACLE_SHOULD_RUN"
exit "$failed"
@@ -137,6 +137,7 @@ function fixture(detectorStep = detector) {
for (const file of [
'package.json',
'config/scripts/pr-code-change-scope.mjs',
'config/scripts/pr-layout-oracle-route.mjs',
'config/scripts/mobile-release-check-scope.mjs',
'config/scripts/mobile-test-change-scope.mjs'
]) {
@@ -9,6 +9,7 @@ const NON_TEST_FILES = new Set([
'.github/workflows/mobile-ios-release.yml',
'config/scripts/pr-code-change-scope.mjs',
'config/scripts/pr-code-change-scope.test.mjs',
'config/scripts/pr-layout-oracle-route.mjs',
'config/scripts/mobile-release-check-scope.mjs',
'config/scripts/mobile-release-check-scope.test.mjs'
])
@@ -301,6 +301,7 @@ function createMobileCiScopeRepository(
for (const file of [
'package.json',
'config/scripts/pr-code-change-scope.mjs',
'config/scripts/pr-layout-oracle-route.mjs',
'config/scripts/git-pull-request-diff-base.mjs',
'config/scripts/pr-e2e-source-routing.mjs',
'config/scripts/ci-e2e-job-selection.mjs'
+5 -1
View File
@@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import process from 'node:process'
import { pathToFileURL } from 'node:url'
import { isLayoutOracleSource } from './pr-layout-oracle-route.mjs'
const DOCS_ONLY_FILES = new Set([
'README.md',
@@ -30,7 +31,8 @@ export const PR_CHECK_JOBS = [
'cross-version-wire',
'managed_hook_node18',
'package',
'package_windows'
'package_windows',
'layout_oracle'
]
const ALWAYS_ON_CODE_JOBS = new Set(['static_analysis', 'typecheck', 'test'])
@@ -591,6 +593,8 @@ function jobDetector(job) {
return (files) => files.some(isLinuxPackagePath)
case 'package_windows':
return (files) => files.some(isWindowsPackagePath)
case 'layout_oracle':
return (files) => files.some(isLayoutOracleSource)
default:
return () => true
}
+3 -1
View File
@@ -394,7 +394,9 @@ describe('per-job path classification', () => {
expectClassification([file], {
'cross-version-wire': true,
package: true,
package_windows: true
package_windows: true,
// The session-tabs RPC publishes layout, so the layout oracle gates it too.
layout_oracle: file === 'src/main/runtime/rpc/methods/session-tabs.ts'
})
}
expectClassification(
@@ -1,71 +1,104 @@
import { describe, expect, it } from 'vitest'
import { existsSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { existsSync, readFileSync } from 'node:fs'
import { dirname, join, relative, resolve } from 'node:path'
import { parse } from 'yaml'
import { classifyE2eJobs, selectGeneralE2eSpecs } from './ci-e2e-job-selection.mjs'
import {
PR_E2E_SOURCE_ROUTES,
selectPrE2eSpecs,
shouldRunReusablePrE2e
} from './pr-e2e-source-routing.mjs'
import { classifyPrJobs } from './pr-code-change-scope.mjs'
import { isLayoutOracleSource, LAYOUT_ORACLE_E2E_SPECS } from './pr-layout-oracle-route.mjs'
import { selectPrE2eSpecs, shouldRunReusablePrE2e } from './pr-e2e-source-routing.mjs'
const projectDir = resolve(import.meta.dirname, '../..')
const ORACLE_ROUTE = PR_E2E_SOURCE_ROUTES.find((route) => route.id === 'workspace-layout.oracle')
const TAB_STORE_ROUTE = PR_E2E_SOURCE_ROUTES.find(
(route) => route.id === 'workspace-layout.oracle-tab-store'
)
const prWorkflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
/** One product file per layout writer family; each alone must run the oracle. */
const LAYOUT_AUTHORITIES = [
'src/shared/workspace-layout/workspace-layout-pane-commands.ts',
'src/shared/workspace-layout/old-client/index.ts',
'src/main/persistence/terminal-topology/terminal-topology-commit.ts',
'src/main/persistence/restoring-sessions/session-owner-fields.ts',
'src/main/persistence/loading-store/pty-binding-persistence.ts',
'src/main/remote-layout-snapshot/index.ts',
'src/main/runtime/orca-runtime-split-pty-backed-terminal.ts',
'src/main/runtime/orca-runtime-move-headless-mobile-session-tab.ts',
'src/main/runtime/orca-runtime-create-terminal.ts',
'src/renderer/src/runtime/mobile-session-tab-close.ts',
'src/renderer/src/runtime/runtime-layout-client.ts',
'src/renderer/src/store/slices/tabs.ts',
'src/renderer/src/store/slices/tabs/tabs-move-actions.ts',
'tests/e2e/workspace-layout-oracle-known-on-main.ts',
'tests/e2e/helpers/workspace-layout-oracle-compare.ts'
'tests/e2e/helpers/workspace-layout-oracle-compare.ts',
...LAYOUT_ORACLE_E2E_SPECS
]
describe('workspace layout oracle PR E2E routing', () => {
it('routes each layout authority on its own', () => {
expect(ORACLE_ROUTE, 'the oracle route was renamed or removed').toBeDefined()
for (const file of LAYOUT_AUTHORITIES) {
expect(selectPrE2eSpecs([file]), file).toEqual(ORACLE_ROUTE.specs.toSorted())
expect(shouldRunReusablePrE2e([file]), file).toBe(true)
}
})
/** Widened while the layout-holder stack is open: these still write layout. */
const STACK_WRITERS = [
'src/renderer/src/store/slices/terminals.ts',
'src/renderer/src/components/terminal-pane/TerminalPane.tsx',
'src/renderer/src/runtime/sync-runtime-graph.ts',
'src/renderer/src/runtime/web-session-tabs-sync.ts',
'src/main/runtime/orca-runtime-split-terminal.ts',
'src/main/runtime/orca-runtime-sync-window-graph.ts',
'src/main/runtime/mobile-session-layout-projection.ts',
'src/main/runtime/headless-runtime-graph.ts',
'src/main/persistence/loading-store/session-host-partitions.ts',
'src/main/persistence/loading-store/workspace-session-snapshot-publication.ts',
'src/renderer/src/store/slices/tab-group-reference-repair.ts',
'src/renderer/src/store/slices/terminal-session-row-hydration.ts',
'src/renderer/src/store/workspace-layout-cache.ts',
'src/renderer/src/app-shell/use-runtime-graph-sync.ts',
'src/renderer/src/components/tab-group/tab-drag-drop-commit.ts',
'src/shared/remote-layout-snapshot.ts',
'src/shared/terminal-leaf-move.ts',
'src/main/ipc/pty/ipc/leaf-move.ts',
'src/main/persistence/closed-terminal-tab-write-fence.ts',
'src/main/runtime/runtime-workspace-session-controller.ts',
'src/main/runtime/terminal-surface-close.ts',
'src/renderer/src/components/terminal/terminal-tab-actions.ts',
'src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts',
'src/shared/rpc-contract/workspace-layout-params.ts',
'src/renderer/src/store/slices/recently-closed-tabs.ts'
]
it('runs the window and headless oracles, not the SSH lane, for the tab store', () => {
for (const file of [
'src/renderer/src/store/slices/tabs.ts',
'src/renderer/src/store/slices/tabs/tabs-move-actions.ts'
]) {
expect(selectPrE2eSpecs([file]), file).toEqual(TAB_STORE_ROUTE.specs.toSorted())
/** Every test file the oracle specs import, transitively: a change there can break the gate. */
function oracleHarnessFiles() {
const seen = new Set()
const pending = [...LAYOUT_ORACLE_E2E_SPECS]
while (pending.length > 0) {
const file = pending.pop()
if (seen.has(file)) {
continue
}
seen.add(file)
const source = readFileSync(join(projectDir, file), 'utf8')
for (const [, specifier] of source.matchAll(/(?:from|import)\s*\(?\s*'(\.[^']+)'/g)) {
const base = relative(projectDir, join(projectDir, dirname(file), specifier))
const resolved = [base, `${base}.ts`, `${base}.tsx`].find(
(candidate) => /\.tsx?$/.test(candidate) && existsSync(join(projectDir, candidate))
)
if (resolved?.startsWith('tests/')) {
pending.push(resolved)
}
}
}
return [...seen]
}
describe('workspace layout oracle merge gate', () => {
it('routes each layout authority and stack writer on its own', () => {
for (const file of [...LAYOUT_AUTHORITIES, ...STACK_WRITERS]) {
expect(isLayoutOracleSource(file), file).toBe(true)
expect(classifyPrJobs([file]).layout_oracle, file).toBe(true)
expect(existsSync(join(projectDir, file)), file).toBe(true)
}
})
it('sends each oracle spec to the job that can run it', () => {
const specs = ORACLE_ROUTE.specs
// Window spec runs in the general shards; headless runs in its dedicated job.
expect(selectGeneralE2eSpecs(specs)).toEqual(['tests/e2e/workspace-layout-oracle.spec.ts'])
expect(classifyE2eJobs(JSON.stringify(specs))).toEqual({
e2e_run_changed: true,
e2e_needs_build: true
})
it('routes every test file the oracle specs import', () => {
const harness = oracleHarnessFiles()
expect(harness.length).toBeGreaterThan(LAYOUT_ORACLE_E2E_SPECS.length)
expect(harness.filter((file) => !isLayoutOracleSource(file))).toEqual([])
})
it('keeps every routed spec and authority a real file', () => {
for (const file of [...ORACLE_ROUTE.specs, ...LAYOUT_AUTHORITIES]) {
expect(existsSync(join(projectDir, file)), file).toBe(true)
}
})
it('leaves tests, fixtures and unrelated runtime modules alone', () => {
it('leaves tests, fixtures, harnesses and unrelated runtime modules alone', () => {
for (const file of [
'src/main/persistence/terminal-topology/terminal-topology-commit.test.ts',
'src/main/persistence/terminal-topology/terminal-leaf-move-fixture.ts',
@@ -75,12 +108,46 @@ describe('workspace layout oracle PR E2E routing', () => {
'src/main/runtime/rpc/methods/terminal/terminal-create.ts',
'src/main/runtime/runtime-rpc-database.ts',
'src/main/runtime/orca-runtime-files.ts',
'src/renderer/src/runtime/sync-runtime-graph.ts',
'src/renderer/src/runtime/remote-runtime-terminal-multiplexer.ts',
'src/renderer/src/runtime/web-session-tabs-sync-test-harness.ts',
'src/renderer/src/runtime/sync-runtime-graph-test-harness.ts',
'src/renderer/src/runtime/sync-runtime-graph.test.ts',
'tests/e2e/workspace-layout-oracle-latency.spec.ts'
]) {
expect(ORACLE_ROUTE.matches(file) || TAB_STORE_ROUTE.matches(file), file).toBe(false)
expect(isLayoutOracleSource(file), file).toBe(false)
}
expect(classifyPrJobs(['src/main/runtime/orca-runtime-files.ts']).layout_oracle).toBe(false)
})
it('keeps the oracle specs out of the advisory e2e job', () => {
expect(selectPrE2eSpecs(LAYOUT_AUTHORITIES)).toEqual([])
expect(shouldRunReusablePrE2e(LAYOUT_AUTHORITIES)).toBe(false)
})
it('runs exactly the oracle specs, each in the lane that can run it', () => {
const job = prWorkflow.jobs.layout_oracle
expect(job.uses).toBe('./.github/workflows/e2e.yml')
// The merge commit, so a stale-base PR cannot pass on an old main.
expect(job.with.ref).toBe('${{ github.sha }}')
const specs = JSON.parse(job.with.test_files)
expect(specs.toSorted()).toEqual(LAYOUT_ORACLE_E2E_SPECS.toSorted())
// Window spec runs in the changed-spec lane; headless runs in the orcad lane.
expect(selectGeneralE2eSpecs(specs)).toEqual(['tests/e2e/workspace-layout-oracle.spec.ts'])
expect(classifyE2eJobs(job.with.test_files)).toEqual({
e2e_run_changed: true,
e2e_needs_build: true
})
})
it('blocks the merge on the oracle and passes when it is skipped', () => {
const verify = prWorkflow.jobs.verify
const gate = verify.steps.find((step) => step.name === 'Require successful checks')
expect(verify.needs).toContain('layout_oracle')
expect(gate.run).toContain(
'check_job layout_oracle "$LAYOUT_ORACLE" "$LAYOUT_ORACLE_SHOULD_RUN"'
)
// Two calls of the reusable workflow in one run must not share artifact names.
expect(prWorkflow.jobs.layout_oracle.with.artifact_suffix).toBeTruthy()
expect(prWorkflow.jobs.e2e.with.artifact_suffix).toBeUndefined()
})
})
+7 -28
View File
@@ -5,10 +5,9 @@ import {
isDaemonInitTestSupportChange,
isUnitTestSupportSource
} from './pr-code-change-scope.mjs'
import { LAYOUT_ORACLE_E2E_SPECS } from './pr-layout-oracle-route.mjs'
const isProductSource = (file) => !/\.test\.tsx?$/.test(file) && !isUnitTestSupportSource(file)
const isLayoutOracleSource = (file) =>
isProductSource(file) && !/\.spec\.ts$|-fixtures?\.ts$/.test(file)
// Why config/patches: the xterm fork owns the helper textarea an input method attaches to, so a
// patch edit can break composition without touching a file named "ime".
@@ -384,31 +383,6 @@ export const PR_E2E_SOURCE_ROUTES = [
/^src\/.*(?:[Cc]lient-?[Hh]osted|browser-host-(?:lease|page|client-page)|browser-client-(?:host|page)|runtime-browser-(?:client-)?page|session-tabs-sync|host-session-snapshot-authority|orca-runtime(?:-browser)?\.ts|\/runtime-(?:status|types)\.ts)/.test(
file
)
},
{
// Why: the oracle is the only check that reads every view of the stored layout back. Runtime
// matches only the modules that create, move, close, persist or publish layout, not every
// session or terminal file, so the orcad build and Docker SSH lane stay off unrelated work.
id: 'workspace-layout.oracle',
specs: [
'tests/e2e/workspace-layout-oracle-headless.spec.ts',
'tests/e2e/workspace-layout-oracle.spec.ts'
],
matches: (file) =>
isLayoutOracleSource(file) &&
/^(?:src\/shared\/workspace-layout\/|src\/main\/persistence\/(?:terminal-topology\/|restoring-sessions\/|loading-store\/pty-binding-)|src\/(?:main|renderer\/src)\/runtime\/(?:[^/]+\/)*[^/]*(?:pane-admission|workspace-layout|session-tab|headless-[^/]*tab|layout-client|create-terminal|split-pty-backed-terminal|terminal-split-layout|spawn-placement)[^/]*\.ts$|tests\/e2e\/(?:helpers\/)?(?:workspace-layout-oracle|headless-layout-oracle|terminal-layout-journeys))/.test(
file
)
},
{
// The window's tab store reaches SSH through the same layout writers, so not the SSH lane.
id: 'workspace-layout.oracle-tab-store',
specs: [
'tests/e2e/workspace-layout-oracle-headless.spec.ts',
'tests/e2e/workspace-layout-oracle.spec.ts'
],
matches: (file) =>
isLayoutOracleSource(file) && file.startsWith('src/renderer/src/store/slices/tabs')
}
]
@@ -420,7 +394,12 @@ export function selectPrE2eSpecs(
const routePaths = isDaemonInitTestSupportChange(changedPaths, allowDaemonInitTestSupport)
? changedPaths.filter((file) => file !== DAEMON_INIT_TEST_SUPPORT)
: changedPaths
const specs = new Set(changedPaths.filter((file) => /^tests\/e2e\/.*\.spec\.ts$/.test(file)))
// The required layout_oracle job owns the oracle specs; running them here too would double the cost.
const specs = new Set(
changedPaths.filter(
(file) => /^tests\/e2e\/.*\.spec\.ts$/.test(file) && !LAYOUT_ORACLE_E2E_SPECS.includes(file)
)
)
for (const route of PR_E2E_SOURCE_ROUTES) {
const matchedFiles = routePaths.filter(route.matches)
if (matchedFiles.length === 0) {
+81
View File
@@ -0,0 +1,81 @@
// Paths whose change runs the required `layout_oracle` job in pr.yml.
/** The layout oracle's two specs; the job runs both and nothing else. */
export const LAYOUT_ORACLE_E2E_SPECS = [
'tests/e2e/workspace-layout-oracle-headless.spec.ts',
'tests/e2e/workspace-layout-oracle.spec.ts'
]
const LAYOUT_ORACLE_PREFIXES = [
// Layout model, persistence and compatibility folders.
'src/shared/workspace-layout/',
'src/shared/remote-layout-snapshot.ts',
'src/shared/terminal-leaf-move.ts',
'src/main/persistence/terminal-topology/',
'src/main/persistence/restoring-sessions/',
'src/main/persistence/loading-store/pty-binding-',
'src/main/persistence/loading-store/session-host-partitions.ts',
'src/main/persistence/loading-store/workspace-session-snapshot-publication.ts',
'src/main/remote-layout-snapshot/',
'src/main/runtime/old-session-tabs-stream/',
'src/renderer/src/runtime/remote-workspace-frozen/',
// Window-side layout writers the layout-holder stack replaces; widened while it is open.
'src/renderer/src/store/slices/tabs',
'src/renderer/src/store/slices/tab-group',
'src/renderer/src/store/slices/terminals',
'src/renderer/src/store/slices/terminal-',
'src/renderer/src/store/terminals/',
'src/renderer/src/store/workspace-layout-cache',
'src/renderer/src/components/terminal-pane/',
'src/renderer/src/components/tab-group/tab-drag-drop-commit.ts',
'src/renderer/src/app-shell/use-runtime-graph-sync.ts',
'src/renderer/src/runtime/sync-runtime-graph',
'src/renderer/src/runtime/web-session-tabs-sync',
// Other layout writers the stack edits.
'src/main/ipc/pty/ipc/leaf-move.ts',
'src/main/persistence/closed-terminal-tab-write-fence.ts',
'src/main/runtime/runtime-workspace-session-controller.ts',
'src/main/runtime/terminal-surface-close.ts',
'src/renderer/src/components/terminal/terminal-tab-actions.ts',
'src/renderer/src/components/tab-bar/web-runtime-tab-move-mirror.ts',
'src/shared/rpc-contract/workspace-layout-params.ts',
'src/renderer/src/store/slices/recently-closed-tabs.ts',
// The client the oracle reads every view through.
'src/cli/runtime/client.ts',
'src/shared/remote-runtime-client.ts',
'src/shared/remote-runtime-subscription-transport.ts',
'src/shared/paired-runtime-connection.ts',
'src/shared/pairing.ts',
// The harness: the oracle imports most of the shared e2e helpers, not just its own.
'tests/e2e/workspace-layout-oracle',
'tests/e2e/terminal-probe-input-sequence.ts',
'tests/e2e/global-',
'tests/e2e/helpers/',
'tests/playwright.config.ts',
'.github/scripts/e2e-with-window-manager.sh',
// The job itself: the called workflow, its window-spec lane picker, and this route.
'.github/workflows/e2e.yml',
'config/scripts/ci-e2e-job-selection.mjs',
'config/scripts/pr-layout-oracle-route.mjs'
]
// Runtime matches only the modules that create, move, close, persist or publish layout.
const LAYOUT_ORACLE_RUNTIME_MODULE =
/^src\/(?:main|renderer\/src)\/runtime\/(?:[^/]+\/)*[^/]*(?:pane-admission|workspace-layout|session-tab|headless-[^/]*tab|layout-client|layout-command-router|layout-projection|create-terminal|split-terminal|split-pty-backed-terminal|terminal-split-layout|spawn-placement|sync-window-graph|runtime-graph|leaf-move)[^/]*\.ts$/
// Why fixtures only under src/: there they serve unit tests; e2e fixtures are the oracle's harness.
const TEST_SUPPORT =
/\.(?:test|spec)\.(?:[cm]?js|tsx?)$|\/__tests__\/|^src\/.*-fixtures?\.ts$|-test-harness\.tsx?$/
export function isLayoutOracleSource(file) {
if (LAYOUT_ORACLE_E2E_SPECS.includes(file)) {
return true
}
if (TEST_SUPPORT.test(file)) {
return false
}
return (
LAYOUT_ORACLE_PREFIXES.some((prefix) => file.startsWith(prefix)) ||
LAYOUT_ORACLE_RUNTIME_MODULE.test(file)
)
}