sdks/node: mirror Elydora-Node-SDK 2.0.1
Rebuilt from the standalone 2101cb7 tree (durable transactions, Kiro IDE adapter, hook retry with a bounded chain lock, version 2.0.1) with the Open-Source API trims re-applied: no webhooks, members, admin events, deep health, token rotation, or agent integration updates. Claude-Session: https://claude.ai/code/session_01QoUjdc7HGKTsgj5GhG8HvF
This commit is contained in:
1 parent
1656a94811
commit
98c61a0b6f
44 files changed
+6794
-1130
No files matched your search
@@ -1,5 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { existsSync, utimesSync, writeFileSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import http from 'node:http';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
@@ -143,3 +145,196 @@ test('Claude runtime failures stay observable while guard and audit delivery rem
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
function mismatchServer(respond) {
|
||||
const submissions = [];
|
||||
const server = http.createServer((request, response) => {
|
||||
let raw = '';
|
||||
request.on('data', (chunk) => { raw += chunk; });
|
||||
request.on('end', () => {
|
||||
if (request.url !== '/v1/operations') {
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify({ agent: { status: 'active' } }));
|
||||
return;
|
||||
}
|
||||
submissions.push(JSON.parse(raw));
|
||||
const reply = respond(submissions) ?? mismatch(String(submissions.length).padStart(43, 'A'), 'x');
|
||||
const send = () => {
|
||||
response.writeHead(reply.status, { 'Content-Type': 'application/json' });
|
||||
response.end(JSON.stringify(reply.body));
|
||||
};
|
||||
if (api.slow) setTimeout(send, 2600); else send();
|
||||
});
|
||||
});
|
||||
const api = { submissions, slow: false };
|
||||
return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve(Object.assign(api, {
|
||||
baseUrl: 'http://127.0.0.1:' + server.address().port,
|
||||
close: () => new Promise((done) => { server.closeAllConnections?.(); server.close(done); }),
|
||||
}))));
|
||||
}
|
||||
|
||||
function mismatch(expected, actual) {
|
||||
return {
|
||||
status: 400,
|
||||
body: { error: { code: 'PREV_HASH_MISMATCH', message: 'Expected prev_chain_hash "' + expected + '", got "' + actual + '".' } },
|
||||
};
|
||||
}
|
||||
|
||||
test('Claude audit retries a rejected prev_chain_hash with the server value', async () => {
|
||||
const expected = 'Rxlf4j36C3KvIQ3hWuOkX698BR5iDypUFuB70JjEuvM';
|
||||
const api = await mismatchServer((submissions) => (submissions.length === 1
|
||||
? mismatch(expected, submissions[0].prev_chain_hash)
|
||||
: { status: 202, body: { receipt: { seq_no: 2 } } }));
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { settings } = await readSettings(fixture.settingsPath);
|
||||
const audit = managedHandler(settings, 'PostToolUse');
|
||||
const result = await runClaudeHook(
|
||||
audit,
|
||||
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(api.submissions.length, 2);
|
||||
assert.equal(api.submissions[1].prev_chain_hash, expected);
|
||||
assert.notEqual(api.submissions[1].operation_id, api.submissions[0].operation_id);
|
||||
assert.notEqual(api.submissions[1].nonce, api.submissions[0].nonce);
|
||||
const state = JSON.parse(await readFile(path.join(fixture.agentDir, 'chain-state.json'), 'utf-8'));
|
||||
assert.equal(state.prev_chain_hash, api.submissions[1].chain_hash);
|
||||
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /resynced to server: Rxlf/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Claude audit stops after five rejected prev_chain_hash attempts', async () => {
|
||||
const api = await mismatchServer((submissions) => (
|
||||
mismatch(String(submissions.length).padStart(43, 'A'), submissions.at(-1).prev_chain_hash)
|
||||
));
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { settings } = await readSettings(fixture.settingsPath);
|
||||
const audit = managedHandler(settings, 'PostToolUse');
|
||||
const result = await runClaudeHook(
|
||||
audit,
|
||||
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(api.submissions.length, 5);
|
||||
assert.equal(api.submissions[4].prev_chain_hash, '4'.padStart(43, 'A'));
|
||||
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /rejected prev_chain_hash 5 times/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Claude audit stops retrying when the submit budget is spent', async () => {
|
||||
const api = await mismatchServer(() => null);
|
||||
api.slow = true;
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { settings } = await readSettings(fixture.settingsPath);
|
||||
const audit = managedHandler(settings, 'PostToolUse');
|
||||
const started = Date.now();
|
||||
const result = await runClaudeHook(
|
||||
audit,
|
||||
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
|
||||
fixture,
|
||||
);
|
||||
const elapsed = Date.now() - started;
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.ok(elapsed < 7000, `hook ran ${elapsed}ms`);
|
||||
assert.ok(api.submissions.length >= 2 && api.submissions.length <= 3, `${api.submissions.length} submissions`);
|
||||
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /aborted|retry budget exhausted/i);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Claude audit does not regress a chain head advanced by a concurrent hook', async () => {
|
||||
const newerHead = 'B'.repeat(43);
|
||||
let statePath;
|
||||
const api = await mismatchServer(() => {
|
||||
writeFileSync(statePath, JSON.stringify({ prev_chain_hash: newerHead }), { mode: 0o600 });
|
||||
return { status: 202, body: { receipt: { seq_no: 1 } } };
|
||||
});
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
statePath = path.join(fixture.agentDir, 'chain-state.json');
|
||||
const { settings } = await readSettings(fixture.settingsPath);
|
||||
const audit = managedHandler(settings, 'PostToolUse');
|
||||
const result = await runClaudeHook(
|
||||
audit,
|
||||
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(api.submissions[0].prev_chain_hash, 'A'.repeat(43));
|
||||
const state = JSON.parse(await readFile(statePath, 'utf-8'));
|
||||
assert.equal(state.prev_chain_hash, newerHead);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Claude audit clears a stale chain-state lock and proceeds', async () => {
|
||||
const api = await mismatchServer(() => ({ status: 202, body: { receipt: { seq_no: 1 } } }));
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const lockPath = path.join(fixture.agentDir, 'chain-state.json.lock');
|
||||
writeFileSync(lockPath, '', { mode: 0o600 });
|
||||
const stale = new Date(Date.now() - 10_000);
|
||||
utimesSync(lockPath, stale, stale);
|
||||
const { settings } = await readSettings(fixture.settingsPath);
|
||||
const audit = managedHandler(settings, 'PostToolUse');
|
||||
const result = await runClaudeHook(
|
||||
audit,
|
||||
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(api.submissions.length, 1);
|
||||
assert.equal(existsSync(lockPath), false);
|
||||
const state = JSON.parse(await readFile(path.join(fixture.agentDir, 'chain-state.json'), 'utf-8'));
|
||||
assert.equal(state.prev_chain_hash, api.submissions[0].chain_hash);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Claude audit does not reclaim a chain-state lock whose owner is alive', async () => {
|
||||
const api = await mismatchServer(() => ({ status: 202, body: { receipt: { seq_no: 1 } } }));
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const lockPath = path.join(fixture.agentDir, 'chain-state.json.lock');
|
||||
writeFileSync(lockPath, String(process.pid), { mode: 0o600 });
|
||||
const stale = new Date(Date.now() - 10_000);
|
||||
utimesSync(lockPath, stale, stale);
|
||||
const { settings } = await readSettings(fixture.settingsPath);
|
||||
const audit = managedHandler(settings, 'PostToolUse');
|
||||
const result = await runClaudeHook(
|
||||
audit,
|
||||
JSON.stringify(officialPayload('PostToolUse', { tool_response: { stdout: 'ok' } })),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(api.submissions.length, 1);
|
||||
assert.equal(existsSync(lockPath), true);
|
||||
assert.match(await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'), /lock timed out/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
@@ -32,7 +32,7 @@ const AUDIT_STATUS = 'Recording Elydora tool use';
|
||||
test('Codex is registered with the native user hooks file', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('codex'), {
|
||||
name: 'OpenAI Codex',
|
||||
name: 'Codex CLI',
|
||||
configDir: '~/.codex',
|
||||
configFile: 'hooks.json',
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
test('Cursor is registered with the native user hook file', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('cursor'), {
|
||||
name: 'Cursor',
|
||||
name: 'Cursor CLI',
|
||||
configDir: '~/.cursor',
|
||||
configFile: 'hooks.json',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createFixture,
|
||||
runProcess,
|
||||
} from '../test-support/kirocli-test-helpers.mjs';
|
||||
|
||||
test('official Kiro CLI 2.13 validates the installed v2 agent contract', {
|
||||
skip: process.env.ELYDORA_KIRO_CLI_BINARY
|
||||
? false
|
||||
: 'set ELYDORA_KIRO_CLI_BINARY to the official Kiro CLI executable',
|
||||
}, async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const install = await fixture.install();
|
||||
assert.equal(install.code, 0, install.stderr);
|
||||
const binary = process.env.ELYDORA_KIRO_CLI_BINARY;
|
||||
const version = await runProcess(binary, ['--version'], {}, fixture.projectDir);
|
||||
assert.equal(version.code, 0, version.stderr);
|
||||
assert.match(version.stdout, /\b2\.13\.\d+\b/);
|
||||
const validation = await runProcess(
|
||||
binary,
|
||||
['agent', 'validate', '--path', fixture.v2Path],
|
||||
{},
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(validation.code, 0, validation.stderr);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -1,314 +1,448 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
VALID_PRIVATE_KEY,
|
||||
cliPath,
|
||||
createFixture,
|
||||
environment,
|
||||
findV3Hook,
|
||||
managedV2Document,
|
||||
readJson,
|
||||
registryModuleUrl,
|
||||
runNode,
|
||||
runPlugin,
|
||||
runShell,
|
||||
startApiServer,
|
||||
} from '../test-support/kirocli-test-helpers.mjs';
|
||||
|
||||
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/kirocli.js')).href;
|
||||
const registryModuleUrl = pathToFileURL(path.resolve('dist/plugins/registry.js')).href;
|
||||
|
||||
function runNode(args, env, input = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, args, {
|
||||
env: { ...process.env, ...env },
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
||||
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => resolve({ code, stdout, stderr }));
|
||||
child.stdin.end(input);
|
||||
});
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
function runCommand(command, input = '') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, { shell: true, stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
||||
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => resolve({ code, stdout, stderr }));
|
||||
child.stdin.end(input);
|
||||
});
|
||||
}
|
||||
|
||||
async function runPlugin(homeDir, method, argument) {
|
||||
const script = `
|
||||
import { kirocliPlugin } from ${JSON.stringify(pluginModuleUrl)};
|
||||
const argument = JSON.parse(process.env.ELYDORA_TEST_ARGUMENT);
|
||||
const result = await kirocliPlugin[process.env.ELYDORA_TEST_METHOD](argument);
|
||||
if (result !== undefined) console.log(JSON.stringify(result));
|
||||
`;
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', script],
|
||||
{
|
||||
HOME: homeDir,
|
||||
USERPROFILE: homeDir,
|
||||
ELYDORA_TEST_ARGUMENT: JSON.stringify(argument),
|
||||
ELYDORA_TEST_METHOD: method,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function writeJsonOrText(filePath, value) {
|
||||
async function writeJson(filePath, value) {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, typeof value === 'string' ? value : JSON.stringify(value, null, 2));
|
||||
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
async function createFixture({ existingV2, existingV3, guardSource, hookSource } = {}) {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-kirocli-'));
|
||||
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
|
||||
const v2Path = path.join(homeDir, '.kiro', 'agents', 'elydora-audit.json');
|
||||
const v3Path = path.join(homeDir, '.kiro', 'hooks', 'elydora-audit.json');
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
|
||||
const guardScriptPath = path.join(agentDir, 'guard.js');
|
||||
const hookScriptPath = path.join(agentDir, 'hook.js');
|
||||
const runtimeConfigPath = path.join(agentDir, 'config.json');
|
||||
await writeFile(
|
||||
guardScriptPath,
|
||||
guardSource ?? "process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n",
|
||||
);
|
||||
await writeFile(hookScriptPath, hookSource ?? 'process.exit(0);\n');
|
||||
await writeFile(runtimeConfigPath, JSON.stringify({
|
||||
agent_id: 'agent-1',
|
||||
agent_name: 'kirocli',
|
||||
}));
|
||||
|
||||
if (existingV2 !== undefined) await writeJsonOrText(v2Path, existingV2);
|
||||
if (existingV3 !== undefined) await writeJsonOrText(v3Path, existingV3);
|
||||
|
||||
const config = {
|
||||
agentName: 'kirocli',
|
||||
agentId: 'agent-1',
|
||||
baseUrl: 'https://api.elydora.com',
|
||||
guardScriptPath,
|
||||
hookScriptPath,
|
||||
};
|
||||
const installResult = await runPlugin(homeDir, 'install', config);
|
||||
|
||||
function userV3Hook() {
|
||||
return {
|
||||
config,
|
||||
guardScriptPath,
|
||||
homeDir,
|
||||
hookScriptPath,
|
||||
installResult,
|
||||
runtimeConfigPath,
|
||||
v2Path,
|
||||
v3Path,
|
||||
async close() {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
name: 'global-context',
|
||||
trigger: 'SessionStart',
|
||||
action: { type: 'agent', prompt: 'Read AGENTS.md' },
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
function legacyCommand(scriptPath) {
|
||||
if (process.platform === 'win32') return `"${process.execPath}" "${scriptPath}"`;
|
||||
const quote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
||||
return `${quote(process.execPath)} ${quote(scriptPath)}`;
|
||||
}
|
||||
|
||||
function legacyDocuments(fixture) {
|
||||
const guard = legacyCommand(fixture.guardScriptPath);
|
||||
const audit = legacyCommand(fixture.hookScriptPath);
|
||||
return {
|
||||
v2: managedV2Document({
|
||||
hooks: {
|
||||
preToolUse: [{ matcher: '*', command: guard, timeout_ms: 5000 }],
|
||||
postToolUse: [{ matcher: '*', command: audit, timeout_ms: 5000 }],
|
||||
},
|
||||
}),
|
||||
v3: {
|
||||
version: 'v1',
|
||||
hooks: [
|
||||
{
|
||||
name: 'elydora-guard',
|
||||
description: 'Block tool use when the Elydora agent is frozen',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: '.*',
|
||||
action: { type: 'command', command: guard },
|
||||
timeout: 5,
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'elydora-audit',
|
||||
description: 'Record tool use in the Elydora audit trail',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: '.*',
|
||||
action: { type: 'command', command: audit },
|
||||
timeout: 5,
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function findV3Hook(config, name) {
|
||||
return config.hooks.find((hook) => hook.name === name);
|
||||
}
|
||||
|
||||
test('Kiro CLI registry points at the v3 global hook contract', async () => {
|
||||
test('Kiro CLI installs exact v2 and 2.13 v3 global contracts atomically', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('kirocli'), {
|
||||
name: 'Kiro CLI',
|
||||
configDir: '~/.kiro/hooks',
|
||||
configFile: 'elydora-audit.json',
|
||||
});
|
||||
});
|
||||
|
||||
test('Kiro CLI install preserves user hooks and writes idempotent v2 and v3 contracts', async () => {
|
||||
const fixture = await createFixture({
|
||||
existingV2: {
|
||||
description: 'User Kiro agent',
|
||||
tools: ['read'],
|
||||
existingV2: managedV2Document({
|
||||
owner: 'user',
|
||||
hooks: {
|
||||
agentSpawn: [{ command: 'existing-spawn' }],
|
||||
preToolUse: [{ matcher: 'read', command: 'existing-v2' }],
|
||||
},
|
||||
},
|
||||
existingV3: {
|
||||
version: 'v1',
|
||||
hooks: [{
|
||||
name: 'existing-v3',
|
||||
trigger: 'SessionStart',
|
||||
action: { type: 'command', command: 'existing-command' },
|
||||
}],
|
||||
},
|
||||
}),
|
||||
existingV3: { version: 'v1', owner: 'user', hooks: [userV3Hook()] },
|
||||
});
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
|
||||
assert.match(fixture.installResult.stdout, /--agent elydora-audit/);
|
||||
assert.match(fixture.installResult.stdout, /--v3/);
|
||||
const secondInstall = await runPlugin(fixture.homeDir, 'install', fixture.config);
|
||||
assert.equal(secondInstall.code, 0, secondInstall.stderr);
|
||||
|
||||
const v2 = JSON.parse(await readFile(fixture.v2Path, 'utf-8'));
|
||||
assert.equal(v2.description, 'User Kiro agent');
|
||||
assert.deepEqual(v2.tools, ['read']);
|
||||
assert.equal(v2.hooks.agentSpawn[0].command, 'existing-spawn');
|
||||
const first = await fixture.install();
|
||||
assert.equal(first.code, 0, first.stderr);
|
||||
assert.match(first.stdout, /2\.13\.0\+/);
|
||||
assert.match(first.stdout, /agent validate/);
|
||||
assert.match(first.stdout, /--agent elydora-audit/);
|
||||
const firstV2Source = await readFile(fixture.v2Path, 'utf-8');
|
||||
const firstV3Source = await readFile(fixture.v3Path, 'utf-8');
|
||||
const v2 = JSON.parse(firstV2Source);
|
||||
assert.equal(v2.owner, 'user');
|
||||
assert.deepEqual(v2.hooks.agentSpawn, [{ command: 'existing-spawn' }]);
|
||||
assert.equal(v2.hooks.preToolUse.length, 2);
|
||||
assert.equal(v2.hooks.postToolUse.length, 1);
|
||||
assert.equal(v2.hooks.preToolUse[1].matcher, '*');
|
||||
assert.equal(typeof v2.hooks.preToolUse[1].command, 'string');
|
||||
|
||||
const v3 = JSON.parse(await readFile(fixture.v3Path, 'utf-8'));
|
||||
assert.equal(v3.version, 'v1');
|
||||
assert.equal(v3.hooks.length, 3);
|
||||
assert.equal(v3.hooks[0].name, 'existing-v3');
|
||||
assert.deepEqual(v2.hooks.preToolUse[1], {
|
||||
matcher: '*',
|
||||
command: v2.hooks.preToolUse[1].command,
|
||||
timeout_ms: 10_000,
|
||||
});
|
||||
assert.deepEqual(v2.hooks.postToolUse[0], {
|
||||
matcher: '*',
|
||||
command: v2.hooks.postToolUse[0].command,
|
||||
timeout_ms: 10_000,
|
||||
});
|
||||
const v3 = JSON.parse(firstV3Source);
|
||||
assert.equal(v3.owner, 'user');
|
||||
assert.deepEqual(v3.hooks[0], userV3Hook());
|
||||
assert.deepEqual(findV3Hook(v3, 'elydora-guard'), {
|
||||
name: 'elydora-guard',
|
||||
description: 'Block tool use when the Elydora agent is frozen',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: '.*',
|
||||
action: { type: 'command', command: findV3Hook(v3, 'elydora-guard').action.command },
|
||||
timeout: 5,
|
||||
timeout: 10,
|
||||
enabled: true,
|
||||
});
|
||||
assert.equal(findV3Hook(v3, 'elydora-audit').trigger, 'PostToolUse');
|
||||
assert.deepEqual(await readJson(path.join(fixture.agentDir, 'config.json')), {
|
||||
org_id: 'org-1',
|
||||
agent_id: fixture.agentId,
|
||||
kid: 'kid-1',
|
||||
base_url: fixture.baseUrl,
|
||||
token: 'token-1',
|
||||
agent_name: 'kirocli',
|
||||
});
|
||||
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
|
||||
const second = await fixture.install();
|
||||
assert.equal(second.code, 0, second.stderr);
|
||||
assert.equal(await readFile(fixture.v2Path, 'utf-8'), firstV2Source);
|
||||
assert.equal(await readFile(fixture.v3Path, 'utf-8'), firstV3Source);
|
||||
const names = [
|
||||
...await readdir(path.dirname(fixture.v2Path)),
|
||||
...await readdir(path.dirname(fixture.v3Path)),
|
||||
...await readdir(fixture.agentDir),
|
||||
];
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false);
|
||||
if (process.platform !== 'win32') {
|
||||
assert.equal((await stat(fixture.v2Path)).mode & 0o777, 0o600);
|
||||
assert.equal((await stat(fixture.guardScriptPath)).mode & 0o777, 0o700);
|
||||
}
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI hook commands enforce freezes and forward the official event payload', async () => {
|
||||
const capturePath = path.join(os.tmpdir(), `elydora-kiro-event-${process.pid}-${Date.now()}.json`);
|
||||
const fixture = await createFixture({
|
||||
hookSource: `
|
||||
const fs = require('node:fs');
|
||||
const chunks = [];
|
||||
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
||||
process.stdin.on('end', () => fs.writeFileSync(${JSON.stringify(capturePath)}, Buffer.concat(chunks)));
|
||||
`,
|
||||
});
|
||||
test('Kiro CLI commands block frozen agents and preserve native events', async () => {
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
|
||||
const v3 = JSON.parse(await readFile(fixture.v3Path, 'utf-8'));
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const v2 = await readJson(fixture.v2Path);
|
||||
const v3 = await readJson(fixture.v3Path);
|
||||
assert.equal(v2.hooks.preToolUse[0].command, findV3Hook(v3, 'elydora-guard').action.command);
|
||||
const payload = {
|
||||
hook_event_name: 'PreToolUse',
|
||||
cwd: fixture.homeDir,
|
||||
hook_event_name: 'preToolUse',
|
||||
cwd: fixture.projectDir,
|
||||
session_id: 'session-1',
|
||||
tool_name: 'execute_bash',
|
||||
tool_input: { command: 'echo test' },
|
||||
tool_input: { command: 'npm test' },
|
||||
future_field: { retained: true },
|
||||
};
|
||||
const guardResult = await runCommand(findV3Hook(v3, 'elydora-guard').action.command, JSON.stringify(payload));
|
||||
assert.equal(guardResult.code, 2);
|
||||
assert.match(guardResult.stderr, /Agent is frozen by Elydora/);
|
||||
|
||||
payload.hook_event_name = 'PostToolUse';
|
||||
payload.tool_response = { success: true, result: 'test' };
|
||||
const auditResult = await runCommand(findV3Hook(v3, 'elydora-audit').action.command, JSON.stringify(payload));
|
||||
assert.equal(auditResult.code, 0, auditResult.stderr);
|
||||
assert.deepEqual(JSON.parse(await readFile(capturePath, 'utf-8')), payload);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await rm(capturePath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI status accepts either contract and requires both runtime scripts', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
|
||||
const status = await runPlugin(fixture.homeDir, 'status', null);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.equal(JSON.parse(status.stdout).installed, true);
|
||||
|
||||
await rm(fixture.v3Path);
|
||||
const v2Only = await runPlugin(fixture.homeDir, 'status', null);
|
||||
const v2Status = JSON.parse(v2Only.stdout);
|
||||
assert.equal(v2Status.installed, true);
|
||||
assert.equal(v2Status.configPath, fixture.v2Path);
|
||||
|
||||
const reinstall = await runPlugin(fixture.homeDir, 'install', fixture.config);
|
||||
assert.equal(reinstall.code, 0, reinstall.stderr);
|
||||
await rm(fixture.v2Path);
|
||||
const v3Only = await runPlugin(fixture.homeDir, 'status', null);
|
||||
const v3Status = JSON.parse(v3Only.stdout);
|
||||
assert.equal(v3Status.installed, true);
|
||||
assert.equal(v3Status.configPath, fixture.v3Path);
|
||||
|
||||
await rm(fixture.guardScriptPath);
|
||||
const degraded = await runPlugin(fixture.homeDir, 'status', null);
|
||||
assert.equal(JSON.parse(degraded.stdout).installed, false);
|
||||
await writeJson(path.join(fixture.agentDir, 'status-cache.json'), {
|
||||
status: 'frozen',
|
||||
cached_at: Date.now(),
|
||||
});
|
||||
const guard = await runShell(
|
||||
findV3Hook(v3, 'elydora-guard').action.command,
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
assert.equal(guard.code, 2, guard.stderr);
|
||||
await rm(path.join(fixture.agentDir, 'status-cache.json'));
|
||||
payload.hook_event_name = 'postToolUse';
|
||||
payload.tool_response = { success: true, result: '12 passed' };
|
||||
const audit = await runShell(
|
||||
findV3Hook(v3, 'elydora-audit').action.command,
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
assert.equal(audit.code, 0, audit.stderr);
|
||||
const operation = JSON.parse(api.requests.find(({ method }) => method === 'POST').raw);
|
||||
assert.deepEqual(operation.payload, payload);
|
||||
assert.deepEqual(operation.action, { tool: 'execute_bash' });
|
||||
assert.deepEqual(operation.subject, { session_id: 'session-1' });
|
||||
} finally {
|
||||
await api.close();
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI status surfaces malformed referenced runtime metadata', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
|
||||
await writeFile(fixture.runtimeConfigPath, '{ malformed');
|
||||
const status = await runPlugin(fixture.homeDir, 'status', null);
|
||||
assert.equal(status.code, 1);
|
||||
assert.match(status.stderr, /parse Elydora runtime config/i);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI uninstall preserves unrelated v2 and v3 hooks', async () => {
|
||||
const fixture = await createFixture({
|
||||
existingV2: { hooks: { preToolUse: [{ matcher: 'read', command: 'existing-v2' }] } },
|
||||
existingV3: {
|
||||
version: 'v1',
|
||||
hooks: [{
|
||||
name: 'existing-v3',
|
||||
trigger: 'SessionStart',
|
||||
action: { type: 'command', command: 'existing-command' },
|
||||
}],
|
||||
},
|
||||
test('Kiro CLI status requires both exact contracts and physical runtime identity', async (t) => {
|
||||
await t.test('missing home is an empty installation', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
await rm(fixture.homeDir, { recursive: true });
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.equal(JSON.parse(status.stdout).installed, false);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
|
||||
const result = await runPlugin(fixture.homeDir, 'uninstall', 'agent-1');
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
const v2 = JSON.parse(await readFile(fixture.v2Path, 'utf-8'));
|
||||
assert.deepEqual(v2.hooks.preToolUse, [{ matcher: 'read', command: 'existing-v2' }]);
|
||||
assert.deepEqual(v2.hooks.postToolUse, []);
|
||||
const v3 = JSON.parse(await readFile(fixture.v3Path, 'utf-8'));
|
||||
assert.equal(v3.hooks.length, 1);
|
||||
assert.equal(v3.hooks[0].name, 'existing-v3');
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await t.test('missing or disabled contract is degraded and reinstall repairs it', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
let status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).installed, true);
|
||||
const v3 = await readJson(fixture.v3Path);
|
||||
findV3Hook(v3, 'elydora-guard').enabled = false;
|
||||
await writeJson(fixture.v3Path, v3);
|
||||
status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).installed, false);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await rm(fixture.v2Path);
|
||||
status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
await t.test('recognized altered hooks degrade status', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const v2 = await readJson(fixture.v2Path);
|
||||
v2.hooks.preToolUse.push({ ...v2.hooks.preToolUse[0], timeout_ms: 5000 });
|
||||
await writeJson(fixture.v2Path, v2);
|
||||
let status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
|
||||
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const v3 = await readJson(fixture.v3Path);
|
||||
v3.hooks.push({ ...findV3Hook(v3, 'elydora-guard'), enabled: false });
|
||||
await writeJson(fixture.v3Path, v3);
|
||||
status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
for (const [label, target, contents, expected] of [
|
||||
['guard runtime', 'guard', 'tampered\n', /"installed":false/],
|
||||
['runtime config', 'config', '{ malformed', /parse Elydora runtime config/i],
|
||||
['runtime identity', 'config', JSON.stringify({
|
||||
org_id: 'org-1',
|
||||
agent_id: 'agent-1',
|
||||
kid: 'kid-1',
|
||||
base_url: 'http://127.0.0.1:9',
|
||||
token: 'token-1',
|
||||
agent_name: 'kiroide',
|
||||
}), /runtime identity/i],
|
||||
['private key', 'key', 'invalid', /private key is invalid/i],
|
||||
]) {
|
||||
await t.test(label, async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const targets = {
|
||||
guard: fixture.guardScriptPath,
|
||||
config: path.join(fixture.agentDir, 'config.json'),
|
||||
key: path.join(fixture.agentDir, 'private.key'),
|
||||
};
|
||||
await writeFile(targets[target], contents);
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.match(`${status.stdout}${status.stderr}`, expected);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI uninstall removes configs owned entirely by Elydora', async () => {
|
||||
test('Kiro CLI rejects malformed, colliding, invalid, and linked inputs before writes', async (t) => {
|
||||
for (const [label, options, pattern] of [
|
||||
['duplicate v2', { existingV2: '{"name":"elydora-audit","name":"other"}' }, /duplicate/i],
|
||||
['v2 path collision', { existingV2: { name: 'user-agent', hooks: {} } }, /v2 agent path conflicts/i],
|
||||
['v3 name collision', { existingV3: { version: 'v1', hooks: [{ name: 'elydora-guard', trigger: 'PreToolUse', action: { type: 'command', command: 'user-command' } }] } }, /hook name.*conflicts/i],
|
||||
]) {
|
||||
await t.test(label, async () => {
|
||||
const fixture = await createFixture(options);
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, pattern);
|
||||
await assertMissing(fixture.guardScriptPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
await t.test('non-canonical private key', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const result = await fixture.install({ privateKey: 'invalid' });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /canonical 32-byte/);
|
||||
await assertMissing(fixture.v2Path);
|
||||
await assertMissing(fixture.v3Path);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
await t.test('linked Kiro directory', async () => {
|
||||
const fixture = await createFixture();
|
||||
const external = path.join(fixture.rootDir, 'external');
|
||||
try {
|
||||
await mkdir(external);
|
||||
await symlink(external, path.join(fixture.homeDir, '.kiro'), 'junction');
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /not a physical directory|symbolic link/i);
|
||||
await assertMissing(path.join(external, 'agents', 'elydora-audit.json'));
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Kiro CLI migrates exact legacy commands and repairs both schemas', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 0, fixture.installResult.stderr);
|
||||
const result = await runPlugin(fixture.homeDir, 'uninstall', 'agent-1');
|
||||
const legacy = legacyDocuments(fixture);
|
||||
await writeJson(fixture.v2Path, legacy.v2);
|
||||
await writeJson(fixture.v3Path, legacy.v3);
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
await assert.rejects(readFile(fixture.v2Path), { code: 'ENOENT' });
|
||||
await assert.rejects(readFile(fixture.v3Path), { code: 'ENOENT' });
|
||||
const v2 = await readJson(fixture.v2Path);
|
||||
const v3 = await readJson(fixture.v3Path);
|
||||
assert.equal(v2.hooks.preToolUse.length, 1);
|
||||
assert.equal(v2.hooks.preToolUse[0].timeout_ms, 10_000);
|
||||
assert.equal(findV3Hook(v3, 'elydora-guard').timeout, 10);
|
||||
assert.equal(findV3Hook(v3, 'elydora-guard').action.command, v2.hooks.preToolUse[0].command);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI install preserves malformed configs for recovery', async () => {
|
||||
const fixture = await createFixture({ existingV3: '{ malformed' });
|
||||
test('Kiro CLI uninstall preserves user hooks and removes fully owned files', async (t) => {
|
||||
await t.test('mixed files', async () => {
|
||||
const fixture = await createFixture({
|
||||
existingV2: managedV2Document({ hooks: { agentSpawn: [{ command: 'existing-spawn' }] } }),
|
||||
existingV3: { version: 'v1', hooks: [userV3Hook()] },
|
||||
});
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
const v2 = await readJson(fixture.v2Path);
|
||||
assert.deepEqual(v2.hooks, { agentSpawn: [{ command: 'existing-spawn' }] });
|
||||
const v3 = await readJson(fixture.v3Path);
|
||||
assert.deepEqual(v3.hooks, [userV3Hook()]);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
await t.test('fully owned files', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
await assertMissing(fixture.v2Path);
|
||||
await assertMissing(fixture.v3Path);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
await t.test('legacy files', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const legacy = legacyDocuments(fixture);
|
||||
await writeJson(fixture.v2Path, legacy.v2);
|
||||
await writeJson(fixture.v3Path, legacy.v3);
|
||||
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
await assertMissing(fixture.v2Path);
|
||||
await assertMissing(fixture.v3Path);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
await t.test('unrelated v3 source bytes', async () => {
|
||||
const source = '{ "version": "v1", "hooks": [{ "name": "user", "trigger": "SessionStart", "action": { "type": "command", "command": "echo user" } }] }\n';
|
||||
const fixture = await createFixture({ existingV3: source });
|
||||
try {
|
||||
const result = await runPlugin(fixture, 'uninstall', fixture.agentId);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(await readFile(fixture.v3Path, 'utf-8'), source);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Kiro CLI command completes install, status, and uninstall end to end', async () => {
|
||||
const fixture = await createFixture();
|
||||
const privateKeyFile = path.join(fixture.projectDir, 'private-key.txt');
|
||||
const tokenFile = path.join(fixture.projectDir, 'token.txt');
|
||||
try {
|
||||
assert.equal(fixture.installResult.code, 1);
|
||||
assert.match(fixture.installResult.stderr, /parse Kiro CLI v3 hooks config/i);
|
||||
assert.equal(await readFile(fixture.v3Path, 'utf-8'), '{ malformed');
|
||||
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
|
||||
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
|
||||
const install = await runNode([
|
||||
cliPath, 'install', '--agent', 'kirocli', '--org_id', 'org-1',
|
||||
'--agent_id', fixture.agentId, '--kid', 'kid-1',
|
||||
'--private_key_file', privateKeyFile, '--token_file', tokenFile,
|
||||
'--base_url', fixture.baseUrl,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(install.code, 0, install.stderr);
|
||||
const status = await runNode([cliPath, 'status'], environment(fixture), fixture.projectDir);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.match(status.stdout, /Kiro CLI \(kirocli\) \[installed\]/);
|
||||
const uninstall = await runNode([
|
||||
cliPath, 'uninstall', '--agent', 'kirocli', '--agent_id', fixture.agentId,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(uninstall.code, 0, uninstall.stderr);
|
||||
await assertMissing(fixture.v2Path);
|
||||
await assertMissing(fixture.v3Path);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
|
||||
const v2Fixture = await createFixture({ existingV2: '{ malformed' });
|
||||
try {
|
||||
assert.equal(v2Fixture.installResult.code, 1);
|
||||
assert.match(v2Fixture.installResult.stderr, /parse Kiro CLI v2 agent config/i);
|
||||
assert.equal(await readFile(v2Fixture.v2Path, 'utf-8'), '{ malformed');
|
||||
} finally {
|
||||
await v2Fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, readFile, readdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
contractModuleUrl,
|
||||
createFixture,
|
||||
environment,
|
||||
installConfig,
|
||||
installationModuleUrl,
|
||||
ioModuleUrl,
|
||||
kiroV1ModuleUrl,
|
||||
managedV2Document,
|
||||
runNode,
|
||||
} from '../test-support/kirocli-test-helpers.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
async function runPreparedInstallation(fixture, { failV3 = false, raceV2 } = {}) {
|
||||
const source = `
|
||||
import fsp from 'node:fs/promises';
|
||||
import {
|
||||
buildKiroCliV2Hooks,
|
||||
buildKiroCliV3Hooks,
|
||||
renderKiroCliV2Installation,
|
||||
} from ${JSON.stringify(contractModuleUrl)};
|
||||
import {
|
||||
commitKiroCliInstallation,
|
||||
preflightKiroCliInstallation,
|
||||
prepareKiroCliInstallation,
|
||||
} from ${JSON.stringify(installationModuleUrl)};
|
||||
import { readKiroCliSources } from ${JSON.stringify(ioModuleUrl)};
|
||||
import { renderKiroIdeDocument } from ${JSON.stringify(kiroV1ModuleUrl)};
|
||||
|
||||
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
|
||||
const sources = await readKiroCliSources();
|
||||
const paths = await preflightKiroCliInstallation(config, sources);
|
||||
const v2 = renderKiroCliV2Installation(
|
||||
sources.v2,
|
||||
buildKiroCliV2Hooks(sources.v2, paths.guardPath, paths.auditPath),
|
||||
);
|
||||
const v3 = renderKiroIdeDocument(
|
||||
sources.v3,
|
||||
buildKiroCliV3Hooks(sources.v3.hooks, paths.guardPath, paths.auditPath),
|
||||
);
|
||||
const prepared = await prepareKiroCliInstallation(config, sources, v2, v3);
|
||||
if (process.env.ELYDORA_TEST_RACE_V2) {
|
||||
await fsp.writeFile(sources.paths.v2Path, process.env.ELYDORA_TEST_RACE_V2);
|
||||
}
|
||||
let failed = false;
|
||||
await commitKiroCliInstallation(prepared, async (from, to) => {
|
||||
if (!failed
|
||||
&& process.env.ELYDORA_TEST_FAIL_V3 === 'true'
|
||||
&& to === sources.paths.v3Path
|
||||
&& from.endsWith('.tmp')) {
|
||||
failed = true;
|
||||
throw new Error('injected Kiro CLI v3 rename failure');
|
||||
}
|
||||
await fsp.rename(from, to);
|
||||
});
|
||||
`;
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
|
||||
ELYDORA_TEST_FAIL_V3: String(failV3),
|
||||
ELYDORA_TEST_RACE_V2: raceV2,
|
||||
},
|
||||
fixture.projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertNoTransactionFiles(...directories) {
|
||||
const names = [];
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
names.push(...await readdir(directory));
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
|
||||
}
|
||||
|
||||
test('Kiro CLI rolls back v2, v3, and runtime files after a late commit failure', async () => {
|
||||
const originalV2 = managedV2Document({ owner: 'v2', hooks: {} });
|
||||
const originalV3 = { version: 'v1', owner: 'v3', hooks: [] };
|
||||
const fixture = await createFixture({ existingV2: originalV2, existingV3: originalV3 });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, { failV3: true });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /injected Kiro CLI v3 rename failure/);
|
||||
assert.deepEqual(JSON.parse(await readFile(fixture.v2Path, 'utf-8')), originalV2);
|
||||
assert.deepEqual(JSON.parse(await readFile(fixture.v3Path, 'utf-8')), originalV3);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(
|
||||
path.dirname(fixture.v2Path),
|
||||
path.dirname(fixture.v3Path),
|
||||
fixture.agentDir,
|
||||
);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro CLI detects concurrent source changes before the first commit', async () => {
|
||||
const originalV2 = managedV2Document({ hooks: {} });
|
||||
const originalV3 = { version: 'v1', hooks: [] };
|
||||
const raceV2 = '{"name":"elydora-audit","race":true,"hooks":{}}\n';
|
||||
const fixture = await createFixture({ existingV2: originalV2, existingV3: originalV3 });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, { raceV2 });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /changed during Kiro CLI installation/i);
|
||||
assert.equal(await readFile(fixture.v2Path, 'utf-8'), raceV2);
|
||||
assert.deepEqual(JSON.parse(await readFile(fixture.v3Path, 'utf-8')), originalV3);
|
||||
await assertMissing(fixture.guardScriptPath);
|
||||
await assertMissing(fixture.hookScriptPath);
|
||||
await assertNoTransactionFiles(path.dirname(fixture.v2Path), fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,412 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
VALID_PRIVATE_KEY,
|
||||
cliPath,
|
||||
createFixture,
|
||||
environment,
|
||||
findHook,
|
||||
installConfig,
|
||||
readJson,
|
||||
registryModuleUrl,
|
||||
runNode,
|
||||
runPlugin,
|
||||
runShell,
|
||||
startApiServer,
|
||||
} from '../test-support/kiroide-test-helpers.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
function userHook() {
|
||||
return {
|
||||
name: 'workspace-context',
|
||||
description: 'Load workspace context',
|
||||
trigger: 'SessionStart',
|
||||
action: { type: 'agent', prompt: 'Read AGENTS.md' },
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
|
||||
function legacyDocument(fixture, agentId = fixture.agentId) {
|
||||
const agentDirectory = path.join(fixture.homeDir, '.elydora', agentId);
|
||||
return {
|
||||
name: 'Elydora Audit',
|
||||
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
|
||||
version: '1.0.0',
|
||||
hooks: {
|
||||
pre_tool_use: {
|
||||
command: `node "${path.join(agentDirectory, 'guard.js')}"`,
|
||||
timeout_ms: 5000,
|
||||
},
|
||||
post_tool_use: {
|
||||
command: `node "${path.join(agentDirectory, 'hook.js')}"`,
|
||||
timeout_ms: 5000,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function writeJson(filePath, value) {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
test('Kiro IDE uses the workspace v1 hook contract', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('kiroide'), {
|
||||
name: 'Kiro IDE',
|
||||
configDir: '.kiro/hooks',
|
||||
configFile: 'elydora-audit.json',
|
||||
});
|
||||
|
||||
const fixture = await createFixture({
|
||||
existingConfig: { version: 'v1', owner: 'workspace', hooks: [userHook()] },
|
||||
});
|
||||
try {
|
||||
const first = await fixture.install();
|
||||
assert.equal(first.code, 0, first.stderr);
|
||||
assert.match(first.stdout, /workspace hooks/);
|
||||
assert.match(first.stdout, /Agent Hooks panel/);
|
||||
const firstSource = await readFile(fixture.configPath, 'utf-8');
|
||||
const document = JSON.parse(firstSource);
|
||||
assert.equal(document.version, 'v1');
|
||||
assert.equal(document.owner, 'workspace');
|
||||
assert.deepEqual(document.hooks[0], userHook());
|
||||
assert.deepEqual(findHook(document, 'elydora-guard'), {
|
||||
name: 'elydora-guard',
|
||||
description: 'Block tool use when the Elydora agent is frozen',
|
||||
trigger: 'PreToolUse',
|
||||
matcher: '.*',
|
||||
action: {
|
||||
type: 'command',
|
||||
command: findHook(document, 'elydora-guard').action.command,
|
||||
},
|
||||
timeout: 10,
|
||||
enabled: true,
|
||||
});
|
||||
assert.deepEqual(findHook(document, 'elydora-audit'), {
|
||||
name: 'elydora-audit',
|
||||
description: 'Record tool use in the Elydora audit trail',
|
||||
trigger: 'PostToolUse',
|
||||
matcher: '.*',
|
||||
action: {
|
||||
type: 'command',
|
||||
command: findHook(document, 'elydora-audit').action.command,
|
||||
},
|
||||
timeout: 10,
|
||||
enabled: true,
|
||||
});
|
||||
assert.deepEqual(await readJson(path.join(fixture.agentDir, 'config.json')), {
|
||||
org_id: 'org-1',
|
||||
agent_id: fixture.agentId,
|
||||
kid: 'kid-1',
|
||||
base_url: fixture.baseUrl,
|
||||
token: 'token-1',
|
||||
agent_name: 'kiroide',
|
||||
});
|
||||
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
|
||||
|
||||
const second = await fixture.install();
|
||||
assert.equal(second.code, 0, second.stderr);
|
||||
assert.equal(await readFile(fixture.configPath, 'utf-8'), firstSource);
|
||||
const transactionFiles = [
|
||||
...await readdir(fixture.hooksDir),
|
||||
...await readdir(fixture.agentDir),
|
||||
];
|
||||
assert.equal(transactionFiles.some((name) => /\.(tmp|rollback)$/.test(name)), false);
|
||||
await assertMissing(path.join(fixture.homeDir, '.kiro', 'hooks', 'elydora-audit.json'));
|
||||
if (process.platform !== 'win32') {
|
||||
assert.equal((await stat(fixture.agentDir)).mode & 0o777, 0o700);
|
||||
assert.equal((await stat(fixture.configPath)).mode & 0o777, 0o600);
|
||||
assert.equal((await stat(fixture.guardScriptPath)).mode & 0o777, 0o700);
|
||||
}
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro IDE commands block frozen agents and preserve the native event', async () => {
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const document = await readJson(fixture.configPath);
|
||||
const payload = {
|
||||
hook_event_name: 'preToolUse',
|
||||
cwd: fixture.projectDir,
|
||||
session_id: 'session-1',
|
||||
tool_name: 'execute_bash',
|
||||
tool_input: { command: 'npm test' },
|
||||
future_field: { retained: true },
|
||||
};
|
||||
await writeJson(path.join(fixture.agentDir, 'status-cache.json'), {
|
||||
status: 'frozen',
|
||||
cached_at: Date.now(),
|
||||
});
|
||||
const guard = await runShell(
|
||||
findHook(document, 'elydora-guard').action.command,
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
assert.equal(guard.code, 2, guard.stderr);
|
||||
assert.match(guard.stderr, /Tool execution blocked/);
|
||||
|
||||
await rm(path.join(fixture.agentDir, 'status-cache.json'));
|
||||
payload.hook_event_name = 'postToolUse';
|
||||
payload.tool_response = { success: true, result: '12 passed' };
|
||||
const audit = await runShell(
|
||||
findHook(document, 'elydora-audit').action.command,
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
JSON.stringify(payload),
|
||||
);
|
||||
assert.equal(audit.code, 0, audit.stderr);
|
||||
const request = api.requests.find(({ method }) => method === 'POST');
|
||||
assert.ok(request);
|
||||
const operation = JSON.parse(request.raw);
|
||||
assert.deepEqual(operation.payload, payload);
|
||||
assert.deepEqual(operation.action, { tool: 'execute_bash' });
|
||||
assert.deepEqual(operation.subject, { session_id: 'session-1' });
|
||||
} finally {
|
||||
await api.close();
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro IDE status requires the exact enabled contract and physical runtime', async (t) => {
|
||||
await t.test('disabled hook is inactive and reinstall repairs it', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const document = await readJson(fixture.configPath);
|
||||
findHook(document, 'elydora-guard').enabled = false;
|
||||
await writeJson(fixture.configPath, document);
|
||||
let status = await runPlugin(fixture, 'status', null);
|
||||
assert.deepEqual(JSON.parse(status.stdout), {
|
||||
installed: false,
|
||||
agentName: 'kiroide',
|
||||
displayName: 'Kiro IDE',
|
||||
hookConfigured: false,
|
||||
hookScriptExists: false,
|
||||
configPath: fixture.configPath,
|
||||
});
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).installed, true);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('recognized altered hook degrades status', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const document = await readJson(fixture.configPath);
|
||||
document.hooks.push({ ...findHook(document, 'elydora-guard'), enabled: false });
|
||||
await writeJson(fixture.configPath, document);
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const [label, target, contents, expected] of [
|
||||
['guard runtime', 'guard', 'tampered\n', /"installed":false/],
|
||||
['runtime config', 'config', '{ malformed', /parse Elydora runtime config/i],
|
||||
['private key', 'key', 'invalid', /private key is invalid/i],
|
||||
]) {
|
||||
await t.test(label, async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const targets = {
|
||||
guard: fixture.guardScriptPath,
|
||||
config: path.join(fixture.agentDir, 'config.json'),
|
||||
key: path.join(fixture.agentDir, 'private.key'),
|
||||
};
|
||||
await writeFile(targets[target], contents);
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.match(`${status.stdout}\n${status.stderr}`, expected);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro IDE rejects malformed files, name collisions, and unsafe workspaces before writes', async (t) => {
|
||||
await t.test('malformed hook file', async () => {
|
||||
const fixture = await createFixture({ existingConfig: '{ malformed' });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /parse Kiro IDE hooks/i);
|
||||
assert.equal(await readFile(fixture.configPath, 'utf-8'), '{ malformed');
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('managed name collision', async () => {
|
||||
const existing = {
|
||||
version: 'v1',
|
||||
hooks: [{
|
||||
name: 'elydora-guard',
|
||||
trigger: 'PreToolUse',
|
||||
action: { type: 'command', command: 'user-command' },
|
||||
}],
|
||||
};
|
||||
const fixture = await createFixture({ existingConfig: existing });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /conflicts with the Elydora contract/i);
|
||||
assert.deepEqual(await readJson(fixture.configPath), existing);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('non-canonical private key', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const result = await fixture.install({ privateKey: `${VALID_PRIVATE_KEY}=` });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /canonical 32-byte base64url/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
await assertMissing(fixture.configPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('linked .kiro directory', async () => {
|
||||
const fixture = await createFixture();
|
||||
const external = path.join(fixture.rootDir, 'external-kiro');
|
||||
const kiroDirectory = path.join(fixture.projectDir, '.kiro');
|
||||
try {
|
||||
await mkdir(external);
|
||||
await symlink(external, kiroDirectory, process.platform === 'win32' ? 'junction' : 'dir');
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /not a physical directory/i);
|
||||
await assertMissing(path.join(external, 'hooks', 'elydora-audit.json'));
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('Kiro IDE migrates only the matching legacy global hook', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
await writeJson(fixture.legacyConfigPath, legacyDocument(fixture));
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await assertMissing(fixture.legacyConfigPath);
|
||||
|
||||
await writeJson(fixture.legacyConfigPath, legacyDocument(fixture, 'agent-2'));
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal((await readJson(fixture.legacyConfigPath)).version, '1.0.0');
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro IDE uninstall removes exact ownership and preserves workspace hooks', async () => {
|
||||
const fixture = await createFixture({
|
||||
existingConfig: { version: 'v1', owner: 'workspace', hooks: [userHook()] },
|
||||
});
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-2')).code, 0);
|
||||
assert.equal((await readJson(fixture.configPath)).hooks.length, 3);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', fixture.agentId)).code, 0);
|
||||
assert.deepEqual(await readJson(fixture.configPath), {
|
||||
version: 'v1',
|
||||
owner: 'workspace',
|
||||
hooks: [userHook()],
|
||||
});
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
|
||||
const owned = await createFixture();
|
||||
try {
|
||||
assert.equal((await owned.install()).code, 0);
|
||||
assert.equal((await runPlugin(owned, 'uninstall', owned.agentId)).code, 0);
|
||||
await assertMissing(owned.configPath);
|
||||
} finally {
|
||||
await owned.close();
|
||||
}
|
||||
|
||||
const source = '{ "version": "v1", "hooks": [{ "name": "user", "trigger": "SessionStart", "action": { "type": "command", "command": "echo user" } }] }\n';
|
||||
const unrelated = await createFixture({ existingConfig: source });
|
||||
try {
|
||||
assert.equal((await runPlugin(unrelated, 'uninstall', unrelated.agentId)).code, 0);
|
||||
assert.equal(await readFile(unrelated.configPath, 'utf-8'), source);
|
||||
} finally {
|
||||
await unrelated.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro IDE CLI completes install, status, and uninstall in the current workspace', async () => {
|
||||
const fixture = await createFixture();
|
||||
const privateKeyFile = path.join(fixture.rootDir, 'private.key');
|
||||
const tokenFile = path.join(fixture.rootDir, 'token');
|
||||
try {
|
||||
await writeFile(privateKeyFile, VALID_PRIVATE_KEY, { mode: 0o600 });
|
||||
await writeFile(tokenFile, 'token-1', { mode: 0o600 });
|
||||
const install = await runNode([
|
||||
'--no-warnings',
|
||||
cliPath,
|
||||
'install',
|
||||
'--agent', 'kiroide',
|
||||
'--org_id', 'org-1',
|
||||
'--agent_id', fixture.agentId,
|
||||
'--kid', 'kid-1',
|
||||
'--private_key_file', privateKeyFile,
|
||||
'--token_file', tokenFile,
|
||||
'--base_url', fixture.baseUrl,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(install.code, 0, install.stderr);
|
||||
assert.match(install.stdout, /Kiro IDE workspace hooks/);
|
||||
const status = await runNode(
|
||||
['--no-warnings', cliPath, 'status'],
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.match(status.stdout, /Kiro IDE \(kiroide\) \[installed\]/);
|
||||
const uninstall = await runNode([
|
||||
'--no-warnings',
|
||||
cliPath,
|
||||
'uninstall',
|
||||
'--agent', 'kiroide',
|
||||
'--agent_id', fixture.agentId,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(uninstall.code, 0, uninstall.stderr);
|
||||
await assertMissing(fixture.configPath);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
contractModuleUrl,
|
||||
createFixture,
|
||||
environment,
|
||||
installConfig,
|
||||
installationModuleUrl,
|
||||
ioModuleUrl,
|
||||
runNode,
|
||||
} from '../test-support/kiroide-test-helpers.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
function legacyDocument(fixture) {
|
||||
return {
|
||||
name: 'Elydora Audit',
|
||||
description: 'Sends tool-use events to the Elydora tamper-evident audit platform',
|
||||
version: '1.0.0',
|
||||
hooks: {
|
||||
pre_tool_use: {
|
||||
command: `node "${fixture.guardScriptPath}"`,
|
||||
timeout_ms: 5000,
|
||||
},
|
||||
post_tool_use: {
|
||||
command: `node "${fixture.hookScriptPath}"`,
|
||||
timeout_ms: 5000,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function writeJson(filePath, value) {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
||||
}
|
||||
|
||||
async function runPreparedInstallation(fixture, { failLegacy = false, raceSource } = {}) {
|
||||
const source = `
|
||||
import fsp from 'node:fs/promises';
|
||||
import {
|
||||
buildKiroIdeHook,
|
||||
renderKiroIdeDocument,
|
||||
withoutManagedKiroIdeHooks,
|
||||
} from ${JSON.stringify(contractModuleUrl)};
|
||||
import { readKiroIdeSources } from ${JSON.stringify(ioModuleUrl)};
|
||||
import {
|
||||
commitKiroIdeInstallation,
|
||||
preflightKiroIdeInstallation,
|
||||
prepareKiroIdeInstallation,
|
||||
} from ${JSON.stringify(installationModuleUrl)};
|
||||
|
||||
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
|
||||
const sources = await readKiroIdeSources();
|
||||
const paths = await preflightKiroIdeInstallation(config, sources.paths);
|
||||
const rendered = renderKiroIdeDocument(sources.document, [
|
||||
...withoutManagedKiroIdeHooks(sources.document.hooks),
|
||||
buildKiroIdeHook('elydora-guard', paths.guardPath),
|
||||
buildKiroIdeHook('elydora-audit', paths.auditPath),
|
||||
]);
|
||||
const prepared = await prepareKiroIdeInstallation(config, sources, rendered);
|
||||
if (process.env.ELYDORA_TEST_RACE_SOURCE) {
|
||||
await fsp.writeFile(sources.paths.configPath, process.env.ELYDORA_TEST_RACE_SOURCE);
|
||||
}
|
||||
await commitKiroIdeInstallation(prepared, async (from, to) => {
|
||||
if (process.env.ELYDORA_TEST_FAIL_LEGACY === 'true'
|
||||
&& from === sources.paths.legacyConfigPath) {
|
||||
throw new Error('injected legacy rename failure');
|
||||
}
|
||||
await fsp.rename(from, to);
|
||||
});
|
||||
`;
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
|
||||
ELYDORA_TEST_FAIL_LEGACY: String(failLegacy),
|
||||
ELYDORA_TEST_RACE_SOURCE: raceSource,
|
||||
},
|
||||
fixture.projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertNoTransactionFiles(...directories) {
|
||||
const names = [];
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
names.push(...await readdir(directory));
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
|
||||
}
|
||||
|
||||
test('Kiro IDE rolls back runtime and workspace hooks when legacy cleanup fails', async () => {
|
||||
const original = { version: 'v1', owner: 'workspace', hooks: [] };
|
||||
const fixture = await createFixture({ existingConfig: original });
|
||||
try {
|
||||
await writeJson(fixture.legacyConfigPath, legacyDocument(fixture));
|
||||
const result = await runPreparedInstallation(fixture, { failLegacy: true });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /injected legacy rename failure/);
|
||||
assert.deepEqual(JSON.parse(await readFile(fixture.configPath, 'utf-8')), original);
|
||||
assert.equal((await readFile(fixture.legacyConfigPath, 'utf-8')).length > 0, true);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(
|
||||
fixture.hooksDir,
|
||||
fixture.agentDir,
|
||||
path.dirname(fixture.legacyConfigPath),
|
||||
);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Kiro IDE detects workspace hook races before the first commit', async () => {
|
||||
const original = { version: 'v1', hooks: [] };
|
||||
const raceSource = '{"version":"v1","hooks":[],"race":true}\n';
|
||||
const fixture = await createFixture({ existingConfig: original });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, { raceSource });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /changed during Kiro IDE installation/i);
|
||||
assert.equal(await readFile(fixture.configPath, 'utf-8'), raceSource);
|
||||
await assertMissing(fixture.guardScriptPath);
|
||||
await assertMissing(fixture.hookScriptPath);
|
||||
await assertNoTransactionFiles(fixture.hooksDir, fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
createFixture,
|
||||
environment,
|
||||
managedHandler,
|
||||
readSettings,
|
||||
runProcess,
|
||||
startApiServer,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
const sourceRoot = process.env.ELYDORA_LETTA_SOURCE;
|
||||
const bunBinary = process.env.ELYDORA_BUN_BINARY ?? 'bun';
|
||||
|
||||
function moduleUrl(relativePath) {
|
||||
return pathToFileURL(path.join(sourceRoot, relativePath)).href;
|
||||
}
|
||||
|
||||
function resultJson(stdout) {
|
||||
const line = stdout.trim().split(/\r?\n/).at(-1);
|
||||
return JSON.parse(line);
|
||||
}
|
||||
|
||||
async function runOfficialProbe(fixture, mode) {
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
const commands = {
|
||||
guard: managedHandler(settings, 'PreToolUse').command,
|
||||
audit: managedHandler(settings, 'PostToolUse').command,
|
||||
failure: managedHandler(settings, 'PostToolUseFailure').command,
|
||||
};
|
||||
const source = `
|
||||
import { settingsManager } from ${JSON.stringify(moduleUrl('src/settings-manager.ts'))};
|
||||
import { getHooksForEvent } from ${JSON.stringify(moduleUrl('src/hooks/loader.ts'))};
|
||||
import { executeCommandHook } from ${JSON.stringify(moduleUrl('src/hooks/executor.ts'))};
|
||||
import { runPostToolUseHooks } from ${JSON.stringify(moduleUrl('src/hooks/index.ts'))};
|
||||
const workspace = process.env.ELYDORA_OFFICIAL_WORKSPACE;
|
||||
const commands = JSON.parse(process.env.ELYDORA_OFFICIAL_COMMANDS);
|
||||
await settingsManager.initialize();
|
||||
await settingsManager.loadProjectSettings(workspace);
|
||||
await settingsManager.loadLocalProjectSettings(workspace);
|
||||
const pre = await getHooksForEvent('PreToolUse', 'Bash', workspace);
|
||||
const post = await getHooksForEvent('PostToolUse', 'Bash', workspace);
|
||||
const failure = await getHooksForEvent('PostToolUseFailure', 'Bash', workspace);
|
||||
const managed = (hooks, command) => hooks.find((hook) => (
|
||||
hook.type === 'command' && hook.command === command
|
||||
));
|
||||
if (process.env.ELYDORA_OFFICIAL_MODE === 'active') {
|
||||
const guardResult = await executeCommandHook(
|
||||
managed(pre, commands.guard),
|
||||
{
|
||||
event_type: 'PreToolUse',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo official' },
|
||||
tool_call_id: 'official-pre',
|
||||
agent_id: 'letta-agent',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
const postResult = await executeCommandHook(
|
||||
managed(post, commands.audit),
|
||||
{
|
||||
event_type: 'PostToolUse',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo official' },
|
||||
tool_result: { status: 'success', output: 'official' },
|
||||
tool_call_id: 'official-post',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
const failureResult = await executeCommandHook(
|
||||
managed(failure, commands.failure),
|
||||
{
|
||||
event_type: 'PostToolUseFailure',
|
||||
working_directory: workspace,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'exit 1' },
|
||||
error_message: 'official failure',
|
||||
error_type: 'ProcessError',
|
||||
tool_call_id: 'official-failure',
|
||||
},
|
||||
workspace,
|
||||
);
|
||||
console.log(JSON.stringify({
|
||||
counts: [pre.length, post.length, failure.length],
|
||||
exits: [guardResult.exitCode, postResult.exitCode, failureResult.exitCode],
|
||||
}));
|
||||
} else {
|
||||
const result = await runPostToolUseHooks(
|
||||
'Bash',
|
||||
{ command: 'echo unavailable' },
|
||||
{ status: 'success', output: 'unavailable' },
|
||||
'official-post-error',
|
||||
workspace,
|
||||
);
|
||||
console.log(JSON.stringify({
|
||||
blocked: result.blocked,
|
||||
errored: result.errored,
|
||||
exits: result.results.map((entry) => entry.exitCode),
|
||||
}));
|
||||
}
|
||||
await settingsManager.flush();
|
||||
`;
|
||||
return runProcess(
|
||||
bunBinary,
|
||||
['--silent', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
LETTA_SKIP_KEYCHAIN_CHECK: '1',
|
||||
ELYDORA_OFFICIAL_COMMANDS: JSON.stringify(commands),
|
||||
ELYDORA_OFFICIAL_MODE: mode,
|
||||
ELYDORA_OFFICIAL_WORKSPACE: fixture.projectDir,
|
||||
},
|
||||
sourceRoot,
|
||||
);
|
||||
}
|
||||
|
||||
test('official Letta Code 0.28.13 loads and executes all managed hooks', {
|
||||
skip: sourceRoot ? false : 'set ELYDORA_LETTA_SOURCE to the official Letta Code source tree',
|
||||
timeout: 60_000,
|
||||
}, async () => {
|
||||
const manifest = JSON.parse(await readFile(path.join(sourceRoot, 'package.json'), 'utf-8'));
|
||||
assert.equal(manifest.version, '0.28.13');
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
let apiClosed = false;
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const active = await runOfficialProbe(fixture, 'active');
|
||||
assert.equal(active.code, 0, active.stderr);
|
||||
assert.deepEqual(resultJson(active.stdout), {
|
||||
counts: [1, 1, 1],
|
||||
exits: [0, 0, 0],
|
||||
});
|
||||
assert.equal(api.requests.filter((request) => request.method === 'POST').length, 2);
|
||||
await api.close();
|
||||
apiClosed = true;
|
||||
const unavailable = await runOfficialProbe(fixture, 'unavailable');
|
||||
assert.equal(unavailable.code, 0, unavailable.stderr);
|
||||
assert.deepEqual(resultJson(unavailable.stdout), {
|
||||
blocked: false,
|
||||
errored: true,
|
||||
exits: [1],
|
||||
});
|
||||
} finally {
|
||||
await fixture.close();
|
||||
if (!apiClosed) await api.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,334 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
VALID_PRIVATE_KEY,
|
||||
cliPath,
|
||||
createFixture,
|
||||
environment,
|
||||
managedHandler,
|
||||
readSettings,
|
||||
registryModuleUrl,
|
||||
runNode,
|
||||
runPlugin,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
function assertManagedTriple(settings) {
|
||||
for (const event of ['PreToolUse', 'PostToolUse', 'PostToolUseFailure']) {
|
||||
const group = settings.hooks[event].find((entry) => entry.hooks[0]?.timeout === 10_000);
|
||||
assert.deepEqual(Object.keys(group).sort(), ['hooks', 'matcher']);
|
||||
assert.equal(group.matcher, '*');
|
||||
assert.equal(group.hooks.length, 1);
|
||||
assert.deepEqual(Object.keys(group.hooks[0]).sort(), ['command', 'timeout', 'type']);
|
||||
assert.equal(group.hooks[0].type, 'command');
|
||||
assert.equal(group.hooks[0].timeout, 10_000);
|
||||
assert.doesNotMatch(group.hooks[0].command, /^node /);
|
||||
}
|
||||
assert.equal(
|
||||
managedHandler(settings, 'PostToolUse').command,
|
||||
managedHandler(settings, 'PostToolUseFailure').command,
|
||||
);
|
||||
}
|
||||
|
||||
test('Letta Code is registered in the SDK and CLI', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('letta'), {
|
||||
name: 'Letta Code',
|
||||
configDir: '~/.letta',
|
||||
configFile: 'settings.json',
|
||||
});
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const result = await runNode(
|
||||
['--no-warnings', cliPath, 'status'],
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.match(result.stdout, /Letta Code \(letta\)/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta installs the current three-event contract and preserves every source', async () => {
|
||||
const globalSettings = [
|
||||
'{',
|
||||
' "theme": "dark",',
|
||||
' "hooks": {',
|
||||
' "FutureEvent": [null],',
|
||||
' "PreToolUse": [{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "user-hook", "quiet": true }] }]',
|
||||
' }',
|
||||
'}',
|
||||
'',
|
||||
].join('\r\n');
|
||||
const projectSettings = { hooks: { PostToolUse: [{
|
||||
matcher: 'Read',
|
||||
hooks: [{ type: 'prompt', prompt: 'Review $ARGUMENTS', model: 'fast' }],
|
||||
}] } };
|
||||
const localSettings = { windowTitle: 'workspace' };
|
||||
const fixture = await createFixture({ globalSettings, projectSettings, localSettings });
|
||||
try {
|
||||
const projectBefore = await readFile(fixture.projectPath, 'utf-8');
|
||||
const localBefore = await readFile(fixture.localPath, 'utf-8');
|
||||
const first = await fixture.install();
|
||||
assert.equal(first.code, 0, first.stderr);
|
||||
assert.match(first.stdout, /run \/hooks/i);
|
||||
assert.match(first.stdout, /restart active sessions/i);
|
||||
const raw = await readFile(fixture.globalPath, 'utf-8');
|
||||
assert.match(raw, /\r\n/);
|
||||
const settings = JSON.parse(raw);
|
||||
assert.equal(settings.theme, 'dark');
|
||||
assert.deepEqual(settings.hooks.FutureEvent, [null]);
|
||||
assert.equal(settings.hooks.PreToolUse[0].hooks[0].command, 'user-hook');
|
||||
assertManagedTriple(settings);
|
||||
assert.equal(await readFile(fixture.projectPath, 'utf-8'), projectBefore);
|
||||
assert.equal(await readFile(fixture.localPath, 'utf-8'), localBefore);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await lstat(filePath);
|
||||
const before = await readFile(fixture.globalPath, 'utf-8');
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal(await readFile(fixture.globalPath, 'utf-8'), before);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta validates known hook schemas and preserves future events', async () => {
|
||||
const fixture = await createFixture({ globalSettings: {
|
||||
hooks: {
|
||||
FutureEvent: [null],
|
||||
PreToolUse: [{ matcher: '[', hooks: [{
|
||||
type: 'command',
|
||||
command: 'user-command',
|
||||
timeout: 0,
|
||||
quiet: false,
|
||||
futureField: true,
|
||||
}] }],
|
||||
Stop: [{ hooks: [{
|
||||
type: 'prompt',
|
||||
prompt: 'Evaluate $ARGUMENTS',
|
||||
model: 'fast',
|
||||
timeout: 30,
|
||||
}] }],
|
||||
},
|
||||
} });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
assert.deepEqual(settings.hooks.FutureEvent, [null]);
|
||||
assert.equal(settings.hooks.PreToolUse[0].matcher, '[');
|
||||
assert.equal(settings.hooks.PreToolUse[0].hooks[0].futureField, true);
|
||||
assert.equal(settings.hooks.Stop[0].hooks[0].type, 'prompt');
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta rejects malformed affected settings before runtime writes', async (t) => {
|
||||
const cases = [
|
||||
['syntax', '{ malformed', /parse Letta Code global settings/i],
|
||||
['root', '[]', /must contain a JSON object/i],
|
||||
['comment', '{ // comment\n "hooks": {} }', /parse Letta Code global settings/i],
|
||||
['trailing comma', '{ "theme": true, }', /parse Letta Code global settings/i],
|
||||
['duplicate', '{ "hooks": {}, "hooks": {} }', /duplicate field "hooks"/i],
|
||||
['hooks shape', '{ "hooks": [] }', /field "hooks" must be an object/i],
|
||||
['disabled', '{ "hooks": { "disabled": "yes" } }', /hooks.disabled.*boolean/i],
|
||||
['event shape', '{ "hooks": { "PreToolUse": null } }', /must be an array/i],
|
||||
['group shape', '{ "hooks": { "PreToolUse": [null] } }', /group.*object/i],
|
||||
['tool matcher', '{ "hooks": { "PreToolUse": [{ "hooks": [] }] } }', /matcher must be a string/i],
|
||||
['simple matcher', '{ "hooks": { "Stop": [{ "matcher": "*", "hooks": [] }] } }', /matcher is unsupported/i],
|
||||
['hooks missing', '{ "hooks": { "PreToolUse": [{ "matcher": "*" }] } }', /hooks array/i],
|
||||
['handler shape', '{ "hooks": { "Stop": [{ "hooks": [null] }] } }', /hooks\[0\].*object/i],
|
||||
['handler type', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "http" }] }] } }', /unsupported type/i],
|
||||
['command', '{ "hooks": { "PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command" }] }] } }', /non-empty command/i],
|
||||
['prompt', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt" }] }] } }', /non-empty prompt/i],
|
||||
['model', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "prompt", "prompt": "x", "model": 1 }] }] } }', /model must be a string/i],
|
||||
['quiet', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "quiet": 1 }] }] } }', /quiet must be a boolean/i],
|
||||
['timeout', '{ "hooks": { "Stop": [{ "hooks": [{ "type": "command", "command": "x", "timeout": -1 }] }] } }', /non-negative finite number/i],
|
||||
];
|
||||
for (const [name, source, pattern] of cases) {
|
||||
await t.test(name, async () => {
|
||||
const fixture = await createFixture({ globalSettings: source });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, pattern);
|
||||
assert.equal(await readFile(fixture.globalPath, 'utf-8'), source);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta surfaces malformed project sources before writes', async (t) => {
|
||||
for (const field of ['projectSettings', 'localSettings']) {
|
||||
await t.test(field, async () => {
|
||||
const fixture = await createFixture({ [field]: '{ malformed' });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /parse Letta Code (project|project-local) settings/i);
|
||||
await assertMissing(fixture.globalPath);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta applies the official hooks.disabled precedence', async (t) => {
|
||||
const cases = [
|
||||
['global true', { globalSettings: { hooks: { disabled: true } } }, 1],
|
||||
['project true', { projectSettings: { hooks: { disabled: true } } }, 1],
|
||||
['local true', { localSettings: { hooks: { disabled: true } } }, 1],
|
||||
['global false override', {
|
||||
globalSettings: { hooks: { disabled: false } },
|
||||
projectSettings: { hooks: { disabled: true } },
|
||||
localSettings: { hooks: { disabled: true } },
|
||||
}, 0],
|
||||
];
|
||||
for (const [name, options, expectedCode] of cases) {
|
||||
await t.test(name, async () => {
|
||||
const fixture = await createFixture(options);
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, expectedCode, result.stderr);
|
||||
if (expectedCode === 1) {
|
||||
assert.match(result.stderr, /hooks.disabled/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
}
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta migrates exact legacy handlers and preserves ownership lookalikes', async () => {
|
||||
const fixture = await createFixture({ globalSettings: {} });
|
||||
const legacy = (scriptPath) => ({
|
||||
matcher: '*',
|
||||
hooks: [{ type: 'command', command: `node "${scriptPath}"` }],
|
||||
});
|
||||
const lookalike = legacy(fixture.guardScriptPath);
|
||||
lookalike.hooks[0].quiet = true;
|
||||
await writeFile(fixture.globalPath, `${JSON.stringify({ hooks: {
|
||||
PreToolUse: [legacy(fixture.guardScriptPath), lookalike],
|
||||
PostToolUse: [legacy(fixture.hookScriptPath)],
|
||||
} }, null, 2)}\n`, { mode: 0o600 });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
let settings = await readSettings(fixture.globalPath);
|
||||
assertManagedTriple(settings);
|
||||
assert(settings.hooks.PreToolUse.some((group) => group.hooks[0].quiet === true));
|
||||
const managed = settings.hooks.PreToolUse.find((group) => group.hooks[0].timeout === 10_000);
|
||||
managed.userField = 'preserve-group';
|
||||
managed.hooks.push({ type: 'command', command: 'user-command' });
|
||||
await writeFile(fixture.globalPath, `${JSON.stringify(settings, null, 2)}\n`);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
|
||||
settings = await readSettings(fixture.globalPath);
|
||||
assert(settings.hooks.PreToolUse.some((group) => group.hooks[0].quiet === true));
|
||||
assert(settings.hooks.PreToolUse.some((group) => (
|
||||
group.hooks.some((handler) => handler.command === 'user-command')
|
||||
)));
|
||||
assert.equal(settings.hooks.PostToolUse, undefined);
|
||||
assert.equal(settings.hooks.PostToolUseFailure, undefined);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta status requires exact hooks and strict runtime identity', async () => {
|
||||
const fixture = await createFixture({ projectSettings: {} });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
let status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, true);
|
||||
await writeFile(fixture.projectPath, '{"hooks":{"disabled":true}}\n');
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.hookConfigured, false);
|
||||
await writeFile(fixture.projectPath, '{}\n');
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, true);
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
settings.hooks.PostToolUseFailure.push(settings.hooks.PostToolUseFailure.at(-1));
|
||||
await writeFile(fixture.globalPath, `${JSON.stringify(settings, null, 2)}\n`);
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, false);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeFile(path.join(fixture.agentDir, 'private.key'), 'invalid');
|
||||
const invalid = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(invalid.code, 1);
|
||||
assert.match(invalid.stderr, /private key is invalid/i);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeFile(fixture.hookScriptPath, 'tampered');
|
||||
status = JSON.parse((await runPlugin(fixture, 'status', null)).stdout);
|
||||
assert.equal(status.installed, false);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta uninstall removes a settings file containing only managed hooks', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', 'agent-1')).code, 0);
|
||||
await assertMissing(fixture.globalPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta CLI completes install, status, and uninstall end to end', async () => {
|
||||
const fixture = await createFixture({ globalSettings: { theme: 'dark' } });
|
||||
const privateKeyFile = path.join(fixture.rootDir, 'install-private.key');
|
||||
const tokenFile = path.join(fixture.rootDir, 'install-token.txt');
|
||||
try {
|
||||
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
|
||||
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
|
||||
const install = await runNode([
|
||||
'--no-warnings', cliPath, 'install',
|
||||
'--agent', 'letta',
|
||||
'--org_id', 'org-1',
|
||||
'--agent_id', 'agent-1',
|
||||
'--kid', 'kid-1',
|
||||
'--private_key_file', privateKeyFile,
|
||||
'--token_file', tokenFile,
|
||||
'--base_url', fixture.baseUrl,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(install.code, 0, install.stderr);
|
||||
assertManagedTriple(await readSettings(fixture.globalPath));
|
||||
const status = await runNode(
|
||||
['--no-warnings', cliPath, 'status'],
|
||||
environment(fixture),
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.match(status.stdout, /Letta Code \(letta\) \[installed\]/);
|
||||
const uninstall = await runNode([
|
||||
'--no-warnings', cliPath, 'uninstall',
|
||||
'--agent', 'letta',
|
||||
'--agent_id', 'agent-1',
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(uninstall.code, 0, uninstall.stderr);
|
||||
assert.deepEqual(await readSettings(fixture.globalPath), { theme: 'dark' });
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,179 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile, stat } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createFixture,
|
||||
managedHandler,
|
||||
readSettings,
|
||||
runLettaHook,
|
||||
startApiServer,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
function officialInput(fixture, event) {
|
||||
const base = {
|
||||
event_type: event,
|
||||
working_directory: fixture.projectDir,
|
||||
session_id: 'session-1',
|
||||
};
|
||||
if (event === 'PreToolUse') {
|
||||
return {
|
||||
...base,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo test' },
|
||||
tool_call_id: 'call-1',
|
||||
agent_id: 'letta-agent-1',
|
||||
};
|
||||
}
|
||||
if (event === 'PostToolUse') {
|
||||
return {
|
||||
...base,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'echo test' },
|
||||
tool_call_id: 'call-1',
|
||||
tool_result: { status: 'success', output: 'test' },
|
||||
agent_id: 'letta-agent-1',
|
||||
preceding_reasoning: 'Run the command.',
|
||||
preceding_assistant_message: 'Executing.',
|
||||
};
|
||||
}
|
||||
return {
|
||||
...base,
|
||||
tool_name: 'Bash',
|
||||
tool_input: { command: 'exit 1' },
|
||||
tool_call_id: 'call-2',
|
||||
error_message: 'Command failed',
|
||||
error_type: 'ProcessError',
|
||||
agent_id: 'letta-agent-1',
|
||||
preceding_reasoning: 'Run the command.',
|
||||
preceding_assistant_message: 'Executing.',
|
||||
};
|
||||
}
|
||||
|
||||
async function installedHandlers(fixture) {
|
||||
const settings = await readSettings(fixture.globalPath);
|
||||
return {
|
||||
guard: managedHandler(settings, 'PreToolUse'),
|
||||
audit: managedHandler(settings, 'PostToolUse'),
|
||||
failure: managedHandler(settings, 'PostToolUseFailure'),
|
||||
};
|
||||
}
|
||||
|
||||
test('Letta guard accepts active agents with native exit semantics', async () => {
|
||||
const api = await startApiServer('active');
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { guard } = await installedHandlers(fixture);
|
||||
const result = await runLettaHook(
|
||||
guard,
|
||||
JSON.stringify(officialInput(fixture, 'PreToolUse')),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.equal(api.requests.filter((request) => request.method === 'GET').length, 1);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta guard propagates frozen and revoked states through exit code 2', async (t) => {
|
||||
for (const status of ['frozen', 'revoked']) {
|
||||
await t.test(status, async () => {
|
||||
const api = await startApiServer(status);
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { guard } = await installedHandlers(fixture);
|
||||
const result = await runLettaHook(
|
||||
guard,
|
||||
JSON.stringify(officialInput(fixture, 'PreToolUse')),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 2, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
assert.match(result.stderr, new RegExp(`is ${status}`, 'i'));
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta audit forwards complete success and failure payloads', async () => {
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { audit, failure } = await installedHandlers(fixture);
|
||||
for (const [handler, event] of [
|
||||
[audit, 'PostToolUse'],
|
||||
[failure, 'PostToolUseFailure'],
|
||||
]) {
|
||||
const payload = {
|
||||
...officialInput(fixture, event),
|
||||
future_provider_field: { preserved: true },
|
||||
};
|
||||
const result = await runLettaHook(handler, JSON.stringify(payload), fixture);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
assert.equal(result.stdout, '');
|
||||
}
|
||||
const posts = api.requests.filter((request) => request.method === 'POST');
|
||||
assert.equal(posts.length, 2);
|
||||
assert.deepEqual(JSON.parse(posts[0].raw).payload, {
|
||||
...officialInput(fixture, 'PostToolUse'),
|
||||
future_provider_field: { preserved: true },
|
||||
});
|
||||
assert.deepEqual(JSON.parse(posts[1].raw).payload, {
|
||||
...officialInput(fixture, 'PostToolUseFailure'),
|
||||
future_provider_field: { preserved: true },
|
||||
});
|
||||
} finally {
|
||||
await fixture.close();
|
||||
await api.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta audit failures emit stderr, persist error.log, and exit 1', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const { audit } = await installedHandlers(fixture);
|
||||
const result = await runLettaHook(
|
||||
audit,
|
||||
JSON.stringify(officialInput(fixture, 'PostToolUse')),
|
||||
fixture,
|
||||
);
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /Elydora audit|fetch failed|ECONNREFUSED/i);
|
||||
assert.match(
|
||||
await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'),
|
||||
/Elydora audit|fetch failed|ECONNREFUSED/i,
|
||||
);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta runtime artifacts use private modes on POSIX', {
|
||||
skip: process.platform === 'win32' ? 'POSIX mode bits are not authoritative on Windows' : false,
|
||||
}, async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
for (const [filePath, expected] of [
|
||||
[path.join(fixture.agentDir, 'config.json'), 0o600],
|
||||
[path.join(fixture.agentDir, 'private.key'), 0o600],
|
||||
[fixture.guardScriptPath, 0o700],
|
||||
[fixture.hookScriptPath, 0o700],
|
||||
[fixture.globalPath, 0o600],
|
||||
]) {
|
||||
assert.equal((await stat(filePath)).mode & 0o777, expected);
|
||||
}
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,157 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, mkdir, readFile, readdir, symlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
configModuleUrl,
|
||||
contractModuleUrl,
|
||||
createFixture,
|
||||
environment,
|
||||
installConfig,
|
||||
installationModuleUrl,
|
||||
runNode,
|
||||
sourcesModuleUrl,
|
||||
} from '../test-support/letta.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
async function assertNoTransactionFiles(...directories) {
|
||||
const names = [];
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
names.push(...await readdir(directory));
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
|
||||
}
|
||||
|
||||
async function runPreparedInstallation(fixture, {
|
||||
failSettings = false,
|
||||
racePath,
|
||||
raceSource,
|
||||
} = {}) {
|
||||
const source = `
|
||||
import fsp from 'node:fs/promises';
|
||||
import { buildLettaGroup } from ${JSON.stringify(contractModuleUrl)};
|
||||
import { renderLettaDocument } from ${JSON.stringify(configModuleUrl)};
|
||||
import { readLettaSources } from ${JSON.stringify(sourcesModuleUrl)};
|
||||
import {
|
||||
commitLettaInstallation,
|
||||
preflightLettaInstallation,
|
||||
prepareLettaInstallation,
|
||||
} from ${JSON.stringify(installationModuleUrl)};
|
||||
|
||||
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
|
||||
const sources = await readLettaSources();
|
||||
const paths = await preflightLettaInstallation(config, sources);
|
||||
const rendered = renderLettaDocument(sources.global, undefined, new Map([
|
||||
['PreToolUse', buildLettaGroup(paths.guardPath)],
|
||||
['PostToolUse', buildLettaGroup(paths.auditPath)],
|
||||
['PostToolUseFailure', buildLettaGroup(paths.auditPath)],
|
||||
]));
|
||||
const prepared = await prepareLettaInstallation(config, sources, rendered);
|
||||
if (process.env.ELYDORA_TEST_RACE_PATH) {
|
||||
await fsp.writeFile(
|
||||
process.env.ELYDORA_TEST_RACE_PATH,
|
||||
process.env.ELYDORA_TEST_RACE_SOURCE,
|
||||
);
|
||||
}
|
||||
await commitLettaInstallation(prepared, async (from, to) => {
|
||||
if (process.env.ELYDORA_TEST_FAIL_SETTINGS === 'true'
|
||||
&& to === sources.global.filePath) {
|
||||
throw new Error('injected Letta settings rename failure');
|
||||
}
|
||||
await fsp.rename(from, to);
|
||||
});
|
||||
`;
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
|
||||
ELYDORA_TEST_FAIL_SETTINGS: String(failSettings),
|
||||
ELYDORA_TEST_RACE_PATH: racePath,
|
||||
ELYDORA_TEST_RACE_SOURCE: raceSource,
|
||||
},
|
||||
fixture.projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
test('Letta rolls back every runtime file when the settings commit fails', async () => {
|
||||
const original = '{"theme":"dark"}\n';
|
||||
const fixture = await createFixture({ globalSettings: original });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, { failSettings: true });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /injected Letta settings rename failure/i);
|
||||
assert.equal(await readFile(fixture.globalPath, 'utf-8'), original);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(path.dirname(fixture.globalPath), fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta aborts when a read-only project source changes after prepare', async () => {
|
||||
const original = '{"windowTitle":"before"}\n';
|
||||
const raceSource = '{"windowTitle":"after"}\n';
|
||||
const fixture = await createFixture({ projectSettings: original });
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, {
|
||||
racePath: fixture.projectPath,
|
||||
raceSource,
|
||||
});
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /project settings changed during Letta Code installation/i);
|
||||
assert.equal(await readFile(fixture.projectPath, 'utf-8'), raceSource);
|
||||
await assertMissing(fixture.globalPath);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(path.dirname(fixture.globalPath), fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta rejects linked configuration directories before writes', {
|
||||
skip: process.platform === 'win32' ? 'directory symlinks require platform privileges on Windows' : false,
|
||||
}, async () => {
|
||||
const fixture = await createFixture();
|
||||
const target = path.join(fixture.rootDir, 'redirected-letta');
|
||||
await mkdir(target);
|
||||
await symlink(target, path.dirname(fixture.globalPath), 'dir');
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /global configuration directory is not a physical directory/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('Letta rejects a settings directory at the settings file path', async () => {
|
||||
const fixture = await createFixture();
|
||||
await mkdir(fixture.globalPath, { recursive: true });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /global settings path is not a physical file/i);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
createFixture,
|
||||
environment,
|
||||
runProcess,
|
||||
} from '../test-support/opencode-test-helpers.mjs';
|
||||
|
||||
const binary = process.env.ELYDORA_OPENCODE_BINARY;
|
||||
|
||||
test(
|
||||
'official OpenCode 1.18 discovers the generated global JavaScript plugin',
|
||||
{ skip: binary ? false : 'set ELYDORA_OPENCODE_BINARY to the official OpenCode executable' },
|
||||
async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const env = {
|
||||
...environment(fixture),
|
||||
OPENCODE_DISABLE_AUTOUPDATE: 'true',
|
||||
OPENCODE_DISABLE_MODELS_FETCH: 'true',
|
||||
};
|
||||
const version = await runProcess(binary, ['--version'], env, fixture.projectDir);
|
||||
assert.equal(version.code, 0, version.stderr);
|
||||
assert.match(version.stdout, /^1\.18\./);
|
||||
const paths = await runProcess(binary, ['debug', 'paths'], env, fixture.projectDir);
|
||||
assert.equal(paths.code, 0, paths.stderr);
|
||||
assert.equal(paths.stdout.includes(path.join(fixture.configRoot, 'opencode')), true);
|
||||
const info = await runProcess(binary, ['debug', 'info'], env, fixture.projectDir);
|
||||
assert.equal(info.code, 0, info.stderr);
|
||||
assert.match(info.stdout, /elydora-audit\.js/);
|
||||
assert.doesNotMatch(info.stdout, /elydora-audit\.mjs/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -1,165 +1,454 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import {
|
||||
lstat,
|
||||
mkdir,
|
||||
readFile,
|
||||
readdir,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { setTimeout as delay } from 'node:timers/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import test from 'node:test';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
VALID_PRIVATE_KEY,
|
||||
cliPath,
|
||||
contractModuleUrl,
|
||||
createFixture,
|
||||
environment,
|
||||
registryModuleUrl,
|
||||
runNode,
|
||||
runPlugin,
|
||||
startApiServer,
|
||||
} from '../test-support/opencode-test-helpers.mjs';
|
||||
|
||||
function runNode(args, env) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, args, {
|
||||
env: { ...process.env, ...env },
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
||||
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
||||
child.once('error', reject);
|
||||
child.once('close', (code) => resolve({ code, stdout, stderr }));
|
||||
});
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
async function createFixture({ guardSource, hookSource }) {
|
||||
const homeDir = await mkdtemp(path.join(os.tmpdir(), 'elydora-opencode-'));
|
||||
const agentDir = path.join(homeDir, '.elydora', 'agent-1');
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
const guardScriptPath = path.join(agentDir, 'guard.cjs');
|
||||
const hookScriptPath = path.join(agentDir, 'hook.cjs');
|
||||
if (guardSource !== undefined) await writeFile(guardScriptPath, guardSource);
|
||||
await writeFile(hookScriptPath, hookSource ?? 'process.exit(0);\n');
|
||||
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode.js')).href;
|
||||
const installScript = `
|
||||
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)};
|
||||
await opencodePlugin.install(JSON.parse(process.env.ELYDORA_TEST_CONFIG));
|
||||
async function writeSource(filePath, source) {
|
||||
await mkdir(path.dirname(filePath), { recursive: true });
|
||||
await writeFile(filePath, source, { mode: 0o600 });
|
||||
}
|
||||
|
||||
async function legacySource(fixture) {
|
||||
const { buildLegacyOpenCodePlugin } = await import(contractModuleUrl);
|
||||
return buildLegacyOpenCodePlugin(fixture.hookScriptPath, fixture.guardScriptPath);
|
||||
}
|
||||
|
||||
async function runGeneratedHook(fixture, name, input, output) {
|
||||
const source = `
|
||||
const module = await import(${JSON.stringify(pathToFileURL(fixture.pluginPath).href)});
|
||||
const hooks = await module.ElydoraAuditPlugin({});
|
||||
await hooks[${JSON.stringify(name)}](
|
||||
JSON.parse(process.env.ELYDORA_TEST_INPUT),
|
||||
JSON.parse(process.env.ELYDORA_TEST_OUTPUT),
|
||||
);
|
||||
`;
|
||||
const installResult = await runNode(
|
||||
['--input-type=module', '--eval', installScript],
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{
|
||||
HOME: homeDir,
|
||||
USERPROFILE: homeDir,
|
||||
ELYDORA_TEST_CONFIG: JSON.stringify({ guardScriptPath, hookScriptPath }),
|
||||
...environment(fixture),
|
||||
ELYDORA_TEST_INPUT: JSON.stringify(input),
|
||||
ELYDORA_TEST_OUTPUT: JSON.stringify(output),
|
||||
},
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(installResult.code, 0, installResult.stderr);
|
||||
const generatedPath = path.join(
|
||||
homeDir,
|
||||
'.config',
|
||||
'opencode',
|
||||
'plugins',
|
||||
'elydora-audit.mjs',
|
||||
);
|
||||
const generatedPlugin = await import(`${pathToFileURL(generatedPath).href}?test=${Date.now()}`);
|
||||
const hooks = await generatedPlugin.ElydoraAuditPlugin({ project: { name: 'project-fallback' } });
|
||||
return {
|
||||
guardScriptPath,
|
||||
homeDir,
|
||||
hooks,
|
||||
async close() {
|
||||
await rm(homeDir, { recursive: true, force: true });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function readPluginStatus(homeDir) {
|
||||
const pluginModuleUrl = pathToFileURL(path.resolve('dist/plugins/opencode.js')).href;
|
||||
const statusScript = `
|
||||
import { opencodePlugin } from ${JSON.stringify(pluginModuleUrl)};
|
||||
console.log(JSON.stringify(await opencodePlugin.status()));
|
||||
`;
|
||||
const result = await runNode(
|
||||
['--input-type=module', '--eval', statusScript],
|
||||
{ HOME: homeDir, USERPROFILE: homeDir },
|
||||
);
|
||||
assert.equal(result.code, 0, result.stderr);
|
||||
return JSON.parse(result.stdout);
|
||||
}
|
||||
|
||||
test('OpenCode rejects tool execution when the Elydora guard blocks', async () => {
|
||||
const fixture = await createFixture({
|
||||
guardSource: "process.stderr.write('Agent is frozen by Elydora.'); process.exit(2);\n",
|
||||
test('OpenCode installs the discoverable global JavaScript plugin and exact runtime', async () => {
|
||||
const { SUPPORTED_AGENTS } = await import(registryModuleUrl);
|
||||
assert.deepEqual(SUPPORTED_AGENTS.get('opencode'), {
|
||||
name: 'OpenCode',
|
||||
configDir: '.config/opencode/plugins',
|
||||
configFile: 'elydora-audit.js',
|
||||
});
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
await assert.rejects(
|
||||
fixture.hooks['tool.execute.before'](
|
||||
{ tool: 'bash', sessionID: 'session-1', callID: 'call-1' },
|
||||
{ args: { command: 'echo test' } },
|
||||
),
|
||||
/Agent is frozen by Elydora/,
|
||||
);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenCode blocks when the guard process cannot start', async () => {
|
||||
const fixture = await createFixture({});
|
||||
try {
|
||||
await assert.rejects(
|
||||
fixture.hooks['tool.execute.before'](
|
||||
{ tool: 'bash', sessionID: 'session-1', callID: 'call-1' },
|
||||
{ args: { command: 'echo test' } },
|
||||
),
|
||||
/Elydora guard failed/,
|
||||
);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenCode forwards the current tool event contract to the audit hook', async () => {
|
||||
const capturePath = path.join(os.tmpdir(), `elydora-event-${process.pid}-${Date.now()}.json`);
|
||||
const hookSource = `
|
||||
const fs = require('node:fs');
|
||||
const chunks = [];
|
||||
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
||||
process.stdin.on('end', () => fs.writeFileSync(${JSON.stringify(capturePath)}, Buffer.concat(chunks)));
|
||||
`;
|
||||
const fixture = await createFixture({
|
||||
guardSource: 'process.exit(0);\n',
|
||||
hookSource,
|
||||
});
|
||||
try {
|
||||
await fixture.hooks['tool.execute.after'](
|
||||
const first = await fixture.install();
|
||||
assert.equal(first.code, 0, first.stderr);
|
||||
assert.match(first.stdout, /elydora-audit\.js/);
|
||||
assert.match(first.stdout, /restart active OpenCode sessions/);
|
||||
await assertMissing(fixture.legacyPluginPath);
|
||||
const firstSource = await readFile(fixture.pluginPath, 'utf-8');
|
||||
assert.match(firstSource, /@elydora-opencode-plugin/);
|
||||
assert.match(firstSource, /tool\.execute\.before/);
|
||||
assert.match(firstSource, /tool\.execute\.after/);
|
||||
assert.doesNotMatch(firstSource, /spawn\('node'/);
|
||||
const hooks = await fixture.loadPlugin();
|
||||
assert.deepEqual(Object.keys(hooks).sort(), ['tool.execute.after', 'tool.execute.before']);
|
||||
assert.deepEqual(
|
||||
JSON.parse(await readFile(path.join(fixture.agentDir, 'config.json'), 'utf-8')),
|
||||
{
|
||||
tool: 'bash',
|
||||
sessionID: 'session-1',
|
||||
callID: 'call-1',
|
||||
args: { command: 'echo test' },
|
||||
org_id: 'org-1',
|
||||
agent_id: fixture.agentId,
|
||||
kid: 'kid-1',
|
||||
base_url: fixture.baseUrl,
|
||||
token: 'token-1',
|
||||
agent_name: 'opencode',
|
||||
},
|
||||
{ title: 'Shell', output: 'test' },
|
||||
);
|
||||
let payload;
|
||||
const deadline = Date.now() + 3000;
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
payload = await readFile(capturePath, 'utf-8');
|
||||
break;
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
await delay(20);
|
||||
}
|
||||
assert.equal(await readFile(path.join(fixture.agentDir, 'private.key'), 'utf-8'), VALID_PRIVATE_KEY);
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.deepEqual(JSON.parse(status.stdout), {
|
||||
installed: true,
|
||||
agentName: 'opencode',
|
||||
displayName: 'OpenCode',
|
||||
hookConfigured: true,
|
||||
hookScriptExists: true,
|
||||
configPath: fixture.pluginPath,
|
||||
});
|
||||
const second = await fixture.install();
|
||||
assert.equal(second.code, 0, second.stderr);
|
||||
assert.equal(await readFile(fixture.pluginPath, 'utf-8'), firstSource);
|
||||
const names = [
|
||||
...await readdir(fixture.pluginsDir),
|
||||
...await readdir(fixture.agentDir),
|
||||
];
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false);
|
||||
if (process.platform !== 'win32') {
|
||||
assert.equal((await stat(fixture.pluginPath)).mode & 0o777, 0o600);
|
||||
assert.equal((await stat(fixture.guardScriptPath)).mode & 0o777, 0o700);
|
||||
}
|
||||
assert(payload, 'audit hook did not receive the tool event');
|
||||
assert.deepEqual(JSON.parse(payload), {
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenCode guard enforces state and audit preserves the complete native event', async () => {
|
||||
const api = await startApiServer();
|
||||
const fixture = await createFixture({ baseUrl: api.baseUrl });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const beforeInput = { tool: 'bash', sessionID: 'session-1', callID: 'call-1' };
|
||||
const beforeOutput = { args: { command: 'echo test' } };
|
||||
const guard = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.before',
|
||||
beforeInput,
|
||||
beforeOutput,
|
||||
);
|
||||
assert.equal(guard.code, 0, guard.stderr);
|
||||
assert.equal(api.requests.some(({ method }) => method === 'GET'), true);
|
||||
|
||||
const afterInput = { ...beforeInput, args: beforeOutput.args };
|
||||
const afterOutput = { title: 'Shell', output: 'test', metadata: { exit: 0 } };
|
||||
const audit = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.after',
|
||||
afterInput,
|
||||
afterOutput,
|
||||
);
|
||||
assert.equal(audit.code, 0, audit.stderr);
|
||||
const request = api.requests.find(({ method }) => method === 'POST');
|
||||
assert.ok(request);
|
||||
const operation = JSON.parse(request.raw);
|
||||
assert.deepEqual(operation.action, { tool: 'bash' });
|
||||
assert.deepEqual(operation.subject, { session_id: 'session-1' });
|
||||
assert.deepEqual(operation.payload, {
|
||||
hook_event_name: 'tool.execute.after',
|
||||
tool_name: 'bash',
|
||||
tool_input: { command: 'echo test' },
|
||||
session_id: 'session-1',
|
||||
call_id: 'call-1',
|
||||
input: afterInput,
|
||||
output: afterOutput,
|
||||
});
|
||||
|
||||
api.setOperationStatus(503);
|
||||
const failedAudit = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.after',
|
||||
afterInput,
|
||||
afterOutput,
|
||||
);
|
||||
assert.equal(failedAudit.code, 0, failedAudit.stderr);
|
||||
assert.match(failedAudit.stderr, /OpenCode audit runtime failed: \[Elydora audit\]/);
|
||||
assert.match(
|
||||
await readFile(path.join(fixture.agentDir, 'error.log'), 'utf-8'),
|
||||
/Audit API returned HTTP 503/,
|
||||
);
|
||||
|
||||
await rm(path.join(fixture.agentDir, 'status-cache.json'));
|
||||
api.setStatus('frozen');
|
||||
const frozen = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.before',
|
||||
beforeInput,
|
||||
beforeOutput,
|
||||
);
|
||||
assert.equal(frozen.code, 1);
|
||||
assert.match(frozen.stderr, /Agent "opencode" is frozen.*Tool execution blocked/s);
|
||||
} finally {
|
||||
await api.close();
|
||||
await fixture.close();
|
||||
await rm(capturePath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenCode status requires both generated runtime scripts', async () => {
|
||||
const fixture = await createFixture({ guardSource: 'process.exit(0);\n' });
|
||||
test('OpenCode bridge forwards both native arguments and surfaces runtime failures', async () => {
|
||||
const fixture = await createFixture();
|
||||
const guardCapture = path.join(fixture.projectDir, 'guard-input.json');
|
||||
const auditCapture = path.join(fixture.projectDir, 'audit-input.json');
|
||||
try {
|
||||
assert.equal((await readPluginStatus(fixture.homeDir)).installed, true);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeFile(fixture.guardScriptPath, `
|
||||
const fs = require('node:fs');
|
||||
const chunks = [];
|
||||
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
||||
process.stdin.on('end', () => {
|
||||
fs.writeFileSync(${JSON.stringify(guardCapture)}, Buffer.concat(chunks));
|
||||
process.stderr.write('[guard warning] degraded status API\\n');
|
||||
});
|
||||
`);
|
||||
const beforeInput = { tool: 'write', sessionID: 'session-2', callID: 'call-2' };
|
||||
const beforeOutput = { args: { filePath: 'a.txt', content: 'value' } };
|
||||
const guard = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.before',
|
||||
beforeInput,
|
||||
beforeOutput,
|
||||
);
|
||||
assert.equal(guard.code, 0, guard.stderr);
|
||||
assert.match(guard.stderr, /guard warning/);
|
||||
assert.deepEqual(JSON.parse(await readFile(guardCapture, 'utf-8')), {
|
||||
hook_event_name: 'tool.execute.before',
|
||||
tool_name: 'write',
|
||||
tool_input: beforeOutput.args,
|
||||
session_id: 'session-2',
|
||||
call_id: 'call-2',
|
||||
input: beforeInput,
|
||||
output: beforeOutput,
|
||||
});
|
||||
|
||||
await writeFile(fixture.hookScriptPath, `
|
||||
const fs = require('node:fs');
|
||||
const chunks = [];
|
||||
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
||||
process.stdin.on('end', () => {
|
||||
fs.writeFileSync(${JSON.stringify(auditCapture)}, Buffer.concat(chunks));
|
||||
process.stderr.write('audit transport failed\\n');
|
||||
process.exitCode = 3;
|
||||
});
|
||||
`);
|
||||
const afterInput = { ...beforeInput, args: beforeOutput.args };
|
||||
const afterOutput = { title: 'Write', output: 'saved', metadata: { bytes: 5 } };
|
||||
const audit = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.after',
|
||||
afterInput,
|
||||
afterOutput,
|
||||
);
|
||||
assert.equal(audit.code, 0, audit.stderr);
|
||||
assert.match(audit.stderr, /OpenCode audit runtime failed: audit transport failed/);
|
||||
assert.deepEqual(JSON.parse(await readFile(auditCapture, 'utf-8')).input, afterInput);
|
||||
|
||||
await rm(fixture.guardScriptPath);
|
||||
assert.equal((await readPluginStatus(fixture.homeDir)).installed, false);
|
||||
const missing = await runGeneratedHook(
|
||||
fixture,
|
||||
'tool.execute.before',
|
||||
beforeInput,
|
||||
beforeOutput,
|
||||
);
|
||||
assert.equal(missing.code, 1);
|
||||
assert.match(missing.stderr, /Elydora guard failed/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenCode status requires an exact plugin, runtime identity, and clean migration state', async (t) => {
|
||||
await t.test('tampered managed plugin', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeFile(fixture.pluginPath, `${await readFile(fixture.pluginPath, 'utf-8')}\n// tampered\n`);
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(status.code, 1);
|
||||
assert.match(status.stderr, /does not match the managed template/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
for (const [label, target, contents, expected] of [
|
||||
['guard runtime', 'guard', 'tampered\n', /"installed":false/],
|
||||
['runtime config', 'config', '{ malformed', /parse Elydora runtime config/i],
|
||||
['runtime identity', 'config', JSON.stringify({
|
||||
org_id: 'org-1',
|
||||
agent_id: 'different-agent',
|
||||
kid: 'kid-1',
|
||||
base_url: 'http://127.0.0.1:9',
|
||||
token: 'token-1',
|
||||
agent_name: 'opencode',
|
||||
}), /runtime identity/i],
|
||||
['private key', 'key', 'invalid', /private key is invalid/i],
|
||||
]) {
|
||||
await t.test(label, async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
const targets = {
|
||||
guard: fixture.guardScriptPath,
|
||||
config: path.join(fixture.agentDir, 'config.json'),
|
||||
key: path.join(fixture.agentDir, 'private.key'),
|
||||
};
|
||||
await writeFile(targets[target], contents);
|
||||
const status = await runPlugin(fixture, 'status', null);
|
||||
assert.match(`${status.stdout}\n${status.stderr}`, expected);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await t.test('stale owned mjs source', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await writeSource(fixture.legacyPluginPath, await legacySource(fixture));
|
||||
let status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).hookConfigured, false);
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await assertMissing(fixture.legacyPluginPath);
|
||||
status = await runPlugin(fixture, 'status', null);
|
||||
assert.equal(JSON.parse(status.stdout).installed, true);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('OpenCode rejects collisions, invalid inputs, and linked plugin paths before writes', async (t) => {
|
||||
await t.test('user plugin collision', async () => {
|
||||
const source = 'export const UserPlugin = async () => ({});\n';
|
||||
const fixture = await createFixture({ existingPlugin: source });
|
||||
try {
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /owned by another integration/);
|
||||
assert.equal(await readFile(fixture.pluginPath, 'utf-8'), source);
|
||||
await assertMissing(fixture.guardScriptPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('non-canonical private key', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const result = await fixture.install({ privateKey: 'invalid' });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /canonical 32-byte/);
|
||||
await assertMissing(fixture.pluginPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('relative XDG_CONFIG_HOME', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
const source = `
|
||||
import { opencodePlugin } from ${JSON.stringify(
|
||||
pathToFileURL(path.resolve('dist/plugins/opencode.js')).href,
|
||||
)};
|
||||
await opencodePlugin.status();
|
||||
`;
|
||||
const result = await runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{ HOME: fixture.homeDir, USERPROFILE: fixture.homeDir, XDG_CONFIG_HOME: 'relative' },
|
||||
fixture.projectDir,
|
||||
);
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /XDG_CONFIG_HOME must be an absolute path/);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('linked plugins directory', async () => {
|
||||
const fixture = await createFixture();
|
||||
const external = path.join(fixture.rootDir, 'external-plugins');
|
||||
try {
|
||||
await mkdir(external);
|
||||
await mkdir(path.dirname(fixture.pluginsDir), { recursive: true });
|
||||
await symlink(external, fixture.pluginsDir, process.platform === 'win32' ? 'junction' : 'dir');
|
||||
const result = await fixture.install();
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /plugins directory is not a physical directory/i);
|
||||
await assertMissing(fixture.guardScriptPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('OpenCode migrates exact legacy ownership and preserves unrelated mjs bytes', async (t) => {
|
||||
await t.test('install and uninstall exact ownership', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
await writeSource(fixture.legacyPluginPath, await legacySource(fixture));
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
await assertMissing(fixture.legacyPluginPath);
|
||||
const uninstall = await runPlugin(fixture, 'uninstall', fixture.agentId);
|
||||
assert.equal(uninstall.code, 0, uninstall.stderr);
|
||||
await assertMissing(fixture.pluginPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('unrelated legacy file', async () => {
|
||||
const source = 'export const UserPlugin = async () => ({});\n';
|
||||
const fixture = await createFixture({ existingLegacy: source });
|
||||
try {
|
||||
assert.equal((await fixture.install()).code, 0);
|
||||
assert.equal(await readFile(fixture.legacyPluginPath, 'utf-8'), source);
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', fixture.agentId)).code, 0);
|
||||
assert.equal(await readFile(fixture.legacyPluginPath, 'utf-8'), source);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('legacy-only uninstall', async () => {
|
||||
const fixture = await createFixture();
|
||||
try {
|
||||
await writeSource(fixture.legacyPluginPath, await legacySource(fixture));
|
||||
assert.equal((await runPlugin(fixture, 'uninstall', fixture.agentId)).code, 0);
|
||||
await assertMissing(fixture.legacyPluginPath);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('OpenCode CLI completes install, status, and uninstall end to end', async () => {
|
||||
const fixture = await createFixture();
|
||||
const privateKeyFile = path.join(fixture.projectDir, 'private-key.txt');
|
||||
const tokenFile = path.join(fixture.projectDir, 'token.txt');
|
||||
try {
|
||||
await writeFile(privateKeyFile, `${VALID_PRIVATE_KEY}\n`, { mode: 0o600 });
|
||||
await writeFile(tokenFile, 'token-1\n', { mode: 0o600 });
|
||||
const install = await runNode([
|
||||
cliPath, 'install', '--agent', 'opencode', '--org_id', 'org-1',
|
||||
'--agent_id', fixture.agentId, '--kid', 'kid-1',
|
||||
'--private_key_file', privateKeyFile, '--token_file', tokenFile,
|
||||
'--base_url', fixture.baseUrl,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(install.code, 0, install.stderr);
|
||||
const status = await runNode([cliPath, 'status'], environment(fixture), fixture.projectDir);
|
||||
assert.equal(status.code, 0, status.stderr);
|
||||
assert.match(status.stdout, /OpenCode \(opencode\) \[installed\]/);
|
||||
const uninstall = await runNode([
|
||||
cliPath, 'uninstall', '--agent', 'opencode', '--agent_id', fixture.agentId,
|
||||
], environment(fixture), fixture.projectDir);
|
||||
assert.equal(uninstall.code, 0, uninstall.stderr);
|
||||
await assertMissing(fixture.pluginPath);
|
||||
await assertMissing(fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import {
|
||||
contractModuleUrl,
|
||||
createFixture,
|
||||
environment,
|
||||
installConfig,
|
||||
installationModuleUrl,
|
||||
ioModuleUrl,
|
||||
runNode,
|
||||
} from '../test-support/opencode-test-helpers.mjs';
|
||||
|
||||
async function assertMissing(filePath) {
|
||||
await assert.rejects(lstat(filePath), { code: 'ENOENT' });
|
||||
}
|
||||
|
||||
async function runPreparedInstallation(fixture, { failLegacy = false, racePlugin } = {}) {
|
||||
const source = `
|
||||
import fsp from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
commitOpenCodeInstallation,
|
||||
prepareOpenCodeInstallation,
|
||||
} from ${JSON.stringify(installationModuleUrl)};
|
||||
import { readOpenCodeSources } from ${JSON.stringify(ioModuleUrl)};
|
||||
|
||||
const config = JSON.parse(process.env.ELYDORA_TEST_CONFIG);
|
||||
const sources = await readOpenCodeSources();
|
||||
const prepared = await prepareOpenCodeInstallation(config, sources);
|
||||
if (process.env.ELYDORA_TEST_RACE_PLUGIN) {
|
||||
await fsp.mkdir(path.dirname(sources.paths.pluginPath), { recursive: true });
|
||||
await fsp.writeFile(sources.paths.pluginPath, process.env.ELYDORA_TEST_RACE_PLUGIN);
|
||||
}
|
||||
let failed = false;
|
||||
await commitOpenCodeInstallation(prepared, async (from, to) => {
|
||||
if (!failed
|
||||
&& process.env.ELYDORA_TEST_FAIL_LEGACY === 'true'
|
||||
&& from === sources.paths.legacyPluginPath
|
||||
&& to.endsWith('.rollback')) {
|
||||
failed = true;
|
||||
throw new Error('injected OpenCode legacy migration failure');
|
||||
}
|
||||
await fsp.rename(from, to);
|
||||
});
|
||||
`;
|
||||
return runNode(
|
||||
['--input-type=module', '--eval', source],
|
||||
{
|
||||
...environment(fixture),
|
||||
ELYDORA_TEST_CONFIG: JSON.stringify(installConfig(fixture)),
|
||||
ELYDORA_TEST_FAIL_LEGACY: String(failLegacy),
|
||||
ELYDORA_TEST_RACE_PLUGIN: racePlugin,
|
||||
},
|
||||
fixture.projectDir,
|
||||
);
|
||||
}
|
||||
|
||||
async function assertNoTransactionFiles(...directories) {
|
||||
const names = [];
|
||||
for (const directory of directories) {
|
||||
try {
|
||||
names.push(...await readdir(directory));
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') throw error;
|
||||
}
|
||||
}
|
||||
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
|
||||
}
|
||||
|
||||
test('OpenCode rolls back plugin, legacy migration, and runtime after a late failure', async () => {
|
||||
const fixture = await createFixture();
|
||||
const { buildLegacyOpenCodePlugin } = await import(contractModuleUrl);
|
||||
const legacy = buildLegacyOpenCodePlugin(fixture.hookScriptPath, fixture.guardScriptPath);
|
||||
try {
|
||||
await mkdir(path.dirname(fixture.legacyPluginPath), { recursive: true });
|
||||
await writeFile(fixture.legacyPluginPath, legacy);
|
||||
const result = await runPreparedInstallation(fixture, { failLegacy: true });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /injected OpenCode legacy migration failure/);
|
||||
assert.equal(await readFile(fixture.legacyPluginPath, 'utf-8'), legacy);
|
||||
await assertMissing(fixture.pluginPath);
|
||||
for (const filePath of [
|
||||
fixture.guardScriptPath,
|
||||
fixture.hookScriptPath,
|
||||
path.join(fixture.agentDir, 'config.json'),
|
||||
path.join(fixture.agentDir, 'private.key'),
|
||||
]) await assertMissing(filePath);
|
||||
await assertNoTransactionFiles(fixture.pluginsDir, fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('OpenCode detects concurrent plugin creation before committing runtime files', async () => {
|
||||
const fixture = await createFixture();
|
||||
const racePlugin = 'export const RacePlugin = async () => ({});\n';
|
||||
try {
|
||||
const result = await runPreparedInstallation(fixture, { racePlugin });
|
||||
assert.equal(result.code, 1);
|
||||
assert.match(result.stderr, /changed during OpenCode installation/i);
|
||||
assert.equal(await readFile(fixture.pluginPath, 'utf-8'), racePlugin);
|
||||
await assertMissing(fixture.guardScriptPath);
|
||||
await assertMissing(fixture.hookScriptPath);
|
||||
await assertNoTransactionFiles(fixture.pluginsDir, fixture.agentDir);
|
||||
} finally {
|
||||
await fixture.close();
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user