// @vitest-environment node import { spawnSync } from 'node:child_process'; import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join, resolve } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { devicePackageKind, inspectDevicePackage, } from '@electron/coding-packages/device-package-format'; import { loadBundledCodingPluginDefinitions } from '@electron/coding-plugins/manifest'; import { createCodingProjectMetadata } from '@electron/coding-projects/project-config'; import { createStaticProjectPackage } from '@electron/services/project-packager'; import { PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID, PROJECT_SCAFFOLD_PLUGIN_ID, isCodeOwnedOptionalBundledPluginId, } from '../../shared/coding-plugins'; const pluginRoot = resolve('resources/coding-plugins/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('loads as a fixed code-owned bundled Skill while retaining portable plugin metadata', async () => { const packageJson = JSON.parse(await readFile(join(pluginRoot, 'package.json'), 'utf8')); const portableManifest = JSON.parse(await readFile( join(pluginRoot, '.codex-plugin', 'plugin.json'), 'utf8', )); const marketplaceManifest = JSON.parse(await readFile(join(pluginRoot, 'plugin.json'), 'utf8')); const inspected = await inspectDevicePackage(pluginRoot); const definitions = await loadBundledCodingPluginDefinitions(resolve('resources/coding-plugins')); const definition = definitions.find(({ id }) => id === PROJECT_SCAFFOLD_PLUGIN_ID); expect({ name: packageJson.name, version: packageJson.version }).toEqual({ name: marketplaceManifest.name, version: marketplaceManifest.version, }); expect({ name: portableManifest.name, version: portableManifest.version }).toEqual({ name: marketplaceManifest.name, version: marketplaceManifest.version, }); expect(portableManifest.skills).toBe('./skills/'); expect(devicePackageKind(inspected)).toBe('skill-only'); expect(inspected).toMatchObject({ packageId: PROJECT_SCAFFOLD_PLUGIN_ID, resolvedVersion: '1.0.0', extensionEntries: [], hasSkillScripts: true, ignoredLifecycleScripts: [], skillEntries: [{ id: 'makelore-project-scaffold', entryPath: 'skills/makelore-project-scaffold/SKILL.md', }], }); expect(definition).toMatchObject({ id: PROJECT_SCAFFOLD_PLUGIN_ID, version: '1.0.0', runtimeKind: 'skill_only', acquisitionMode: 'user_acquired', releaseId: PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID, provenance: { source: 'bundled', packageRoot: 'project-scaffold' }, skills: [{ id: 'makelore-project-scaffold', entryPath: 'skills/makelore-project-scaffold/SKILL.md', grants: [], }], tools: [], }); expect(isCodeOwnedOptionalBundledPluginId(PROJECT_SCAFFOLD_PLUGIN_ID)).toBe(true); }); it('ships the executable scaffold and every Skill resource in the bundled root', async () => { const skillRoot = join(pluginRoot, 'skills', 'makelore-project-scaffold'); 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 = 'interactive_ai_app' as const; 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/'])); }); });