Files
ai-terminal-assistant/packages/core/tests/unit/agent/prompt-template.test.ts
T
kurihada 58f1bc8718 feat(core): 实现动态提示词模板系统
- 新增 prompt-template 模块,支持运行时变量替换
- 支持 ${variable}、${obj.prop}、${cond ? "a" : "b"} 语法
- AgentInfo 新增 promptTemplate 字段标记动态模板
- Plan Agent 提示词改用模板语法,支持动态工具名和计划文件路径
- AgentExecutor.buildSystemPrompt 集成模板渲染
- 新增 27 个单元测试验证模板功能
2025-12-16 14:15:10 +08:00

221 lines
7.5 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest';
import {
renderTemplate,
createDefaultContext,
createPlanContext,
DEFAULT_TOOL_NAMES,
generatePlanPrompt,
resolveAgentPrompt,
} from '../../../src/agent/prompt-template/index.js';
import type { PromptContext } from '../../../src/agent/prompt-template/types.js';
describe('Prompt Template System', () => {
let context: PromptContext;
beforeEach(() => {
context = createDefaultContext({
plan: {
isActive: true,
planExists: false,
planFilePath: '/tmp/test-plan.md',
allowedWritePaths: ['/tmp/*'],
},
env: {
workdir: '/test/project',
isGitRepo: true,
platform: 'darwin',
date: '2024-01-01',
homeDir: '/home/test',
},
agent: {
name: 'test-agent',
mode: 'primary',
isSubagent: false,
},
});
});
describe('renderTemplate', () => {
it('should replace simple variables', () => {
const template = 'Use ${tools.glob} to find files';
const result = renderTemplate(template, context);
expect(result).toBe('Use glob to find files');
});
it('should replace nested properties', () => {
const template = 'Plan file: ${plan.planFilePath}';
const result = renderTemplate(template, context);
expect(result).toBe('Plan file: /tmp/test-plan.md');
});
it('should handle multiple variables in one template', () => {
const template = 'Use ${tools.glob}, ${tools.grep}, and ${tools.read}';
const result = renderTemplate(template, context);
expect(result).toBe('Use glob, grep_content, and read_file');
});
it('should handle ternary expressions - false condition', () => {
const template = '${plan.planExists ? "exists" : "not exists"}';
const result = renderTemplate(template, context);
expect(result).toBe('not exists');
});
it('should handle ternary expressions - true condition', () => {
context.plan.planExists = true;
const template = '${plan.planExists ? "exists" : "not exists"}';
const result = renderTemplate(template, context);
expect(result).toBe('exists');
});
it('should handle nested variables in ternary expressions', () => {
const template =
'${plan.planExists ? "File at ${plan.planFilePath}" : "Create at ${plan.planFilePath}"}';
const result = renderTemplate(template, context);
expect(result).toBe('Create at /tmp/test-plan.md');
});
it('should handle boolean values', () => {
const template = 'Is git repo: ${env.isGitRepo}';
const result = renderTemplate(template, context);
expect(result).toBe('Is git repo: true');
});
it('should handle undefined variables gracefully', () => {
const template = 'Value: ${custom.undefined}';
const result = renderTemplate(template, context);
expect(result).toBe('Value: ');
});
it('should preserve text without variables', () => {
const template = 'No variables here';
const result = renderTemplate(template, context);
expect(result).toBe('No variables here');
});
it('should handle complex nested ternary with tool references', () => {
context.plan.planExists = true;
const template =
'${plan.planExists ? "Edit with ${tools.edit}" : "Write with ${tools.write}"}';
const result = renderTemplate(template, context);
expect(result).toBe('Edit with edit_file');
});
});
describe('createDefaultContext', () => {
it('should create context with default tool names', () => {
const ctx = createDefaultContext();
expect(ctx.tools.glob).toBe('glob');
expect(ctx.tools.grep).toBe('grep_content');
expect(ctx.tools.read).toBe('read_file');
expect(ctx.tools.write).toBe('write_file');
expect(ctx.tools.bash).toBe('bash');
});
it('should allow overriding tool names', () => {
const ctx = createDefaultContext({
tools: {
...DEFAULT_TOOL_NAMES,
glob: 'custom_glob',
},
});
expect(ctx.tools.glob).toBe('custom_glob');
expect(ctx.tools.grep).toBe('grep_content'); // Others unchanged
});
it('should set default plan values', () => {
const ctx = createDefaultContext();
expect(ctx.plan.isActive).toBe(false);
expect(ctx.plan.planExists).toBe(false);
});
});
describe('createPlanContext', () => {
it('should create context with plan mode active', () => {
const ctx = createPlanContext({});
expect(ctx.plan.isActive).toBe(true);
expect(ctx.agent.name).toBe('plan');
});
it('should set isSubagent correctly', () => {
const ctx = createPlanContext({ isSubagent: true });
expect(ctx.agent.isSubagent).toBe(true);
expect(ctx.agent.mode).toBe('subagent');
});
it('should use custom workdir', () => {
const ctx = createPlanContext({ workdir: '/custom/path' });
expect(ctx.env.workdir).toBe('/custom/path');
});
it('should use custom plan file path', () => {
const ctx = createPlanContext({ planFilePath: '/custom/plan.md' });
expect(ctx.plan.planFilePath).toBe('/custom/plan.md');
});
});
describe('generatePlanPrompt', () => {
it('should generate prompt with tool names resolved', () => {
const prompt = generatePlanPrompt({});
expect(prompt).toContain('glob');
expect(prompt).toContain('grep_content');
expect(prompt).toContain('read_file');
});
it('should include plan file path', () => {
const prompt = generatePlanPrompt({ planFilePath: '/test/plan.md' });
expect(prompt).toContain('/test/plan.md');
});
it('should generate shorter prompt for subagent', () => {
const mainPrompt = generatePlanPrompt({ isSubagent: false });
const subPrompt = generatePlanPrompt({ isSubagent: true });
expect(subPrompt.length).toBeLessThan(mainPrompt.length);
});
});
describe('resolveAgentPrompt', () => {
it('should resolve template variables in prompt', () => {
const basePrompt = 'Use ${tools.glob} to search';
const resolved = resolveAgentPrompt(basePrompt, {});
expect(resolved).toBe('Use glob to search');
});
it('should handle isPlanMode option', () => {
const basePrompt = '${plan.isActive ? "Plan mode" : "Normal mode"}';
const planResolved = resolveAgentPrompt(basePrompt, { isPlanMode: true });
expect(planResolved).toBe('Plan mode');
const normalResolved = resolveAgentPrompt(basePrompt, { isPlanMode: false });
expect(normalResolved).toBe('Normal mode');
});
});
describe('Edge Cases', () => {
it('should handle empty template', () => {
const result = renderTemplate('', context);
expect(result).toBe('');
});
it('should handle template with only variable', () => {
const result = renderTemplate('${tools.bash}', context);
expect(result).toBe('bash');
});
it('should handle unclosed variable syntax', () => {
const result = renderTemplate('${tools.bash', context);
expect(result).toBe('${tools.bash');
});
it('should handle escaped-like patterns (no real escaping)', () => {
// Note: This system doesn't support escaping, so $$ or similar won't work
const result = renderTemplate('Price: $100', context);
expect(result).toBe('Price: $100');
});
it('should handle deeply nested properties', () => {
const result = renderTemplate('${agent.mode}', context);
expect(result).toBe('primary');
});
});
});