mirror of
https://github.com/nexu-io/open-design.git
synced 2026-06-01 03:14:35 +07:00
- 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.
125 lines
5.1 KiB
TypeScript
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);
|
|
});
|
|
});
|