feat: update Makelore modules and conversations
This commit is contained in:
@@ -1,264 +1,133 @@
|
||||
import { mkdtemp, readFile, readdir, rm } from 'node:fs/promises';
|
||||
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, projectTemplates, validateAgentNames } from '../../shared/project-config';
|
||||
import { createInitialProjectConfig, normalizeProjectConfig, readProjectConfig, writeProjectConfig } from '../../electron/opencode/project-config';
|
||||
import {
|
||||
createProjectConfig,
|
||||
validateAgentConfigs,
|
||||
type ProjectAgentConfig,
|
||||
} from '../../shared/project-config';
|
||||
import {
|
||||
createInitialProjectConfig,
|
||||
normalizeProjectConfig,
|
||||
readProjectConfig,
|
||||
writeProjectConfig,
|
||||
} from '../../electron/opencode/project-config';
|
||||
|
||||
describe('project-owned configuration', () => {
|
||||
it('exposes the replacement templates with four standard and five game Agents', () => {
|
||||
expect(projectTemplates.map((template) => template.id)).toEqual(['standard-development', 'game-development']);
|
||||
expect(projectTemplates.find((template) => template.id === 'standard-development')?.agents).toHaveLength(4);
|
||||
const gameAgents = projectTemplates.find((template) => template.id === 'game-development')?.agents ?? [];
|
||||
expect(gameAgents).toHaveLength(5);
|
||||
expect(gameAgents[gameAgents.length - 1]).toMatchObject({ id: 'game-promotion', roleName: '运营宣传角色' });
|
||||
expect(projectTemplates.flatMap((template) => template.agents).every((agent) => agent.builtIn)).toBe(true);
|
||||
expect(projectTemplates.flatMap((template) => template.agents).every((agent) => agent.skillIds.includes('youth-plain-language'))).toBe(true);
|
||||
});
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
it('creates an incomplete config and validates unique required names', () => {
|
||||
const config = createProjectConfig('standard-development', '2026-07-11T00:00:00.000Z');
|
||||
expect(config.initialized).toBe(false);
|
||||
expect(config.superpowersEnabled).toBe(true);
|
||||
expect(config.knowledgeDirectory).toBe('knowledge');
|
||||
expect(validateAgentNames(config.agents)).toContain('product-planning:name-required');
|
||||
config.agents.forEach((agent, index) => { agent.name = `伙伴${index + 1}`; });
|
||||
expect(validateAgentNames(config.agents)).toEqual([]);
|
||||
config.agents[1]!.name = config.agents[0]!.name;
|
||||
expect(validateAgentNames(config.agents)).toContain(`${config.agents[1]!.id}:name-duplicate`);
|
||||
const custom = { ...structuredClone(config.agents[0]!), id: 'custom-agent', builtIn: false, name: '' };
|
||||
expect(validateAgentNames([...config.agents, custom])).toContain('custom-agent:name-required');
|
||||
});
|
||||
|
||||
it('uses template defaults for Superpowers and restores them when the field is absent', () => {
|
||||
const gameConfig = createProjectConfig('game-development');
|
||||
expect(gameConfig.superpowersEnabled).toBe(false);
|
||||
expect(normalizeProjectConfig({ ...gameConfig, superpowersEnabled: undefined })).toMatchObject({
|
||||
templateId: 'game-development',
|
||||
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 })],
|
||||
});
|
||||
|
||||
const standardConfig = createProjectConfig('standard-development');
|
||||
expect(standardConfig.superpowersEnabled).toBe(true);
|
||||
expect(normalizeProjectConfig({ ...standardConfig, superpowersEnabled: undefined })).toMatchObject({
|
||||
templateId: 'standard-development',
|
||||
superpowersEnabled: 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('materializes project-only runtime prompts after initialization', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-config-'));
|
||||
const initial = await createInitialProjectConfig(projectPath, 'game-development');
|
||||
initial.agents.forEach((agent, index) => { agent.name = `游戏伙伴${index + 1}`; });
|
||||
initial.agents[1]!.enabled = false;
|
||||
expect((await readProjectConfig(projectPath)).status).toBe('valid');
|
||||
const saved = await writeProjectConfig(projectPath, { ...initial, initialized: true });
|
||||
expect(saved.initialized).toBe(true);
|
||||
expect(saved.agents.every((agent) => agent.enabled)).toBe(true);
|
||||
const markdown = await readFile(path.join(projectPath, '.opencode', 'agent', 'game-art.md'), 'utf8');
|
||||
const worksSquareIndex = markdown.indexOf('https://square.nianxx.cn/api/assets');
|
||||
const kenneyIndex = markdown.indexOf('Kenney');
|
||||
expect(worksSquareIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(kenneyIndex).toBeGreaterThan(worksSquareIndex);
|
||||
expect(markdown).toContain('limit');
|
||||
expect(markdown).toContain('offset');
|
||||
expect(markdown).toContain('# 游戏伙伴2 · 游戏素材');
|
||||
expect(markdown).toContain('你的名字是「游戏伙伴2」');
|
||||
expect(markdown).toContain('Kenney');
|
||||
expect(markdown).toContain('OpenGameArt');
|
||||
expect(markdown).toContain('ASSET_PLAN.md');
|
||||
expect(markdown).toContain('ART_GUIDE.md');
|
||||
expect(markdown).toContain('项目目录是所有 Agent 共享的长期上下文');
|
||||
expect(markdown).toContain('聊天里的口头总结不能代替项目产物');
|
||||
expect(markdown).toContain('缺少上游产物时不要把它当作阶段门禁');
|
||||
expect(markdown).toContain('不依赖自动派发或跨会话消息');
|
||||
expect(markdown).not.toContain('selectionStatus');
|
||||
expect(markdown).not.toContain('必须停止');
|
||||
expect(markdown).toContain('game-design');
|
||||
expect(markdown).toContain('knowledge/');
|
||||
expect(markdown).toContain('nianxxgame-skill: allow');
|
||||
expect(markdown).toContain('game-assets: allow');
|
||||
expect(markdown).toContain('bash: allow');
|
||||
expect(markdown).toContain('youth-plain-language: allow');
|
||||
expect(markdown).toContain('10 至 16 岁青少年能直接看懂');
|
||||
expect(markdown).toContain('每次开始工作前读取 `VERSION.md` 和 `TASKS.md`');
|
||||
expect(markdown).toContain('所有 Agent 共同维护的当前版本任务中枢');
|
||||
expect(markdown).toContain('Document Revision');
|
||||
expect(markdown).toContain('只能在 `TASKS.md` 提议 PATCH、MINOR 或 MAJOR');
|
||||
|
||||
const version = await readFile(path.join(projectPath, 'VERSION.md'), 'utf8');
|
||||
const tasks = await readFile(path.join(projectPath, 'TASKS.md'), 'utf8');
|
||||
const gdd = await readFile(path.join(projectPath, 'GDD.md'), 'utf8');
|
||||
const assetPlan = await readFile(path.join(projectPath, 'ASSET_PLAN.md'), 'utf8');
|
||||
const artGuide = await readFile(path.join(projectPath, 'ART_GUIDE.md'), 'utf8');
|
||||
expect(version).toContain('Current: v0.1.0');
|
||||
expect(version).toContain('Version Owner: game-design');
|
||||
expect(version).toContain('Status: active');
|
||||
expect(tasks).toContain('Project Version: v0.1.0');
|
||||
expect(tasks).toContain('所有 Agent 共同读取和维护本文件');
|
||||
expect(tasks).toContain('TASK-002 建立素材清单和接入策略');
|
||||
expect(tasks).toContain('TASK-003 确认 2D/3D 渲染路径');
|
||||
expect(tasks).toContain('## Version Proposals');
|
||||
expect(gdd).toContain('首个可试玩结果');
|
||||
expect(gdd).toContain('2D(默认)或 3D');
|
||||
const techStack = await readFile(path.join(projectPath, 'TECH_STACK.md'), 'utf8');
|
||||
expect(assetPlan).toContain('game-assets Skill');
|
||||
expect(assetPlan).toContain('config-status');
|
||||
expect(assetPlan).toContain('assets/generated/meowa/<run>/');
|
||||
expect(assetPlan).toContain('generation.meta.json');
|
||||
expect(assetPlan).toContain('"assets": []');
|
||||
expect(assetPlan).not.toMatch(/每日.{0,30}10 次|每日额度|quota\.remaining|剩余额度|额度耗尽/);
|
||||
expect(assetPlan).toContain('不把 2D 生成图伪装成 3D 模型');
|
||||
expect(assetPlan).toContain('GLB/glTF');
|
||||
expect(techStack).toContain('Three.js + TypeScript');
|
||||
expect(techStack).toContain('3D 模型不走 Meowa 生成链路');
|
||||
expect(artGuide).toContain('nearest-neighbor');
|
||||
expect(artGuide).toContain('GLB/glTF');
|
||||
expect(artGuide).not.toMatch(/每日.{0,30}10 次|每日额度|quota\.remaining|剩余额度|额度不足/);
|
||||
expect(await readdir(path.join(projectPath, 'assets'))).toEqual(expect.arrayContaining(['generated', 'review-previews']));
|
||||
expect(await readdir(path.join(projectPath, 'assets', 'generated'))).toContain('meowa');
|
||||
expect(await readdir(path.join(projectPath, 'assets'))).toContain('models');
|
||||
expect(await readdir(path.join(projectPath, 'public'))).toContain('assets');
|
||||
expect(await readdir(path.join(projectPath, 'public'))).toContain('models');
|
||||
|
||||
const ownerMarkdown = await readFile(path.join(projectPath, '.opencode', 'agent', 'game-design.md'), 'utf8');
|
||||
expect(ownerMarkdown).toContain('## 版本负责人权限');
|
||||
expect(ownerMarkdown).toContain('只归档本版本变化的文档');
|
||||
expect(ownerMarkdown).toContain('未完成任务带来源迁移到下一版本');
|
||||
const promotionMarkdown = await readFile(path.join(projectPath, '.opencode', 'agent', 'game-promotion.md'), 'utf8');
|
||||
expect(promotionMarkdown).toContain('# 游戏伙伴5 · 运营宣传角色');
|
||||
expect(promotionMarkdown).toContain('PRODUCT_OVERVIEW.md');
|
||||
expect(promotionMarkdown).toContain('不是投放排期、预算表或项目任务计划');
|
||||
expect(promotionMarkdown).toContain('marketing-launch-story: allow');
|
||||
});
|
||||
|
||||
it('gives every game Agent explicit shared-artifact inputs and owned outputs', () => {
|
||||
const agents = createProjectConfig('game-development').agents;
|
||||
const promptById = Object.fromEntries(agents.map((agent) => [agent.id, agent.prompt]));
|
||||
const releaseAgent = agents.find((agent) => agent.id === 'game-test-release');
|
||||
const promotionAgent = agents.find((agent) => agent.id === 'game-promotion');
|
||||
expect(releaseAgent?.skillIds).toContain('deploy-publish-check');
|
||||
expect(promotionAgent?.skillIds).toEqual(expect.arrayContaining(['nianxxgame-skill', 'marketing-launch-story', 'youth-plain-language']));
|
||||
expect(promptById['game-design']).toContain('每次策划工作都更新 GDD.md');
|
||||
expect(promptById['game-design']).toContain('2D/3D 渲染选择');
|
||||
expect(promptById['game-design']).toContain('读取 TECH_STACK.md');
|
||||
expect(promptById['game-design']).toContain('不直接运行 game-assets 或请求 Meowa');
|
||||
expect(promptById['game-art']).toContain('每次素材工作都更新 ASSET_PLAN.md');
|
||||
expect(promptById['game-art']).toContain('game-assets');
|
||||
expect(promptById['game-art']).toContain('唯一直接调用 game-assets 的 Agent');
|
||||
expect(promptById['game-art']).toContain('读取 TECH_STACK.md');
|
||||
expect(promptById['game-art']).toContain('3D 模型改走项目、用户或许可证明确的外部来源');
|
||||
expect(promptById['game-art']).not.toMatch(/每日.{0,30}10 次|每日额度|quota\.remaining|剩余额度|额度耗尽/);
|
||||
expect(promptById['game-development']).toContain('每次开发工作都更新代码与 TASKS.md');
|
||||
expect(promptById['game-development']).toContain('不直接运行 game-assets 或绕过素材审核');
|
||||
expect(promptById['game-development']).toContain('2D Phaser 或 3D Three.js 路径');
|
||||
expect(promptById['game-development']).toContain('不得把 3D 需求静默降级为 2D');
|
||||
expect(promptById['game-test-release']).toContain('每次测试发布工作都更新 TEST_REPORT.md');
|
||||
expect(promptById['game-test-release']).toContain('不为缺失素材自行调用 game-assets 或生成替代品');
|
||||
expect(promptById['game-test-release']).toContain('按 TECH_STACK.md 检查 2D 或 3D 路径');
|
||||
expect(promptById['game-promotion']).toContain('每次工作都更新 PRODUCT_OVERVIEW.md');
|
||||
expect(promptById['game-promotion']).toContain('产品运营介绍');
|
||||
expect(promptById['game-promotion']).toContain('不是投放排期、预算表或项目任务计划');
|
||||
expect(promptById['game-promotion']).toContain('不自行宣称游戏已上线');
|
||||
expect(promptById['game-promotion']).not.toContain('game-assets');
|
||||
expect(promptById['game-promotion']).not.toContain('Meowa');
|
||||
for (const prompt of Object.values(promptById)) {
|
||||
expect(prompt).toContain('项目目录是所有 Agent 共享的长期上下文');
|
||||
expect(prompt).toContain('聊天里的口头总结不能代替项目产物');
|
||||
}
|
||||
});
|
||||
|
||||
it('documents the Meowa handoff boundary in the bundled game Skills', async () => {
|
||||
const gameSkill = await readFile(path.join(process.cwd(), '.opencode', 'skills', 'nianxxgame-skill', 'SKILL.md'), 'utf8');
|
||||
const assetSkill = await readFile(path.join(process.cwd(), '.opencode', 'skills', 'game-assets', 'SKILL.md'), 'utf8');
|
||||
const nituSource = await readFile(path.join(process.cwd(), 'src', 'pages', 'NiTu', 'index.tsx'), 'utf8');
|
||||
|
||||
expect(gameSkill).toContain('game-art` is the only role that directly uses');
|
||||
expect(gameSkill).toContain('game-development` integrates only assets whose project-local review state is `approved`');
|
||||
expect(gameSkill).toContain('The product-operations role only describes assets');
|
||||
expect(gameSkill).toContain('Three.js with TypeScript');
|
||||
expect(gameSkill).toContain('references/web3d-standards.md');
|
||||
expect(gameSkill).toContain('Meowa `game-assets` currently generates 2D pixel/HD raster assets only');
|
||||
expect(assetSkill).toContain('`game-art` 是唯一直接调用本 Skill 的 Agent');
|
||||
expect(assetSkill).toContain('`game-promotion` 不属于素材生成链路');
|
||||
expect(assetSkill).toContain('当前 Meowa 适配器只生成 2D 像素/高清光栅素材,不生成 3D 模型');
|
||||
expect(assetSkill).not.toMatch(/每日.{0,30}10 次|每日额度|quota\.remaining|剩余额度|额度耗尽/);
|
||||
expect(nituSource).not.toMatch(/每日.{0,30}10 次|每日额度|quota\.remaining|剩余额度/);
|
||||
});
|
||||
|
||||
it('runs the bundled Meowa adapter with NianCode managed Python', async () => {
|
||||
const assetSkill = await readFile(path.join(process.cwd(), '.opencode', 'skills', 'game-assets', 'SKILL.md'), 'utf8');
|
||||
const adapterGuide = await readFile(path.join(process.cwd(), '.opencode', 'skills', 'game-assets', 'meowart_api.md'), 'utf8');
|
||||
|
||||
expect(assetSkill).toContain('"$NIANCODE_PYTHON_PATH" "$MEOWA_GAME_ASSETS_CLI"');
|
||||
expect(assetSkill).not.toMatch(/\bpython3\s+"\$MEOWA_GAME_ASSETS_CLI"/);
|
||||
expect(adapterGuide).toContain('"$NIANCODE_PYTHON_PATH" meowart_api.py');
|
||||
expect(adapterGuide).not.toMatch(/^python3 meowart_api\.py/gm);
|
||||
expect(adapterGuide).not.toMatch(/每日.{0,30}10 次|每日额度|quota\.remaining|剩余额度|额度耗尽/);
|
||||
});
|
||||
|
||||
it('does not backfill the promotion Agent into an existing saved game config', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-config-no-migration-'));
|
||||
const initial = await createInitialProjectConfig(projectPath, 'game-development');
|
||||
const legacyAgents = initial.agents.filter((agent) => agent.id !== 'game-promotion');
|
||||
await writeProjectConfig(projectPath, { ...initial, agents: legacyAgents, initialized: false });
|
||||
|
||||
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.status).toBe('valid');
|
||||
if (loaded.status !== 'valid') throw new Error('Expected a valid project config');
|
||||
expect(loaded.config.agents.map((agent) => agent.id)).toEqual(['game-design', 'game-art', 'game-development', 'game-test-release']);
|
||||
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('does not backfill game starter documents when an existing project is saved', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-config-no-document-migration-'));
|
||||
const initial = await createInitialProjectConfig(projectPath, 'game-development');
|
||||
await rm(path.join(projectPath, 'ASSET_PLAN.md'));
|
||||
|
||||
await writeProjectConfig(projectPath, { ...initial, initialized: false });
|
||||
|
||||
await expect(readFile(path.join(projectPath, 'ASSET_PLAN.md'), 'utf8')).rejects.toMatchObject({ code: 'ENOENT' });
|
||||
});
|
||||
|
||||
it('binds the deployment check Skill to standard and game release Agents', () => {
|
||||
const standardRelease = createProjectConfig('standard-development').agents.find((agent) => agent.id === 'release-maintenance');
|
||||
const gameRelease = createProjectConfig('game-development').agents.find((agent) => agent.id === 'game-test-release');
|
||||
expect(standardRelease?.skillIds).toContain('deploy-publish-check');
|
||||
expect(gameRelease?.skillIds).toContain('deploy-publish-check');
|
||||
expect(standardRelease?.prompt).toContain('静态安全检查或真实执行失败时不允许云端提交');
|
||||
expect(gameRelease?.prompt).toContain('真实失败输出 BLOCKED');
|
||||
expect(standardRelease?.prompt).toContain('SKIPPED');
|
||||
expect(gameRelease?.prompt).toContain('任何失败输出 BLOCKED');
|
||||
expect(gameRelease?.prompt).toContain('SKIPPED');
|
||||
});
|
||||
|
||||
it('refuses to initialize invalid built-in Agent names', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-project-config-invalid-'));
|
||||
const initial = await createInitialProjectConfig(projectPath, 'standard-development');
|
||||
await expect(writeProjectConfig(projectPath, { ...initial, initialized: true })).rejects.toThrow('invalid Agent names');
|
||||
});
|
||||
|
||||
it('restores the required youth language Skill when saved config omits it', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-required-language-skill-'));
|
||||
const initial = await createInitialProjectConfig(projectPath, 'standard-development');
|
||||
initial.agents.forEach((agent, index) => {
|
||||
agent.name = `通俗表达伙伴${index + 1}`;
|
||||
agent.skillIds = [];
|
||||
});
|
||||
|
||||
const saved = await writeProjectConfig(projectPath, { ...initial, initialized: true });
|
||||
expect(saved.agents.every((agent) => agent.skillIds.includes('youth-plain-language'))).toBe(true);
|
||||
expect(await readFile(path.join(projectPath, '.opencode', 'agent', 'development.md'), 'utf8')).toContain('youth-plain-language: allow');
|
||||
});
|
||||
|
||||
it('uses product planning as the standard project version owner', async () => {
|
||||
const projectPath = await mkdtemp(path.join(tmpdir(), 'niancode-standard-version-'));
|
||||
const initial = await createInitialProjectConfig(projectPath, 'standard-development');
|
||||
initial.agents.forEach((agent, index) => { agent.name = `标准伙伴${index + 1}`; });
|
||||
await writeProjectConfig(projectPath, { ...initial, initialized: true });
|
||||
|
||||
expect(await readFile(path.join(projectPath, 'VERSION.md'), 'utf8')).toContain('Version Owner: product-planning');
|
||||
expect(await readFile(path.join(projectPath, '.opencode', 'agent', 'product-planning.md'), 'utf8')).toContain('## 版本负责人权限');
|
||||
expect(await readFile(path.join(projectPath, '.opencode', 'agent', 'development.md'), 'utf8')).not.toContain('## 版本负责人权限');
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user