mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
259 lines
10 KiB
JavaScript
259 lines
10 KiB
JavaScript
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
buildUnitDependencyGraph,
|
|
collectUnitDependencyGraph
|
|
} from './ci-unit-dependency-graph.mjs'
|
|
import { selectUnitFiles } from './ci-unit-selection.mjs'
|
|
|
|
const temporary = []
|
|
afterEach(() => {
|
|
for (const root of temporary.splice(0)) {
|
|
rmSync(root, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
function fixture(extra = {}) {
|
|
const root = mkdtempSync(join(tmpdir(), 'unit-workspace-graph-'))
|
|
temporary.push(root)
|
|
const manifest = (name, source) =>
|
|
JSON.stringify({
|
|
name,
|
|
exports: {
|
|
'.': { 'orca-source': source, types: './dist/index.d.ts', default: './dist/index.js' }
|
|
}
|
|
})
|
|
const files = {
|
|
'pnpm-workspace.yaml': 'packages:\n - src/packages/*\n',
|
|
'src/packages/process-host/package.json': manifest(
|
|
'@orca/process-host',
|
|
'./src/run-process.ts'
|
|
),
|
|
'src/packages/process-host/src/run-process.ts':
|
|
"import { value } from '@orca/byte-buffer'; export { value }; throw Error('source must not execute')",
|
|
'src/packages/process-host/dist/index.js': 'throw Error("stale output")',
|
|
'src/packages/byte-buffer/package.json': manifest('@orca/byte-buffer', './src/byte-buffer.ts'),
|
|
'src/packages/byte-buffer/src/byte-buffer.ts': 'export const value = 1',
|
|
'src/consumer.test.ts': "import '@orca/process-host'",
|
|
'src/dynamic.test.ts': "import('@orca/process-host')",
|
|
'src/require.test.ts': "require('@orca/process-host')",
|
|
'src/type.test.ts': "import type { value } from '@orca/process-host'",
|
|
'src/unrelated.test.ts': 'export const unrelated = true',
|
|
...extra
|
|
}
|
|
for (const [file, source] of Object.entries(files)) {
|
|
const path = join(root, file)
|
|
mkdirSync(join(path, '..'), { recursive: true })
|
|
writeFileSync(path, source)
|
|
}
|
|
return root
|
|
}
|
|
|
|
it('tracks transitive public workspace source consumers including type and dynamic imports', () => {
|
|
const graph = collectUnitDependencyGraph(fixture())
|
|
const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort()
|
|
expect(
|
|
selectUnitFiles(tests, ['src/packages/byte-buffer/src/byte-buffer.ts'], graph)
|
|
).toMatchObject({
|
|
full: false,
|
|
files: [
|
|
'src/consumer.test.ts',
|
|
'src/dynamic.test.ts',
|
|
'src/require.test.ts',
|
|
'src/type.test.ts'
|
|
]
|
|
})
|
|
expect(graph.files.has('src/packages/process-host/dist/index.js')).toBe(false)
|
|
})
|
|
|
|
it('tracks package manifests as inputs to all public consumers', () => {
|
|
const graph = collectUnitDependencyGraph(fixture())
|
|
const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort()
|
|
for (const manifest of [
|
|
'src/packages/process-host/package.json',
|
|
'src/packages/byte-buffer/package.json'
|
|
]) {
|
|
const selection = selectUnitFiles(tests, [manifest], graph)
|
|
expect(selection.full).toBe(false)
|
|
expect(selection.files).toEqual(tests.filter((file) => file !== 'src/unrelated.test.ts'))
|
|
}
|
|
})
|
|
|
|
it.each(['@orca/unknown', '@orca/process-host/private'])(
|
|
'does not silently drop unresolved internal imports: %s',
|
|
(specifier) => {
|
|
expect(() =>
|
|
collectUnitDependencyGraph(fixture({ 'src/consumer.test.ts': `import '${specifier}'` }))
|
|
).toThrow('No public orca-source export')
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
[
|
|
'an assigned native loader',
|
|
'src/main/windows-native-registry.ts',
|
|
"import { createRequire } from 'node:module'; const requireFromMain = createRequire(__filename); export const loadWindowsNativeRegistry = () => requireFromMain('@orca/windows-registry')",
|
|
'src/main/windows-registry-addon.test.ts',
|
|
"import { loadWindowsNativeRegistry } from './windows-native-registry'"
|
|
],
|
|
[
|
|
'a direct loader with a runtime serializer path',
|
|
'src/main/daemon/serialize-grid-roundtrip.ts',
|
|
"import { createRequire } from 'node:module'; export const loadOldSerializer = (path) => createRequire(import.meta.url)(path)",
|
|
'src/main/daemon/serialize-grid.differential.fuzz.test.ts',
|
|
"import { loadOldSerializer } from './serialize-grid-roundtrip'"
|
|
],
|
|
[
|
|
'a module namespace whose computed loader escapes through an object',
|
|
'src/main/stored-native-loader.ts',
|
|
"import * as hostModule from 'node:module'; const native = hostModule; const load = native['create' + 'Require'](__filename); const api = { load }; export const open = (path) => api.load(path)",
|
|
'src/main/stored-native-loader.test.ts',
|
|
"import { open } from './stored-native-loader'"
|
|
],
|
|
[
|
|
'an optional builtin loader',
|
|
'src/main/sqlite/sync-database.ts',
|
|
"export const isSqliteAvailable = () => process.getBuiltinModule?.('node:sqlite')",
|
|
'src/main/sqlite/sync-database-portability.test.ts',
|
|
"import { isSqliteAvailable } from './sync-database'"
|
|
],
|
|
[
|
|
'an assigned builtin loader',
|
|
'src/main/sqlite/assigned-reader.ts',
|
|
"const getBuiltin = process.getBuiltinModule; export const sqlite = () => getBuiltin('node:sqlite')",
|
|
'src/main/sqlite/assigned-reader.test.ts',
|
|
"import { sqlite } from './assigned-reader'"
|
|
]
|
|
])(
|
|
'keeps real test consumers of %s when another covered source changes',
|
|
(_label, owner, source, consumer, testSource) => {
|
|
const graph = collectUnitDependencyGraph(
|
|
fixture({
|
|
[owner]: source,
|
|
[consumer]: testSource,
|
|
'src/selected-input.ts': 'export const value = 1',
|
|
'src/selected-input.test.ts': "import { value } from './selected-input'"
|
|
})
|
|
)
|
|
const tests = [...graph.files].filter((file) => file.endsWith('.test.ts')).sort()
|
|
expect(selectUnitFiles(tests, ['src/selected-input.ts'], graph)).toMatchObject({
|
|
full: false,
|
|
files: [consumer, 'src/selected-input.test.ts'].sort()
|
|
})
|
|
}
|
|
)
|
|
|
|
describe('unit graph import resolution', () => {
|
|
it('retains every existing candidate across literal paths, extensions and index files', () => {
|
|
const sources = new Map([
|
|
['src/leaf', ''],
|
|
['src/leaf.ts', ''],
|
|
['src/leaf.tsx', ''],
|
|
['src/component.tsx', ''],
|
|
['src/component.js', ''],
|
|
['src/folder/index.ts', ''],
|
|
['src/folder/index.tsx', ''],
|
|
['src/config.json', '{}'],
|
|
['src/renderer/src/view.tsx', ''],
|
|
['src/use.ts', "import './leaf'; import './component'; import './folder'; import './config'"],
|
|
['src/aliases.ts', "import '@renderer/view'; import '@/view'; import 'external-package'"],
|
|
['src/missing.ts', "import './missing-file'"],
|
|
['src/dynamic.ts', 'import(variablePath)'],
|
|
['config/owner.mjs', "import '../src/leaf'"],
|
|
['tests/owner.ts', "import '../src/leaf'"]
|
|
])
|
|
expect(buildUnitDependencyGraph(sources)).toMatchObject({
|
|
reverse: new Map([
|
|
['src/leaf', new Set(['src/use.ts', 'config/owner.mjs', 'tests/owner.ts'])],
|
|
['src/leaf.ts', new Set(['src/use.ts', 'config/owner.mjs', 'tests/owner.ts'])],
|
|
['src/leaf.tsx', new Set(['src/use.ts', 'config/owner.mjs', 'tests/owner.ts'])],
|
|
['src/component.tsx', new Set(['src/use.ts'])],
|
|
['src/component.js', new Set(['src/use.ts'])],
|
|
['src/folder/index.ts', new Set(['src/use.ts'])],
|
|
['src/folder/index.tsx', new Set(['src/use.ts'])],
|
|
['src/config.json', new Set(['src/use.ts'])],
|
|
['src/renderer/src/view.tsx', new Set(['src/aliases.ts'])]
|
|
]),
|
|
opaque: new Set(['src/missing.ts', 'src/dynamic.ts', 'config/owner.mjs', 'tests/owner.ts'])
|
|
})
|
|
})
|
|
|
|
it('keeps extensionless consumers when a covered JavaScript sibling changes', () => {
|
|
const graph = collectUnitDependencyGraph(
|
|
fixture({
|
|
'src/value.ts': 'export const value = 1',
|
|
'src/value.js': 'export const value = 2',
|
|
'src/extensionless.test.ts': "import { value } from './value'",
|
|
'src/explicit.test.ts': "import { value } from './value.js'"
|
|
})
|
|
)
|
|
const tests = ['src/extensionless.test.ts', 'src/explicit.test.ts', 'src/unrelated.test.ts']
|
|
expect(selectUnitFiles(tests, ['src/value.js'], graph)).toMatchObject({
|
|
full: false,
|
|
files: ['src/extensionless.test.ts', 'src/explicit.test.ts']
|
|
})
|
|
})
|
|
|
|
it.each(['jsx', 'mts', 'cts', 'mjs', 'cjs'])(
|
|
'follows transitive imports through a competing .%s source',
|
|
(extension) => {
|
|
const graph = collectUnitDependencyGraph(
|
|
fixture({
|
|
'src/value.ts': 'export const value = 1',
|
|
[`src/value.${extension}`]: "export { value } from './selected-input'",
|
|
'src/selected-input.ts': 'export const value = 2',
|
|
'src/extensionless.test.ts': "import { value } from './value'",
|
|
'src/explicit.test.ts': "import { value } from './selected-input'"
|
|
})
|
|
)
|
|
const tests = ['src/extensionless.test.ts', 'src/explicit.test.ts', 'src/unrelated.test.ts']
|
|
expect(selectUnitFiles(tests, ['src/selected-input.ts'], graph)).toMatchObject({
|
|
full: false,
|
|
files: ['src/extensionless.test.ts', 'src/explicit.test.ts']
|
|
})
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
['js', 'ts'],
|
|
['js', 'tsx'],
|
|
['jsx', 'tsx'],
|
|
['mjs', 'mts'],
|
|
['cjs', 'cts']
|
|
])('retains TypeScript substitutions for .%s imports through .%s', (output, source) => {
|
|
const graph = collectUnitDependencyGraph(
|
|
fixture({
|
|
[`src/value.${source}`]: 'export const value = 1',
|
|
[`src/value.${output}.ts`]: 'export const value = 2',
|
|
'src/substitution.test.ts': `import { value } from './value.${output}'`,
|
|
'src/explicit.test.ts': `import { value } from './value.${source}'`
|
|
})
|
|
)
|
|
const tests = ['src/substitution.test.ts', 'src/explicit.test.ts', 'src/unrelated.test.ts']
|
|
expect(selectUnitFiles(tests, [`src/value.${source}`], graph)).toMatchObject({
|
|
full: false,
|
|
files: ['src/substitution.test.ts', 'src/explicit.test.ts']
|
|
})
|
|
})
|
|
|
|
it('retains directory consumers whose manifest can override an index file', () => {
|
|
const graph = collectUnitDependencyGraph(
|
|
fixture({
|
|
'src/folder/package.json': JSON.stringify({ main: './entry.js' }),
|
|
'src/folder/index.ts': 'export const value = 1',
|
|
'src/folder/entry.js': "export { value } from '../selected-input'",
|
|
'src/selected-input.ts': 'export const value = 2',
|
|
'src/directory.test.ts': "import { value } from './folder'",
|
|
'src/explicit.test.ts': "import { value } from './selected-input'"
|
|
})
|
|
)
|
|
const tests = ['src/directory.test.ts', 'src/explicit.test.ts', 'src/unrelated.test.ts']
|
|
expect(selectUnitFiles(tests, ['src/selected-input.ts'], graph)).toMatchObject({
|
|
full: false,
|
|
files: ['src/directory.test.ts', 'src/explicit.test.ts']
|
|
})
|
|
})
|
|
})
|