feat(core): 扩展模板引擎支持函数调用和算术运算
- 新增模板函数调用语法 ${FUNC_NAME()}
- 新增除法算术运算 ${value/divisor}
- 添加 bash 工具配置常量 (CUSTOM_TIMEOUT_MS 等)
- 更新 bash 工具描述使用动态模板格式
- 添加 Git 操作指令文本 (commit/PR 说明)
- 添加模板渲染器单元测试 (31 个测试用例)
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user