返回 oh-my-ppt
import-boundaries.test.ts
根目录 / tests / unit / agent-runtime / import-boundaries.test.ts
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
160 lines TYPESCRIPT