feat(plugins): bundle project scaffold marketplace plugin
This commit is contained in:
292
tests/project-scaffold/scaffold.test.mjs
Normal file
292
tests/project-scaffold/scaffold.test.mjs
Normal file
@@ -0,0 +1,292 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
cp,
|
||||
mkdtemp,
|
||||
mkdir,
|
||||
open,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
rmdir,
|
||||
unlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import test from 'node:test';
|
||||
|
||||
const pluginRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'../../resources/coding-plugins/project-scaffold',
|
||||
);
|
||||
const scaffoldScript = path.join(
|
||||
pluginRoot,
|
||||
'skills',
|
||||
'makelore-project-scaffold',
|
||||
'scripts',
|
||||
'scaffold.mjs',
|
||||
);
|
||||
const { writeScaffold } = await import(pathToFileURL(scaffoldScript).href);
|
||||
|
||||
const expectedFiles = [
|
||||
'index.html',
|
||||
'package-lock.json',
|
||||
'package.json',
|
||||
'src/main.js',
|
||||
'src/style.css',
|
||||
'vite.config.js',
|
||||
];
|
||||
|
||||
async function initializeProject(root, projectType) {
|
||||
await mkdir(path.join(root, '.makelore'));
|
||||
await mkdir(path.join(root, 'knowledge'));
|
||||
const config = `${JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
projectId: 'scaffold-test',
|
||||
projectType,
|
||||
}, null, 2)}\n`;
|
||||
await writeFile(path.join(root, '.makelore', 'project.json'), config, 'utf8');
|
||||
return config;
|
||||
}
|
||||
|
||||
async function createProject(projectType) {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-'));
|
||||
return { root, config: await initializeProject(root, projectType) };
|
||||
}
|
||||
|
||||
function runScaffold(projectRoot, scriptPath = scaffoldScript) {
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[scriptPath, '--project-root', projectRoot],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
return {
|
||||
...result,
|
||||
payload: JSON.parse((result.status === 0 ? result.stdout : result.stderr).trim()),
|
||||
};
|
||||
}
|
||||
|
||||
async function listTree(root, directory = root) {
|
||||
const result = [];
|
||||
for (const entry of await readdir(directory, { withFileTypes: true })) {
|
||||
const absolutePath = path.join(directory, entry.name);
|
||||
const relativePath = path.relative(root, absolutePath).split(path.sep).join('/');
|
||||
if (entry.isDirectory()) {
|
||||
result.push(`${relativePath}/`, ...await listTree(root, absolutePath));
|
||||
} else {
|
||||
result.push(relativePath);
|
||||
}
|
||||
}
|
||||
return result.sort();
|
||||
}
|
||||
|
||||
for (const sourceProjectType of ['interactive_ai_app', 'mini_game', 'mini_program']) {
|
||||
test(`creates the unified scaffold for ${sourceProjectType} without changing project metadata`, async () => {
|
||||
const { root, config } = await createProject(sourceProjectType);
|
||||
try {
|
||||
await writeFile(path.join(root, 'notes.txt'), 'keep me', 'utf8');
|
||||
const result = runScaffold(root);
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.equal(result.stderr, '');
|
||||
assert.deepEqual(result.payload, {
|
||||
schemaVersion: 1,
|
||||
ok: true,
|
||||
status: 'created',
|
||||
projectType: 'interactive_ai_app',
|
||||
createdFiles: expectedFiles,
|
||||
});
|
||||
assert.equal(
|
||||
await readFile(path.join(root, '.makelore', 'project.json'), 'utf8'),
|
||||
config,
|
||||
);
|
||||
const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
||||
const packageLock = JSON.parse(await readFile(path.join(root, 'package-lock.json'), 'utf8'));
|
||||
assert.equal(packageJson.packageManager, 'npm@11.6.2');
|
||||
assert.equal(packageJson.devDependencies.vite, '7.3.1');
|
||||
assert.equal(packageLock.lockfileVersion, 3);
|
||||
assert.match(await readFile(path.join(root, 'vite.config.js'), 'utf8'), /base:\s*['"]\.\/['"]/u);
|
||||
assert.equal(await readFile(path.join(root, 'notes.txt'), 'utf8'), 'keep me');
|
||||
for (const relativePath of expectedFiles) {
|
||||
assert.ok((await readFile(path.join(root, ...relativePath.split('/')))).length > 0);
|
||||
}
|
||||
assert.match(await readFile(path.join(root, 'index.html'), 'utf8'), /src="\.\/src\/main\.js"/u);
|
||||
assert.match(await readFile(path.join(root, 'src', 'main.js'), 'utf8'), /import '\.\/style\.css'/u);
|
||||
assert.deepEqual(await listTree(root), [
|
||||
'.makelore/',
|
||||
'.makelore/project.json',
|
||||
'knowledge/',
|
||||
'notes.txt',
|
||||
'src/',
|
||||
...expectedFiles,
|
||||
].sort());
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('reports every existing target conflict and writes nothing else', async () => {
|
||||
const { root } = await createProject('interactive_ai_app');
|
||||
try {
|
||||
await writeFile(path.join(root, 'index.html'), 'mine', 'utf8');
|
||||
await writeFile(path.join(root, 'package.json'), 'mine', 'utf8');
|
||||
|
||||
const result = runScaffold(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.deepEqual(result.payload, {
|
||||
schemaVersion: 1,
|
||||
ok: false,
|
||||
code: 'TARGET_CONFLICT',
|
||||
message: '目标路径已存在或必需的父路径不是目录,未写入任何文件。',
|
||||
paths: ['index.html', 'package.json'],
|
||||
});
|
||||
assert.equal(await readFile(path.join(root, 'index.html'), 'utf8'), 'mine');
|
||||
assert.equal(await readFile(path.join(root, 'package.json'), 'utf8'), 'mine');
|
||||
assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'index.html', 'knowledge', 'package.json']);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('reports a blocking parent path as a target conflict', async () => {
|
||||
const { root } = await createProject('interactive_ai_app');
|
||||
try {
|
||||
await writeFile(path.join(root, 'src'), 'mine', 'utf8');
|
||||
const result = runScaffold(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.payload.code, 'TARGET_CONFLICT');
|
||||
assert.deepEqual(result.payload.paths, ['src']);
|
||||
assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'knowledge', 'src']);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects custom projects without creating scaffold files', async () => {
|
||||
const { root } = await createProject('custom');
|
||||
try {
|
||||
const result = runScaffold(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.payload.code, 'PROJECT_TYPE_UNSUPPORTED');
|
||||
assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'knowledge']);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects a missing project config without creating scaffold files', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-'));
|
||||
try {
|
||||
const result = runScaffold(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.payload.code, 'PROJECT_CONFIG_MISSING');
|
||||
assert.deepEqual(await readdir(root), []);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects invalid project metadata without creating scaffold files', async () => {
|
||||
const { root } = await createProject('interactive_ai_app');
|
||||
try {
|
||||
await writeFile(path.join(root, '.makelore', 'project.json'), '{broken', 'utf8');
|
||||
const result = runScaffold(root);
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.payload.code, 'PROJECT_CONFIG_INVALID');
|
||||
assert.deepEqual((await readdir(root)).sort(), ['.makelore', 'knowledge']);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects invalid CLI arguments with a single stderr JSON object', () => {
|
||||
const result = spawnSync(process.execPath, [scaffoldScript], { encoding: 'utf8' });
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.deepEqual(JSON.parse(result.stderr.trim()), {
|
||||
schemaVersion: 1,
|
||||
ok: false,
|
||||
code: 'USAGE_INVALID',
|
||||
message: '脚本参数无效,必须提供 --project-root <project-directory>。',
|
||||
});
|
||||
});
|
||||
|
||||
test('reports a missing template before writing any target', async () => {
|
||||
const outer = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-template-'));
|
||||
try {
|
||||
const projectRoot = path.join(outer, 'project');
|
||||
const copiedSkill = path.join(outer, 'skill');
|
||||
await mkdir(projectRoot);
|
||||
await initializeProject(projectRoot, 'interactive_ai_app');
|
||||
await cp(path.dirname(path.dirname(scaffoldScript)), copiedSkill, { recursive: true });
|
||||
await rm(path.join(copiedSkill, 'assets', 'templates', 'interactive_ai_app', 'main.js'));
|
||||
|
||||
const result = runScaffold(projectRoot, path.join(copiedSkill, 'scripts', 'scaffold.mjs'));
|
||||
assert.equal(result.status, 1);
|
||||
assert.equal(result.payload.code, 'TEMPLATE_UNAVAILABLE');
|
||||
assert.deepEqual(result.payload.paths, ['src/main.js']);
|
||||
assert.deepEqual(await listTree(projectRoot), [
|
||||
'.makelore/',
|
||||
'.makelore/project.json',
|
||||
'knowledge/',
|
||||
]);
|
||||
} finally {
|
||||
await rm(outer, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('runs when both the installed Skill path and project path contain spaces and Chinese', async () => {
|
||||
const outer = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-paths-'));
|
||||
try {
|
||||
const projectRoot = path.join(outer, '项目 空格');
|
||||
const copiedSkill = path.join(outer, '插件 安装', '项目 初始化');
|
||||
await mkdir(projectRoot);
|
||||
await mkdir(path.dirname(copiedSkill), { recursive: true });
|
||||
await initializeProject(projectRoot, 'interactive_ai_app');
|
||||
await cp(path.dirname(path.dirname(scaffoldScript)), copiedSkill, { recursive: true });
|
||||
|
||||
const result = runScaffold(projectRoot, path.join(copiedSkill, 'scripts', 'scaffold.mjs'));
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.deepEqual(result.payload.createdFiles, expectedFiles);
|
||||
} finally {
|
||||
await rm(outer, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rolls back files and directories created before a handled write failure', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-scaffold-rollback-'));
|
||||
let openCalls = 0;
|
||||
try {
|
||||
await assert.rejects(
|
||||
writeScaffold(
|
||||
root,
|
||||
[
|
||||
{ targetPath: 'first.txt', contents: Buffer.from('first') },
|
||||
{ targetPath: 'nested/second.txt', contents: Buffer.from('second') },
|
||||
],
|
||||
['nested'],
|
||||
{
|
||||
mkdir,
|
||||
open: async (...args) => {
|
||||
openCalls += 1;
|
||||
if (openCalls === 2) throw new Error('injected write failure');
|
||||
return await open(...args);
|
||||
},
|
||||
rmdir,
|
||||
unlink,
|
||||
},
|
||||
),
|
||||
(error) => {
|
||||
assert.equal(error.code, 'WRITE_FAILED');
|
||||
assert.equal(error.details.paths, undefined);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
assert.deepEqual(await readdir(root), []);
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
|
||||
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
|
||||
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
|
||||
const PROJECT_SCAFFOLD_ROOT = path.resolve('resources/coding-plugins/project-scaffold');
|
||||
|
||||
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
|
||||
return {
|
||||
@@ -33,12 +34,17 @@ describe('bundled coding plugin manifests', () => {
|
||||
it('loads the fixed Data Service package and immutable declarations', async () => {
|
||||
const definitions = await loadBundledCodingPluginDefinitions(path.resolve('resources/coding-plugins'));
|
||||
const startupDefinitions = loadBundledCodingPluginDefinitionsSync(path.resolve('resources/coding-plugins'));
|
||||
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource']);
|
||||
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual([
|
||||
'data-service',
|
||||
'game-resource',
|
||||
'project-scaffold',
|
||||
]);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
||||
PACKAGE_ROOT,
|
||||
GAME_RESOURCE_ROOT,
|
||||
PROJECT_SCAFFOLD_ROOT,
|
||||
]);
|
||||
expect(definitions).toHaveLength(2);
|
||||
expect(definitions).toHaveLength(3);
|
||||
expect(definitions[0]).toMatchObject({
|
||||
id: 'makelore.data-service',
|
||||
adapterId: 'data-service',
|
||||
@@ -54,6 +60,17 @@ describe('bundled coding plugin manifests', () => {
|
||||
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
||||
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
|
||||
},
|
||||
{
|
||||
id: 'makelore.project-scaffold', version: '1.0.0', runtimeKind: 'skill_only',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000303',
|
||||
provenance: { source: 'bundled', packageRoot: 'project-scaffold' },
|
||||
skills: [{
|
||||
id: 'makelore-project-scaffold',
|
||||
entryPath: 'skills/makelore-project-scaffold/SKILL.md',
|
||||
grants: [],
|
||||
}],
|
||||
tools: [],
|
||||
},
|
||||
]);
|
||||
expect(startupDefinitions).toEqual(definitions);
|
||||
expect(Object.isFrozen(startupDefinitions)).toBe(true);
|
||||
@@ -128,6 +145,7 @@ describe('bundled coding plugin manifests', () => {
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
|
||||
path.resolve('tmp/data-service'),
|
||||
path.resolve('tmp/game-resource'),
|
||||
path.resolve('tmp/project-scaffold'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -1419,8 +1419,8 @@ describe('PluginPackageStore', () => {
|
||||
});
|
||||
const cases = [
|
||||
{
|
||||
name: 'unsupported asset extension',
|
||||
archive: buildSkillOnlyArchive({ 'skills/example-skill/assets/run.exe': Buffer.from('not executable') }),
|
||||
name: 'executable Skill script in a downloadable artifact',
|
||||
archive: buildSkillOnlyArchive({ 'skills/example-skill/scripts/run.mjs': Buffer.from('export {};') }),
|
||||
},
|
||||
{
|
||||
name: 'non-UTF-8 Skill text',
|
||||
|
||||
@@ -261,40 +261,45 @@ describe('buildPluginWorkspaceProjection', () => {
|
||||
});
|
||||
|
||||
it('keeps bundled and downloadable official command sets inside their delivery boundaries', () => {
|
||||
const bundledCatalog = {
|
||||
...catalog.items[0]!,
|
||||
pluginId: 'makelore.game-resource',
|
||||
title: 'Game Resource',
|
||||
runtimeKind: 'bundled_typed' as const,
|
||||
};
|
||||
const bundledLibrary = {
|
||||
...library.items[0]!,
|
||||
pluginId: bundledCatalog.pluginId,
|
||||
title: bundledCatalog.title,
|
||||
};
|
||||
const bundledProject = {
|
||||
...project,
|
||||
items: [{
|
||||
...project.items[0]!,
|
||||
id: bundledCatalog.pluginId,
|
||||
displayName: bundledCatalog.title,
|
||||
enabled: false,
|
||||
state: 'disabled' as const,
|
||||
}],
|
||||
};
|
||||
for (const [pluginId, title, runtimeKind] of [
|
||||
['makelore.game-resource', 'Game Resource', 'platform_hosted'],
|
||||
['makelore.project-scaffold', 'Project Scaffold', 'skill_only'],
|
||||
] as const) {
|
||||
const bundledCatalog = {
|
||||
...catalog.items[0]!,
|
||||
pluginId,
|
||||
title,
|
||||
runtimeKind,
|
||||
};
|
||||
const bundledLibrary = {
|
||||
...library.items[0]!,
|
||||
pluginId: bundledCatalog.pluginId,
|
||||
title: bundledCatalog.title,
|
||||
};
|
||||
const bundledProject = {
|
||||
...project,
|
||||
items: [{
|
||||
...project.items[0]!,
|
||||
id: bundledCatalog.pluginId,
|
||||
displayName: bundledCatalog.title,
|
||||
enabled: false,
|
||||
state: 'disabled' as const,
|
||||
}],
|
||||
};
|
||||
|
||||
expect(commandKinds({
|
||||
catalog: { ...catalog, items: [bundledCatalog] },
|
||||
library: { ...library, items: [] },
|
||||
marketplaceInstallations: {},
|
||||
project: null,
|
||||
})).toEqual(['acquire']);
|
||||
expect(commandKinds({
|
||||
catalog: { ...catalog, items: [bundledCatalog] },
|
||||
library: { ...library, items: [bundledLibrary] },
|
||||
marketplaceInstallations: {},
|
||||
project: bundledProject,
|
||||
})).toEqual(['remove_from_library', 'enable_project', 'open_agent_assignment']);
|
||||
expect(commandKinds({
|
||||
catalog: { ...catalog, items: [bundledCatalog] },
|
||||
library: { ...library, items: [] },
|
||||
marketplaceInstallations: {},
|
||||
project: null,
|
||||
})).toEqual(['acquire']);
|
||||
expect(commandKinds({
|
||||
catalog: { ...catalog, items: [bundledCatalog] },
|
||||
library: { ...library, items: [bundledLibrary] },
|
||||
marketplaceInstallations: {},
|
||||
project: bundledProject,
|
||||
})).toEqual(['remove_from_library', 'enable_project', 'open_agent_assignment']);
|
||||
}
|
||||
|
||||
expect(commandKinds({ marketplaceInstallations: {} })).toEqual([
|
||||
'remove_from_library',
|
||||
|
||||
@@ -3,18 +3,22 @@
|
||||
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 { 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 { loadBundledCodingPluginDefinitions } from '@electron/coding-plugins/manifest';
|
||||
import { createCodingProjectMetadata } from '@electron/coding-projects/project-config';
|
||||
import { createStaticProjectPackage } from '@electron/services/project-packager';
|
||||
import type { ProjectType } from '../../shared/project-config';
|
||||
import {
|
||||
PROJECT_SCAFFOLD_BUNDLED_RELEASE_ID,
|
||||
PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||
isCodeOwnedOptionalBundledPluginId,
|
||||
} from '../../shared/coding-plugins';
|
||||
|
||||
const pluginRoot = resolve('plugins/makelore-project-scaffold');
|
||||
const pluginRoot = resolve('resources/coding-plugins/project-scaffold');
|
||||
const scaffoldScript = join(
|
||||
pluginRoot,
|
||||
'skills',
|
||||
@@ -42,23 +46,30 @@ afterEach(async () => {
|
||||
});
|
||||
|
||||
describe('MakeLore project scaffold plugin', () => {
|
||||
it('is discovered as a skill-only package with executable Skill scripts', async () => {
|
||||
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 pluginManifest = JSON.parse(await readFile(
|
||||
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: pluginManifest.name,
|
||||
version: pluginManifest.version,
|
||||
name: marketplaceManifest.name,
|
||||
version: marketplaceManifest.version,
|
||||
});
|
||||
expect(pluginManifest.skills).toBe('./skills/');
|
||||
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: 'makelore-project-scaffold',
|
||||
resolvedVersion: '0.1.0',
|
||||
packageId: PROJECT_SCAFFOLD_PLUGIN_ID,
|
||||
resolvedVersion: '1.0.0',
|
||||
extensionEntries: [],
|
||||
hasSkillScripts: true,
|
||||
ignoredLifecycleScripts: [],
|
||||
@@ -67,30 +78,25 @@ describe('MakeLore project scaffold plugin', () => {
|
||||
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('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));
|
||||
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'))
|
||||
@@ -164,7 +170,7 @@ describe('MakeLore project scaffold plugin', () => {
|
||||
});
|
||||
|
||||
it('creates an interactive AI application accepted by the existing source packager', async () => {
|
||||
const projectType: ProjectType = 'interactive_ai_app';
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user