Files
superwork/vscode/src/settings/promptMigration.test.ts
T
cruldra 7bac5c8b03 ✨ feat(vscode): 七份提示词统一走 Handlebars,头脑风暴三档合一
- 模板引擎换成 Handlebars(noEscape,提示词是纯文本不能被转义),单花括号写法彻底退场
- brainstorm-continue / brainstorm-continue-prototype 两份并进 brainstorm.md,
  用 {{#if issueNumber}} 分新建 / 续聊,续聊里用 {{#if prototypeFile}} 嵌原型段落,
  设置面板对应的两个 textarea 一并删掉
- 模板上下文 = 工单 state JSON 全量字段 + issueNumber / userRequest / nonce /
  planFile / prNumber / prRef,以后模板想用新字段不用改扩展代码
- 各会话入口共用一条 state 读取路径,读失败只记 warn 按空状态继续,不拦开会话
- 升级后首次启动把存量自定义提示词迁到双花括号并合并成一份,改写前把
  settings.json 备份成 settings.json.bak-<时间戳>,迁移动作全写日志
- 装 handlebars 时给 vsce 传递依赖的老 semver 加 trustPolicyExclude 豁免,
  否则 no-downgrade 策略卡住整个装包

Claude-Session: https://claude.ai/code/session_01Kgmb1LvrPmFX3NoUDB4zZC
2026-09-15 00:44:28 +08:00

150 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}}')
})
})