| 1 | import { describe, expect, it } from 'vitest' |
| 2 | import fs from 'fs' |
| 3 | import path from 'path' |
| 4 | |
| 5 | const readSource = (relativePath: string): string => |
| 6 | fs.readFileSync(path.join(process.cwd(), relativePath), 'utf-8') |
| 7 | |
| 8 | describe('source-grounded prompt rules', () => { |
| 9 | it('parse plan uses single-shot model and outline scan', () => { |
| 10 | const source = readSource('src/main/io/document-parse-handlers.ts') |
| 11 | const outlineScan = readSource('src/main/io/document-outline-scan.ts') |
| 12 | |
| 13 | expect(source).toContain('single-shot document parsing task') |
| 14 | expect(source).toContain('You have no filesystem tools in this call') |
| 15 | expect(source).toContain('bounded source preview') |
| 16 | expect(source).toContain('MAX_PARSE_SOURCE_PREVIEW_CHARS') |
| 17 | expect(source).not.toContain('attachProductSkillsBackend') |
| 18 | expect(source).not.toContain('createDeepAgent') |
| 19 | expect(source).not.toContain('product_skill_read_file') |
| 20 | expect(source).toContain('Do not ask to read the file') |
| 21 | expect(source).toContain('Do not write detailed facts') |
| 22 | expect(source).toContain('hasOutlinePageCandidateSkeleton') |
| 23 | expect(source).not.toContain('rawPageCountInput') |
| 24 | expect(source).not.toContain('requestedPageCount') |
| 25 | expect(source).not.toContain('userPageCount') |
| 26 | expect(source).not.toContain('User-provided page count') |
| 27 | expect(source).toContain('runSingleShotDocumentPlanModel') |
| 28 | expect(source).toContain('single-shot model invoke') |
| 29 | expect(source).toContain('sourcePreviewLength') |
| 30 | expect(source).toContain('[documents:parsePlan] end') |
| 31 | expect(source).toContain('durationMs') |
| 32 | expect(source).toContain('csv converted for reading') |
| 33 | expect(source).toContain('normalized candidate plan') |
| 34 | expect(source).toContain('document outline page-count estimate') |
| 35 | expect(source).toContain('deterministic source-structure page-count estimate') |
| 36 | expect(source).toContain('outline quality check failed after retry, rejecting plan') |
| 37 | expect(source).toContain('isDocumentOutlineQualityError') |
| 38 | expect(source).toContain('Source document path for later generation only') |
| 39 | expect(outlineScan).toContain('Document structure scan') |
| 40 | expect(outlineScan).toContain('Heading map truncated') |
| 41 | expect(outlineScan).toContain('Deterministic slide-count estimate') |
| 42 | expect(outlineScan).toContain('deriveOutlinePageCandidates') |
| 43 | expect(outlineScan).toContain('Page candidate skeleton') |
| 44 | expect(source).toContain('page candidate skeleton') |
| 45 | expect(source).toContain('skeleton count') |
| 46 | expect(source).toContain('compact page skeleton') |
| 47 | expect(source).toContain('source line range') |
| 48 | expect(source).toContain('chapter divider slides') |
| 49 | expect(source).toContain('页面角色:章节页') |
| 50 | expect(source).toContain('assertPlanMatchesDocumentOutline') |
| 51 | }) |
| 52 | |
| 53 | it('frontend lets document parse infer pageCount from source structure', () => { |
| 54 | const sessionCreate = readSource('src/renderer/src/pages/session-create.tsx') |
| 55 | const templateUseDialog = readSource( |
| 56 | 'src/renderer/src/components/templates/TemplateUseDialog.tsx' |
| 57 | ) |
| 58 | const sessionParseCall = sessionCreate.slice( |
| 59 | sessionCreate.indexOf('const result = await ipc.parseDocumentPlan({'), |
| 60 | sessionCreate.indexOf('const nextSuggestion = {') |
| 61 | ) |
| 62 | const templateParseCall = templateUseDialog.slice( |
| 63 | templateUseDialog.indexOf('const result = await ipc.parseDocumentPlan({'), |
| 64 | templateUseDialog.indexOf('const referenceFile = result.files[0] || attachedReferenceFile') |
| 65 | ) |
| 66 | |
| 67 | expect(sessionParseCall).toContain('ipc.parseDocumentPlan') |
| 68 | expect(templateParseCall).toContain('ipc.parseDocumentPlan') |
| 69 | expect(sessionParseCall).not.toContain('pageCount:') |
| 70 | expect(sessionParseCall).not.toContain('resolvePageCount') |
| 71 | expect(templateParseCall).not.toContain('pageCount:') |
| 72 | expect(templateParseCall).not.toContain('resolvePageCount') |
| 73 | }) |
| 74 | |
| 75 | it('template document analysis reuses the shared suggestion dialog', () => { |
| 76 | const templateUseDialog = readSource( |
| 77 | 'src/renderer/src/components/templates/TemplateUseDialog.tsx' |
| 78 | ) |
| 79 | |
| 80 | expect(templateUseDialog).toContain('SessionCreateSuggestionDialog') |
| 81 | expect(templateUseDialog).not.toContain('updateDraftSourcePlanItems') |
| 82 | expect(templateUseDialog).not.toContain('editingOutlineIndex') |
| 83 | expect(templateUseDialog).not.toContain('suggestionCardClass') |
| 84 | }) |
| 85 | |
| 86 | it('keeps source documents for edits and retries but excludes them from generated add-page', () => { |
| 87 | const generationContext = readSource('src/main/generation/context.ts') |
| 88 | const sourceDocuments = readSource('src/main/generation/source-documents.ts') |
| 89 | const editFlow = readSource('src/main/generation/edit-flow.ts') |
| 90 | const deckAllPageEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 91 | const addPageFlow = readSource('src/main/generation/add-page-flow.ts') |
| 92 | const retrySinglePageFlow = readSource('src/main/generation/retry-single-page-flow.ts') |
| 93 | |
| 94 | expect(generationContext).toContain( |
| 95 | "export { resolveSourceDocuments } from './source-documents'" |
| 96 | ) |
| 97 | expect(sourceDocuments).toContain('appendSourceDocumentPath(resolveExistingSessionDoc') |
| 98 | expect(sourceDocuments).toContain('appendSourceDocumentPath(`/docs/${safeName}`)') |
| 99 | expect(generationContext).not.toContain("mode === 'edit') return []") |
| 100 | expect(generationContext).not.toContain('isFirstDeckGeneration') |
| 101 | expect(editFlow).toContain('resolveSourceDocuments') |
| 102 | expect(editFlow).toContain('sourceDocumentPaths: context.sourceDocumentPaths') |
| 103 | expect(deckAllPageEditFlow).toContain('sourceDocumentPaths: context.sourceDocumentPaths') |
| 104 | expect(addPageFlow).not.toContain('resolveSourceDocuments') |
| 105 | expect(addPageFlow).not.toContain('context.sourceDocumentPaths') |
| 106 | expect(addPageFlow.match(/sourceDocumentPaths: \[\]/g)).toHaveLength(3) |
| 107 | expect(retrySinglePageFlow).toContain('resolveSourceDocuments') |
| 108 | expect(retrySinglePageFlow).toContain('sourceDocumentPaths: context.sourceDocumentPaths') |
| 109 | expect(retrySinglePageFlow).not.toContain('sourceDocumentPaths: []') |
| 110 | }) |
| 111 | |
| 112 | it('deck all-page edit selected page ids only match file slugs', () => { |
| 113 | const deckAllPageEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 114 | |
| 115 | expect(deckAllPageEditFlow).toContain('requestedPageIdSet.has(ref.pageId)') |
| 116 | expect(deckAllPageEditFlow).not.toContain('requestedPageIdSet.has(ref.id)') |
| 117 | }) |
| 118 | |
| 119 | it('main-session deck edit enforces the shared selected-page limit', () => { |
| 120 | const sharedGeneration = readSource('src/shared/generation.ts') |
| 121 | const deckAllPageEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 122 | const chatPanel = readSource( |
| 123 | 'src/renderer/src/components/session-detail/ai-panel/ChatPanel.tsx' |
| 124 | ) |
| 125 | |
| 126 | expect(sharedGeneration).toContain('export const MAX_SELECTED_PAGES = 50') |
| 127 | expect(sharedGeneration).toContain('export const MAX_STYLE_SWITCH_PAGES = 500') |
| 128 | expect(sharedGeneration).not.toContain('.slice(0, 200)') |
| 129 | expect(deckAllPageEditFlow).toContain( |
| 130 | 'context.resetVisualStyle ? MAX_STYLE_SWITCH_PAGES : MAX_SELECTED_PAGES' |
| 131 | ) |
| 132 | expect(chatPanel).toContain('effectiveMainPageIds.length >= MAX_SELECTED_PAGES') |
| 133 | expect(chatPanel).toContain('pageIds.length > MAX_SELECTED_PAGES') |
| 134 | }) |
| 135 | |
| 136 | it('deck edit batch aborts finalize the run before rethrowing', () => { |
| 137 | const deckAllPageEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 138 | |
| 139 | expect(deckAllPageEditFlow).toContain('batchResults = await executeDeckEditBatchFlow') |
| 140 | expect(deckAllPageEditFlow).toContain( |
| 141 | "await db.updateGenerationRunStatus(context.runId, 'failed', message)" |
| 142 | ) |
| 143 | expect(deckAllPageEditFlow).toContain("type: 'run_error'") |
| 144 | expect(deckAllPageEditFlow).toContain('throw error') |
| 145 | }) |
| 146 | |
| 147 | it('deck edit staggers three independent page agents to reduce rate-limit bursts', () => { |
| 148 | const batchFlow = readSource('src/main/generation/edit-deck-batch-flow.ts') |
| 149 | const deckAllPageEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 150 | const engine = readSource('src/main/generation/agent-runner.ts') |
| 151 | |
| 152 | expect(batchFlow).toContain('export const BATCH_EDIT_LAUNCH_STAGGER_MS = 100') |
| 153 | expect(batchFlow).toContain('import pLimit from') |
| 154 | expect(batchFlow).toContain('pLimit(BATCH_EDIT_CHUNK_SIZE)') |
| 155 | expect(batchFlow).toContain('pageIndex % BATCH_EDIT_CHUNK_SIZE') |
| 156 | expect(batchFlow).toContain('Promise.allSettled') |
| 157 | expect(deckAllPageEditFlow).toContain('runPageAttempt') |
| 158 | expect(deckAllPageEditFlow).toContain('selectPageIds: [pageId]') |
| 159 | expect(deckAllPageEditFlow).toContain('isDeckEditRateLimitRetryableError(error)') |
| 160 | expect(engine).toContain('setPageAgent(args.sessionId, concurrentDeckPageId, editAgent)') |
| 161 | }) |
| 162 | |
| 163 | it('keeps generation progress in the local page surfaces without a modal', () => { |
| 164 | const chatPanel = readSource( |
| 165 | 'src/renderer/src/components/session-detail/ai-panel/ChatPanel.tsx' |
| 166 | ) |
| 167 | const previewStage = readSource( |
| 168 | 'src/renderer/src/components/session-detail/preview/PreviewStage.tsx' |
| 169 | ) |
| 170 | const sessionDetail = readSource('src/renderer/src/pages/session-detail.tsx') |
| 171 | |
| 172 | expect(chatPanel).not.toContain('<Progress value={progress.progress}') |
| 173 | expect(chatPanel).toContain('(isPageEditing || isDeckEditing)') |
| 174 | expect(previewStage).not.toContain('useGenerationLoading') |
| 175 | expect(previewStage).not.toContain('generationLoading') |
| 176 | expect(previewStage).toContain('pageEditJob && isPageEditing') |
| 177 | expect(sessionDetail).not.toContain('GenerationActivityDialog') |
| 178 | expect(sessionDetail).not.toContain('onStyleSwitchCompleted') |
| 179 | expect(sessionDetail).not.toContain('<PageProgressOverlay') |
| 180 | }) |
| 181 | |
| 182 | it('preserves chat messages when generation is cancelled', () => { |
| 183 | const sessionStore = readSource('src/renderer/src/store/sessionStore.ts') |
| 184 | const sessionDetail = readSource('src/renderer/src/pages/session-detail.tsx') |
| 185 | const loadSessionSource = sessionStore.slice( |
| 186 | sessionStore.indexOf('loadSession: async'), |
| 187 | sessionStore.indexOf('loadMessages: async') |
| 188 | ) |
| 189 | |
| 190 | expect(loadSessionSource).not.toContain('currentMessages: []') |
| 191 | expect(sessionDetail).toContain('if (payload.cancelled)') |
| 192 | expect(sessionDetail).toContain('cancelGeneration(payload.message)') |
| 193 | }) |
| 194 | |
| 195 | it('keeps concurrent deck-page progress on the active page instead of resetting to understanding', () => { |
| 196 | const deckAllPageEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 197 | const batchFlow = readSource('src/main/generation/edit-deck-batch-flow.ts') |
| 198 | const engine = readSource('src/main/generation/agent-runner.ts') |
| 199 | |
| 200 | expect(deckAllPageEditFlow).toContain("'正在准备批量编辑'") |
| 201 | expect(engine).toContain('`正在编辑页面 ${concurrentDeckPageId}`') |
| 202 | expect(engine).toContain("'正在生成并校验当前页面'") |
| 203 | expect(batchFlow).toContain('`正在编辑 P${args.pageNumber}`') |
| 204 | }) |
| 205 | |
| 206 | it('uses operation-specific progress copy for add-page and failed-page retries', () => { |
| 207 | const engine = readSource('src/main/generation/agent-runner.ts') |
| 208 | const addPageFlow = readSource('src/main/generation/add-page-flow.ts') |
| 209 | const retrySinglePageFlow = readSource('src/main/generation/retry-single-page-flow.ts') |
| 210 | const retryFlow = readSource('src/main/generation/retry-flow.ts') |
| 211 | |
| 212 | expect(engine).toContain('renderingLabel?: string') |
| 213 | expect(engine).toContain( |
| 214 | "const renderingLabel = args.renderingLabel || progressText(args.appLocale, 'generating')" |
| 215 | ) |
| 216 | expect(addPageFlow).toContain("'正在规划新增页面'") |
| 217 | expect(addPageFlow).toContain("'正在生成新增页面'") |
| 218 | expect(retrySinglePageFlow).toContain('`正在重新生成第 ${context.pageNumber} 页`') |
| 219 | expect(retryFlow).toContain('`正在重新生成 ${retryPages.length} 个失败页面`') |
| 220 | }) |
| 221 | |
| 222 | it('uses successful edit facts for edit replies instead of raw agent/tool output', () => { |
| 223 | const deckFlow = readSource('src/main/generation/deck-flow.ts') |
| 224 | const editFlow = readSource('src/main/generation/edit-flow.ts') |
| 225 | const batchEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 226 | const addPageFlow = readSource('src/main/generation/add-page-flow.ts') |
| 227 | const retryFlow = readSource('src/main/generation/retry-flow.ts') |
| 228 | const retrySinglePageFlow = readSource('src/main/generation/retry-single-page-flow.ts') |
| 229 | |
| 230 | expect(deckFlow).toContain('agentSummary.trim() || fallbackCompletionSummary') |
| 231 | expect(editFlow).toContain('emitSuccessfulEditSummary(context, editSummary, emitAssistant)') |
| 232 | expect(editFlow).not.toContain('editSummaryFromEngine') |
| 233 | expect(batchEditFlow).toContain( |
| 234 | 'emitSuccessfulEditSummary(context, fallbackEditSummary, emitAssistant)' |
| 235 | ) |
| 236 | expect(batchEditFlow).not.toContain('result.summary') |
| 237 | expect(editFlow.lastIndexOf('await db.updateGenerationRunStatus(')).toBeLessThan( |
| 238 | editFlow.indexOf('await emitSuccessfulEditSummary(context, editSummary, emitAssistant)') |
| 239 | ) |
| 240 | expect(batchEditFlow.lastIndexOf('await db.updateGenerationRunStatus(')).toBeLessThan( |
| 241 | batchEditFlow.indexOf( |
| 242 | 'await emitSuccessfulEditSummary(context, fallbackEditSummary, emitAssistant)' |
| 243 | ) |
| 244 | ) |
| 245 | expect(addPageFlow).toContain('agentSummary ||') |
| 246 | expect(retryFlow).toContain('agentSummary.trim() || fallbackCompletionSummary') |
| 247 | expect(retrySinglePageFlow).toContain('generationResult.summary.trim() ||') |
| 248 | expect(editFlow).not.toContain('我准备开始调整') |
| 249 | expect(batchEditFlow).not.toContain('我准备按主会话指令调整') |
| 250 | }) |
| 251 | |
| 252 | it('publishes durable batch page results without stealing preview focus or duplicating summaries', () => { |
| 253 | const sharedGeneration = readSource('src/shared/generation.ts') |
| 254 | const sessionDetail = readSource('src/renderer/src/pages/session-detail.tsx') |
| 255 | const batchEditFlow = readSource('src/main/generation/edit-deck-allpage-flow.ts') |
| 256 | |
| 257 | expect(sharedGeneration).toContain('focusPage?: boolean') |
| 258 | expect(sessionDetail).toContain('if (payload.focusPage !== false)') |
| 259 | expect(batchEditFlow).toContain('focusPage: false') |
| 260 | expect(batchEditFlow).toContain( |
| 261 | 'emitSuccessfulEditSummary(context, fallbackEditSummary, emitAssistant)' |
| 262 | ) |
| 263 | expect(batchEditFlow.indexOf('await db.upsertSessionPage({')).toBeLessThan( |
| 264 | batchEditFlow.indexOf("type: isExisting ? 'page_updated' : 'page_generated'") |
| 265 | ) |
| 266 | expect(batchEditFlow).toContain("status: existing?.status || 'failed'") |
| 267 | expect(batchEditFlow).toContain('error: existing?.error || null') |
| 268 | expect(batchEditFlow).toContain('resolveRemainingFailedPageInfo({') |
| 269 | }) |
| 270 | |
| 271 | it('main-session page scope is visible and resets after a successful send', () => { |
| 272 | const chatPanel = readSource( |
| 273 | 'src/renderer/src/components/session-detail/ai-panel/ChatPanel.tsx' |
| 274 | ) |
| 275 | const chatController = readSource( |
| 276 | 'src/renderer/src/components/session-detail/hooks/useChatPanelController.ts' |
| 277 | ) |
| 278 | |
| 279 | expect(chatPanel).toContain('mainPageScopeConflictWarning') |
| 280 | expect(chatPanel).toContain('if (started) setSelectedMainPageIds([])') |
| 281 | expect(chatController).toContain('mainPageScopeMessagePrefix') |
| 282 | expect(chatController).toContain('userMessage: scopedMessageContent') |
| 283 | expect(chatController).toContain('content: scopedMessageContent') |
| 284 | }) |
| 285 | |
| 286 | it('deck edit selected single-page scope still uses deck edit tools', () => { |
| 287 | const agent = readSource('src/main/agent-runtime/agent/factory.ts') |
| 288 | const editSystem = readSource('src/main/agent-runtime/prompt/composers/edit-system.ts') |
| 289 | const deckSystem = readSource('src/main/agent-runtime/prompt/composers/deck-system.ts') |
| 290 | const deckTools = readSource('src/main/agent-runtime/tools/deck-tools.ts') |
| 291 | const editTemplates = [ |
| 292 | 'container.md', |
| 293 | 'selector.md', |
| 294 | 'single-page.md', |
| 295 | 'deck.md' |
| 296 | ].map((fileName) => |
| 297 | readSource(`src/main/agent-runtime/prompt/templates/edit-system/${fileName}`) |
| 298 | ) |
| 299 | const templateSource = editTemplates.join('\n') |
| 300 | |
| 301 | expect(agent).toContain("return context.mode === 'edit'") |
| 302 | expect(agent).toContain('当前编辑任务禁止使用 write_file') |
| 303 | expect(editSystem).toContain('createPromptCatalog<EditSystemTemplateVars>') |
| 304 | expect(templateSource).toContain('仅允许调用 set_index_transition(type, durationMs)') |
| 305 | expect(templateSource).toContain('read_file target page + grep to locate target → edit_file') |
| 306 | expect(templateSource).toContain('update_single_page_file(pageId="{{targetPageId}}"') |
| 307 | expect(templateSource).toContain('For each target page: update_page_file(pageId, content)') |
| 308 | expect(deckSystem).toContain("context.mode !== 'edit'") |
| 309 | expect(deckTools).toContain('!isEditMode &&') |
| 310 | }) |
| 311 | |
| 312 | it('deck edit prompt applies UI-selected page ids only to deck scope', () => { |
| 313 | const editSystem = readSource('src/main/agent-runtime/prompt/composers/edit-system.ts') |
| 314 | const selectorPromptSource = editSystem.slice( |
| 315 | editSystem.indexOf('function buildSelectorEditPrompt('), |
| 316 | editSystem.indexOf('function buildSinglePageEditPrompt(') |
| 317 | ) |
| 318 | const deckPromptSource = editSystem.slice(editSystem.indexOf('function buildDeckEditPrompt(')) |
| 319 | |
| 320 | expect(selectorPromptSource).not.toContain('explicitTargetInfo') |
| 321 | expect(selectorPromptSource).not.toContain('Selected page ids from UI (hard target)') |
| 322 | expect(deckPromptSource).toContain('const explicitTargetInfo =') |
| 323 | expect(deckPromptSource).toContain('context.selectPageIds?.length') |
| 324 | expect(deckPromptSource).toContain( |
| 325 | 'Selected page ids from UI (hard target): ${context.selectPageIds.join' |
| 326 | ) |
| 327 | expect(deckPromptSource).toContain("'Target pages: all relevant /<pageId>.html files'") |
| 328 | expect(deckPromptSource).toContain(' explicitTargetInfo,') |
| 329 | }) |
| 330 | |
| 331 | it('edit prompt injects source document rules', () => { |
| 332 | const editSystem = readSource('src/main/agent-runtime/prompt/composers/edit-system.ts') |
| 333 | |
| 334 | expect(editSystem).toContain('Source documents (content evidence)') |
| 335 | expect(editSystem).toContain('SOURCE_DOCUMENT_READ_STRATEGY') |
| 336 | expect(editSystem).toContain('sourceDocumentPaths:') |
| 337 | expect(editSystem).toContain('For pure visual/style-only edits') |
| 338 | }) |
| 339 | |
| 340 | it('planNewPage includes source document context', () => { |
| 341 | const engineGenerate = readSource('src/main/generation/agent-runner.ts') |
| 342 | |
| 343 | expect(engineGenerate).toContain('sourceDocumentPaths?: string[]') |
| 344 | expect(engineGenerate).toContain('Source document context:') |
| 345 | }) |
| 346 | |
| 347 | it('single-slide planning can preserve explicit topic lists', () => { |
| 348 | const engineGenerate = readSource('src/main/generation/agent-runner.ts') |
| 349 | const generationUser = readSource('src/main/agent-runtime/prompt/composers/generation-user.ts') |
| 350 | const planningComposer = readSource('src/main/agent-runtime/prompt/composers/planning.ts') |
| 351 | const planningTemplate = readSource( |
| 352 | 'src/main/agent-runtime/prompt/templates/planning/system.md' |
| 353 | ) |
| 354 | const runtimeUserSource = readSource('src/main/agent-runtime/prompt/composers/runtime-user.ts') |
| 355 | |
| 356 | expect(engineGenerate).toContain('keyPoints must contain 1-10 short phrases') |
| 357 | expect(engineGenerate).toContain('preserve each listed topic as a separate key point') |
| 358 | expect(generationUser).not.toContain('final slide should cover all of them') |
| 359 | expect(generationUser).toContain('not as a checklist') |
| 360 | expect(generationUser).toContain('Do not duplicate the same source facts') |
| 361 | expect(generationUser).toContain('grouping related points') |
| 362 | expect(generationUser).toContain('keep them distinct only where the layout allows') |
| 363 | expect(planningComposer).toContain('planningPromptCatalog.render') |
| 364 | expect(planningTemplate).toContain('Provide 1-10 key points per slide') |
| 365 | expect(runtimeUserSource).toContain('keyPoints must contain 1-10 strings') |
| 366 | }) |
| 367 | |
| 368 | it('blocks generic filler slides during planning', () => { |
| 369 | const sharedSource = readSource('src/main/agent-runtime/prompt/composers/shared.ts') |
| 370 | const planningComposer = readSource('src/main/agent-runtime/prompt/composers/planning.ts') |
| 371 | const planningTemplate = readSource( |
| 372 | 'src/main/agent-runtime/prompt/templates/planning/system.md' |
| 373 | ) |
| 374 | const runtimeUserSource = readSource('src/main/agent-runtime/prompt/composers/runtime-user.ts') |
| 375 | |
| 376 | expect(planningComposer).toContain('SOURCE_MATERIAL_PLANNING_RULES') |
| 377 | expect(sharedSource).toContain('Apply these rules only when source documents') |
| 378 | expect(sharedSource).toContain('Stay source-grounded and avoid creative drift') |
| 379 | expect(sharedSource).toContain('evidence, not a slide checklist') |
| 380 | expect(sharedSource).toContain( |
| 381 | 'split into multiple slides when one page would become a data dump' |
| 382 | ) |
| 383 | expect(sharedSource).toContain('split source-backed sections') |
| 384 | expect(sharedSource).toContain('deepen each slide from the available material') |
| 385 | expect(sharedSource).toContain('SOURCE_GROUNDED_EXPANSION_RULES') |
| 386 | expect(sharedSource).toContain('actively enrich the slide from the material') |
| 387 | expect(sharedSource).toContain('source-grounded does not mean exhaustive') |
| 388 | expect(sharedSource).toContain('Do not add generic agenda') |
| 389 | expect(planningTemplate).toContain('For open-ended topics without source materials') |
| 390 | expect(planningTemplate).not.toContain('split or merge') |
| 391 | expect(runtimeUserSource).toContain('hasSourceMaterialCue') |
| 392 | expect(runtimeUserSource).toContain('hasSourceMaterials?: boolean') |
| 393 | expect(runtimeUserSource).toContain('args.hasSourceMaterials || hasSourceMaterialCue') |
| 394 | expect(runtimeUserSource).toContain('SOURCE_MATERIAL_PLANNING_RULES') |
| 395 | expect(runtimeUserSource).not.toContain( |
| 396 | 'Do not reinterpret the reference document into a new creative storyline' |
| 397 | ) |
| 398 | }) |
| 399 | |
| 400 | it('requires source inspection before source-backed slide generation', () => { |
| 401 | const sharedSource = readSource('src/main/agent-runtime/prompt/composers/shared.ts') |
| 402 | const source = readSource('src/main/agent-runtime/prompt/composers/generation-user.ts') |
| 403 | const sourceReadingSkill = readSource('resources/skills/oh-my-ppt-source-reading/SKILL.md') |
| 404 | |
| 405 | expect(sharedSource).toContain('SOURCE_READING_SKILL_NAME') |
| 406 | expect(sharedSource).toContain('Before using source documents') |
| 407 | expect(sharedSource).toContain('Grounding forbids inventing facts the source lacks') |
| 408 | expect(sharedSource).not.toContain('Before writing source-backed content') |
| 409 | expect(sharedSource).not.toContain('Do not read entire long documents into context at once') |
| 410 | expect(sourceReadingSkill).toContain( |
| 411 | 'Use the DeepAgents filesystem tool `grep(pattern, path, glob)`' |
| 412 | ) |
| 413 | expect(sourceReadingSkill).toContain('Use the DeepAgents filesystem tool `glob(pattern, path)`') |
| 414 | expect(sourceReadingSkill).toContain('`pattern` is a literal string') |
| 415 | expect(sourceReadingSkill).toContain('Use `read_file` only on targeted sections') |
| 416 | expect(sourceReadingSkill).toContain('repeat grep -> targeted read') |
| 417 | expect(sourceReadingSkill).toContain('retrieved snippet conflicts with the source passage') |
| 418 | expect(sourceReadingSkill).toContain('Slide title: "Q3 Revenue Highlights"') |
| 419 | expect(sourceReadingSkill).toContain('Prefer 50-80 lines around grep matches') |
| 420 | expect(source).toContain('expansion must be source-grounded') |
| 421 | expect(source).toContain('SOURCE_GROUNDED_EXPANSION_RULES') |
| 422 | expect(source).toContain('if inspected material is thin, enrich the slide') |
| 423 | expect(readSource('src/main/agent-runtime/prompt/composers/deck-system.ts')).toContain( |
| 424 | 'SOURCE_GROUNDED_EXPANSION_RULES' |
| 425 | ) |
| 426 | expect(readSource('src/main/agent-runtime/prompt/composers/edit-system.ts')).toContain( |
| 427 | 'SOURCE_GROUNDED_EXPANSION_RULES' |
| 428 | ) |
| 429 | expect(source).toContain('SOURCE_DOCUMENT_FACT_RULE') |
| 430 | expect(sharedSource).toContain('examples, risks, decisions, or conclusions') |
| 431 | expect(source).not.toContain('first use grep or glob') |
| 432 | expect(source).not.toContain('you do not need to reread') |
| 433 | }) |
| 434 | |
| 435 | it('source-reading skill expands thin pages instead of over-suppressing into sparse slides', () => { |
| 436 | const sourceReadingSkill = readSource('resources/skills/oh-my-ppt-source-reading/SKILL.md') |
| 437 | |
| 438 | // The old wording ("build ONLY from inspected passages" / "do not fill gaps") |
| 439 | // over-suppressed: with a reference doc present the model rendered bare source |
| 440 | // (chart + a couple facts) and left the page blank, because it read the skill as |
| 441 | // forbidding any addition. Those over-strict lines are gone. |
| 442 | expect(sourceReadingSkill).not.toContain('Build slide content only from inspected') |
| 443 | expect(sourceReadingSkill).not.toContain('Do not fill gaps with plausible-sounding') |
| 444 | |
| 445 | // The skill now tells the model to expand a thin page into a full argument with |
| 446 | // analytical structure derived from the inspected material. |
| 447 | expect(sourceReadingSkill).toContain('A half-empty slide is a failure') |
| 448 | expect(sourceReadingSkill).toContain('Expand the slide into a complete argument') |
| 449 | expect(sourceReadingSkill).toContain('comparison dimensions') |
| 450 | |
| 451 | // ...while keeping the anti-hallucination core: never invent EXACT facts. |
| 452 | expect(sourceReadingSkill).toContain('Do not invent exact facts') |
| 453 | }) |
| 454 | }) |
| 455 |