返回 oh-my-ppt
slide-size-prompt.test.ts
根目录 / tests / unit / prompt / slide-size-prompt.test.ts
1 import { describe, expect, it } from 'vitest'
2 import fs from 'fs'
3 import path from 'path'
4 import {
5 buildCanvasConstraints,
6 buildLayoutCollisionRules,
7 buildPageSemanticStructure
8 } from '../../../src/main/agent-runtime/prompt'
9 import {
10 buildDeckAgentSystemPrompt,
11 buildSinglePageGenerationPrompt,
12 buildCanvasScenarioContentRules,
13 buildCanvasScenarioDeliveryGuard,
14 resolveCanvasScenario
15 } from '../../../src/main/agent-runtime/prompt'
16 import { buildEditAgentSystemPrompt } from '../../../src/main/agent-runtime/prompt'
17 import type { SessionDeckGenerationContext } from '../../../src/main/agent-runtime/agent'
18 import { resolveSlideSize } from '../../../src/shared/slide-size'
19 import {
20 CHART_SKILL_NAME,
21 DATA_ANIM_SKILL_NAME,
22 LAYOUT_SKILL_NAME,
23 RED_LAYOUT_SKILL_NAME,
24 SOURCE_READING_SKILL_NAME,
25 SQUARE_1_1_LAYOUT_SKILL_NAME,
26 STANDARD_4_3_LAYOUT_SKILL_NAME,
27 VERTICAL_3_4_LAYOUT_SKILL_NAME,
28 VERTICAL_9_16_LAYOUT_SKILL_NAME,
29 getRequiredProductSkillNamesForSlideSize,
30 resolveLayoutSkillName
31 } from '../../../src/main/product-skills/contract'
32
33 const readSource = (relativePath: string): string =>
34 fs.readFileSync(path.join(process.cwd(), relativePath), 'utf-8')
35
36 const makeContext = (
37 slideSize = resolveSlideSize({ id: 'wide-16-9' })
38 ): SessionDeckGenerationContext => ({
39 sessionId: 'session-1',
40 projectDir: '/tmp/project',
41 indexPath: '/tmp/project/index.html',
42 pageFileMap: { 'page-1': '/tmp/project/page-1.html' },
43 topic: '测试主题',
44 deckTitle: '测试标题',
45 styleId: 'test-style',
46 styleSkillPrompt: 'Use a clean style.',
47 userMessage: '生成内容',
48 outlineTitles: ['第一页'],
49 outlineItems: [{ title: '第一页', contentOutline: '说明重点' }],
50 slideSize,
51 appLocale: 'zh'
52 })
53
54 describe('slide size prompt', () => {
55 it('uses the selected canvas dimensions', () => {
56 const prompt = buildCanvasConstraints(resolveSlideSize({ id: 'standard-4-3' }))
57 expect(prompt).toContain('1600×1200')
58 expect(prompt).toContain('4:3')
59 expect(prompt).not.toContain('1600×900')
60 expect(prompt).toContain(STANDARD_4_3_LAYOUT_SKILL_NAME)
61 expect(prompt).not.toContain('oh-my-ppt-adaptive-layout')
62 expect(prompt).not.toContain(`skill ${LAYOUT_SKILL_NAME}`)
63 })
64
65 it('adds portrait layout guidance', () => {
66 const prompt = buildCanvasConstraints(resolveSlideSize({ id: 'vertical-9-16' }))
67 expect(prompt).toContain('900×1600')
68 expect(prompt).toContain('竖版画布')
69 expect(prompt).toContain('不要照搬横向三列')
70 expect(prompt).toContain(VERTICAL_9_16_LAYOUT_SKILL_NAME)
71 })
72
73 it('injects the PPT layout skill only for 16:9', () => {
74 const slideSize = resolveSlideSize({ id: 'wide-16-9' })
75 expect(resolveLayoutSkillName(slideSize)).toBe(LAYOUT_SKILL_NAME)
76 expect(buildCanvasConstraints(slideSize)).toContain(`skill ${LAYOUT_SKILL_NAME}`)
77 expect(buildPageSemanticStructure(slideSize)).toContain(`skill ${LAYOUT_SKILL_NAME}`)
78 expect(buildLayoutCollisionRules(slideSize)).toContain(`skill ${LAYOUT_SKILL_NAME}`)
79 expect(getRequiredProductSkillNamesForSlideSize(slideSize)).toEqual([
80 LAYOUT_SKILL_NAME,
81 DATA_ANIM_SKILL_NAME,
82 CHART_SKILL_NAME,
83 SOURCE_READING_SKILL_NAME
84 ])
85 })
86
87 it('injects red-layout-skill for Xiaohongshu', () => {
88 const slideSize = resolveSlideSize({ id: 'xiaohongshu-note' })
89 expect(resolveLayoutSkillName(slideSize)).toBe(RED_LAYOUT_SKILL_NAME)
90 expect(buildCanvasConstraints(slideSize)).toContain(`skill ${RED_LAYOUT_SKILL_NAME}`)
91 expect(buildPageSemanticStructure(slideSize)).toContain(`skill ${RED_LAYOUT_SKILL_NAME}`)
92 expect(buildLayoutCollisionRules(slideSize)).toContain(`skill ${RED_LAYOUT_SKILL_NAME}`)
93 expect(getRequiredProductSkillNamesForSlideSize(slideSize)).toEqual([
94 RED_LAYOUT_SKILL_NAME,
95 DATA_ANIM_SKILL_NAME,
96 CHART_SKILL_NAME,
97 SOURCE_READING_SKILL_NAME
98 ])
99 })
100
101 it('injects a dedicated layout skill for every non-16:9 preset', () => {
102 const cases = [
103 ['vertical-9-16', VERTICAL_9_16_LAYOUT_SKILL_NAME],
104 ['standard-4-3', STANDARD_4_3_LAYOUT_SKILL_NAME],
105 ['square-1-1', SQUARE_1_1_LAYOUT_SKILL_NAME],
106 ['vertical-3-4', VERTICAL_3_4_LAYOUT_SKILL_NAME]
107 ] as const
108 for (const [id, skillName] of cases) {
109 const slideSize = resolveSlideSize({ id })
110 expect(resolveLayoutSkillName(slideSize)).toBe(skillName)
111 expect(buildCanvasConstraints(slideSize)).toContain(`skill ${skillName}`)
112 expect(buildPageSemanticStructure(slideSize)).toContain(`skill ${skillName}`)
113 expect(buildLayoutCollisionRules(slideSize)).toContain(`skill ${skillName}`)
114 expect(getRequiredProductSkillNamesForSlideSize(slideSize)[0]).toBe(skillName)
115 expect(buildCanvasConstraints(slideSize)).not.toContain('oh-my-ppt-adaptive-layout')
116 }
117 })
118
119 it('uses PPT scenario wording only for presentation canvases', () => {
120 const wide = resolveSlideSize({ id: 'wide-16-9' })
121 const prompt = [
122 buildCanvasScenarioContentRules(wide),
123 buildCanvasScenarioDeliveryGuard(wide)
124 ].join('\n')
125
126 expect(resolveCanvasScenario(wide).id).toBe('presentation-wide')
127 expect(prompt).toContain('PPT 是演讲辅助')
128 expect(prompt).toContain('投影')
129 expect(prompt).toContain('主图表页')
130 })
131
132 it('maps every supported size to an explicit scenario with no adaptive fallback', () => {
133 const cases = [
134 ['wide-16-9', 'presentation-wide'],
135 ['standard-4-3', 'presentation-standard'],
136 ['square-1-1', 'square-card'],
137 ['vertical-9-16', 'mobile-story'],
138 ['vertical-3-4', 'poster-card'],
139 ['xiaohongshu-note', 'social-note']
140 ] as const
141 for (const [id, scenarioId] of cases) {
142 expect(resolveCanvasScenario(resolveSlideSize({ id })).id).toBe(scenarioId)
143 }
144
145 const source = readSource('src/main/agent-runtime/prompt/composers/canvas-scenario.ts')
146 expect(source).not.toContain('adaptive-canvas')
147 expect(source).not.toContain('fallback')
148 expect(source).toContain('No canvas scenario prompt configured')
149 })
150
151 it('uses mobile-story wording for 9:16 without PPT presentation delivery checks', () => {
152 const vertical = resolveSlideSize({ id: 'vertical-9-16' })
153 const prompt = buildDeckAgentSystemPrompt('test-style', makeContext(vertical))
154
155 expect(resolveCanvasScenario(vertical).id).toBe('mobile-story')
156 expect(prompt).toContain('移动端竖屏')
157 expect(prompt).toContain('首屏')
158 expect(prompt).toContain('上下阅读路径')
159 expect(prompt).not.toContain('PPT 是演讲辅助')
160 expect(prompt).not.toContain('投影/大屏')
161 expect(prompt).not.toContain('主图表页')
162 })
163
164 it('uses square-card wording for 1:1 across deck, single-page, and edit prompts', () => {
165 const square = resolveSlideSize({ id: 'square-1-1' })
166 const deckPrompt = buildDeckAgentSystemPrompt('test-style', makeContext(square))
167 const pagePrompt = buildSinglePageGenerationPrompt({
168 topic: '测试主题',
169 deckTitle: '测试标题',
170 pageId: 'page-1',
171 pageNumber: 1,
172 pageTitle: '第一页',
173 pageOutline: '说明重点',
174 slideSize: square
175 })
176 const editPrompt = buildEditAgentSystemPrompt('test-style', {
177 ...makeContext(square),
178 mode: 'edit',
179 editScope: 'page',
180 selectedPageId: 'page-1'
181 })
182 const combined = [deckPrompt, pagePrompt, editPrompt].join('\n')
183
184 expect(square).toMatchObject({ id: 'square-1-1', width: 1200, height: 1200 })
185 expect(resolveCanvasScenario(square).id).toBe('square-card')
186 expect(resolveLayoutSkillName(square)).toBe(SQUARE_1_1_LAYOUT_SKILL_NAME)
187 expect(combined).toContain('1:1 方形内容卡')
188 expect(combined).toContain('中心焦点')
189 expect(combined).toContain('四边平衡')
190 expect(combined).toContain(SQUARE_1_1_LAYOUT_SKILL_NAME)
191 expect(combined).not.toContain('PPT 是演讲辅助')
192 expect(combined).not.toContain('投影/大屏')
193 expect(combined).not.toContain('主图表页')
194 })
195
196 it('uses social-note wording for Xiaohongshu across deck, single-page, and edit prompts', () => {
197 const xhs = resolveSlideSize({ id: 'xiaohongshu-note' })
198 const deckPrompt = buildDeckAgentSystemPrompt('test-style', makeContext(xhs))
199 const pagePrompt = buildSinglePageGenerationPrompt({
200 topic: '测试主题',
201 deckTitle: '测试标题',
202 pageId: 'page-1',
203 pageNumber: 1,
204 pageTitle: '第一页',
205 pageOutline: '说明重点',
206 slideSize: xhs
207 })
208 const editPrompt = buildEditAgentSystemPrompt('test-style', {
209 ...makeContext(xhs),
210 mode: 'edit',
211 editScope: 'page',
212 selectedPageId: 'page-1'
213 })
214 const combined = [deckPrompt, pagePrompt, editPrompt].join('\n')
215
216 expect(resolveCanvasScenario(xhs).id).toBe('social-note')
217 expect(combined).toContain('小红书图文笔记')
218 expect(combined).toContain('标题钩子')
219 expect(combined).toContain('收藏价值')
220 expect(combined).not.toContain('PPT 是演讲辅助')
221 expect(combined).not.toContain('投影/大屏')
222 expect(combined).not.toContain('主图表页')
223 })
224 })
225
225 lines TYPESCRIPT