820 lines
26 KiB
TypeScript
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: [],
|
|
});
|
|
});
|
|
});
|