mirror of
https://github.com/jackwener/OpenCLI.git
synced 2026-09-14 18:25:42 +08:00
78d52d984b
* test(plugin): add E2E integration tests for plugin lifecycle Add plugin-management.test.ts covering the full plugin lifecycle using real GitHub clone of opencli-plugin-hot-digest: - plugin install from github:ByteYue/opencli-plugin-hot-digest - plugin list (table and JSON formats) - plugin update on installed plugin - plugin uninstall with cleanup verification - error paths: invalid source, non-existent plugin, missing args Tests safely backup/restore existing plugin state to avoid interfering with user's real installed plugins. Update TESTING.md to document the new test file. * test(plugin): isolate lifecycle e2e from user home * fix(plugin): respect HOME env var for test isolation The E2E tests for plugin management were failing because os.homedir() doesn't respect the HOME environment variable. This made test isolation impossible since all tests would use the real ~/.opencli directory. Added getHomeDir() helper that checks process.env.HOME first before falling back to os.homedir(). Updated readLockFile() and writeLockFile() to use this new function. Fixes test failures in plugin-management.test.ts where: - plugin install would write to real home instead of temp dir - lock file assertions would fail with ENOENT --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
138 lines
4.9 KiB
TypeScript
138 lines
4.9 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).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(1);
|
|
expect(stderr).toContain('specify a plugin name');
|
|
});
|
|
});
|