Files
orca/config/scripts/check-process-host-imports.test.mjs
T
Neil b930606ee9 chore(wsl): require the allowlist check in any file that hand-builds a wsl.exe exec (#34615)
resolveCommand only checks what it resolves, so a file that also builds its
own wsl.exe exec no longer passes the import check on a resolveCommand call
alone. Note editor --remote wsl+<distro> launches as out of scope.
2026-10-27 16:53:48 -07:00

282 lines
11 KiB
JavaScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import {
assessProcessHostImports,
collectProcessHostSources,
main
} from './check-process-host-imports.mjs'
import { collectModuleSpecifiers } from './static-module-specifiers.mjs'
const PACKAGE_SOURCE = 'src/packages/process-host/src/'
const manifest = {
exports: { '.': './src/run-process.ts', './fork-process': './src/fork-process.ts' },
devDependencies: { vitest: '*' }
}
const roots = []
afterEach(() => {
vi.restoreAllMocks()
for (const root of roots.splice(0)) {
rmSync(root, { recursive: true, force: true })
}
})
function assess(files, baseline = [], packageManifest = manifest) {
return assessProcessHostImports(new Map(Object.entries(files)), packageManifest, baseline)
}
function fixture(files) {
const root = mkdtempSync(path.join(tmpdir(), 'orca-process-host-imports-'))
roots.push(root)
for (const [file, source] of Object.entries(files)) {
const target = path.join(root, file)
mkdirSync(path.dirname(target), { recursive: true })
writeFileSync(target, source)
}
return root
}
describe('process-host import boundary', () => {
it('reads actual imports and reexports while ignoring comments and embedded programs', () => {
expect(
collectModuleSpecifiers(
'file.ts',
`
// import { spawn } from 'node:child_process'
const script = "require('node:child_process')"
import type { ProcessSpec } from '@orca/process-host/process-spec'
export { forkProcess } from '@orca/process-host/fork-process'
type Child = import('child_process').ChildProcess
import legacy = require('legacy-package')
import('dynamic-package')
require('required-package')
require.resolve('resolved-package')
`
)
).toEqual([
'@orca/process-host/process-spec',
'@orca/process-host/fork-process',
'child_process',
'legacy-package',
'dynamic-package',
'required-package',
'resolved-package'
])
})
it('fails closed on source it cannot parse', () => {
expect(() => collectModuleSpecifiers('broken.ts', 'import {')).toThrow('Cannot parse broken.ts')
})
it('allows Node implementation imports and public consumer entry points', () => {
expect(
assess({
[`${PACKAGE_SOURCE}run-process.ts`]: "import { spawn } from 'node:child_process'",
[`${PACKAGE_SOURCE}run-process.test.ts`]: "import { it } from 'vitest'",
'src/main/consumer.ts': "import { spawnProcess } from '@orca/process-host'",
'config/scripts/consumer.mjs':
"import { forkProcess } from '@orca/process-host/fork-process'"
})
).toEqual({ direct: [], added: [], stale: [], violations: [] })
})
it.each([
['src/main/consumer.ts', '../packages/process-host'],
['src/main/consumer.ts', '../packages/process-host/src/run-process'],
['src/main/consumer.test.ts', '../packages/process-host/src/run-process'],
['config/scripts/consumer.mjs', '../../src/packages/process-host/src/run-process'],
['tests/e2e/consumer.ts', '../../src/packages/process-host/src/run-process'],
['mobile/src/consumer.ts', '../../src/packages/process-host/src/run-process'],
['src/main/consumer.ts', '@orca/process-host/src/run-process'],
['src/main/consumer.ts', '@orca/process-host/private-process-state']
])('rejects private package access in %s', (file, specifier) => {
expect(assess({ [file]: `import '${specifier}'` }).violations).toHaveLength(1)
})
it.each([
['src/main/consumer.ts', '..\\packages\\process-host'],
['src/main/consumer.ts', '..\\packages\\process-host\\src\\run-process'],
['src/main/consumer.ts', '..\\packages\\process-host\\dist\\run-process.js'],
['config/scripts/consumer.mjs', '..\\..\\src\\packages\\process-host\\src\\run-process'],
['src/main/consumer.ts', '@orca\\process-host\\src\\run-process'],
['src/main/consumer.ts', '@orca/process-host\\private-process-state']
])('rejects Windows private package access in %s through %s', (file, specifier) => {
expect(assess({ [file]: `require(${JSON.stringify(specifier)})` }).violations).toHaveLength(1)
})
it('shares fixture corpora only with test-only importers, keeping implementation imports public', () => {
const corpus = '../packages/process-host/src/__fixtures__/windows-argument-corpus'
expect(assess({ 'src/main/consumer.test.ts': `import '${corpus}'` }).violations).toEqual([])
expect(assess({ 'src/main/consumer.ts': `import '${corpus}'` }).violations).toHaveLength(1)
expect(
assess({
'src/main/consumer.test.ts':
"import '../packages/process-host/src/__fixtures__/../run-process'"
}).violations
).toHaveLength(1)
})
it.each([
['../main/runtime', 'cannot import outside'],
['../../../main/runtime', 'cannot import outside'],
['/repo/src/main/runtime.ts', 'absolute path'],
['@renderer/store', 'undeclared'],
['@/store', 'undeclared'],
['electron', 'undeclared'],
['vitest', 'undeclared'],
['undeclared-package/subpath', 'undeclared']
])('rejects package dependence on %s', (specifier, reason) => {
expect(
assess({ [`${PACKAGE_SOURCE}runner.ts`]: `import '${specifier}'` }).violations[0]
).toContain(reason)
})
it.each([
['..\\..\\..\\main\\runtime', 'cannot import outside'],
['.\\..\\..\\..\\main\\runtime', 'cannot import outside'],
['C:\\repo\\src\\main\\runtime.ts', 'absolute path'],
['\\\\server\\repo\\src\\main\\runtime.ts', 'absolute path'],
['@renderer\\store', 'undeclared']
])('rejects package dependence on Windows specifier %s', (specifier, reason) => {
expect(
assess({ [`${PACKAGE_SOURCE}runner.ts`]: `require(${JSON.stringify(specifier)})` })
.violations[0]
).toContain(reason)
})
it('permits Windows relative imports within package source and test fixture corpora', () => {
const corpus = '..\\packages\\process-host\\src\\__fixtures__\\windows-argument-corpus'
const traversal = '..\\packages\\process-host\\src\\__fixtures__\\..\\run-process'
expect(
assess({
[`${PACKAGE_SOURCE}runner.ts`]: `require(${JSON.stringify('.\\process-spec')})`,
'src/main/consumer.test.ts': `require(${JSON.stringify(corpus)})`
}).violations
).toEqual([])
expect(
assess({
'src/main/consumer.test.ts': `require(${JSON.stringify(traversal)})`
}).violations
).toHaveLength(1)
})
it('allows only declared runtime dependencies in production package source', () => {
expect(
assess(
{ [`${PACKAGE_SOURCE}runner.ts`]: "import 'declared/subpath'; import './process-spec'" },
[],
{ ...manifest, dependencies: { declared: '*' } }
).violations
).toEqual([])
})
it('reports new and removed legacy direct importers without exempting type imports', () => {
expect(
assess(
{
'src/main/legacy.ts': "import { spawn } from 'node:child_process'",
'src/relay/new.ts': "import type { ChildProcess } from 'child_process'",
'src/main/consumer.test.ts': "import { spawn } from 'node:child_process'",
'config/scripts/build.mjs': "import { spawn } from 'node:child_process'"
},
['src/main/legacy.ts', 'src/main/removed.ts']
)
).toEqual({
direct: ['src/main/legacy.ts', 'src/relay/new.ts'],
added: ['src/relay/new.ts'],
stale: ['src/main/removed.ts'],
violations: []
})
})
it('requires the distro allowlist check before any hand-built wsl.exe exec outside process-host', () => {
const exec = "import { execFile } from 'node:child_process'\n"
expect(
assess(
{
'src/main/unguarded.ts': `${exec}execFile('wsl.exe', ['-d', distro])`,
'src/main/resolved.ts': `${exec}execFile(resolveWslExecutablePath(), args)`,
'src/main/guarded.ts': `${exec}assertWslDistroAllowed('wsl.exe', args)\nexecFile('wsl.exe', args)`,
'src/main/unrelated.ts': `${exec}execFile('git', ['status'])`,
'src/main/built.ts': `${exec}execFile(program, buildWslExecArgs(distro, ['sh']))`,
'src/main/resolved-git.ts': `${exec}const r = resolveGitCommand(args, o)\nspawn(r.binary, r.args)`,
// resolveCommand checks only what it resolves, not a wsl.exe exec built beside it.
'src/main/mixed.ts': `${exec}const r = resolveCommand('git', a, cwd)\nexecFile('wsl.exe', ['-d', distro])`,
'src/main/comment-only.ts':
"import type { ChildProcess } from 'node:child_process'\n// wsl.exe pads output",
'src/main/caller.test.ts': `${exec}execFile('wsl.exe', [])`,
'config/scripts/tool.mjs': `${exec}execFile('wsl.exe', [])`
},
[
'src/main/unguarded.ts',
'src/main/resolved.ts',
'src/main/guarded.ts',
'src/main/unrelated.ts',
'src/main/built.ts',
'src/main/resolved-git.ts',
'src/main/mixed.ts',
'src/main/comment-only.ts'
]
).violations
).toEqual(
[
'src/main/built.ts',
'src/main/mixed.ts',
'src/main/resolved.ts',
'src/main/unguarded.ts'
].map((file) => `${file}: direct wsl.exe exec must call assertWslDistroAllowed`)
)
})
it('scans nested package, consumer, test, config, and mobile source without generated checkouts', () => {
const files = [
`${PACKAGE_SOURCE}nested/runner.ts`,
'src/main/consumer.ts',
'electron.vite.config.ts',
'config/build-plugins/consumer.ts',
'config/scripts/nested/build.mjs',
'tests/e2e/consumer.ts',
'mobile/src/consumer.ts',
'mobile/app/consumer.tsx',
'mobile/scripts/consumer.mts'
]
const ignored = [
'tests/e2e/.cross-version-checkouts/src/runtime.ts',
'src/packages/process-host/node_modules/dep/index.js'
]
const root = fixture(
Object.fromEntries([...files, ...ignored].map((file) => [file, 'export {}']))
)
expect([...collectProcessHostSources(root).keys()]).toEqual(files.sort())
})
it('checks the command-line path against the source tree and baseline', () => {
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
const files = {
'src/packages/process-host/package.json': JSON.stringify(manifest),
'config/process-host-direct-import-baseline.txt': 'src/main/legacy.ts\n',
[`${PACKAGE_SOURCE}runner.ts`]: "import { spawn } from 'node:child_process'",
'src/main/legacy.ts': "import { spawn } from 'node:child_process'"
}
expect(main(fixture(files))).toBe(0)
expect(log).toHaveBeenCalledWith(expect.stringContaining('1 legacy direct importers remain'))
expect(
main(fixture({ ...files, 'src/main/new.ts': "import { spawn } from 'node:child_process'" }))
).toBe(1)
expect(error).toHaveBeenCalledWith(
expect.stringContaining('src/main/new.ts: new direct child_process import')
)
})
it('fails loudly when the package implementation disappears', () => {
const root = fixture({
'src/packages/process-host/package.json': JSON.stringify(manifest),
'config/process-host-direct-import-baseline.txt': ''
})
expect(() => main(root)).toThrow('Missing process-host implementation')
})
})