项目文件夹

文件
wehub-resource-sync 2771cff92b
CI / lint (push) Failing after 1s
CI / typecheck (push) Failing after 2s
CI / test (push) Failing after 1s
CI / build (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:22:56 +08:00

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);
});
});