fix(marketplace): close client review findings
This commit is contained in:
@@ -61,6 +61,7 @@ test.describe('Plugin Marketplace', () => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
try {
|
||||
const page = await getStableWindow(app);
|
||||
await expect(page.getByTestId('ai-module-option-programming')).toBeVisible();
|
||||
await app.evaluate(({ ipcMain }) => {
|
||||
let installedChannel: 'stable' | 'beta' | null = 'stable';
|
||||
const installedVersion = '1.0.0';
|
||||
|
||||
@@ -139,6 +139,53 @@ describe('coding plugin bounded product service', () => {
|
||||
expect(result.items).not.toContainEqual(expect.objectContaining({ id: 'makelore.removed' }));
|
||||
});
|
||||
|
||||
it('projects a client-incompatible installed package as unavailable', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-incompatible-'));
|
||||
roots.push(root);
|
||||
await createCodingProjectMetadata(root, { now: '2026-08-28T00:00:00.000Z' });
|
||||
await createCodingProjectAgent(root, {
|
||||
id: 'builder', avatarId: 'avatar-01', roleName: '实现者', name: 'Builder',
|
||||
model: null, modelResolution: 'required', skillIds: ['notes'],
|
||||
responsibility: { mission: 'Build', owns: [], boundaries: [], collaborators: [], principles: [] },
|
||||
});
|
||||
const definition = {
|
||||
...DATA_SERVICE_PLUGIN_DEFINITION,
|
||||
id: 'makelore.notes', displayName: 'Notes', description: 'Notes',
|
||||
requiresBackend: false, runtimeKind: 'skill_only' as const,
|
||||
acquisitionMode: 'user_acquired' as const, releaseId: 'release-notes',
|
||||
provenance: { source: 'marketplace' as const, packageRoot: 'C:/packages/release-notes' },
|
||||
adapterId: '', operations: [], tools: [],
|
||||
skills: [{ id: 'notes', entryPath: 'skills/notes/SKILL.md', grants: [] }],
|
||||
surfaces: {},
|
||||
};
|
||||
const service = createCodingProjectPluginService({
|
||||
projects: { getProject: vi.fn().mockResolvedValue({ id: 'local-a', path: root }) },
|
||||
projectPlugins: {
|
||||
getEnabledPluginIds: vi.fn().mockResolvedValue([definition.id]), setEnabled: vi.fn(),
|
||||
},
|
||||
policyClient: {
|
||||
refresh: vi.fn(),
|
||||
getState: () => ({ status: 'unavailable' as const, revision: 0, lastVerifiedAt: null, catalog: null }),
|
||||
},
|
||||
adapters: [], definitions: [definition],
|
||||
effectiveResolver: {
|
||||
resolve: vi.fn().mockResolvedValue({
|
||||
accountSessionId: 'account-a\u00001', projectId: 'local-a',
|
||||
pluginReleaseIds: [], effectiveSkillIds: [], skillEntries: [],
|
||||
toolDefinitions: [], runtimePolicies: [],
|
||||
unavailableReasons: [{
|
||||
pluginId: definition.id, code: 'client_incompatible',
|
||||
message: 'Plugin Release is incompatible with this MakeLore client',
|
||||
}],
|
||||
}),
|
||||
} as never,
|
||||
});
|
||||
|
||||
await expect(service.list('local-a')).resolves.toMatchObject({
|
||||
items: [{ id: definition.id, enabled: true, state: 'unavailable' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('projects the exact three capabilities and fourteen mixed-policy operations', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-plugin-policy-join-'));
|
||||
roots.push(root);
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
createEffectivePluginResolver,
|
||||
type EffectivePluginResolver,
|
||||
} from '../../electron/coding-plugins/effective-resolver';
|
||||
import type { CodingPluginDefinition } from '../../shared/coding-plugins';
|
||||
import {
|
||||
DATA_SERVICE_PLUGIN_DEFINITION,
|
||||
type CodingPluginDefinition,
|
||||
} from '../../shared/coding-plugins';
|
||||
|
||||
const skillOnlyDefinition: CodingPluginDefinition = {
|
||||
id: 'makelore.notes',
|
||||
@@ -205,6 +208,41 @@ describe('effective plugin resolver', () => {
|
||||
expect(result.unavailableReasons).toEqual([]);
|
||||
});
|
||||
|
||||
it('reserves every bundled Skill owner, including Data Service, before accepting Marketplace packages', async () => {
|
||||
const marketplaceDataServiceCollision: CodingPluginDefinition = {
|
||||
...skillOnlyDefinition,
|
||||
id: 'makelore.data-service-shadow',
|
||||
releaseId: 'data-service-shadow-1',
|
||||
provenance: { source: 'marketplace', packageRoot: 'C:/packages/data-service-shadow-1' },
|
||||
skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md', grants: [] }],
|
||||
};
|
||||
const result = await createEffectivePluginResolver({
|
||||
definitions: [DATA_SERVICE_PLUGIN_DEFINITION, marketplaceDataServiceCollision],
|
||||
getAccountBinding: () => binding,
|
||||
getLibrary: vi.fn(async () => ({
|
||||
...library(),
|
||||
items: [{ ...library().items[0], pluginId: marketplaceDataServiceCollision.id }],
|
||||
})),
|
||||
getInstalled: vi.fn(async (pluginId: string) => pluginId === marketplaceDataServiceCollision.id
|
||||
? installed(marketplaceDataServiceCollision)
|
||||
: null),
|
||||
getEnabledPluginIds: vi.fn(async () => [
|
||||
DATA_SERVICE_PLUGIN_DEFINITION.id,
|
||||
marketplaceDataServiceCollision.id,
|
||||
]),
|
||||
}).resolve({
|
||||
projectId: 'project-a',
|
||||
projectPath: 'C:/project-a',
|
||||
assignedSkillIds: ['data-service'],
|
||||
role: 'parent',
|
||||
});
|
||||
|
||||
expect(result.pluginReleaseIds).toEqual([]);
|
||||
expect(result.unavailableReasons).not.toEqual([
|
||||
expect.objectContaining({ pluginId: marketplaceDataServiceCollision.id }),
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires current Library, installed Release, project selection, and assignment', async () => {
|
||||
const effective = resolver();
|
||||
await expect(resolve(effective)).resolves.toMatchObject({
|
||||
|
||||
@@ -86,7 +86,7 @@ function makeResolveResult(input: ResolveRequest, itemOverrides: Record<string,
|
||||
sha256: SHA256,
|
||||
sizeBytes: 1,
|
||||
releaseAdmissionId: ADMISSION_ID,
|
||||
expiresAt: '2026-08-29T00:00:00Z',
|
||||
expiresAt: '2100-01-01T00:00:00Z',
|
||||
channel: input.channel,
|
||||
reason: null,
|
||||
...itemOverrides,
|
||||
@@ -97,17 +97,20 @@ function makeResolveResult(input: ResolveRequest, itemOverrides: Record<string,
|
||||
};
|
||||
}
|
||||
|
||||
function buildSkillOnlyArchive(): Buffer {
|
||||
function buildSkillOnlyArchive(
|
||||
overrides: Readonly<Record<string, Buffer | string>> = {},
|
||||
): Buffer {
|
||||
const zip = new AdmZip();
|
||||
zip.addFile('plugin.json', Buffer.from(JSON.stringify({
|
||||
const files = new Map<string, Buffer | string>([
|
||||
['plugin.json', JSON.stringify({
|
||||
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
|
||||
name: PLUGIN_ID,
|
||||
version: '1.0.0',
|
||||
description: 'Example Skill',
|
||||
author: { name: 'MakeLore' },
|
||||
extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } },
|
||||
})));
|
||||
zip.addFile('com.makelore/capability.json', Buffer.from(JSON.stringify({
|
||||
})],
|
||||
['com.makelore/capability.json', JSON.stringify({
|
||||
schemaVersion: 2,
|
||||
pluginId: PLUGIN_ID,
|
||||
contractVersion: 1,
|
||||
@@ -115,8 +118,13 @@ function buildSkillOnlyArchive(): Buffer {
|
||||
runtime: { kind: 'skill_only' },
|
||||
skills: [{ id: 'example-skill', entry: '../skills/example-skill/SKILL.md', grants: [] }],
|
||||
tools: [],
|
||||
})));
|
||||
zip.addFile('skills/example-skill/SKILL.md', Buffer.from('# Example\n'));
|
||||
})],
|
||||
['skills/example-skill/SKILL.md', '# Example\n'],
|
||||
]);
|
||||
for (const [filePath, content] of Object.entries(overrides)) files.set(filePath, content);
|
||||
for (const [filePath, content] of files) {
|
||||
zip.addFile(filePath, Buffer.isBuffer(content) ? content : Buffer.from(content));
|
||||
}
|
||||
return zip.toBuffer();
|
||||
}
|
||||
|
||||
@@ -156,7 +164,7 @@ function signedGrant(
|
||||
sha256,
|
||||
signingKeyId: options.signingKeyId ?? 'test-key',
|
||||
descriptorSignature: signature,
|
||||
expiresAt: '2026-08-29T00:00:00Z',
|
||||
expiresAt: '2100-01-01T00:00:00Z',
|
||||
contentUrl: `/api/plugin-marketplace/v1/releases/${releaseId}/content?release_admission_id=${ADMISSION_ID}`,
|
||||
},
|
||||
publicKey: publicKey.export({ type: 'spki', format: 'der' }) as Buffer,
|
||||
@@ -749,12 +757,87 @@ describe('PluginPackageStore', () => {
|
||||
current = second;
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toMatchObject({ status: 'kept', releaseId: 'release-current' });
|
||||
await expect(store.removeUnused(PLUGIN_ID)).resolves.toMatchObject({ status: 'kept', releaseId: 'release-current' });
|
||||
await expect(store.readInstalledIndex()).resolves.toHaveLength(1);
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-current' });
|
||||
await expect(readFile(path.join(temporaryRoot, 'current.json'), 'utf8')).resolves.toContain('release-current');
|
||||
});
|
||||
|
||||
it('explicitly uninstalls every unprotected release without selecting by install time', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const archive = buildSkillOnlyArchive();
|
||||
const first = signedGrant(archive, { releaseId: 'release-old' });
|
||||
const second = signedGrant(archive, { releaseId: 'release-current', signingKeyId: 'test-key-2' });
|
||||
let current = first;
|
||||
const marketplace: MarketplaceClient = {
|
||||
resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
|
||||
releaseId: current.grant.releaseId,
|
||||
sha256: current.grant.sha256,
|
||||
sizeBytes: current.grant.sizeBytes,
|
||||
})),
|
||||
issueDownload: vi.fn(async () => current.grant),
|
||||
downloadContent: async () => archive,
|
||||
getCurrentAccountBinding: () => ACCOUNT_A,
|
||||
} as MarketplaceClient;
|
||||
const store = new PluginPackageStore({
|
||||
rootDir: temporaryRoot,
|
||||
marketplace,
|
||||
getAccountBinding: () => ACCOUNT_A,
|
||||
keyStore: new Map([['test-key', first.publicKey], ['test-key-2', second.publicKey]]),
|
||||
clientVersion: '1.0.0',
|
||||
});
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
current = second;
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toMatchObject({
|
||||
status: 'removed', pluginId: PLUGIN_ID, releaseId: 'release-current',
|
||||
});
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull();
|
||||
await expect(readFile(path.join(temporaryRoot, 'current.json'), 'utf8')).resolves.toContain('"current":{}');
|
||||
});
|
||||
|
||||
it('removes an unprotected current Release while retaining only an active-worker Release', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const archive = buildSkillOnlyArchive();
|
||||
const protectedRelease = signedGrant(archive, { releaseId: 'release-worker' });
|
||||
const selectedRelease = signedGrant(archive, {
|
||||
releaseId: 'release-current', signingKeyId: 'test-key-current',
|
||||
});
|
||||
let current = protectedRelease;
|
||||
const marketplace: MarketplaceClient = {
|
||||
resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
|
||||
releaseId: current.grant.releaseId,
|
||||
sha256: current.grant.sha256,
|
||||
sizeBytes: current.grant.sizeBytes,
|
||||
})),
|
||||
issueDownload: vi.fn(async () => current.grant),
|
||||
downloadContent: async () => archive,
|
||||
getCurrentAccountBinding: () => ACCOUNT_A,
|
||||
} as MarketplaceClient;
|
||||
const store = new PluginPackageStore({
|
||||
rootDir: temporaryRoot, marketplace, getAccountBinding: () => ACCOUNT_A,
|
||||
keyStore: new Map([
|
||||
['test-key', protectedRelease.publicKey],
|
||||
['test-key-current', selectedRelease.publicKey],
|
||||
]),
|
||||
clientVersion: '1.0.0',
|
||||
});
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
current = selectedRelease;
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
store.registerActiveWorker('release-worker');
|
||||
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({
|
||||
status: 'kept', pluginId: PLUGIN_ID, reason: 'active_worker_reference',
|
||||
});
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([
|
||||
expect.objectContaining({ releaseId: 'release-worker' }),
|
||||
]);
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('preserves the old release across download, signature, and extraction failures', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const oldArchive = buildSkillOnlyArchive();
|
||||
@@ -886,6 +969,82 @@ describe('PluginPackageStore', () => {
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('captures the account before queued package mutations execute', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const archive = buildSkillOnlyArchive();
|
||||
const { grant, publicKey } = signedGrant(archive);
|
||||
let binding: AccountBinding | null = ACCOUNT_A;
|
||||
const firstResolve = deferred<ResolveSnapshot>();
|
||||
const resolve = vi.fn(async (input: ResolveRequest) => (
|
||||
resolve.mock.calls.length === 1
|
||||
? firstResolve.promise
|
||||
: makeResolveResult(input, { sha256: grant.sha256, sizeBytes: grant.sizeBytes })
|
||||
));
|
||||
const marketplace: MarketplaceClient = {
|
||||
resolve,
|
||||
issueDownload: vi.fn(async () => grant),
|
||||
downloadContent: async () => archive,
|
||||
getCurrentAccountBinding: () => binding,
|
||||
} as MarketplaceClient;
|
||||
const store = new PluginPackageStore({
|
||||
rootDir: temporaryRoot,
|
||||
marketplace,
|
||||
clientVersion: '1.0.0',
|
||||
keyStore: new Map([['test-key', publicKey]]),
|
||||
getAccountBinding: () => binding,
|
||||
});
|
||||
|
||||
const inFlight = store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
await vi.waitFor(() => expect(resolve).toHaveBeenCalledTimes(1));
|
||||
const queued = store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
binding = ACCOUNT_B;
|
||||
firstResolve.resolve(makeResolveResult({ makeloreVersion: '1.0.0', channel: 'stable' }));
|
||||
|
||||
await expect(inFlight).rejects.toMatchObject({ code: 'plugin_account_changed' });
|
||||
await expect(queued).rejects.toMatchObject({ code: 'plugin_account_changed' });
|
||||
expect(resolve).toHaveBeenCalledTimes(1);
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('does not let a queued uninstall switch accounts while waiting', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const archive = buildSkillOnlyArchive();
|
||||
const { grant, publicKey } = signedGrant(archive);
|
||||
let binding: AccountBinding | null = ACCOUNT_A;
|
||||
const resolve = vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
|
||||
sha256: grant.sha256,
|
||||
sizeBytes: grant.sizeBytes,
|
||||
}));
|
||||
const marketplace: MarketplaceClient = {
|
||||
resolve,
|
||||
issueDownload: vi.fn(async () => grant),
|
||||
downloadContent: async () => archive,
|
||||
getCurrentAccountBinding: () => binding,
|
||||
} as MarketplaceClient;
|
||||
const store = new PluginPackageStore({
|
||||
rootDir: temporaryRoot,
|
||||
marketplace,
|
||||
clientVersion: '1.0.0',
|
||||
keyStore: new Map([['test-key', publicKey]]),
|
||||
getAccountBinding: () => binding,
|
||||
});
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
|
||||
const firstResolve = deferred<ResolveSnapshot>();
|
||||
resolve.mockImplementationOnce(async () => firstResolve.promise);
|
||||
const inFlight = store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
await vi.waitFor(() => expect(resolve).toHaveBeenCalledTimes(2));
|
||||
const queued = store.uninstall(PLUGIN_ID);
|
||||
binding = ACCOUNT_B;
|
||||
firstResolve.resolve(makeResolveResult({ makeloreVersion: '1.0.0', channel: 'stable' }, {
|
||||
action: 'keep',
|
||||
}));
|
||||
|
||||
await expect(inFlight).rejects.toMatchObject({ code: 'plugin_account_changed' });
|
||||
await expect(queued).rejects.toMatchObject({ code: 'plugin_account_changed' });
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID });
|
||||
});
|
||||
|
||||
it('rejects archive traversal before materializing a package', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const zip = new AdmZip();
|
||||
@@ -912,6 +1071,68 @@ describe('PluginPackageStore', () => {
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('enforces the canonical archive contract before installing a package', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const oversizedManifest = JSON.stringify({
|
||||
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
|
||||
name: PLUGIN_ID,
|
||||
version: '1.0.0',
|
||||
description: 'x'.repeat(256 * 1024),
|
||||
author: { name: 'MakeLore' },
|
||||
extensions: { 'com.makelore': { capabilityManifest: './com.makelore/capability.json' } },
|
||||
});
|
||||
const cases = [
|
||||
{
|
||||
name: 'unsupported asset extension',
|
||||
archive: buildSkillOnlyArchive({ 'skills/example-skill/assets/run.exe': Buffer.from('not executable') }),
|
||||
},
|
||||
{
|
||||
name: 'non-UTF-8 Skill text',
|
||||
archive: buildSkillOnlyArchive({ 'skills/example-skill/SKILL.md': Buffer.from([0xff, 0xfe, 0xfd]) }),
|
||||
},
|
||||
{
|
||||
name: 'non-UTF-8 capability manifest',
|
||||
archive: buildSkillOnlyArchive({ 'com.makelore/capability.json': Buffer.from([0xff, 0xfe, 0xfd]) }),
|
||||
},
|
||||
{
|
||||
name: 'asset outside a declared Skill',
|
||||
archive: buildSkillOnlyArchive({ 'skills/other-skill/notes.md': '# Hidden\n' }),
|
||||
},
|
||||
{
|
||||
name: 'oversized manifest',
|
||||
archive: buildSkillOnlyArchive({ 'plugin.json': oversizedManifest }),
|
||||
},
|
||||
{
|
||||
name: 'oversized Skill',
|
||||
archive: buildSkillOnlyArchive({ 'skills/example-skill/SKILL.md': Buffer.alloc(256 * 1024 + 1, 0x61) }),
|
||||
},
|
||||
];
|
||||
|
||||
for (const [index, scenario] of cases.entries()) {
|
||||
const { grant, publicKey } = signedGrant(scenario.archive, { releaseId: `release-invalid-${index}` });
|
||||
const marketplace: MarketplaceClient = {
|
||||
resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
|
||||
releaseId: grant.releaseId,
|
||||
sha256: grant.sha256,
|
||||
sizeBytes: grant.sizeBytes,
|
||||
})),
|
||||
issueDownload: vi.fn(async () => grant),
|
||||
downloadContent: async () => scenario.archive,
|
||||
getCurrentAccountBinding: () => ACCOUNT_A,
|
||||
} as MarketplaceClient;
|
||||
const store = new PluginPackageStore({
|
||||
rootDir: temporaryRoot,
|
||||
marketplace,
|
||||
clientVersion: '1.0.0',
|
||||
keyStore: new Map([['test-key', publicKey]]),
|
||||
getAccountBinding: () => ACCOUNT_A,
|
||||
});
|
||||
await expect(store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' }), scenario.name)
|
||||
.rejects.toMatchObject({ code: 'plugin_artifact_invalid' });
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('requires explicit beta selection and rejects content paths outside the server route', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(response({
|
||||
release_admission_id: ADMISSION_ID,
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 { finished } from 'node:stream/promises';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
verifyBundledCodingPluginResources,
|
||||
defaultProductExecutable,
|
||||
readPackagedMarketplaceTrustSource,
|
||||
verifyPackagedMarketplaceClientArtifact,
|
||||
validatePiArtifactMetadata,
|
||||
verifyMarketplaceClientArtifact,
|
||||
} from '../../scripts/lib/pi-product-artifact.mjs';
|
||||
@@ -24,6 +26,11 @@ const roots: string[] = [];
|
||||
const PI_PACKAGE = '@earendil-works/pi-coding-agent';
|
||||
const { createPackage } = createRequire(import.meta.url)('@electron/asar');
|
||||
|
||||
async function createAsarFixture(source: string, archive: string) {
|
||||
const output = await createPackage(source, archive);
|
||||
await finished(output);
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
||||
});
|
||||
@@ -149,7 +156,7 @@ describe('final Pi product artifact verification', () => {
|
||||
);
|
||||
await writeFile(path.join(source, 'resources', 'opencode-runtime', 'legacy.js'), 'export {};');
|
||||
const archive = path.join(root, 'app.asar');
|
||||
await createPackage(source, archive);
|
||||
await createAsarFixture(source, archive);
|
||||
|
||||
const inventory = collectForbiddenAsarPaths(archive);
|
||||
expect(inventory.entryCount).toBeGreaterThan(2);
|
||||
@@ -240,10 +247,11 @@ describe('final Pi product artifact verification', () => {
|
||||
await writeFile(path.join(source, 'dist-electron', 'main.js'), [
|
||||
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
||||
'const unrelatedConfiguration = process.env.NIANCODE_E2E;',
|
||||
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
||||
'export const marketplace = true;',
|
||||
].join('\n'));
|
||||
const appAsar = path.join(root, 'app.asar');
|
||||
await createPackage(source, appAsar);
|
||||
await createAsarFixture(source, appAsar);
|
||||
await expect(readPackagedMarketplaceTrustSource(appAsar)).resolves.toBe(
|
||||
'CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({})',
|
||||
);
|
||||
@@ -260,7 +268,7 @@ describe('final Pi product artifact verification', () => {
|
||||
'function createTrust() { return Object.freeze({ get: load, sourceMarker: "makelore.plugin-trust.code-owned.v1" }); }',
|
||||
].join('\n'));
|
||||
const appAsar = path.join(root, 'app.asar');
|
||||
await createPackage(source, appAsar);
|
||||
await createAsarFixture(source, appAsar);
|
||||
await expect(readPackagedMarketplaceTrustSource(appAsar)).resolves.toContain(
|
||||
'makelore.plugin-trust.code-owned.v1',
|
||||
);
|
||||
@@ -289,10 +297,59 @@ describe('final Pi product artifact verification', () => {
|
||||
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
||||
].join('\n'));
|
||||
const appAsar = path.join(root, 'app.asar');
|
||||
await createPackage(source, appAsar);
|
||||
await createAsarFixture(source, appAsar);
|
||||
await expect(readPackagedMarketplaceTrustSource(appAsar)).rejects.toThrow('trust source');
|
||||
});
|
||||
|
||||
it('binds Marketplace route, Renderer, and effective markers to the package main graph', async () => {
|
||||
const root = await mkdtemp(path.join(tmpdir(), 'makelore-marketplace-contract-asar-'));
|
||||
roots.push(root);
|
||||
const source = path.join(root, 'source');
|
||||
await mkdir(path.join(source, 'dist-electron'), { recursive: true });
|
||||
await mkdir(path.join(source, 'dist', 'assets'), { recursive: true });
|
||||
await writeFile(path.join(source, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
||||
await writeFile(path.join(source, 'dist-electron', 'main.js'), [
|
||||
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
||||
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
||||
'loadFile("../dist/index.html");',
|
||||
].join('\n'));
|
||||
await writeFile(path.join(source, 'dist', 'index.html'), [
|
||||
'<script type="module" src="./assets/index.js"></script>',
|
||||
].join('\n'));
|
||||
await writeFile(path.join(source, 'dist', 'assets', 'index.js'), 'import("./project-plugins.js");');
|
||||
await writeFile(
|
||||
path.join(source, 'dist', 'assets', 'project-plugins.js'),
|
||||
'import{marketplace}from"./plugin-marketplace.js";export{marketplace};',
|
||||
);
|
||||
await writeFile(path.join(source, 'dist', 'assets', 'plugin-marketplace.js'), [
|
||||
'makelore-plugin-release.v1 skill_only plugin_signature_invalid signing key is not trusted',
|
||||
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件',
|
||||
].join('\n'));
|
||||
const appAsar = path.join(root, 'app.asar');
|
||||
await createAsarFixture(source, appAsar);
|
||||
await expect(verifyPackagedMarketplaceClientArtifact(appAsar)).resolves.toMatchObject({ result: 'pass' });
|
||||
|
||||
const staleSource = path.join(root, 'stale-source');
|
||||
await mkdir(path.join(staleSource, 'dist-electron'), { recursive: true });
|
||||
await writeFile(path.join(staleSource, 'package.json'), JSON.stringify({ main: 'dist-electron/main.js' }));
|
||||
await writeFile(path.join(staleSource, 'dist-electron', 'main.js'), [
|
||||
'const CODE_OWNED_PLUGIN_SIGNING_KEYS = Object.freeze({});',
|
||||
'export const sourceMarker = "makelore.plugin-trust.code-owned.v1";',
|
||||
].join('\n'));
|
||||
await writeFile(path.join(staleSource, 'dist-electron', 'stale-marketplace.js'), [
|
||||
'makelore-plugin-release.v1 skill_only plugin_signature_invalid signing key is not trusted',
|
||||
'/api/coding/plugin-marketplace plugin-marketplace\\/install\\/ plugin-marketplace\\/update\\/',
|
||||
'effectiveSkillIds pluginReleaseIds',
|
||||
'/api/coding/plugin-marketplace/catalog /api/coding/plugin-marketplace/library 免费获取 我的插件',
|
||||
].join('\n'));
|
||||
const staleAsar = path.join(root, 'stale.asar');
|
||||
await createAsarFixture(staleSource, staleAsar);
|
||||
await expect(verifyPackagedMarketplaceClientArtifact(staleAsar))
|
||||
.rejects.toThrow('Marketplace contract markers');
|
||||
});
|
||||
|
||||
it('rejects a packaged plugin tree that drops an SDK asset or catalog marker', async () => {
|
||||
const fixture = await bundledResourceFixture();
|
||||
await rm(path.join(
|
||||
|
||||
@@ -154,6 +154,25 @@ describe('My Plugins', () => {
|
||||
expect(screen.getByText('设备版本 1.5.0')).toBeVisible();
|
||||
});
|
||||
|
||||
it('does not present a no-version unavailable projection as an installed device package', () => {
|
||||
render(<MemoryRouter><MyPluginsView
|
||||
library={{ ...library, items: [library.items[0]] }}
|
||||
installations={{
|
||||
'makelore.notes': {
|
||||
status: 'unavailable', pluginId: 'makelore.notes', reason: 'plugin_incompatible_client',
|
||||
},
|
||||
}}
|
||||
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={vi.fn()}
|
||||
onInstallBeta={vi.fn()} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
|
||||
/></MemoryRouter>);
|
||||
|
||||
expect(screen.getByText('尚未下载到设备')).toBeVisible();
|
||||
expect(screen.getByRole('button', { name: '下载灵感笔记' })).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: '删除设备上的灵感笔记' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('设备版本已是最新')).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/当前 MakeLore 版本不兼容/)).toBeVisible();
|
||||
});
|
||||
|
||||
it('honestly groups stable Main failure codes without claiming hidden authority', () => {
|
||||
const { rerender } = render(<MemoryRouter><MyPluginsView
|
||||
library={{ ...library, items: [library.items[0]] }} installations={{}} state="ready"
|
||||
|
||||
@@ -141,6 +141,31 @@ describe('plugin Marketplace store', () => {
|
||||
expect(store.getState().installations['makelore.data']).toMatchObject({ status: 'installed' });
|
||||
});
|
||||
|
||||
it('merges same-plugin Library and device mutations in either completion order', async () => {
|
||||
for (const order of ['library-then-device', 'device-then-library'] as const) {
|
||||
const acquire = deferred<MarketplaceLibraryProjection>();
|
||||
const install = deferred<MarketplaceInstallation>();
|
||||
const store = createPluginMarketplaceStore({
|
||||
acquire: vi.fn(() => acquire.promise),
|
||||
install: vi.fn(() => install.promise),
|
||||
});
|
||||
store.getState().activateAccount('account-a');
|
||||
const libraryMutation = store.getState().acquire('makelore.notes');
|
||||
const deviceMutation = store.getState().install('makelore.notes');
|
||||
const installed = { status: 'installed' as const, pluginId: 'makelore.notes', version: '1.0.0' };
|
||||
const acquired = { library: library('Notes acquired'), installations: [] };
|
||||
if (order === 'library-then-device') {
|
||||
acquire.resolve(acquired); await libraryMutation;
|
||||
install.resolve(installed); await deviceMutation;
|
||||
} else {
|
||||
install.resolve(installed); await deviceMutation;
|
||||
acquire.resolve(acquired); await libraryMutation;
|
||||
}
|
||||
expect(store.getState().library?.items[0]?.title).toBe('Notes acquired');
|
||||
expect(store.getState().installations['makelore.notes']).toMatchObject(installed);
|
||||
}
|
||||
});
|
||||
|
||||
it('calls only the selected state mutation', async () => {
|
||||
const acquire = vi.fn().mockResolvedValue(projection('Acquired'));
|
||||
const install = vi.fn().mockResolvedValue({ status: 'installed', pluginId: 'makelore.notes', version: '1.0.0' });
|
||||
|
||||
Reference in New Issue
Block a user