mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 08:02:32 +00:00
* test: keep structured chat free of Electron imports
* test: widen runtime Electron ratchet to structured chat
* test: check whole structured-chat directories for Electron imports
Cover src/main/{native-chat,claude,codex}, src/shared and every structured-*
or agent-session-* file under src/main/runtime, so new files are checked by
default. A lane that exists today now fails loudly if it goes missing; only
the not-yet-landed acp/ and provider-process/ may be absent.
Move the test-only file rule into one classifier shared with the
localization audit, so -test-<thing> helpers and test doubles no longer
enter the production gate.
* test: run the Electron-import CLI path in tests and retire may-be-absent lanes
Export main() so the real-tree tests exercise the entry list CI uses,
check every required lane for the missing-directory error, and fail once
acp/ or provider-process/ exists while still allowed to be absent.
218 lines
8.4 KiB
JavaScript
218 lines
8.4 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import process from 'node:process'
|
|
import {
|
|
collectElectronImporters,
|
|
collectStructuredChatEntryPoints,
|
|
defaultEntryPoints,
|
|
diffAgainstBaseline,
|
|
main,
|
|
readBaseline,
|
|
STRUCTURED_CHAT_LANES
|
|
} from './check-runtime-electron-ratchet.mjs'
|
|
|
|
describe('structured chat coverage', () => {
|
|
const roots = []
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
function fixture(files) {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'orca-electron-ratchet-'))
|
|
roots.push(root)
|
|
for (const [file, source] of Object.entries(files)) {
|
|
const absolute = path.join(root, file)
|
|
mkdirSync(path.dirname(absolute), { recursive: true })
|
|
writeFileSync(absolute, source)
|
|
}
|
|
return root
|
|
}
|
|
|
|
// Every lane that must exist; acp/ and provider-process/ may be absent until they land.
|
|
const requiredLanes = {
|
|
'src/main/native-chat/reader.ts': 'export {}',
|
|
'src/main/claude/claude-session.ts': 'export {}',
|
|
'src/main/codex/codex-session.ts': 'export {}',
|
|
'src/main/runtime/structured-agent-session-host.ts': 'export {}',
|
|
'src/shared/agent-session-record.ts': 'export {}'
|
|
}
|
|
|
|
it('covers whole lane directories, structured runtime files at any depth, and no test code', () => {
|
|
const sources = [
|
|
...Object.keys(requiredLanes),
|
|
'src/main/native-chat/nested/reader.ts',
|
|
'src/main/native-chat/worker.mjs',
|
|
'src/main/claude/other.ts',
|
|
'src/main/codex/codex-provider-timeline-identity.ts',
|
|
'src/shared/nested/agent-session-account-home.ts',
|
|
'src/shared/relay-runtime-self-test-report.ts',
|
|
'src/main/runtime/agent-session-record.ts',
|
|
'src/main/runtime/structured-agent-runtime-registrations.ts',
|
|
'src/main/runtime/rpc/methods/structured-agent-session-agents.ts',
|
|
'src/main/acp/adapter.ts',
|
|
'src/main/provider-process/worker.ts'
|
|
]
|
|
const excluded = [
|
|
'src/main/native-chat/reader.test.ts',
|
|
'src/main/native-chat/reader.spec.ts',
|
|
'src/main/native-chat/reader-test-support.ts',
|
|
'src/main/native-chat/reader.test-support.ts',
|
|
'src/main/native-chat/structured-agent-session-rest-test-rig.ts',
|
|
'src/main/native-chat/structured-agent-session-host-test-data.ts',
|
|
'src/main/native-chat/reader.test-fixture.ts',
|
|
'src/main/native-chat/reader-fixtures.ts',
|
|
'src/main/codex/codex-turn-lifecycle-fake.ts',
|
|
'src/main/codex/codex-session-backfill-fs-mocks.ts',
|
|
'src/main/native-chat/__fixtures__/reader.ts',
|
|
'src/main/native-chat/test-support/reader.ts',
|
|
'src/main/runtime/orca-runtime-tests/structured-agent-session-host.ts',
|
|
'src/shared/types.d.ts',
|
|
'src/main/runtime/other.ts',
|
|
'src/main/runtime/rpc/methods/browser.ts'
|
|
]
|
|
const root = fixture(
|
|
Object.fromEntries([...sources, ...excluded].map((file) => [file, 'export {}']))
|
|
)
|
|
expect(collectStructuredChatEntryPoints(root)).toEqual(
|
|
sources.map((file) => path.join(root, ...file.split('/'))).sort()
|
|
)
|
|
})
|
|
|
|
it.each(Object.keys(requiredLanes).map((file) => path.posix.dirname(file)))(
|
|
'fails loudly when %s goes missing, so a rename cannot empty it',
|
|
(lane) => {
|
|
const without = Object.fromEntries(
|
|
Object.entries(requiredLanes).filter(([file]) => !file.startsWith(`${lane}/`))
|
|
)
|
|
expect(() => collectStructuredChatEntryPoints(fixture(without))).toThrow(`${lane} is missing`)
|
|
expect(collectStructuredChatEntryPoints(fixture(requiredLanes))).toHaveLength(5)
|
|
}
|
|
)
|
|
|
|
it('finds Electron through a package imported by each unwired future lane', async () => {
|
|
const root = fixture({
|
|
...requiredLanes,
|
|
'src/main/acp/adapter.ts': "import 'acp-desktop-package'",
|
|
'src/main/provider-process/worker.ts': "import 'provider-desktop-package'",
|
|
'node_modules/acp-desktop-package/package.json': '{"main":"index.js"}',
|
|
'node_modules/acp-desktop-package/index.js': "require('electron')",
|
|
'node_modules/provider-desktop-package/package.json': '{"main":"index.js"}',
|
|
'node_modules/provider-desktop-package/index.js': "require('electron')"
|
|
})
|
|
const current = await collectElectronImporters(collectStructuredChatEntryPoints(root))
|
|
expect(current.map((file) => file.split('/node_modules/').pop())).toEqual([
|
|
'acp-desktop-package/index.js',
|
|
'provider-desktop-package/index.js'
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('the default entry points', () => {
|
|
const lanes = ['native-chat', 'claude', 'codex', 'runtime'].map((lane) => `src/main/${lane}/`)
|
|
|
|
it('are the runtime entries plus a file from every lane that exists', () => {
|
|
const entries = defaultEntryPoints().map((file) =>
|
|
path.relative(process.cwd(), file).split(path.sep).join('/')
|
|
)
|
|
expect(entries.slice(0, 3)).toEqual([
|
|
'src/main/runtime/orca-runtime.ts',
|
|
'src/main/runtime/runtime-rpc.ts',
|
|
'src/main/orcad/main.ts'
|
|
])
|
|
for (const lane of [...lanes, 'src/shared/']) {
|
|
expect(entries.some((file) => file.startsWith(lane))).toBe(true)
|
|
}
|
|
})
|
|
|
|
// Retires the temporary flag: the PR that adds acp/ or provider-process/ must make it required.
|
|
it('lets only directories that have not landed yet be absent', () => {
|
|
for (const lane of STRUCTURED_CHAT_LANES.filter((candidate) => candidate.mayBeAbsent)) {
|
|
expect(
|
|
existsSync(path.join(process.cwd(), ...lane.directory)),
|
|
lane.directory.join('/')
|
|
).toBe(false)
|
|
}
|
|
})
|
|
})
|
|
|
|
// Why `main`: it is what `pnpm lint` and CI run, so these fail if its entry list drops the lanes.
|
|
describe('the command-line check', () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
// Why a file the runtime doesn't load: only the structured-chat lanes can catch it.
|
|
it('fails on Electron in structured-chat code the runtime graph does not reach', async () => {
|
|
const target = path.join(
|
|
process.cwd(),
|
|
'src',
|
|
'main',
|
|
'native-chat',
|
|
'transcript-read-cache.ts'
|
|
)
|
|
const addElectron = {
|
|
name: 'add-electron-import',
|
|
setup(pluginBuild) {
|
|
pluginBuild.onLoad({ filter: /transcript-read-cache\.ts$/ }, (args) =>
|
|
args.path === target
|
|
? { contents: `import 'electron'\n${readFileSync(target, 'utf8')}`, loader: 'ts' }
|
|
: undefined
|
|
)
|
|
}
|
|
}
|
|
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
expect(await main([], { plugins: [addElectron] })).toBe(1)
|
|
expect(error.mock.calls.join('\n')).toContain('+ src/main/native-chat/transcript-read-cache.ts')
|
|
}, 120_000)
|
|
|
|
// Why real: the value of this gate is the transitive edges, which a fixture cannot model.
|
|
it('passes on the tree as it is, matching the checked-in baseline', async () => {
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
expect(await main([])).toBe(0)
|
|
}, 120_000)
|
|
})
|
|
|
|
describe('readBaseline', () => {
|
|
it('drops comments and blank lines and sorts, so baseline formatting cannot cause a false diff', () => {
|
|
expect(readBaseline('# header\n\n b/second.ts \na/first.ts\n')).toEqual([
|
|
'a/first.ts',
|
|
'b/second.ts'
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('diffAgainstBaseline', () => {
|
|
it('reports a module that started importing electron', () => {
|
|
expect(diffAgainstBaseline(['a.ts', 'b.ts'], ['a.ts'])).toEqual({
|
|
added: ['b.ts'],
|
|
removed: []
|
|
})
|
|
})
|
|
|
|
it('reports a module that stopped, so the baseline is forced to tighten rather than drift', () => {
|
|
expect(diffAgainstBaseline(['a.ts'], ['a.ts', 'b.ts'])).toEqual({
|
|
added: [],
|
|
removed: ['b.ts']
|
|
})
|
|
})
|
|
|
|
it('is quiet when the set is unchanged', () => {
|
|
expect(diffAgainstBaseline(['a.ts'], ['a.ts'])).toEqual({ added: [], removed: [] })
|
|
})
|
|
})
|
|
|
|
describe('the checked-in baseline', () => {
|
|
// Why an exact-empty assertion now: the reachable set reached zero, so "may only
|
|
// shrink" has no room left and any entry at all is a regression. This is strictly
|
|
// stronger than the old under-src/ check, which only stopped a node_modules path from
|
|
// padding a non-empty count.
|
|
it('stays empty, so nothing reachable from the runtime imports electron', () => {
|
|
const baseline = readBaseline(readFileSync('config/runtime-electron-baseline.txt', 'utf8'))
|
|
expect(baseline).toEqual([])
|
|
})
|
|
})
|