feat(plugins): move game resources behind marketplace

This commit is contained in:
2026-08-31 10:45:20 +08:00
parent 62304dc85b
commit fe656dd865
33 changed files with 1413 additions and 1018 deletions

View File

@@ -12,7 +12,10 @@ import {
DataServiceCloudClient,
type DataServiceOperations,
} from '../../electron/services/data-service-client';
import { DATA_SERVICE_PLUGIN_DEFINITION } from '../../shared/coding-plugins';
import {
DATA_SERVICE_PLUGIN_DEFINITION,
type CodingPluginDefinition,
} from '../../shared/coding-plugins';
import { productToolDetailsOfResult } from '../../shared/coding-conversation-product-tool-protocol';
import type { DataServiceErrorContext, DataServiceHostResult } from '../../shared/data-service';
import type {
@@ -203,6 +206,131 @@ describe('CodingCapabilityRegistry', () => {
expect(JSON.stringify(invalidIdentity)).not.toMatch(/[0-9a-f]{8}-[0-9a-f]{4}/i);
});
it('dispatches a Marketplace-hosted tool from the frozen effective snapshot without a static tool list', async () => {
const hostedDefinition: CodingPluginDefinition = {
id: 'makelore.game-resource', version: '1.0.0', contractVersion: 1,
displayName: 'Game Resource', description: 'Hosted game resources',
runtimeKind: 'platform_hosted', acquisitionMode: 'user_acquired',
releaseId: 'release-game-1',
provenance: { source: 'marketplace', packageRoot: 'C:/packages/game-resource' },
scope: 'project', adapterId: '', requiresBackend: true,
skills: [{
id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md',
grants: ['game-resource.generate'],
}],
tools: [{
name: 'game_resource_generate', label: 'Generate', description: 'Generate an asset',
capabilityId: 'game-resource.generate', operation: 'generate', roles: ['parent'],
mutation: 'write', projectWriteLease: false,
permissions: ['hosted.game-resource.generate'], executionMode: 'job',
inputSchema: {
type: 'object', additionalProperties: false, required: ['kind'],
properties: { kind: { type: 'string' } },
},
}],
operations: [{
capabilityId: 'game-resource.generate', operation: 'generate', toolName: 'game_resource_generate',
}],
surfaces: {},
};
const hostedPolicy: PluginPolicyClientState = {
status: 'current', revision: 9, lastVerifiedAt: 1,
catalog: {
schema_version: 1, catalog_version: 'hosted-1', pricing_version: 'pricing-1',
plugins: [{
plugin_id: hostedDefinition.id, supported_contract_versions: [1], status: 'active',
capabilities: [{
capability_id: 'game-resource.generate',
operations: [{
operation: 'generate',
billing: {
mode: 'platform_metered', entitlement_scope: 'plugin_usage', notice: 'Metered',
unit_name: 'generation', unit_size: 1, rate_points: '1.00',
minimum_charge_points: '1.00', rounding_mode: 'ceil',
},
}],
}],
}],
},
};
const frozenSnapshot: EffectivePluginSnapshot = {
accountSessionId: 'account-a\u00001', projectId: context.projectId,
pluginReleaseIds: ['release-game-1'], effectiveSkillIds: ['game-resource'],
skillEntries: [{
id: 'game-resource', entryPath: 'skills/game-resource/SKILL.md',
packageRoot: 'C:/packages/game-resource',
}],
toolDefinitions: hostedDefinition.tools,
runtimePolicies: [{
pluginId: hostedDefinition.id, pluginVersion: hostedDefinition.version,
releaseId: hostedDefinition.releaseId, contractVersion: 1,
capabilityId: 'game-resource.generate', operation: 'generate',
billing: hostedPolicy.catalog!.plugins[0]!.capabilities[0]!.operations[0]!.billing,
}],
unavailableReasons: [],
};
const resolve = vi.fn(async () => frozenSnapshot);
const effectiveResolver = {
resolve,
getSkillSources: vi.fn(async () => []),
getPolicyState: vi.fn(() => hostedPolicy),
getInstalledDefinition: vi.fn(async () => hostedDefinition),
} as unknown as EffectivePluginResolver;
const invoke = vi.fn(async () => ({
success: true as const, status: 202, code: null, error: null, retryable: false as const,
payload_schema: 'game-resource.v1', data: { executionId: 'execution-a', status: 'accepted' },
billing: {
mode: 'platform_metered' as const, status: 'dispatched' as const,
reserved_points: '2.00', usage_amount: 1, unit: 'generation',
},
}));
const capabilityRegistry = registry({
definitions: [], effectiveResolver,
adapters: [{ pluginId: hostedDefinition.id, inspect: async () => ({ status: 'ready' }), invoke }],
policyClient: { getState: () => hostedPolicy, refresh: vi.fn() },
getEnabledPluginIds: async () => [hostedDefinition.id],
});
const result = await capabilityRegistry.invoke({
toolName: 'game_resource_generate',
context: { ...context, skillIds: ['game-resource'], effectiveSnapshot: frozenSnapshot },
workerRole: 'parent', effectiveSkillIds: ['game-resource'], value: { kind: 'pixel' },
});
expect(effectiveResolver.getInstalledDefinition).toHaveBeenCalledWith(
hostedDefinition.id,
hostedDefinition.releaseId,
);
expect(invoke).toHaveBeenCalledWith(expect.objectContaining({
requestId: 'pi:run-a:resource-a', workerRole: 'parent', effectiveSkillIds: ['game-resource'],
pluginReleaseId: hostedDefinition.releaseId,
}), hostedDefinition.tools[0], { kind: 'pixel' });
expect(result.details).toMatchObject({
schema: 'makelore-capability.v1', plugin_id: hostedDefinition.id,
capability_id: 'game-resource.generate', operation: 'generate',
request_id: 'pi:run-a:resource-a', success: true, status: 202,
billing: { mode: 'platform_metered', status: 'dispatched', reserved_points: '2.00' },
payload_schema: 'game-resource.v1',
});
resolve.mockResolvedValue({
...frozenSnapshot,
pluginReleaseIds: ['release-game-2'],
runtimePolicies: [{
...frozenSnapshot.runtimePolicies[0]!,
pluginVersion: '2.0.0',
releaseId: 'release-game-2',
}],
});
const stale = await capabilityRegistry.invoke({
toolName: 'game_resource_generate',
context: { ...context, skillIds: ['game-resource'], effectiveSnapshot: frozenSnapshot },
workerRole: 'parent', effectiveSkillIds: ['game-resource'], value: { kind: 'pixel' },
});
expect(stale.details).toMatchObject({ success: false, code: 'plugin_runtime_stale' });
expect(invoke).toHaveBeenCalledTimes(1);
});
it('refuses a new plugin action from an old worker after lifecycle invalidation', async () => {
const frozenSnapshot: EffectivePluginSnapshot = {
accountSessionId: 'account-a\u00001',