200 lines
7.4 KiB
TypeScript
200 lines
7.4 KiB
TypeScript
// @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/']));
|
||
});
|
||
});
|