mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 09:57:48 +00:00
test: remove low-value workflow and compiler test inventories (#38587)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,183 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildUnitDependencyGraph } from './ci-unit-dependency-graph.mjs'
|
||||
import { selectUnitFiles } from '../ci-unit-selection.mjs'
|
||||
import { checkUnitInputOwnership, unitInputSourceDigest } from './ci-unit-input-ownership.mjs'
|
||||
|
||||
const owner = 'src/census.test.ts'
|
||||
const helper = 'src/census-reader.ts'
|
||||
const options = 'src/census-options.ts'
|
||||
const original = new Map([
|
||||
[owner, "import { census } from './census-reader'; census()"],
|
||||
[
|
||||
helper,
|
||||
"import { options } from './census-options'; export const census = () => ts.createProgram(options)"
|
||||
],
|
||||
[options, 'export const options = {}'],
|
||||
['src/domain/value.ts', 'export const value = 1'],
|
||||
['src/domain/value.test.ts', "import './value'"],
|
||||
['src/other.ts', 'export const other = 1'],
|
||||
['src/other.test.ts', "import './other'"]
|
||||
])
|
||||
const record = {
|
||||
file: helper,
|
||||
inputs: ['src/domain/'],
|
||||
emptyEnv: [],
|
||||
contextSha256: buildUnitDependencyGraph(original).ownershipContextSha256,
|
||||
sourceClosureSha256: unitInputSourceDigest(
|
||||
new Map([
|
||||
[helper, original.get(helper)],
|
||||
[options, original.get(options)]
|
||||
])
|
||||
),
|
||||
reason: 'This fixture compiler reads only the domain source directory.'
|
||||
}
|
||||
|
||||
function graph(sources = original, records = [record]) {
|
||||
return {
|
||||
...buildUnitDependencyGraph(sources, undefined, records),
|
||||
files: new Set(sources.keys())
|
||||
}
|
||||
}
|
||||
|
||||
function selection(changed, input = graph()) {
|
||||
const tests = [...input.files].filter((file) => file.endsWith('.test.ts'))
|
||||
return selectUnitFiles(tests, changed, input)
|
||||
}
|
||||
|
||||
describe('checked indirect unit inputs', () => {
|
||||
it('retains a declared consumer while excluding it for unrelated source changes', () => {
|
||||
expect(selection(['src/domain/value.ts']).files).toEqual([owner, 'src/domain/value.test.ts'])
|
||||
expect(selection(['src/other.ts']).files).toEqual(['src/other.test.ts'])
|
||||
expect(graph().opaque.has(helper)).toBe(false)
|
||||
})
|
||||
|
||||
it('owns new files under a directory even with a direct consumer hiding full fallback', () => {
|
||||
const sources = new Map(original)
|
||||
sources.set('src/domain/added.ts', 'export const added = 1')
|
||||
sources.set('src/domain/added.test.ts', "import './added'")
|
||||
expect(selection(['src/domain/added.ts'], graph(sources))).toMatchObject({
|
||||
full: false,
|
||||
files: [owner, 'src/domain/added.test.ts']
|
||||
})
|
||||
})
|
||||
|
||||
it('bounds an entry point without suppressing other callers of its indirect reader', () => {
|
||||
const sources = new Map(original)
|
||||
const otherCaller = 'src/other-census.test.ts'
|
||||
sources.set(otherCaller, "import { census } from './census-reader'; census()")
|
||||
const entry = {
|
||||
...record,
|
||||
file: owner,
|
||||
sourceClosureSha256: unitInputSourceDigest(
|
||||
new Map([owner, helper, options].map((file) => [file, sources.get(file)]))
|
||||
)
|
||||
}
|
||||
const input = graph(sources, [entry])
|
||||
expect(selection(['src/other.ts'], input).files).toEqual(['src/other.test.ts', otherCaller])
|
||||
expect(selection(['src/domain/value.ts'], input).files).toContain(owner)
|
||||
expect(selection([helper], input).files).toContain(owner)
|
||||
expect(selection([owner], input).files).toContain(owner)
|
||||
expect(input.opaque.has(helper)).toBe(true)
|
||||
})
|
||||
|
||||
it('invalidates declarations after dependency or shared-runtime changes', () => {
|
||||
const dependencyInputs = new Map([['pnpm-lock.yaml', 'reviewed dependencies']])
|
||||
const contextSha256 = buildUnitDependencyGraph(
|
||||
original,
|
||||
undefined,
|
||||
[],
|
||||
dependencyInputs
|
||||
).ownershipContextSha256
|
||||
const records = [{ ...record, contextSha256 }]
|
||||
expect(
|
||||
buildUnitDependencyGraph(original, undefined, records, dependencyInputs).opaque.has(helper)
|
||||
).toBe(false)
|
||||
dependencyInputs.set('pnpm-lock.yaml', 'updated dependencies')
|
||||
expect(
|
||||
buildUnitDependencyGraph(original, undefined, records, dependencyInputs).opaque.has(helper)
|
||||
).toBe(true)
|
||||
const sources = new Map(original)
|
||||
sources.set(
|
||||
'config/scripts/test-runtime/dom/vitest-dom-setup.ts',
|
||||
'configureDifferentRuntime()'
|
||||
)
|
||||
expect(graph(sources).opaque.has(helper)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps deleted and renamed inputs on the full fallback', () => {
|
||||
const sources = new Map(original)
|
||||
sources.delete('src/domain/value.ts')
|
||||
sources.set('src/domain/renamed.ts', 'export const value = 1')
|
||||
expect(selection(['src/domain/value.ts'], graph(sources)).full).toBe(true)
|
||||
expect(selection(['src/domain/value.ts', 'src/domain/renamed.ts'], graph(sources)).full).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it.each(
|
||||
[
|
||||
[],
|
||||
[{ ...record, sourceClosureSha256: '0'.repeat(64) }],
|
||||
[record, { ...record, reason: '' }],
|
||||
[{ ...record, inputs: ['src/domain/../other.ts'] }],
|
||||
[{ ...record, inputs: ['src/domain\\value.ts'] }],
|
||||
[{ ...record, inputs: ['src/domain/**'] }]
|
||||
].map((records) => ({ records }))
|
||||
)('retains unowned or invalidly owned consumers: %j', ({ records }) => {
|
||||
expect(selection(['src/other.ts'], graph(original, records)).files).toContain(owner)
|
||||
expect(graph(original, records).opaque.has(helper)).toBe(true)
|
||||
})
|
||||
|
||||
it('invalidates ownership when a statically imported helper changes', () => {
|
||||
const sources = new Map(original)
|
||||
sources.set(options, 'export const options = { rootDir: "src/other" }')
|
||||
const result = graph(sources)
|
||||
expect(result.opaque.has(helper)).toBe(true)
|
||||
expect(selection(['src/other.ts'], result).files).toContain(owner)
|
||||
})
|
||||
|
||||
it('rejects a closure changed by a newly resolved JavaScript sibling', () => {
|
||||
const sources = new Map(original)
|
||||
sources.set('src/census-options.js', 'export const options = { rootDir: "src/other" }')
|
||||
expect(graph(sources).opaque.has(helper)).toBe(true)
|
||||
expect(selection(['src/other.ts'], graph(sources)).files).toContain(owner)
|
||||
})
|
||||
|
||||
it('does not clear unresolved local-import fallback with a matching known-source digest', () => {
|
||||
const sources = new Map(original)
|
||||
sources.set(
|
||||
helper,
|
||||
"import { options } from './census-options'; import './missing'; export const census = () => ts.createProgram(options)"
|
||||
)
|
||||
const matching = {
|
||||
...record,
|
||||
sourceClosureSha256: unitInputSourceDigest(
|
||||
new Map([
|
||||
[helper, sources.get(helper)],
|
||||
[options, sources.get(options)]
|
||||
])
|
||||
)
|
||||
}
|
||||
expect(graph(sources, [matching]).opaque.has(helper)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects environment-dependent declarations when the required variable is set', () => {
|
||||
const conditional = { ...record, emptyEnv: ['ORCA_CENSUS_EXTRA_ROOT'] }
|
||||
const closure = () =>
|
||||
new Map([
|
||||
[helper, original.get(helper)],
|
||||
[options, original.get(options)]
|
||||
])
|
||||
expect(
|
||||
checkUnitInputOwnership([conditional], closure, {}, record.contextSha256).has(helper)
|
||||
).toBe(true)
|
||||
expect(
|
||||
checkUnitInputOwnership(
|
||||
[conditional],
|
||||
closure,
|
||||
{ ORCA_CENSUS_EXTRA_ROOT: 'src/other/' },
|
||||
record.contextSha256
|
||||
).size
|
||||
).toBe(0)
|
||||
})
|
||||
})
|
||||
@@ -1,98 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { compileAggregateExpression } from '../../workflow/pr-admission/pr-aggregate-gate-context.mjs'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
|
||||
|
||||
function context(event, runId, inputs = {}) {
|
||||
return {
|
||||
github: {
|
||||
event_name: event,
|
||||
run_id: runId,
|
||||
ref: 'refs/heads/main',
|
||||
event: { pull_request: { number: 123 } }
|
||||
},
|
||||
inputs: { build_template: false, ref: '', ...inputs },
|
||||
matrix: { os: 'windows-2022' },
|
||||
format: (template, value) => template.replace('{0}', value)
|
||||
}
|
||||
}
|
||||
|
||||
function expression(source, ctx) {
|
||||
return runInNewContext(source.slice(3, -2).trim(), ctx)
|
||||
}
|
||||
|
||||
function group(policy, ctx) {
|
||||
return policy.group.replace(/\$\{\{([\s\S]*?)\}\}/g, (_match, source) =>
|
||||
String(runInNewContext(source, ctx))
|
||||
)
|
||||
}
|
||||
|
||||
it('skips draft detection and rechecks the same draft once it is ready', () => {
|
||||
const draft = context('pull_request', 1)
|
||||
draft.github.event.pull_request.draft = true
|
||||
expect(runInNewContext(workflow.jobs.changes.if, draft)).toBe(false)
|
||||
draft.github.event.pull_request.draft = false
|
||||
expect(runInNewContext(workflow.jobs.changes.if, draft)).toBe(true)
|
||||
expect(workflow.on.pull_request.types).toContain('ready_for_review')
|
||||
expect(runInNewContext(workflow.jobs.changes.if, context('push', 2))).toBe(true)
|
||||
for (const event of ['schedule', 'workflow_dispatch', 'workflow_call']) {
|
||||
expect(runInNewContext(workflow.jobs.changes.if, context(event, 2))).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('lets main pushes finish detection without cancelling another push', () => {
|
||||
const first = context('push', 1)
|
||||
const second = context('push', 2)
|
||||
expect(group(workflow.concurrency, first)).not.toBe(group(workflow.concurrency, second))
|
||||
expect(expression(workflow.concurrency['cancel-in-progress'], first)).toBe(false)
|
||||
})
|
||||
|
||||
it('still replaces superseded pull requests at workflow level', () => {
|
||||
const first = context('pull_request', 1)
|
||||
const second = context('pull_request', 2)
|
||||
expect(group(workflow.concurrency, first)).toBe(group(workflow.concurrency, second))
|
||||
expect(expression(workflow.concurrency['cancel-in-progress'], first)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['persistence', 'linux_glibc_floor', 'linux_glibc217_compat', 'linux_musl'])(
|
||||
'%s only supersedes eligible main qualification, isolating releases and nightly runs',
|
||||
(name) => {
|
||||
const job = workflow.jobs[name]
|
||||
const eligible = compileAggregateExpression(job.if, ['needs', 'github', 'inputs'])
|
||||
for (const shouldRun of ['true', 'false']) {
|
||||
const ctx = {
|
||||
...context('push', 1),
|
||||
needs: {
|
||||
changes: {
|
||||
outputs: { should_run: shouldRun, qualification: 'true', native_compatibility: 'true' }
|
||||
},
|
||||
mobile_web_bundle: { result: 'success' }
|
||||
}
|
||||
}
|
||||
expect(eligible(ctx).coerceString()).toBe(shouldRun)
|
||||
}
|
||||
const policy = job.concurrency
|
||||
const first = context('push', 1)
|
||||
const second = context('push', 2)
|
||||
expect(group(policy, first)).toBe(group(policy, second))
|
||||
expect(expression(policy['cancel-in-progress'], first)).toBe(true)
|
||||
for (const [event, inputs] of [
|
||||
['schedule', {}],
|
||||
['workflow_dispatch', {}],
|
||||
['push', { build_template: true }],
|
||||
['push', { ref: 'refs/tags/v1' }]
|
||||
]) {
|
||||
const isolated = context(event, 2, inputs)
|
||||
expect(group(policy, isolated)).not.toBe(group(policy, first))
|
||||
expect(expression(policy['cancel-in-progress'], isolated)).toBe(false)
|
||||
expect(group(policy, isolated)).not.toBe(group(policy, context(event, 3, inputs)))
|
||||
}
|
||||
if (job.strategy?.matrix) {
|
||||
second.matrix.os = 'windows-11-arm'
|
||||
expect(group(policy, second)).not.toBe(group(policy, first))
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,94 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { compileAggregateExpression } from '../../workflow/pr-admission/pr-aggregate-gate-context.mjs'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
|
||||
|
||||
function runnerFor(job, os, fast, event = 'workflow_dispatch') {
|
||||
return compileAggregateExpression(job['runs-on'], ['inputs', 'matrix', 'github'])({
|
||||
inputs: { fast_runners: fast },
|
||||
matrix: { os },
|
||||
github: { event_name: event }
|
||||
}).coerceString()
|
||||
}
|
||||
|
||||
it.each([
|
||||
['ubuntu-22.04', 'blacksmith-8vcpu-ubuntu-2204'],
|
||||
['ubuntu-24.04-arm', 'blacksmith-8vcpu-ubuntu-2404-arm'],
|
||||
['macos-15', 'blacksmith-6vcpu-macos-15'],
|
||||
['macos-15-intel', 'macos-15-intel'],
|
||||
['windows-2022', 'windows-2022'],
|
||||
['windows-11-arm', 'windows-11-arm']
|
||||
])('preserves the qualification architecture for %s', (os, fastRunner) => {
|
||||
expect(runnerFor(workflow.jobs.persistence, os, false)).toBe(os)
|
||||
expect(runnerFor(workflow.jobs.persistence, os, true)).toBe(fastRunner)
|
||||
expect(runnerFor(workflow.jobs.persistence, os, undefined)).toBe(os)
|
||||
for (const event of ['pull_request', 'schedule']) {
|
||||
expect(runnerFor(workflow.jobs.persistence, os, undefined, event)).toBe(os)
|
||||
}
|
||||
})
|
||||
|
||||
it('retains both Linux architectures and the pinned libc qualification containers', () => {
|
||||
for (const name of ['linux_glibc_floor', 'linux_musl']) {
|
||||
const job = workflow.jobs[name]
|
||||
expect(job.strategy.matrix.os).toEqual(['ubuntu-22.04', 'ubuntu-24.04-arm'])
|
||||
for (const os of job.strategy.matrix.os) {
|
||||
expect(runnerFor(job, os, false)).toBe(os)
|
||||
expect(runnerFor(job, os, true)).toBe(
|
||||
`blacksmith-8vcpu-ubuntu-${os === 'ubuntu-22.04' ? '2204' : '2404-arm'}`
|
||||
)
|
||||
for (const event of ['pull_request', 'schedule']) {
|
||||
expect(runnerFor(job, os, undefined, event)).toBe(os)
|
||||
}
|
||||
}
|
||||
}
|
||||
const floor = workflow.jobs.linux_glibc_floor
|
||||
expect(floor.container).toBe('${{ matrix.image }}')
|
||||
for (const { image } of floor.strategy.matrix.include) {
|
||||
expect(image).toMatch(/^quay\.io\/pypa\/manylinux_2_28_.*@sha256:/)
|
||||
}
|
||||
expect(workflow.jobs.linux_glibc217_compat.steps.map((step) => step.run).join('\n')).toContain(
|
||||
'quay.io/pypa/manylinux2014_x86_64@sha256:'
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['changes', 'ubuntu-latest', 'blacksmith-2vcpu-ubuntu-2404'],
|
||||
['mobile_web_bundle', 'ubuntu-latest', 'blacksmith-8vcpu-ubuntu-2404'],
|
||||
['desktop_template', 'ubuntu-latest', 'blacksmith-4vcpu-ubuntu-2404'],
|
||||
['linux_glibc217_compat', 'ubuntu-22.04', 'blacksmith-8vcpu-ubuntu-2204']
|
||||
])('sizes %s while preserving the signing workflow override', (name, hostedRunner, fastRunner) => {
|
||||
expect(runnerFor(workflow.jobs[name], undefined, false)).toBe(hostedRunner)
|
||||
expect(runnerFor(workflow.jobs[name], undefined, true)).toBe(fastRunner)
|
||||
expect(runnerFor(workflow.jobs[name], undefined, undefined, 'pull_request')).toBe(hostedRunner)
|
||||
expect(runnerFor(workflow.jobs[name], undefined, undefined, 'schedule')).toBe(hostedRunner)
|
||||
})
|
||||
|
||||
it('still builds and uploads cross-compiled Windows template addons on the x64 lane', () => {
|
||||
const steps = workflow.jobs.persistence.steps
|
||||
const launcher = steps.find((step) => step.uses?.includes('prepare-windows-cli-launcher'))
|
||||
for (const [os, arch, buildTemplate, expectedArches, expectedUpload] of [
|
||||
['windows-2022', 'X64', true, 'x64 arm64', true],
|
||||
['windows-2022', 'X64', false, 'x64', false],
|
||||
['windows-11-arm', 'ARM64', true, 'arm64', false],
|
||||
['windows-11-arm', 'ARM64', false, 'arm64', false]
|
||||
]) {
|
||||
for (const hasSecurityBuilder of [true, false]) {
|
||||
const ctx = {
|
||||
inputs: { build_template: buildTemplate },
|
||||
matrix: { os },
|
||||
runner: { arch },
|
||||
hashFiles: () => (hasSecurityBuilder ? 'fixture-builder-hash' : '')
|
||||
}
|
||||
for (const fragment of ['process-table addons', 'CLI launchers', 'security addons']) {
|
||||
const upload = steps.find((step) => step.name?.includes(`Keep the Windows ${fragment}`))
|
||||
expect(runInNewContext(upload.if, ctx), `${os} ${fragment}`).toBe(
|
||||
expectedUpload && (fragment !== 'security addons' || hasSecurityBuilder)
|
||||
)
|
||||
}
|
||||
expect(runInNewContext(launcher.with.arches.slice(3, -2), ctx)).toBe(expectedArches)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,276 +0,0 @@
|
||||
import { readFileSync, statSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { scanSourceTree, stripComments } from '../../../../src/shared/source-scan/source-tree-scan'
|
||||
import { classifyPrJobs } from '../../workflow/source-routing/pr-code-change-scope.mjs'
|
||||
import { WINDOWS_BOUNDARY_TESTS } from '../../workflow/source-routing/pr-package-test-paths.mjs'
|
||||
|
||||
// Register every Windows-named or suite-gated file in sole Windows lane’s curated list; otherwise Ubuntu self-skips can pass without execution.
|
||||
// Follow suite/describe gates, nested aliases, local boolean polarity and narrowing &&; do not follow case-level gates, imported/function flags, broad OR/skipIf conjunctions.
|
||||
// Proves registration/routing list membership, not actual suite execution (manual env gates can skip), diff-trigger coverage or assertion quality.
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../../../..')
|
||||
const WINDOWS_LANE_JOB = 'package_windows'
|
||||
const WINDOWS_LANE_STEPS = [
|
||||
'Test Windows-specific boundaries',
|
||||
'Test Windows installer process probe'
|
||||
]
|
||||
|
||||
// Shrink-only pre-existing Windows files missing lane registration; remove entry when registered, never add.
|
||||
const UNREGISTERED_ON_MAIN = [
|
||||
// Suite gated with `describe.skipIf(platform !== 'win32')`; the cross-platform
|
||||
// half of the file still runs on ubuntu, the Windows half runs nowhere.
|
||||
'src/main/antigravity/windows-hook-payload-delivery.test.ts',
|
||||
// `.win32.test.ts` by name yet not registered -- the plainest instance of the class.
|
||||
'src/main/daemon/pty-subprocess/testing/node-pty-windows-input-error.win32.test.ts',
|
||||
// Same shape as the antigravity file: a win32-only sibling suite that never runs.
|
||||
'src/main/grok/windows-grok-hook-script.test.ts',
|
||||
// Whole file is `describe.runIf(platform === 'win32')`; runs on no machine.
|
||||
'src/main/ipc/preflight-windows-path-refresh.repro.test.ts',
|
||||
// `describeWindows` ternary over the whole file; runs on no machine.
|
||||
'src/main/providers/windows-shell-preflight-runtime.windows.test.ts',
|
||||
// Whole file is `describe.runIf(platform === 'win32')`; runs on no machine.
|
||||
'src/main/startup/windows-shell-path-restoration.windows.test.ts',
|
||||
// Whole file is `describe.skipIf(platform !== 'win32')`; runs on no machine.
|
||||
'src/shared/setup-agent-sequencing.windows.test.ts'
|
||||
]
|
||||
|
||||
// Manual suites need real-distro/filesystem opt-in as well as win32; assert env actually reaches gate so this cannot hide registrable debt.
|
||||
const MANUAL_OPT_IN = [
|
||||
// `runIf(platform === 'win32' && Boolean(distro))`, distro from ORCA_TEST_WSL_DISTRO.
|
||||
'src/main/git/runner-wsl-linked-gitdir-windows.test.ts',
|
||||
// `runRealWsl = … && ORCA_REAL_WSL_BANNER_TEST === '1'`; needs a real distro.
|
||||
'src/main/workspaces/removal/local-worktree-filesystem-wsl-banner.wsl.test.ts',
|
||||
// `ORCA_REAL_WINDOWS_SKILL_TEST === '1'`; installs into a real Windows workspace.
|
||||
'src/main/skills/skill-windows-workspace.integration.test.ts',
|
||||
// `RUN_REAL_WSL = … && ORCA_REAL_WSL_SKILL_TEST === '1'`; real distro filesystem.
|
||||
'src/main/skills/skill-wsl-delete.integration.test.ts',
|
||||
// Same flag; real WSL install transactions.
|
||||
'src/main/skills/skill-wsl-install-transaction.integration.test.ts',
|
||||
// Same flag; real WSL POSIX semantics.
|
||||
'src/main/skills/skill-wsl-posix-semantics.integration.test.ts',
|
||||
// `runRealWsl = … && ORCA_REAL_WSL_DELETE_TEST === '1'`; real distro traversal race.
|
||||
'src/main/persistence/moving-wsl-projects/wsl-approved-root-race.wsl.test.ts',
|
||||
// Same flag; real UNC delete against a distro.
|
||||
'src/main/wsl/filesystem/wsl-unc-delete.wsl.test.ts',
|
||||
// `enabled = platform === 'win32' && ORCA_REAL_WSL_RUNNER_TEST === '1'`; mutates a real distro's ~/.profile.
|
||||
'src/main/wsl/execution/wsl-runner.wsl.test.ts'
|
||||
]
|
||||
|
||||
const TEST_FILE_PATTERN = /\.(?:test|spec)\.(?:ts|tsx|mjs|cjs|js)$/
|
||||
|
||||
// Exclude mobile: separate Vitest job never triggers desktop package_windows lane.
|
||||
const UNREACHABLE_BY_THE_WINDOWS_LANE = 'mobile/'
|
||||
|
||||
const SCANNER_SELF_PATH = 'config/scripts/ci/unit-selection/win32-test-lane-registration.test.mjs'
|
||||
|
||||
export function isScannerSelfPath(path) {
|
||||
return path === SCANNER_SELF_PATH
|
||||
}
|
||||
|
||||
const WIN32_TRUE_EXPRESSION = String.raw`process\.platform\s*===\s*['"]win32['"]`
|
||||
const WIN32_FALSE_EXPRESSION = String.raw`process\.platform\s*!==\s*['"]win32['"]`
|
||||
const SUITE = String.raw`(?:describe|suite)`
|
||||
|
||||
// Resolve named flag polarity from assignment, excluding ternary suite aliases. Truthy win32 && narrows; falsy non-win32 && widens skipIf to run on POSIX too. Reject || for both.
|
||||
const FLAG_TRUE_ASSIGNMENT = new RegExp(
|
||||
String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${WIN32_TRUE_EXPRESSION}(?=\s*(?:&&|;|\r?\n|$))`,
|
||||
'g'
|
||||
)
|
||||
const FLAG_FALSE_ASSIGNMENT = new RegExp(
|
||||
String.raw`(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${WIN32_FALSE_EXPRESSION}(?=\s*(?:;|\r?\n|$))`,
|
||||
'g'
|
||||
)
|
||||
|
||||
function escapeForAlternation(name) {
|
||||
return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/** Never-matching branch, so an empty flag set cannot widen a pattern. */
|
||||
const MATCHES_NOTHING = String.raw`(?!)`
|
||||
|
||||
function alternation(names) {
|
||||
return names.length === 0 ? MATCHES_NOTHING : names.map(escapeForAlternation).join('|')
|
||||
}
|
||||
|
||||
function buildGates(source) {
|
||||
const trueOnWindows = [...source.matchAll(FLAG_TRUE_ASSIGNMENT)].map(([, name]) => name)
|
||||
const falseOnWindows = [...source.matchAll(FLAG_FALSE_ASSIGNMENT)].map(([, name]) => name)
|
||||
const isTrue = `(?:${WIN32_TRUE_EXPRESSION}|\\b(?:${alternation(trueOnWindows)})\\b)`
|
||||
const isFalse = `(?:${WIN32_FALSE_EXPRESSION}|!\\s*(?:${alternation(trueOnWindows)})\\b|\\b(?:${alternation(falseOnWindows)})\\b)`
|
||||
return [
|
||||
// Accept bare gate or narrowing &&; requiring only closing parenthesis misses runIf(win32 && hasAddon).
|
||||
new RegExp(String.raw`\b${SUITE}\s*\.\s*runIf\s*\(\s*${isTrue}\s*(?:\)|&&)`),
|
||||
new RegExp(String.raw`\b${SUITE}\s*\.\s*skipIf\s*\(\s*${isFalse}\s*(?:\)|\|\|)`),
|
||||
// Reject inverted describe.skip branch, which gates POSIX rather than Windows.
|
||||
new RegExp(String.raw`=\s*${isTrue}\s*\?\s*${SUITE}\s*(?!\s*\.\s*skip)`),
|
||||
new RegExp(String.raw`=\s*${isFalse}\s*\?\s*${SUITE}\s*\.\s*skip`)
|
||||
]
|
||||
}
|
||||
|
||||
export function isWindows32GatedTestFile(path, source) {
|
||||
if (/\.win32\.(?:test|spec)\./.test(path)) {
|
||||
return true
|
||||
}
|
||||
// Strip actual comments with quote tracking; slash-star in string must not erase live gate.
|
||||
const code = stripComments(source)
|
||||
return buildGates(code).some((gate) => gate.test(code))
|
||||
}
|
||||
|
||||
// Manual exemption requires env read in same win32 conjunct or one referenced const; unrelated env elsewhere cannot park registrable suite. Deeper chains fail closed as registrable.
|
||||
const WIN32_CONJUNCT = new RegExp(String.raw`${WIN32_TRUE_EXPRESSION}\s*&&([^\n]*)`, 'g')
|
||||
const ENV_READ = /process\.env\.[A-Za-z0-9_]+/
|
||||
const IDENTIFIER = /[A-Za-z_$][\w$]*/g
|
||||
|
||||
function isAssignedFromEnv(name, code) {
|
||||
return new RegExp(
|
||||
String.raw`(?:const|let|var)\s+${escapeForAlternation(name)}\s*=[^\n]*process\.env\.`
|
||||
).test(code)
|
||||
}
|
||||
|
||||
export function requiresEnvOptIn(source) {
|
||||
const code = stripComments(source)
|
||||
return [...code.matchAll(WIN32_CONJUNCT)].some(([, conjunct]) => {
|
||||
if (ENV_READ.test(conjunct)) {
|
||||
return true
|
||||
}
|
||||
return [...conjunct.matchAll(IDENTIFIER)].some(([name]) => isAssignedFromEnv(name, code))
|
||||
})
|
||||
}
|
||||
|
||||
/** The Windows job's curated files: the boundary list plus any argv the lane steps spell out. */
|
||||
function readWindowsWorkflow() {
|
||||
const workflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
|
||||
const steps = workflow.jobs?.[WINDOWS_LANE_JOB]?.steps ?? []
|
||||
const runs = WINDOWS_LANE_STEPS.map((name) => {
|
||||
const step = steps.find((candidate) => candidate?.name === name)
|
||||
if (!step) {
|
||||
throw new Error(
|
||||
`No "${name}" step in ${WINDOWS_LANE_JOB}; update WINDOWS_LANE_STEPS if renamed.`
|
||||
)
|
||||
}
|
||||
const run = String(step.run ?? '')
|
||||
if (!/(?:vitest|vitest\.mjs) run/.test(run)) {
|
||||
throw new Error(`The "${name}" step no longer invokes vitest; this guard is stale.`)
|
||||
}
|
||||
return run
|
||||
})
|
||||
if (!runs[0].includes('config/scripts/workflow/source-routing/pr-package-test-paths.mjs')) {
|
||||
throw new Error(`The "${WINDOWS_LANE_STEPS[0]}" step no longer reads WINDOWS_BOUNDARY_TESTS.`)
|
||||
}
|
||||
const run = runs.join(' ')
|
||||
return {
|
||||
laneFiles: [
|
||||
...WINDOWS_BOUNDARY_TESTS,
|
||||
...run.split(/\s+/).filter((token) => TEST_FILE_PATTERN.test(token))
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const { laneFiles } = readWindowsWorkflow()
|
||||
const scannedTestFiles = scanSourceTree(projectDir, {
|
||||
includeTests: true,
|
||||
extensions: TEST_FILE_PATTERN
|
||||
}).filter(({ relativePath }) => !relativePath.startsWith(UNREACHABLE_BY_THE_WINDOWS_LANE))
|
||||
const gatedFiles = scannedTestFiles
|
||||
.filter(({ relativePath }) => !isScannerSelfPath(relativePath))
|
||||
.filter(({ relativePath, source }) => isWindows32GatedTestFile(relativePath, source))
|
||||
.map(({ relativePath }) => relativePath)
|
||||
|
||||
// Ask actual CI classifier, not private WINDOWS_PACKAGE_TESTS; force-all paths could look registered without listing, though none are test paths today.
|
||||
function isInClassifier(path) {
|
||||
return classifyPrJobs([path])[WINDOWS_LANE_JOB] === true
|
||||
}
|
||||
|
||||
function registrationFailure(path) {
|
||||
const missing = []
|
||||
if (!laneFiles.includes(path)) {
|
||||
missing.push(
|
||||
`add '${path}' to WINDOWS_BOUNDARY_TESTS in config/scripts/workflow/source-routing/pr-package-test-paths.mjs`
|
||||
)
|
||||
}
|
||||
if (!isInClassifier(path)) {
|
||||
missing.push(
|
||||
`add '${path}' to WINDOWS_PACKAGE_TESTS in config/scripts/workflow/source-routing/pr-package-test-paths.mjs`
|
||||
)
|
||||
}
|
||||
return missing.length === 0 ? null : `${path}: ${missing.join('; and ')}`
|
||||
}
|
||||
|
||||
function sourceOf(path) {
|
||||
return readFileSync(join(projectDir, path), 'utf8')
|
||||
}
|
||||
|
||||
describe('Windows-gated test files are registered in the Windows CI lane', () => {
|
||||
it('enables real Windows rename contention tests in the registered step', () => {
|
||||
const workflow = parse(readFileSync(join(projectDir, '.github/workflows/pr.yml'), 'utf8'))
|
||||
const step = workflow.jobs.package_windows.steps.find(
|
||||
(candidate) => candidate.name === 'Test Windows-specific boundaries'
|
||||
)
|
||||
expect(step.env.ORCA_REAL_WINDOWS_SKILL_TEST).toBe('1')
|
||||
expect(step.env.ORCA_BACKGROUND_LAUNCH).toBe('1')
|
||||
expect(WINDOWS_BOUNDARY_TESTS).toContain(
|
||||
'src/main/skills/skill-windows-rename-contention.integration.test.ts'
|
||||
)
|
||||
})
|
||||
|
||||
it('parses a plausible Windows lane invocation', () => {
|
||||
expect(laneFiles.length).toBeGreaterThan(15)
|
||||
const missingFromDisk = laneFiles.filter((path) => {
|
||||
try {
|
||||
return !statSync(join(projectDir, path)).isFile()
|
||||
} catch {
|
||||
return true
|
||||
}
|
||||
})
|
||||
expect(
|
||||
missingFromDisk,
|
||||
'The Windows lane invokes vitest on paths that do not exist -- vitest will run nothing for them.'
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('has every Windows-gated test file registered in the Windows lane', () => {
|
||||
const grandfathered = new Set([...UNREGISTERED_ON_MAIN, ...MANUAL_OPT_IN])
|
||||
const failures = gatedFiles
|
||||
.filter((path) => !grandfathered.has(path))
|
||||
.map(registrationFailure)
|
||||
.filter((failure) => failure !== null)
|
||||
expect(
|
||||
failures,
|
||||
'A Windows-gated test file is not registered in the Windows CI lane. It self-skips on ' +
|
||||
'ubuntu and reports success, so it runs on no machine. Fix each line below.'
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('has no stale entry in either grandfathered list', () => {
|
||||
const stale = [...UNREGISTERED_ON_MAIN, ...MANUAL_OPT_IN].filter(
|
||||
(path) => !gatedFiles.includes(path) || registrationFailure(path) === null
|
||||
)
|
||||
expect(
|
||||
stale,
|
||||
'These files are no longer unregistered Windows-gated debt -- they were registered, ' +
|
||||
'renamed, un-gated, or deleted. Delete each line from UNREGISTERED_ON_MAIN or ' +
|
||||
'MANUAL_OPT_IN; the lists only ever shrink.'
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps MANUAL_OPT_IN to suites CI genuinely cannot run', () => {
|
||||
// Otherwise this list is just a quieter way to skip registration.
|
||||
const notActuallyOptIn = MANUAL_OPT_IN.filter((path) => !requiresEnvOptIn(sourceOf(path)))
|
||||
expect(
|
||||
notActuallyOptIn,
|
||||
'A MANUAL_OPT_IN entry has no env-var opt-in, so registering it WOULD make it run. ' +
|
||||
'Register it in WINDOWS_BOUNDARY_TESTS and delete the line.'
|
||||
).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps every UNREGISTERED_ON_MAIN file ineligible for MANUAL_OPT_IN', () => {
|
||||
// Registrable debt cannot move to manual list just to lower cap.
|
||||
const movable = UNREGISTERED_ON_MAIN.filter((path) => requiresEnvOptIn(sourceOf(path)))
|
||||
expect(
|
||||
movable,
|
||||
'This file is registrable; it cannot be reclassified as MANUAL_OPT_IN.'
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,94 +0,0 @@
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { compileAggregateExpression } from '../../workflow/pr-admission/pr-aggregate-gate-context.mjs'
|
||||
|
||||
const workflows = readdirSync('.github/workflows').flatMap((file) => {
|
||||
const workflow = parse(readFileSync(`.github/workflows/${file}`, 'utf8'))
|
||||
// The LoC reporter updates PR metadata rather than running CI.
|
||||
return Object.hasOwn(workflow.on ?? {}, 'pull_request') && file !== 'pr-test-loc.yml'
|
||||
? [{ file, workflow }]
|
||||
: []
|
||||
})
|
||||
|
||||
function admitted(condition, draft, needs = {}, eventName = 'pull_request') {
|
||||
return (
|
||||
compileAggregateExpression(condition ?? 'true', ['github', 'needs', 'inputs'])({
|
||||
github: {
|
||||
event_name: eventName,
|
||||
repository: 'stablyai/orca',
|
||||
event: { pull_request: { draft } }
|
||||
},
|
||||
needs,
|
||||
inputs: {}
|
||||
}).coerceString() === 'true'
|
||||
)
|
||||
}
|
||||
|
||||
it.each(workflows)('$file starts no CI jobs for a draft PR', ({ workflow }) => {
|
||||
const needs = Object.fromEntries(
|
||||
Object.keys(workflow.jobs).map((id) => [id, { result: 'skipped', outputs: {} }])
|
||||
)
|
||||
for (const [id, job] of Object.entries(workflow.jobs)) {
|
||||
// Jobs without a status function inherit success(), so skipped parents block them.
|
||||
if (job.needs && !/\b(?:cancelled|always|success|failure)\(/.test(job.if ?? '')) {
|
||||
continue
|
||||
}
|
||||
expect(admitted(job.if, true, needs), id).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it.each(workflows)('$file starts CI when ready and cancels it when drafted', ({ workflow }) => {
|
||||
const types = workflow.on.pull_request.types
|
||||
expect(types).toEqual(
|
||||
expect.arrayContaining([
|
||||
'opened',
|
||||
'synchronize',
|
||||
'reopened',
|
||||
'ready_for_review',
|
||||
'converted_to_draft'
|
||||
])
|
||||
)
|
||||
expect(Object.values(workflow.jobs).some((job) => !job.needs && admitted(job.if, false))).toBe(
|
||||
true
|
||||
)
|
||||
const concurrency = workflow.concurrency['cancel-in-progress']
|
||||
expect(typeof concurrency === 'boolean' ? concurrency : admitted(concurrency, true)).toBe(true)
|
||||
})
|
||||
|
||||
it.each(workflows)('$file keeps its non-PR entry points eligible', ({ workflow }) => {
|
||||
const needs = Object.fromEntries(
|
||||
Object.keys(workflow.jobs).map((id) => [id, { result: 'success', outputs: {} }])
|
||||
)
|
||||
for (const event of Object.keys(workflow.on).filter((name) => name !== 'pull_request')) {
|
||||
for (const [id, job] of Object.entries(workflow.jobs)) {
|
||||
expect(admitted(job.if, true, needs, event), `${event}/${id}`).toBe(
|
||||
admitted(job.if, false, needs, event)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the root and README guards on ready docs-only PRs without another runner', () => {
|
||||
const { workflow: prWorkflow } = workflows.find(({ file }) => file === 'pr.yml')
|
||||
const detector = prWorkflow.jobs.code_paths
|
||||
const eligible = compileAggregateExpression(detector.if ?? 'true', ['github', 'steps', 'needs'])
|
||||
expect(admitted(detector.if, true)).toBe(false)
|
||||
expect(admitted(prWorkflow.jobs.verify.if, true)).toBe(false)
|
||||
for (const shouldRun of ['false', 'true']) {
|
||||
expect(
|
||||
eligible({
|
||||
github: { event_name: 'pull_request', event: { pull_request: { draft: false } } },
|
||||
steps: { filter: { outputs: { should_run: shouldRun } } },
|
||||
needs: {}
|
||||
}).coerceString()
|
||||
).toBe('true')
|
||||
}
|
||||
expect(detector.needs).toBeUndefined()
|
||||
for (const name of ['Reject new root-level files and folders', 'Check README local links']) {
|
||||
const step = detector.steps.find((candidate) => candidate.name === name)
|
||||
expect(step).toBeDefined()
|
||||
expect(step.if).toBeUndefined()
|
||||
}
|
||||
expect(prWorkflow.jobs.root_directory_guard).toBeUndefined()
|
||||
})
|
||||
@@ -4,21 +4,14 @@ import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import {
|
||||
managedSshShards,
|
||||
MANAGED_SSH_E2E_SPEC_PREFIX
|
||||
} from '../e2e-selection/ci-e2e-job-selection.mjs'
|
||||
import { compileAggregateExpression } from '../../workflow/pr-admission/pr-aggregate-gate-context.mjs'
|
||||
import { selectPrE2eSpecs } from '../../workflow/source-routing/pr-e2e-source-routing.mjs'
|
||||
import { managedSshShards } from '../e2e-selection/ci-e2e-job-selection.mjs'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8'))
|
||||
const prWorkflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
const lane = workflow.jobs['orcad-ssh-docker']
|
||||
const command = lane.steps.find(
|
||||
(step) => step.name === 'Run managed SSH specs on a Docker host'
|
||||
).run
|
||||
const selector = readFileSync('config/scripts/ci/e2e-selection/ci-e2e-job-selection.mjs', 'utf8')
|
||||
const small = ['tests/e2e/ssh-orcad-open-in-owner.spec.ts', 'tests/e2e/ssh-orcad-idle-exit.spec.ts']
|
||||
const directories = []
|
||||
afterEach(() => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
@@ -26,61 +19,6 @@ afterEach(() => {
|
||||
}
|
||||
})
|
||||
|
||||
it('uses one PR shard for at most two selected files and preserves the full default', () => {
|
||||
const caller = compileAggregateExpression(prWorkflow.jobs.e2e.with.managed_ssh_shards)
|
||||
const matrix = compileAggregateExpression(lane.strategy.matrix.shard, ['inputs'])
|
||||
for (const [specs, expected] of [
|
||||
[[small[0]], ['1/1']],
|
||||
[small, ['1/1']],
|
||||
[
|
||||
[...small, 'tests/e2e/ssh-orcad-workspace-tools.spec.ts'],
|
||||
['1/2', '2/2']
|
||||
],
|
||||
[[MANAGED_SSH_E2E_SPEC_PREFIX], ['1/2', '2/2']]
|
||||
]) {
|
||||
const shards = caller({
|
||||
needs: {
|
||||
code_paths: {
|
||||
outputs: {
|
||||
e2e_ssh_shards: JSON.stringify(managedSshShards(JSON.stringify(specs)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}).coerceString()
|
||||
expect(
|
||||
matrix({ inputs: { managed_ssh_shards: shards } })
|
||||
.values()
|
||||
.map((item) => item.value)
|
||||
).toEqual(expected)
|
||||
}
|
||||
expect(
|
||||
matrix({ inputs: {} })
|
||||
.values()
|
||||
.map((item) => item.value)
|
||||
).toEqual(['1/2', '2/2'])
|
||||
})
|
||||
|
||||
it('keeps shared SSH fixtures on the full suite but excludes the experimental sidebar spec', () => {
|
||||
for (const file of [
|
||||
'tests/e2e/helpers/orcad-managed-workspace.ts',
|
||||
'tests/e2e/helpers/paired-host-terminal.ts',
|
||||
'.github/workflows/e2e.yml'
|
||||
]) {
|
||||
expect(selectPrE2eSpecs([file])).toContain(MANAGED_SSH_E2E_SPEC_PREFIX)
|
||||
}
|
||||
for (const file of [
|
||||
'tests/e2e/ssh-orcad-phone-feed-list.spec.ts',
|
||||
'tests/e2e/helpers/phone-feed-list.ts',
|
||||
'tests/e2e/helpers/sidebar-inputs-build.ts',
|
||||
'src/shared/sidebar-list/sidebar-feed-list.ts'
|
||||
]) {
|
||||
expect(
|
||||
selectPrE2eSpecs([file]).some((spec) => spec.startsWith(MANAGED_SSH_E2E_SPEC_PREFIX)),
|
||||
file
|
||||
).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
async function execute(input, version = 'current') {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-ssh-selection-'))
|
||||
directories.push(directory)
|
||||
@@ -134,44 +72,6 @@ require('node:fs').writeFileSync(process.env.ARGV_CAPTURE, JSON.stringify(proces
|
||||
|
||||
// The production lane runs Bash on Linux; the stubs never start Docker, Electron, or Xvfb.
|
||||
describe.skipIf(process.platform === 'win32')('managed SSH workflow command', () => {
|
||||
it('passes only the selected SSH files to Playwright, with literal file matching', async () => {
|
||||
const { result, args } = await execute(
|
||||
JSON.stringify([...small, 'tests/e2e/other.spec.ts', small[0]])
|
||||
)
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
const filters = args.slice(
|
||||
args.indexOf('tests/playwright.config.ts') + 1,
|
||||
args.indexOf('--project=electron-headless')
|
||||
)
|
||||
expect(filters).toHaveLength(2)
|
||||
expect(small.map((file) => filters.some((filter) => new RegExp(filter).test(file)))).toEqual([
|
||||
true,
|
||||
true
|
||||
])
|
||||
expect(
|
||||
filters.some((filter) => new RegExp(filter).test('tests/e2e/ssh-orcad-open-in-ownerXspecYts'))
|
||||
).toBe(false)
|
||||
expect(
|
||||
filters.some((filter) => new RegExp(filter).test('tests/e2e/ssh-orcad-unselected.spec.ts'))
|
||||
).toBe(false)
|
||||
expect(args).toContain('--shard=1/1')
|
||||
})
|
||||
|
||||
it.each(['', JSON.stringify([MANAGED_SSH_E2E_SPEC_PREFIX, small[0]])])(
|
||||
'keeps full daily and explicit suite execution for %s',
|
||||
async (input) => {
|
||||
const { result, args } = await execute(input)
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
expect(args).toContain(MANAGED_SSH_E2E_SPEC_PREFIX)
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['legacy', 'missing'])('keeps compatibility with %s checkout refs', async (version) => {
|
||||
const { result, args } = await execute(JSON.stringify(small), version)
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
expect(args).toContain(MANAGED_SSH_E2E_SPEC_PREFIX)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'malformed',
|
||||
'[]',
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { parse } from 'yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const steps = parse(readFileSync('.github/actions/prepare-native-runtime/action.yml', 'utf8')).runs
|
||||
.steps
|
||||
const toolchain = steps.find((step) => step.name === 'Use external node-gyp')
|
||||
|
||||
describe('CI native toolchain preparation', () => {
|
||||
// The action's toolchain workaround only runs in Linux Bash.
|
||||
it.skipIf(process.platform === 'win32').each([
|
||||
['node', 'true', '0', false],
|
||||
['node', 'true', '1', true],
|
||||
['node', 'false', '0', true],
|
||||
['node', '', '0', true]
|
||||
])('runtime=%s cache=%s probe=%s installs=%s', (runtime, hit, probeStatus, installs) => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-ci-native-toolchain-'))
|
||||
const log = join(directory, 'commands')
|
||||
const environment = join(directory, 'github-env')
|
||||
try {
|
||||
writeFileSync(log, '')
|
||||
writeFileSync(environment, '')
|
||||
for (const [name, source] of [
|
||||
['node', 'echo "node $*" >> "$COMMAND_LOG"\nexit "$PROBE_STATUS"'],
|
||||
['npm', 'echo "npm $*" >> "$COMMAND_LOG"\nif [ "$1" = root ]; then echo /global; fi']
|
||||
]) {
|
||||
const path = join(directory, name)
|
||||
writeFileSync(path, `#!/bin/sh\n${source}\n`)
|
||||
chmodSync(path, 0o755)
|
||||
}
|
||||
execFileSync('bash', ['-e', '-o', 'pipefail', '-c', toolchain.run], {
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${directory}:${process.env.PATH}`,
|
||||
NATIVE_RUNTIME: runtime,
|
||||
NATIVE_CACHE_HIT: hit,
|
||||
PROBE_STATUS: probeStatus,
|
||||
COMMAND_LOG: log,
|
||||
GITHUB_ENV: environment
|
||||
}
|
||||
})
|
||||
const commands = readFileSync(log, 'utf8')
|
||||
expect(commands.includes('npm install -g node-gyp@11.5.0')).toBe(installs)
|
||||
expect(
|
||||
commands.includes(
|
||||
'node config/scripts/native-build/node-addons/ensure-native-runtime.mjs --check-only'
|
||||
)
|
||||
).toBe(runtime === 'node' && hit === 'true')
|
||||
expect(readFileSync(environment, 'utf8')).toBe(
|
||||
installs ? 'npm_config_node_gyp=/global/node-gyp/bin/node-gyp.js\n' : ''
|
||||
)
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -77,16 +77,3 @@ it('fetches both merge parents and deleted content without unrelated branches or
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('completes history before scanning the same HEAD and Cloud paths', () => {
|
||||
expect(steps[0].with['fetch-depth']).toBe(1)
|
||||
expect(history['working-directory']).toBe('.')
|
||||
const scanIndex = steps.findIndex(
|
||||
(step) => step.run?.includes('docker run') && step.run.includes('gitleaks@')
|
||||
)
|
||||
expect(steps.indexOf(history)).toBeLessThan(scanIndex)
|
||||
expect(steps[scanIndex].run).toContain(
|
||||
'--log-opts="HEAD -- cloud :(glob).github/workflows/cloud-*.yml .github/actions/cloud-sql-rollout-lease"'
|
||||
)
|
||||
expect(steps.some((step) => step.run?.includes('trufflehog@'))).toBe(true)
|
||||
})
|
||||
|
||||
@@ -58,23 +58,6 @@ async function checkFreshness(overrides = {}) {
|
||||
}
|
||||
|
||||
describe('hourly build preflight', () => {
|
||||
it('gates Mac allocation and pins the checkout and downstream identity', () => {
|
||||
const build = workflow.jobs['build-hourly-mac']
|
||||
expect(preflight['runs-on']).toBe('ubuntu-latest')
|
||||
expect(preflight.steps.some((step) => step.uses?.startsWith('actions/checkout'))).toBe(false)
|
||||
expect(
|
||||
preflight.steps.find((step) => step.id === 'app_token').with['permission-contents']
|
||||
).toBe('read')
|
||||
expect(build.needs).toEqual(['preflight', 'orcad-template'])
|
||||
expect(build.if).toBe("needs.preflight.outputs.should_build == 'true'")
|
||||
expect(build.steps.find((step) => step.name === 'Checkout').with.ref).toBe(
|
||||
build.outputs.head_sha
|
||||
)
|
||||
expect(build.outputs.head_sha).toBe('${{ needs.preflight.outputs.head_sha }}')
|
||||
expect(build.steps.find((step) => step.id === 'release').env.SHA).toBe(build.outputs.head_sha)
|
||||
expect(workflow.concurrency).toEqual({ group: 'hourly-mac-build', 'cancel-in-progress': false })
|
||||
})
|
||||
|
||||
it.each([
|
||||
['unchanged', {}, false],
|
||||
['changed', { LAST_SHA: '123456789012' }, true],
|
||||
|
||||
@@ -9,226 +9,11 @@ import {
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { parse } from 'yaml'
|
||||
import { classifyPrJobs } from '../../workflow/source-routing/pr-code-change-scope.mjs'
|
||||
import { collectNodeServerInputs } from '../../node-server/qualification/node-server-change-scope.mjs'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
const unit = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8'))
|
||||
const relaySteps = unit.jobs.relay_integration.steps
|
||||
const chrome = relaySteps.find((step) => step.name === 'Resolve Chrome for the browser provider')
|
||||
const browser = relaySteps.find((step) => step.name === 'Test external Chromium browser provider')
|
||||
const routes = [
|
||||
['git_compatibility', 'src/shared/git/commands/git-capability-cache.ts'],
|
||||
['codex_index_heal_contract', 'src/main/codex/history/index/codex-session-index-heal.ts'],
|
||||
['xterm_patch_sync', 'config/patches/xterm-upstream.json'],
|
||||
['shell_contracts', 'src/main/pty/pty-manager.ts'],
|
||||
['orcad_browser', 'src/main/orcad/orcad-browser-provider.ts'],
|
||||
['cross-version-wire', 'src/shared/orchestration-rpc-contract.ts'],
|
||||
['managed_hook_node18', 'src/shared/agent-hook-status.ts']
|
||||
]
|
||||
|
||||
function evaluate(expression, context) {
|
||||
return runInNewContext(
|
||||
expression
|
||||
.replace(/^\$\{\{\s*|\s*\}\}$/g, '')
|
||||
.replaceAll('.cross-version-wire', '["cross-version-wire"]'),
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
function routedOutputs(files, reused, options) {
|
||||
const scope = classifyPrJobs(files, options)
|
||||
const context = {
|
||||
steps: {
|
||||
readiness: { outputs: { reused } },
|
||||
filter: {
|
||||
outputs: Object.fromEntries(
|
||||
Object.entries(scope).map(([key, value]) => [key, String(value)])
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.fromEntries(
|
||||
[
|
||||
'test',
|
||||
'static_analysis',
|
||||
'typecheck',
|
||||
'mobile_ci_tools',
|
||||
'relay_integration',
|
||||
...routes.map(([job]) => job)
|
||||
].map((name) => [name, String(evaluate(workflow.jobs.code_paths.outputs[name], context))])
|
||||
)
|
||||
}
|
||||
|
||||
it('preserves relay continuation, setup guards and cancellation', () => {
|
||||
const steps = relaySteps
|
||||
const install = steps.find((step) => step.id === 'relay_dependencies')
|
||||
const test = steps.find((step) => step.name === 'Test relay integration contracts')
|
||||
const context = {
|
||||
cancelled: () => false,
|
||||
success: () => false,
|
||||
steps: {
|
||||
root_dependencies: { outcome: 'success' },
|
||||
relay_dependencies: { outcome: 'success' }
|
||||
}
|
||||
}
|
||||
expect(evaluate(install.if, context)).toBe(true)
|
||||
expect(evaluate(test.if, context)).toBe(true)
|
||||
context.steps.root_dependencies.outcome = 'failure'
|
||||
expect(evaluate(install.if, context)).toBe(false)
|
||||
context.steps.relay_dependencies.outcome = 'failure'
|
||||
expect(evaluate(test.if, context)).toBe(false)
|
||||
context.steps.root_dependencies.outcome = 'success'
|
||||
context.steps.relay_dependencies.outcome = 'success'
|
||||
context.cancelled = () => true
|
||||
expect(evaluate(install.if, context)).toBe(false)
|
||||
expect(evaluate(test.if, context)).toBe(false)
|
||||
expect(
|
||||
steps.findIndex((step) => step.name === 'Test external Chromium browser provider')
|
||||
).toBeLessThan(steps.indexOf(install))
|
||||
expect(test['continue-on-error']).toBeUndefined()
|
||||
expect(unit.jobs.relay_integration['continue-on-error']).toBeUndefined()
|
||||
expect(chrome['continue-on-error']).toBeUndefined()
|
||||
expect(browser['continue-on-error']).toBeUndefined()
|
||||
expect(workflow.jobs.verify.needs).toContain('test')
|
||||
})
|
||||
|
||||
it('guards both browser steps after setup or discovery failure and for default callers', () => {
|
||||
const context = {
|
||||
inputs: { browser_contract: true },
|
||||
success: () => true
|
||||
}
|
||||
expect(evaluate(chrome.if, context)).toBe(true)
|
||||
expect(evaluate(browser.if, context)).toBe(true)
|
||||
context.success = () => false
|
||||
expect(evaluate(chrome.if, context)).toBe(false)
|
||||
expect(evaluate(browser.if, context)).toBe(false)
|
||||
context.success = () => true
|
||||
context.inputs.browser_contract = unit.on.workflow_call.inputs.browser_contract.default
|
||||
expect(evaluate(chrome.if, context)).toBe(false)
|
||||
expect(evaluate(browser.if, context)).toBe(false)
|
||||
})
|
||||
|
||||
describe('Mobile CI tool preflight routing', () => {
|
||||
const verified = { allowMobileCiTools: true }
|
||||
|
||||
it.each([
|
||||
{
|
||||
files: [
|
||||
'config/scripts/mobile-web/check-entry/run-mobile-ci-tests.mjs',
|
||||
'config/mobile-ci.vitest.config.mjs'
|
||||
]
|
||||
},
|
||||
{ files: ['config/scripts/mobile-web/check-entry/mobile-release-check-scope.test.mjs'] },
|
||||
{ files: ['config/scripts/mobile-web/check-entry/mobile-test-change-scope.test.mjs'] }
|
||||
])('keeps affected controls required without starting root shards for $files', ({ files }) => {
|
||||
const outputs = routedOutputs(files, 'false', verified)
|
||||
expect(outputs.mobile_ci_tools).toBe('true')
|
||||
expect(outputs.test).toBe('true')
|
||||
const context = { needs: { code_paths: { outputs } } }
|
||||
expect(evaluate(workflow.jobs.preflight.if, context)).toBe(true)
|
||||
expect(evaluate(workflow.jobs.test.with.run_shards, context)).toBe(false)
|
||||
expect(evaluate(unit.jobs.test.if, { inputs: { run_shards: false } })).toBe(false)
|
||||
const required = workflow.jobs.verify.steps.find(
|
||||
(step) => step.name === 'Require successful checks'
|
||||
)
|
||||
expect(evaluate(required.env.PREFLIGHT_SHOULD_RUN, context)).toBe(true)
|
||||
})
|
||||
|
||||
it('requires complete modification evidence and an exact path scope', () => {
|
||||
const tool = 'config/scripts/mobile-web/check-entry/run-mobile-ci-tests.mjs'
|
||||
expect(classifyPrJobs([tool]).test).toBe(true)
|
||||
expect(classifyPrJobs([tool]).mobile_ci_tools).toBe(false)
|
||||
for (const files of [
|
||||
[],
|
||||
[tool, 'src/main/runtime/orca-runtime.ts'],
|
||||
[tool, 'pnpm-lock.yaml'],
|
||||
[tool, 'package.json'],
|
||||
[tool, 'config/vitest.config.ts'],
|
||||
[tool, 'config/scripts/workflow/source-routing/pr-code-change-scope.mjs'],
|
||||
[tool, 'config/scripts/workflow/source-routing/pr-package-test-paths.mjs'],
|
||||
[tool, 'config/scripts/new-mobile-tool.mjs'],
|
||||
[tool, 'README.md'],
|
||||
[tool, 'mobile/vitest.config.ts']
|
||||
]) {
|
||||
const selected = classifyPrJobs(files, verified)
|
||||
expect(selected.mobile_ci_tools).toBe(false)
|
||||
expect(selected.test).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Renderer-only relay admission', () => {
|
||||
const renderer = 'src/renderer/src/components/right-sidebar/file-explorer/panel/FileExplorer.tsx'
|
||||
const admitted = (files) => {
|
||||
const outputs = routedOutputs(files, 'false')
|
||||
const runRelay = evaluate(workflow.jobs.test.with.run_relay_integration, {
|
||||
needs: { code_paths: { outputs } }
|
||||
})
|
||||
return evaluate(unit.jobs.relay_integration.if, {
|
||||
inputs: { run_relay_integration: runRelay }
|
||||
})
|
||||
}
|
||||
|
||||
it('skips relay setup for renderer-only changes while retaining unit and package checks', () => {
|
||||
const files = [renderer, 'src/renderer/src/i18n/locales/en.json', 'README.md']
|
||||
expect(admitted(files)).toBe(false)
|
||||
expect(classifyPrJobs(files)).toMatchObject({
|
||||
test: true,
|
||||
static_analysis: true,
|
||||
typecheck: true,
|
||||
package: true,
|
||||
package_windows: true
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
[],
|
||||
['src/main/runtime/mobile-cloud-relay/relay-http-client.ts'],
|
||||
['src/shared/relay-region-preference.ts'],
|
||||
['tests/e2e/relay-region-compatibility.unit.test.ts'],
|
||||
['src/main/orcad/orcad-browser-provider.ts'],
|
||||
['package.json'],
|
||||
['pnpm-lock.yaml'],
|
||||
['config/vitest.config.ts'],
|
||||
['.github/workflows/unit-tests.yml'],
|
||||
['.github/workflows/pr.yml'],
|
||||
['unknown-input'],
|
||||
[renderer, 'cloud/packages/relay-contract/src/director-messages.ts'],
|
||||
[renderer, 'src/main/runtime/mobile-cloud-relay/relay-http-client.ts'],
|
||||
[renderer, 'src/shared/relay-region-preference.ts']
|
||||
])('retains relay coverage for non-renderer or incomplete evidence: %j', (...files) => {
|
||||
expect(admitted(files)).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
{},
|
||||
{ relay_integration: '' },
|
||||
{ relay_integration: 'unknown' },
|
||||
{ relay_integration: 'false', orcad_browser: 'true' }
|
||||
])(
|
||||
'retains relay/browser execution for missing routes or a selected browser contract: %j',
|
||||
(outputs) => {
|
||||
expect(
|
||||
evaluate(workflow.jobs.test.with.run_relay_integration, {
|
||||
needs: { code_paths: { outputs } }
|
||||
})
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the shipped relay contracts independent of renderer inputs', async () => {
|
||||
const testStep = relaySteps.find((step) => step.name === 'Test relay integration contracts')
|
||||
const entryPoints = testStep.run.split(/\s+/).filter((arg) => arg.endsWith('.test.ts'))
|
||||
expect(entryPoints.length).toBeGreaterThan(0)
|
||||
const inputs = await collectNodeServerInputs({ entryPoints })
|
||||
expect([...inputs].filter((file) => file.startsWith('src/renderer/'))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
const mobileCiScopeDirectories = []
|
||||
afterEach(() => {
|
||||
@@ -371,69 +156,3 @@ describe.skipIf(process.platform === 'win32')('Mobile CI complete Git change evi
|
||||
expect(result.stderr).toContain('not a git repository')
|
||||
})
|
||||
})
|
||||
|
||||
describe.skipIf(process.platform === 'win32')('Daemon mock complete Git change evidence', () => {
|
||||
const helper = 'src/main/daemon/providers/lifecycle/testing/daemon-init-dependency-mocks.ts'
|
||||
it('keeps real unit and type contracts while omitting unrelated packages for a modification', () => {
|
||||
const repo = createMobileCiScopeRepository(true, helper)
|
||||
writeFileSync(repo.tool, 'export const revision = 2\n')
|
||||
repo.commit()
|
||||
const { result, output } = repo.detect()
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
const flags = readFileSync(output, 'utf8')
|
||||
expect(flags).toContain('package=false\n')
|
||||
expect(flags).toContain('package_windows=false\n')
|
||||
expect(flags).toContain('test=true\n')
|
||||
expect(flags).toContain('typecheck=true\n')
|
||||
})
|
||||
it('routes the complete three-file modification through the real detector', () => {
|
||||
const companions = [
|
||||
'src/main/native-chat/agent-session-wire/structured-agent-session-claude-queued-stop.test.ts',
|
||||
'config/scripts/desktop-distribution/runtime-verification/process-host-build-dist.test.mjs'
|
||||
]
|
||||
const repo = createMobileCiScopeRepository(true, helper, companions)
|
||||
for (const file of [helper, ...companions]) {
|
||||
writeFileSync(join(repo.directory, file), 'export const revision = 2\n')
|
||||
}
|
||||
repo.commit()
|
||||
const { result, output } = repo.detect()
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
const flags = readFileSync(output, 'utf8')
|
||||
for (const name of ['package', 'package_windows']) {
|
||||
expect(classifyPrJobs([helper, ...companions])[name]).toBe(true)
|
||||
expect(flags).toContain(`${name}=false\n`)
|
||||
}
|
||||
for (const name of ['test', 'static_analysis', 'typecheck', 'cross-version-wire']) {
|
||||
expect(flags).toContain(`${name}=true\n`)
|
||||
}
|
||||
const e2e = repo.detect('e2e_filter')
|
||||
expect(e2e.result.code, e2e.result.stderr).toBe(0)
|
||||
const routes = readFileSync(e2e.output, 'utf8')
|
||||
expect(routes).toContain('test_files=[]\n')
|
||||
expect(routes).toContain('should_run=false\n')
|
||||
})
|
||||
it.each(['addition', 'deletion', 'rename', 'type change'])(
|
||||
'keeps platform packages for an actual %s',
|
||||
(change) => {
|
||||
const repo = createMobileCiScopeRepository(change !== 'addition', helper)
|
||||
if (change === 'addition') {
|
||||
writeFileSync(repo.tool, 'export const revision = 1\n')
|
||||
} else if (change === 'deletion') {
|
||||
rmSync(repo.tool)
|
||||
} else if (change === 'rename') {
|
||||
repo.git('mv', helper, helper.replace('.ts', '-renamed.ts'))
|
||||
} else {
|
||||
rmSync(repo.tool)
|
||||
symlinkSync(
|
||||
'../../../config/scripts/workflow/source-routing/pr-code-change-scope.mjs',
|
||||
repo.tool
|
||||
)
|
||||
}
|
||||
repo.commit()
|
||||
const { result, output } = repo.detect()
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
expect(readFileSync(output, 'utf8')).toContain('package=true\n')
|
||||
expect(readFileSync(output, 'utf8')).toContain('package_windows=true\n')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
listPullFiles,
|
||||
updatePullRequest
|
||||
} from '../../../../.github/scripts/pr-test-loc-summary.mjs'
|
||||
import {
|
||||
LOC_HANDS_OFF_COMMENT,
|
||||
mergeLocBlock,
|
||||
renderLocBlock,
|
||||
sumChangedFiles
|
||||
} from '../../../../.github/scripts/pr-test-loc-table.mjs'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../../../..')
|
||||
const locScript = join(projectDir, '.github/scripts/pr-test-loc-summary.mjs')
|
||||
const tempDirs = []
|
||||
|
||||
function runLoc(args, { env } = {}) {
|
||||
return spawnSync(process.execPath, [locScript, ...args], {
|
||||
cwd: projectDir,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, ...env }
|
||||
})
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
while (tempDirs.length > 0) {
|
||||
rmSync(tempDirs.pop(), { force: true, recursive: true })
|
||||
}
|
||||
})
|
||||
|
||||
describe('PR test LoC summary', () => {
|
||||
it('paginates pull files until Link rel=next is gone', async () => {
|
||||
const pages = {
|
||||
'https://api.github.com/repos/stablyai/orca/pulls/9/files?per_page=100': {
|
||||
body: [{ filename: 'src/app.ts', additions: 2, deletions: 0 }],
|
||||
link: '<https://api.github.com/repos/stablyai/orca/pulls/9/files?page=2>; rel="next"'
|
||||
},
|
||||
'https://api.github.com/repos/stablyai/orca/pulls/9/files?page=2': {
|
||||
body: [{ filename: 'src/app.test.ts', additions: 5, deletions: 1 }],
|
||||
link: null
|
||||
}
|
||||
}
|
||||
|
||||
const files = await listPullFiles({
|
||||
owner: 'stablyai',
|
||||
repo: 'orca',
|
||||
pullNumber: 9,
|
||||
token: 'test-token',
|
||||
fetchImpl: async (url) => {
|
||||
const page = pages[url]
|
||||
if (page == null) {
|
||||
throw new Error(`unexpected url ${String(url)}`)
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => page.body,
|
||||
headers: { get: (name) => (name === 'link' ? page.link : null) }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
expect(sumChangedFiles(files)).toEqual({
|
||||
test: { files: 1, added: 5, deleted: 1 },
|
||||
nonTest: { files: 1, added: 2, deleted: 0 }
|
||||
})
|
||||
})
|
||||
|
||||
it('retries transient GitHub errors when updating the PR body', async () => {
|
||||
const body = '## Description\n'
|
||||
const totals = {
|
||||
test: { files: 1, added: 2, deleted: 0 },
|
||||
nonTest: { files: 0, added: 0, deleted: 0 }
|
||||
}
|
||||
const delays = []
|
||||
const responses = [
|
||||
{ ok: false, status: 503, statusText: 'Service Unavailable' },
|
||||
{ ok: true, status: 200, statusText: 'OK' }
|
||||
]
|
||||
const requests = []
|
||||
const result = await updatePullRequest({
|
||||
owner: 'stablyai',
|
||||
repo: 'orca',
|
||||
pullNumber: 14656,
|
||||
token: 'test-token',
|
||||
totals,
|
||||
fetchImpl: async (url, options) => {
|
||||
requests.push({ url, options })
|
||||
if (requests.length === 1) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ body })
|
||||
}
|
||||
}
|
||||
return responses.shift()
|
||||
},
|
||||
sleepImpl: async (milliseconds) => {
|
||||
delays.push(milliseconds)
|
||||
}
|
||||
})
|
||||
|
||||
expect(result).toBe(0)
|
||||
expect(requests).toHaveLength(3)
|
||||
expect(requests.map(({ url }) => url)).toEqual(
|
||||
Array(3).fill('https://api.github.com/repos/stablyai/orca/pulls/14656')
|
||||
)
|
||||
for (const { options } of requests.slice(1)) {
|
||||
expect(options.method).toBe('PATCH')
|
||||
expect(JSON.parse(options.body)).toEqual({
|
||||
body: `${body}\n${renderLocBlock(totals)}\n`
|
||||
})
|
||||
}
|
||||
expect(delays).toEqual([1000])
|
||||
})
|
||||
|
||||
it('renders the table with a hands-off marker', () => {
|
||||
const block = renderLocBlock({
|
||||
test: { files: 1, added: 2, deleted: 1 },
|
||||
nonTest: { files: 1, added: 4, deleted: 0 }
|
||||
})
|
||||
|
||||
expect(block).toContain(LOC_HANDS_OFF_COMMENT)
|
||||
expect(block).toContain(
|
||||
'| Test | 1 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$2 | $\\color{#cf222e}{\\Huge{\\mathbf{−}}}$1 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$1 |'
|
||||
)
|
||||
expect(block).toContain(
|
||||
'| Prod | 1 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$4 | 0 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$4 |'
|
||||
)
|
||||
expect(block).not.toContain('| Total |')
|
||||
})
|
||||
|
||||
it('places the table after the ELI5 section so link unfurls stay readable', () => {
|
||||
const totals = {
|
||||
test: { files: 1, added: 2, deleted: 1 },
|
||||
nonTest: { files: 1, added: 4, deleted: 0 }
|
||||
}
|
||||
const block = renderLocBlock(totals)
|
||||
const expected = `## ELI5\n\nHello\n\n${block}\n\n## Why\n\nBecause\n`
|
||||
|
||||
expect(mergeLocBlock('## ELI5\n\nHello\n\n## Why\n\nBecause\n', totals)).toBe(expected)
|
||||
// Idempotent once placed.
|
||||
expect(mergeLocBlock(expected, totals)).toBe(expected)
|
||||
// Migrates a block previously prepended at the top, including legacy formats.
|
||||
expect(mergeLocBlock(`${block}\n\n## ELI5\n\nHello\n\n## Why\n\nBecause\n`, totals)).toBe(
|
||||
expected
|
||||
)
|
||||
expect(
|
||||
mergeLocBlock(
|
||||
'<!-- orca-pr-loc -->\n**LoC** · test **+1 / −0**\n<!-- /orca-pr-loc -->\n\n## ELI5\n\nHello\n\n## Why\n\nBecause\n',
|
||||
totals
|
||||
)
|
||||
).toBe(expected)
|
||||
// ELI5 subsections stay inside the section.
|
||||
expect(mergeLocBlock('## ELI5\n\nHi\n\n### Detail\n\nMore\n\n## Why\n', totals)).toBe(
|
||||
`## ELI5\n\nHi\n\n### Detail\n\nMore\n\n${block}\n\n## Why\n`
|
||||
)
|
||||
})
|
||||
|
||||
it('appends the table when there is no ELI5 section to follow', () => {
|
||||
const totals = {
|
||||
test: { files: 0, added: 0, deleted: 0 },
|
||||
nonTest: { files: 1, added: 4, deleted: 0 }
|
||||
}
|
||||
const block = renderLocBlock(totals)
|
||||
|
||||
expect(mergeLocBlock('## ELI5\n\nHello\n', totals)).toBe(`## ELI5\n\nHello\n\n${block}\n`)
|
||||
expect(mergeLocBlock('Bump deps\n', totals)).toBe(`Bump deps\n\n${block}\n`)
|
||||
expect(mergeLocBlock(`${block}\n\nBump deps\n`, totals)).toBe(`Bump deps\n\n${block}\n`)
|
||||
expect(mergeLocBlock('', totals)).toBe(`${block}\n`)
|
||||
expect(mergeLocBlock(null, totals)).toBe(`${block}\n`)
|
||||
})
|
||||
|
||||
it('counts and merges from a files JSON fixture', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-pr-test-loc-'))
|
||||
tempDirs.push(root)
|
||||
const filesPath = join(root, 'files.json')
|
||||
const bodyPath = join(root, 'body.md')
|
||||
writeFileSync(
|
||||
filesPath,
|
||||
JSON.stringify([
|
||||
{ filename: 'src/app.ts', additions: 2, deletions: 0 },
|
||||
{ filename: 'src/app.test.ts', additions: 6, deletions: 1 }
|
||||
])
|
||||
)
|
||||
writeFileSync(bodyPath, '## ELI5\n\nHello\n')
|
||||
|
||||
const result = runLoc(['--files-json', filesPath, '--merge-body', bodyPath])
|
||||
|
||||
expect(result.status).toBe(0)
|
||||
expect(result.stdout.startsWith('## ELI5\n\nHello\n\n<!-- orca-pr-loc -->')).toBe(true)
|
||||
expect(result.stdout).toContain(LOC_HANDS_OFF_COMMENT)
|
||||
expect(result.stdout).toContain(
|
||||
'| Test | 1 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$\u200b6 | $\\color{#cf222e}{\\Huge{\\mathbf{−}}}$\u200b1 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$\u200b5 |'
|
||||
)
|
||||
expect(result.stdout).toContain(
|
||||
'| Prod | 1 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$\u200b2 | 0 | $\\color{#1a7f37}{\\Huge{\\mathbf{+}}}$\u200b2 |'
|
||||
)
|
||||
expect(result.stdout).not.toContain('| Total |')
|
||||
expect(result.stdout).toContain('## ELI5\n\nHello\n')
|
||||
expect(result.stdout.match(/<!-- orca-pr-loc -->/g)).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
@@ -1,100 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { parse } from 'yaml'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
const unitTestWorkflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8'))
|
||||
const planWorkflow = parse(readFileSync('.github/workflows/unit-plan.yml', 'utf8'))
|
||||
const evidenceWorkflow = parse(
|
||||
readFileSync('.github/workflows/unit-selection-evidence.yml', 'utf8')
|
||||
)
|
||||
|
||||
describe('PR workflow parallelism', () => {
|
||||
it.each([
|
||||
['shadow', false, true],
|
||||
['shadow', true, true],
|
||||
['selected', false, true],
|
||||
['selected', true, false],
|
||||
['', true, true]
|
||||
])('plans only once for mode %s, draft %s', (mode, draft, deferred) => {
|
||||
const evaluate = (expression, context) => runInNewContext(expression.slice(3, -2), context)
|
||||
const context = {
|
||||
vars: { ORCA_UNIT_SELECTION_MODE: mode },
|
||||
github: { event: { pull_request: { draft } } }
|
||||
}
|
||||
const inputs = {
|
||||
defer_shadow: evaluate(workflow.jobs.unit_plan.with.defer_shadow, context),
|
||||
selection_mode: evaluate(workflow.jobs.unit_plan.with.selection_mode, context)
|
||||
}
|
||||
const planContext = { ...context, inputs }
|
||||
expect(evaluate(planWorkflow.jobs.plan.env.DEFER_SHADOW, planContext)).toBe(deferred)
|
||||
// Downstream jobs read the plan's output rather than re-deriving the draft condition.
|
||||
const planned = String(evaluate(planWorkflow.jobs.plan.outputs.selection_planned, planContext))
|
||||
const needs = { unit_plan: { outputs: { selection_planned: planned } } }
|
||||
expect(evaluate(workflow.jobs.test.with.download_selection_plan, { needs })).toBe(!deferred)
|
||||
expect(evaluate(workflow.jobs.unit_selection_evidence.with.rebuild_plan, { needs })).toBe(
|
||||
deferred
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps advisory unit-selection evidence off the gate', () => {
|
||||
// Keep advisory continue-on-error work off unit-tests.yml’s critical path: caller needs:test otherwise waits ~36s beyond last shard.
|
||||
const evidence = workflow.jobs.unit_selection_evidence
|
||||
expect(evidence.uses).toBe('./.github/workflows/unit-selection-evidence.yml')
|
||||
expect(evidence.needs).toEqual(['unit_plan', 'test'])
|
||||
for (const [result, cancelled, expected] of [
|
||||
['success', false, true],
|
||||
['failure', false, true],
|
||||
['skipped', false, false],
|
||||
['cancelled', false, false],
|
||||
['success', true, false],
|
||||
['failure', true, false]
|
||||
]) {
|
||||
expect(
|
||||
runInNewContext(evidence.if.slice(3, -2), {
|
||||
cancelled: () => cancelled,
|
||||
needs: { test: { result } }
|
||||
})
|
||||
).toBe(expected)
|
||||
}
|
||||
expect(workflow.jobs.verify.needs).not.toContain('unit_selection_evidence')
|
||||
expect(unitTestWorkflow.jobs.selection_evidence).toBeUndefined()
|
||||
const job = evidenceWorkflow.jobs.selection_evidence
|
||||
expect(job['continue-on-error']).toBe(true)
|
||||
})
|
||||
|
||||
it.each([undefined, 'false', 'true'])(
|
||||
'runs the advisory CLI without an action credential and preserves failures (deferred %s)',
|
||||
async (rebuild) => {
|
||||
const steps = evidenceWorkflow.jobs.selection_evidence.steps
|
||||
expect(steps.some((step) => step.uses?.startsWith('actions/setup-node@'))).toBe(false)
|
||||
const comparison = steps.find((step) => step.name === 'Compare selection with full results')
|
||||
expect(comparison.uses).toBe('actions/github-script@v8')
|
||||
expect(comparison.with['github-token']).toBe('unused')
|
||||
const publicEnv = {
|
||||
GITHUB_STEP_SUMMARY: '/tmp/summary',
|
||||
...(rebuild === undefined ? {} : { REBUILD_SELECTION_PLAN: rebuild })
|
||||
}
|
||||
const env = { ...publicEnv, 'INPUT_GITHUB-TOKEN': 'test-token' }
|
||||
const failure = new Error('comparison failed')
|
||||
const result = runInNewContext(`(async () => { ${comparison.with.script} })()`, {
|
||||
process: { env, execPath: '/bundled/node24' },
|
||||
exec: {
|
||||
exec: async (command, args, options) => {
|
||||
expect(command).toBe('/bundled/node24')
|
||||
expect(args).toEqual([
|
||||
'config/scripts/ci/unit-selection/ci-unit-selection-review.mjs',
|
||||
'unit-evidence',
|
||||
...(rebuild === 'true' ? ['ci-shards/unit-selection.json'] : [])
|
||||
])
|
||||
expect(options.env).toEqual(publicEnv)
|
||||
throw failure
|
||||
}
|
||||
}
|
||||
})
|
||||
await expect(result).rejects.toBe(failure)
|
||||
expect(env['INPUT_GITHUB-TOKEN']).toBe('test-token')
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,39 +1,7 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { verifyRequiredPrContext } from '../../../../.github/scripts/check-required-pr-context.mjs'
|
||||
|
||||
it('reserves the required PR context for the full aggregate with both YAML parsers', () => {
|
||||
expect(() => verifyRequiredPrContext(parse)).not.toThrow()
|
||||
const result = runProcessSync({
|
||||
program: 'bun',
|
||||
args: ['.github/scripts/check-required-pr-context.mjs'],
|
||||
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
|
||||
timeoutMs: 10_000
|
||||
})
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
})
|
||||
|
||||
it('checks context names even when workflow-only changes skip root tests', () => {
|
||||
const workflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
const job = workflow.jobs.code_paths
|
||||
const step = job.steps.find(
|
||||
(candidate) => candidate.run === 'bun .github/scripts/check-required-pr-context.mjs'
|
||||
)
|
||||
expect(step).toBeDefined()
|
||||
expect(step.if).toBeUndefined()
|
||||
expect(step['continue-on-error']).toBeUndefined()
|
||||
const checkout = job.steps.find((candidate) => candidate.uses?.startsWith('actions/checkout@'))
|
||||
for (const path of [
|
||||
'/.github/workflows/',
|
||||
'/.github/scripts/check-required-pr-context.mjs',
|
||||
'/config/.bun-version'
|
||||
]) {
|
||||
expect(checkout.with['sparse-checkout'].split('\n')).toContain(path)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects aggregate conditions that hide a successful result', () => {
|
||||
for (const condition of [undefined, 'success()', 'always()', '!cancelled() && !(true)']) {
|
||||
const parseChanged = (source) => {
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../../../..')
|
||||
|
||||
const readWorkflow = (relativePath) => parse(readFileSync(join(projectDir, relativePath), 'utf8'))
|
||||
|
||||
describe('ref-mirroring vet steps', () => {
|
||||
it.each(['daily', 'hourly', 'adhoc'])('%s builds only need the current commit', (channel) => {
|
||||
const { jobs } = readWorkflow(`.github/workflows/${channel}-mac-build.yml`)
|
||||
const job = jobs[channel === 'adhoc' ? 'build-adhoc-mac-arch' : `build-${channel}-mac`]
|
||||
const identity = channel === 'adhoc' ? jobs['orcad-template-support'] : job
|
||||
const checkout = job.steps.find((step) => step.uses === 'actions/checkout@v6')
|
||||
expect(checkout.with['fetch-depth']).toBe(1)
|
||||
expect(
|
||||
(channel === 'adhoc' ? jobs['build-adhoc-mac'] : job).steps.some((step) =>
|
||||
step.run?.includes('gh release list')
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
identity.steps.some((step) => step.run?.includes('ORCA_PUBLISHED_VERSIONS="$published"'))
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// Use matching refs, not releases: unpublished stable release still owns tag number and must not roll channel backward.
|
||||
it.each(['daily', 'hourly', 'adhoc'])(
|
||||
'%s versions from git tags, not main GitHub releases',
|
||||
(channel) => {
|
||||
const { jobs } = readWorkflow(`.github/workflows/${channel}-mac-build.yml`)
|
||||
const identity =
|
||||
channel === 'adhoc'
|
||||
? jobs['orcad-template-support']
|
||||
: jobs[channel === 'adhoc' ? 'build-adhoc-mac-arch' : `build-${channel}-mac`]
|
||||
const step = identity.steps.find(
|
||||
(candidate) => candidate.name === `Compute ${channel} version`
|
||||
)
|
||||
expect(step.run).toContain('git/matching-refs/tags/v')
|
||||
expect(step.run).not.toMatch(
|
||||
/gh release list[\s\S]*--repo "\$GITHUB_REPOSITORY"[\s\S]*--json tagName/
|
||||
)
|
||||
if (channel !== 'adhoc') {
|
||||
expect(step.run).toContain('channel_tags=')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('retains release-cut history for version reservation and retry ancestry', () => {
|
||||
const checkout = readWorkflow('.github/workflows/release-cut.yml').jobs.cut.steps.find(
|
||||
(step) => step.uses === 'actions/checkout@v6'
|
||||
)
|
||||
expect(checkout.with['fetch-depth']).toBe(0)
|
||||
})
|
||||
|
||||
it('resolves identical dev identities in full and depth-one checkouts without local tags', () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-checkout-identity-'))
|
||||
const source = join(directory, 'source')
|
||||
const shallow = join(directory, 'shallow')
|
||||
const run = (program, args, cwd) => {
|
||||
const result = runProcessSync({ program, args, cwd })
|
||||
expect(result.code, result.stderr).toBe(0)
|
||||
return result.stdout.trim()
|
||||
}
|
||||
const git = (args, cwd = directory) => run('git', args, cwd)
|
||||
try {
|
||||
git(['init', source])
|
||||
git(['config', 'user.name', 'CI test'], source)
|
||||
git(['config', 'user.email', 'ci@example.invalid'], source)
|
||||
writeFileSync(join(source, 'package.json'), JSON.stringify({ version: '1.4.165-rc.0' }))
|
||||
git(['add', 'package.json'], source)
|
||||
git(['-c', 'commit.gpgsign=false', 'commit', '-m', 'initial'], source)
|
||||
git(['tag', 'v1.4.167'], source)
|
||||
git(['-c', 'commit.gpgsign=false', 'commit', '--allow-empty', '-m', 'head'], source)
|
||||
git(['clone', '--depth=1', '--no-tags', pathToFileURL(source).href, shallow])
|
||||
expect(git(['rev-list', '--count', 'HEAD'], shallow)).toBe('1')
|
||||
expect(git(['tag', '--list'], shallow)).toBe('')
|
||||
const script = `
|
||||
const result = [];
|
||||
for (const [channel, exported] of [['daily', 'Daily'], ['hourly', 'Hourly'], ['adhoc', 'Adhoc']]) {
|
||||
const module = await import(${JSON.stringify(pathToFileURL(join(projectDir, 'config/scripts/release/versioning/')).href)} + channel + '-build-version.mjs');
|
||||
const date = new Date('2026-09-12T00:00:00Z');
|
||||
result.push(channel === 'adhoc'
|
||||
? module.getAdhocBuildIdentity(date, 'branch', ['v1.4.167'])
|
||||
: module['get' + exported + 'BuildIdentity'](date, { publishedVersions: ['v1.4.167'], releaseNames: [] }));
|
||||
}
|
||||
process.stdout.write(JSON.stringify(result));
|
||||
`
|
||||
const identities = (cwd) => run(process.execPath, ['--input-type=module', '-e', script], cwd)
|
||||
const shallowIdentities = identities(shallow)
|
||||
expect(shallowIdentities).toBe(identities(source))
|
||||
expect(
|
||||
JSON.parse(shallowIdentities).every((identity) => identity.version.startsWith('1.4.168-'))
|
||||
).toBe(true)
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,244 +0,0 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import {
|
||||
chmodSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join } from 'node:path'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/zcode-credential-contract.yml', 'utf8'))
|
||||
const accountWorkflow = parse(readFileSync('.github/workflows/zcode-account-contract.yml', 'utf8'))
|
||||
const job = workflow.jobs['credential-files']
|
||||
const scope = job.steps.find((step) => step.id === 'scope')
|
||||
const account = job.steps.find(
|
||||
(step) => step.name === 'Verify read-only credentials and account changes'
|
||||
)
|
||||
const credentials = job.steps.find(
|
||||
(step) => step.name === 'Verify rollback and native Windows permissions'
|
||||
)
|
||||
const accountFiles = [
|
||||
'src/main/zcode/zcode-v2-plan-credentials.test.ts',
|
||||
'src/main/zcode/zcode-v2-personal-overlay-raw-json.test.ts',
|
||||
'src/main/rate-limits/service-zcode-usage.test.ts',
|
||||
'src/shared/node-bounded-file-reader-sync.test.ts'
|
||||
]
|
||||
const credentialFiles = [
|
||||
'src/main/zcode/zcode-plan-api-key-store.test.ts',
|
||||
'src/main/zcode/zcode-plan-api-key-store-plaintext-rollback.test.ts',
|
||||
'src/main/zcode/zcode-plan-api-key-store-publication-race.test.ts',
|
||||
'src/main/zcode/zcode-plan-api-key-store-native-windows.test.ts',
|
||||
'src/shared/secure-file.test.ts'
|
||||
]
|
||||
|
||||
it('shares one bootstrap across the original modules and operating systems', () => {
|
||||
expect(job.strategy).toEqual({
|
||||
'fail-fast': false,
|
||||
matrix: { os: ['ubuntu-22.04', 'macos-15', 'windows-2022'] }
|
||||
})
|
||||
expect(job.name).toBe('ZCode credentials (${{ matrix.os }})')
|
||||
expect(account.run.split(/\s+/).slice(2)).toEqual(accountFiles)
|
||||
expect(credentials.run.split(/\s+/).slice(2)).toEqual(credentialFiles)
|
||||
expect(
|
||||
job.steps.filter((step) => step.uses === './.github/actions/install-node-dependencies')
|
||||
).toHaveLength(1)
|
||||
expect(job.steps.find((step) => step.id === 'dependencies').with['native-runtime']).toBe('node')
|
||||
expect(job.env.ORCA_BACKGROUND_LAUNCH).toBe('1')
|
||||
expect(job['timeout-minutes']).toBe(20)
|
||||
expect(job.steps[0].with['fetch-depth']).toBe(2)
|
||||
expect(workflow.on.pull_request.paths).toEqual([
|
||||
'src/main/zcode/**',
|
||||
'src/shared/node-bounded-file-reader*',
|
||||
'src/main/rate-limits/**',
|
||||
'src/shared/secure-file*',
|
||||
'src/shared/secure-path-windows-acl*',
|
||||
'src/shared/windows-security-descriptor*',
|
||||
'.github/workflows/zcode-account-contract.yml',
|
||||
'.github/workflows/zcode-credential-contract.yml',
|
||||
'config/scripts/ci/workflow-contracts/zcode-credential-workflow.test.mjs'
|
||||
])
|
||||
expect(accountWorkflow.on).toEqual({ workflow_dispatch: null })
|
||||
expect(accountWorkflow.jobs['account-contract']).toEqual({
|
||||
uses: './.github/workflows/zcode-credential-contract.yml',
|
||||
with: { contract: 'account' }
|
||||
})
|
||||
expect(workflow.on.workflow_dispatch).toBeNull()
|
||||
expect(workflow.on.workflow_call.inputs.contract.default).toBe('credentials')
|
||||
})
|
||||
|
||||
it('runs the other selected contract after a sibling fails and retains prerequisite failures', () => {
|
||||
for (const step of [account, credentials]) {
|
||||
expect(step['continue-on-error']).toBeUndefined()
|
||||
for (const [cancelled, installed, bun, selected, expected] of [
|
||||
[false, 'success', 'success', 'true', true],
|
||||
[false, 'failure', 'success', 'true', false],
|
||||
[false, 'success', 'failure', 'true', false],
|
||||
[false, 'success', 'success', 'false', false],
|
||||
[true, 'success', 'success', 'true', false]
|
||||
]) {
|
||||
expect(
|
||||
runInNewContext(step.if.slice(3, -2), {
|
||||
cancelled: () => cancelled,
|
||||
steps: {
|
||||
scope: { outputs: { account: selected, credentials: selected } },
|
||||
dependencies: { outcome: installed },
|
||||
bun: { outcome: bun }
|
||||
}
|
||||
})
|
||||
).toBe(expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function executeScope(directory, overrides = {}) {
|
||||
const output = join(directory, 'github-output')
|
||||
writeFileSync(output, '')
|
||||
execFileSync('bash', ['-e', '-o', 'pipefail', '-c', scope.run], {
|
||||
cwd: directory,
|
||||
env: {
|
||||
...process.env,
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
EVENT_NAME: 'pull_request',
|
||||
MANUAL_ACCOUNT: 'false',
|
||||
MANUAL_CREDENTIALS: 'true',
|
||||
GITHUB_OUTPUT: output,
|
||||
...overrides
|
||||
}
|
||||
})
|
||||
return Object.fromEntries(
|
||||
readFileSync(output, 'utf8')
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((line) => line.split('='))
|
||||
)
|
||||
}
|
||||
|
||||
// The workflow owns Bash execution on Windows; this contract executes the same source on Unix.
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'selects the original path groups, including deletions and renames',
|
||||
() => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-zcode-workflow-'))
|
||||
const git = (args) => execFileSync('git', args, { cwd: directory, stdio: 'pipe' })
|
||||
const write = (file, value) => {
|
||||
mkdirSync(dirname(join(directory, file)), { recursive: true })
|
||||
writeFileSync(join(directory, file), value)
|
||||
}
|
||||
const commit = () => {
|
||||
git(['add', '-A'])
|
||||
git([
|
||||
'-c',
|
||||
'user.name=Fixture',
|
||||
'-c',
|
||||
'user.email=fixture@example.test',
|
||||
'commit',
|
||||
'--allow-empty',
|
||||
'-qm',
|
||||
'fixture'
|
||||
])
|
||||
}
|
||||
const cases = [
|
||||
['src/main/zcode/credentials.ts', true, true],
|
||||
['src/main/zcode/nested/credentials.ts', true, true],
|
||||
['src/shared/node-bounded-file-reader-sync.test.ts', true, false],
|
||||
['src/main/rate-limits/service-zcode-usage.ts', true, false],
|
||||
['src/shared/secure-file.ts', false, true],
|
||||
['src/shared/secure-path-windows-acl.ts', false, true],
|
||||
['src/shared/windows-security-descriptor.ts', false, true],
|
||||
['.github/workflows/zcode-account-contract.yml', true, false],
|
||||
['.github/workflows/zcode-credential-contract.yml', true, true],
|
||||
['config/scripts/ci/workflow-contracts/zcode-credential-workflow.test.mjs', true, true],
|
||||
['docs/unrelated.md', false, false]
|
||||
]
|
||||
try {
|
||||
git(['init', '-q'])
|
||||
write('README.md', 'fixture')
|
||||
commit()
|
||||
expect(executeScope(directory)).toEqual({ account: 'true', credentials: 'true' })
|
||||
for (const [file, runsAccount, runsCredentials] of cases) {
|
||||
write(file, 'first')
|
||||
commit()
|
||||
expect(executeScope(directory), file).toEqual({
|
||||
account: String(runsAccount),
|
||||
credentials: String(runsCredentials)
|
||||
})
|
||||
rmSync(join(directory, file))
|
||||
commit()
|
||||
expect(executeScope(directory), `deleted ${file}`).toEqual({
|
||||
account: String(runsAccount),
|
||||
credentials: String(runsCredentials)
|
||||
})
|
||||
}
|
||||
write('src/main/zcode/credentials.ts', 'renamed')
|
||||
commit()
|
||||
renameSync(join(directory, 'src/main/zcode/credentials.ts'), join(directory, 'README.md'))
|
||||
commit()
|
||||
expect(executeScope(directory)).toEqual({ account: 'true', credentials: 'true' })
|
||||
write('src/main/rate-limits/service.ts', 'mixed')
|
||||
write('src/shared/secure-file.ts', 'mixed')
|
||||
commit()
|
||||
expect(executeScope(directory)).toEqual({ account: 'true', credentials: 'true' })
|
||||
commit()
|
||||
expect(executeScope(directory)).toEqual({ account: 'true', credentials: 'true' })
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'keeps both manual entry points and missing diff evidence',
|
||||
() => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-zcode-manual-'))
|
||||
try {
|
||||
expect(executeScope(directory)).toEqual({ account: 'true', credentials: 'true' })
|
||||
expect(executeScope(directory, { EVENT_NAME: 'workflow_dispatch' })).toEqual({
|
||||
account: 'false',
|
||||
credentials: 'true'
|
||||
})
|
||||
expect(
|
||||
executeScope(directory, {
|
||||
EVENT_NAME: 'workflow_dispatch',
|
||||
MANUAL_ACCOUNT: 'true',
|
||||
MANUAL_CREDENTIALS: 'false'
|
||||
})
|
||||
).toEqual({ account: 'true', credentials: 'false' })
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'runs both groups when the full diff fails even if scoped diffs would report no changes',
|
||||
() => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-zcode-diff-failure-'))
|
||||
try {
|
||||
const gitShim = join(directory, 'git')
|
||||
writeFileSync(
|
||||
gitShim,
|
||||
'#!/bin/sh\nif [ "$1" = rev-parse ]; then exit 0; fi\nif [ "$#" = 5 ]; then exit "$FULL_DIFF_STATUS"; fi\nexit 0\n'
|
||||
)
|
||||
chmodSync(gitShim, 0o755)
|
||||
const env = { PATH: `${directory}${delimiter}${process.env.PATH}` }
|
||||
expect(executeScope(directory, { ...env, FULL_DIFF_STATUS: '1' })).toEqual({
|
||||
account: 'false',
|
||||
credentials: 'false'
|
||||
})
|
||||
for (const status of ['2', '128']) {
|
||||
expect(executeScope(directory, { ...env, FULL_DIFF_STATUS: status })).toEqual({
|
||||
account: 'true',
|
||||
credentials: 'true'
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,217 +0,0 @@
|
||||
import {
|
||||
mkdirSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
rmSync,
|
||||
symlinkSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { availableParallelism, tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { runProcess, runProcessSync } from '@orca/process-host'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { buildCli } from './build-cli.mjs'
|
||||
import { resolvePinnedBunExecutable } from '../tool-invocation/pinned-bun-executable.mjs'
|
||||
import { resolveProjectCompiler } from '../typecheck/project-execution/run-typecheck-projects-in-parallel.mjs'
|
||||
import { supportsTsrs } from '../typecheck/project-execution/tsrs-typecheck-compiler.mjs'
|
||||
|
||||
const ROOT = process.cwd()
|
||||
const resolveBunExecutable = () => resolvePinnedBunExecutable(ROOT, runProcessSync)
|
||||
const directories = []
|
||||
afterEach(() => {
|
||||
for (const directory of directories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
function fixture() {
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-cli-build-'))
|
||||
directories.push(root)
|
||||
mkdirSync(join(root, 'config'))
|
||||
mkdirSync(join(root, 'src'))
|
||||
symlinkSync(resolve('node_modules'), join(root, 'node_modules'), 'junction')
|
||||
writeFileSync(join(root, 'package.json'), '{"type":"commonjs"}')
|
||||
writeFileSync(join(root, 'config', '.bun-version'), readFileSync('config/.bun-version'))
|
||||
writeFileSync(
|
||||
join(root, 'config', 'tsconfig.build.cli.json'),
|
||||
JSON.stringify({
|
||||
compilerOptions: {
|
||||
module: 'Node20',
|
||||
moduleResolution: 'Node16',
|
||||
target: 'ES2022',
|
||||
strict: true,
|
||||
types: [],
|
||||
rootDir: '../src',
|
||||
declaration: true,
|
||||
declarationMap: true,
|
||||
sourceMap: true
|
||||
},
|
||||
include: ['../src/**/*.ts']
|
||||
})
|
||||
)
|
||||
writeFileSync(join(root, 'src', 'constants.ts'), 'export const enum Mode { Ready = 7 }\n')
|
||||
writeFileSync(
|
||||
join(root, 'src', 'entry.ts'),
|
||||
`import { Mode } from './constants'
|
||||
export interface Item { value: number }
|
||||
export const item: Item = { value: Mode.Ready }
|
||||
`
|
||||
)
|
||||
return root
|
||||
}
|
||||
|
||||
it('checks the emit project first, retains all CPU cores and preserves the TypeScript emitter', async () => {
|
||||
const order = []
|
||||
const check = vi.fn(async () => {
|
||||
order.push('check')
|
||||
return []
|
||||
})
|
||||
const execute = vi.fn(async () => {
|
||||
order.push('emit')
|
||||
return { code: 0 }
|
||||
})
|
||||
await buildCli({ check, execute, resolveBunExecutable: () => 'pinned-bun' })
|
||||
expect(order).toEqual(['check', 'emit'])
|
||||
expect(check).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
compiler: 'bun',
|
||||
bunExecutable: 'pinned-bun',
|
||||
projects: [{ config: 'config/tsconfig.build.cli.json', heapGib: 2 }],
|
||||
bunThreads: availableParallelism()
|
||||
})
|
||||
)
|
||||
expect(execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
program: process.execPath,
|
||||
args: [
|
||||
resolveProjectCompiler('config/tsconfig.build.cli.json'),
|
||||
'-p',
|
||||
'config/tsconfig.build.cli.json',
|
||||
'--outDir',
|
||||
'out',
|
||||
'--composite',
|
||||
'false',
|
||||
'--incremental',
|
||||
'false',
|
||||
'--noCheck'
|
||||
]
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('does not emit when checking fails or the pinned Bun is missing', async () => {
|
||||
const execute = vi.fn()
|
||||
await expect(
|
||||
buildCli({
|
||||
check: async () => [new Error('type error')],
|
||||
execute,
|
||||
resolveBunExecutable: () => 'pinned-bun'
|
||||
})
|
||||
).rejects.toThrow('type error')
|
||||
await expect(buildCli({ execute, resolveBunExecutable: () => null })).rejects.toThrow(
|
||||
'run pnpm install'
|
||||
)
|
||||
expect(execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('propagates emitter failures', async () => {
|
||||
await expect(
|
||||
buildCli({
|
||||
check: async () => [],
|
||||
resolveBunExecutable: () => 'pinned-bun',
|
||||
execute: async () => ({ code: null, signal: 'SIGKILL' })
|
||||
})
|
||||
).rejects.toThrow('signal=SIGKILL')
|
||||
})
|
||||
|
||||
it('rejects unknown compiler selections before emission', async () => {
|
||||
const execute = vi.fn()
|
||||
await expect(buildCli({ compiler: 'unknown', execute })).rejects.toThrow(
|
||||
'Invalid ORCA_CLI_BUILD_COMPILER'
|
||||
)
|
||||
expect(execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('produces byte-identical JavaScript, declarations and maps with the real compilers', async () => {
|
||||
const root = fixture()
|
||||
const baseline = await runProcess({
|
||||
program: process.execPath,
|
||||
args: [
|
||||
resolveProjectCompiler('config/tsconfig.build.cli.json', root),
|
||||
'-p',
|
||||
'config/tsconfig.build.cli.json',
|
||||
'--outDir',
|
||||
'baseline',
|
||||
'--composite',
|
||||
'false',
|
||||
'--incremental',
|
||||
'false'
|
||||
],
|
||||
cwd: root
|
||||
})
|
||||
expect(baseline.code, baseline.stdout + baseline.stderr).toBe(0)
|
||||
await buildCli({ root, outDir: 'optimized', resolveBunExecutable })
|
||||
const files = readdirSync(join(root, 'baseline')).sort()
|
||||
expect(readdirSync(join(root, 'optimized')).sort()).toEqual(files)
|
||||
expect(files).toHaveLength(8)
|
||||
for (const file of files) {
|
||||
expect(
|
||||
readFileSync(join(root, 'optimized', file)).equals(
|
||||
readFileSync(join(root, 'baseline', file))
|
||||
),
|
||||
file
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('blocks output for real type errors and rejects compiler version drift', async () => {
|
||||
const root = fixture()
|
||||
writeFileSync(join(root, 'src', 'entry.ts'), 'export const value: number = "wrong"\n')
|
||||
const execute = vi.fn()
|
||||
await expect(buildCli({ root, execute, resolveBunExecutable })).rejects.toThrow(
|
||||
'exited with code 1'
|
||||
)
|
||||
expect(execute).not.toHaveBeenCalled()
|
||||
|
||||
const drift = fixture()
|
||||
rmSync(join(drift, 'node_modules'))
|
||||
mkdirSync(join(drift, 'node_modules', 'typescript'), { recursive: true })
|
||||
writeFileSync(
|
||||
join(drift, 'node_modules', 'typescript', 'package.json'),
|
||||
JSON.stringify({
|
||||
name: 'typescript',
|
||||
version: '99.0.0',
|
||||
bin: { tsc: 'compiler.cjs' }
|
||||
})
|
||||
)
|
||||
await expect(buildCli({ root: drift, execute, resolveBunExecutable })).rejects.toThrow(
|
||||
'resolves 99.0.0'
|
||||
)
|
||||
expect(execute).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.skipIf(!supportsTsrs())(
|
||||
'the optional compiler preserves emitted bytes and blocks output on type errors',
|
||||
async () => {
|
||||
const root = fixture()
|
||||
await buildCli({ root, outDir: 'baseline', compiler: 'bun', resolveBunExecutable })
|
||||
await buildCli({ root, outDir: 'optimized', compiler: 'tsrs' })
|
||||
const files = readdirSync(join(root, 'baseline')).sort()
|
||||
expect(readdirSync(join(root, 'optimized')).sort()).toEqual(files)
|
||||
for (const file of files) {
|
||||
expect(
|
||||
readFileSync(join(root, 'optimized', file)).equals(
|
||||
readFileSync(join(root, 'baseline', file))
|
||||
),
|
||||
file
|
||||
).toBe(true)
|
||||
}
|
||||
writeFileSync(join(root, 'src', 'entry.ts'), 'export const value: number = "wrong"\n')
|
||||
await expect(buildCli({ root, outDir: 'failed', compiler: 'tsrs' })).rejects.toThrow(
|
||||
'CLI compilation failed'
|
||||
)
|
||||
expect(existsSync(join(root, 'failed'))).toBe(false)
|
||||
}
|
||||
)
|
||||
@@ -77,14 +77,6 @@ describe('selectLatestStableReleaseTag', () => {
|
||||
expect(selectLatestStableReleaseTag(['v0.0.0'])).toBe('v0.0.0')
|
||||
}
|
||||
)
|
||||
|
||||
it('skips unfilled entries in a sparse tag list', () => {
|
||||
const sparse = []
|
||||
sparse.length = 12
|
||||
sparse[3] = 'v1.2.3'
|
||||
sparse[8] = 'v2.0.0'
|
||||
expect(selectLatestStableReleaseTag(Object.freeze(sparse))).toBe('v2.0.0')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkDaemonProtocolCrossing', () => {
|
||||
@@ -103,17 +95,6 @@ describe('checkDaemonProtocolCrossing', () => {
|
||||
|
||||
const env = {}
|
||||
|
||||
it('passes a bump that lists the newest release version, reporting rollback as info', () => {
|
||||
const repo = repoWithTags([
|
||||
['v1.0.9', 5],
|
||||
['v1.0.10', 6]
|
||||
])
|
||||
writeProtocol(repo, 7)
|
||||
const result = checkDaemonProtocolCrossing({ repoRoot: repo, env })
|
||||
expect(result).toMatchObject({ upgrade: true, rollback: false })
|
||||
expect(result.lines[0]).toBe('release v1.0.10: daemon protocol 6')
|
||||
})
|
||||
|
||||
it('fails when the working tree drops the newest release version', () => {
|
||||
const repo = repoWithTags([['v1.0.0', 6]])
|
||||
writeFileSync(
|
||||
|
||||
+3
-77
@@ -1,19 +1,13 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { dirname, join, resolve } from 'node:path'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
RUNTIME_LAUNCHER_PATHS,
|
||||
RUNTIME_PROTOCOL_OVERRIDE_ENV,
|
||||
assessRuntimeLauncherProtocolRatchet,
|
||||
checkRuntimeLauncherProtocolRatchet,
|
||||
isOverrideEnabled
|
||||
checkRuntimeLauncherProtocolRatchet
|
||||
} from './check-runtime-launcher-protocol-ratchet.mjs'
|
||||
import { DAEMON_PROTOCOL_SOURCE_PATH } from '../schema-facts/daemon-protocol-facts.mjs'
|
||||
import { classifyPrJobs } from '../../workflow/source-routing/pr-code-change-scope.mjs'
|
||||
|
||||
const repoRoot = resolve(import.meta.dirname, '../../..', '..')
|
||||
const repos = []
|
||||
const LAUNCHER = 'src/main/managed-server/install/orcad-remote-runtime.ts'
|
||||
|
||||
@@ -76,74 +70,6 @@ afterEach(() => {
|
||||
}
|
||||
})
|
||||
|
||||
describe('RUNTIME_LAUNCHER_PATHS', () => {
|
||||
it('names only files that exist, so a rename cannot silently drop one from the gate', () => {
|
||||
expect(RUNTIME_LAUNCHER_PATHS).toContain(LAUNCHER)
|
||||
expect(RUNTIME_LAUNCHER_PATHS.filter((path) => !existsSync(join(repoRoot, path)))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('PR routing', () => {
|
||||
it('runs the ratchet job when its checker or the protocol changes', () => {
|
||||
for (const file of [
|
||||
'config/scripts/daemon-protocol/admission/check-runtime-launcher-protocol-ratchet.mjs',
|
||||
DAEMON_PROTOCOL_SOURCE_PATH
|
||||
]) {
|
||||
expect(classifyPrJobs([file])['cross-version-wire']).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('assessRuntimeLauncherProtocolRatchet', () => {
|
||||
const launcherAndBump = {
|
||||
changedFiles: [LAUNCHER, DAEMON_PROTOCOL_SOURCE_PATH],
|
||||
baseProtocolVersion: 3,
|
||||
candidateProtocolVersion: 4
|
||||
}
|
||||
|
||||
it('fails a launcher change that also bumps the protocol', () => {
|
||||
expect(
|
||||
assessRuntimeLauncherProtocolRatchet({ ...launcherAndBump, overridden: false })
|
||||
).toMatchObject({
|
||||
ok: false,
|
||||
violated: true
|
||||
})
|
||||
})
|
||||
|
||||
it('lets an explicit override through while still reporting the violation', () => {
|
||||
const result = assessRuntimeLauncherProtocolRatchet({ ...launcherAndBump, overridden: true })
|
||||
expect(result).toMatchObject({ ok: true, violated: true })
|
||||
expect(result.lines.at(-1)).toContain('OVERRIDDEN')
|
||||
})
|
||||
|
||||
it('passes a protocol bump without a launcher change, and a launcher change without a bump', () => {
|
||||
expect(
|
||||
assessRuntimeLauncherProtocolRatchet({
|
||||
...launcherAndBump,
|
||||
changedFiles: [DAEMON_PROTOCOL_SOURCE_PATH],
|
||||
overridden: false
|
||||
}).ok
|
||||
).toBe(true)
|
||||
expect(
|
||||
assessRuntimeLauncherProtocolRatchet({
|
||||
...launcherAndBump,
|
||||
candidateProtocolVersion: 3,
|
||||
overridden: false
|
||||
}).ok
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOverrideEnabled', () => {
|
||||
it('accepts only an explicit true', () => {
|
||||
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: 'true' })).toBe(true)
|
||||
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: '1' })).toBe(true)
|
||||
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: 'false' })).toBe(false)
|
||||
expect(isOverrideEnabled({ [RUNTIME_PROTOCOL_OVERRIDE_ENV]: '' })).toBe(false)
|
||||
expect(isOverrideEnabled({})).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkRuntimeLauncherProtocolRatchet against git history', () => {
|
||||
it('enforces the restriction when the base predates the protocol source move', () => {
|
||||
const { repo, base } = repoWithCandidate((candidate) => {
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DAEMON_PROTOCOL_SOURCE_PATH,
|
||||
canAttach,
|
||||
crossingRequirements,
|
||||
parseDaemonProtocolFacts
|
||||
} from './daemon-protocol-facts.mjs'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../../../..')
|
||||
import { parseDaemonProtocolFacts } from './daemon-protocol-facts.mjs'
|
||||
|
||||
function source(current, previous) {
|
||||
return [
|
||||
@@ -24,16 +15,6 @@ const facts = (protocolVersion, previousProtocolVersions) => ({
|
||||
})
|
||||
|
||||
describe('parseDaemonProtocolFacts', () => {
|
||||
it('reads the working tree declarations and keeps the append-only range', () => {
|
||||
const parsed = parseDaemonProtocolFacts(
|
||||
readFileSync(join(projectDir, DAEMON_PROTOCOL_SOURCE_PATH), 'utf8')
|
||||
)
|
||||
expect(parsed.protocolVersion).toBeGreaterThan(1)
|
||||
expect(parsed.previousProtocolVersions).toEqual(
|
||||
Array.from({ length: parsed.protocolVersion - 1 }, (_, index) => index + 1)
|
||||
)
|
||||
})
|
||||
|
||||
it('parses multi-line lists and type annotations', () => {
|
||||
const text = [
|
||||
'// PROTOCOL_VERSION = 99 in a comment is ignored',
|
||||
@@ -63,28 +44,3 @@ describe('parseDaemonProtocolFacts', () => {
|
||||
expect(() => parseDaemonProtocolFacts(text, 'fixture.ts')).toThrow(/fixture\.ts/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('protocol crossing', () => {
|
||||
const release = facts(37, [36, 35])
|
||||
|
||||
it('lets a same-version or listing candidate adopt the release daemon', () => {
|
||||
expect(canAttach(facts(37, [36, 35]), release)).toBe(true)
|
||||
expect(canAttach(facts(38, [37, 36, 35]), release)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a candidate that dropped the release version', () => {
|
||||
expect(canAttach(facts(38, [36, 35]), release)).toBe(false)
|
||||
})
|
||||
|
||||
it('cannot roll back past a protocol bump', () => {
|
||||
expect(canAttach(release, facts(38, [37, 36, 35]))).toBe(false)
|
||||
expect(canAttach(release, facts(36, [35]))).toBe(true)
|
||||
})
|
||||
|
||||
it('describes the requirements in both directions', () => {
|
||||
expect(crossingRequirements(release)).toMatchObject({
|
||||
upgrade: 'candidate speaks 37 or lists 37 as previous',
|
||||
rollback: expect.stringContaining('35..37')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
-136
@@ -1,136 +0,0 @@
|
||||
import {
|
||||
cpSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { isAbsolute, join, parse, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { resolveCliCommand } from '../../../../src/shared/node-cli-command-resolution.ts'
|
||||
import { removeTreeSync } from '../../../../src/shared/windows-transient-lock-removal.ts'
|
||||
import { resolvePnpmCliInvocation } from '../../tool-invocation/pnpm-cli-invocation.mjs'
|
||||
import { workspacePackageManifests } from '../../workspace/source-exports/workspace-source-exports.mjs'
|
||||
|
||||
// Run actual pnpm: it hashes LF-normalized patch content; repeated raw SHA checks agreed with a bad CRLF lockfile and missed frozen-install failures.
|
||||
// lockfile-only/ignore-scripts checks resolution without node_modules or native builds; patch application is tested separately below.
|
||||
const PROJECT_DIR = resolve(import.meta.dirname, '../../../..')
|
||||
const WINDOWS_PROCESS_TREE_PATCH = '@vscode__windows-process-tree@0.8.0.patch'
|
||||
|
||||
// Use pnpm-cli-invocation’s Windows shim resolver; omit shell because runProcessSync rejects it and handles interpreter routing itself.
|
||||
function resolvePnpmInvocation() {
|
||||
const { command, prefixArgs } = resolvePnpmCliInvocation()
|
||||
if (isAbsolute(command)) {
|
||||
return existsSync(command) ? { program: command, prefixArgs } : null
|
||||
}
|
||||
// Without npm_execpath, use bare extensionless name so shared resolver can try every executable form.
|
||||
const resolved = resolveCliCommand(parse(command).name)
|
||||
return isAbsolute(resolved) ? { program: resolved, prefixArgs } : null
|
||||
}
|
||||
|
||||
describe('patched dependencies', () => {
|
||||
it('installs with --frozen-lockfile, which is what validates every patch hash', () => {
|
||||
const pnpm = resolvePnpmInvocation()
|
||||
expect(pnpm, 'pnpm must be installed; it is the only thing that can check this').not.toBeNull()
|
||||
|
||||
// Use a copy: even frozen-lockfile rewrites untracked lockfile sections; preserve repository bytes.
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'orca-frozen-install-'))
|
||||
try {
|
||||
for (const file of [
|
||||
'package.json',
|
||||
'pnpm-lock.yaml',
|
||||
'pnpm-workspace.yaml',
|
||||
// The lockfile pins its checksum, so a frozen install fails without it.
|
||||
'config/pnpmfile.cjs',
|
||||
...workspacePackageManifests(PROJECT_DIR)
|
||||
]) {
|
||||
mkdirSync(resolve(scratch, file, '..'), { recursive: true })
|
||||
copyFileSync(join(PROJECT_DIR, file), join(scratch, file))
|
||||
}
|
||||
mkdirSync(join(scratch, 'config'), { recursive: true })
|
||||
cpSync(join(PROJECT_DIR, 'config', 'patches'), join(scratch, 'config', 'patches'), {
|
||||
recursive: true
|
||||
})
|
||||
|
||||
const result = runProcessSync({
|
||||
program: pnpm.program,
|
||||
args: [
|
||||
...pnpm.prefixArgs,
|
||||
'install',
|
||||
'--frozen-lockfile',
|
||||
'--lockfile-only',
|
||||
'--ignore-scripts'
|
||||
],
|
||||
cwd: scratch,
|
||||
timeoutMs: 300_000
|
||||
})
|
||||
|
||||
expect(result.code, `${result.stdout}\n${result.stderr}`).toBe(0)
|
||||
} finally {
|
||||
removeTreeSync(scratch)
|
||||
}
|
||||
// Allow the full 300s spawn budget beyond Vitest’s 30s case default.
|
||||
}, 300_000)
|
||||
|
||||
// lockfile-only never applies patches; install one real dependency and check its marker to cover application without a full install.
|
||||
it('materializes the patched command-line reader on a real install', () => {
|
||||
const pnpm = resolvePnpmInvocation()
|
||||
expect(pnpm, 'pnpm must be installed; it is the only thing that can check this').not.toBeNull()
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'orca-patch-apply-'))
|
||||
try {
|
||||
mkdirSync(join(scratch, 'config', 'patches'), { recursive: true })
|
||||
copyFileSync(
|
||||
join(PROJECT_DIR, 'config', 'patches', WINDOWS_PROCESS_TREE_PATCH),
|
||||
join(scratch, 'config', 'patches', WINDOWS_PROCESS_TREE_PATCH)
|
||||
)
|
||||
writeFileSync(
|
||||
join(scratch, 'package.json'),
|
||||
`${JSON.stringify(
|
||||
{
|
||||
name: 'orca-patch-apply-probe',
|
||||
version: '1.0.0',
|
||||
dependencies: { '@vscode/windows-process-tree': '0.8.0' }
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
)
|
||||
writeFileSync(
|
||||
join(scratch, 'pnpm-workspace.yaml'),
|
||||
'packages: []\n' +
|
||||
'patchedDependencies:\n' +
|
||||
` '@vscode/windows-process-tree@0.8.0': config/patches/${WINDOWS_PROCESS_TREE_PATCH}\n`
|
||||
)
|
||||
|
||||
const result = runProcessSync({
|
||||
program: pnpm.program,
|
||||
args: [...pnpm.prefixArgs, 'install', '--no-frozen-lockfile', '--ignore-scripts'],
|
||||
cwd: scratch,
|
||||
timeoutMs: 300_000
|
||||
})
|
||||
expect(result.code, `${result.stdout}\n${result.stderr}`).toBe(0)
|
||||
|
||||
const materialized = readFileSync(
|
||||
join(
|
||||
scratch,
|
||||
'node_modules',
|
||||
'@vscode',
|
||||
'windows-process-tree',
|
||||
'src',
|
||||
'process_commandline.cc'
|
||||
),
|
||||
'utf8'
|
||||
)
|
||||
expect(materialized).toContain('kProcessCommandLineInformation')
|
||||
// Require upstream reader removal, not merely a supplementary patched reader.
|
||||
expect(materialized).not.toContain('ReadProcessMemory')
|
||||
} finally {
|
||||
removeTreeSync(scratch)
|
||||
}
|
||||
}, 300_000)
|
||||
})
|
||||
Reference in New Issue
Block a user