166 lines
7.5 KiB
TypeScript
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. 对外介绍与运营表达');
|
|
});
|
|
});
|