Files
buzz/beehive/test/acp.test.ts
2026-09-16 08:59:46 -04:00

123 lines
8.1 KiB
TypeScript

import { provisionSetup } from './provision.ts';
import { test } from 'node:test';
import { spawnSync } from 'node:child_process';
import assert from 'node:assert/strict';
import { mkdtempSync, realpathSync, rmSync, mkdirSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { AgentSession, prepareAgent, type AgentLaunch } from '../src/acp.ts';
import { host } from '../src/host.ts';
import { relay } from '../src/relay.ts';
import { connect } from '../src/client.ts';
import { message, newKey, publicKey, type Message } from '../src/protocol.ts';
import { writePrivate } from '../src/storage.ts';
const fixture = resolve('test/acp-fixture.ts');
function plan(dir: string, mode = 'ok'): AgentLaunch {
return { executable: realpathSync(process.execPath), args: [fixture, mode], workspace: dir, home: dir, configDirectory: dir, databricksHost: 'https://fixture.invalid', model: 'databricks-claude-haiku-4-5' };
}
async function cleanup(s: AgentSession) { await s.owned.stop(); }
test('external ACP boundary: snapshot, catalog provenance, exact-model same-session evidence and failures', async () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-acp-')));
try {
const input = plan(dir); const prepared = prepareAgent(input);
(input.args as string[]).push('mutated');
assert.equal(prepared.plan.args.length, 2);
assert.equal(Object.hasOwn(prepared.env, 'DATABRICKS_TOKEN'), false);
assert.equal(Object.hasOwn(prepared.env, 'BUZZ_PRIVATE_KEY'), false);
assert.throws(() => prepareAgent({ ...plan(dir), databricksHost: 'https://user:secret@example.com' }));
for (const mode of ['ok', 'empty', 'filtered', 'wrong-model', 'wrong-session', 'cancelled', 'reject', 'malformed', 'timeout', 'flood', 'bad-tail', 'current-drift', 'config-drift', 'coalesced-drift', 'foreign-drift', 'non-model']) {
const s = new AgentSession(plan(dir, mode), 1500);
try {
if (['malformed', 'timeout', 'flood'].includes(mode)) { await assert.rejects(s.catalog()); continue; }
const catalog = await s.catalog();
assert.equal(catalog.authentication, 'unverified');
assert.equal(catalog.state, mode === 'empty' ? 'empty' : mode === 'filtered' ? 'filtered' : 'reported');
assert.ok(!JSON.stringify(catalog).includes('DO-NOT-RELAY'));
if (['wrong-model', 'wrong-session', 'cancelled', 'reject', 'bad-tail', 'current-drift', 'config-drift', 'coalesced-drift'].includes(mode)) {
await assert.rejects(s.verify(), e => e instanceof Error && !e.message.includes('DO-NOT-RELAY'));
} else {
const evidence = await s.verify();
assert.equal(evidence.model, plan(dir).model);
assert.equal(evidence.session, 'fixture-session');
assert.match(evidence.responseHash, /^[0-9a-f]{64}$/);
assert.equal(evidence.executableHash, s.prepared.executableHash);
}
} catch (error) { throw new Error(`ACP fixture mode ${mode} failed`, { cause: error }); } finally { await cleanup(s); }
}
} finally { rmSync(dir, { recursive: true, force: true }); }
});
test('production host Start uses external ACP evidence; Save and Stop do not need auth', async () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-acp-host-')));
const hostDir = join(dir, 'host'); mkdirSync(hostDir);
const ownerSecret = newKey(); const agentSecret = newKey(); const agent = publicKey(agentSecret);
const p = plan(dir);
const authDir = join(dir, 'isolated-auth'); mkdirSync(authDir);
provisionSetup(join(hostDir, 'setup.json'), { host: 'acp-host', ownerSecret, agentSecret, runner: p.executable, args: p.args, workspace: dir, mode: 'buzz-agent-databricks-v2', serviceHome: dir, configDirectory: authDir, databricksHost: p.databricksHost });
const authInfo = spawnSync(process.execPath, ['src/cli.ts', 'auth-info', hostDir], { encoding: 'utf8', timeout: 3000 });
assert.equal(authInfo.status, 0); assert.match(authInfo.stdout, /acp-host/); assert.match(authInfo.stdout, /auth databricks/);
assert.ok(!authInfo.stdout.includes(ownerSecret) && !authInfo.stdout.includes(agentSecret));
const server = await relay(0, publicKey(ownerSecret), join(dir, 'relay.json'));
const address = server.address(); assert.ok(address && typeof address !== 'string');
const url = `ws://127.0.0.1:${address.port}`;
const h = await host(hostDir, url);
const seen: Message[] = [];
const client = connect(url, ownerSecret, m => seen.push(m)); await client.ready;
async function request(type: 'start' | 'save' | 'stop', revision: number, body = {}) {
const m = message(type, 'acp-host', agent, revision, body); client.send(m);
for (let i = 0; i < 200; i++) {
const r = seen.find(r => r.type === 'receipt' && r.body.operation === m.id);
if (r) return r;
await delay(25);
}
throw Error('No host receipt');
}
try {
assert.equal((await request('save', 0, { model: p.model, workspace: dir, profile: 'default' })).body.result, 'saved; running configuration unchanged');
assert.equal((await request('start', 1)).body.result, 'accepted');
const state = JSON.parse(readFileSync(join(hostDir, 'journal.json'), 'utf8'));
assert.equal(state.actual.evidence.model, p.model);
assert.equal(state.actual.evidence.session, 'fixture-session');
assert.equal(state.actual.selection.model, p.model);
assert.equal(state.actual.executableHash, state.actual.evidence.executableHash);
rmSync(authDir, { recursive: true });
assert.equal((await request('save', 2, { model: p.model, workspace: dir, profile: 'default' })).body.result, 'saved; running configuration unchanged');
// The auth directory is already absent; both Save and Stop remain usable.
assert.equal((await request('stop', 3)).body.result, 'accepted');
const stopped = JSON.parse(readFileSync(join(hostDir, 'journal.json'), 'utf8'));
assert.equal(stopped.binding.host, 'acp-host'); assert.equal(stopped.phase, 'stopped');
assert.ok(!readFileSync(join(dir, 'relay.json'), 'utf8').includes('fixture-session'));
} finally {
await h.close(); client.close(); for (const c of server.clients) c.terminate();
await new Promise<void>(resolve => server.close(() => resolve())); rmSync(dir, { recursive: true, force: true });
}
});
for (const mode of ['current-drift', 'config-drift', 'coalesced-drift', 'foreign-drift', 'non-model']) test(`diagnostic Start consuming boundary: ${mode}`, async () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-model-host-')));
const ownerSecret = newKey(), agentSecret = newKey(), agent = publicKey(agentSecret);
const p = plan(dir, mode);
provisionSetup(join(dir, 'setup.json'), { host: 'model-host', ownerSecret, agentSecret, runner: p.executable, args: p.args, workspace: dir, mode: 'buzz-agent-databricks-v2', serviceHome: dir, configDirectory: dir, databricksHost: p.databricksHost });
const server = await relay(0, publicKey(ownerSecret), join(dir, 'relay.json'));
const address = server.address(); assert.ok(address && typeof address !== 'string');
const url = `ws://127.0.0.1:${address.port}`;
const h = await host(dir, url), seen: Message[] = [];
const client = connect(url, ownerSecret, m => seen.push(m)); await client.ready;
try {
const op = message('start', 'model-host', agent, 0); client.send(op);
for (let n = 0; n < 200 && !seen.some(m => m.type === 'receipt' && m.body.operation === op.id); n++) await delay(25);
const receipt = seen.find(m => m.type === 'receipt' && m.body.operation === op.id); assert.ok(receipt);
const state = JSON.parse(readFileSync(join(dir, 'journal.json'), 'utf8'));
if (mode === 'foreign-drift' || mode === 'non-model') {
assert.equal(receipt.body.result, 'accepted'); assert.equal(state.actual.evidence.model, p.model);
} else {
assert.notEqual(receipt.body.result, 'accepted'); assert.equal(state.actual, null); assert.equal(state.phase, 'stopped');
}
} finally {
await h.close(); client.close(); for (const c of server.clients) c.terminate();
await new Promise<void>(resolve => server.close(() => resolve())); rmSync(dir, { recursive: true, force: true });
}
});