Files
makelore/tests/unit/pi-product-artifact.test.ts

228 lines
9.8 KiB
TypeScript

// @vitest-environment node
import { cp, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
assertNodeEngineCompatible,
classifyOpenCodeResourcePaths,
collectAbsoluteManifestValues,
collectForbiddenAsarPaths,
collectForbiddenResourcePaths,
verifyBundledCodingPluginResources,
defaultProductExecutable,
validatePiArtifactMetadata,
} from '../../scripts/lib/pi-product-artifact.mjs';
import { parsePiArtifactVerifierArgs } from '../../scripts/verify-pi-product-artifact.mjs';
const roots: string[] = [];
const PI_PACKAGE = '@earendil-works/pi-coding-agent';
const { createPackage } = createRequire(import.meta.url)('@electron/asar');
afterEach(async () => {
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
});
function matchingMetadata() {
return {
rootPackage: { dependencies: { [PI_PACKAGE]: '0.84.2' } },
lockfile: {
importers: {
'.': { dependencies: { [PI_PACKAGE]: { specifier: '0.84.2', version: '0.84.2(ws@8.20.0)' } } },
},
},
packagedPackage: { dependencies: { [PI_PACKAGE]: '0.84.2' } },
runtimePackage: {
name: PI_PACKAGE,
version: '0.84.2',
bin: { pi: 'dist/cli.js' },
},
manifest: {
runtime: {
packageName: PI_PACKAGE,
version: '0.84.2',
cliEntry: 'dist/cli.js',
nodeEngine: '>=22.19.0',
},
target: { platform: 'win32', arch: 'x64' },
},
runtimePlatform: { platform: 'win32', arch: 'x64', node: '24.18.1' },
};
}
async function bundledResourceFixture() {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-bundled-plugin-'));
roots.push(root);
const projectRoot = path.join(root, 'project');
const sourceResources = path.join(projectRoot, 'resources');
const pluginRoot = path.join(sourceResources, 'coding-plugins', 'example');
const capability = {
pluginId: 'example.plugin',
adapterId: 'example-adapter',
skills: [{ id: 'example', entry: '../skills/example/SKILL.md', grants: [] }],
tools: [{ name: 'example_read' }],
};
await mkdir(path.join(pluginRoot, 'com.makelore'), { recursive: true });
await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true });
await mkdir(path.join(sourceResources, 'coding-skills', 'agent-browser'), { recursive: true });
await writeFile(path.join(pluginRoot, 'plugin.json'), JSON.stringify({
name: 'example.plugin',
version: '1.0.0',
extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } },
}));
await writeFile(path.join(pluginRoot, 'com.makelore', 'capability.json'), JSON.stringify(capability));
await writeFile(path.join(pluginRoot, 'skills', 'example', 'SKILL.md'), '# Example Skill\n');
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.ts'), 'export {};\n');
await writeFile(path.join(pluginRoot, 'skills', 'example', 'assets', 'sdk.js'), 'export {};\n');
await writeFile(path.join(sourceResources, 'coding-skills', 'agent-browser', 'SKILL.md'), '# Browser\n');
const resourcesDirectory = path.join(root, 'packaged');
await cp(sourceResources, path.join(resourcesDirectory, 'resources'), { recursive: true });
return {
projectRoot,
resourcesDirectory,
appAsarContents: Buffer.from('example.plugin example-adapter example_read'),
};
}
describe('final Pi product artifact verification', () => {
it('requires the same pinned Pi version across package, lock, app, runtime, and manifest', () => {
expect(validatePiArtifactMetadata(matchingMetadata())).toMatchObject({
expected: '0.84.2',
rootDependency: '0.84.2',
lockedVersion: '0.84.2',
manifest: '0.84.2',
});
const mismatch = matchingMetadata();
mismatch.packagedPackage.dependencies[PI_PACKAGE] = '0.84.1';
expect(() => validatePiArtifactMetadata(mismatch)).toThrow('versions do not match');
});
it('checks the packaged Node version against the exact supported engine shape', () => {
expect(() => assertNodeEngineCompatible('>=22.19.0', '24.18.1')).not.toThrow();
expect(() => assertNodeEngineCompatible('>=22.19.0', '22.18.9')).toThrow('does not satisfy');
expect(() => assertNodeEngineCompatible('^22.19.0', '24.18.1')).toThrow('Unsupported');
});
it('rejects absolute manifest values and OpenCode-named artifact resources', async () => {
expect(collectAbsoluteManifestValues({
entry: 'dist/cli.js',
asset: 'node_modules/example/file.wasm',
leaked: 'D:\\work\\pi-runtime',
})).toEqual([{ at: '$.leaked', value: 'D:\\work\\pi-runtime' }]);
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-artifact-test-'));
roots.push(root);
await mkdir(path.join(root, 'pi-runtime', 'node_modules', 'opencode-ai'), { recursive: true });
await writeFile(path.join(root, 'pi-runtime', 'node_modules', 'opencode-ai', 'package.json'), '{}');
expect(await collectForbiddenResourcePaths(root)).toEqual([
'pi-runtime/node_modules/opencode-ai',
]);
expect(classifyOpenCodeResourcePaths([
'app.asar.unpacked/resources/opencode-runtime',
'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode-codex-responses.js',
])).toEqual({
productOwned: ['app.asar.unpacked/resources/opencode-runtime'],
upstreamPiProvider: [
'pi-runtime/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode-codex-responses.js',
],
});
});
it('enumerates OpenCode-named paths inside app.asar instead of scanning only physical resources', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'makelore-pi-asar-paths-'));
roots.push(root);
const source = path.join(root, 'source');
await mkdir(path.join(source, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers'), {
recursive: true,
});
await mkdir(path.join(source, 'resources', 'opencode-runtime'), { recursive: true });
await writeFile(
path.join(source, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'opencode.js'),
'export {};',
);
await writeFile(path.join(source, 'resources', 'opencode-runtime', 'legacy.js'), 'export {};');
const archive = path.join(root, 'app.asar');
await createPackage(source, archive);
const inventory = collectForbiddenAsarPaths(archive);
expect(inventory.entryCount).toBeGreaterThan(2);
expect(inventory.matches).toEqual(expect.arrayContaining([
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
'app.asar/resources/opencode-runtime',
]));
expect(classifyOpenCodeResourcePaths(inventory.matches)).toMatchObject({
productOwned: expect.arrayContaining(['app.asar/resources/opencode-runtime']),
upstreamPiProvider: expect.arrayContaining([
'app.asar/node_modules/@earendil-works/pi-ai/dist/providers/opencode.js',
]),
});
});
it('proves bundled plugin manifests, Skills, SDK assets, catalog, and core resources', async () => {
const fixture = await bundledResourceFixture();
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
root: 'resources/coding-plugins',
packages: [{
id: 'example.plugin',
manifestPath: 'resources/coding-plugins/example/plugin.json',
capabilityManifestPath: 'resources/coding-plugins/example/com.makelore/capability.json',
skills: [{ id: 'example', path: 'skills/example/SKILL.md' }],
sdkAssets: expect.arrayContaining([
'resources/coding-plugins/example/skills/example/assets/sdk.ts',
'resources/coding-plugins/example/skills/example/assets/sdk.js',
]),
adapterId: 'example-adapter',
tools: ['example_read'],
}],
catalog: { adapters: ['example-adapter'], tools: ['example_read'], packagedInAsar: true },
coreResources: { root: 'resources/coding-skills', skills: ['agent-browser'] },
result: 'pass',
});
});
it('rejects a packaged plugin tree that drops an SDK asset or catalog marker', async () => {
const fixture = await bundledResourceFixture();
await rm(path.join(
fixture.resourcesDirectory,
'resources',
'coding-plugins',
'example',
'skills',
'example',
'assets',
'sdk.js',
));
await expect(verifyBundledCodingPluginResources(fixture)).rejects.toThrow('files differ');
const complete = await bundledResourceFixture();
await expect(verifyBundledCodingPluginResources({
...complete,
appAsarContents: Buffer.from('example.plugin example-adapter'),
})).rejects.toThrow('adapter/tool catalog');
});
it('uses final unpacked-product paths and strictly parses verifier options', () => {
expect(defaultProductExecutable('/repo', 'linux'))
.toBe(path.resolve('/repo', 'release', 'linux-unpacked', 'niancode'));
expect(defaultProductExecutable('D:\\repo', 'win32'))
.toBe(path.resolve('D:\\repo', 'release', 'win-unpacked', 'Makelore.exe'));
expect(parsePiArtifactVerifierArgs([
'--app-exe', 'release/custom/Makelore.exe',
'--samples', '5',
'--timeout-ms', '12000',
'--report', 'release/evidence/pi.json',
], 'D:\\repo')).toMatchObject({ samples: 5, timeoutMs: 12_000 });
expect(parsePiArtifactVerifierArgs(['--', '--samples', '3'], 'D:\\repo'))
.toMatchObject({ samples: 3 });
expect(() => parsePiArtifactVerifierArgs(['--samples', '0'], 'D:\\repo'))
.toThrow('--samples must be a positive integer');
expect(() => parsePiArtifactVerifierArgs(['--unknown'], 'D:\\repo'))
.toThrow('Unknown argument');
});
});