| 1 | import fs from 'fs' |
| 2 | import path from 'path' |
| 3 | import { describe, expect, it } from 'vitest' |
| 4 | |
| 5 | const readTypeScriptSources = (directory: string): Array<{ filePath: string; source: string }> => { |
| 6 | const sources: Array<{ filePath: string; source: string }> = [] |
| 7 | for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { |
| 8 | const filePath = path.join(directory, entry.name) |
| 9 | if (entry.isDirectory()) { |
| 10 | sources.push(...readTypeScriptSources(filePath)) |
| 11 | continue |
| 12 | } |
| 13 | if (entry.isFile() && /\.tsx?$/.test(entry.name)) { |
| 14 | sources.push({ filePath, source: fs.readFileSync(filePath, 'utf8') }) |
| 15 | } |
| 16 | } |
| 17 | return sources |
| 18 | } |
| 19 | |
| 20 | const importPathPattern = (pathFragment: string): RegExp => |
| 21 | new RegExp(`(?:from|import)\\s*['\"][^'\"]*${pathFragment}`) |
| 22 | |
| 23 | const extractImportPaths = (source: string): string[] => |
| 24 | Array.from(source.matchAll(/(?:from|import)\s*['\"]([^'\"]+)['\"]/g)).map((match) => match[1]) |
| 25 | |
| 26 | describe('Runtime dependency boundaries', () => { |
| 27 | it('keeps Agent Runtime independent from IPC', () => { |
| 28 | for (const { filePath, source } of readTypeScriptSources('src/main/agent-runtime')) { |
| 29 | expect(source, filePath).not.toMatch(importPathPattern('/ipc/')) |
| 30 | } |
| 31 | }) |
| 32 | |
| 33 | it('keeps Agent Runtime independent from the database implementation', () => { |
| 34 | for (const { filePath, source } of readTypeScriptSources('src/main/agent-runtime')) { |
| 35 | expect(source, filePath).not.toMatch(importPathPattern('/db/')) |
| 36 | } |
| 37 | }) |
| 38 | |
| 39 | it('keeps presentation and product skills independent from Runtime and IPC', () => { |
| 40 | for (const directory of ['src/main/presentation', 'src/main/product-skills']) { |
| 41 | for (const { filePath, source } of readTypeScriptSources(directory)) { |
| 42 | expect(source, filePath).not.toMatch(importPathPattern('/agent-runtime')) |
| 43 | expect(source, filePath).not.toMatch(importPathPattern('/ipc/')) |
| 44 | } |
| 45 | } |
| 46 | }) |
| 47 | |
| 48 | it('makes IPC use only Agent Runtime public entries', () => { |
| 49 | const allowedEntries = new Set([ |
| 50 | 'agent-runtime', |
| 51 | 'agent-runtime/agent', |
| 52 | 'agent-runtime/model', |
| 53 | 'agent-runtime/prompt', |
| 54 | 'agent-runtime/provider/image', |
| 55 | 'agent-runtime/provider/vision', |
| 56 | 'agent-runtime/skills', |
| 57 | 'agent-runtime/tools' |
| 58 | ]) |
| 59 | |
| 60 | for (const { filePath, source } of readTypeScriptSources('src/main/ipc')) { |
| 61 | for (const importPath of extractImportPaths(source)) { |
| 62 | const entryStart = importPath.indexOf('agent-runtime') |
| 63 | if (entryStart === -1) continue |
| 64 | expect(allowedEntries, `${filePath}: ${importPath}`).toContain( |
| 65 | importPath.slice(entryStart) |
| 66 | ) |
| 67 | } |
| 68 | } |
| 69 | }) |
| 70 | |
| 71 | it('keeps the Thinking compatibility facade model-only', () => { |
| 72 | const source = fs.readFileSync('src/main/agent.ts', 'utf8') |
| 73 | |
| 74 | expect(source).toContain("export { resolveModel } from './agent-runtime/model/resolve'") |
| 75 | expect(source).not.toContain("from './tools'") |
| 76 | expect(source).not.toContain("from './agent-runtime/agent'") |
| 77 | expect(source).not.toContain("from './agent-runtime/prompt'") |
| 78 | expect(source).not.toContain("from './agent-runtime/skills'") |
| 79 | }) |
| 80 | |
| 81 | it('keeps ResourceLock internal to JobCoordinator', () => { |
| 82 | const source = fs.readFileSync('src/main/agent-runtime/index.ts', 'utf8') |
| 83 | |
| 84 | expect(source).not.toContain("export * from './lock/resource-lock'") |
| 85 | }) |
| 86 | |
| 87 | it('removes obsolete non-Thinking Runtime facades after their callers migrate', () => { |
| 88 | const removedPaths = [ |
| 89 | 'src/main/model-runtime.ts', |
| 90 | 'src/main/model-usage.ts', |
| 91 | 'src/main/openai-model-options.ts', |
| 92 | 'src/main/openai-responses-compat.ts', |
| 93 | 'src/main/utils/vision-model.ts', |
| 94 | 'src/main/utils/design-contract.ts', |
| 95 | 'src/main/image-generation/providers.ts', |
| 96 | 'src/main/image-generation/types.ts', |
| 97 | 'src/main/tools/index.ts', |
| 98 | 'src/main/skills/index.ts' |
| 99 | ] |
| 100 | |
| 101 | expect(removedPaths.filter((filePath) => fs.existsSync(filePath))).toEqual([]) |
| 102 | }) |
| 103 | |
| 104 | it('keeps IPC domains named by their product responsibility', () => { |
| 105 | const expectedPaths = [ |
| 106 | 'src/main/ipc/runtime/context.ts', |
| 107 | 'src/main/ipc/runtime/event-bridge.ts', |
| 108 | 'src/main/ipc/runtime/event-contract.ts', |
| 109 | 'src/main/generation/handlers.ts', |
| 110 | 'src/main/generation/agent-runner.ts', |
| 111 | 'src/main/session/template-builder.ts', |
| 112 | 'src/main/element-editor/handlers.ts' |
| 113 | ] |
| 114 | const removedDirectories = [ |
| 115 | 'src/main/ipc/engine', |
| 116 | 'src/main/ipc/editor', |
| 117 | 'src/main/ipc/config', |
| 118 | 'src/main/ipc/edit-jobs', |
| 119 | 'src/main/ipc/element-editor', |
| 120 | 'src/main/ipc/generation', |
| 121 | 'src/main/ipc/history', |
| 122 | 'src/main/ipc/html-editor', |
| 123 | 'src/main/ipc/image-generation', |
| 124 | 'src/main/ipc/io', |
| 125 | 'src/main/ipc/session', |
| 126 | 'src/main/ipc/speech', |
| 127 | 'src/main/ipc/templates' |
| 128 | ] |
| 129 | const ipcIndex = fs.readFileSync('src/main/ipc/index.ts', 'utf8') |
| 130 | |
| 131 | expect(expectedPaths.filter((filePath) => !fs.existsSync(filePath))).toEqual([]) |
| 132 | expect(removedDirectories.filter((directory) => fs.existsSync(directory))).toEqual([]) |
| 133 | expect(ipcIndex).toContain("from '../generation/handlers'") |
| 134 | expect(ipcIndex).toContain("from '../element-editor'") |
| 135 | expect(ipcIndex).toContain("from './runtime/event-bridge'") |
| 136 | }) |
| 137 | |
| 138 | it('keeps presentation, generation, and Agent-run context types out of Agent tool adapter types', () => { |
| 139 | const source = fs.readFileSync('src/main/agent-runtime/tools/types.ts', 'utf8') |
| 140 | |
| 141 | expect(source).not.toContain("from '@shared/generation'") |
| 142 | expect(source).not.toContain('interface DesignContract') |
| 143 | expect(source).not.toContain('interface OutlineItem') |
| 144 | expect(source).not.toContain("type DeckEditScope = 'page' | 'deck' | 'presentation-container'") |
| 145 | expect(source).not.toContain('export type { DeckEditScope, DesignContract, OutlineItem }') |
| 146 | expect(source).not.toContain('interface SessionDeckGenerationContext') |
| 147 | }) |
| 148 | |
| 149 | it('keeps presentation filesystem capabilities out of LangChain tool adapters', () => { |
| 150 | for (const filePath of [ |
| 151 | 'src/main/agent-runtime/tools/page-writer.ts', |
| 152 | 'src/main/agent-runtime/tools/deck-tools.ts' |
| 153 | ]) { |
| 154 | const source = fs.readFileSync(filePath, 'utf8') |
| 155 | expect(source, filePath).not.toMatch(/from ['"]fs['"]|\bfs\./) |
| 156 | expect(source, filePath).not.toContain('serializedWrite(') |
| 157 | } |
| 158 | }) |
| 159 | }) |
| 160 |