Files
makelore/tests/unit/plugin-workspace-model.test.ts

820 lines
26 KiB
TypeScript

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> = {}): 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<PluginWorkspaceInputs>): 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: [{ id: `${packageId}.skill`, entryPath: 'SKILL.md' }],
extensionEntries: [],
enabled,
confirmedExecutableCode: false,
installedAt: '2026-09-03T00:00:00Z',
};
}
describe('buildPluginWorkspaceProjection', () => {
it('places a user-installed loose Skill only in the installed tab', () => {
const looseSkill = {
...devicePackage('local-writing', 'Local Writing', true),
source: {
kind: 'file' as const,
requested: '/Users/example/.codex/skills/local-writing/SKILL.md',
resolved: '/Users/example/.codex/skills/local-writing/SKILL.md',
},
};
const base = {
catalog: null,
library: null,
marketplaceInstallations: {},
project: null,
activeProject: null,
devicePackages: { schemaVersion: 1 as const, generation: 2, packages: [looseSkill] },
};
const installed = buildPluginWorkspaceProjection(input({
...base,
filters: { scope: 'all', source: 'all', state: 'mine', search: '' },
}));
expect(installed.items).toEqual([
expect.objectContaining({
key: 'local:local-writing',
source: 'local',
delivery: 'local_installed',
local: expect.objectContaining({ kind: 'skill-only' }),
}),
]);
expect(installed.counts).toMatchObject({ available: 0, mine: 1 });
const available = buildPluginWorkspaceProjection(input({
...base,
filters: { scope: 'all', source: 'all', state: 'available', search: '' },
}));
expect(available.items).toEqual([]);
});
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_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']);
}
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<PluginWorkspaceInputs> = {
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<PluginWorkspaceInputs> = {
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: 2, mine: 4, enabled: 2, update: 1, unavailable: 2 });
const expectedByState = {
available: ['official:makelore.available', 'official:makelore.suspended'],
mine: ['official:makelore.notes', 'local:alpha.package', 'local:zeta.package', 'retained:makelore.ghost'],
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: [],
});
});
});