feat(plugins): share hosted admission resolution

This commit is contained in:
2026-09-01 01:11:53 +08:00
parent c4db68767f
commit 1edd75e246
5 changed files with 474 additions and 22 deletions

View File

@@ -94,7 +94,7 @@ async function fixture() {
})),
getInstalledRelease: vi.fn(async () => ({
pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, version: '1.0.0',
sha256: 'a'.repeat(64), channel: 'stable',
sha256: 'a'.repeat(64), sizeBytes: 1, channel: 'stable',
})),
} as unknown as Pick<PluginPackageStore, 'getInstalled' | 'getInstalledRelease'>,
makeloreVersion: '2.0.0',
@@ -181,4 +181,22 @@ describe('GameResourcePluginAdapter', () => {
releaseId: RELEASE_ID, releaseAdmissionId: 'admission-a', kind: 'pixel',
});
});
it('fails closed when a worker has no frozen release instead of using the current package', async () => {
const { adapter, context, client, resolve } = await fixture();
await expect(adapter.invoke(
{ ...context, pluginReleaseId: undefined },
tool('game_resource_templates'),
{ kind: 'pixel' },
)).resolves.toMatchObject({
success: false,
status: 409,
code: 'plugin_release_unavailable',
retryable: false,
});
expect(resolve).not.toHaveBeenCalled();
expect(client.templates).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,138 @@
// @vitest-environment node
import { describe, expect, it, vi } from 'vitest';
import {
MarketplaceHostedAdmissionError,
MarketplaceHostedAdmissionResolver,
} from '../../electron/coding-plugins/hosted-admission';
import type { MarketplacePackageClientPort, PluginPackageStore } from '../../electron/coding-plugins/package-store';
const PLUGIN_ID = 'makelore.game-resource';
const RELEASE_ID = 'release-game-1';
const REQUEST_ID = 'pi:run-a:resource-a';
const DIGEST = 'a'.repeat(64);
function installed() {
return {
pluginId: PLUGIN_ID,
releaseId: RELEASE_ID,
version: '1.0.0',
packageSchemaVersion: 2,
contractVersion: 1,
runtimeKind: 'platform_hosted' as const,
sha256: DIGEST,
sizeBytes: 1,
installedAt: '2026-09-01T00:00:00.000Z',
channel: 'stable' as const,
};
}
function resolved(overrides: Record<string, unknown> = {}) {
return {
resolveRequestId: REQUEST_ID,
resolveRequestDigest: DIGEST,
items: [{
pluginId: PLUGIN_ID,
action: 'keep' as const,
releaseId: RELEASE_ID,
version: '1.0.0',
sha256: DIGEST,
sizeBytes: 1,
releaseAdmissionId: 'admission-game-1',
expiresAt: '2100-01-01T00:00:00Z',
channel: 'stable' as const,
reason: null,
...overrides,
}],
catalogGeneration: 1,
etag: '"plugins-1-tp-free"',
stale: false,
};
}
function resolverFixture() {
const getInstalledRelease = vi.fn(async () => installed());
const resolve = vi.fn(async () => resolved());
const resolver = new MarketplaceHostedAdmissionResolver({
packageStore: { getInstalledRelease } as unknown as Pick<PluginPackageStore, 'getInstalledRelease'>,
marketplace: { resolve } as unknown as Pick<MarketplacePackageClientPort, 'resolve'>,
makeloreVersion: '2.0.0',
});
return { resolver, getInstalledRelease, resolve };
}
describe('MarketplaceHostedAdmissionResolver', () => {
it('resolves the frozen package release through the selected channel and current account admission', async () => {
const { resolver, getInstalledRelease, resolve } = resolverFixture();
await expect(resolver.resolve({
pluginId: PLUGIN_ID,
workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID },
})).resolves.toEqual({
releaseId: RELEASE_ID,
releaseAdmissionId: 'admission-game-1',
});
expect(getInstalledRelease).toHaveBeenCalledWith(PLUGIN_ID, RELEASE_ID);
expect(resolve).toHaveBeenCalledWith({
resolveRequestId: REQUEST_ID,
makeloreVersion: '2.0.0',
channel: 'stable',
installed: [{ pluginId: PLUGIN_ID, releaseId: RELEASE_ID, sha256: DIGEST }],
});
});
it('does not substitute the current package when the worker has no frozen release', async () => {
const { resolver, getInstalledRelease, resolve } = resolverFixture();
await expect(resolver.resolve({
pluginId: PLUGIN_ID,
workerSnapshot: { requestId: REQUEST_ID },
})).rejects.toMatchObject({
code: 'plugin_release_unavailable',
status: 409,
retryable: false,
});
expect(getInstalledRelease).not.toHaveBeenCalled();
expect(resolve).not.toHaveBeenCalled();
});
it.each([
['release mismatch', { releaseId: 'release-other' }],
['channel mismatch', { channel: 'beta' }],
['action mismatch', { action: 'update' }],
['digest mismatch', { sha256: 'b'.repeat(64) }],
])('returns one typed stale failure for %s', async (_label, item) => {
const marketplace = {
resolve: vi.fn(async () => resolved(item)),
} as unknown as Pick<MarketplacePackageClientPort, 'resolve'>;
const staleResolver = new MarketplaceHostedAdmissionResolver({
packageStore: { getInstalledRelease: vi.fn(async () => installed()) } as unknown as Pick<PluginPackageStore, 'getInstalledRelease'>,
marketplace,
makeloreVersion: '2.0.0',
});
const error = await staleResolver.resolve({
pluginId: PLUGIN_ID,
workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID },
}).catch((value: unknown) => value);
expect(error).toBeInstanceOf(MarketplaceHostedAdmissionError);
expect(error).toMatchObject({ code: 'plugin_runtime_stale', status: 409, retryable: false });
});
it('projects an account change as a typed non-retryable worker failure', async () => {
const { resolver, resolve } = resolverFixture();
resolve.mockRejectedValueOnce({ code: 'marketplace_account_changed' });
await expect(resolver.resolve({
pluginId: PLUGIN_ID,
workerSnapshot: { requestId: REQUEST_ID, pluginReleaseId: RELEASE_ID },
})).rejects.toMatchObject({
code: 'plugin_account_changed',
status: 409,
retryable: false,
});
});
});