import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { renderTemplate, renderPromptTemplate, createToolDescriptionContext, clearGitInstructionsCache, } from '../../../src/template/renderer.js'; import type { ExtendedTemplateContext } from '../../../src/template/types.js'; import { CUSTOM_TIMEOUT_MS, MAX_TIMEOUT_MS, MAX_OUTPUT_CHARS, } from '../../../src/constants/bash-tool.js'; describe('Template Renderer - 模板渲染器', () => { describe('基础变量替换', () => { it('替换简单变量', () => { const result = renderTemplate('Hello ${name}!', { name: 'World' }); expect(result).toBe('Hello World!'); }); it('替换嵌套属性', () => { const result = renderTemplate('${user.name} is ${user.age}', { user: { name: 'Alice', age: 30 }, }); expect(result).toBe('Alice is 30'); }); it('处理未定义变量 - 默认空字符串', () => { const result = renderTemplate('Value: ${undefined_var}', {}); expect(result).toBe('Value: '); }); it('处理未定义变量 - 自定义默认值', () => { const result = renderTemplate('Value: ${undefined_var}', {}, { undefinedValue: 'N/A' }); expect(result).toBe('Value: N/A'); }); }); describe('三元表达式', () => { it('条件为真时返回 trueValue', () => { const result = renderTemplate('${isActive ? "Active" : "Inactive"}', { isActive: true }); expect(result).toBe('Active'); }); it('条件为假时返回 falseValue', () => { const result = renderTemplate('${isActive ? "Active" : "Inactive"}', { isActive: false }); expect(result).toBe('Inactive'); }); it('空字符串被视为假', () => { const result = renderTemplate('${value ? "yes" : "no"}', { value: '' }); expect(result).toBe('no'); }); }); describe('函数调用', () => { it('调用注册的函数', () => { const context: ExtendedTemplateContext = { __functions__: { GET_VALUE: () => 42, }, }; const result = renderTemplate('Value: ${GET_VALUE()}', context); expect(result).toBe('Value: 42'); }); it('调用返回字符串的函数', () => { const context: ExtendedTemplateContext = { __functions__: { GET_MESSAGE: () => 'Hello from function', }, }; const result = renderTemplate('${GET_MESSAGE()}', context); expect(result).toBe('Hello from function'); }); it('未注册的函数返回空字符串', () => { const context: ExtendedTemplateContext = { __functions__: {}, }; const result = renderTemplate('Value: ${UNKNOWN_FUNC()}', context); expect(result).toBe('Value: '); }); it('没有 __functions__ 时返回空字符串', () => { const result = renderTemplate('Value: ${SOME_FUNC()}', {}); expect(result).toBe('Value: '); }); }); describe('算术运算(除法)', () => { it('数值除法', () => { const result = renderTemplate('${value / 1000}', { value: 60000 }); expect(result).toBe('60'); }); it('函数调用结果除法', () => { const context: ExtendedTemplateContext = { __functions__: { GET_MS: () => 120000, }, }; const result = renderTemplate('${GET_MS() / 60000} minutes', context); expect(result).toBe('2 minutes'); }); it('结果向下取整', () => { const result = renderTemplate('${value / 1000}', { value: 12345 }); expect(result).toBe('12'); }); it('除以零不进行除法运算', () => { // 除以零时,divMatch 匹配但 divisor === 0,会跳过除法逻辑 // 然后尝试作为变量处理 "value / 0",找不到返回空 const result = renderTemplate('${value / 0}', { value: 100 }); expect(result).toBe(''); }); }); describe('组合表达式', () => { it('函数调用 + 除法 + 字符串', () => { const context: ExtendedTemplateContext = { __functions__: { TIMEOUT_MS: () => 600000, }, }; const result = renderTemplate( 'Timeout: ${TIMEOUT_MS()}ms (${TIMEOUT_MS()/60000} minutes)', context ); expect(result).toBe('Timeout: 600000ms (10 minutes)'); }); it('多个函数调用', () => { const context: ExtendedTemplateContext = { __functions__: { MIN_VAL: () => 10, MAX_VAL: () => 100, }, }; const result = renderTemplate('Range: ${MIN_VAL()} - ${MAX_VAL()}', context); expect(result).toBe('Range: 10 - 100'); }); it('函数调用 + 变量混合', () => { const context: ExtendedTemplateContext = { toolName: 'bash', __functions__: { GET_TIMEOUT: () => 120000, }, }; const result = renderTemplate( 'Use ${toolName} with timeout ${GET_TIMEOUT()}ms', context ); expect(result).toBe('Use bash with timeout 120000ms'); }); }); }); describe('createToolDescriptionContext - 工具描述上下文', () => { beforeEach(() => { clearGitInstructionsCache(); }); afterEach(() => { clearGitInstructionsCache(); }); it('包含工具名称变量', () => { const context = createToolDescriptionContext(); expect(context.custom?.BASH_TOOL_NAME).toBe('bash'); expect(context.custom?.GREP_TOOL_NAME).toBe('grep'); expect(context.custom?.GLOB_TOOL_NAME).toBe('glob'); }); it('包含注册的模板函数', () => { const context = createToolDescriptionContext(); expect(context.__functions__).toBeDefined(); expect(typeof context.__functions__.CUSTOM_TIMEOUT_MS).toBe('function'); expect(typeof context.__functions__.MAX_TIMEOUT_MS).toBe('function'); expect(typeof context.__functions__.MAX_OUTPUT_CHARS).toBe('function'); expect(typeof context.__functions__.BASH_TOOL_EXTRA_NOTES).toBe('function'); expect(typeof context.__functions__.GIT_COMMIT_AND_PR_CREATION_INSTRUCTION).toBe('function'); }); it('函数返回正确的配置值', () => { const context = createToolDescriptionContext(); expect(context.__functions__.CUSTOM_TIMEOUT_MS()).toBe(CUSTOM_TIMEOUT_MS); expect(context.__functions__.MAX_TIMEOUT_MS()).toBe(MAX_TIMEOUT_MS); expect(context.__functions__.MAX_OUTPUT_CHARS()).toBe(MAX_OUTPUT_CHARS); }); it('BASH_TOOL_EXTRA_NOTES 默认返回空字符串', () => { const context = createToolDescriptionContext(); expect(context.__functions__.BASH_TOOL_EXTRA_NOTES()).toBe(''); }); it('GIT_COMMIT_AND_PR_CREATION_INSTRUCTION 返回 Git 指令', () => { const context = createToolDescriptionContext(); const gitInstructions = context.__functions__.GIT_COMMIT_AND_PR_CREATION_INSTRUCTION(); expect(typeof gitInstructions).toBe('string'); // 验证包含关键内容 expect(gitInstructions).toContain('Committing changes with git'); expect(gitInstructions).toContain('Creating pull requests'); }); }); describe('renderPromptTemplate - 提示词模板渲染', () => { it('渲染带函数调用的模板', () => { const context = createToolDescriptionContext(); const template = 'Timeout: ${CUSTOM_TIMEOUT_MS()}ms (${CUSTOM_TIMEOUT_MS()/60000} minutes)'; const result = renderPromptTemplate(template, context); expect(result).toBe(`Timeout: ${CUSTOM_TIMEOUT_MS}ms (${Math.floor(CUSTOM_TIMEOUT_MS / 60000)} minutes)`); }); it('渲染带工具名变量的模板', () => { const context = createToolDescriptionContext(); const template = 'Use ${BASH_TOOL_NAME} or ${GREP_TOOL_NAME}'; const result = renderPromptTemplate(template, context); expect(result).toBe('Use bash or grep'); }); it('渲染完整的 bash 描述模板片段', () => { const context = createToolDescriptionContext(); const template = `Commands timeout after \${MAX_TIMEOUT_MS()}ms (\${MAX_TIMEOUT_MS()/60000} minutes). Output truncated at \${MAX_OUTPUT_CHARS()} chars. Use \${GREP_TOOL_NAME} for search.`; const result = renderPromptTemplate(template, context); expect(result).toContain(`${MAX_TIMEOUT_MS}ms`); expect(result).toContain(`${Math.floor(MAX_TIMEOUT_MS / 60000)} minutes`); expect(result).toContain(`${MAX_OUTPUT_CHARS} chars`); expect(result).toContain('grep for search'); }); }); describe('回归测试 - 确保现有功能正常', () => { it('字符串字面量中的变量插值', () => { const result = renderTemplate('${cond ? "Value is ${val}" : "No value"}', { cond: true, val: 42, }); expect(result).toBe('Value is 42'); }); it('布尔值处理', () => { expect(renderTemplate('${flag}', { flag: true })).toBe('true'); expect(renderTemplate('${flag}', { flag: false })).toBe(''); }); it('数字值处理', () => { expect(renderTemplate('${num}', { num: 123 })).toBe('123'); expect(renderTemplate('${num}', { num: 0 })).toBe('0'); }); it('嵌套对象访问', () => { const result = renderTemplate('${a.b.c}', { a: { b: { c: 'deep' } } }); expect(result).toBe('deep'); }); it('多个模板变量', () => { const result = renderTemplate('${a} + ${b} = ${c}', { a: 1, b: 2, c: 3 }); expect(result).toBe('1 + 2 = 3'); }); });