mirror of
https://github.com/stablyai/orca.git
synced 2026-10-02 08:02:02 +00:00
* 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
224 lines
9.4 KiB
JavaScript
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} `
|
|
)
|
|
)
|
|
})
|