75 lines
3.3 KiB
TypeScript
75 lines
3.3 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
|||
|
|
import { readFileSync } from 'node:fs';
|
|||
|
|
import { join } from 'node:path';
|
|||
|
|
import {
|
|||
|
|
OPENCODE_SERVER_PLUGIN_SOURCE,
|
|||
|
|
OPENCODE_TUI_PLUGIN_SOURCE,
|
|||
|
|
OPENCODE_SERVER_PLUGIN_FILENAME,
|
|||
|
|
OPENCODE_TUI_PLUGIN_FILENAME,
|
|||
|
|
} from '../../src/config/opencode-extension.js';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* `mcpctl config opencode` installs the *embedded* copy of the plugins, not the
|
|||
|
|
* files in src/opencode-ext/. Editing the sources without re-running the
|
|||
|
|
* generator therefore ships stale code to users while the repo looks correct —
|
|||
|
|
* and the embedded copy is the one thing no typecheck covers. Same guarantee
|
|||
|
|
* the completions check gives.
|
|||
|
|
*/
|
|||
|
|
const repoRoot = join(import.meta.dirname, '..', '..', '..', '..');
|
|||
|
|
const extDir = join(repoRoot, 'src', 'opencode-ext');
|
|||
|
|
|
|||
|
|
describe('embedded opencode plugins', () => {
|
|||
|
|
it('match the sources in src/opencode-ext (re-run scripts/generate-opencode-extension.ts)', () => {
|
|||
|
|
expect(OPENCODE_SERVER_PLUGIN_SOURCE, 'server plugin is stale — regenerate the embed')
|
|||
|
|
.toBe(readFileSync(join(extDir, 'mcpctl-opencode.ts'), 'utf-8'));
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE, 'TUI plugin is stale — regenerate the embed')
|
|||
|
|
.toBe(readFileSync(join(extDir, 'mcpctl-opencode-tui.tsx'), 'utf-8'));
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('install under names opencode can actually load', () => {
|
|||
|
|
// The server plugin is auto-discovered from plugin/*.ts; the TUI plugin is
|
|||
|
|
// referenced by path from tui.json and must stay .tsx for its JSX to be
|
|||
|
|
// transpiled.
|
|||
|
|
expect(OPENCODE_SERVER_PLUGIN_FILENAME).toBe('mcpctl.ts');
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_FILENAME).toBe('mcpctl-tui.tsx');
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('are self-contained — the installed files have no mcpctl imports to resolve', () => {
|
|||
|
|
for (const src of [OPENCODE_SERVER_PLUGIN_SOURCE, OPENCODE_TUI_PLUGIN_SOURCE]) {
|
|||
|
|
expect(src).not.toMatch(/from '@mcpctl\//);
|
|||
|
|
expect(src).not.toMatch(/from '\.\.\//);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('keep the JSX pragma the TUI plugin needs to render its indicator', () => {
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE.startsWith('/** @jsxImportSource @opentui/solid */')).toBe(true);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('agree on the MCP server name, so a switch re-points one mount instead of stacking two', () => {
|
|||
|
|
for (const src of [OPENCODE_SERVER_PLUGIN_SOURCE, OPENCODE_TUI_PLUGIN_SOURCE]) {
|
|||
|
|
expect(src).toContain("const SERVER_NAME = 'mcpctl'");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('agree on the state file both read', () => {
|
|||
|
|
for (const src of [OPENCODE_SERVER_PLUGIN_SOURCE, OPENCODE_TUI_PLUGIN_SOURCE]) {
|
|||
|
|
expect(src).toContain("join(homedir(), '.mcpctl', 'opencode-state.json')");
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('spawn the CLI without a shell, so a project name is never interpolated into a command string', () => {
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("execFile('mcpctl', args");
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE).not.toMatch(/\bexecSync\s*\(/);
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE).not.toMatch(/\bexec\(`/);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('sync skills into opencode’s own tree, never Claude’s', () => {
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--agent', 'opencode'");
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('switch without rewriting the plugin file opencode has already loaded', () => {
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--skip-plugin'");
|
|||
|
|
expect(OPENCODE_TUI_PLUGIN_SOURCE).toContain("'--skip-marker'");
|
|||
|
|
});
|
|||
|
|
});
|