GitNexus/gitnexus/test/integration/opencode-plugin.test.ts
rgb-vgx 338dee53ee
feat(opencode): ship hooks as auto-loaded global plugin for full support (#3619)
* feat(opencode): ship hooks as auto-loaded global plugin for full support

* fix(opencode): honor -e/--regexp as pattern and surface non-ENOENT plugin stat errors

* fix(opencode): block shell-metachar patterns in Windows fallback and unlink dangling plugin symlinks

* fix(opencode): quote-aware argv tokenization for quoted search phrases

* fix(opencode): skip plugin write on literal drift, glob token extraction, segment-anchored mutation check

* fix(opencode): parse shell search arguments safely

* fix(opencode): check multiline git mutations

* fix(opencode): recognize quoted git command boundaries

* fix(opencode): skip ambiguous search options

* docs(opencode): clarify compound search limits

---------

Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-10-28 13:45:08 +00:00

120 lines
4.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { pathToFileURL } from 'node:url';
const exec = promisify(execFile);
const cli = path.resolve('dist/cli/index.js');
describe('installed OpenCode plugin', () => {
let home: string;
let plugin: string;
let env: NodeJS.ProcessEnv;
beforeEach(async () => {
home = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-opencode ')));
plugin = path.join(home, '.config', 'opencode', 'plugins', 'gitnexus.js');
await fs.mkdir(path.dirname(plugin), { recursive: true });
await fs.writeFile(path.join(path.dirname(plugin), 'mine.js'), '// user plugin');
const skills = path.join(home, 'skills');
await fs.mkdir(skills);
env = {
...process.env,
HOME: home,
USERPROFILE: home,
GITNEXUS_HOME: path.join(home, 'registry'),
GITNEXUS_TEST_SKILLS_ROOT: skills,
};
// Uninstall visits every editor; an inherited override would escape this fixture.
delete env.CODEX_HOME;
await exec(process.execPath, [cli, 'setup', '-c', 'opencode'], { env, cwd: home });
});
afterEach(async () => {
await fs.rm(home, { recursive: true, force: true });
});
it("exposes only a plugin initializer to OpenCode's module loader", async () => {
const { stdout } = await exec(
process.execPath,
[
'--input-type=module',
'-e',
`
const mod = await import(${JSON.stringify(pathToFileURL(plugin).href)});
const hooks = [];
// OpenCode v1's legacy loader invokes every exported function.
for (const init of new Set(Object.values(mod))) hooks.push(await init({ directory: process.cwd() }));
console.log(JSON.stringify({ exports: Object.keys(mod), hooks: hooks.map(Object.keys) }));
`,
],
{ env, cwd: home },
);
expect(JSON.parse(stdout)).toEqual({
exports: ['GitNexusPlugin'],
hooks: [['tool.execute.after']],
});
});
it('runs the CLI outside the host executable, yields to the event loop, and uninstalls surgically', async () => {
const fakeCli = path.join(home, 'fake cli.cjs');
const calls = path.join(home, 'calls.jsonl');
await fs.writeFile(
fakeCli,
`
require('node:fs').appendFileSync(${JSON.stringify(calls)}, JSON.stringify({ argv: process.argv.slice(2), cwd: process.cwd() }) + '\\n');
setTimeout(() => {
if (process.argv[2] === 'augment') process.stderr.write('[GitNexus] callers: A');
else process.stdout.write(JSON.stringify({ status: 'stale', index: { commit: 'abc1234567' } }));
}, 30);
`,
);
const { stdout } = await exec(
process.execPath,
[
'--input-type=module',
'-e',
`
const { GitNexusPlugin } = await import(${JSON.stringify(pathToFileURL(plugin).href)});
// Compiled OpenCode reports its own binary here, not Node or Bun.
Object.defineProperty(process, 'execPath', { value: '/missing/opencode' });
const hooks = await GitNexusPlugin({ directory: process.cwd() });
let responsive = false;
const timer = setTimeout(() => { responsive = true; }, 0);
const search = { output: 'hits' };
await hooks['tool.execute.after']({ tool: 'grep', args: { pattern: 'User Service' } }, search);
clearTimeout(timer);
const mutation = { output: 'committed' };
await hooks['tool.execute.after']({ tool: 'bash', args: { command: 'git commit -m test' } }, mutation);
console.log(JSON.stringify({ search, mutation, responsive }));
`,
],
{ env: { ...env, GITNEXUS_HOOK_CLI_PATH: fakeCli }, cwd: home },
);
expect(JSON.parse(stdout)).toEqual({
search: { output: 'hits\n\n[GitNexus] callers: A' },
mutation: {
output:
'committed\n\n[GitNexus] GitNexus index is stale (last indexed: abc1234). Run `gitnexus analyze` to update the knowledge graph.',
},
responsive: true,
});
expect(
(await fs.readFile(calls, 'utf8'))
.trim()
.split('\n')
.map((line) => JSON.parse(line)),
).toEqual([
{ argv: ['augment', '--', 'User Service'], cwd: home },
{ argv: ['status', '--json'], cwd: home },
]);
await exec(process.execPath, [cli, 'uninstall', '--force'], { env, cwd: home });
await expect(fs.access(plugin)).rejects.toMatchObject({ code: 'ENOENT' });
expect(await fs.readFile(path.join(path.dirname(plugin), 'mine.js'), 'utf8')).toBe(
'// user plugin',
);
});
});