import { describe, expect, it } from 'vitest'; import type { CodingPluginProject } from '@/lib/coding-plugins'; import type { MarketplaceCatalogPage, MarketplaceLibrarySnapshot, } from '@/lib/plugin-marketplace'; import type { DevicePackageIndexV1, DevicePackageRecordV1 } from '../../shared/device-packages'; import { buildPluginWorkspaceProjection, type PluginWorkspaceInputs, } from '@/pages/Plugins/plugin-workspace-model'; const catalog: MarketplaceCatalogPage = { items: [{ pluginId: 'makelore.notes', title: 'Notes', summary: 'Write project notes', category: 'productivity', tags: ['notes'], providerDisplayName: 'MakeLore', runtimeKind: 'skill_only', runtimeStatus: 'enabled', acquisition: 'free', usageBilling: 'token_point', includedOperationCount: 0, meteredOperationCount: 1, stableVersion: '2.0.0', betaVersion: '3.0.0-beta.1', }], nextCursor: null, total: 1, catalogGeneration: 4, etag: 'catalog-4', pricingVersionId: 'pricing-1', stale: false, fetchedAt: 4, }; const library: MarketplaceLibrarySnapshot = { items: [{ pluginId: 'makelore.notes', title: 'Notes', summary: 'Write project notes', category: 'productivity', acquisition: 'free', acquisitionMode: 'user_acquired', catalogStatus: 'active', runtimeStatus: 'enabled', acquiredAt: '2026-09-03T00:00:00Z', removedAt: null, stableVersion: '2.0.0', betaVersion: '3.0.0-beta.1', }], total: 1, stale: false, fetchedAt: 4, }; const project: CodingPluginProject = { schemaVersion: 1, project: { localProjectId: 'project-a', durableProjectId: 'durable-a' }, policyStatus: 'current', unknownPluginIds: [], items: [{ id: 'makelore.notes', version: '1.0.0', contractVersion: 1, requiresBackend: false, displayName: 'Notes', description: 'Write project notes', enabled: true, state: 'ready', backend: { status: 'not_required' }, skills: [{ id: 'notes', assignedAgentIds: ['agent-a'] }], capabilities: [{ id: 'notes', operations: [{ id: 'write', billing: { mode: 'platform_metered', availability: 'available', notice: 'Metered', unitName: 'request', }, tool: null, }], }], settingsSurface: null, }], }; function input(overrides: Partial = {}): PluginWorkspaceInputs { return { catalog, details: {}, library, marketplaceInstallations: { 'makelore.notes': { status: 'installed', pluginId: 'makelore.notes', releaseId: 'release-1', version: '1.0.0', channel: 'stable', }, }, devicePackages: { schemaVersion: 1, generation: 1, packages: [], } satisfies DevicePackageIndexV1, project, activeProject: { id: 'project-a', name: 'Project A' }, agentNames: { 'agent-a': 'Alice' }, authenticated: true, filters: { scope: 'all', source: 'all', state: 'all', search: '' }, ...overrides, }; } function commandKinds(overrides: Partial): string[] { return buildPluginWorkspaceProjection(input(overrides)).items[0]?.commands.map(({ kind }) => kind) ?? []; } function devicePackage(packageId: string, displayName: string, enabled: boolean): DevicePackageRecordV1 { return { schemaVersion: 1, packageId, displayName, resolvedVersion: '1.0.0', source: { kind: 'npm', requested: packageId, resolved: `${packageId}@1.0.0` }, kind: 'skill-only', skillEntries: [], extensionEntries: [], enabled, confirmedExecutableCode: false, installedAt: '2026-09-03T00:00:00Z', }; } describe('buildPluginWorkspaceProjection', () => { it('merges every official source into one item with only its legal commands', () => { const result = buildPluginWorkspaceProjection(input()); expect(result.items).toHaveLength(1); expect(result.items[0]).toMatchObject({ key: 'official:makelore.notes', source: 'official', pluginId: 'makelore.notes', delivery: 'account_acquired', projectState: 'enabled', assignedAgentNames: ['Alice'], updateAvailable: true, }); expect(result.items[0]?.commands.map(({ kind }) => kind)).toEqual([ 'remove_from_library', 'install_beta', 'update_official', 'remove_official_device_package', 'disable_project', 'open_agent_assignment', ]); }); it('keeps official and local identities separate while excluding only the native web-search tool', () => { const nativeCatalogItem = { ...catalog.items[0], pluginId: 'makelore_web_search', title: 'Native Web Search', }; const result = buildPluginWorkspaceProjection(input({ catalog: { ...catalog, items: [...catalog.items, nativeCatalogItem], total: 2 }, devicePackages: { schemaVersion: 1, generation: 2, packages: [ { schemaVersion: 1, packageId: 'makelore.notes', displayName: 'Local Notes', resolvedVersion: '4.0.0', source: { kind: 'npm', requested: 'makelore.notes', resolved: 'makelore.notes@4.0.0' }, kind: 'skill-only', skillEntries: [{ id: 'notes', entryPath: 'skills/notes/SKILL.md' }], extensionEntries: [], enabled: true, confirmedExecutableCode: false, installedAt: '2026-09-03T00:00:00Z', }, { schemaVersion: 1, packageId: 'makelore_web_search', displayName: 'Third-party Web Search', resolvedVersion: '1.0.0', source: { kind: 'git', requested: 'example/web-search', resolved: 'https://example.test/web-search.git' }, kind: 'pi-extension', skillEntries: [], extensionEntries: ['index.js'], enabled: false, confirmedExecutableCode: true, installedAt: '2026-09-03T00:00:00Z', }, ], }, })); expect(result.items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'local:makelore.notes', 'local:makelore_web_search', ]); expect(result.items[1]).toMatchObject({ source: 'local', delivery: 'local_installed', projectState: 'not_applicable', localEnabled: true, billing: 'none', }); expect(result.items[1]?.commands.map(({ kind }) => kind)).toEqual(['disable_local', 'remove_local']); expect(result.items[2]?.commands.map(({ kind }) => kind)).toEqual(['enable_local', 'remove_local']); }); it('never offers account or device commands for a system-included plugin', () => { const systemCatalog = { ...catalog.items[0]!, pluginId: 'makelore.data-service', title: 'Data Service', acquisition: 'system_included' as const, runtimeKind: 'bundled_typed' as const, }; const systemLibrary = { ...library.items[0]!, pluginId: systemCatalog.pluginId, title: systemCatalog.title, acquisition: 'system_included' as const, acquisitionMode: 'system_included' as const, }; const systemProject = { ...project, items: [{ ...project.items[0]!, id: systemCatalog.pluginId, displayName: systemCatalog.title, settingsSurface: 'data-service', }], }; expect(commandKinds({ catalog: { ...catalog, items: [systemCatalog] }, library: { ...library, items: [systemLibrary] }, project: systemProject, marketplaceInstallations: { [systemCatalog.pluginId]: { status: 'installed', pluginId: systemCatalog.pluginId, releaseId: 'should-be-ignored', version: '0.1.0', channel: 'stable', }, }, })).toEqual(['disable_project', 'open_agent_assignment', 'open_settings']); }); it('keeps bundled and downloadable official command sets inside their delivery boundaries', () => { for (const [pluginId, title, runtimeKind] of [ ['makelore.game-resource', 'Game Resource', 'platform_hosted'], ['makelore.project-scaffold', 'Project Scaffold', 'skill_only'], ] as const) { const bundledCatalog = { ...catalog.items[0]!, pluginId, title, runtimeKind, }; const bundledLibrary = { ...library.items[0]!, pluginId: bundledCatalog.pluginId, title: bundledCatalog.title, }; const bundledProject = { ...project, items: [{ ...project.items[0]!, id: bundledCatalog.pluginId, displayName: bundledCatalog.title, enabled: false, state: 'disabled' as const, }], }; expect(commandKinds({ catalog: { ...catalog, items: [bundledCatalog] }, library: { ...library, items: [] }, marketplaceInstallations: {}, project: null, })).toEqual(['acquire']); expect(commandKinds({ catalog: { ...catalog, items: [bundledCatalog] }, library: { ...library, items: [bundledLibrary] }, marketplaceInstallations: {}, project: bundledProject, })).toEqual(['remove_from_library', 'enable_project', 'open_agent_assignment']); } expect(commandKinds({ marketplaceInstallations: {} })).toEqual([ 'remove_from_library', 'install_stable', 'install_beta', 'disable_project', ]); }); it('allows recovery and cleanup, but no new delivery, for removed or unavailable official items', () => { const removed = { ...library.items[0]!, removedAt: '2026-09-03T01:00:00Z' }; expect(commandKinds({ library: { ...library, items: [removed] } })).toEqual([ 'reacquire', 'remove_official_device_package', 'disable_project', ]); expect(commandKinds({ library: { ...library, items: [{ ...removed, catalogStatus: 'retired' }] }, })).toEqual(['remove_official_device_package', 'disable_project']); expect(commandKinds({ catalog: { ...catalog, items: [{ ...catalog.items[0]!, runtimeStatus: 'suspended' }], }, library: { ...library, items: [{ ...library.items[0]!, runtimeStatus: 'suspended' }], }, })).toEqual([ 'remove_from_library', 'remove_official_device_package', 'disable_project', ]); }); 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: { 'makelore.notes': { status: 'installed', pluginId: 'makelore.notes', releaseId: 'beta-old', version: '2.0.0-beta.1', channel: 'beta', }, }, })).toEqual([ 'remove_from_library', 'install_beta', 'remove_official_device_package', 'disable_project', 'open_agent_assignment', ]); }); it('orders source groups deterministically and creates retained items only for unexplained project ids', () => { const libraryOnly = { ...library.items[0]!, pluginId: 'makelore.library-only', title: 'Library Only', }; const projectOnly = { ...project.items[0]!, id: 'makelore.project-only', displayName: 'Project Only', enabled: false, state: 'disabled' as const, }; const localPackage = (packageId: string, displayName: string) => ({ schemaVersion: 1 as const, packageId, displayName, resolvedVersion: '1.0.0', source: { kind: 'npm' as const, requested: packageId, resolved: `${packageId}@1.0.0` }, kind: 'skill-only' as const, skillEntries: [], extensionEntries: [], enabled: true, confirmedExecutableCode: false, installedAt: '2026-09-03T00:00:00Z', }); const overrides: Partial = { library: { ...library, items: [library.items[0]!, libraryOnly], total: 2 }, marketplaceInstallations: {}, project: { ...project, items: [project.items[0]!, projectOnly], unknownPluginIds: ['makelore.notes', 'makelore.ghost', 'makelore_web_search'], }, devicePackages: { schemaVersion: 1, generation: 3, packages: [localPackage('zeta.package', 'Zeta'), localPackage('alpha.package', 'Alpha')], }, }; expect(buildPluginWorkspaceProjection(input(overrides)).items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'official:makelore.library-only', 'official:makelore.project-only', 'local:alpha.package', 'local:zeta.package', 'retained:makelore.ghost', ]); const projectScope = buildPluginWorkspaceProjection(input({ ...overrides, filters: { scope: 'project', source: 'all', state: 'all', search: '' }, })); expect(projectScope.items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'official:makelore.project-only', 'local:alpha.package', 'local:zeta.package', 'retained:makelore.ghost', ]); expect(projectScope.items.at(-1)?.commands).toEqual([{ kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.ghost', }]); }); 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]!, pluginId: 'makelore.available', title: 'Available Writer', summary: 'A fresh plugin', betaVersion: null, }; const suspended = { ...catalog.items[0]!, pluginId: 'makelore.suspended', title: 'Suspended Tool', runtimeStatus: 'suspended' as const, betaVersion: null, }; const suspendedProject = { ...project.items[0]!, id: suspended.pluginId, displayName: suspended.title, enabled: false, state: 'unavailable' as const, }; const sources: Partial = { catalog: { ...catalog, items: [catalog.items[0]!, available, suspended], total: 3 }, project: { ...project, items: [project.items[0]!, suspendedProject], unknownPluginIds: ['makelore.ghost'], }, devicePackages: { schemaVersion: 1, generation: 4, packages: [ devicePackage('alpha.package', 'Alpha Local', true), devicePackage('zeta.package', 'Zeta Local', false), ], }, }; const projectAll = (filters: PluginWorkspaceInputs['filters']) => buildPluginWorkspaceProjection(input({ ...sources, filters, })); const all = projectAll({ scope: 'all', source: 'all', state: 'all', search: '' }); expect(all.counts).toEqual({ all: 6, available: 1, mine: 3, enabled: 2, update: 1, unavailable: 2 }); const expectedByState = { available: ['official:makelore.available'], mine: ['official:makelore.notes', 'local:alpha.package', 'local:zeta.package'], enabled: ['official:makelore.notes', 'local:alpha.package'], update: ['official:makelore.notes'], unavailable: ['official:makelore.suspended', 'retained:makelore.ghost'], } as const; for (const [state, expected] of Object.entries(expectedByState)) { const result = projectAll({ scope: 'all', source: 'all', state: state as keyof typeof expectedByState, search: '', }); expect(result.items.map(({ key }) => key), state).toEqual(expected); expect(result.counts, state).toEqual(all.counts); } expect(projectAll({ scope: 'all', source: 'official', state: 'all', search: 'fresh', }).items.map(({ key }) => key)).toEqual(['official:makelore.available']); expect(projectAll({ scope: 'all', source: 'local', state: 'all', search: 'alpha.package', }).items.map(({ key }) => key)).toEqual(['local:alpha.package']); expect(projectAll({ scope: 'project', source: 'all', state: 'all', search: '', }).items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'official:makelore.suspended', 'local:alpha.package', 'local:zeta.package', 'retained:makelore.ghost', ]); }); it('falls back from project scope without an active project and closes invalid selections', () => { const fallback = buildPluginWorkspaceProjection(input({ activeProject: null, project: null, filters: { scope: 'project', source: 'all', state: 'all', search: '', selectedKey: 'official:makelore.notes', }, })); expect(fallback.items.map(({ key }) => key)).toEqual(['official:makelore.notes']); expect(fallback.selected?.key).toBe('official:makelore.notes'); expect(fallback.notices).toContainEqual(expect.objectContaining({ kind: 'scope_fallback' })); const hiddenSelection = buildPluginWorkspaceProjection(input({ filters: { scope: 'all', source: 'local', state: 'all', search: '', selectedKey: 'official:makelore.notes', }, })); expect(hiddenSelection.selected).toBeNull(); }); it('never lets a late detail for A replace the selected B item', () => { const lateNotesDetail = { ...catalog.items[0], descriptionMarkdown: 'Late detail for Notes', permissions: [], operations: [], stableRelease: { releaseId: 'notes-2', version: '2.0.0' }, betaRelease: null, etag: 'notes-detail', pricingVersionId: null, stale: false, fetchedAt: 5, }; const tasks = { ...catalog.items[0], pluginId: 'makelore.tasks', title: 'Tasks', summary: 'Track tasks', }; const result = buildPluginWorkspaceProjection(input({ catalog: { ...catalog, items: [catalog.items[0], tasks], total: 2 }, details: { 'makelore.notes': lateNotesDetail }, library: null, marketplaceInstallations: {}, project: null, activeProject: null, filters: { scope: 'all', source: 'all', state: 'all', search: '', selectedKey: 'official:makelore.tasks', }, })); expect(result.selected).toMatchObject({ key: 'official:makelore.tasks', title: 'Tasks' }); expect(result.selected?.official?.detail).toBeNull(); }); it('keeps independent sources visible and does not infer acquisition when Library is unavailable', () => { const local = devicePackage('local.notes', 'Local Notes', true); const unknownLibrary = buildPluginWorkspaceProjection(input({ library: null, marketplaceInstallations: {}, project: null, devicePackages: { schemaVersion: 1, generation: 5, packages: [local] }, })); expect(unknownLibrary.items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'local:local.notes', ]); expect(unknownLibrary.items[0]).toMatchObject({ delivery: 'account_unknown', commands: [], }); const unavailableCatalog = buildPluginWorkspaceProjection(input({ catalog: null, project: null, devicePackages: { schemaVersion: 1, generation: 5, packages: [local] }, })); expect(unavailableCatalog.items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'local:local.notes', ]); const unavailableDevice = buildPluginWorkspaceProjection(input({ devicePackages: null })); expect(unavailableDevice.items.map(({ key }) => key)).toEqual(['official:makelore.notes']); const unavailableProject = buildPluginWorkspaceProjection(input({ project: null })); expect(unavailableProject.items[0]).toMatchObject({ key: 'official:makelore.notes', projectState: 'unknown', }); }); it('keeps device and project snapshots visible and marks only their retained rows stale after refresh errors', () => { const catalogOnly = { ...catalog.items[0]!, pluginId: 'makelore.catalog-only', title: 'Catalog Only', }; const result = buildPluginWorkspaceProjection(input({ catalog: { ...catalog, items: [...catalog.items, catalogOnly], total: 2 }, devicePackages: { schemaVersion: 1, generation: 8, packages: [devicePackage('local.cached', 'Cached Local', true)], }, project: { ...project, unknownPluginIds: ['makelore.retained'], }, sourceFailures: { device: true, project: true }, })); expect(result.items.map(({ key }) => key)).toEqual([ 'official:makelore.notes', 'official:makelore.catalog-only', 'local:local.cached', 'retained:makelore.retained', ]); expect(result.items.find(({ key }) => key === 'official:makelore.notes')?.stale).toBe(true); expect(result.items.find(({ key }) => key === 'official:makelore.catalog-only')?.stale).toBe(false); expect(result.items.find(({ key }) => key === 'local:local.cached')?.stale).toBe(true); expect(result.items.find(({ key }) => key === 'retained:makelore.retained')?.stale).toBe(true); }); it.each(['current', 'stale', 'unavailable'] as const)( 'projects %s policy status onto project-backed and retained rows without changing actions', (policyStatus) => { const result = buildPluginWorkspaceProjection(input({ project: { ...project, policyStatus, unknownPluginIds: ['makelore.retained'], }, })); expect(result.items.find(({ key }) => key === 'official:makelore.notes')).toMatchObject({ projectPolicyStatus: policyStatus, }); expect(result.items.find(({ key }) => key === 'retained:makelore.retained')).toMatchObject({ projectPolicyStatus: policyStatus, commands: [{ kind: 'disable_project', projectId: 'project-a', pluginId: 'makelore.retained' }], }); }, ); 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: [], }); }); });