9b395f5cc3
E2E Headed Chrome / e2e-headed (macos-15) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (ubuntu-latest) (push) Has been cancelled
E2E Headed Chrome / e2e-headed (windows-latest) (push) Has been cancelled
CI / build (macos-latest) (push) Has been cancelled
CI / build (ubuntu-latest) (push) Has been cancelled
CI / build (windows-latest) (push) Has been cancelled
CI / unit-test (push) Has been cancelled
CI / bun-test (push) Has been cancelled
CI / adapter-test (push) Has been cancelled
CI / smoke-test (macos-latest) (push) Has been cancelled
CI / smoke-test (ubuntu-latest) (push) Has been cancelled
Security Audit / audit (push) Has been cancelled
Build Chrome Extension / build (push) Has been cancelled
Trigger Website Rebuild (Docs Updated) / dispatch (push) Has been cancelled
141 lines
5.0 KiB
TypeScript
141 lines
5.0 KiB
TypeScript
/**
|
|
* 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<string, string> } = {},
|
|
) {
|
|
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');
|
|
});
|
|
});
|