feat: add OpenCode adapter
Thin OpenCode plugin injecting the ponytail ruleset via experimental.chat.system.transform, reusing the shared instruction builder. Verified end-to-end on OpenCode 1.17.4. Supersedes #15.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
// Smoke test for the OpenCode adapter: the plugin's hooks behave against the
|
||||
// real (structural) OpenCode hook shapes. No live OpenCode needed.
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { pathToFileURL } = require('url');
|
||||
|
||||
// Point the plugin's mode-flag at a temp config home BEFORE it loads — the
|
||||
// plugin resolves its state path once at load (as it does under a real OpenCode
|
||||
// process, where XDG_CONFIG_HOME is already set). The dynamic import below runs
|
||||
// after this assignment, so the ordering holds.
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'ponytail-opencode-'));
|
||||
process.env.XDG_CONFIG_HOME = tmp;
|
||||
delete process.env.PONYTAIL_DEFAULT_MODE;
|
||||
const statePath = path.join(tmp, 'opencode', '.ponytail-active');
|
||||
|
||||
let loadPlugin;
|
||||
test.before(async () => {
|
||||
const url = pathToFileURL(path.join(__dirname, '..', '.opencode', 'plugins', 'ponytail.mjs'));
|
||||
loadPlugin = (await import(url)).default;
|
||||
});
|
||||
|
||||
function transform(hooks) {
|
||||
const output = { system: [] };
|
||||
return hooks['experimental.chat.system.transform']({ model: {} }, output).then(() => output.system);
|
||||
}
|
||||
|
||||
test('system.transform injects the ruleset at the default mode (full)', async () => {
|
||||
try { fs.unlinkSync(statePath); } catch (e) {}
|
||||
const hooks = await loadPlugin({});
|
||||
const system = await transform(hooks);
|
||||
assert.equal(system.length, 1);
|
||||
assert.match(system[0], /PONYTAIL MODE ACTIVE — level: full/);
|
||||
assert.match(system[0], /lazy senior developer/);
|
||||
});
|
||||
|
||||
test('command.execute.before persists /ponytail ultra, transform follows it', async () => {
|
||||
const hooks = await loadPlugin({});
|
||||
await hooks['command.execute.before']({ command: 'ponytail', arguments: 'ultra', sessionID: 's' });
|
||||
assert.equal(fs.readFileSync(statePath, 'utf8'), 'ultra');
|
||||
const system = await transform(hooks);
|
||||
assert.match(system[0], /PONYTAIL MODE ACTIVE — level: ultra/);
|
||||
});
|
||||
|
||||
test('/ponytail off persists off and transform injects nothing', async () => {
|
||||
const hooks = await loadPlugin({});
|
||||
await hooks['command.execute.before']({ command: 'ponytail', arguments: 'off', sessionID: 's' });
|
||||
assert.equal(fs.readFileSync(statePath, 'utf8'), 'off');
|
||||
const system = await transform(hooks);
|
||||
assert.deepEqual(system, []);
|
||||
});
|
||||
|
||||
test('unrelated commands do not touch the flag', async () => {
|
||||
try { fs.unlinkSync(statePath); } catch (e) {}
|
||||
const hooks = await loadPlugin({});
|
||||
await hooks['command.execute.before']({ command: 'commit', arguments: 'x', sessionID: 's' });
|
||||
assert.equal(fs.existsSync(statePath), false);
|
||||
});
|
||||
|
||||
test.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
||||
Reference in New Issue
Block a user