fix(plugins): load official hosted plugins from bundled resources
This commit is contained in:
@@ -395,4 +395,58 @@ describe('effective plugin resolver', () => {
|
||||
})).resolves.toMatchObject({ effectiveSkillIds: ['notes'], toolDefinitions: [], runtimePolicies: [] });
|
||||
expect(refresh).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('materializes an acquired code-owned bundled hosted Plugin without Package Store bytes', async () => {
|
||||
const bundled: CodingPluginDefinition = {
|
||||
...serverDefinition,
|
||||
id: 'makelore.web-search',
|
||||
releaseId: '00000000-0000-4000-8000-000000000204',
|
||||
provenance: { source: 'bundled', packageRoot: 'web-search' },
|
||||
skills: [{
|
||||
id: 'makelore-web-search',
|
||||
entryPath: 'skills/makelore-web-search/SKILL.md',
|
||||
grants: ['remote.read'],
|
||||
}],
|
||||
};
|
||||
const getInstalled = vi.fn(async () => null);
|
||||
const getInstalledRelease = vi.fn(async () => null);
|
||||
const policy = currentPolicy();
|
||||
policy.catalog.plugins[0] = { ...policy.catalog.plugins[0], plugin_id: bundled.id };
|
||||
const effective = createEffectivePluginResolver({
|
||||
definitions: [bundled],
|
||||
getAccountBinding: () => binding,
|
||||
getLibrary: vi.fn(async () => ({
|
||||
...library(),
|
||||
items: [{ ...library().items[0], pluginId: bundled.id }],
|
||||
})),
|
||||
packageStore: {
|
||||
readInstalledIndex: vi.fn(async () => []),
|
||||
getInstalled,
|
||||
getInstalledRelease,
|
||||
},
|
||||
getEnabledPluginIds: vi.fn(async () => [bundled.id]),
|
||||
policyClient: { getState: () => policy, refresh: vi.fn() },
|
||||
});
|
||||
|
||||
await expect(effective.resolve({
|
||||
projectId: 'project-a',
|
||||
projectPath: 'C:/project-a',
|
||||
assignedSkillIds: ['makelore-web-search'],
|
||||
role: 'parent',
|
||||
})).resolves.toMatchObject({
|
||||
pluginReleaseIds: [bundled.releaseId],
|
||||
effectiveSkillIds: ['makelore-web-search'],
|
||||
skillEntries: [{
|
||||
id: 'makelore-web-search',
|
||||
entryPath: 'skills/makelore-web-search/SKILL.md',
|
||||
}],
|
||||
toolDefinitions: [{ name: 'remote_read' }],
|
||||
unavailableReasons: [],
|
||||
});
|
||||
expect(getInstalled).not.toHaveBeenCalled();
|
||||
await expect(effective.getSkillSources()).resolves.toEqual([]);
|
||||
await expect(effective.getInstalledDefinition(bundled.id, bundled.releaseId))
|
||||
.resolves.toMatchObject({ id: bundled.id, releaseId: bundled.releaseId });
|
||||
expect(getInstalledRelease).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from '../../shared/coding-plugins';
|
||||
|
||||
const PACKAGE_ROOT = path.resolve('resources/coding-plugins/data-service');
|
||||
const GAME_RESOURCE_ROOT = path.resolve('resources/coding-plugins/game-resource');
|
||||
const WEB_SEARCH_ROOT = path.resolve('resources/coding-plugins/web-search');
|
||||
|
||||
async function packageManifests(): Promise<{ root: Record<string, unknown>; capability: Record<string, unknown> }> {
|
||||
return {
|
||||
@@ -32,9 +34,13 @@ 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']);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([PACKAGE_ROOT]);
|
||||
expect(definitions).toHaveLength(1);
|
||||
expect(BUNDLED_CODING_PLUGIN_ROOTS).toEqual(['data-service', 'game-resource', 'web-search']);
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('resources/coding-plugins'))).toEqual([
|
||||
PACKAGE_ROOT,
|
||||
GAME_RESOURCE_ROOT,
|
||||
WEB_SEARCH_ROOT,
|
||||
]);
|
||||
expect(definitions).toHaveLength(3);
|
||||
expect(definitions[0]).toMatchObject({
|
||||
id: 'makelore.data-service',
|
||||
adapterId: 'data-service',
|
||||
@@ -42,7 +48,21 @@ describe('bundled coding plugin manifests', () => {
|
||||
skills: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
|
||||
});
|
||||
expect(definitions[0]?.tools.map(({ name }) => name)).toEqual(DATA_SERVICE_TOOL_NAMES);
|
||||
expect(definitions).toEqual([DATA_SERVICE_PLUGIN_DEFINITION]);
|
||||
expect(definitions[0]).toEqual(DATA_SERVICE_PLUGIN_DEFINITION);
|
||||
expect(definitions.slice(1)).toMatchObject([
|
||||
{
|
||||
id: 'makelore.game-resource', version: '1.0.0', runtimeKind: 'platform_hosted',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000105',
|
||||
provenance: { source: 'bundled', packageRoot: 'game-resource' },
|
||||
skills: [{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' }],
|
||||
},
|
||||
{
|
||||
id: 'makelore.web-search', version: '1.0.0', runtimeKind: 'platform_hosted',
|
||||
acquisitionMode: 'user_acquired', releaseId: '00000000-0000-4000-8000-000000000204',
|
||||
provenance: { source: 'bundled', packageRoot: 'web-search' },
|
||||
skills: [{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' }],
|
||||
},
|
||||
]);
|
||||
expect(startupDefinitions).toEqual(definitions);
|
||||
expect(Object.isFrozen(startupDefinitions)).toBe(true);
|
||||
expect(Object.isFrozen(startupDefinitions[0]?.operations)).toBe(true);
|
||||
@@ -115,6 +135,8 @@ describe('bundled coding plugin manifests', () => {
|
||||
it('does not infer package roots from arbitrary directories', async () => {
|
||||
expect(resolveBundledCodingPluginRootPaths(path.resolve('tmp'))).toEqual([
|
||||
path.resolve('tmp/data-service'),
|
||||
path.resolve('tmp/game-resource'),
|
||||
path.resolve('tmp/web-search'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -135,4 +135,30 @@ describe('MarketplaceHostedAdmissionResolver', () => {
|
||||
retryable: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves a code-owned bundled Release without consulting the device Package Store', async () => {
|
||||
const getInstalledRelease = vi.fn(async () => null);
|
||||
const resolve = vi.fn(async () => resolved({ sha256: null, sizeBytes: null }));
|
||||
const options = {
|
||||
packageStore: { getInstalledRelease },
|
||||
marketplace: { resolve },
|
||||
makeloreVersion: '2.0.0',
|
||||
bundledReleases: {
|
||||
[PLUGIN_ID]: { releaseId: RELEASE_ID, version: '1.0.0', channel: 'stable' as const },
|
||||
},
|
||||
} as unknown as ConstructorParameters<typeof MarketplaceHostedAdmissionResolver>[0];
|
||||
const bundledResolver = new MarketplaceHostedAdmissionResolver(options);
|
||||
|
||||
await expect(bundledResolver.resolve({
|
||||
pluginId: PLUGIN_ID,
|
||||
workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID },
|
||||
})).resolves.toEqual({ releaseId: RELEASE_ID, releaseAdmissionId: 'admission-game-1' });
|
||||
expect(getInstalledRelease).not.toHaveBeenCalled();
|
||||
expect(resolve).toHaveBeenCalledWith({
|
||||
resolveRequestId: REQUEST_ID,
|
||||
makeloreVersion: '2.0.0',
|
||||
channel: 'stable',
|
||||
installed: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -74,20 +74,24 @@ function matchingMetadata() {
|
||||
};
|
||||
}
|
||||
|
||||
async function bundledResourceFixture() {
|
||||
async function bundledResourceFixture({ schemaVersion = 1, includeSdkAssets = true } = {}) {
|
||||
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 = {
|
||||
schemaVersion,
|
||||
pluginId: 'example.plugin',
|
||||
adapterId: 'example-adapter',
|
||||
...(schemaVersion === 2
|
||||
? { runtime: { kind: 'platform_hosted', protocol: 'makelore-hosted.v1' } }
|
||||
: {}),
|
||||
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(pluginRoot, 'skills', 'example'), { 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',
|
||||
@@ -96,8 +100,11 @@ async function bundledResourceFixture() {
|
||||
}));
|
||||
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');
|
||||
if (includeSdkAssets) {
|
||||
await mkdir(path.join(pluginRoot, 'skills', 'example', 'assets'), { recursive: true });
|
||||
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 });
|
||||
@@ -206,6 +213,20 @@ describe('final Pi product artifact verification', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts schema-2 hosted plugins whose implementation has no client SDK assets', async () => {
|
||||
const fixture = await bundledResourceFixture({ schemaVersion: 2, includeSdkAssets: false });
|
||||
await expect(verifyBundledCodingPluginResources(fixture)).resolves.toMatchObject({
|
||||
packages: [{
|
||||
id: 'example.plugin',
|
||||
skills: [{ id: 'example', path: 'skills/example/SKILL.md' }],
|
||||
sdkAssets: [],
|
||||
adapterId: 'example.plugin',
|
||||
tools: ['example_read'],
|
||||
}],
|
||||
result: 'pass',
|
||||
});
|
||||
});
|
||||
|
||||
it('proves the packaged Marketplace trust, routes, effective snapshot, and Renderer assets', () => {
|
||||
const artifact = Buffer.from(MARKETPLACE_ARTIFACT_TEXT);
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ async function fixtureRoot(): Promise<{
|
||||
mkdir(path.join(skillsDir, 'grilling'), { recursive: true }),
|
||||
mkdir(path.join(skillsDir, 'agent-browser'), { recursive: true }),
|
||||
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service'), { recursive: true }),
|
||||
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource'), { recursive: true }),
|
||||
mkdir(path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search'), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(path.join(projectDir, '.pi', 'skills', 'untrusted-project-skill', 'SKILL.md'), 'untrusted', 'utf8'),
|
||||
@@ -51,6 +53,16 @@ async function fixtureRoot(): Promise<{
|
||||
'---\nname: data-service\n---\n',
|
||||
'utf8',
|
||||
),
|
||||
writeFile(
|
||||
path.join(path.dirname(skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'),
|
||||
'---\nname: game-resource\n---\n',
|
||||
'utf8',
|
||||
),
|
||||
writeFile(
|
||||
path.join(path.dirname(skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'),
|
||||
'---\nname: makelore-web-search\n---\n',
|
||||
'utf8',
|
||||
),
|
||||
]);
|
||||
return { root, userDataDir, projectDir, skillsDir };
|
||||
}
|
||||
@@ -160,15 +172,21 @@ describe('Pi managed resource loader', () => {
|
||||
projectId: 'project-1',
|
||||
agentId: 'agent-1',
|
||||
prompt: 'plugin prompt',
|
||||
skillEntries: [{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' }],
|
||||
skillEntries: [
|
||||
{ id: 'data-service', entryPath: 'skills/data-service/SKILL.md' },
|
||||
{ id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md' },
|
||||
{ id: 'makelore-web-search', entryPath: 'skills/makelore-web-search/SKILL.md' },
|
||||
],
|
||||
catalogRevision: 13,
|
||||
bundledSkillsDir: fixture.skillsDir,
|
||||
revision: { provider: 1, resources: 1 },
|
||||
});
|
||||
|
||||
expect(resources.skillIds).toEqual(['data-service']);
|
||||
expect(resources.skillIds).toEqual(['data-service', 'game-resource', 'makelore-web-search']);
|
||||
expect(resources.skillPaths).toEqual([
|
||||
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'data-service', 'skills', 'data-service', 'SKILL.md'),
|
||||
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'game-resource', 'skills', 'game-resource', 'SKILL.md'),
|
||||
path.join(path.dirname(fixture.skillsDir), 'coding-plugins', 'web-search', 'skills', 'makelore-web-search', 'SKILL.md'),
|
||||
]);
|
||||
expect(resources.catalogRevision).toBe(13);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,29 @@ describe('My Plugins', () => {
|
||||
expect(screen.getByRole('link', { name: '分配给伙伴' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('shows acquired code-owned bundled Plugins as supplied without device download actions', () => {
|
||||
const bundled = {
|
||||
...library.items[0],
|
||||
pluginId: 'makelore.web-search',
|
||||
title: '联网搜索',
|
||||
stableVersion: '1.0.0',
|
||||
betaVersion: null,
|
||||
};
|
||||
render(<MemoryRouter><MyPluginsView
|
||||
library={{ ...library, items: [bundled] }} installations={{}} 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('随 MakeLore 提供')).toBeVisible();
|
||||
expect(screen.queryByText('尚未下载到设备')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '下载联网搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '更新联网搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: '删除设备上的联网搜索' })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: '移除联网搜索' })).toBeVisible();
|
||||
expect(screen.getByRole('link', { name: '启用到项目' })).toBeVisible();
|
||||
});
|
||||
|
||||
it('updates an installed Beta package only through the explicit Beta action', () => {
|
||||
const onUpdate = vi.fn();
|
||||
const onInstallBeta = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user