import { readFileSync } from 'node:fs' import * as path from 'node:path' import { describe, expect, it } from 'vitest' import { composeBrainstormTemplate, planPromptMigration, PROMPT_TEMPLATE_VERSION, splitIfBlock, upgradePlaceholders, } from './promptMigration' const defaultBrainstorm = readFileSync( path.resolve(__dirname, '../../prompts/brainstorm.md'), 'utf-8', ) describe('upgradePlaceholders', () => { it('已知占位符改写成双花括号', () => { expect(upgradePlaceholders('实施 @{planFile} 的 #{issueNumber}')).toBe('实施 @{{planFile}} 的 #{{issueNumber}}') }) it('已经是双花括号的不再加层', () => { expect(upgradePlaceholders('{{issueNumber}}')).toBe('{{issueNumber}}') }) it('名单外的花括号内容原样保留', () => { expect(upgradePlaceholders('{foo} {planfile} {"a":1}')).toBe('{foo} {planfile} {"a":1}') }) }) describe('composeBrainstormTemplate', () => { it('两份正文拼成条件分支,原型段落嵌进 prototypeRef 的位置', () => { const out = composeBrainstormTemplate({ newIssue: '新建 {{userRequest}}', cont: '续聊 {{issueNumber}}\n\n{{prototypeRef}}\n\n收尾', proto: '原型 {{prototypeFile}}\n- id:{{prototypeSessionId}}', }) expect(out).toBe([ '{{#if issueNumber}}', '续聊 {{issueNumber}}', '', '{{#if prototypeFile}}', '原型 {{prototypeFile}}', '{{#if prototypeSessionId}}', '- id:{{prototypeSessionId}}', '{{/if}}', '{{/if}}', '', '收尾', '{{else}}', '新建 {{userRequest}}', '{{/if}}', '', ].join('\n')) }) it('续聊正文里没有 prototypeRef 时原型段落接在末尾', () => { const out = composeBrainstormTemplate({ newIssue: '新建', cont: '续聊', proto: '原型 {{prototypeFile}}' }) expect(out).toContain('续聊\n\n{{#if prototypeFile}}\n原型 {{prototypeFile}}\n{{/if}}') }) it('没有原型段落时不留空的条件块', () => { const out = composeBrainstormTemplate({ newIssue: '新建', cont: '续聊\n\n{{prototypeRef}}', proto: '' }) expect(out).not.toContain('prototypeFile') expect(out).not.toContain('prototypeRef') }) }) describe('splitIfBlock', () => { it('只认第一层的 else,内层条件块不当分界', () => { const branches = splitIfBlock(defaultBrainstorm, 'issueNumber') expect(branches!.then).toContain('/ecc:grilling') expect(branches!.then).toContain('{{#if prototypeFile}}') expect(branches!.else).toContain('opencli spx issue create') expect(branches!.else).not.toContain('{{else}}') }) it('没有这个条件块时返回 null', () => { expect(splitIfBlock('普通模板', 'issueNumber')).toBeNull() }) }) describe('planPromptMigration', () => { const legacy = { brainstormPrompt: '建工单 {userRequest} nonce={nonce}', brainstormContinuePrompt: '讨论 {issueNumber}\n\n{prototypeRef}\n\n收尾', prototypeRefPrompt: '原型 {prototypeFile}\n- id:{prototypeSessionId}', implementPlanPrompt: '实施 @{planFile} #{issueNumber}', testPrompt: '验收 {issueNumber}({prRef})', reviewPrompt: '审查 #{prNumber}', prototypePrompt: '画原型 {issueNumber}', webhookPort: 17421, } it('五份存量提示词全部换成双花括号,并合并成一份头脑风暴模板', () => { const { blob, notes } = planPromptMigration(legacy, defaultBrainstorm) expect(blob).not.toBeNull() expect(blob!.implementPlanPrompt).toBe('实施 @{{planFile}} #{{issueNumber}}') expect(blob!.testPrompt).toBe('验收 {{issueNumber}}({{prRef}})') expect(blob!.reviewPrompt).toBe('审查 #{{prNumber}}') expect(blob!.prototypePrompt).toBe('画原型 {{issueNumber}}') expect(blob!.brainstormPrompt).toContain('{{#if issueNumber}}') expect(blob!.brainstormPrompt).toContain('{{#if prototypeFile}}') expect(blob!.brainstormPrompt).toContain('{{#if prototypeSessionId}}') expect(blob!.brainstormPrompt).toContain('{{else}}') expect(blob!.brainstormPrompt).toContain('建工单 {{userRequest}} nonce={{nonce}}') expect(notes.some(n => n.level === 'warn')).toBe(false) }) it('被删掉的两个字段迁移后不再留在 settings 里,其他设置不动', () => { const { blob } = planPromptMigration(legacy, defaultBrainstorm) expect(blob!.brainstormContinuePrompt).toBeUndefined() expect(blob!.prototypeRefPrompt).toBeUndefined() expect(blob!.webhookPort).toBe(17421) expect(blob!.promptTemplateVersion).toBe(PROMPT_TEMPLATE_VERSION) }) it('迁过一次就不再重复迁', () => { const { blob } = planPromptMigration(legacy, defaultBrainstorm) expect(planPromptMigration(blob!, defaultBrainstorm)).toEqual({ blob: null, notes: [] }) }) it('没存过任何提示词时什么都不做', () => { expect(planPromptMigration({ webhookPort: 17421 }, defaultBrainstorm)).toEqual({ blob: null, notes: [] }) }) it('只改过新建工单提示词时,续聊分支用内置默认补齐', () => { const { blob } = planPromptMigration( { brainstormPrompt: '建工单 {userRequest}', brainstormContinuePrompt: '' }, defaultBrainstorm, ) const merged = String(blob!.brainstormPrompt) expect(merged).toContain('建工单 {{userRequest}}') expect(merged).toContain('/ecc:grilling 讨论下 {{issueNumber}} 号工单') // 默认续聊分支自带原型段落,别再插第二份 expect(merged.match(/\{\{#if prototypeFile\}\}/g)).toHaveLength(1) }) it('只改过续聊提示词时,新建分支和原型段落用内置默认补齐', () => { const { blob } = planPromptMigration( { brainstormContinuePrompt: '讨论 {issueNumber}\n\n{prototypeRef}' }, defaultBrainstorm, ) const merged = String(blob!.brainstormPrompt) expect(merged).toContain('讨论 {{issueNumber}}') expect(merged).toContain('opencli spx issue create') expect(merged).toContain('工单已有原型插件 {{prototypeFile}}') }) })