mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-04 07:27:20 +00:00
* 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>
120 lines
4.6 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|