134 lines
5.2 KiB
TypeScript
134 lines
5.2 KiB
TypeScript
import { mkdtemp, readFile, readdir } from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
createProjectConfig,
|
|
validateAgentConfigs,
|
|
type ProjectAgentConfig,
|
|
} from '../../shared/project-config';
|
|
import {
|
|
createInitialProjectConfig,
|
|
normalizeProjectConfig,
|
|
readProjectConfig,
|
|
writeProjectConfig,
|
|
} from '../../electron/opencode/project-config';
|
|
|
|
function createContact(overrides: Partial<ProjectAgentConfig> = {}): ProjectAgentConfig {
|
|
const now = '2026-07-11T00:00:00.000Z';
|
|
return {
|
|
id: 'agent-test',
|
|
avatarId: 'avatar-01',
|
|
roleName: '项目伙伴',
|
|
name: '小明',
|
|
builtIn: false,
|
|
enabled: true,
|
|
model: 'openai/gpt-4o-mini',
|
|
skillIds: [],
|
|
responsibility: {
|
|
mission: '把用户的想法整理成清晰的下一步。',
|
|
owns: [],
|
|
boundaries: [],
|
|
collaborators: [],
|
|
principles: [],
|
|
},
|
|
prompt: '',
|
|
archivedAt: null,
|
|
pinned: false,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('project-owned contact configuration', () => {
|
|
it('creates an empty, template-free project config', () => {
|
|
const config = createProjectConfig('2026-07-11T00:00:00.000Z');
|
|
expect(config).toMatchObject({
|
|
initialized: false,
|
|
superpowersEnabled: false,
|
|
agents: [],
|
|
knowledgeDirectory: 'knowledge',
|
|
});
|
|
expect('templateId' in config).toBe(false);
|
|
});
|
|
|
|
it('requires a unique name, preset avatar, model, and responsibility for every contact', () => {
|
|
expect(validateAgentConfigs([])).toEqual([]);
|
|
expect(validateAgentConfigs([createContact({ name: '' })])).toContain('agent-test:name-required');
|
|
expect(validateAgentConfigs([createContact({ avatarId: 'upload-me' })])).toContain('agent-test:avatar-required');
|
|
expect(validateAgentConfigs([createContact({ model: null })])).toContain('agent-test:model-required');
|
|
expect(validateAgentConfigs([createContact({ responsibility: { mission: '', owns: [], boundaries: [], collaborators: [], principles: [] } })])).toContain('agent-test:responsibility-required');
|
|
expect(validateAgentConfigs([
|
|
createContact(),
|
|
createContact({ id: 'agent-test-2', name: '小明' }),
|
|
])).toContain('agent-test-2:name-duplicate');
|
|
expect(validateAgentConfigs([createContact()])).toEqual([]);
|
|
});
|
|
|
|
it('normalizes a manually configured contact without injecting skills or a built-in role', () => {
|
|
const config = createProjectConfig();
|
|
const normalized = normalizeProjectConfig({
|
|
...config,
|
|
initialized: true,
|
|
agents: [createContact()],
|
|
superpowersEnabled: undefined,
|
|
});
|
|
expect(normalized).toMatchObject({
|
|
superpowersEnabled: false,
|
|
agents: [{ builtIn: false, prompt: '', skillIds: [], model: 'openai/gpt-4o-mini' }],
|
|
});
|
|
expect('templateId' in normalized).toBe(false);
|
|
});
|
|
|
|
it('preserves the legacy built-in marker when reading an existing project', () => {
|
|
const config = createProjectConfig();
|
|
const normalized = normalizeProjectConfig({
|
|
...config,
|
|
initialized: true,
|
|
agents: [createContact({ builtIn: true })],
|
|
});
|
|
|
|
expect(normalized.agents[0]?.builtIn).toBe(true);
|
|
});
|
|
|
|
it('materializes a custom contact with its exact model and prompt', async () => {
|
|
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-'));
|
|
const initial = await createInitialProjectConfig(projectPath);
|
|
const saved = await writeProjectConfig(projectPath, {
|
|
...initial,
|
|
initialized: true,
|
|
agents: [createContact()],
|
|
});
|
|
|
|
expect(saved.agents).toHaveLength(1);
|
|
const markdown = await readFile(path.join(projectPath, '.opencode', 'agent', 'agent-test.md'), 'utf8');
|
|
expect(markdown).toContain('model: "openai/gpt-4o-mini"');
|
|
expect(markdown).toContain('description: "小明"');
|
|
expect(markdown).toContain('把用户的想法整理成清晰的下一步');
|
|
expect(markdown).not.toContain('youth-plain-language: allow');
|
|
expect(markdown).toContain('"*": deny');
|
|
});
|
|
|
|
it('does not install default Agent files or template documents in a new project', async () => {
|
|
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-empty-'));
|
|
const initial = await createInitialProjectConfig(projectPath);
|
|
const loaded = await readProjectConfig(projectPath);
|
|
expect(loaded).toMatchObject({ status: 'valid', config: { agents: [], initialized: false } });
|
|
await expect(readdir(path.join(projectPath, '.opencode', 'agent'))).rejects.toMatchObject({ code: 'ENOENT' });
|
|
expect(initial.agents).toEqual([]);
|
|
expect(await readFile(path.join(projectPath, 'VERSION.md'), 'utf8')).toContain('Version Owner: user');
|
|
expect(await readFile(path.join(projectPath, 'TASKS.md'), 'utf8')).toContain('Owner: user');
|
|
});
|
|
|
|
it('rejects an initialized contact with missing required basics at save time', async () => {
|
|
const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-project-config-invalid-'));
|
|
const initial = await createInitialProjectConfig(projectPath);
|
|
await expect(writeProjectConfig(projectPath, {
|
|
...initial,
|
|
initialized: true,
|
|
agents: [createContact({ model: null })],
|
|
})).rejects.toThrow('model-required');
|
|
});
|
|
});
|