fix(marketplace): close release lifecycle gaps

This commit is contained in:
2026-08-30 09:19:39 +08:00
parent 8925f37b16
commit c56ddb9d3b
8 changed files with 382 additions and 22 deletions

View File

@@ -796,6 +796,78 @@ describe('PluginPackageStore', () => {
await expect(upgradedStore.readInstalledIndex()).resolves.toHaveLength(1);
});
it('persists explicit Stable and Beta intent when resolve keeps the same Release', async () => {
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
const archive = buildSkillOnlyArchive();
const { grant, publicKey } = signedGrant(archive);
let action: 'install' | 'keep' = 'install';
const issueDownload = vi.fn(async () => grant);
const marketplace: MarketplaceClient = {
resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
action, sha256: grant.sha256, sizeBytes: grant.sizeBytes,
})),
issueDownload,
downloadContent: async () => archive,
getCurrentAccountBinding: () => ACCOUNT_A,
} as MarketplaceClient;
const store = new PluginPackageStore({
rootDir: temporaryRoot,
marketplace,
getAccountBinding: () => ACCOUNT_A,
clientVersion: '1.0.0',
keyStore: new Map([['test-key', publicKey]]),
});
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'stable' });
action = 'keep';
await expect(store.resolveAndInstall({
pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'beta', explicitBeta: true,
})).resolves.toMatchObject({ status: 'kept', releaseId: RELEASE_ID, channel: 'beta' });
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID, channel: 'beta' });
await expect(store.resolveAndInstall({
pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'stable',
})).resolves.toMatchObject({ status: 'kept', releaseId: RELEASE_ID, channel: 'stable' });
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID, channel: 'stable' });
expect(issueDownload).toHaveBeenCalledOnce();
});
it('persists explicit channel intent when resolve reuses a cached immutable Release', async () => {
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
const archive = buildSkillOnlyArchive();
const { grant, publicKey } = signedGrant(archive);
let action: 'install' | 'update' = 'install';
const issueDownload = vi.fn(async () => grant);
const marketplace: MarketplaceClient = {
resolve: vi.fn(async (input: ResolveRequest) => makeResolveResult(input, {
action, sha256: grant.sha256, sizeBytes: grant.sizeBytes,
})),
issueDownload,
downloadContent: async () => archive,
getCurrentAccountBinding: () => ACCOUNT_A,
} as MarketplaceClient;
const store = new PluginPackageStore({
rootDir: temporaryRoot,
marketplace,
getAccountBinding: () => ACCOUNT_A,
clientVersion: '1.0.0',
keyStore: new Map([['test-key', publicKey]]),
});
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'stable' });
action = 'update';
await expect(store.resolveAndInstall({
pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'beta', explicitBeta: true,
})).resolves.toMatchObject({ status: 'kept', releaseId: RELEASE_ID, channel: 'beta' });
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID, channel: 'beta' });
await expect(store.resolveAndInstall({
pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'stable',
})).resolves.toMatchObject({ status: 'kept', releaseId: RELEASE_ID, channel: 'stable' });
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: RELEASE_ID, channel: 'stable' });
expect(issueDownload).toHaveBeenCalledOnce();
});
it('removes only old releases and never guesses a new current selection from installedAt', async () => {
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
const archive = buildSkillOnlyArchive();
@@ -907,7 +979,7 @@ describe('PluginPackageStore', () => {
await expect(store.readInstalledIndex()).resolves.toEqual([]);
});
it('makes an active current Release unavailable to new workers before deferred cleanup', async () => {
it('finishes an explicit uninstall when the last active worker releases its frozen Release', async () => {
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
const archive = buildSkillOnlyArchive();
const active = signedGrant(archive, { releaseId: 'release-active' });
@@ -937,11 +1009,83 @@ describe('PluginPackageStore', () => {
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 store.releaseActiveWorker('release-active');
await expect(store.readInstalledIndex()).resolves.toEqual([]);
await expect(stat(path.join(temporaryRoot, 'packages', PLUGIN_ID, 'release-active'))).rejects.toThrow();
});
it('keeps deferred explicit cleanup blocked by another local account snapshot', async () => {
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
const archive = buildSkillOnlyArchive();
const { grant, publicKey } = signedGrant(archive, { releaseId: 'release-active' });
const accountCache = new AccountPluginCache();
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 () => archive,
getCurrentAccountBinding: () => ACCOUNT_A,
} as MarketplaceClient;
const store = new PluginPackageStore({
rootDir: temporaryRoot,
marketplace,
accountCache,
getAccountBinding: () => ACCOUNT_A,
keyStore: new Map([['test-key', publicKey]]),
clientVersion: '1.0.0',
});
await store.resolveAndInstall({ pluginId: PLUGIN_ID, makeloreVersion: '1.0.0' });
accountCache.setResolve(ACCOUNT_B, 'account-b-reference', makeResolveResult({
makeloreVersion: '1.0.0', resolveRequestId: 'account-b-reference',
}, { releaseId: grant.releaseId, sha256: grant.sha256, sizeBytes: grant.sizeBytes }));
store.registerActiveWorker('release-active');
await expect(store.uninstall(PLUGIN_ID)).resolves.toEqual({
status: 'kept', pluginId: PLUGIN_ID, reason: 'active_worker_reference',
});
await store.releaseActiveWorker('release-active');
await expect(store.getInstalled(PLUGIN_ID)).resolves.toBeNull();
await expect(store.readInstalledIndex()).resolves.toEqual([
expect.objectContaining({ pluginId: PLUGIN_ID, releaseId: 'release-active' }),
]);
await expect(stat(path.join(temporaryRoot, 'packages', PLUGIN_ID, 'release-active')))
.resolves.toMatchObject({ isDirectory: expect.any(Function) });
});
it('cancels deferred cleanup when the user explicitly installs the same Release again', async () => {
temporaryRoot = await mkdtemp(path.join(process.cwd(), '.marketplace-test-'));
const archive = buildSkillOnlyArchive();
const { grant, publicKey } = signedGrant(archive, { releaseId: 'release-active' });
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 () => archive,
getCurrentAccountBinding: () => ACCOUNT_A,
} as MarketplaceClient;
const store = new PluginPackageStore({
rootDir: temporaryRoot,
marketplace,
getAccountBinding: () => ACCOUNT_A,
keyStore: new Map([['test-key', 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.toMatchObject({
status: 'kept', reason: 'active_worker_reference',
});
await expect(store.resolveAndInstall({
pluginId: PLUGIN_ID, makeloreVersion: '1.0.0', channel: 'stable',
})).resolves.toMatchObject({ status: 'kept', releaseId: 'release-active' });
await store.releaseActiveWorker('release-active');
await expect(store.getInstalled(PLUGIN_ID)).resolves.toMatchObject({ releaseId: 'release-active' });
await expect(store.readInstalledIndex()).resolves.toHaveLength(1);
});
it('preserves the old release across download, signature, and extraction failures', async () => {