feat(projects): add interactive AI app scaffold skill
This commit is contained in:
193
tests/unit/project-scaffold-plugin.test.ts
Normal file
193
tests/unit/project-scaffold-plugin.test.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
// @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/']));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user