mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 09:07:40 +00:00
## ELI5 Keep the test suite working with the Jira features that have just landed. There is no visible app change. ## What Changed The host-picker test now expects the host-qualified selection the parent already accepts. The startup fixture provides Electron's protocol handler and verifies that ready installs the Jira image scheme. Jira edit tests use exported wrappers with module-local setters that are cleared on unmount, preserving the issue/source switch tests without the banned mutable function export. The WSL storage inventory classifies Jira list columns, linked-view choice and prompt templates as device preferences, matching their keys and the existing Linear policy. Skill-guide tests check the actual generated discovery projection rather than assuming it is shorter than every full guide or uses one fixed phrase. The cache test follows the headless workflow's deliberate removal of its push trigger; cache-warmer checks remain intact. ## Why Final CI found six stale integration assumptions after the Jira batch. Updating the fixtures and inventory preserves the landed behavior and the underlying checks. Changing production code, padding the Jira guide, or restoring an intentionally removed workflow trigger would change the product to satisfy incorrect test assumptions. ## Linked Issue Follow-up to #28622, #28631 and #28592. The existing headless trigger policy was introduced in #28664. ## Visual Proof N/A — tests, test fixtures and storage inventory declarations only; no visual or interaction change. ## Testing - [x] Automated tests updated. - [x] 196 focused tests across nine files pass on macOS, covering every failed shard assertion, fixture consumers and the headless workflow policy. - [x] Node and web typechecks pass; all seven changed-code quality stages, formatting and diff checks pass. - Reproduced five failing assertions in startup/export/storage tests and the skill projection failure before the repairs. The host-picker owner also reproduced the stale selection failure before correcting it. - Final full CI will run on this PR. The prior Ubuntu 22.04 persistence job crashed in the native watcher build before persistence tests ran; this change does not claim a Linux release, live Jira or real SSH pass. ## Review Independent source review found no issues; all six reviewed file hashes and the complete diff match committed head `aa703eb50147ba95646cb82f10b6d86b4f890fc4`. Risk: low. No authentication, network, protocol implementation, workflow, renderer behavior, remote messages or migration rewrite logic changes. ## Agent skill upstream boundary - [x] Not applicable; no upstream skill-installer source or artifacts copied. ## Notes The three storage keys contain device-level preferences, without workspace paths or identifiers. Tests preserve host disambiguation and issue/source switching. Platform, SSH and provider behavior remains in the existing implementations. ## Checklist - [x] Small and focused. - [x] Explained the before/after, mechanism and choice of approach. - [x] Visual proof marked N/A with a reason. - [x] Self-reviewed for correctness and cleanup. - [x] Cross-platform and SSH impact considered. - [x] Focused tests, types, formatting and quality checks pass; full CI covers the broader build.
224 lines
8.5 KiB
JavaScript
224 lines
8.5 KiB
JavaScript
import {
|
|
appendFileSync,
|
|
cpSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
readdirSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, expect, it } from 'vitest'
|
|
import { parse } from 'yaml'
|
|
import {
|
|
activateCompilerCache,
|
|
compilerCacheIdentity,
|
|
packCompilerCache
|
|
} from './headless-detector-compiler-cache.mjs'
|
|
import { collectNodeServerInputs } from './node-server-change-scope.mjs'
|
|
import { runProcessSync } from '@orca/process-host'
|
|
|
|
const temporary = []
|
|
afterEach(() => {
|
|
for (const dir of temporary.splice(0)) {
|
|
rmSync(dir, { recursive: true, force: true })
|
|
}
|
|
})
|
|
function fixture() {
|
|
const directory = mkdtempSync(join(tmpdir(), 'headless-compiler-cache-'))
|
|
temporary.push(directory)
|
|
const root = join(directory, 'checkout')
|
|
mkdirSync(root)
|
|
writeFileSync(join(root, 'package.json'), '{"type":"module"}')
|
|
const identity = compilerCacheIdentity({ policyHash: 'policy', cacheRoot: directory })
|
|
packCompilerCache({ identity })
|
|
return { root, identity }
|
|
}
|
|
function changeManifest(identity, update) {
|
|
const path = join(identity.path, 'manifest.json')
|
|
const manifest = JSON.parse(readFileSync(path, 'utf8'))
|
|
update(manifest)
|
|
writeFileSync(path, JSON.stringify(manifest))
|
|
}
|
|
|
|
it('separates policy, Node, platform and architecture identities with the same archive path', () => {
|
|
const options = {
|
|
policyHash: 'policy',
|
|
cacheRoot: '/cache',
|
|
platform: 'linux',
|
|
arch: 'x64',
|
|
node: 'v24.21.0'
|
|
}
|
|
const original = compilerCacheIdentity(options)
|
|
for (const override of [
|
|
{ policyHash: 'changed' },
|
|
{ node: 'v24.22.0' },
|
|
{ platform: 'darwin' },
|
|
{ arch: 'arm64' }
|
|
]) {
|
|
const other = compilerCacheIdentity({ ...options, ...override })
|
|
expect(other.key).not.toBe(original.key)
|
|
expect(other.path).toBe(original.path)
|
|
}
|
|
})
|
|
|
|
it('activates the compiler and workspace-policy parser and preserves import graph behavior', async () => {
|
|
const { root, identity } = fixture()
|
|
expect(await activateCompilerCache({ root, identity })).toEqual({ available: true })
|
|
expect(readdirSync(join(root, 'node_modules')).sort()).toEqual(['@esbuild', 'esbuild', 'yaml'])
|
|
for (const name of [
|
|
'node-server-change-scope',
|
|
'node-server-test-paths',
|
|
'node-server-qualification',
|
|
'orcad-entry-build',
|
|
'workspace-source-exports'
|
|
]) {
|
|
mkdirSync(join(root, 'config', 'scripts'), { recursive: true })
|
|
cpSync(
|
|
new URL(`./${name}.mjs`, import.meta.url),
|
|
join(root, 'config', 'scripts', `${name}.mjs`)
|
|
)
|
|
}
|
|
mkdirSync(join(root, 'src', 'shared'), { recursive: true })
|
|
cpSync(
|
|
new URL('../../src/shared/orcad-artifacts.ts', import.meta.url),
|
|
join(root, 'src', 'shared', 'orcad-artifacts.ts')
|
|
)
|
|
writeFileSync(
|
|
join(root, 'entry.ts'),
|
|
"import './first'; export * from './exports'; import('./dynamic'); require('./required'); import 'external-package'; import './native.node'; import '@orca/process-host'"
|
|
)
|
|
writeFileSync(join(root, 'pnpm-workspace.yaml'), 'packages:\n - src/packages/*\n')
|
|
mkdirSync(join(root, 'src/packages/process-host/src'), { recursive: true })
|
|
writeFileSync(
|
|
join(root, 'src/packages/process-host/package.json'),
|
|
JSON.stringify({
|
|
name: '@orca/process-host',
|
|
exports: { '.': { 'orca-source': './src/run-process.ts', default: './dist/run-process.js' } }
|
|
})
|
|
)
|
|
writeFileSync(
|
|
join(root, 'src/packages/process-host/src/run-process.ts'),
|
|
'export const value = 1'
|
|
)
|
|
for (const name of ['first', 'exports', 'dynamic', 'required']) {
|
|
writeFileSync(join(root, `${name}.ts`), 'export const value = 1')
|
|
}
|
|
writeFileSync(
|
|
join(root, 'probe.mjs'),
|
|
"import { collectNodeServerInputs } from './config/scripts/node-server-change-scope.mjs'; console.log(JSON.stringify([...(await collectNodeServerInputs({ root: process.cwd(), entryPoints: ['entry.ts'] }))].sort()))"
|
|
)
|
|
const baseline = [...(await collectNodeServerInputs({ root, entryPoints: ['entry.ts'] }))].sort()
|
|
expect(baseline).toContain('src/packages/process-host/src/run-process.ts')
|
|
expect(baseline).toContain('src/packages/process-host/package.json')
|
|
const candidate = runProcessSync({
|
|
program: process.execPath,
|
|
args: ['probe.mjs'],
|
|
cwd: root,
|
|
timeoutMs: 10_000
|
|
})
|
|
expect(candidate.code, candidate.stderr).toBe(0)
|
|
expect(JSON.parse(candidate.stdout.trim())).toEqual(baseline)
|
|
}, 20_000)
|
|
|
|
it.each(['key', 'node', 'version', 'files'])(
|
|
'falls back on invalid manifest %s and cleans partial activation',
|
|
async (field) => {
|
|
const { root, identity } = fixture()
|
|
changeManifest(identity, (manifest) => {
|
|
manifest[field] = 'wrong'
|
|
})
|
|
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
|
|
expect(existsSync(join(root, 'node_modules'))).toBe(false)
|
|
}
|
|
)
|
|
|
|
it.each(['modified', 'missing', 'extra', 'symlink', 'malformed'])(
|
|
'falls back on %s cache contents before loading code',
|
|
async (kind) => {
|
|
const { root, identity } = fixture()
|
|
const compiler = join(identity.path, 'node_modules', 'esbuild', 'lib', 'main.js')
|
|
if (kind === 'modified') {
|
|
appendFileSync(compiler, '\nthrow Error("must not load")')
|
|
}
|
|
if (kind === 'missing') {
|
|
rmSync(compiler)
|
|
}
|
|
if (kind === 'extra') {
|
|
writeFileSync(join(identity.path, 'unexpected'), 'extra')
|
|
}
|
|
if (kind === 'symlink') {
|
|
rmSync(compiler)
|
|
symlinkSync(join(root, 'package.json'), compiler)
|
|
}
|
|
if (kind === 'malformed') {
|
|
writeFileSync(join(identity.path, 'manifest.json'), '{')
|
|
}
|
|
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
|
|
expect(existsSync(join(root, 'node_modules'))).toBe(false)
|
|
}
|
|
)
|
|
|
|
it('leaves existing dependencies alone and falls back on an absent archive', async () => {
|
|
const { root, identity } = fixture()
|
|
rmSync(identity.path, { recursive: true })
|
|
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
|
|
mkdirSync(join(root, 'node_modules'))
|
|
writeFileSync(join(root, 'node_modules', 'retained'), 'retained')
|
|
expect((await activateCompilerCache({ root, identity })).available).toBe(false)
|
|
expect(readFileSync(join(root, 'node_modules', 'retained'), 'utf8')).toBe('retained')
|
|
})
|
|
|
|
it('uses exact optional restores, seeds only main and retains full dependency fallback', () => {
|
|
const action = parse(readFileSync('.github/actions/prepare-headless-compiler/action.yml', 'utf8'))
|
|
const steps = action.runs.steps
|
|
const restore = steps.find((step) => step.id === 'cache')
|
|
expect(restore.uses).toBe('actions/cache/restore@v5')
|
|
expect(restore['continue-on-error']).toBe(true)
|
|
expect(restore.with['restore-keys']).toBeUndefined()
|
|
const save = steps.find((step) => step.uses === 'actions/cache/save@v5')
|
|
expect(save.if).toContain("github.ref == 'refs/heads/main'")
|
|
expect(save.if).toContain("github.event_name != 'pull_request'")
|
|
expect(save['continue-on-error']).toBe(true)
|
|
expect(save.with).toEqual(restore.with)
|
|
const workflow = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
|
|
const detector = workflow.jobs.changes.steps
|
|
const cached = detector.find((step) => step.id === 'compiler')
|
|
expect(cached.if).toBe("steps.scope.outputs.graph_required == 'true'")
|
|
expect(cached['continue-on-error']).toBe(true)
|
|
expect(
|
|
detector.find((step) => step.uses === './.github/actions/install-node-dependencies').if
|
|
).toBe(
|
|
"steps.scope.outputs.graph_required == 'true' && steps.compiler.outputs.available != 'true'"
|
|
)
|
|
expect(workflow.on.pull_request.paths).toContain('.github/actions/prepare-headless-compiler/**')
|
|
const warmer = parse(readFileSync('.github/workflows/ci-cache-warmup.yml', 'utf8'))
|
|
const warmSteps = warmer.jobs.warm.steps
|
|
expect(
|
|
warmSteps.findIndex((step) => step.uses === './.github/actions/prepare-headless-compiler')
|
|
).toBeGreaterThan(
|
|
warmSteps.findIndex((step) => step.uses === './.github/actions/install-node-dependencies')
|
|
)
|
|
for (const event of ['push', 'pull_request']) {
|
|
expect(warmer.on[event].paths).toContain('.github/actions/prepare-headless-compiler/**')
|
|
}
|
|
const inputs = [
|
|
...steps.find((step) => step.id === 'identity').env.COMPILER_POLICY_HASH.matchAll(/'([^']+)'/g)
|
|
].map((match) => match[1])
|
|
for (const input of inputs) {
|
|
expect(
|
|
warmer.on.push.paths.some(
|
|
(pattern) =>
|
|
pattern === input ||
|
|
(pattern.endsWith('/**') && input.startsWith(pattern.slice(0, -2))) ||
|
|
(pattern.endsWith('*') && input.startsWith(pattern.slice(0, -1)))
|
|
),
|
|
input
|
|
).toBe(true)
|
|
}
|
|
})
|