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, main } 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 } 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 and CLI 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, 4)).toEqual([ 'src/main/runtime/orca-runtime.ts', 'src/main/runtime/runtime-rpc.ts', 'src/main/orcad/main.ts', 'src/cli/index.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) })