mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
[CI] Avoid native jobs for isolated policy self-test edits (#28334)
This commit is contained in:
@@ -23,6 +23,10 @@ const BUILD_SCRIPTS = [
|
||||
'config/scripts/vitest-host-ports-setup.ts',
|
||||
'config/scripts/vitest-real-agent-home-write-guard.ts'
|
||||
]
|
||||
const NODE_SERVER_POLICY_TESTS = new Set([
|
||||
'config/scripts/node-server-change-scope.test.mjs',
|
||||
'config/scripts/node-server-qualification.test.mjs'
|
||||
])
|
||||
const ALWAYS_FILES = new Set([
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
@@ -32,12 +36,11 @@ const ALWAYS_FILES = new Set([
|
||||
'tsconfig.json',
|
||||
'.github/workflows/node-server-tests.yml',
|
||||
'config/scripts/node-server-change-scope.mjs',
|
||||
'config/scripts/node-server-change-scope.test.mjs',
|
||||
...NODE_SERVER_POLICY_TESTS,
|
||||
'config/scripts/workspace-source-exports.mjs',
|
||||
'config/scripts/workspace-source-exports.test.mjs',
|
||||
'config/scripts/headless-detector-compiler-cache.mjs',
|
||||
'config/scripts/node-server-qualification.mjs',
|
||||
'config/scripts/node-server-qualification.test.mjs',
|
||||
// The source graph sees process-host source, not the build that emits the dist servers load.
|
||||
'src/packages/process-host/.gitignore',
|
||||
'src/packages/process-host/scripts/build-dist.mjs',
|
||||
@@ -153,10 +156,16 @@ export async function classifyNodeServerChanges(
|
||||
if (changedFiles.length === 0) {
|
||||
return { shouldRun: true, reason: 'No complete changed-file evidence' }
|
||||
}
|
||||
const modifiedOnly =
|
||||
Array.isArray(modifiedFiles) &&
|
||||
new Set(changedFiles).size === changedFiles.length &&
|
||||
new Set(modifiedFiles).size === modifiedFiles.length &&
|
||||
modifiedFiles.length === changedFiles.length &&
|
||||
changedFiles.every((file) => modifiedFiles.includes(file))
|
||||
const selectors = nodeServerTestPaths({ artifact: true, crossRuntime: true, template: true })
|
||||
const forced = changedFiles.find(
|
||||
(file) =>
|
||||
ALWAYS_FILES.has(file) ||
|
||||
(ALWAYS_FILES.has(file) && !(modifiedOnly && NODE_SERVER_POLICY_TESTS.has(file))) ||
|
||||
(ALWAYS_PREFIXES.some((prefix) => file.startsWith(prefix)) && !isSourceUnitTest(file)) ||
|
||||
/^config\/scripts\/[^/]*orcad[^/]*$/.test(file) ||
|
||||
selectors.some((selector) => file.includes(selector))
|
||||
@@ -171,12 +180,6 @@ export async function classifyNodeServerChanges(
|
||||
const inputs = await collect()
|
||||
const matchedFiles = changedFiles.filter((file) => inputs.has(file))
|
||||
const matched = matchedFiles[0]
|
||||
const modifiedOnly =
|
||||
Array.isArray(modifiedFiles) &&
|
||||
new Set(changedFiles).size === changedFiles.length &&
|
||||
new Set(modifiedFiles).size === modifiedFiles.length &&
|
||||
modifiedFiles.length === changedFiles.length &&
|
||||
changedFiles.every((file) => modifiedFiles.includes(file))
|
||||
return {
|
||||
...(modifiedOnly ? { graphResolved: true, matchedFiles } : {}),
|
||||
shouldRun: Boolean(matched),
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { classifyNodeServerChanges, collectNodeServerInputs } from './node-server-change-scope.mjs'
|
||||
import { NODE_SERVER_RUNNERS, nodeServerQualification } from './node-server-qualification.mjs'
|
||||
import { discoverUnitFiles } from './ci-unit-files.mjs'
|
||||
import { planUnitSelection } from './ci-unit-selection.mjs'
|
||||
import { classifyPrJobs } from './pr-code-change-scope.mjs'
|
||||
|
||||
const policyTests = [
|
||||
'config/scripts/node-server-change-scope.test.mjs',
|
||||
'config/scripts/node-server-qualification.test.mjs'
|
||||
]
|
||||
|
||||
const unavailableGraph = async () => {
|
||||
throw new Error('graph unavailable')
|
||||
}
|
||||
|
||||
describe('modified Node-server policy self-tests', () => {
|
||||
let inputs
|
||||
let unitFiles
|
||||
beforeAll(async () => {
|
||||
inputs = await collectNodeServerInputs()
|
||||
unitFiles = discoverUnitFiles()
|
||||
}, 60_000)
|
||||
|
||||
it.each(policyTests)(
|
||||
'keeps %s in required root units without native qualification',
|
||||
async (file) => {
|
||||
expect(classifyPrJobs([file], { allowMobileCiTools: true })).toMatchObject({
|
||||
test: true,
|
||||
static_analysis: true,
|
||||
typecheck: true,
|
||||
mobile_ci_tools: false
|
||||
})
|
||||
expect(inputs.has(file)).toBe(false)
|
||||
const scope = await classifyNodeServerChanges([file], async () => inputs, {
|
||||
modifiedFiles: [file]
|
||||
})
|
||||
expect(scope).toMatchObject({ shouldRun: false, graphResolved: true, matchedFiles: [] })
|
||||
const plan = planUnitSelection({
|
||||
files: unitFiles,
|
||||
changed: [file],
|
||||
graph: () => {
|
||||
throw new Error('config edits must select the full unit inventory')
|
||||
},
|
||||
timings: {},
|
||||
event: { pull_request: { draft: true } },
|
||||
mode: 'selected'
|
||||
})
|
||||
expect(plan.mode).toBe('shadow')
|
||||
expect(plan.executionFiles).toEqual(unitFiles)
|
||||
expect(plan.executionFiles).toContain(file)
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps both modified policy self-tests in root units', async () => {
|
||||
const scope = await classifyNodeServerChanges(policyTests, async () => inputs, {
|
||||
modifiedFiles: policyTests
|
||||
})
|
||||
expect(scope.shouldRun).toBe(false)
|
||||
expect(unitFiles).toEqual(expect.arrayContaining(policyTests))
|
||||
})
|
||||
|
||||
it.each(policyTests)('requests the real graph before excluding %s', async (file) => {
|
||||
const scope = await classifyNodeServerChanges([file], unavailableGraph, {
|
||||
deferGraph: true,
|
||||
modifiedFiles: [file]
|
||||
})
|
||||
expect(scope.graphRequired).toBe(true)
|
||||
expect(scope.shouldRun).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ evidence: 'incomplete modified paths', changed: policyTests, modified: [policyTests[0]] },
|
||||
{ evidence: 'missing modified paths', changed: policyTests },
|
||||
{
|
||||
evidence: 'duplicate modified paths',
|
||||
changed: policyTests,
|
||||
modified: [policyTests[0], policyTests[0]]
|
||||
},
|
||||
{
|
||||
evidence: 'foreign modified path',
|
||||
changed: policyTests,
|
||||
modified: [policyTests[0], 'config/scripts/foreign.test.mjs']
|
||||
},
|
||||
{
|
||||
evidence: 'unproven rename side',
|
||||
changed: [...policyTests, 'config/scripts/node-server-renamed.test.mjs'],
|
||||
modified: policyTests
|
||||
},
|
||||
{
|
||||
evidence: 'duplicate changed paths',
|
||||
changed: [policyTests[0], policyTests[0]],
|
||||
modified: policyTests
|
||||
}
|
||||
])('retains full qualification with $evidence', async ({ changed, modified }) => {
|
||||
const scope = await classifyNodeServerChanges(changed, unavailableGraph, {
|
||||
modifiedFiles: modified
|
||||
})
|
||||
expect(scope.shouldRun).toBe(true)
|
||||
expect(scope.graphUnavailable).toBeUndefined()
|
||||
expect(nodeServerQualification(changed, scope)).toEqual({
|
||||
qualification: true,
|
||||
runners: NODE_SERVER_RUNNERS
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'config/scripts/node-server-change-scope.mjs',
|
||||
'config/scripts/node-server-qualification.mjs',
|
||||
'config/scripts/workspace-source-exports.test.mjs',
|
||||
'.github/workflows/node-server-tests.yml',
|
||||
'.github/actions/prepare-native-runtime/action.yml',
|
||||
'pnpm-lock.yaml'
|
||||
])('keeps full qualification for mixed edits including %s', async (file) => {
|
||||
const changed = [...policyTests, file]
|
||||
const scope = await classifyNodeServerChanges(changed, unavailableGraph, {
|
||||
modifiedFiles: changed
|
||||
})
|
||||
expect(scope.shouldRun).toBe(true)
|
||||
expect(scope.graphUnavailable).toBeUndefined()
|
||||
expect(nodeServerQualification(changed, scope)).toEqual({
|
||||
qualification: true,
|
||||
runners: NODE_SERVER_RUNNERS
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
'config/scripts/node-server-concurrency.test.mjs',
|
||||
'config/scripts/node-server-packaged-chat-selection.test.mjs'
|
||||
])(
|
||||
'preserves unrelated %s routing with complete, absent, or non-modification evidence',
|
||||
async (file) => {
|
||||
expect(inputs.has(file)).toBe(false)
|
||||
for (const modifiedFiles of [[file], undefined, []]) {
|
||||
const scope = await classifyNodeServerChanges([file], async () => inputs, { modifiedFiles })
|
||||
expect(scope.shouldRun).toBe(false)
|
||||
expect(scope.graphUnavailable).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
'config/scripts/node-server-concurrency.test.mjs',
|
||||
'config/scripts/node-server-packaged-chat-selection.test.mjs'
|
||||
])('preserves both unmodified rename sides for unrelated %s', async (file) => {
|
||||
const changed = [file, file.replace('.test.mjs', '-renamed.test.mjs')]
|
||||
const scope = await classifyNodeServerChanges(changed, async () => inputs, {
|
||||
modifiedFiles: []
|
||||
})
|
||||
expect(scope.shouldRun).toBe(false)
|
||||
expect(scope.graphUnavailable).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves unrelated tests when only part of their changed vector is modified', async () => {
|
||||
const changed = [
|
||||
'config/scripts/node-server-concurrency.test.mjs',
|
||||
'config/scripts/node-server-packaged-chat-selection.test.mjs'
|
||||
]
|
||||
const scope = await classifyNodeServerChanges(changed, async () => inputs, {
|
||||
modifiedFiles: [changed[0]]
|
||||
})
|
||||
expect(scope.shouldRun).toBe(false)
|
||||
expect(scope.graphUnavailable).toBeUndefined()
|
||||
})
|
||||
|
||||
it.each(policyTests)(
|
||||
'retains native qualification when %s becomes a real graph input',
|
||||
async (file) => {
|
||||
const reachable = await collectNodeServerInputs({ entryPoints: [file] })
|
||||
expect(reachable.has(file)).toBe(true)
|
||||
const scope = await classifyNodeServerChanges([file], async () => reachable, {
|
||||
modifiedFiles: [file]
|
||||
})
|
||||
expect(scope).toMatchObject({ shouldRun: true, graphResolved: true, matchedFiles: [file] })
|
||||
expect(nodeServerQualification([file], scope)).toEqual({
|
||||
qualification: true,
|
||||
runners: NODE_SERVER_RUNNERS
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('retains full qualification when graph collection fails', async () => {
|
||||
const scope = await classifyNodeServerChanges(policyTests, unavailableGraph, {
|
||||
modifiedFiles: policyTests
|
||||
})
|
||||
expect(scope).toMatchObject({ shouldRun: true, graphUnavailable: true })
|
||||
expect(nodeServerQualification(policyTests, scope)).toEqual({
|
||||
qualification: true,
|
||||
runners: NODE_SERVER_RUNNERS
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user