fix(plugins): close R2 workspace gaps

This commit is contained in:
2026-09-03 15:07:29 +08:00
parent c5020ae22c
commit 0bfabc0df2
10 changed files with 405 additions and 15 deletions

View File

@@ -76,6 +76,7 @@ const officialItem: PluginWorkspaceItem = {
version: '1.0.0',
delivery: 'system_included',
projectState: 'enabled',
projectPolicyStatus: 'current',
localEnabled: null,
assignedAgentIds: ['agent-a'],
assignedAgentNames: ['小明'],
@@ -138,6 +139,7 @@ const localItem: PluginWorkspaceItem = {
version: '1.2.3',
delivery: 'local_installed',
projectState: 'not_applicable',
projectPolicyStatus: null,
localEnabled: true,
assignedAgentIds: [],
assignedAgentNames: [],
@@ -289,6 +291,63 @@ describe('PluginsView', () => {
expect(screen.getByText('当前没有打开的项目,已显示全部插件。')).toBeVisible();
});
it('keeps old device and project rows visible and labels each affected snapshot cached after refresh errors', async () => {
const activeProject = {
id: 'project-a',
name: 'Project A',
createdAt: '2026-09-03T00:00:00Z',
updatedAt: '2026-09-03T00:00:00Z',
lastOpenedAt: '2026-09-03T00:00:00Z',
};
useAuthStore.setState({ user: null });
codingWorkspaceStore.setState({
activeProjectId: activeProject.id,
activeProject,
config: null,
loadState: 'ready',
});
codingPluginsStore.setState({
projectId: activeProject.id,
projection: {
schemaVersion: 1,
project: { localProjectId: activeProject.id, durableProjectId: 'durable-a' },
policyStatus: 'current',
unknownPluginIds: ['makelore.retained'],
items: [projectPlugin],
},
loadState: 'error',
error: 'project refresh failed',
});
devicePackageStore.setState({
index: { schemaVersion: 1, generation: 8, packages: [localItem.local!] },
state: 'error',
error: 'device refresh failed',
});
pluginMarketplaceStore.setState({
accountKey: null,
catalog: null,
catalogState: 'ready',
catalogError: null,
library: null,
libraryState: 'idle',
libraryError: null,
installations: {},
details: {},
detailState: {},
});
vi.spyOn(pluginMarketplaceStore.getState(), 'loadCatalog').mockResolvedValue(undefined);
vi.spyOn(devicePackageStore.getState(), 'load').mockResolvedValue(undefined);
vi.spyOn(codingPluginsStore.getState(), 'load').mockResolvedValue(undefined);
render(<MemoryRouter initialEntries={['/plugins?scope=project&source=all&state=all']}><Plugins /></MemoryRouter>);
for (const name of ['开发数据服务', 'Pi Web Search', 'makelore.retained']) {
const heading = await screen.findByRole('heading', { name });
expect(heading.closest('article')).toHaveTextContent('缓存');
}
expect(screen.getAllByRole('alert')).toHaveLength(2);
});
it('renders the unified workspace and exposes URL-backed filter changes', () => {
const view = props();
view.sourceErrors = [
@@ -434,6 +493,24 @@ describe('PluginsView', () => {
expect(dialog).toHaveTextContent('价格版本:pricing-7');
});
it.each([
['stale', '当前项目策略使用缓存,所示能力与价格可能不是最新状态。'],
['unavailable', '当前项目策略不可用,无法确认最新能力与价格。'],
] as const)('shows %s project policy status beside policy-owned capability and pricing', (projectPolicyStatus, copy) => {
const policyItem: PluginWorkspaceItem = { ...officialItem, projectPolicyStatus };
render(<MemoryRouter><PluginsView {...props(policyItem)} /></MemoryRouter>);
expect(screen.getByRole('dialog', { name: '开发数据服务' })).toHaveTextContent(copy);
});
it('does not mark current project policy as stale or unavailable', () => {
render(<MemoryRouter><PluginsView {...props(officialItem)} /></MemoryRouter>);
const dialog = screen.getByRole('dialog', { name: '开发数据服务' });
expect(dialog).not.toHaveTextContent('当前项目策略使用缓存');
expect(dialog).not.toHaveTextContent('当前项目策略不可用');
});
it('deduplicates operation identity while preferring the current project policy projection', () => {
const projectPolicy = {
...projectPlugin,