Files
makelore/tests/unit/project-scaffold-plugin.test.ts

200 lines
7.4 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @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',
'1150',
'存在但不阻断',
]) {
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/']));
});
});