// @vitest-environment node import { spawnSync } from 'node:child_process'; import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join, resolve } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { devicePackageKind, inspectDevicePackage, } from '@electron/coding-packages/device-package-format'; import { DevicePackageManager } from '@electron/coding-packages/device-package-manager'; import { createCodingProjectMetadata } from '@electron/coding-projects/project-config'; import { createStaticProjectPackage } from '@electron/services/project-packager'; import type { ProjectType } from '../../shared/project-config'; const pluginRoot = resolve('plugins/makelore-project-scaffold'); const scaffoldScript = join( pluginRoot, 'skills', 'makelore-project-scaffold', 'scripts', 'scaffold.mjs', ); const tempDirectories: string[] = []; function extractStringList(source: string, declaration: string): string[] { const declarationStart = source.indexOf(`const ${declaration} =`); expect(declarationStart).toBeGreaterThanOrEqual(0); const listStart = source.indexOf('[', declarationStart); const listEnd = source.indexOf(']', listStart); expect(listStart).toBeGreaterThanOrEqual(0); expect(listEnd).toBeGreaterThan(listStart); return [...source.slice(listStart + 1, listEnd).matchAll(/'([^']+)'/g)] .map((match) => match[1]); } afterEach(async () => { await Promise.all( tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })), ); }); describe('MakeLore project scaffold plugin', () => { it('is discovered as a skill-only package with executable Skill scripts', async () => { const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8')); const pluginManifest = JSON.parse(await readFile( join(pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8', )); const inspected = await inspectDevicePackage(pluginRoot); expect({ name: packageJson.name, version: packageJson.version }).toEqual({ name: pluginManifest.name, version: pluginManifest.version, }); expect(pluginManifest.skills).toBe('./skills/'); expect(devicePackageKind(inspected)).toBe('skill-only'); expect(inspected).toMatchObject({ packageId: 'makelore-project-scaffold', resolvedVersion: '0.1.0', extensionEntries: [], hasSkillScripts: true, ignoredLifecycleScripts: [], skillEntries: [{ id: 'makelore-project-scaffold', entryPath: 'skills/makelore-project-scaffold/SKILL.md', }], }); }); it('prepares, confirms, and installs the real package with every Skill resource', async () => { const root = await mkdtemp(join(tmpdir(), 'makelore-scaffold-device-package-')); tempDirectories.push(root); const manager = new DevicePackageManager({ rootDir: join(root, 'store'), now: () => Date.parse('2026-09-04T00:00:00.000Z'), createId: () => 'project-scaffold-plan', }); const preview = await manager.prepare(pluginRoot, 'prepare-turn'); expect(preview).toMatchObject({ kind: 'skill-only', includesExecutableCode: true, skillEntries: [{ id: 'makelore-project-scaffold' }], extensionEntries: [], }); expect(preview.warnings.join(' ')).toContain('可执行 Skill 脚本'); await manager.commit(preview.planId, true, 'confirm-turn'); const [resource] = (await manager.resolveEnabledResources()).skillEntries; expect(resource?.id).toBe('makelore-project-scaffold'); const skillRoot = dirname(join(resource!.packageRoot, resource!.entryPath)); expect(await readFile(join(skillRoot, 'scripts', 'scaffold.mjs'), 'utf8')) .toContain('schemaVersion: 1'); expect(await readFile(join(skillRoot, 'assets', 'templates', 'common', 'package-lock.json'), 'utf8')) .toContain('"lockfileVersion": 3'); expect(await readFile(join(skillRoot, 'references', 'submission-requirements.md'), 'utf8')) .toContain('提交审批要求'); }); it('carries the complete categorized release-readiness contract', async () => { const skill = await readFile( join(pluginRoot, 'skills', 'makelore-project-scaffold', 'SKILL.md'), 'utf8', ); const requirements = await readFile( join( pluginRoot, 'skills', 'makelore-project-scaffold', 'references', 'submission-requirements.md', ), 'utf8', ); const packagerSource = await readFile('electron/services/project-packager.ts', 'utf8'); expect(skill).toContain('Release-readiness workflow'); expect(skill).toContain('不能进入一键提交'); expect(skill).toContain('可进入一键提交,仍需运行期验证'); expect(skill).toContain('Do not infer an external-network blocker'); for (const heading of ['必须具备', '直接阻断', '打包排除', '运行时或平台确认']) { expect(requirements).toContain(`**${heading}**`); } for (const value of [ '.makelore/project.json', 'schema v2', 'interactive_ai_app', 'mini_game', 'mini_program', 'custom', 'npm ci --ignore-scripts', 'lockfileVersion', '2,000', '200 MiB', '50 MiB', 'release.json', 'CON', '1280×720', '390×844', '30 秒', 'HTTP/HTTPS', '.env', 'serviceaccount', 'works-', 'PNG、JPEG 或 WebP', '1–150', '存在但不阻断', ]) { expect(requirements).toContain(value); } const excludedValues = [ ...extractStringList(packagerSource, 'EXCLUDED_DIRECTORY_NAMES'), ...extractStringList(packagerSource, 'EXCLUDED_FILE_NAMES'), ...extractStringList(packagerSource, 'EXCLUDED_FILE_SUFFIXES'), ]; expect(excludedValues.length).toBeGreaterThan(50); for (const value of excludedValues) { expect(requirements).toContain(value); } }); it('creates an interactive AI application accepted by the existing source packager', async () => { const projectType: ProjectType = 'interactive_ai_app'; const projectPath = await mkdtemp(join(tmpdir(), `makelore-${projectType}-`)); const outputPath = await mkdtemp(join(tmpdir(), 'makelore-scaffold-package-')); tempDirectories.push(projectPath, outputPath); await createCodingProjectMetadata(projectPath, { projectType, now: '2026-09-04T00:00:00.000Z', }); const scaffold = spawnSync( process.execPath, [scaffoldScript, '--project-root', projectPath], { encoding: 'utf8' }, ); expect(scaffold.status, scaffold.stderr).toBe(0); const packaged = await createStaticProjectPackage({ projectPath, archivePath: join(outputPath, 'project.zip'), }); expect(packaged.manifest.project_type).toBe(projectType); expect(packaged.archiveName).toBe('project.zip'); expect(packaged.fileCount).toBeGreaterThan(1); expect(packaged.archiveBytes).toBeGreaterThan(0); expect(packaged.excludedPaths).toEqual(expect.arrayContaining(['.makelore/', 'knowledge/'])); }); });