yishentu--claudian
396 行
12 KiB
TypeScript
396 行
12 KiB
TypeScript
import type { AgentDefinition } from '@/core/types';
|
|
import { buildAgentFromFrontmatter, parseAgentFile } from '@/providers/claude/agents/AgentStorage';
|
|
import { serializeAgent, validateAgentName } from '@/utils/agent';
|
|
|
|
describe('validateAgentName', () => {
|
|
it('returns null for valid name', () => {
|
|
expect(validateAgentName('code-reviewer')).toBeNull();
|
|
});
|
|
|
|
it('returns null for single character', () => {
|
|
expect(validateAgentName('a')).toBeNull();
|
|
});
|
|
|
|
it('returns null for numbers and hyphens', () => {
|
|
expect(validateAgentName('agent-v2')).toBeNull();
|
|
});
|
|
|
|
it('returns error for empty name', () => {
|
|
expect(validateAgentName('')).toBe('Agent name is required');
|
|
});
|
|
|
|
it('returns error for name exceeding max length', () => {
|
|
const longName = 'a'.repeat(65);
|
|
expect(validateAgentName(longName)).toBe('Agent name must be 64 characters or fewer');
|
|
});
|
|
|
|
it('returns null for exactly max length', () => {
|
|
const maxName = 'a'.repeat(64);
|
|
expect(validateAgentName(maxName)).toBeNull();
|
|
});
|
|
|
|
it('returns error for uppercase letters', () => {
|
|
expect(validateAgentName('CodeReviewer')).toBe(
|
|
'Agent name can only contain lowercase letters, numbers, and hyphens'
|
|
);
|
|
});
|
|
|
|
it('returns error for spaces', () => {
|
|
expect(validateAgentName('code reviewer')).toBe(
|
|
'Agent name can only contain lowercase letters, numbers, and hyphens'
|
|
);
|
|
});
|
|
|
|
it('returns error for underscores', () => {
|
|
expect(validateAgentName('code_reviewer')).toBe(
|
|
'Agent name can only contain lowercase letters, numbers, and hyphens'
|
|
);
|
|
});
|
|
|
|
it('returns error for special characters', () => {
|
|
expect(validateAgentName('code@reviewer')).toBe(
|
|
'Agent name can only contain lowercase letters, numbers, and hyphens'
|
|
);
|
|
});
|
|
|
|
it.each(['true', 'false', 'null', 'yes', 'no', 'on', 'off'])(
|
|
'returns error for YAML reserved word "%s"',
|
|
(word) => {
|
|
expect(validateAgentName(word)).toBe(
|
|
'Agent name cannot be a YAML reserved word (true, false, null, yes, no, on, off)'
|
|
);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('serializeAgent', () => {
|
|
const baseAgent: AgentDefinition = {
|
|
id: 'test-agent',
|
|
name: 'test-agent',
|
|
description: 'A test agent',
|
|
prompt: 'You are a test agent.',
|
|
source: 'vault',
|
|
};
|
|
|
|
it('serializes minimal agent', () => {
|
|
const result = serializeAgent(baseAgent);
|
|
expect(result).toBe(
|
|
'---\nname: test-agent\ndescription: A test agent\n---\nYou are a test agent.'
|
|
);
|
|
});
|
|
|
|
it('serializes agent with tools', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
tools: ['Read', 'Grep'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('tools:\n - Read\n - Grep');
|
|
});
|
|
|
|
it('serializes agent with disallowedTools', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
disallowedTools: ['Write', 'Bash'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('disallowedTools:\n - Write\n - Bash');
|
|
});
|
|
|
|
it('serializes agent with model (non-inherit)', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
model: 'sonnet',
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('model: sonnet');
|
|
});
|
|
|
|
it('omits model when inherit', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
model: 'inherit',
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).not.toContain('model:');
|
|
});
|
|
|
|
it('serializes agent with permissionMode', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
permissionMode: 'dontAsk',
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('permissionMode: dontAsk');
|
|
});
|
|
|
|
it('omits permissionMode when undefined', () => {
|
|
const result = serializeAgent(baseAgent);
|
|
expect(result).not.toContain('permissionMode');
|
|
});
|
|
|
|
it('serializes agent with skills', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
skills: ['my-skill', 'another'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('skills:\n - my-skill\n - another');
|
|
});
|
|
|
|
it('quotes description with special YAML characters', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
description: 'Test: agent with #special chars',
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('description: "Test: agent with #special chars"');
|
|
});
|
|
|
|
it('includes prompt as body after frontmatter', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
prompt: 'Multi\nline\nprompt',
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toMatch(/---\nMulti\nline\nprompt$/);
|
|
});
|
|
|
|
it('serializes hooks as JSON', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
hooks: { preToolUse: { command: 'echo test' } },
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('hooks: {"preToolUse":{"command":"echo test"}}');
|
|
});
|
|
|
|
it('omits hooks when undefined', () => {
|
|
const result = serializeAgent(baseAgent);
|
|
expect(result).not.toContain('hooks');
|
|
});
|
|
|
|
it('serializes all fields together', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
tools: ['Read'],
|
|
disallowedTools: ['Bash'],
|
|
model: 'opus',
|
|
permissionMode: 'acceptEdits',
|
|
skills: ['review'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('name: test-agent');
|
|
expect(result).toContain('description: A test agent');
|
|
expect(result).toContain('tools:\n - Read');
|
|
expect(result).toContain('disallowedTools:\n - Bash');
|
|
expect(result).toContain('model: opus');
|
|
expect(result).toContain('permissionMode: acceptEdits');
|
|
expect(result).toContain('skills:\n - review');
|
|
});
|
|
|
|
it('quotes list items containing colons', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
tools: ['mcp__server:tool', 'Read'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain(' - "mcp__server:tool"');
|
|
expect(result).toContain(' - Read');
|
|
});
|
|
|
|
it('quotes list items containing hash', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
skills: ['skill#1'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain(' - "skill#1"');
|
|
});
|
|
|
|
it('quotes list items with leading spaces', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
tools: [' leading-space'],
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain(' - " leading-space"');
|
|
});
|
|
|
|
it('serializes extraFrontmatter keys', () => {
|
|
const agent: AgentDefinition = {
|
|
...baseAgent,
|
|
extraFrontmatter: { maxTurns: 5, customFlag: true },
|
|
};
|
|
const result = serializeAgent(agent);
|
|
expect(result).toContain('maxTurns: 5');
|
|
expect(result).toContain('customFlag: true');
|
|
});
|
|
|
|
it('omits extraFrontmatter when undefined', () => {
|
|
const result = serializeAgent(baseAgent);
|
|
expect(result).not.toContain('extraFrontmatter');
|
|
});
|
|
});
|
|
|
|
describe('serializeAgent / parseAgentFile round-trip', () => {
|
|
it('round-trips a minimal agent', () => {
|
|
const agent: AgentDefinition = {
|
|
id: 'my-agent',
|
|
name: 'my-agent',
|
|
description: 'A test agent',
|
|
prompt: 'You are a test agent.',
|
|
source: 'vault',
|
|
};
|
|
|
|
const serialized = serializeAgent(agent);
|
|
const parsed = parseAgentFile(serialized);
|
|
expect(parsed).not.toBeNull();
|
|
|
|
const rebuilt = buildAgentFromFrontmatter(parsed!.frontmatter, parsed!.body, {
|
|
id: parsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt.name).toBe(agent.name);
|
|
expect(rebuilt.description).toBe(agent.description);
|
|
expect(rebuilt.prompt).toBe(agent.prompt);
|
|
});
|
|
|
|
it('round-trips a fully populated agent', () => {
|
|
const agent: AgentDefinition = {
|
|
id: 'full-agent',
|
|
name: 'full-agent',
|
|
description: 'Full agent',
|
|
prompt: 'Do everything.',
|
|
tools: ['Read', 'Grep'],
|
|
disallowedTools: ['Bash'],
|
|
model: 'opus',
|
|
permissionMode: 'acceptEdits',
|
|
skills: ['review', 'deploy'],
|
|
hooks: { preToolUse: { command: 'echo test' } },
|
|
source: 'vault',
|
|
};
|
|
|
|
const serialized = serializeAgent(agent);
|
|
const parsed = parseAgentFile(serialized);
|
|
expect(parsed).not.toBeNull();
|
|
|
|
const rebuilt = buildAgentFromFrontmatter(parsed!.frontmatter, parsed!.body, {
|
|
id: parsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt.name).toBe(agent.name);
|
|
expect(rebuilt.description).toBe(agent.description);
|
|
expect(rebuilt.prompt).toBe(agent.prompt);
|
|
expect(rebuilt.tools).toEqual(agent.tools);
|
|
expect(rebuilt.disallowedTools).toEqual(agent.disallowedTools);
|
|
expect(rebuilt.model).toBe(agent.model);
|
|
expect(rebuilt.permissionMode).toBe(agent.permissionMode);
|
|
expect(rebuilt.skills).toEqual(agent.skills);
|
|
expect(rebuilt.hooks).toEqual(agent.hooks);
|
|
});
|
|
|
|
it('round-trips tools with special YAML characters', () => {
|
|
const agent: AgentDefinition = {
|
|
id: 'special-tools',
|
|
name: 'special-tools',
|
|
description: 'Agent with special tool names',
|
|
prompt: 'Use special tools.',
|
|
tools: ['mcp__server:tool', 'Read'],
|
|
source: 'vault',
|
|
};
|
|
|
|
const serialized = serializeAgent(agent);
|
|
const parsed = parseAgentFile(serialized);
|
|
expect(parsed).not.toBeNull();
|
|
|
|
const rebuilt = buildAgentFromFrontmatter(parsed!.frontmatter, parsed!.body, {
|
|
id: parsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt.tools).toEqual(['mcp__server:tool', 'Read']);
|
|
});
|
|
|
|
it('round-trips unknown frontmatter keys', () => {
|
|
const content = `---
|
|
name: custom-agent
|
|
description: An agent with extra keys
|
|
maxTurns: 5
|
|
mcpServers: ["server1"]
|
|
---
|
|
Do stuff.`;
|
|
|
|
const parsed = parseAgentFile(content);
|
|
expect(parsed).not.toBeNull();
|
|
|
|
const rebuilt = buildAgentFromFrontmatter(parsed!.frontmatter, parsed!.body, {
|
|
id: parsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt.extraFrontmatter).toEqual({
|
|
maxTurns: 5,
|
|
mcpServers: ['server1'],
|
|
});
|
|
|
|
const reserialized = serializeAgent(rebuilt);
|
|
expect(reserialized).toContain('maxTurns: 5');
|
|
expect(reserialized).toContain('mcpServers:');
|
|
|
|
const reparsed = parseAgentFile(reserialized);
|
|
expect(reparsed).not.toBeNull();
|
|
|
|
const rebuilt2 = buildAgentFromFrontmatter(reparsed!.frontmatter, reparsed!.body, {
|
|
id: reparsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt2.extraFrontmatter?.maxTurns).toBe(5);
|
|
});
|
|
|
|
it('does not set extraFrontmatter when no unknown keys exist', () => {
|
|
const agent: AgentDefinition = {
|
|
id: 'no-extra',
|
|
name: 'no-extra',
|
|
description: 'Standard agent',
|
|
prompt: 'Do stuff.',
|
|
source: 'vault',
|
|
};
|
|
|
|
const serialized = serializeAgent(agent);
|
|
const parsed = parseAgentFile(serialized);
|
|
expect(parsed).not.toBeNull();
|
|
|
|
const rebuilt = buildAgentFromFrontmatter(parsed!.frontmatter, parsed!.body, {
|
|
id: parsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt.extraFrontmatter).toBeUndefined();
|
|
});
|
|
|
|
it('round-trips description with special YAML characters', () => {
|
|
const agent: AgentDefinition = {
|
|
id: 'special-desc',
|
|
name: 'special-desc',
|
|
description: 'Test: agent with #special chars',
|
|
prompt: 'Handle edge cases.',
|
|
source: 'vault',
|
|
};
|
|
|
|
const serialized = serializeAgent(agent);
|
|
const parsed = parseAgentFile(serialized);
|
|
expect(parsed).not.toBeNull();
|
|
|
|
const rebuilt = buildAgentFromFrontmatter(parsed!.frontmatter, parsed!.body, {
|
|
id: parsed!.frontmatter.name,
|
|
source: 'vault',
|
|
});
|
|
|
|
expect(rebuilt.description).toBe(agent.description);
|
|
});
|
|
});
|