mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
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:
+17
-11
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user