import { cp, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { afterEach, describe, expect, it } from 'vitest'; import { parse as parseYaml } from 'yaml'; import { initializeWorksPackageTemplate, resolveWorksPackageTemplateDirectory, } from '@electron/opencode/project-package-template'; const temporaryDirectories: string[] = []; const resourcesDirectory = join(process.cwd(), 'resources'); async function temporaryDirectory(prefix: string): Promise { const directory = await mkdtemp(join(tmpdir(), prefix)); temporaryDirectories.push(directory); return directory; } afterEach(async () => { await Promise.all(temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))); }); describe('Works Square project package template', () => { it('copies the shared baseline and only the Web example for a standard project', async () => { const projectPath = await temporaryDirectory('niancode-works-template-web-'); const result = await initializeWorksPackageTemplate(projectPath, 'standard-development', { resourcesDirectory }); expect(result.files).toEqual(expect.arrayContaining([ '.dockerignore', 'Dockerfile', 'docker-compose.yml', 'nginx.conf', 'niancode.yml', 'deploy/works-deploy-check.schema.json', 'deploy/examples/works-deploy-check.web.json', 'deploy/部署报告模板.md', ])); expect((await stat(join(projectPath, 'deploy/examples/works-deploy-check.web.json'))).isFile()).toBe(true); await expect(stat(join(projectPath, 'deploy/examples/works-deploy-check.game.json'))).rejects.toMatchObject({ code: 'ENOENT' }); for (const releaseArtifact of ['works-deploy-check.json', 'works-publish.json', '部署报告.md']) { await expect(stat(join(projectPath, releaseArtifact))).rejects.toMatchObject({ code: 'ENOENT' }); } }); it('copies only the complete game/canvas example for a game project', async () => { const projectPath = await temporaryDirectory('niancode-works-template-game-'); await initializeWorksPackageTemplate(projectPath, 'game-development', { resourcesDirectory }); const example = JSON.parse(await readFile(join(projectPath, 'deploy/examples/works-deploy-check.game.json'), 'utf8')); const evidence = example.checks.game_canvas_smoke.canvas_evidence; expect(example.status).toBe('BLOCKED'); expect(evidence.resize_verified).toBe(false); for (const measurement of [ evidence.logical_resolution, evidence.desktop_viewport, evidence.mobile_viewport, evidence.desktop_canvas, evidence.mobile_canvas, evidence.expected_max_canvas.desktop, evidence.expected_max_canvas.mobile, ]) { expect(typeof measurement.width).toBe('number'); expect(typeof measurement.height).toBe('number'); expect(measurement.width).toBeGreaterThan(0); expect(measurement.height).toBeGreaterThan(0); } await expect(stat(join(projectPath, 'deploy/examples/works-deploy-check.web.json'))).rejects.toMatchObject({ code: 'ENOENT' }); }); it('keeps the manifest, Compose, Nginx, schema, and examples parseable and contract-safe', async () => { const templateRoot = resolveWorksPackageTemplateDirectory(resourcesDirectory); const manifest = parseYaml(await readFile(join(templateRoot, 'niancode.yml'), 'utf8')); const compose = parseYaml(await readFile(join(templateRoot, 'docker-compose.yml'), 'utf8')); const nginx = await readFile(join(templateRoot, 'nginx.conf'), 'utf8'); const schema = JSON.parse(await readFile(join(templateRoot, 'deploy/works-deploy-check.schema.json'), 'utf8')); const web = JSON.parse(await readFile(join(templateRoot, 'deploy/examples/works-deploy-check.web.json'), 'utf8')); const game = JSON.parse(await readFile(join(templateRoot, 'deploy/examples/works-deploy-check.game.json'), 'utf8')); expect(manifest).toEqual({ runtime: 'compose', compose: { file: 'docker-compose.yml', public_service: 'frontend', public_port: 8080 } }); expect(compose.services.frontend.ports).toContain('127.0.0.1::8080'); expect(nginx).toContain('listen 0.0.0.0:8080'); expect(schema.properties.schema_version.const).toBe(1); expect(web.checks).not.toHaveProperty('game_canvas_smoke'); expect(game.checks).toHaveProperty('game_canvas_smoke'); }); it('rejects invalid JSON with a source-relative error and never overwrites a destination file', async () => { const fixtureResources = await temporaryDirectory('niancode-works-template-fixture-'); await cp(resolveWorksPackageTemplateDirectory(resourcesDirectory), resolveWorksPackageTemplateDirectory(fixtureResources), { recursive: true }); await writeFile(join(resolveWorksPackageTemplateDirectory(fixtureResources), 'deploy/examples/works-deploy-check.web.json'), '{invalid', 'utf8'); const invalidProject = await temporaryDirectory('niancode-works-template-invalid-'); await expect(initializeWorksPackageTemplate(invalidProject, 'standard-development', { resourcesDirectory: fixtureResources })) .rejects.toThrow('deploy/examples/works-deploy-check.web.json'); const existingProject = await temporaryDirectory('niancode-works-template-existing-'); await writeFile(join(existingProject, 'niancode.yml'), 'user-owned', 'utf8'); await expect(initializeWorksPackageTemplate(existingProject, 'standard-development', { resourcesDirectory })) .rejects.toThrow('niancode.yml'); expect(await readFile(join(existingProject, 'niancode.yml'), 'utf8')).toBe('user-owned'); }); it('is included by the packaged resources rule', async () => { const builderConfig = parseYaml(await readFile(join(process.cwd(), 'electron-builder.yml'), 'utf8')); expect(builderConfig.extraResources).toEqual(expect.arrayContaining([ expect.objectContaining({ from: 'resources/', to: 'resources/' }), ])); expect((await stat(resolveWorksPackageTemplateDirectory(resourcesDirectory))).isDirectory()).toBe(true); }); });