fix: close plugin workspace acceptance gaps

This commit is contained in:
2026-09-03 14:29:21 +08:00
parent 1b1f206dd8
commit c5020ae22c
10 changed files with 885 additions and 53 deletions

View File

@@ -111,6 +111,7 @@ function input(overrides: Partial<PluginWorkspaceInputs> = {}): PluginWorkspaceI
project,
activeProject: { id: 'project-a', name: 'Project A' },
agentNames: { 'agent-a': 'Alice' },
authenticated: true,
filters: { scope: 'all', source: 'all', state: 'all', search: '' },
...overrides,
};
@@ -331,6 +332,62 @@ describe('buildPluginWorkspaceProjection', () => {
]);
});
it('keeps an unavailable installation as a retained device version with same-channel update and cleanup', () => {
const item = buildPluginWorkspaceProjection(input({
marketplaceInstallations: {
'makelore.notes': {
status: 'unavailable',
pluginId: 'makelore.notes',
releaseId: 'release-1',
version: '1.0.0',
channel: 'stable',
reason: '当前客户端与新版本不兼容',
},
},
})).items[0]!;
expect(item).toMatchObject({
version: '1.0.0',
updateAvailable: true,
unavailable: true,
deviceReason: '当前客户端与新版本不兼容',
});
expect(item.commands.map(({ kind }) => kind)).toEqual([
'remove_from_library',
'install_beta',
'update_official',
'remove_official_device_package',
'disable_project',
]);
expect(item.commands.some(({ kind }) => kind === 'install_stable')).toBe(false);
expect(item.commands.some(({ kind }) => kind === 'open_agent_assignment')).toBe(false);
});
it('marks retained snapshots stale after refresh failure and distinguishes suspended from retired', () => {
const item = buildPluginWorkspaceProjection(input({
catalog: {
...catalog,
items: [{ ...catalog.items[0]!, runtimeStatus: 'suspended' }],
},
library: {
...library,
items: [{
...library.items[0]!,
runtimeStatus: 'suspended',
catalogStatus: 'retired',
}],
},
sourceFailures: { catalog: true, library: true },
})).items[0]!;
expect(item).toMatchObject({
stale: true,
suspended: true,
retired: true,
unavailable: true,
});
});
it('keeps an installed Beta update on the explicit Beta command', () => {
expect(commandKinds({
marketplaceInstallations: {
@@ -419,6 +476,43 @@ describe('buildPluginWorkspaceProjection', () => {
}]);
});
it('keeps a catalog-known unknown project selection as a metadata-rich disable-only retained item', () => {
const projectWithRecognizedUnknown = {
...project,
items: [],
unknownPluginIds: ['makelore.notes'],
};
const projectScope = buildPluginWorkspaceProjection(input({
project: projectWithRecognizedUnknown,
filters: { scope: 'project', source: 'all', state: 'all', search: '' },
}));
expect(projectScope.items).toHaveLength(1);
expect(projectScope.items[0]).toMatchObject({
key: 'retained:makelore.notes',
source: 'retained',
title: 'Notes',
summary: 'Write project notes',
category: 'productivity',
publisher: 'MakeLore',
commands: [{
kind: 'disable_project',
projectId: 'project-a',
pluginId: 'makelore.notes',
}],
});
const allScope = buildPluginWorkspaceProjection(input({
project: projectWithRecognizedUnknown,
filters: { scope: 'all', source: 'all', state: 'all', search: '' },
}));
expect(allScope.items.map(({ key }) => key)).toEqual([
'official:makelore.notes',
'retained:makelore.notes',
]);
expect(allScope.items[1]?.commands.map(({ kind }) => kind)).toEqual(['disable_project']);
});
it('applies scope, source, state, and search filters while keeping state counts stable', () => {
const available = {
...catalog.items[0]!,
@@ -599,4 +693,30 @@ describe('buildPluginWorkspaceProjection', () => {
projectState: 'unknown',
});
});
it('offers sign-in for a signed-out free catalog item but fails closed for an authenticated Library failure', () => {
const signedOut = buildPluginWorkspaceProjection(input({
authenticated: false,
library: null,
marketplaceInstallations: {},
project: null,
activeProject: null,
})).items[0]!;
expect(signedOut).toMatchObject({
delivery: 'account_unknown',
commands: [{ kind: 'sign_in' }],
});
const authenticatedLibraryFailure = buildPluginWorkspaceProjection(input({
authenticated: true,
library: null,
marketplaceInstallations: {},
project: null,
activeProject: null,
})).items[0]!;
expect(authenticatedLibraryFailure).toMatchObject({
delivery: 'account_unknown',
commands: [],
});
});
});