Files
Elydora-Node-SDK/test/grok-transaction.test.mjs

176 lines
6.0 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile, readdir } from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import {
contractModuleUrl,
createFixture,
installationModuleUrl,
installConfig,
ioModuleUrl,
runNode,
} from '../test-support/grok-test-helpers.mjs';
const runtimeNames = ['guard.js', 'config.json', 'private.key', 'hook.js'];
async function snapshotFixture(fixture) {
const paths = [
...runtimeNames.map((name) => path.join(fixture.agentDir, name)),
fixture.configPath,
];
return new Map(await Promise.all(paths.map(async (filePath) => [
filePath,
await readFile(filePath, 'utf-8'),
])));
}
async function assertSnapshot(snapshot) {
for (const [filePath, contents] of snapshot) {
assert.equal(await readFile(filePath, 'utf-8'), contents, filePath);
}
}
async function assertNoTransactionFiles(fixture) {
for (const directory of [fixture.agentDir, path.dirname(fixture.configPath)]) {
const names = await readdir(directory);
assert.equal(names.some((name) => /\.(tmp|rollback)$/.test(name)), false, names.join(', '));
}
}
function runFixtureSource(fixture, source, environment = {}) {
return runNode(
['--input-type=module', '--eval', source],
{
HOME: fixture.homeDir,
USERPROFILE: fixture.homeDir,
GROK_HOME: fixture.grokHome,
...environment,
},
fixture.projectDir,
);
}
const prepareInstallSource = `
import {
buildGrokGroup,
removeManagedGrokHooks,
renderGrokDocument,
} from ${JSON.stringify(contractModuleUrl)};
import { readGrokDocument } from ${JSON.stringify(ioModuleUrl)};
import {
preflightGrokInstallation,
prepareGrokInstallation,
} from ${JSON.stringify(installationModuleUrl)};
const config = JSON.parse(process.env.ELYDORA_INSTALL_CONFIG);
const document = await readGrokDocument();
const paths = await preflightGrokInstallation(config, document.filePath);
const cleaned = removeManagedGrokHooks(document.hooks);
const hooks = {
...cleaned,
PreToolUse: [...(cleaned.PreToolUse ?? []), buildGrokGroup(paths.guardPath)],
PostToolUse: [...(cleaned.PostToolUse ?? []), buildGrokGroup(paths.auditPath)],
PostToolUseFailure: [
...(cleaned.PostToolUseFailure ?? []),
buildGrokGroup(paths.auditPath),
],
};
const prepared = await prepareGrokInstallation(
config,
renderGrokDocument(document, hooks),
);
`;
test('Grok restores its hook config and four runtime files after the final commit fails', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotFixture(fixture);
const source = `${prepareInstallSource}
import { rename } from 'node:fs/promises';
import { commitGrokInstallation } from ${JSON.stringify(installationModuleUrl)};
let calls = 0;
await commitGrokInstallation(prepared, async (from, to) => {
calls += 1;
if (calls === 5) throw new Error('injected Grok config failure');
await rename(from, to);
});
`;
const result = await runFixtureSource(fixture, source, {
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture, {
orgId: 'org-updated',
token: 'token-updated',
})),
});
assert.equal(result.code, 1);
assert.match(result.stderr, /injected Grok config failure/i);
await assertSnapshot(before);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Grok rejects concurrent config changes before committing runtime updates', async () => {
const fixture = await createFixture();
try {
assert.equal((await fixture.install()).code, 0);
const before = await snapshotFixture(fixture);
const concurrent = '{"owner":"concurrent"}\n';
const source = `${prepareInstallSource}
import { writeFile } from 'node:fs/promises';
import { commitGrokInstallation } from ${JSON.stringify(installationModuleUrl)};
await writeFile(document.filePath, process.env.ELYDORA_CONCURRENT, 'utf-8');
await commitGrokInstallation(prepared);
`;
const result = await runFixtureSource(fixture, source, {
ELYDORA_CONCURRENT: concurrent,
ELYDORA_INSTALL_CONFIG: JSON.stringify(installConfig(fixture, { orgId: 'updated' })),
});
assert.equal(result.code, 1);
assert.match(result.stderr, /changed during Grok Build installation/i);
for (const [filePath, contents] of before) {
if (filePath !== fixture.configPath) {
assert.equal(await readFile(filePath, 'utf-8'), contents, filePath);
}
}
assert.equal(await readFile(fixture.configPath, 'utf-8'), concurrent);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});
test('Grok uninstall keeps the original hook config when commit fails', async () => {
const fixture = await createFixture({ config: '{"owner":"user"}\n' });
try {
assert.equal((await fixture.install()).code, 0);
const before = await readFile(fixture.configPath, 'utf-8');
const source = `
import {
removeManagedGrokHooks,
renderGrokDocument,
} from ${JSON.stringify(contractModuleUrl)};
import { readGrokDocument } from ${JSON.stringify(ioModuleUrl)};
import {
commitGrokUninstall,
prepareGrokUninstall,
} from ${JSON.stringify(installationModuleUrl)};
const document = await readGrokDocument();
const prepared = await prepareGrokUninstall(renderGrokDocument(
document,
removeManagedGrokHooks(document.hooks, 'agent-1'),
));
await commitGrokUninstall(prepared, async () => {
throw new Error('injected Grok uninstall failure');
});
`;
const result = await runFixtureSource(fixture, source);
assert.equal(result.code, 1);
assert.match(result.stderr, /injected Grok uninstall failure/i);
assert.equal(await readFile(fixture.configPath, 'utf-8'), before);
await assertNoTransactionFiles(fixture);
} finally {
await fixture.close();
}
});