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