mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 17:47:40 +00:00
Extract subprocess execution and process contracts into @orca/process-host with explicit public entry points, independent checks, and one Node 18 compatible CommonJS runtime. Migrate application consumers and integrate package builds, dependency selection, import enforcement, and runtime resource staging. Preserve daemon startup ownership and profile isolation, and qualify development rebuild recovery and concurrent output publication. Remove unnecessary aliases, test forwarding exports, and a duplicate output scan. Validation: full typechecks, changed-code quality, package and artifact checks, native Windows builds/types plus 306 runtime tests, and independent native reviews. Matched full E2E controls on original main and unchanged reviewed head both pass all 17 applicable tests (nine skips). Earlier intermittent E2E failures are documented in the PR; no assertions were weakened. The final combined-tree audit preserves both branches with no import-boundary violations.
293 lines
11 KiB
JavaScript
293 lines
11 KiB
JavaScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { 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
|
|
} 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.
|
|
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/main/provider-process/provider-process-teardown.ts': 'export {}',
|
|
'src/main/acp/acp-structured-session-adapter.ts': 'export {}',
|
|
'src/main/jsonl-rpc/peer.ts': 'export {}',
|
|
'src/main/pi/rpc-launch.ts': 'export {}',
|
|
'src/shared/agent-session-record.ts': 'export {}',
|
|
'src/packages/process-host/src/run-process.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/packages/process-host/src/nested/process-state.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',
|
|
'src/main/jsonl-rpc/nested/dialect.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/packages/process-host/src/run-process.test.ts',
|
|
'src/packages/process-host/src/__fixtures__/spawned-child.ts',
|
|
'src/main/runtime/other.ts',
|
|
'src/main/runtime/rpc/methods/browser.ts',
|
|
'src/main/jsonl-rpc/peer.test.ts',
|
|
'src/main/pi/rpc-launch.test.ts',
|
|
'src/main/pi/titlebar-extension-service.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(
|
|
Object.keys(requiredLanes).length
|
|
)
|
|
}
|
|
)
|
|
|
|
it.each([true, false])(
|
|
'finds Electron through an unwired lane package (sideEffects=%s)',
|
|
async (sideEffects) => {
|
|
const root = fixture({
|
|
...requiredLanes,
|
|
'src/main/acp/adapter.ts': "import 'acp-desktop-package'",
|
|
'src/main/provider-process/worker.ts': "import 'provider-desktop-package'",
|
|
'src/main/jsonl-rpc/peer.ts': "import 'rpc-desktop-package'",
|
|
'src/main/pi/rpc-launch.ts': "import 'pi-desktop-package'",
|
|
'node_modules/acp-desktop-package/package.json': JSON.stringify({
|
|
main: 'index.js',
|
|
sideEffects
|
|
}),
|
|
'node_modules/acp-desktop-package/index.js': "require('electron')",
|
|
'node_modules/provider-desktop-package/package.json': JSON.stringify({
|
|
main: 'index.js',
|
|
sideEffects
|
|
}),
|
|
'node_modules/provider-desktop-package/index.js': "require('electron')",
|
|
'node_modules/rpc-desktop-package/package.json': JSON.stringify({
|
|
main: 'index.js',
|
|
sideEffects
|
|
}),
|
|
'node_modules/rpc-desktop-package/index.js': "require('electron')",
|
|
'node_modules/pi-desktop-package/package.json': JSON.stringify({
|
|
main: 'index.js',
|
|
sideEffects
|
|
}),
|
|
'node_modules/pi-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',
|
|
'pi-desktop-package/index.js',
|
|
'provider-desktop-package/index.js',
|
|
'rpc-desktop-package/index.js'
|
|
])
|
|
}
|
|
)
|
|
|
|
it('checks public workspace imports against source without requiring a built package', async () => {
|
|
const root = fixture({ 'entry.ts': "import '@orca/process-host'" })
|
|
const target = path.join(process.cwd(), 'src/packages/process-host/src/run-process.ts')
|
|
const current = await collectElectronImporters([path.join(root, 'entry.ts')], {
|
|
plugins: [
|
|
{
|
|
name: 'inject-process-host-electron-import',
|
|
setup(pluginBuild) {
|
|
pluginBuild.onLoad({ filter: /run-process\.ts$/ }, (args) =>
|
|
args.path === target ? { contents: "import 'electron'", loader: 'ts' } : undefined
|
|
)
|
|
}
|
|
}
|
|
]
|
|
})
|
|
expect(current).toEqual(['src/packages/process-host/src/run-process.ts'])
|
|
})
|
|
|
|
it('bounds shared dependency output without dropping any entry point', async () => {
|
|
const entries = Array.from({ length: 40 }, (_, index) => `entry-${index}.ts`)
|
|
const root = fixture({
|
|
'shared.ts': `export const payload = ${JSON.stringify('x'.repeat(64 * 1024))}`,
|
|
...Object.fromEntries(
|
|
entries.map((file) => [file, "import 'electron'; export { payload } from './shared'"])
|
|
)
|
|
})
|
|
let emittedBytes = 0
|
|
const current = await collectElectronImporters(
|
|
entries.map((file) => path.join(root, file)),
|
|
{
|
|
plugins: [
|
|
{
|
|
name: 'measure-audit-output',
|
|
setup(builder) {
|
|
builder.onEnd((result) => {
|
|
emittedBytes = result.outputFiles.reduce(
|
|
(bytes, file) => bytes + file.contents.byteLength,
|
|
0
|
|
)
|
|
})
|
|
}
|
|
}
|
|
]
|
|
}
|
|
)
|
|
expect(current.map((file) => path.basename(file)).sort()).toEqual(entries.sort())
|
|
expect(emittedBytes).toBeLessThan(128 * 1024)
|
|
})
|
|
})
|
|
|
|
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/', 'src/packages/process-host/src/']) {
|
|
expect(entries.some((file) => file.startsWith(lane))).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
// 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([])
|
|
})
|
|
})
|