Files
orca/config/scripts/node-server-change-scope.test.mjs
T
Neil 8ff6296bc7 Speed up serializer checks and keep native caches stable (#24476)
* Reuse serializer oracle cells and isolate native cache policy

* Preserve native cache post-save paths and record hosted oracle gain

* Record native cache reuse and separate cancel-test startup budget
2026-10-01 21:43:56 -07:00

224 lines
9.4 KiB
JavaScript

import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
import { parse } from 'yaml'
import {
classifyNodeServerChanges,
collectNodeServerInputs,
discoverNodeServerTests
} from './node-server-change-scope.mjs'
import { nodeServerTestPaths } from './node-server-test-paths.mjs'
import { ORCAD_CHILD_ENTRY_POINTS } from './orcad-entry-build.mjs'
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
const temporaryDirs = []
afterEach(() => {
for (const root of temporaryDirs.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
function moduleTree(files) {
const root = mkdtempSync(join(tmpdir(), 'node-server-scope-'))
temporaryDirs.push(root)
for (const [file, source] of Object.entries(files)) {
const path = join(root, file)
mkdirSync(join(path, '..'), { recursive: true })
writeFileSync(path, source)
}
return root
}
it('follows static imports, re-exports, dynamic imports and require without executing source', async () => {
const root = moduleTree({
'entry.ts': `import './first'; export * from './exports'; import('./dynamic'); require('./required'); throw Error('never execute')`,
'first.ts': `import './nested/leaf'`,
'exports.ts': 'export const value = 1',
'dynamic.ts': 'export const value = 2',
'required.ts': 'module.exports = 3',
'nested/leaf.ts': 'export const value = 4',
'unrelated.ts': 'throw Error("unrelated")'
})
const inputs = await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
expect([...inputs].sort()).toEqual([
'dynamic.ts',
'entry.ts',
'exports.ts',
'first.ts',
'nested/leaf.ts',
'required.ts'
])
})
it('runs the matrix when a dependency is deleted or graph analysis fails', async () => {
const root = moduleTree({ 'entry.ts': `import './deleted'` })
const result = await classifyNodeServerChanges(['deleted.ts'], () =>
collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
)
expect(result.shouldRun).toBe(true)
expect(result.reason).toContain('Dependency graph unavailable')
expect((await classifyNodeServerChanges([])).shouldRun).toBe(true)
})
it.each([
['tests/e2e/daemon-running-work-probe.unit.test.ts'],
['config/scripts/zip-extractor-command.test.mjs'],
['config/scripts/zip-extractor-command.test.mjs', 'config/scripts/renamed-command.test.mjs']
])(
'runs deleted or renamed selected tests even when absent from the graph: %j',
async (...files) => {
expect((await classifyNodeServerChanges(files, async () => new Set())).shouldRun).toBe(true)
}
)
it.each([
'package.json',
'pnpm-lock.yaml',
'pnpm-workspace.yaml',
'.npmrc',
'tsconfig.json',
'config/tsconfig.node.json',
'config/scripts/node-server-qualification.mjs',
'config/patches/node-pty@1.1.0.patch',
'native/windows-registry/src/addon.cc',
'.github/actions/install-node-dependencies/action.yml',
'.github/actions/prepare-native-runtime/action.yml',
'.github/workflows/node-server-tests.yml',
'src/main/persistence/profile-state/new-worker.ts'
])('always selects build, native and dynamically opened inputs: %s', async (file) => {
expect((await classifyNodeServerChanges([file], async () => new Set())).shouldRun).toBe(true)
})
describe('the actual Bun build and profile-test dependency graph', () => {
let inputs
beforeAll(async () => {
inputs = await collectNodeServerInputs()
}, 60_000)
it.each([
'config/scripts/ci-shard-timings.json',
'config/scripts/mobile-web-app-terminal-render.test.mjs',
'src/main/ssh/ssh-relay-upload-stage-commands.test.ts',
'src/main/menu/register-app-menu.ts'
])('skips unrelated work: %s', async (file) => {
expect((await classifyNodeServerChanges([file], async () => inputs)).shouldRun).toBe(false)
})
it.each([
...Object.values(ORCAD_CHILD_ENTRY_POINTS),
'src/shared/keybindings/definitions-core-1.ts',
'src/main/runtime/orca-runtime.ts',
'src/main/windows/windows-process-table.ts',
'src/main/worker-thread-entry-path.ts',
'config/scripts/zip-extractor-command.mjs',
'config/scripts/windows-process-tree-gyp-rebuild.mjs',
'config/scripts/profile-state-worker-smoke.mjs',
'config/scripts/vitest-host-ports-setup.ts',
'tests/e2e/daemon-running-work-probe.unit.test.ts'
])('retains the full matrix for a real runtime, worker or test input: %s', async (file) => {
expect(inputs.has(file)).toBe(true)
expect((await classifyNodeServerChanges([file], async () => inputs)).shouldRun).toBe(true)
})
it('retains all selected tests and the selectors the Bun runner uses', () => {
const tests = discoverNodeServerTests()
expect(tests.length).toBeGreaterThan(80)
expect(tests.every((file) => inputs.has(file))).toBe(true)
expect(
nodeServerTestPaths().every((selector) => tests.some((file) => file.includes(selector)))
).toBe(true)
})
})
it('keeps all ten platform jobs and runs them when detection is skipped or fails', () => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/node-server-tests.yml', import.meta.url), 'utf8')
)
expect(workflow.on).toHaveProperty('workflow_dispatch')
expect(workflow.jobs.changes.if).toBe("github.event_name == 'pull_request'")
expect(workflow.jobs.changes.steps[0].with['fetch-depth']).toBe(2)
expect(workflow.jobs.changes.steps[0].with['persist-credentials']).toBe(false)
const detect = workflow.jobs.changes.steps.find((step) => step.id === 'scope')
expect(detect.run).toContain('git diff --name-only --no-renames -z HEAD^1 HEAD')
expect(workflow.on.pull_request.types).toContain('ready_for_review')
expect(workflow.on.schedule).toHaveLength(1)
// A pull request may qualify one platform, so the merged commit must re-qualify all six.
expect(workflow.on.push.branches).toEqual(['main'])
expect(workflow.on.push.paths).toEqual(workflow.on.pull_request.paths)
// The push and schedule paths must not rest on a null property comparison.
expect(workflow.jobs.persistence.if).toContain(
"github.event_name != 'pull_request' || github.event.pull_request.draft != true"
)
expect(workflow.jobs.persistence.strategy.matrix.os).toContain('needs.changes.outputs.runners')
for (const jobName of ['linux_glibc_floor', 'linux_musl']) {
const job = workflow.jobs[jobName]
expect(job.needs).toEqual(['changes', 'persistence'])
expect(job.if).toContain("needs.persistence.result == 'success'")
expect(job.if).toContain("needs.changes.outputs.qualification != 'false'")
expect(job.if).toContain("needs.changes.outputs.should_run != 'false'")
expect(job.strategy.matrix.os).toEqual(['ubuntu-22.04', 'ubuntu-24.04-arm'])
}
})
it('builds server glibc slots on glibc 2.28 and the compat slot on glibc 2.17 (design D6)', () => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/node-server-tests.yml', import.meta.url), 'utf8')
)
const floor = workflow.jobs.linux_glibc_floor
expect(floor.container).toBe('${{ matrix.image }}')
expect(floor.strategy.matrix.include).toEqual([
{
os: 'ubuntu-22.04',
image: expect.stringMatching(/^quay\.io\/pypa\/manylinux_2_28_x86_64@sha256:[0-9a-f]{64}$/)
},
{
os: 'ubuntu-24.04-arm',
image: expect.stringMatching(/^quay\.io\/pypa\/manylinux_2_28_aarch64@sha256:[0-9a-f]{64}$/)
}
])
expect(floor.env).toMatchObject({ CC: 'gcc', CXX: 'g++' })
const compat = workflow.jobs.linux_glibc217_compat
expect(compat.needs).toEqual(['changes', 'persistence'])
expect(compat.if).toBe(floor.if)
expect(compat['runs-on']).toBe('ubuntu-22.04')
const run = compat.steps.map((step) => step.run ?? '').join('\n')
expect(run).toMatch(/quay\.io\/pypa\/manylinux2014_x86_64@sha256:[0-9a-f]{64} /)
expect(run).toContain('--slot=linux-x64-glibc217 --print-runtime')
expect(run).toContain('build-orcad-prebuilds.mjs --slot=linux-x64-glibc217\n')
expect(run).toContain('--require-slots linux-x64-glibc217')
expect(run).toContain(
'env -u LD_LIBRARY_PATH node config/scripts/build-orcad-prebuilds.mjs --slot=linux-x64-glibc217 --smoke'
)
})
it('runs the Bun and Node cross-runtime tests on Linux against pinned inputs', () => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/node-server-tests.yml', import.meta.url), 'utf8')
)
const steps = workflow.jobs.persistence.steps
const setupBun = steps.find((step) => String(step.uses).startsWith('oven-sh/setup-bun@'))
expect(setupBun.uses).toMatch(/^oven-sh\/setup-bun@[0-9a-f]{40}$/)
// Mirrors LAST_BUN_ORCAD_VERSION in src/main/orcad/orcad-node-slot-fixture.ts.
expect(setupBun.with['bun-version']).toBe('1.4.2')
const build = steps.find((step) => String(step.run).includes('build-orcad-bun.mjs'))
expect(build.env.BUN_ORCAD_COMMIT).toMatch(/^[0-9a-f]{40}$/)
expect(build.run).toContain('ORCA_BUN_ORCAD_SLOT=')
expect(build.run).toContain('BUN_EXECUTABLE=')
for (const step of [setupBun, build]) {
expect(step.if).toBe("runner.os == 'Linux'")
}
expect(steps.map((step) => step.run).join('\n')).toContain(
"pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}"
)
const alpine = workflow.jobs.linux_musl.steps.find((step) =>
String(step.run).includes('docker run')
)
expect(alpine.run).toMatch(
new RegExp(
`node:${NODE_RUNTIME_PIN.version.replaceAll('.', '\\.')}-alpine@sha256:[0-9a-f]{64} `
)
)
})