import { createHash } from 'node:crypto'; import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createProjectConfig } from '../../shared/project-config'; const runElectronNodeMock = vi.hoisted(() => vi.fn()); vi.mock('@electron/services/publish-runtime', () => ({ resolvePublishRuntime: async () => ({ nodeExecutable: 'electron', npmCli: 'npm-cli.js', nodeVersion: '22.0.0', npmVersion: '11.6.2' }), runElectronNode: runElectronNodeMock, PublishRuntimeError: class extends Error {}, })); import { prepareProjectRelease } from '@electron/services/project-release-builder'; import { startStaticReleaseServer, staticArtifactSnapshotFiles } from '@electron/services/static-release-server'; describe('project release builder', () => { let root: string | null = null; afterEach(async () => { if (root) await rm(root, { recursive: true, force: true }); root = null; vi.clearAllMocks(); }); it('builds from the exact source archive and returns the protocol-v1 canonical contract', async () => { root = await mkdtemp(join(tmpdir(), 'release-builder-test-')); await mkdir(join(root, '.niancode'), { recursive: true }); await writeFile(join(root, '.niancode', 'project.json'), JSON.stringify(createProjectConfig(new Date().toISOString(), 'mini_game'))); await writeFile(join(root, 'package.json'), JSON.stringify({ name: 'demo', packageManager: 'npm@11.6.2', devDependencies: { vite: '7.3.1' } })); await writeFile(join(root, 'package-lock.json'), JSON.stringify({ name: 'demo', lockfileVersion: 3, requires: true, packages: {} })); await writeFile(join(root, 'index.html'), '
source
'); runElectronNodeMock.mockImplementation(async (input: { args: string[]; cwd: string }) => { if (input.args.includes('ci')) { await mkdir(join(input.cwd, 'node_modules', 'vite', 'bin'), { recursive: true }); await writeFile(join(input.cwd, 'node_modules', 'vite', 'package.json'), JSON.stringify({ version: '7.3.1' })); } else if (input.args.includes('build')) { const output = input.args[input.args.indexOf('--outDir') + 1]; await mkdir(join(output, 'assets'), { recursive: true }); await writeFile(join(output, 'index.html'), '
built
'); await writeFile(join(output, 'assets', '中.js'), 'ok'); } return { stdout: '', stderr: '' }; }); const release = await prepareProjectRelease({ projectPath: root, clientVersion: '2.0.0' }); try { expect(release.contract).toMatchObject({ schema_version: 1, entry_path: 'index.html', security_profile: 'works-square-static-sandbox-v1', toolchain: { client: 'makelore', client_version: '2.0.0', node: '22.0.0', npm: '11.6.2', vite: '7.3.1' }, }); expect(release.contract.files.map((file) => file.path)).toEqual(['assets/中.js', 'index.html']); const canonical = JSON.stringify(release.contract.files.map(({ path, sha256, size }) => ({ path, sha256, size }))); expect(release.contract.artifact_digest).toBe(createHash('sha256').update(canonical).digest('hex')); expect(release.contract.source_digest).toBe(createHash('sha256').update(await readFile(release.sourceArchive.path)).digest('hex')); expect(release.contract.built_archive_digest).toBe(createHash('sha256').update(release.builtArchive.bytes).digest('hex')); expect(runElectronNodeMock.mock.calls[0][0].args).toEqual([ 'npm-cli.js', 'ci', '--ignore-scripts', '--no-audit', '--no-fund', '--userconfig', expect.stringMatching(/empty-npmrc$/), '--cache', expect.stringMatching(/npm-cache$/), ]); await rm(release.distRoot, { recursive: true, force: true }); expect(staticArtifactSnapshotFiles(release.staticArtifact).map(({ path, bytes }) => ({ path, size: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex'), }))).toEqual(release.contract.files); const server = await startStaticReleaseServer(release.staticArtifact); try { expect(await (await fetch(server.entryUrl)).text()).toBe('
built
'); expect(await (await fetch(new URL('assets/%E4%B8%AD.js', server.entryUrl))).text()).toBe('ok'); } finally { await server.close(); } } finally { await release.dispose(); } await expect(readFile(release.sourceArchive.path)).rejects.toThrow(); }); });