open-design/apps/daemon/tests/plugins-e2e-fixture.test.ts
pftom 4c7cd5d9f2 feat(plugins): introduce plugin system with installation and management capabilities
- Added support for a new plugin system, allowing users to install, uninstall, and manage plugins through the daemon.
- Implemented API endpoints for listing installed plugins, retrieving plugin details, and applying plugins with input validation.
- Introduced a plugin doctor feature to validate plugin manifests and check for issues before application.
- Established a plugin persistence layer with SQLite migrations for managing installed plugins and their metadata.
- Enhanced the CLI with commands for plugin operations, improving user interaction with the plugin ecosystem.
2026-05-09 18:24:44 +08:00

125 lines
5.1 KiB
TypeScript

// e2e-1 closed-loop test (plan §10.1).
//
// Walks the Phase 1 happy path end-to-end using only the daemon Phase 1
// modules (no HTTP layer, no live agent). Specifically:
//
// 1. Install fixtures/plugin-fixtures/sample-plugin into a sandboxed
// registry root.
// 2. Read it back via listInstalledPlugins.
// 3. Compute applyPlugin → AppliedPluginSnapshot, persist via
// createSnapshot, then re-fetch it and confirm the manifest digest
// survives the round-trip.
// 4. Doctor the plugin and assert `ok: true`.
//
// This is the closed-loop the spec calls out as the Phase 1 definition of
// done. We keep it daemon-only so the loop runs without spinning the
// HTTP server, leaving the HTTP wiring to a separate smoke test.
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { mkdtemp, rm } from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import url from 'node:url';
import Database from 'better-sqlite3';
import { migratePlugins } from '../src/plugins/persistence.js';
import { applyPlugin } from '../src/plugins/apply.js';
import { doctorPlugin } from '../src/plugins/doctor.js';
import { installFromLocalFolder } from '../src/plugins/installer.js';
import { getInstalledPlugin, listInstalledPlugins } from '../src/plugins/registry.js';
import { createSnapshot, getSnapshot } from '../src/plugins/snapshots.js';
import { FIRST_PARTY_ATOMS, type AtomCatalogEntry } from '../src/plugins/atoms.js';
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'plugin-fixtures', 'sample-plugin');
let tmpRoot: string;
let pluginsRoot: string;
let db: Database.Database;
beforeEach(async () => {
tmpRoot = await mkdtemp(path.join(os.tmpdir(), 'od-plugin-e2e-'));
pluginsRoot = path.join(tmpRoot, 'plugins');
db = new Database(':memory:');
db.exec(`
CREATE TABLE projects (id TEXT PRIMARY KEY, name TEXT);
CREATE TABLE conversations (id TEXT PRIMARY KEY, project_id TEXT, title TEXT);
`);
migratePlugins(db);
db.prepare('INSERT INTO projects (id, name) VALUES (?, ?)').run('project-1', 'Project 1');
});
afterEach(async () => {
db.close();
await rm(tmpRoot, { recursive: true, force: true });
});
describe('plugin-system Phase 1 closed loop (e2e-1)', () => {
it('install → list → apply → snapshot → doctor', async () => {
let installed = false;
for await (const ev of installFromLocalFolder(db, {
source: FIXTURE_DIR,
roots: { userPluginsRoot: pluginsRoot },
})) {
if (ev.kind === 'success') installed = true;
if (ev.kind === 'error') throw new Error(ev.message);
}
expect(installed).toBe(true);
const list = listInstalledPlugins(db);
expect(list).toHaveLength(1);
const record = list[0]!;
expect(record.id).toBe('sample-plugin');
expect(record.title).toBe('Sample Plugin');
const fetched = getInstalledPlugin(db, 'sample-plugin');
expect(fetched).not.toBeNull();
const computed = applyPlugin({
plugin: fetched!,
inputs: { topic: 'AI design tools' },
registry: {
skills: [],
designSystems: [],
craft: [],
atoms: FIRST_PARTY_ATOMS.map((a: AtomCatalogEntry) => ({ id: a.id, label: a.label })),
},
});
expect(computed.result.appliedPlugin.pluginId).toBe('sample-plugin');
expect(computed.result.appliedPlugin.manifestSourceDigest).toMatch(/^[0-9a-f]{64}$/);
const snap = createSnapshot(db, {
projectId: 'project-1',
pluginId: computed.result.appliedPlugin.pluginId,
pluginVersion: computed.result.appliedPlugin.pluginVersion,
pluginTitle: computed.result.appliedPlugin.pluginTitle,
pluginDescription: computed.result.appliedPlugin.pluginDescription,
manifestSourceDigest: computed.manifestSourceDigest,
taskKind: computed.result.appliedPlugin.taskKind,
inputs: computed.result.appliedPlugin.inputs,
resolvedContext: computed.result.appliedPlugin.resolvedContext,
pipeline: computed.result.appliedPlugin.pipeline,
genuiSurfaces: computed.result.appliedPlugin.genuiSurfaces ?? [],
capabilitiesGranted: computed.result.appliedPlugin.capabilitiesGranted,
capabilitiesRequired: computed.result.appliedPlugin.capabilitiesRequired,
assetsStaged: computed.result.appliedPlugin.assetsStaged,
connectorsRequired: computed.result.appliedPlugin.connectorsRequired,
connectorsResolved: computed.result.appliedPlugin.connectorsResolved,
mcpServers: computed.result.appliedPlugin.mcpServers,
query: computed.result.query,
});
expect(snap.snapshotId).toMatch(/^[0-9a-f-]{36}$/);
const refetched = getSnapshot(db, snap.snapshotId);
expect(refetched).not.toBeNull();
expect(refetched!.manifestSourceDigest).toBe(computed.manifestSourceDigest);
expect(refetched!.inputs.topic).toBe('AI design tools');
const report = doctorPlugin(fetched!, {
skills: [],
designSystems: [],
craft: [],
atoms: FIRST_PARTY_ATOMS.map((a: AtomCatalogEntry) => ({ id: a.id, label: a.label })),
});
expect(report.ok).toBe(true);
});
});