195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|||
|
|
import type { PluginEntry, RegistryResult } from '@/stores/usePluginsStore';
|
||
|
|
import * as thirdPartyCatalog from './thirdPartyPlugins';
|
||
|
|
import {
|
||
|
|
getCatalogPluginState,
|
||
|
|
getCatalogPluginPrimaryAction,
|
||
|
|
getLatestNpmSpec,
|
||
|
|
specMatchesPackage,
|
||
|
|
} from './thirdPartyPlugins';
|
||
|
|
|
||
|
|
type CatalogPresentationStatus =
|
||
|
|
| 'not-installed'
|
||
|
|
| 'installed'
|
||
|
|
| 'installed-version'
|
||
|
|
| 'update-available'
|
||
|
|
| 'unpinned'
|
||
|
|
| 'ambiguous'
|
||
|
|
| 'restart-required'
|
||
|
|
| 'registry-unavailable'
|
||
|
|
| 'provider-unavailable';
|
||
|
|
|
||
|
|
type GetCatalogPluginPresentation = (
|
||
|
|
state: ReturnType<typeof getCatalogPluginState>,
|
||
|
|
options?: {
|
||
|
|
registryUnavailable?: boolean;
|
||
|
|
restartRequired?: boolean;
|
||
|
|
providerUnavailable?: boolean;
|
||
|
|
},
|
||
|
|
) => {
|
||
|
|
status: CatalogPresentationStatus;
|
||
|
|
latestVersion: string | null;
|
||
|
|
};
|
||
|
|
|
||
|
|
const getCatalogPluginPresentation = (
|
||
|
|
thirdPartyCatalog as unknown as {
|
||
|
|
getCatalogPluginPresentation?: GetCatalogPluginPresentation;
|
||
|
|
}
|
||
|
|
).getCatalogPluginPresentation;
|
||
|
|
|
||
|
|
const claudePackage = '@otto-assistant/opencode-claude';
|
||
|
|
|
||
|
|
const entry = (spec: string, scope: PluginEntry['scope'] = 'user'): PluginEntry => ({
|
||
|
|
id: `config:${scope}:${spec}`,
|
||
|
|
spec,
|
||
|
|
scope,
|
||
|
|
kind: 'config',
|
||
|
|
parsedKind: 'npm',
|
||
|
|
});
|
||
|
|
|
||
|
|
const registry = (spec: string, currentVersion: string | null, latestVersion = '0.7.0'): RegistryResult => ({
|
||
|
|
kind: 'npm-ok',
|
||
|
|
spec,
|
||
|
|
name: claudePackage,
|
||
|
|
currentVersion,
|
||
|
|
latestVersion,
|
||
|
|
versions: ['0.6.0', latestVersion],
|
||
|
|
hasUpdate: currentVersion !== null && currentVersion !== latestVersion,
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('third-party plugin catalog helpers', () => {
|
||
|
|
test('derives compact-card status with explicit transient-state priority', () => {
|
||
|
|
expect(typeof getCatalogPluginPresentation).toBe('function');
|
||
|
|
if (!getCatalogPluginPresentation) return;
|
||
|
|
|
||
|
|
const notInstalled = getCatalogPluginState([], claudePackage, {});
|
||
|
|
expect(getCatalogPluginPresentation(notInstalled)).toEqual({
|
||
|
|
status: 'not-installed',
|
||
|
|
latestVersion: null,
|
||
|
|
});
|
||
|
|
|
||
|
|
const current = getCatalogPluginState(
|
||
|
|
[entry(`${claudePackage}@0.7.0`)],
|
||
|
|
claudePackage,
|
||
|
|
{ [`${claudePackage}@0.7.0`]: registry(`${claudePackage}@0.7.0`, '0.7.0') },
|
||
|
|
);
|
||
|
|
expect(getCatalogPluginPresentation(current)).toEqual({
|
||
|
|
status: 'installed-version',
|
||
|
|
latestVersion: '0.7.0',
|
||
|
|
});
|
||
|
|
|
||
|
|
const outdated = getCatalogPluginState(
|
||
|
|
[entry(`${claudePackage}@0.6.0`)],
|
||
|
|
claudePackage,
|
||
|
|
{ [`${claudePackage}@0.6.0`]: registry(`${claudePackage}@0.6.0`, '0.6.0') },
|
||
|
|
);
|
||
|
|
expect(getCatalogPluginPresentation(outdated)).toEqual({
|
||
|
|
status: 'update-available',
|
||
|
|
latestVersion: '0.7.0',
|
||
|
|
});
|
||
|
|
expect(getCatalogPluginPresentation(outdated, { registryUnavailable: true })).toEqual({
|
||
|
|
status: 'registry-unavailable',
|
||
|
|
latestVersion: '0.7.0',
|
||
|
|
});
|
||
|
|
expect(getCatalogPluginPresentation(outdated, { providerUnavailable: true })).toEqual({
|
||
|
|
status: 'provider-unavailable',
|
||
|
|
latestVersion: '0.7.0',
|
||
|
|
});
|
||
|
|
expect(getCatalogPluginPresentation(outdated, {
|
||
|
|
providerUnavailable: true,
|
||
|
|
restartRequired: true,
|
||
|
|
})).toEqual({
|
||
|
|
status: 'restart-required',
|
||
|
|
latestVersion: '0.7.0',
|
||
|
|
});
|
||
|
|
|
||
|
|
const ambiguous = getCatalogPluginState(
|
||
|
|
[entry(claudePackage), entry(`${claudePackage}@0.6.0`)],
|
||
|
|
claudePackage,
|
||
|
|
{},
|
||
|
|
);
|
||
|
|
expect(getCatalogPluginPresentation(ambiguous)).toEqual({
|
||
|
|
status: 'ambiguous',
|
||
|
|
latestVersion: null,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
test('matches only a package or its versioned spec', () => {
|
||
|
|
expect(specMatchesPackage(claudePackage, claudePackage)).toBe(true);
|
||
|
|
expect(specMatchesPackage(`${claudePackage}@0.6.0`, claudePackage)).toBe(true);
|
||
|
|
expect(specMatchesPackage('@otto-assistant/opencode-claude-extra@0.6.0', claudePackage)).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('uses the configured user entry and its registry result', () => {
|
||
|
|
const installed = entry(`${claudePackage}@0.6.0`);
|
||
|
|
const state = getCatalogPluginState(
|
||
|
|
[installed],
|
||
|
|
claudePackage,
|
||
|
|
{ [installed.spec]: registry(installed.spec, '0.6.0') },
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(state.userEntry).toEqual(installed);
|
||
|
|
expect(state.userEntryIsAmbiguous).toBe(false);
|
||
|
|
expect(state.projectEntries).toEqual([]);
|
||
|
|
expect(state.registry).toEqual(registry(installed.spec, '0.6.0'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('does not choose an entry when multiple user specs would make a mutation ambiguous', () => {
|
||
|
|
const state = getCatalogPluginState(
|
||
|
|
[entry(claudePackage), entry(`${claudePackage}@0.6.0`), entry(claudePackage, 'project')],
|
||
|
|
claudePackage,
|
||
|
|
{},
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(state.userEntry).toBeNull();
|
||
|
|
expect(state.userEntryIsAmbiguous).toBe(true);
|
||
|
|
expect(state.projectEntries).toHaveLength(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('returns an exact latest spec only from a valid npm registry result', () => {
|
||
|
|
expect(getLatestNpmSpec(claudePackage, registry(claudePackage, null))).toBe(`${claudePackage}@0.7.0`);
|
||
|
|
expect(getLatestNpmSpec(claudePackage, {
|
||
|
|
kind: 'npm-network',
|
||
|
|
spec: claudePackage,
|
||
|
|
error: 'offline',
|
||
|
|
})).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('chooses an update for a bare or outdated user-wide entry', () => {
|
||
|
|
const bare = getCatalogPluginState(
|
||
|
|
[entry(claudePackage)],
|
||
|
|
claudePackage,
|
||
|
|
{ [claudePackage]: registry(claudePackage, null) },
|
||
|
|
);
|
||
|
|
const outdated = getCatalogPluginState(
|
||
|
|
[entry(`${claudePackage}@0.6.0`)],
|
||
|
|
claudePackage,
|
||
|
|
{ [`${claudePackage}@0.6.0`]: registry(`${claudePackage}@0.6.0`, '0.6.0') },
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(getCatalogPluginPrimaryAction(bare, claudePackage)).toBe('update');
|
||
|
|
expect(getCatalogPluginPrimaryAction(outdated, claudePackage)).toBe('update');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('keeps setup as the primary action once the exact latest spec is installed', () => {
|
||
|
|
const installed = entry(`${claudePackage}@0.7.0`);
|
||
|
|
const state = getCatalogPluginState(
|
||
|
|
[installed],
|
||
|
|
claudePackage,
|
||
|
|
{ [installed.spec]: registry(installed.spec, '0.7.0') },
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(getCatalogPluginPrimaryAction(state, claudePackage)).toBe('setup');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('sends ambiguous entries to manual plugin management', () => {
|
||
|
|
const state = getCatalogPluginState(
|
||
|
|
[entry(claudePackage), entry(`${claudePackage}@0.6.0`)],
|
||
|
|
claudePackage,
|
||
|
|
{},
|
||
|
|
);
|
||
|
|
|
||
|
|
expect(getCatalogPluginPrimaryAction(state, claudePackage)).toBe('manage');
|
||
|
|
});
|
||
|
|
});
|