/** * E2E integration tests for plugin management commands. * Uses a real GitHub plugin (opencli-plugin-hot-digest) to verify the full * install → list → update → uninstall lifecycle in an isolated HOME. */ import { describe, it, expect, afterAll } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { runCli, parseJsonOutput } from './helpers.js'; const TEST_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-plugin-e2e-')); const OPENCLI_HOME = path.join(TEST_HOME, '.opencli'); const PLUGINS_DIR = path.join(OPENCLI_HOME, 'plugins'); const PLUGIN_SOURCE = 'github:ByteYue/opencli-plugin-hot-digest'; const PLUGIN_NAME = 'hot-digest'; const PLUGIN_DIR = path.join(PLUGINS_DIR, PLUGIN_NAME); const LOCK_FILE = path.join(OPENCLI_HOME, 'plugins.lock.json'); function runPluginCli( args: string[], opts: { timeout?: number; env?: Record } = {}, ) { return runCli(args, { ...opts, env: { HOME: TEST_HOME, USERPROFILE: TEST_HOME, ...opts.env, }, }); } describe('plugin management E2E', () => { afterAll(() => { fs.rmSync(TEST_HOME, { recursive: true, force: true }); }); // ── plugin list (empty) ── it('plugin list shows "No plugins installed" when none exist', async () => { const { stdout, code } = await runPluginCli(['plugin', 'list']); expect(code).toBe(0); expect(stdout).toContain('No plugins installed'); }); // ── plugin install ── it('plugin install clones and sets up a real plugin', async () => { const { stdout, code } = await runPluginCli(['plugin', 'install', PLUGIN_SOURCE], { timeout: 60_000, }); expect(code).toBe(0); expect(stdout).toContain('installed successfully'); expect(stdout).toContain(PLUGIN_NAME); // Verify the plugin directory was created expect(fs.existsSync(PLUGIN_DIR)).toBe(true); // Verify lock file was updated const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8')); expect(lock[PLUGIN_NAME]).toBeDefined(); expect(lock[PLUGIN_NAME].commitHash).toBeTruthy(); expect(lock[PLUGIN_NAME].source).toMatchObject({ kind: 'git', }); expect(lock[PLUGIN_NAME].source.url).toContain('opencli-plugin-hot-digest'); expect(lock[PLUGIN_NAME].installedAt).toBeTruthy(); }, 60_000); // ── plugin list (after install) ── it('plugin list shows the installed plugin', async () => { const { stdout, code } = await runPluginCli(['plugin', 'list']); expect(code).toBe(0); expect(stdout).toContain(PLUGIN_NAME); }); it('plugin list -f json returns structured data', async () => { const { stdout, code } = await runPluginCli(['plugin', 'list', '-f', 'json']); expect(code).toBe(0); const data = parseJsonOutput(stdout); expect(Array.isArray(data)).toBe(true); const plugin = data.find((p: any) => p.name === PLUGIN_NAME); expect(plugin).toBeDefined(); expect(plugin.name).toBe(PLUGIN_NAME); expect(Array.isArray(plugin.commands)).toBe(true); expect(plugin.commands.length).toBeGreaterThan(0); }); // ── plugin update ── it('plugin update succeeds on an installed plugin', async () => { const { stdout, code } = await runPluginCli(['plugin', 'update', PLUGIN_NAME], { timeout: 30_000, }); expect(code).toBe(0); expect(stdout).toContain('updated successfully'); // Verify lock file has updatedAt const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8')); expect(lock[PLUGIN_NAME].updatedAt).toBeTruthy(); }, 30_000); // ── plugin uninstall ── it('plugin uninstall removes the plugin', async () => { const { stdout, code } = await runPluginCli(['plugin', 'uninstall', PLUGIN_NAME]); expect(code).toBe(0); expect(stdout).toContain('uninstalled'); // Verify directory was removed expect(fs.existsSync(PLUGIN_DIR)).toBe(false); // Verify lock entry was removed const lock = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8')); expect(lock[PLUGIN_NAME]).toBeUndefined(); }); // ── error paths ── it('plugin install rejects invalid source', async () => { const { stderr, code } = await runPluginCli(['plugin', 'install', 'invalid-source-format']); expect(code).toBe(1); expect(stderr).toContain('Invalid plugin source'); }); it('plugin uninstall rejects non-existent plugin', async () => { const { stderr, code } = await runPluginCli(['plugin', 'uninstall', '__nonexistent_plugin_xyz__']); expect(code).toBe(1); expect(stderr).toContain('not installed'); }); it('plugin update rejects non-existent plugin', async () => { const { stderr, code } = await runPluginCli(['plugin', 'update', '__nonexistent_plugin_xyz__']); expect(code).toBe(1); }); it('plugin update without name or --all shows error', async () => { const { stderr, code } = await runPluginCli(['plugin', 'update']); expect(code).toBe(2); expect(stderr).toContain('specify a plugin name'); }); });