494 lines
22 KiB
TypeScript
494 lines
22 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
builtInProjectTemplates,
|
||
createProjectTemplateSnapshot,
|
||
getProjectTemplateRequiredSkillIds,
|
||
hasProjectCapability,
|
||
type ProjectTemplateCourseStage,
|
||
validateProjectTemplateSnapshot,
|
||
} from '../../shared/project-template';
|
||
import { isCourseProjectTemplate } from '../../src/lib/project-templates';
|
||
|
||
const LEGACY_YOUTH_DEPLOY_STAGE: ProjectTemplateCourseStage = {
|
||
id: 'deploy',
|
||
order: 6,
|
||
title: '发布部署',
|
||
agentName: '部署工程师',
|
||
agentRole: '发布部署',
|
||
goal: '确认作品可以通过 Docker 构建、启动、打包并通过作品广场上传接口提交。',
|
||
task: '创建或维护 Dockerfile 和 docker-compose.yml,检查 Docker 构建状态,把当前项目打成 zip 包,通过作品广场上传接口提交,并记录发布目标、发布步骤、回滚说明和作品广场提交清单。',
|
||
deliverableTitle: '部署报告.md',
|
||
deliverableType: 'report',
|
||
deliverableSummary: 'Dockerfile、docker-compose.yml、发布目标、准备情况、Docker 执行命令、zip 包路径、作品广场上传接口结果、发布步骤、回滚步骤。',
|
||
acceptanceCriteria: [
|
||
'有明确发布目标',
|
||
'有 Dockerfile',
|
||
'有 docker-compose.yml',
|
||
'有 Docker 构建或检查结果',
|
||
'有 zip 包路径',
|
||
'有作品广场上传接口结果',
|
||
'有发布步骤',
|
||
'有回滚步骤',
|
||
'没有隐私或密钥风险',
|
||
],
|
||
handoffTarget: null,
|
||
};
|
||
|
||
const LEGACY_GAME_RELEASE_STAGE: ProjectTemplateCourseStage = {
|
||
id: 'game-release',
|
||
order: 4,
|
||
title: '发布打包',
|
||
agentName: '游戏发布官',
|
||
agentRole: '游戏发布官',
|
||
goal: '准备构建结果、试玩记录和发布检查清单。',
|
||
task: '记录如何运行游戏、试玩了什么,以及发布前后需要检查哪些项目。',
|
||
deliverableTitle: 'RELEASE_CHECKLIST.md',
|
||
deliverableType: 'report',
|
||
deliverableSummary: '构建结果、试玩检查、运行说明、发布检查项和已知风险。',
|
||
acceptanceCriteria: [
|
||
'包含构建命令和结果',
|
||
'包含试玩记录',
|
||
'包含运行说明',
|
||
'包含发布检查项',
|
||
'包含已知风险或问题',
|
||
],
|
||
handoffTarget: 'game-showcase',
|
||
};
|
||
|
||
function replacePublishStageWithLegacy(
|
||
snapshot: ReturnType<typeof createProjectTemplateSnapshot>,
|
||
legacyStage: ProjectTemplateCourseStage,
|
||
) {
|
||
if (!snapshot.capabilities.course.enabled) throw new Error('Expected course capability');
|
||
snapshot.capabilities.course.stages = snapshot.capabilities.course.stages.map((stage) => (
|
||
stage.id === legacyStage.id ? structuredClone(legacyStage) : stage
|
||
));
|
||
snapshot.capabilities.course.slots = snapshot.capabilities.course.slots.map((slot) => (
|
||
slot.id === legacyStage.id ? { ...slot, goal: legacyStage.goal } : slot
|
||
));
|
||
}
|
||
|
||
describe('project template snapshots', () => {
|
||
it('creates gate-free built-in snapshots with static course metadata and Chinese display text', () => {
|
||
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
|
||
|
||
expect(snapshot.schemaVersion).toBe(1);
|
||
expect(snapshot.template).toMatchObject({
|
||
id: 'youth-ai-course',
|
||
version: 1,
|
||
title: 'AI 伙伴课程项目',
|
||
createdAt: '2026-07-01T00:00:00.000Z',
|
||
});
|
||
expect(snapshot.capabilities.chat.enabled).toBe(true);
|
||
expect(snapshot.capabilities.chat.defaultRoute).toBe('/opencode-chat');
|
||
expect(snapshot.capabilities.course.enabled).toBe(true);
|
||
expect(snapshot.capabilities.publish.enabled).toBe(true);
|
||
|
||
if (snapshot.capabilities.course.enabled) {
|
||
expect(snapshot.capabilities.course.stages).toHaveLength(6);
|
||
expect(snapshot.capabilities.course.slots).toHaveLength(6);
|
||
expect(snapshot.capabilities.course).not.toHaveProperty('reviewer');
|
||
expect(snapshot.capabilities.course).not.toHaveProperty('partnerAgent');
|
||
expect(snapshot.capabilities.course).not.toHaveProperty('reviewPolicy');
|
||
expect(snapshot.capabilities.course).not.toHaveProperty('completionUnlock');
|
||
for (const stage of snapshot.capabilities.course.stages) {
|
||
const slot = snapshot.capabilities.course.slots.find((candidate) => candidate.id === stage.id);
|
||
const snapshotStage = snapshot.capabilities.course.stages.find((candidate) => candidate.id === stage.id);
|
||
|
||
expect(slot).toMatchObject({
|
||
id: stage.id,
|
||
title: stage.title,
|
||
agentName: stage.agentName,
|
||
agentRole: stage.agentRole,
|
||
recommendedAgentProfile: {
|
||
roleId: stage.id,
|
||
displayName: stage.agentName,
|
||
notes: [],
|
||
},
|
||
});
|
||
expect(snapshotStage).toMatchObject({
|
||
id: stage.id,
|
||
order: stage.order,
|
||
title: stage.title,
|
||
agentName: stage.agentName,
|
||
agentRole: stage.agentRole,
|
||
deliverableTitle: stage.deliverableTitle,
|
||
deliverableType: stage.deliverableType,
|
||
handoffTarget: stage.handoffTarget,
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
it('keeps standard development projects out of course and publish capabilities', () => {
|
||
const snapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
|
||
|
||
expect(snapshot.template.title).toBe('普通开发项目');
|
||
expect(hasProjectCapability(snapshot, 'chat')).toBe(true);
|
||
expect(hasProjectCapability(snapshot, 'course')).toBe(false);
|
||
expect(hasProjectCapability(snapshot, 'publish')).toBe(false);
|
||
expect(hasProjectCapability(snapshot, 'gameDevelopment')).toBe(false);
|
||
});
|
||
|
||
it('normalizes legacy youth course snapshots that still point at the retired subagents route', () => {
|
||
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
|
||
snapshot.capabilities.chat.defaultRoute = '/subagents';
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(true);
|
||
if (result.ok) {
|
||
expect(result.snapshot.capabilities.chat.defaultRoute).toBe('/opencode-chat');
|
||
}
|
||
});
|
||
|
||
it('normalizes the exact legacy youth deploy contract after route and scaffold compatibility', () => {
|
||
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
|
||
snapshot.capabilities.chat.defaultRoute = '/subagents';
|
||
delete snapshot.capabilities.scaffold;
|
||
replacePublishStageWithLegacy(snapshot, LEGACY_YOUTH_DEPLOY_STAGE);
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(true);
|
||
if (result.ok && result.snapshot.capabilities.course.enabled) {
|
||
const stage = result.snapshot.capabilities.course.stages.find((item) => item.id === 'deploy');
|
||
expect(stage?.acceptanceCriteria).toContain('没有失败/阻塞或未验证的必需发布证据');
|
||
expect(result.snapshot.capabilities.chat.defaultRoute).toBe('/opencode-chat');
|
||
expect(result.snapshot.capabilities.scaffold).toEqual({ enabled: false });
|
||
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
|
||
}
|
||
});
|
||
|
||
it('normalizes the exact five-stage legacy game release delivery contract', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
replacePublishStageWithLegacy(snapshot, LEGACY_GAME_RELEASE_STAGE);
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(true);
|
||
if (result.ok && result.snapshot.capabilities.course.enabled) {
|
||
const stage = result.snapshot.capabilities.course.stages.find((item) => item.id === 'game-release');
|
||
expect(stage?.acceptanceCriteria).toContain('没有失败/阻塞或未验证的必需发布证据');
|
||
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
|
||
}
|
||
});
|
||
|
||
it('accepts legacy built-in snapshots without scaffold when the built-in scaffold is disabled', () => {
|
||
const standardSnapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
|
||
const youthSnapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
|
||
|
||
delete standardSnapshot.capabilities.scaffold;
|
||
delete youthSnapshot.capabilities.scaffold;
|
||
|
||
const standardResult = validateProjectTemplateSnapshot(standardSnapshot);
|
||
const youthResult = validateProjectTemplateSnapshot(youthSnapshot);
|
||
|
||
expect(standardResult.ok).toBe(true);
|
||
expect(youthResult.ok).toBe(true);
|
||
if (standardResult.ok) {
|
||
expect(standardResult.snapshot.capabilities.scaffold).toEqual({ enabled: false });
|
||
}
|
||
if (youthResult.ok) {
|
||
expect(youthResult.snapshot.capabilities.scaffold).toEqual({ enabled: false });
|
||
}
|
||
});
|
||
|
||
it('accepts built-in mini-game course snapshots with non-youth stage ids', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(true);
|
||
});
|
||
|
||
it('normalizes legacy mini-game snapshots that predate the showcase stage', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
|
||
snapshot.template.version = 1;
|
||
snapshot.capabilities.course.stages = snapshot.capabilities.course.stages.filter((stage) => stage.id !== 'game-showcase');
|
||
snapshot.capabilities.course.slots = snapshot.capabilities.course.slots.filter((slot) => slot.id !== 'game-showcase');
|
||
const releaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release');
|
||
if (!releaseStage) throw new Error('Expected release stage');
|
||
releaseStage.handoffTarget = null;
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(true);
|
||
if (result.ok && result.snapshot.capabilities.course.enabled) {
|
||
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
|
||
expect(result.snapshot.capabilities.course.stages.map((stage) => stage.id)).toEqual([
|
||
'gameplay',
|
||
'assets',
|
||
'game-dev',
|
||
'game-release',
|
||
'game-showcase',
|
||
]);
|
||
expect(result.snapshot.capabilities.course.slots.map((slot) => slot.id)).toContain('game-showcase');
|
||
expect(result.snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release')).toMatchObject({
|
||
handoffTarget: 'game-showcase',
|
||
});
|
||
}
|
||
});
|
||
|
||
it('normalizes a four-stage mini-game snapshot with the frozen legacy release delivery', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
replacePublishStageWithLegacy(snapshot, { ...LEGACY_GAME_RELEASE_STAGE, handoffTarget: null });
|
||
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
|
||
snapshot.capabilities.course.stages = snapshot.capabilities.course.stages.filter((stage) => stage.id !== 'game-showcase');
|
||
snapshot.capabilities.course.slots = snapshot.capabilities.course.slots.filter((slot) => slot.id !== 'game-showcase');
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(true);
|
||
if (result.ok && result.snapshot.capabilities.course.enabled) {
|
||
const stage = result.snapshot.capabilities.course.stages.find((item) => item.id === 'game-release');
|
||
expect(stage?.acceptanceCriteria).toContain('没有失败/阻塞或未验证的必需发布证据');
|
||
expect(stage?.handoffTarget).toBe('game-showcase');
|
||
expect(result.snapshot.capabilities.course.stages.map((item) => item.id)).toContain('game-showcase');
|
||
expect(result.snapshot.template.createdAt).toBe('2026-07-01T00:00:00.000Z');
|
||
}
|
||
});
|
||
|
||
it('rejects a near-match legacy delivery snapshot with an unrelated stage tamper', () => {
|
||
const snapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
|
||
snapshot.capabilities.chat.defaultRoute = '/subagents';
|
||
delete snapshot.capabilities.scaffold;
|
||
replacePublishStageWithLegacy(snapshot, LEGACY_YOUTH_DEPLOY_STAGE);
|
||
if (!snapshot.capabilities.course.enabled) throw new Error('Expected youth course capability');
|
||
const planStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'pm');
|
||
if (!planStage) throw new Error('Expected plan stage');
|
||
planStage.title = '被篡改的阶段';
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('built-in contract');
|
||
}
|
||
});
|
||
|
||
it('rejects unrelated tampering across every mini-game legacy matcher path', () => {
|
||
const fiveStageLegacy = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
replacePublishStageWithLegacy(fiveStageLegacy, LEGACY_GAME_RELEASE_STAGE);
|
||
|
||
const fourStageCurrent = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
if (!fourStageCurrent.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
|
||
fourStageCurrent.capabilities.course.stages = fourStageCurrent.capabilities.course.stages
|
||
.filter((stage) => stage.id !== 'game-showcase');
|
||
fourStageCurrent.capabilities.course.slots = fourStageCurrent.capabilities.course.slots
|
||
.filter((slot) => slot.id !== 'game-showcase');
|
||
const currentRelease = fourStageCurrent.capabilities.course.stages
|
||
.find((stage) => stage.id === 'game-release');
|
||
if (!currentRelease) throw new Error('Expected current release stage');
|
||
currentRelease.handoffTarget = null;
|
||
|
||
const fourStageLegacy = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
replacePublishStageWithLegacy(fourStageLegacy, { ...LEGACY_GAME_RELEASE_STAGE, handoffTarget: null });
|
||
if (!fourStageLegacy.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
|
||
fourStageLegacy.capabilities.course.stages = fourStageLegacy.capabilities.course.stages
|
||
.filter((stage) => stage.id !== 'game-showcase');
|
||
fourStageLegacy.capabilities.course.slots = fourStageLegacy.capabilities.course.slots
|
||
.filter((slot) => slot.id !== 'game-showcase');
|
||
|
||
for (const snapshot of [fiveStageLegacy, fourStageCurrent, fourStageLegacy]) {
|
||
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
|
||
const gameplayStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'gameplay');
|
||
if (!gameplayStage) throw new Error('Expected gameplay stage');
|
||
gameplayStage.title = '被篡改的玩法阶段';
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('built-in contract');
|
||
}
|
||
}
|
||
});
|
||
|
||
it('does not repair a non-string createdAt while matching a legacy delivery snapshot', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
replacePublishStageWithLegacy(snapshot, LEGACY_GAME_RELEASE_STAGE);
|
||
(snapshot.template as { createdAt: unknown }).createdAt = 123;
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('template.createdAt');
|
||
}
|
||
});
|
||
|
||
it('rejects built-in mini-game course snapshots when scaffold is missing', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
delete snapshot.capabilities.scaffold;
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('built-in contract web-mini-game-course@3');
|
||
}
|
||
});
|
||
|
||
it('rejects duplicate course stage ids within a template snapshot', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
if (snapshot.capabilities.course.enabled) {
|
||
snapshot.capabilities.course.slots[1] = {
|
||
...snapshot.capabilities.course.slots[1],
|
||
id: snapshot.capabilities.course.slots[0].id,
|
||
recommendedAgentProfile: {
|
||
...snapshot.capabilities.course.slots[1].recommendedAgentProfile,
|
||
roleId: snapshot.capabilities.course.slots[0].id,
|
||
},
|
||
};
|
||
}
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('capabilities.course.slots.id');
|
||
expect(result.error).toContain('capabilities.course.slotStageAlignment');
|
||
}
|
||
});
|
||
|
||
it('validates damaged snapshots with actionable errors', () => {
|
||
const result = validateProjectTemplateSnapshot({
|
||
schemaVersion: 1,
|
||
template: { id: '', version: 0, title: '', description: '' },
|
||
capabilities: { chat: { enabled: true }, course: { enabled: false }, publish: { enabled: false } },
|
||
});
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('template.id');
|
||
expect(result.error).toContain('template.version');
|
||
expect(result.error).toContain('template.title');
|
||
}
|
||
});
|
||
|
||
it('rejects structurally valid snapshots that do not match the built-in semantic contract', () => {
|
||
const snapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
|
||
const courseSnapshot = createProjectTemplateSnapshot('youth-ai-course', '2026-07-01T00:00:00.000Z');
|
||
snapshot.capabilities.course = structuredClone(courseSnapshot.capabilities.course);
|
||
snapshot.capabilities.publish = {
|
||
enabled: true,
|
||
provider: 'works-square',
|
||
};
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('built-in contract');
|
||
expect(result.error).toContain('standard-dev@1');
|
||
}
|
||
});
|
||
|
||
it('exports the clean v3 mini-game template alongside the existing templates', () => {
|
||
expect(builtInProjectTemplates.map((template) => template.template.id)).toEqual([
|
||
'standard-dev',
|
||
'youth-ai-course',
|
||
'web-mini-game-course',
|
||
]);
|
||
});
|
||
|
||
it('includes the mini-game template scaffold contract and five course stages', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
|
||
expect(snapshot.template).toMatchObject({
|
||
version: 3,
|
||
title: 'Web 小游戏课程项目',
|
||
description: '启用结对式 Web 小游戏课程、游戏设计任务文档和 Phaser 3 脚手架支持。',
|
||
});
|
||
expect(snapshot.capabilities).toMatchObject({
|
||
scaffold: {
|
||
enabled: true,
|
||
kind: 'phaser-2d-casual',
|
||
entryFile: 'src/main.ts',
|
||
startCommand: 'pnpm dev',
|
||
packageManager: 'pnpm',
|
||
},
|
||
gameDevelopment: {
|
||
enabled: true,
|
||
engine: 'phaser3',
|
||
requiredSkillIds: ['nianxxgame-skill'],
|
||
requiredRoleIds: ['gameplay', 'game-dev', 'game-release'],
|
||
designDocument: 'GDD.md',
|
||
taskDocument: 'TASKS.md',
|
||
collaborationMode: 'pair',
|
||
},
|
||
});
|
||
expect(getProjectTemplateRequiredSkillIds(snapshot, 'game-dev')).toEqual(['nianxxgame-skill']);
|
||
expect(getProjectTemplateRequiredSkillIds(snapshot, 'assets')).toEqual([]);
|
||
|
||
if (snapshot.capabilities.course.enabled) {
|
||
expect(snapshot.capabilities.course.title).toBe('Web 小游戏制作路径');
|
||
expect(snapshot.capabilities.course.stages.map((stage) => stage.id)).toEqual([
|
||
'gameplay',
|
||
'assets',
|
||
'game-dev',
|
||
'game-release',
|
||
'game-showcase',
|
||
]);
|
||
expect(snapshot.capabilities.course.stages.map((stage) => stage.title)).toEqual([
|
||
'玩法规划',
|
||
'素材搜索',
|
||
'游戏开发',
|
||
'发布打包',
|
||
'宣传展示',
|
||
]);
|
||
const showcaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-showcase');
|
||
expect(showcaseStage).toMatchObject({
|
||
order: 5,
|
||
deliverableTitle: 'SHOWCASE_PACKAGE.md',
|
||
handoffTarget: null,
|
||
});
|
||
const releaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release');
|
||
expect(releaseStage).toMatchObject({
|
||
order: 4,
|
||
handoffTarget: 'game-showcase',
|
||
});
|
||
}
|
||
});
|
||
|
||
it('names the complete Works Square package evidence in the game release starter prompt', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
if (!snapshot.capabilities.course.enabled) throw new Error('Expected mini-game course capability');
|
||
const releaseStage = snapshot.capabilities.course.stages.find((stage) => stage.id === 'game-release');
|
||
if (!releaseStage) throw new Error('Expected game release stage');
|
||
|
||
const starterPrompt = [
|
||
releaseStage.deliverableTitle,
|
||
releaseStage.task,
|
||
releaseStage.deliverableSummary,
|
||
...releaseStage.acceptanceCriteria,
|
||
].join('\n');
|
||
|
||
expect(starterPrompt).toContain('RELEASE_CHECKLIST.md');
|
||
expect(starterPrompt).toContain('Compose 配置、构建、启动、动态端口、HTTP 冒烟和清理');
|
||
expect(starterPrompt).toContain('ZIP 根目录、大小、文件数、路径安全、重复项和符号链接');
|
||
expect(starterPrompt).toContain('works-publish.json');
|
||
expect(starterPrompt).toContain('发布与回滚');
|
||
});
|
||
|
||
it('treats every course-capable template as a course project template', () => {
|
||
expect(isCourseProjectTemplate('youth-ai-course')).toBe(true);
|
||
expect(isCourseProjectTemplate('web-mini-game-course')).toBe(true);
|
||
expect(isCourseProjectTemplate('standard-dev')).toBe(false);
|
||
});
|
||
|
||
it('rejects the removed v1 mini-game contract instead of migrating it', () => {
|
||
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
|
||
snapshot.template.version = 1;
|
||
|
||
const result = validateProjectTemplateSnapshot(snapshot);
|
||
|
||
expect(result.ok).toBe(false);
|
||
if (!result.ok) {
|
||
expect(result.error).toContain('built-in contract web-mini-game-course@1');
|
||
}
|
||
});
|
||
});
|