返回 oh-my-ppt
thinking-tools.test.ts
根目录 / tests / unit / thinking / thinking-tools.test.ts
1 import fs from 'fs'
2 import os from 'os'
3 import path from 'path'
4 import { afterEach, describe, expect, it } from 'vitest'
5 import { createThinkingWorkflowTools } from '../../../src/main/thinking/thinking-tools'
6
7 const tempDirs: string[] = []
8
9 const makeTempThinkingDir = async (): Promise<string> => {
10 const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'thinking-tools-'))
11 tempDirs.push(dir)
12 await fs.promises.writeFile(
13 path.join(dir, 'thinking.md'),
14 [
15 '# Thinking Brief',
16 '',
17 '## Topic',
18 '',
19 '## Page Count',
20 '0',
21 ''
22 ].join('\n'),
23 'utf-8'
24 )
25 return dir
26 }
27
28 const page = (title: string): {
29 title: string
30 role: 'content'
31 objective: string
32 summary: string
33 keyPoints: string[]
34 } => ({
35 title,
36 role: 'content',
37 objective: `说明 ${title}`,
38 summary: `${title} 的内容摘要。`,
39 keyPoints: [`${title} 重点一`, `${title} 重点二`]
40 })
41
42 afterEach(async () => {
43 await Promise.all(
44 tempDirs.splice(0).map((dir) => fs.promises.rm(dir, { recursive: true, force: true }))
45 )
46 })
47
48 describe('thinking workflow tools', () => {
49 it('normalizes object-form confirmed decisions and preserves omitted context fields', async () => {
50 const thinkingDir = await makeTempThinkingDir()
51 const { tools, state } = createThinkingWorkflowTools({
52 thinkingDir,
53 currentStage: 'collect'
54 })
55 const updateContext = tools.find((tool) => tool.name === 'update_context_document')
56 expect(updateContext).toBeTruthy()
57
58 await updateContext!.invoke({
59 topic: '2026 AI动漫发展',
60 userIntent: '面向行业从业者进行案例分享。',
61 confirmedDecisions: [
62 {
63 主题: '2026年AI动漫发展',
64 听众: '行业从业者',
65 页数: '8页'
66 }
67 ],
68 openQuestions: ['是否需要补充更多海外案例?']
69 })
70 await updateContext!.invoke({
71 latestDirection: '用户确认开始生成大纲。'
72 })
73
74 const context = await fs.promises.readFile(path.join(thinkingDir, 'context.md'), 'utf-8')
75
76 expect(state.contextUpdated).toBe(true)
77 expect(state.contextUpdateCount).toBe(2)
78 expect(context).toContain('## Topic\n2026 AI动漫发展')
79 expect(context).toContain(
80 '- 主题: 2026年AI动漫发展;听众: 行业从业者;页数: 8页'
81 )
82 expect(context).toContain('- 是否需要补充更多海外案例?')
83 expect(context).toContain('## Latest Direction\n用户确认开始生成大纲。')
84 })
85
86 it('clears a context list only when the model explicitly passes an empty array', async () => {
87 const thinkingDir = await makeTempThinkingDir()
88 const { tools } = createThinkingWorkflowTools({
89 thinkingDir,
90 currentStage: 'collect'
91 })
92 const updateContext = tools.find((tool) => tool.name === 'update_context_document')
93 expect(updateContext).toBeTruthy()
94
95 await updateContext!.invoke({
96 topic: '保留主题',
97 openQuestions: ['待确认问题']
98 })
99 await updateContext!.invoke({
100 openQuestions: []
101 })
102
103 const context = await fs.promises.readFile(path.join(thinkingDir, 'context.md'), 'utf-8')
104 expect(context).toContain('## Topic\n保留主题')
105 expect(context).not.toContain('## Open Questions')
106 expect(context).not.toContain('待确认问题')
107 })
108
109 it('stages page batches in memory and writes thinking.md only on final commit', async () => {
110 const thinkingDir = await makeTempThinkingDir()
111 const initial = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
112 const { tools, state } = createThinkingWorkflowTools({
113 thinkingDir,
114 currentStage: 'collect'
115 })
116 const updateThinking = tools.find((tool) => tool.name === 'update_thinking_document')
117 expect(updateThinking).toBeTruthy()
118
119 await updateThinking!.invoke({
120 topic: '分批大纲',
121 pageCount: 4,
122 pageStart: 1,
123 pages: [page('第一页'), page('第二页')]
124 })
125 expect(state.thinkingStaged).toBe(true)
126 expect(state.thinkingUpdated).toBe(false)
127 expect(await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')).toBe(initial)
128
129 await updateThinking!.invoke({
130 pageStart: 3,
131 pages: [page('第三页'), page('第四页')],
132 commit: true
133 })
134 const committed = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
135
136 expect(state.thinkingStaged).toBe(false)
137 expect(state.thinkingUpdated).toBe(true)
138 expect(state.thinkingUpdateCount).toBe(1)
139 expect(committed).toContain('## Topic\n分批大纲')
140 expect(committed).toContain('## Page Count\n4')
141 expect(committed).toContain('## Page 1: 第一页')
142 expect(committed).toContain('## Page 2: 第二页')
143 expect(committed).toContain('## Page 3: 第三页')
144 expect(committed).toContain('## Page 4: 第四页')
145 })
146
147 it('keeps immediate full-page replacement behavior for small outlines', async () => {
148 const thinkingDir = await makeTempThinkingDir()
149 const { tools, state } = createThinkingWorkflowTools({
150 thinkingDir,
151 currentStage: 'collect'
152 })
153 const updateThinking = tools.find((tool) => tool.name === 'update_thinking_document')
154 expect(updateThinking).toBeTruthy()
155
156 await updateThinking!.invoke({
157 topic: '小大纲',
158 pages: [page('封面'), page('结论')]
159 })
160 const committed = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
161
162 expect(state.thinkingStaged).toBe(false)
163 expect(state.thinkingUpdated).toBe(true)
164 expect(committed).toContain('## Page Count\n2')
165 expect(committed).toContain('## Page 1: 封面')
166 expect(committed).toContain('## Page 2: 结论')
167 })
168
169 it('auto-commits a complete staged document when the model forgets commit', async () => {
170 const thinkingDir = await makeTempThinkingDir()
171 const { tools, state, finalizeStagedThinkingDocument } = createThinkingWorkflowTools({
172 thinkingDir,
173 currentStage: 'collect'
174 })
175 const updateThinking = tools.find((tool) => tool.name === 'update_thinking_document')
176 expect(updateThinking).toBeTruthy()
177
178 await updateThinking!.invoke({
179 topic: '忘记提交',
180 pageCount: 2,
181 pageStart: 1,
182 pages: [page('第一页'), page('第二页')]
183 })
184 const result = await finalizeStagedThinkingDocument()
185 const committed = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
186
187 expect(result).toMatchObject({ status: 'committed', pageCount: 2 })
188 expect(state.thinkingStaged).toBe(false)
189 expect(state.thinkingUpdated).toBe(true)
190 expect(committed).toContain('## Topic\n忘记提交')
191 expect(committed).toContain('## Page 1: 第一页')
192 expect(committed).toContain('## Page 2: 第二页')
193 })
194
195 it('keeps incomplete staged batches recoverable when commit is called too early', async () => {
196 const thinkingDir = await makeTempThinkingDir()
197 const initial = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
198 const { tools, state } = createThinkingWorkflowTools({
199 thinkingDir,
200 currentStage: 'collect'
201 })
202 const updateThinking = tools.find((tool) => tool.name === 'update_thinking_document')
203 expect(updateThinking).toBeTruthy()
204
205 const earlyResult = await updateThinking!.invoke({
206 topic: '提前提交',
207 pageCount: 3,
208 pageStart: 1,
209 pages: [page('第一页')],
210 commit: true
211 })
212 const afterEarlyCommit = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
213
214 expect(String(earlyResult)).toContain('thinking.md is still staged')
215 expect(String(earlyResult)).toContain('missing page 2')
216 expect(state.thinkingStaged).toBe(true)
217 expect(state.thinkingUpdated).toBe(false)
218 expect(afterEarlyCommit).toBe(initial)
219
220 const finalResult = await updateThinking!.invoke({
221 pageStart: 2,
222 pages: [page('第二页'), page('第三页')],
223 commit: true
224 })
225 const committed = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
226
227 expect(String(finalResult)).toContain('thinking.md updated from staged batches')
228 expect(state.thinkingStaged).toBe(false)
229 expect(state.thinkingUpdated).toBe(true)
230 expect(committed).toContain('## Page 1: 第一页')
231 expect(committed).toContain('## Page 2: 第二页')
232 expect(committed).toContain('## Page 3: 第三页')
233 })
234
235 it('discards an incomplete staged document instead of writing a partial thinking.md', async () => {
236 const thinkingDir = await makeTempThinkingDir()
237 const initial = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
238 const { tools, state, finalizeStagedThinkingDocument } = createThinkingWorkflowTools({
239 thinkingDir,
240 currentStage: 'collect'
241 })
242 const updateThinking = tools.find((tool) => tool.name === 'update_thinking_document')
243 expect(updateThinking).toBeTruthy()
244
245 await updateThinking!.invoke({
246 topic: '半截大纲',
247 pageCount: 3,
248 pageStart: 1,
249 pages: [page('第一页'), page('第二页')]
250 })
251 const result = await finalizeStagedThinkingDocument()
252 const after = await fs.promises.readFile(path.join(thinkingDir, 'thinking.md'), 'utf-8')
253
254 expect(result).toMatchObject({
255 status: 'discarded',
256 reason: 'missing page 3',
257 pageCount: 2,
258 expectedPageCount: 3
259 })
260 expect(state.thinkingStaged).toBe(false)
261 expect(state.thinkingUpdated).toBe(false)
262 expect(after).toBe(initial)
263 })
264 })
265
265 lines TYPESCRIPT