mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* test: exercise fresh orchestration output and identity fixtures * test: check brief task values at the current output boundary * test: observe worker CLI receipts through actual output * test: reach sender lookup and print real worker wait states * test: verify retry deadlines and headless recovery admission * test: audit ACP lifecycle and recorded response boundaries * test: exercise mobile and CI admission contracts * test: check cache producers and exact capped output * test: reconcile upstream audit coverage and cache admission * docs: align integrated audit record hashes --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
633 lines
26 KiB
JavaScript
633 lines
26 KiB
JavaScript
import { mkdtempSync, mkdirSync, readFileSync, realpathSync, 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'
|
|
import { runProcessSync } from '@orca/process-host'
|
|
import { NODE_SERVER_RUNNERS, nodeServerQualification } from './node-server-qualification.mjs'
|
|
|
|
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)
|
|
})
|
|
|
|
function workspaceTree(imported = '@orca/process-host') {
|
|
const manifest = (name, source) =>
|
|
JSON.stringify({
|
|
name,
|
|
exports: { '.': { 'orca-source': source, default: './dist/index.js' } }
|
|
})
|
|
return moduleTree({
|
|
'entry.ts': `export * from '${imported}'`,
|
|
'pnpm-workspace.yaml': 'packages:\n - src/packages/*\n',
|
|
'src/packages/process-host/package.json': manifest(
|
|
'@orca/process-host',
|
|
'./src/run-process.ts'
|
|
),
|
|
'src/packages/process-host/src/run-process.ts':
|
|
"import('@orca/byte-buffer'); require('./private'); export const value = 1",
|
|
'src/packages/process-host/src/private.ts': 'module.exports = 2',
|
|
'src/packages/process-host/dist/index.js': "require('./missing-stale-output')",
|
|
'src/packages/byte-buffer/package.json': manifest('@orca/byte-buffer', './src/byte-buffer.ts'),
|
|
'src/packages/byte-buffer/src/byte-buffer.ts': 'export const value = 3',
|
|
'src/packages/unrelated/package.json': manifest('@orca/unrelated', './src/unrelated.ts'),
|
|
'src/packages/unrelated/src/unrelated.ts': 'throw Error("unrelated")'
|
|
})
|
|
}
|
|
|
|
it('tracks workspace source and manifest closures without trusting dist or widening to unrelated packages', async () => {
|
|
const root = workspaceTree()
|
|
const inputs = await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
|
expect([...inputs].sort()).toEqual([
|
|
'entry.ts',
|
|
'src/packages/byte-buffer/package.json',
|
|
'src/packages/byte-buffer/src/byte-buffer.ts',
|
|
'src/packages/process-host/package.json',
|
|
'src/packages/process-host/src/private.ts',
|
|
'src/packages/process-host/src/run-process.ts'
|
|
])
|
|
for (const file of [
|
|
'src/packages/process-host/package.json',
|
|
'src/packages/process-host/src/private.ts',
|
|
'src/packages/byte-buffer/src/byte-buffer.ts'
|
|
]) {
|
|
expect((await classifyNodeServerChanges([file], async () => inputs)).shouldRun).toBe(true)
|
|
}
|
|
expect(
|
|
(
|
|
await classifyNodeServerChanges(
|
|
['src/packages/unrelated/src/unrelated.ts'],
|
|
async () => inputs
|
|
)
|
|
).shouldRun
|
|
).toBe(false)
|
|
})
|
|
|
|
it.each([
|
|
'src/packages/process-host/.gitignore',
|
|
'src/packages/process-host/scripts/build-dist.mjs',
|
|
'src/packages/process-host/tsconfig.json'
|
|
])('runs for a change to the process-host build input %s', async (file) => {
|
|
const result = await classifyNodeServerChanges([file], async () => new Set())
|
|
expect(result.shouldRun).toBe(true)
|
|
expect(result.reason).toBe(`Build or CI input changed: ${file}`)
|
|
})
|
|
|
|
it.each(['@orca/unknown', '@orca/process-host/private'])(
|
|
'retains qualification for an unresolved internal package export: %s',
|
|
async (specifier) => {
|
|
const root = workspaceTree(specifier)
|
|
const result = await classifyNodeServerChanges(
|
|
['src/packages/process-host/src/run-process.ts'],
|
|
() => collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
|
)
|
|
expect(result.shouldRun).toBe(true)
|
|
expect(result.graphUnavailable).toBe(true)
|
|
expect(result.reason).toContain('No public orca-source export')
|
|
}
|
|
)
|
|
|
|
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/scripts/node-server-native-compat-scope.mjs',
|
|
'config/patches/node-pty@1.1.0.patch',
|
|
'native/windows-registry/src/addon.cc',
|
|
'.github/actions/install-node-dependencies/action.yml',
|
|
'.github/actions/restore-pnpm-verification/action.yml',
|
|
'.github/actions/prepare-headless-compiler/action.yml',
|
|
'config/scripts/headless-detector-compiler-cache.mjs',
|
|
'config/scripts/workspace-source-exports.mjs',
|
|
'config/scripts/workspace-source-exports.test.mjs',
|
|
'.github/actions/prepare-native-runtime/action.yml',
|
|
'.github/actions/prepare-orcad-prebuilds/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)
|
|
})
|
|
|
|
it('checks ordinary source unit files against actual imports before skipping the matrix', async () => {
|
|
const reachable = 'src/main/persistence/example/used.test.ts'
|
|
const unrelated = 'src/main/persistence/example/unrelated.test.tsx'
|
|
const removed = 'src/main/persistence/example/removed.test.ts'
|
|
const root = moduleTree({
|
|
'entry.ts': `import './${reachable}'; throw Error('never execute')`,
|
|
[reachable]: 'export const value = 1',
|
|
[unrelated]: 'throw Error("never execute unrelated test")'
|
|
})
|
|
const inputs = await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
|
expect(inputs.has(reachable)).toBe(true)
|
|
expect(inputs.has(unrelated)).toBe(false)
|
|
expect((await classifyNodeServerChanges([reachable], async () => inputs)).shouldRun).toBe(true)
|
|
expect((await classifyNodeServerChanges([unrelated], async () => inputs)).shouldRun).toBe(false)
|
|
expect((await classifyNodeServerChanges([removed], async () => inputs)).shouldRun).toBe(false)
|
|
})
|
|
|
|
it('retains qualification when a removed ordinary unit file breaks an actual import', async () => {
|
|
const removed = 'src/main/persistence/example/removed.test.ts'
|
|
const root = moduleTree({ 'entry.ts': `import './${removed}'` })
|
|
const result = await classifyNodeServerChanges([removed], () =>
|
|
collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
|
)
|
|
expect(result.shouldRun).toBe(true)
|
|
expect(result.graphUnavailable).toBe(true)
|
|
expect(result.reason).toContain('Dependency graph unavailable')
|
|
expect(result.reason).toContain('removed.test.ts')
|
|
})
|
|
|
|
it('requests the graph for an ordinary source unit edit before dependencies are installed', async () => {
|
|
const result = await classifyNodeServerChanges(
|
|
['src/main/persistence/example/unrelated.test.ts'],
|
|
async () => {
|
|
throw new Error('graph must not run before installation')
|
|
},
|
|
{ deferGraph: true }
|
|
)
|
|
expect(result.graphRequired).toBe(true)
|
|
expect(result.shouldRun).toBeUndefined()
|
|
})
|
|
|
|
it.each([
|
|
'src/main/persistence/profile-state/removed.test.ts',
|
|
'src/main/sqlite/removed.test.ts',
|
|
'src/main/orcad/orcad-entry.test.ts',
|
|
'src/main/daemon/pty-subprocess/removed.test.ts',
|
|
'src/main/providers/agent-foreground-process-git-bash.win32.test.ts',
|
|
'config/scripts/node-server-change-scope.test.mjs'
|
|
])('forces explicitly protected tests without relying on graph discovery: %s', async (file) => {
|
|
const result = await classifyNodeServerChanges([file], async () => {
|
|
throw new Error('protected test must not require graph analysis')
|
|
})
|
|
expect(result.shouldRun).toBe(true)
|
|
expect(result.graphUnavailable).toBeUndefined()
|
|
expect(result.reason).toBe(`Build or CI input changed: ${file}`)
|
|
})
|
|
|
|
it.each([
|
|
'src/main/persistence/example/worker.ts',
|
|
'src/main/persistence/example/state.fixture.ts',
|
|
'src/main/persistence/example/state.test.ts.fixture',
|
|
'src/main/persistence/example/state.spec.ts',
|
|
'src/main/persistence/example/../state.test.ts',
|
|
'src/main/persistence/example/./state.test.ts',
|
|
'.github/actions/prepare-native-runtime/cache.test.ts'
|
|
])(
|
|
'retains prefix protection for production, fixtures and other file conventions: %s',
|
|
async (file) => {
|
|
const result = await classifyNodeServerChanges([file], async () => {
|
|
throw new Error('protected input must not require graph analysis')
|
|
})
|
|
expect(result.shouldRun).toBe(true)
|
|
expect(result.graphUnavailable).toBeUndefined()
|
|
expect(result.reason).toBe(`Build or CI input changed: ${file}`)
|
|
}
|
|
)
|
|
|
|
it('retains a production force when an unrelated unit test changes in the same diff', async () => {
|
|
const result = await classifyNodeServerChanges(
|
|
['src/main/persistence/example/unrelated.test.ts', 'src/main/persistence/example/worker.ts'],
|
|
async () => {
|
|
throw new Error('production input must not require graph analysis')
|
|
}
|
|
)
|
|
expect(result.shouldRun).toBe(true)
|
|
expect(result.graphUnavailable).toBeUndefined()
|
|
expect(result.reason).toBe('Build or CI input changed: src/main/persistence/example/worker.ts')
|
|
})
|
|
|
|
it('defers uncertain paths without issuing a qualification verdict', async () => {
|
|
const result = await classifyNodeServerChanges(
|
|
['src/renderer/src/example.ts'],
|
|
async () => {
|
|
throw new Error('graph must not run before installation')
|
|
},
|
|
{ deferGraph: true }
|
|
)
|
|
expect(result.graphRequired).toBe(true)
|
|
expect(result.shouldRun).toBeUndefined()
|
|
})
|
|
|
|
it.each([
|
|
{ files: [], deferred: true, expected: 'should_run=true' },
|
|
{ files: ['package.json'], deferred: true, expected: 'should_run=true' },
|
|
{
|
|
files: ['src/main/persistence/profile-state/profile-state-windows.ts'],
|
|
deferred: true,
|
|
fullQualification: false,
|
|
qualification: false,
|
|
runners: ['ubuntu-22.04', 'windows-2022', 'windows-11-arm'],
|
|
expected: 'should_run=true'
|
|
},
|
|
{
|
|
files: ['src/main/persistence/profile-state/profile-state-windows.ts'],
|
|
deferred: true,
|
|
expected: 'should_run=true'
|
|
},
|
|
{
|
|
files: ['src/main/providers/provider-windows.ts'],
|
|
deferred: true,
|
|
fullQualification: false,
|
|
expected: 'should_run=true'
|
|
},
|
|
{
|
|
files: ['src/main/persistence/profile-state/profile-state-recovery-crash-boundaries.test.ts'],
|
|
deferred: true,
|
|
fullQualification: false,
|
|
pullRequest: true,
|
|
nativeGraph: true,
|
|
expected: 'should_run=true'
|
|
},
|
|
{ files: ['src/renderer/src/example.ts'], deferred: true, expected: 'graph_required=true' },
|
|
{ files: ['src/renderer/src/example.ts'], deferred: false, expected: 'should_run=true' }
|
|
])('fails closed or requests dependencies in an uninstalled checkout: %j', (scenario) => {
|
|
const root = moduleTree(
|
|
Object.fromEntries(
|
|
[
|
|
'node-server-change-scope',
|
|
'node-server-test-paths',
|
|
'node-server-qualification',
|
|
'node-server-native-compat-scope'
|
|
].map((name) => [
|
|
`config/scripts/${name}.mjs`,
|
|
readFileSync(new URL(`./${name}.mjs`, import.meta.url), 'utf8')
|
|
])
|
|
)
|
|
)
|
|
const changes = join(root, 'changes')
|
|
const stepOutput = join(root, 'step-output')
|
|
const statuses = join(root, 'native-diff')
|
|
writeFileSync(changes, scenario.files.map((file) => `${file}\0`).join(''))
|
|
writeFileSync(
|
|
statuses,
|
|
scenario.files
|
|
.map((file) => `:100644 100644 ${'1'.repeat(40)} ${'2'.repeat(40)} M\0${file}\0`)
|
|
.join('')
|
|
)
|
|
const result = runProcessSync({
|
|
program: process.execPath,
|
|
args: [
|
|
realpathSync(join(root, 'config/scripts/node-server-change-scope.mjs')),
|
|
changes,
|
|
...(scenario.fullQualification === false ? [] : ['--full-qualification']),
|
|
...(scenario.deferred ? ['--defer-graph'] : []),
|
|
...(scenario.pullRequest ? ['--pull-request', '--diff-paths', statuses] : [])
|
|
],
|
|
cwd: root,
|
|
env: { ...process.env, GITHUB_OUTPUT: stepOutput },
|
|
timeoutMs: 5_000
|
|
})
|
|
expect(result.code).toBe(0)
|
|
const output = readFileSync(stepOutput, 'utf8')
|
|
expect(output).toContain(scenario.expected)
|
|
if (scenario.nativeGraph) {
|
|
expect(output).toContain('graph_required=true')
|
|
expect(output).not.toContain('native_compatibility=')
|
|
}
|
|
if (scenario.expected === 'graph_required=true') {
|
|
expect(output).not.toContain('should_run=')
|
|
expect(output).not.toContain('runners=')
|
|
} else {
|
|
expect(output).toContain(`qualification=${scenario.qualification !== false}`)
|
|
expect(output).toContain(`runners=${JSON.stringify(scenario.runners ?? NODE_SERVER_RUNNERS)}`)
|
|
}
|
|
})
|
|
|
|
const registry = 'config/scripts/vitest-node-runtime-files.mjs'
|
|
const unrelatedSshUnit = 'src/main/ssh/pinned-runtime-materializer-context.test.ts'
|
|
|
|
function qualificationTree(extra = {}) {
|
|
return moduleTree({
|
|
'entry.ts': `import './${registry}'; throw Error('never execute')`,
|
|
[registry]: 'export const NODE_RUNTIME_INCLUDE = []',
|
|
[unrelatedSshUnit]: 'throw Error("never execute unrelated test")',
|
|
...extra
|
|
})
|
|
}
|
|
|
|
it('keeps a Node harness smoke without widening it for an unrelated modified SSH unit', async () => {
|
|
const root = qualificationTree()
|
|
writeFileSync(join(root, registry), 'export const NODE_RUNTIME_INCLUDE = ["new.unit.test.ts"]')
|
|
const changed = [registry, unrelatedSshUnit]
|
|
const scope = await classifyNodeServerChanges(
|
|
changed,
|
|
() => collectNodeServerInputs({ root, entryPoints: ['entry.ts'] }),
|
|
{ modifiedFiles: changed }
|
|
)
|
|
expect(scope.shouldRun).toBe(true)
|
|
expect(nodeServerQualification(changed, scope)).toEqual({
|
|
qualification: false,
|
|
runners: ['ubuntu-22.04']
|
|
})
|
|
expect(nodeServerQualification(changed, scope, { fullQualification: true })).toEqual({
|
|
qualification: true,
|
|
runners: NODE_SERVER_RUNNERS
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
['src/main/daemon/daemon-authenticated-client-activity.test.ts', NODE_SERVER_RUNNERS, true],
|
|
[
|
|
'src/main/daemon/daemon-endpoint-windows.test.ts',
|
|
['ubuntu-22.04', 'windows-2022', 'windows-11-arm'],
|
|
false
|
|
]
|
|
])('retains daemon platform coverage: %s', async (daemon, runners, qualification) => {
|
|
const root = qualificationTree({ [daemon]: 'throw Error("unselected")' })
|
|
const diff = [registry, daemon, unrelatedSshUnit]
|
|
const g = await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
|
expect(g.has(registry)).toBe(true)
|
|
expect(g.has(daemon)).toBe(false)
|
|
const s = await classifyNodeServerChanges(diff, async () => g, { modifiedFiles: diff })
|
|
expect(s.shouldRun).toBe(true)
|
|
expect(nodeServerQualification(diff, s)).toEqual({ qualification, runners })
|
|
})
|
|
|
|
it('keeps every host when the modified SSH production module is an actual consumer', async () => {
|
|
const ssh = 'src/main/ssh/pinned-runtime-materializer.ts'
|
|
const root = qualificationTree({
|
|
'entry.ts': `import './${registry}'; import './${ssh}'; throw Error('never execute')`,
|
|
[ssh]: 'export const materializer = 1'
|
|
})
|
|
const changed = [registry, ssh, unrelatedSshUnit]
|
|
const scope = await classifyNodeServerChanges(
|
|
changed,
|
|
() => collectNodeServerInputs({ root, entryPoints: ['entry.ts'] }),
|
|
{ modifiedFiles: changed }
|
|
)
|
|
expect(scope.shouldRun).toBe(true)
|
|
expect(scope).toMatchObject({ graphResolved: true, matchedFiles: [registry, ssh] })
|
|
expect(nodeServerQualification(changed, scope)).toEqual({
|
|
qualification: true,
|
|
runners: NODE_SERVER_RUNNERS
|
|
})
|
|
})
|
|
|
|
it.each(['added', 'deleted', 'renamed', 'unknown', 'duplicate', 'foreign'])(
|
|
'retains complete-path qualification when modification evidence is %s',
|
|
async (status) => {
|
|
const root = qualificationTree()
|
|
const changed = [registry, unrelatedSshUnit]
|
|
let modifiedFiles = [registry]
|
|
if (status === 'deleted' || status === 'renamed') {
|
|
rmSync(join(root, unrelatedSshUnit))
|
|
}
|
|
if (status === 'renamed') {
|
|
const renamed = 'src/main/ssh/renamed-context.test.ts'
|
|
writeFileSync(join(root, renamed), 'throw Error("never execute renamed test")')
|
|
changed.push(renamed)
|
|
}
|
|
if (status === 'duplicate') {
|
|
modifiedFiles = [registry, registry]
|
|
}
|
|
if (status === 'foreign') {
|
|
modifiedFiles = [registry, 'src/main/ssh/foreign.test.ts']
|
|
}
|
|
const scope = await classifyNodeServerChanges(
|
|
changed,
|
|
() => collectNodeServerInputs({ root, entryPoints: ['entry.ts'] }),
|
|
status === 'unknown' ? {} : { modifiedFiles }
|
|
)
|
|
expect(scope.shouldRun).toBe(true)
|
|
expect(nodeServerQualification(changed, scope)).toEqual({
|
|
qualification: true,
|
|
runners: NODE_SERVER_RUNNERS
|
|
})
|
|
}
|
|
)
|
|
|
|
it('retains protected server tests and broken imports despite complete modification evidence', async () => {
|
|
const protectedTest = 'src/main/sqlite/example.test.ts'
|
|
const root = qualificationTree({ [protectedTest]: 'export const value = 1' })
|
|
const changed = [registry, protectedTest, unrelatedSshUnit]
|
|
const collect = () => collectNodeServerInputs({ root, entryPoints: ['entry.ts'] })
|
|
const protectedScope = await classifyNodeServerChanges(changed, collect, {
|
|
modifiedFiles: changed
|
|
})
|
|
expect(nodeServerQualification(changed, protectedScope)).toEqual({
|
|
qualification: true,
|
|
runners: NODE_SERVER_RUNNERS
|
|
})
|
|
writeFileSync(join(root, 'entry.ts'), `import './${registry}'; import './missing'`)
|
|
const brokenChanged = [registry, unrelatedSshUnit]
|
|
const brokenScope = await classifyNodeServerChanges(brokenChanged, collect, {
|
|
modifiedFiles: brokenChanged
|
|
})
|
|
expect(brokenScope.graphUnavailable).toBe(true)
|
|
expect(nodeServerQualification(brokenChanged, brokenScope)).toEqual({
|
|
qualification: true,
|
|
runners: NODE_SERVER_RUNNERS
|
|
})
|
|
})
|
|
|
|
describe('the actual Bun build and profile-test dependency graph', () => {
|
|
let inputs
|
|
beforeAll(async () => {
|
|
inputs = await collectNodeServerInputs()
|
|
}, 60_000)
|
|
|
|
it('tracks the shared close probe without pulling in its mocked renderer adapter', () => {
|
|
expect(inputs.has('src/shared/pty-running-work-probe.ts')).toBe(true)
|
|
expect(inputs.has('src/shared/pty-running-work-probe.test.ts')).toBe(true)
|
|
expect(inputs.has('src/renderer/src/components/terminal/pty-running-work-probe.ts')).toBe(false)
|
|
expect(inputs.has('src/renderer/src/runtime/runtime-terminal-inspection.ts')).toBe(false)
|
|
expect([...inputs].some((file) => file.startsWith('src/renderer/'))).toBe(false)
|
|
})
|
|
|
|
it.each([
|
|
'config/scripts/ci-shard-timings.json',
|
|
'config/scripts/mobile-web-app/mobile-web-app-terminal-render.test.mjs',
|
|
'src/renderer/src/components/terminal/pty-running-work-probe.ts',
|
|
'src/renderer/src/runtime/runtime-terminal-inspection.ts',
|
|
'src/main/ssh/ssh-relay-upload-stage-commands.test.ts',
|
|
'src/main/persistence/terminal-topology/terminal-topology-boundary-ratchet.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/shared/pty-running-work-probe.ts',
|
|
'src/shared/pty-running-work-probe.test.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/windows-process-tree-prepared-addon.mjs',
|
|
'config/scripts/orcad-windows-prebuild-cache.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(0)
|
|
expect(tests.every((file) => inputs.has(file))).toBe(true)
|
|
expect(
|
|
nodeServerTestPaths().every((selector) => tests.some((file) => file.includes(selector)))
|
|
).toBe(true)
|
|
})
|
|
})
|
|
|
|
it('keeps every platform job 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.on).toHaveProperty('workflow_call')
|
|
expect(workflow.on).not.toHaveProperty('push')
|
|
expect(workflow.on.schedule).toHaveLength(1)
|
|
expect(workflow.jobs.changes.if).toBe(
|
|
"github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft != true)"
|
|
)
|
|
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(detect.env.PUSH_BASE).toBe('${{ github.event.before }}')
|
|
expect(detect.run).toContain('git fetch --no-tags --depth=1 origin "$PUSH_BASE"')
|
|
expect(detect.run).toContain('git diff --name-only --no-renames -z "$PUSH_BASE" HEAD')
|
|
expect(detect.run).toContain('node-server-changes" --defer-graph --full-qualification')
|
|
expect(workflow.on.pull_request.types).toContain('ready_for_review')
|
|
// Release callers can inherit push or schedule events without a pull request.
|
|
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', 'mobile_web_bundle'])
|
|
// The containers never install mobile/, and orcad refuses to build without the page.
|
|
expect(job.steps.some((step) => step.with?.name === 'orcad-mobile-web-bundle')).toBe(true)
|
|
expect(job.steps.some((step) => step.name === 'Wait for the qualified mobile bundle')).toBe(
|
|
false
|
|
)
|
|
expect(job.if).not.toContain('needs.persistence')
|
|
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('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}$/)
|
|
expect(setupBun.with['bun-version-file']).toBe('config/.bun-version')
|
|
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(setupBun.if).toBe("runner.os == 'Linux'")
|
|
expect(build.id).toBe('bun-orcad')
|
|
expect(build.background).toBe(true)
|
|
expect(build.if).toBeUndefined()
|
|
expect(build['continue-on-error']).toBeUndefined()
|
|
expect(build.run).toMatch(/^if \[ "\$RUNNER_OS" != Linux \]; then exit 0; fi\n/)
|
|
expect(build.run).toContain(
|
|
'pnpm --dir "$RUNNER_TEMP/bun-orcad-source" install --frozen-lockfile --ignore-scripts'
|
|
)
|
|
expect(build.run).not.toContain('"$GITHUB_WORKSPACE/node_modules"')
|
|
expect(build.run).toContain('echo "slot=$RUNNER_TEMP/bun-orcad" >> "$GITHUB_OUTPUT"')
|
|
expect(build.run).toContain('echo "executable=$(command -v bun)" >> "$GITHUB_OUTPUT"')
|
|
expect(build.run).not.toContain('GITHUB_ENV')
|
|
const join = steps.findIndex((step) => step.wait === build.id)
|
|
expect(join).toBeGreaterThan(steps.indexOf(build))
|
|
expect(steps[join].if).toBeUndefined()
|
|
expect(steps[join]['continue-on-error']).toBeUndefined()
|
|
const consumer = steps.find((step) => step.run?.startsWith('pnpm test:node-server --artifact '))
|
|
expect(steps.indexOf(consumer)).toBeGreaterThan(join)
|
|
expect(consumer.run).toBe(
|
|
"pnpm test:node-server --artifact ${{ runner.os == 'Linux' && '--cross-runtime' || '' }}"
|
|
)
|
|
expect(consumer.if).toBeUndefined()
|
|
expect(consumer.env).toEqual({
|
|
ORCA_BUN_ORCAD_SLOT: '${{ steps.bun-orcad.outputs.slot }}',
|
|
BUN_EXECUTABLE: '${{ steps.bun-orcad.outputs.executable }}'
|
|
})
|
|
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} `
|
|
)
|
|
)
|
|
})
|