open-design/apps/daemon/tests/plugins-stats.test.ts
Cursor Agent e79095a603
feat(plugins): od plugin manifest <id> + od plugin sources (Phase 4)
Plan MM1 + MM2.

MM1: `od plugin manifest <id>`. Prints just the parsed manifest
JSON (record.manifest) without the wrapper fields the existing
`od plugin info` returns. Useful for plugin authors who want
to compare the daemon's view to their on-disk open-design.json
without scrolling past sourceKind / fsPath / installedAt etc.
Returns 404 \u2192 exit 65, missing-manifest \u2192 exit 1.

MM2: `od plugin sources`. Lists every distinct (sourceKind,
source) tuple + count of plugins installed from each. Sorted by
descending count, then ascending sourceKind, then ascending
source. Useful for ops audits ('which github repos do my plugins
come from') + for plugin authors comparing forks to upstream.

Pure helper backing the CLI:

  pluginSourceBuckets(plugins) \u2192 PluginSourceBucketsResult
    {
      total: <count>,
      buckets: [
        { sourceKind, source, count, plugins: [{ id, version }, ...] },
        ...
      ],
    }

Plugins within a bucket sort by id for byte-deterministic output.

CLI summary section:

  od plugin manifest my-plugin
  od plugin sources
  od plugin sources --json | jq

Daemon tests: 1813 \u2192 1818 (+5 cases on plugins-stats covering
pluginSourceBuckets: empty list \u2192 zero shape, (sourceKind,
source) aggregation, descending-count then alphabetic sort,
count-tie alphabetic break, plugins inside a bucket sorted
by id alphabetically).

Co-authored-by: Tom Huang <1043269994@qq.com>
2026-05-09 18:25:15 +00:00

209 lines
7.3 KiB
TypeScript

// Plan §3.DD1 — pluginInventoryStats + snapshotInventoryStats.
import { describe, expect, it } from 'vitest';
import type { InstalledPluginRecord, PluginManifest } from '@open-design/contracts';
import {
pluginInventoryStats,
pluginSourceBuckets,
snapshotInventoryStats,
type SnapshotStatsRow,
} from '../src/plugins/stats.js';
const make = (
id: string,
over: Partial<{
sourceKind: 'bundled' | 'local' | 'github' | 'url' | 'marketplace';
trust: 'trusted' | 'restricted' | 'bundled';
taskKind: string;
capabilities: string[];
installedAt: number;
updatedAt: number;
}> = {},
): InstalledPluginRecord => ({
id,
title: `Title ${id}`,
version: '0.1.0',
sourceKind: over.sourceKind ?? 'local',
source: '/tmp/' + id,
fsPath: '/tmp/' + id,
trust: over.trust ?? 'trusted',
capabilitiesGranted: [],
installedAt: over.installedAt ?? 1000,
updatedAt: over.updatedAt ?? 1000,
manifest: {
$schema: 'https://open-design.ai/schemas/plugin.v1.json',
name: id,
version: '0.1.0',
title: `Title ${id}`,
od: {
...(over.taskKind ? { taskKind: over.taskKind } : {}),
...(over.capabilities ? { capabilities: over.capabilities } : {}),
},
} as PluginManifest,
});
describe('pluginInventoryStats', () => {
it('returns zero-shape for an empty list', () => {
const stats = pluginInventoryStats([]);
expect(stats.total).toBe(0);
expect(stats.bundled).toBe(0);
expect(stats.thirdParty).toBe(0);
expect(stats.lastInstalledAt).toBeNull();
expect(stats.lastUpdatedAt).toBeNull();
expect(stats.bySourceKind).toEqual({});
});
it('counts by sourceKind', () => {
const plugins = [
make('a', { sourceKind: 'local' }),
make('b', { sourceKind: 'local' }),
make('c', { sourceKind: 'bundled', trust: 'bundled' }),
make('d', { sourceKind: 'github' }),
];
const stats = pluginInventoryStats(plugins);
expect(stats.bySourceKind).toEqual({ local: 2, bundled: 1, github: 1 });
});
it('counts by trust', () => {
const plugins = [
make('a', { trust: 'trusted' }),
make('b', { trust: 'restricted' }),
make('c', { trust: 'bundled', sourceKind: 'bundled' }),
];
const stats = pluginInventoryStats(plugins);
expect(stats.byTrust).toEqual({ trusted: 1, restricted: 1, bundled: 1 });
});
it('counts by taskKind, defaulting unset to "unknown"', () => {
const plugins = [
make('a', { taskKind: 'code-migration' }),
make('b', { taskKind: 'code-migration' }),
make('c'),
];
const stats = pluginInventoryStats(plugins);
expect(stats.byTaskKind).toEqual({ 'code-migration': 2, unknown: 1 });
});
it('flags plugins with elevated capabilities', () => {
const plugins = [
make('a', { capabilities: ['prompt:inject'] }),
make('b', { capabilities: ['fs:write'] }),
make('c', { capabilities: ['network'] }),
make('d', { capabilities: ['connector:slack'] }),
make('e', { capabilities: ['subprocess', 'bash'] }),
];
const stats = pluginInventoryStats(plugins);
expect(stats.withElevatedCapabilities).toBe(4); // b/c/d/e
});
it('splits bundled vs. third-party correctly', () => {
const plugins = [
make('a', { sourceKind: 'bundled', trust: 'bundled' }),
make('b', { sourceKind: 'github' }),
make('c', { sourceKind: 'local' }),
];
const stats = pluginInventoryStats(plugins);
expect(stats.bundled).toBe(1);
expect(stats.thirdParty).toBe(2);
});
it('finds the newest installedAt + updatedAt across the roster', () => {
const plugins = [
make('a', { installedAt: 1000, updatedAt: 2000 }),
make('b', { installedAt: 5000, updatedAt: 1500 }),
];
const stats = pluginInventoryStats(plugins);
expect(stats.lastInstalledAt).toBe(5000);
expect(stats.lastUpdatedAt).toBe(2000);
});
});
describe('snapshotInventoryStats', () => {
const row = (over: Partial<SnapshotStatsRow> = {}): SnapshotStatsRow => ({
status: 'fresh',
project_id: null,
run_id: null,
applied_at: 1000,
...over,
});
it('returns zero-shape for an empty list', () => {
const s = snapshotInventoryStats([]);
expect(s.total).toBe(0);
expect(s.byStatus).toEqual({});
expect(s.withProject).toBe(0);
expect(s.withRun).toBe(0);
expect(s.oldestAppliedAt).toBeNull();
expect(s.newestAppliedAt).toBeNull();
});
it('counts status / project / run / oldest+newest applied', () => {
const rows = [
row({ status: 'fresh', project_id: 'p1', run_id: 'r1', applied_at: 100 }),
row({ status: 'fresh', project_id: 'p2', run_id: null, applied_at: 200 }),
row({ status: 'stale', project_id: null, run_id: null, applied_at: 50 }),
];
const s = snapshotInventoryStats(rows);
expect(s.total).toBe(3);
expect(s.byStatus).toEqual({ fresh: 2, stale: 1 });
expect(s.withProject).toBe(2);
expect(s.withRun).toBe(1);
expect(s.oldestAppliedAt).toBe(50);
expect(s.newestAppliedAt).toBe(200);
});
});
describe('pluginSourceBuckets — Plan §3.MM2', () => {
it('returns total=0 + empty buckets[] for an empty list', () => {
const result = pluginSourceBuckets([]);
expect(result.total).toBe(0);
expect(result.buckets).toEqual([]);
});
// Local helper that overrides the make()'s baked-in source/fsPath.
const withSource = (rec: ReturnType<typeof make>, source: string) => ({ ...rec, source, fsPath: source });
it('aggregates plugins by (sourceKind, source) tuples', () => {
const result = pluginSourceBuckets([
withSource(make('a', { sourceKind: 'github' }), 'github:owner/repo'),
withSource(make('b', { sourceKind: 'github' }), 'github:owner/repo'),
withSource(make('c', { sourceKind: 'local' }), '/tmp/c'),
]);
expect(result.total).toBe(3);
expect(result.buckets).toHaveLength(2);
const github = result.buckets.find((b) => b.sourceKind === 'github');
expect(github?.count).toBe(2);
expect(github?.source).toBe('github:owner/repo');
expect(github?.plugins.map((p) => p.id).sort()).toEqual(['a', 'b']);
});
it('sorts buckets by descending count, then ascending sourceKind/source', () => {
const result = pluginSourceBuckets([
withSource(make('a', { sourceKind: 'local' }), '/x'),
withSource(make('b', { sourceKind: 'local' }), '/x'),
withSource(make('c', { sourceKind: 'github' }), 'github:owner/repo'),
]);
// local/(/x)=2, github/(github:owner/repo)=1 → local first.
expect(result.buckets[0]?.sourceKind).toBe('local');
expect(result.buckets[1]?.sourceKind).toBe('github');
});
it('breaks count ties alphabetically by sourceKind', () => {
const result = pluginSourceBuckets([
withSource(make('a', { sourceKind: 'local' }), '/x'),
withSource(make('b', { sourceKind: 'github' }), 'github:owner/repo'),
]);
// Both count=1; github < local alphabetically.
expect(result.buckets[0]?.sourceKind).toBe('github');
expect(result.buckets[1]?.sourceKind).toBe('local');
});
it('plugins inside a bucket sort by id alphabetically', () => {
const result = pluginSourceBuckets([
withSource(make('zeta', { sourceKind: 'local' }), '/x'),
withSource(make('alpha', { sourceKind: 'local' }), '/x'),
withSource(make('mango', { sourceKind: 'local' }), '/x'),
]);
expect(result.buckets[0]?.plugins.map((p) => p.id)).toEqual(['alpha', 'mango', 'zeta']);
});
});