Files
makelore/tests/unit/project-template-snapshot.test.ts
2026-07-29 17:22:35 +08:00

166 lines
7.5 KiB
TypeScript

import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { afterEach, describe, expect, it } from 'vitest';
import { mkdtemp } from 'node:fs/promises';
import { createProjectTemplateSnapshot } from '../../shared/project-template';
import { maybeWriteProjectScaffold } from '@electron/opencode/project-scaffold';
import {
PROJECT_TEMPLATE_SNAPSHOT_PATH,
getProjectTemplateSnapshotPath,
writeProjectTemplateSnapshot,
} from '@electron/opencode/project-template-snapshot';
const tempDirs: string[] = [];
afterEach(async () => {
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
});
async function createTempProjectDir(prefix: string) {
const dir = await mkdtemp(join(tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}
describe('project template snapshot helper', () => {
it('rejects overwriting an existing valid snapshot', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-valid-');
const snapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
await writeProjectTemplateSnapshot(projectPath, snapshot);
await expect(writeProjectTemplateSnapshot(projectPath, snapshot)).rejects.toThrow(PROJECT_TEMPLATE_SNAPSHOT_PATH);
await expect(writeProjectTemplateSnapshot(projectPath, snapshot)).rejects.toThrow(/already exists/i);
});
it('rejects overwriting an existing invalid snapshot and preserves its contents', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-invalid-');
const snapshotPath = getProjectTemplateSnapshotPath(projectPath);
await mkdir(join(projectPath, '.niancode'), { recursive: true });
await writeFile(snapshotPath, '{"broken":true}\n', 'utf8');
const snapshot = createProjectTemplateSnapshot('standard-dev', '2026-07-01T00:00:00.000Z');
await expect(writeProjectTemplateSnapshot(projectPath, snapshot)).rejects.toThrow(/already exists/i);
await expect(readFile(snapshotPath, 'utf8')).resolves.toBe('{"broken":true}\n');
});
it('does not overwrite an existing scaffold file and preserves its contents', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-scaffold-');
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
await mkdir(join(projectPath, 'src'), { recursive: true });
await writeFile(join(projectPath, 'src', 'main.ts'), '// keep me\n', 'utf8');
const result = await maybeWriteProjectScaffold(projectPath, snapshot);
expect(result).toEqual({
status: 'skipped_existing_files',
files: ['src/main.ts'],
});
await expect(readFile(join(projectPath, 'src', 'main.ts'), 'utf8')).resolves.toBe('// keep me\n');
});
it('allows scaffold generation when the project root only contains ignorable entries', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-ignorable-');
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
await mkdir(join(projectPath, '.git'), { recursive: true });
await mkdir(join(projectPath, '.niancode'), { recursive: true });
await writeFile(join(projectPath, '.DS_Store'), '', 'utf8');
await writeFile(join(projectPath, 'Thumbs.db'), '', 'utf8');
const result = await maybeWriteProjectScaffold(projectPath, snapshot);
expect(result.status).toBe('generated');
await expect(readFile(join(projectPath, 'package.json'), 'utf8')).resolves.toContain('"phaser"');
});
it('treats extra files inside an existing scaffold root directory as non-empty project content', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-non-empty-src-');
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
await mkdir(join(projectPath, 'src'), { recursive: true });
await writeFile(join(projectPath, 'src', 'utils.ts'), 'export const noop = () => {};\n', 'utf8');
const result = await maybeWriteProjectScaffold(projectPath, snapshot);
expect(result).toEqual({
status: 'skipped_non_empty',
entries: ['src/utils.ts'],
});
await expect(readFile(join(projectPath, 'src', 'utils.ts'), 'utf8')).resolves.toContain('noop');
});
it('treats nested extra files inside an existing scaffold root directory as non-empty project content', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-nested-src-');
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
await mkdir(join(projectPath, 'src', 'assets'), { recursive: true });
await writeFile(join(projectPath, 'src', 'assets', 'existing.png'), 'fake-png', 'utf8');
const result = await maybeWriteProjectScaffold(projectPath, snapshot);
expect(result).toEqual({
status: 'skipped_non_empty',
entries: ['src/assets/existing.png'],
});
});
it('treats a root file named src as non-empty project content', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-src-file-');
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
await writeFile(join(projectPath, 'src'), 'not-a-directory\n', 'utf8');
const result = await maybeWriteProjectScaffold(projectPath, snapshot);
expect(result).toEqual({
status: 'skipped_non_empty',
entries: ['src'],
});
});
it('writes the Phaser starter scaffold for an empty mini-game project', async () => {
const projectPath = await createTempProjectDir('niancode-template-helper-generated-');
const snapshot = createProjectTemplateSnapshot('web-mini-game-course', '2026-07-01T00:00:00.000Z');
const result = await maybeWriteProjectScaffold(projectPath, snapshot);
expect(result.status).toBe('generated');
if (result.status !== 'generated') {
throw new Error(`Expected generated scaffold, received ${result.status}`);
}
expect(result.files).toEqual([
'ASSET_PLAN.md',
'GDD.md',
'README.md',
'RELEASE_CHECKLIST.md',
'PRODUCT_OVERVIEW.md',
'TASKS.md',
'index.html',
'package.json',
'src/main.ts',
'src/styles.css',
'tsconfig.json',
'vite.config.ts',
]);
const packageJson = JSON.parse(await readFile(join(projectPath, 'package.json'), 'utf8')) as {
scripts: Record<string, string>;
dependencies: Record<string, string>;
devDependencies: Record<string, string>;
};
expect(packageJson.scripts).toEqual({
dev: 'vite --host 0.0.0.0',
build: 'tsc && vite build',
preview: 'vite preview --host 0.0.0.0',
});
expect(packageJson.dependencies.phaser).toBe('3.90.0');
expect(packageJson.devDependencies.typescript).toMatch(/^\^[0-9]+\.[0-9]+\.[0-9]+$/);
expect(packageJson.devDependencies.vite).toMatch(/^\^[0-9]+\.[0-9]+\.[0-9]+$/);
await expect(readFile(join(projectPath, 'src', 'main.ts'), 'utf8')).resolves.toContain('GDD.md');
await expect(readFile(join(projectPath, 'src', 'main.ts'), 'utf8')).resolves.toContain('Phaser.Scale.FIT');
await expect(readFile(join(projectPath, 'GDD.md'), 'utf8')).resolves.toContain('## 5. 当前系统');
await expect(readFile(join(projectPath, 'TASKS.md'), 'utf8')).resolves.toContain('## Last verified');
await expect(readFile(join(projectPath, 'PRODUCT_OVERVIEW.md'), 'utf8')).resolves.toContain('## 5. 对外介绍与运营表达');
});
});