fix(marketplace): close client contract gaps
This commit is contained in:
@@ -740,11 +740,11 @@ describe('PluginPackageStore', () => {
|
||||
expect(index.releases.map(({ release_id }) => release_id)).toEqual(['release-2', 'release-1']);
|
||||
|
||||
store.registerActiveWorker('release-1');
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toMatchObject({
|
||||
status: 'kept', pluginId: PLUGIN_ID, releaseId: 'release-1', version: '1.0.0',
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({
|
||||
status: 'kept', pluginId: PLUGIN_ID, reason: 'active_worker_reference',
|
||||
});
|
||||
await expect(store.readInstalledIndex()).resolves.toHaveLength(1);
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-1' });
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull();
|
||||
});
|
||||
|
||||
it('persists the installed channel and client range, and fails closed after a client upgrade', async () => {
|
||||
@@ -899,6 +899,49 @@ describe('PluginPackageStore', () => {
|
||||
expect.objectContaining({ releaseId: 'release-worker' }),
|
||||
]);
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull();
|
||||
|
||||
store.releaseActiveWorker('release-worker');
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({
|
||||
status: 'removed', pluginId: PLUGIN_ID, reason: 'none',
|
||||
});
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('makes an active current Release unavailable to new workers before deferred cleanup', async () => {
|
||||
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
|
||||
const archive = buildSkillOnlyArchive();
|
||||
const active = signedGrant(archive, { releaseId: 'release-active' });
|
||||
const marketplace: MarketplaceClient = {
|
||||
resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
|
||||
releaseId: active.grant.releaseId,
|
||||
sha256: active.grant.sha256,
|
||||
sizeBytes: active.grant.sizeBytes,
|
||||
})),
|
||||
issueDownload: vi.fn(async () => active.grant),
|
||||
downloadContent: async () => archive,
|
||||
getCurrentAccountBinding: () => ACCOUNT_A,
|
||||
} as MarketplaceClient;
|
||||
const store = new PluginPackageStore({
|
||||
rootDir: temporaryRoot,
|
||||
marketplace,
|
||||
getAccountBinding: () => ACCOUNT_A,
|
||||
keyStore: new Map([['test-key', active.publicKey]]),
|
||||
clientVersion: '1.0.0',
|
||||
});
|
||||
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
|
||||
store.registerActiveWorker('release-active');
|
||||
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({
|
||||
status: 'kept', pluginId: PLUGIN_ID, reason: 'active_worker_reference',
|
||||
});
|
||||
await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull();
|
||||
await expect(store.readInstalledIndex()).resolves.toHaveLength(1);
|
||||
|
||||
store.releaseActiveWorker('release-active');
|
||||
await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({
|
||||
status: 'removed', pluginId: PLUGIN_ID, reason: 'none',
|
||||
});
|
||||
await expect(store.readInstalledIndex()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('preserves the old release across download, signature, and extraction failures', async () => {
|
||||
|
||||
@@ -121,6 +121,16 @@ describe('Marketplace Release A package contract', () => {
|
||||
expect(Object.isFrozen(definition.tools[0]?.outputSchema)).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts only the canonical asynchronous job execution mode', () => {
|
||||
const job = structuredClone(HOSTED_CAPABILITY) as Record<string, unknown>;
|
||||
(job.tools as Array<Record<string, unknown>>)[0]!.executionMode = 'job';
|
||||
expect(parse(job).tools[0]?.executionMode).toBe('job');
|
||||
|
||||
const accepted = structuredClone(HOSTED_CAPABILITY) as Record<string, unknown>;
|
||||
(accepted.tools as Array<Record<string, unknown>>)[0]!.executionMode = 'accepted';
|
||||
expect(() => parse(accepted)).toThrow(CodingPluginManifestError);
|
||||
});
|
||||
|
||||
it('requires the exact capability manifest path even for direct parsing', () => {
|
||||
const root = structuredClone(ROOT) as Record<string, unknown>;
|
||||
const extensions = root.extensions as Record<string, unknown>;
|
||||
|
||||
@@ -130,6 +130,31 @@ describe('My Plugins', () => {
|
||||
expect(screen.getByText('当前频道:Beta')).toBeVisible();
|
||||
});
|
||||
|
||||
it('keeps a failed Beta update on the explicit Beta route', () => {
|
||||
const onUpdate = vi.fn();
|
||||
const onInstallBeta = vi.fn();
|
||||
render(<MemoryRouter><MyPluginsView
|
||||
library={{ ...library, items: [library.items[0]] }}
|
||||
installations={{
|
||||
'makelore.notes': {
|
||||
status: 'unavailable',
|
||||
pluginId: 'makelore.notes',
|
||||
channel: 'beta',
|
||||
version: '2.0.0',
|
||||
releaseId: 'beta-1',
|
||||
reason: 'plugin_release_unavailable',
|
||||
},
|
||||
}}
|
||||
state="ready" pending={{}} onRefresh={vi.fn()} onInstall={vi.fn()} onUpdate={onUpdate}
|
||||
onInstallBeta={onInstallBeta} onUninstall={vi.fn()} onRemove={vi.fn()} onReacquire={vi.fn()}
|
||||
/></MemoryRouter>);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '更新 Beta灵感笔记' }));
|
||||
expect(onInstallBeta).toHaveBeenCalledWith('makelore.notes');
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
expect(screen.getByText('当前频道:Beta')).toBeVisible();
|
||||
});
|
||||
|
||||
it('does not silently fall back to stable when the installed Beta channel has no release', () => {
|
||||
render(<MemoryRouter><MyPluginsView
|
||||
library={{ ...library, items: [{ ...library.items[0], betaVersion: null }] }}
|
||||
|
||||
@@ -199,4 +199,32 @@ describe('plugin Marketplace store', () => {
|
||||
reason: 'plugin_signature_invalid: Release signature is invalid',
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves the installed Beta channel when a device update fails', async () => {
|
||||
const error = Object.assign(new Error('Release is temporarily unavailable'), {
|
||||
code: 'plugin_release_unavailable',
|
||||
});
|
||||
const store = createPluginMarketplaceStore({
|
||||
installBeta: vi.fn().mockResolvedValue({
|
||||
status: 'installed',
|
||||
pluginId: 'makelore.notes',
|
||||
releaseId: 'release-beta-1',
|
||||
version: '2.0.0-beta.1',
|
||||
channel: 'beta',
|
||||
}),
|
||||
update: vi.fn().mockRejectedValue(error),
|
||||
});
|
||||
store.getState().activateAccount('account-a');
|
||||
await store.getState().installBeta('makelore.notes');
|
||||
|
||||
await expect(store.getState().update('makelore.notes')).rejects.toBe(error);
|
||||
expect(store.getState().installations['makelore.notes']).toMatchObject({
|
||||
status: 'unavailable',
|
||||
pluginId: 'makelore.notes',
|
||||
releaseId: 'release-beta-1',
|
||||
version: '2.0.0-beta.1',
|
||||
channel: 'beta',
|
||||
reason: 'plugin_release_unavailable: Release is temporarily unavailable',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user