2026-08-12 08:57:09 +00:00
|
|
|
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;
|
|
|
|
|
|
2026-08-13 16:08:28 +03:00
|
|
|
const claudePackage = '@openchamber/opencode-claude';
|
2026-08-12 08:57:09 +00:00
|
|
|
|
|
|
|
|
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);
|
2026-08-13 16:08:28 +03:00
|
|
|
expect(specMatchesPackage('@openchamber/opencode-claude-extra@0.6.0', claudePackage)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('points catalog plugins at the OpenChamber GitHub and npm packages', () => {
|
|
|
|
|
expect(thirdPartyCatalog.THIRD_PARTY_PLUGINS.map((plugin) => ({
|
|
|
|
|
id: plugin.id,
|
|
|
|
|
packageName: plugin.packageName,
|
|
|
|
|
homepage: plugin.homepage,
|
|
|
|
|
}))).toEqual([
|
|
|
|
|
{
|
|
|
|
|
id: 'opencode-claude',
|
|
|
|
|
packageName: '@openchamber/opencode-claude',
|
|
|
|
|
homepage: 'https://github.com/openchamber/opencode-claude',
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: 'opencode-cursor-oauth',
|
|
|
|
|
packageName: '@openchamber/opencode-cursor',
|
|
|
|
|
homepage: 'https://github.com/openchamber/opencode-cursor',
|
|
|
|
|
},
|
|
|
|
|
]);
|
2026-08-12 08:57:09 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
});
|