Files
buzz/beehive/test/catalog-tui.test.ts
2026-09-10 22:33:43 -04:00

242 lines
19 KiB
TypeScript

import { isolatedFileCredentials } from './isolated-file-credentials.ts';
import { credentialReference } from '../src/credential-store.ts';
import { importSlotKey, installationSlots } from '../src/slots.ts';
import { conversationRelayFixture } from './conversation-relay-fixture.ts';
import test from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, realpathSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { newKey, publicKey, message } from '../src/protocol.ts';
import { hostPairing, registerHost } from '../src/host-registration.ts';
import { verifyHostCatalog, catalogResponse, HostInventory } from '../src/host-catalog.ts';
import { writePrivate } from '../src/storage.ts';
import { provisionCredentialSlot } from '../src/credential-slots.ts';
import { createGenesis } from '../src/assignment.ts';
import { nostrFixture } from './nostr-fixture.ts';
import { connectNostr } from '../src/nostr-client.ts';
function registration(owner: string, key: string, relay: string, label = 'same label') {
return registerHost(hostPairing({ version: 1, purpose: 'beehive-host-registration', host: publicKey(key), owner: publicKey(owner), label, relay, nonce: newKey() }), owner, Math.floor(Date.now() / 1000) + 600);
}
test('catalog verifies owner/signature/relay/expiry; labels cannot spoof identity and missing is not stopped', () => {
const owner = newKey(), a = newKey(), b = newKey(), relay = 'wss://example.invalid';
const one = registration(owner, a, relay), two = registration(owner, b, relay);
const value = { version: 1, owner: publicKey(owner), relay, registrations: [one, two] };
const catalog = verifyHostCatalog(value, value.owner, relay);
assert.throws(() => verifyHostCatalog(value, publicKey(newKey()), relay));
assert.throws(() => verifyHostCatalog(value, value.owner, 'wss://wrong.invalid'));
assert.throws(() => verifyHostCatalog({ ...value, registrations: [{ ...one, signature: '0'.repeat(128) }] }, value.owner, relay));
assert.throws(() => verifyHostCatalog(value, value.owner, relay, one.expires));
const inventory = message('inventory', publicKey(a), 'agent', 0, { observedAt: Date.now(), phase: 'stopped' });
assert.throws(() => catalogResponse(catalog, { sender: publicKey(b), message: inventory }));
assert.throws(() => catalogResponse(catalog, { sender: publicKey(a), message: { ...inventory, host: 'same label' } }));
const view = new HostInventory(catalog);
assert.deepEqual(view.rows().map(r => r.status), ['unreachable/unknown', 'unreachable/unknown']);
view.receive({ sender: publicKey(a), message: inventory });
assert.equal(view.rows()[0]!.status, 'recent-host-report');
assert.equal(view.rows()[0]!.report!.body.phase, 'stopped');
assert.equal(view.rows()[1]!.status, 'unreachable/unknown');
assert.equal(view.rows(Date.now() + 7000)[0]!.status, 'unreachable/unknown');
assert.equal(view.rows(one.expires * 1000)[0]!.report, undefined);
});
for (const scenario of ['external', 'configured', 'installed', 'v3-conversion']) {
const installed = scenario === 'installed' || scenario === 'v3-conversion', converting = scenario === 'v3-conversion';
const profiled = scenario === 'installed';
const profileInstructions = 'PRIVATE_C66_EXACT: keep this behavior private and answer carefully.';
test(`actual owner TUI owner-public credential slots Start/Stop via private transport (${scenario})`, { skip: installed && !process.env.BEEHIVE_REAL_BUZZ_ACP }, async () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-catalog-tui-')));
const owner = newKey(), a = newKey(), b = newKey(), agent = newKey();
const members = new Set([owner, a, b].map(publicKey));
const relay = await nostrFixture(publicKey(owner), members, { open: scenario === 'installed' });
const one = registration(owner, a, relay.url), two = registration(owner, b, relay.url);
const catalog = verifyHostCatalog({ version: 1, owner: publicKey(owner), relay: relay.url, registrations: (scenario === 'configured' || scenario === 'installed') ? [two] : [one, two] }, publicKey(owner), relay.url);
const directory = join(root, 'host'); mkdirSync(directory);
const credentialFile = join(root, 'credentials.json');
const credentials = isolatedFileCredentials(credentialFile);
credentials.create(credentialReference('host', publicKey(a)), a);
writePrivate(join(directory, 'host-identity.json'), { version: 3, pairing: one.request, key: credentialReference('host', publicKey(a)), registration: (scenario === 'configured' || scenario === 'installed') ? null : one });
const conversation = installed ? await conversationRelayFixture(root, owner, publicKey(agent)) : undefined;
provisionCredentialSlot(directory, { host: publicKey(a), ownerPublic: publicKey(owner), runner: realpathSync(process.execPath), args: [resolve(installed ? 'test/conversation-harness-fixture.ts' : 'test/runner.ts')], workspace: root, mode: installed ? 'buzz-agent-databricks-v2' : 'fixture', ...(conversation ? { serviceHome: root, configDirectory: root, databricksHost: 'https://fixture.invalid', ...(!converting ? { conversation: { executable: realpathSync(process.env.BEEHIVE_REAL_BUZZ_ACP!), relay: conversation.url, replyTool: { executable: realpathSync(join(process.env.BEEHIVE_REAL_BUZZ_ACP!, '..', 'buzz')) } } } : {}) } : {}) }, agent, createGenesis(publicKey(owner), publicKey(agent), publicKey(a)), credentials);
const journalPath = join(directory, 'agents', publicKey(agent), 'journal.json');
if (converting) {
const original = readFileSync(journalPath);
const wizard = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), 'src/cli.ts', 'local-setup', directory], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: join(root, 'unused-credentials.json') }, stdio: ['pipe', 'pipe', 'pipe'] });
let output = ''; wizard.stdout.on('data', b => output += b); wizard.stderr.on('data', b => output += b);
const exited = new Promise(resolve => wizard.on('close', resolve));
try {
for (const [prompt, answer] of [['Local action [', 'normal'], ['Existing agent public key:', publicKey(agent)], ['NEW immutable normal binding ID:', 'normal'], ['Absolute installed buzz-acp executable:', realpathSync(process.env.BEEHIVE_REAL_BUZZ_ACP!)], ['Buzz CONVERSATION relay URL', conversation!.url], ['Absolute installed buzz CLI executable:', realpathSync(join(process.env.BEEHIVE_REAL_BUZZ_ACP!, '..', 'buzz'))], ['Save NEW normal binding under installation lock', 'yes']]) {
for (let n = 0; n < 400 && !output.includes(prompt!); n++) { if (wizard.exitCode !== null) break; await delay(20); }
assert.ok(output.includes(prompt!), output); wizard.stdin.write(`${answer}\n`);
}
assert.equal(await exited, 0, output);
} finally { if (wizard.exitCode === null && wizard.signalCode === null) wizard.kill('SIGKILL'); await exited; }
assert.deepEqual(readFileSync(journalPath), original);
assert.equal(installationSlots(directory, credentials)[0]!.setup.conversation, undefined);
assert.ok(installationSlots(directory, credentials)[0]!.bindings.normal!.conversation);
}
const manifest = readFileSync(join(directory, 'setup.json'), 'utf8');
assert.ok(!manifest.includes(owner) && !manifest.includes(agent));
const helper = join(root, 'credential-child.mjs');
writeFileSync(helper, `import { readFileSync } from 'node:fs'; let input=''; for await (const b of process.stdin) input+=b; const secret=JSON.parse(readFileSync(${JSON.stringify(credentialFile)},'utf8'))[JSON.stringify(JSON.parse(input))]; process.stdout.write(JSON.stringify(secret ? {status:'present',secret} : {status:'missing'}));`);
const loader = join(root, 'credential-helper-loader.mjs');
writeFileSync(loader, `import { registerHooks } from 'node:module'; registerHooks({load(url,ctx,next){const r=next(url,ctx); if(!url.endsWith('/src/credential-helper.ts'))return r;return {...r,source:String(r.source).replace("new URL('./credential-helper-child.ts', import.meta.url)", ${JSON.stringify(`new URL(${JSON.stringify('file://' + helper)})`)})};}});`);
// Actual normal setup resumes both approved compatibility and unapproved
// installed reply/Stop fixtures, even when an agent manifest already exists.
const beforeSetup = readFileSync(join(directory, 'host-identity.json'));
const setupChild = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), '--import', loader, 'src/cli.ts', 'setup', directory], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: credentialFile }, stdio: ['pipe', 'pipe', 'pipe'] });
let setupOutput = ''; setupChild.stdout.on('data', b => setupOutput += b); setupChild.stderr.on('data', b => setupOutput += b);
const setupExit = new Promise<number | null>(done => setupChild.on('close', done));
try {
assert.equal(await setupExit, 0, setupOutput);
assert.match(setupOutput, /Host configured, NOT serving/);
assert.ok(!setupOutput.includes('Check/join'));
assert.deepEqual(readFileSync(join(directory, 'host-identity.json')), beforeSetup);
assert.equal(readFileSync(join(directory, 'setup.json'), 'utf8'), manifest);
} catch (error) {
await conversation?.close(); await relay.close(); rmSync(root, { recursive: true, force: true });
throw error;
} finally { if (setupChild.exitCode === null && setupChild.signalCode === null) setupChild.kill('SIGKILL'); await setupExit; }
const hostChild = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), '--import', loader, 'src/cli.ts', 'host', directory, ...(installed ? [relay.url] : []), '--owner-present'], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: credentialFile }, stdio: ['ignore','pipe','pipe'] });
let hostOutput = ''; hostChild.stdout.on('data', b => hostOutput += b); hostChild.stderr.on('data', b => hostOutput += b);
const hostExit = new Promise<number | null>(resolve => hostChild.on('close', resolve));
const running = { async close() { if (hostChild.exitCode === null && hostChild.signalCode === null) hostChild.kill('SIGTERM'); assert.equal(await hostExit, 0, hostOutput); } };
for (let n=0; n<400 && !hostOutput.includes('Host online'); n++) { if (hostChild.exitCode !== null) break; await delay(20); }
assert.match(hostOutput, /Host online/);
const foreignMessages: unknown[] = [];
const foreign = connectNostr(relay.url, Buffer.from(b, 'hex'), m => foreignMessages.push(m), () => {});
await foreign.ready;
const path = join(root, 'catalog.json'); writePrivate(path, catalog);
const env: NodeJS.ProcessEnv = { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: credentialFile };
const child = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), 'src/cli.ts', 'tui', path, ...(installed ? [relay.url] : [])], { env, stdio: ['pipe', 'pipe', 'pipe'] });
let output = ''; child.stdout.on('data', b => output += b.toString()); child.stderr.on('data', b => output += b.toString());
const exit = new Promise<number | null>((done, reject) => { child.on('exit', done); child.on('error', reject); });
async function wait(predicate: () => boolean) { for (let n = 0; n < (installed ? 3000 : 400); n++) { if (predicate()) return; if (child.exitCode !== null) throw Error(output); await delay(20); } throw Error(`TUI observation timeout: ${output}`); }
async function command(line: string) { const start = output.length; child.stdin.write(`${line}\n`); await wait(() => output.slice(start).includes('beehive> ')); return output.slice(start); }
try {
await wait(() => output.includes('Owner private key'));
child.stdin.write(`${owner}\n`);
await wait(() => output.includes('beehive> '));
let rows = await command('hosts');
assert.match(rows, /UNREACHABLE \/ UNKNOWN/);
assert.ok(rows.includes(publicKey(a))); assert.ok(rows.includes(publicKey(b)));
assert.match(rows, /recent host report/);
await command(`select ${publicKey(a)}`);
if (converting) {
await command('binding normal');
let selected = '';
for (let n = 0; n < 80; n++) { selected = await command('show'); if (selected.includes('"revision": 1,\n "body"')) break; await delay(25); }
assert.ok(selected.includes('"revision": 1,\n "body"'), selected);
const saved = JSON.parse(readFileSync(journalPath, 'utf8'));
assert.equal(saved.selected.harnessSetup.id, 'normal'); assert.equal(saved.actual, null);
}
if (profiled) {
let at = output.length; child.stdin.write('profile-new\n');
await wait(() => output.slice(at).includes('Profile name:')); child.stdin.write('Private careful\n');
await wait(() => output.slice(at).includes('Nonsecret behavior instructions')); child.stdin.write(`${profileInstructions}\n`);
await wait(() => output.slice(at).includes('Publish immutable revision?')); child.stdin.write('no\n');
await wait(() => output.slice(at).includes('beehive> '));
const drafts = await command('drafts');
const id = drafts.match(/([a-f0-9-]{36}) \| saved draft/)![1];
assert.match(drafts, /not submitted/);
at = output.length; child.stdin.write(`profile-resume ${id}\n`);
await wait(() => output.slice(at).includes('Resume [edit/publish/cancel]')); child.stdin.write('publish\n');
await wait(() => output.slice(at).includes('Publish immutable revision?')); child.stdin.write('yes\n');
await wait(() => output.slice(at).includes('beehive> '));
let published = '';
for (let n = 0; n < 80; n++) { published = await command('profiles'); if (published.includes(profileInstructions)) break; await delay(25); }
assert.ok(published.includes(profileInstructions), published);
await command('apply 1');
let selected = '';
for (let n = 0; n < 80; n++) { selected = await command('show'); if (selected.includes('"revision": 1,\n "body"')) break; await delay(25); }
assert.ok(selected.includes('"revision": 1,\n "body"'), selected);
assert.equal(JSON.parse(readFileSync(journalPath, 'utf8')).actual, null);
}
await command('start');
await wait(() => output.includes('completed | accepted'));
let shown = '';
for (let n = 0; n < 80; n++) { shown = await command('show'); if (shown.includes('"phase": "running"')) break; await delay(25); }
assert.match(shown, /"phase": "running"/);
assert.ok(shown.includes(publicKey(agent)));
if (conversation) {
await wait(() => conversation.replies.length > 0);
const actual = JSON.parse(readFileSync(journalPath, 'utf8')).actual;
assert.equal(actual.evidence.agentPublicKey, publicKey(agent));
assert.equal(actual.evidence.session, 'conversation-session');
assert.equal(actual.selection.model, 'databricks-claude-haiku-4-5');
if (profiled) {
assert.equal(actual.selection.behavior.instructions, profileInstructions);
assert.equal(readFileSync(join(root, 'received-system-instructions'), 'utf8'), profileInstructions);
const nativeInputs = readFileSync(join(root, 'native-session-inputs.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
assert.ok(nativeInputs.length > 0);
assert.ok(nativeInputs.every(input => input.systemPrompt === profileInstructions));
}
console.log('New private management transport installed signed agent reply:', conversation.replies[0]!.id);
}
const stopAt = output.length;
await command('stop');
await wait(() => output.slice(stopAt).includes('accepted'));
for (let n = 0; n < 80; n++) { shown = await command('show'); if (shown.includes('"phase": "stopped"')) break; await delay(25); }
assert.match(shown, /"phase": "stopped"/);
if (conversation) for (const file of ['harness-pid', 'descendant-pid', 'tool-shim-pid']) {
const pid = Number(readFileSync(join(root, file), 'utf8'));
assert.ok(Number.isSafeInteger(pid) && pid > 0);
assert.throws(() => process.kill(pid, 0), (error: NodeJS.ErrnoException) => error.code === 'ESRCH', `${file} survives accepted Stop`);
}
const beforeReplay = readFileSync(journalPath, 'utf8');
await foreign.publish(message('start', publicKey(a), publicKey(agent), 2), publicKey(a));
await foreign.publish(message('inventory', publicKey(a), publicKey(agent), 999, { phase: 'FORGED', observedAt: Date.now() + 500 }), publicKey(owner));
const ownerWire = connectNostr(relay.url, Buffer.from(owner, 'hex'), () => {}, () => {});
await ownerWire.ready;
try {
const journal = JSON.parse(beforeReplay);
// Rewrap the exact original durable Start after Stop. Relay ACK is not another execution.
const intentDirectory = join(root, 'management-intents');
const { readdirSync } = await import('node:fs');
const { open } = await import('../src/protocol.ts');
const scope = join(intentDirectory, readdirSync(intentDirectory)[0]!);
const intent = readdirSync(scope).filter(n => n.endsWith('.intent')).map(n => JSON.parse(readFileSync(join(scope, n), 'utf8'))).map(v => open(v.envelope, owner)).find(m => m.type === 'start')!;
await ownerWire.publish(intent, publicKey(a));
await command('reconcile');
await delay(100);
shown = await command('show');
assert.ok(!shown.includes('FORGED'));
assert.match(shown, /"phase": "stopped"/);
assert.equal(readFileSync(journalPath, 'utf8'), beforeReplay);
assert.equal(Object.keys(journal.operations).length, converting || profiled ? 3 : 2);
} finally { ownerWire.close(); }
assert.equal(foreignMessages.length, 0, 'another admitted host cannot read owner inventory or commands');
assert.equal(relay.httpRequests.length, 0);
assert.ok(relay.history.every(e => e.kind === 1059 && e.tags.length === 1));
assert.ok(!output.includes(owner)); assert.ok(!output.includes(a)); assert.ok(!output.includes(agent));
const state = JSON.parse(readFileSync(journalPath, 'utf8'));
assert.equal(state.phase, 'stopped'); assert.equal(state.revision, converting || profiled ? 3 : 2);
// Simulate deliberate OS deletion while the host still has its old hydrated
// setup in memory: the production pre-spawn re-read must refuse that cache.
const reference = JSON.parse(manifest).agents[publicKey(agent)].key;
credentials.remove(reference);
const runs = JSON.stringify(state.runs);
await command('start');
await wait(() => Object.keys(JSON.parse(readFileSync(journalPath, 'utf8')).operations).length === (converting || profiled ? 4 : 3));
const refused = JSON.parse(readFileSync(journalPath, 'utf8'));
assert.equal(refused.phase, 'stopped'); assert.equal(refused.actual, null);
assert.equal(JSON.stringify(refused.runs), runs);
assert.equal(installationSlots(directory, credentials)[0]!.keyPresent, false);
await running.close();
importSlotKey(directory, publicKey(agent), agent, credentials);
assert.equal(installationSlots(directory, credentials)[0]!.setup.agentSecret, agent);
assert.equal(readFileSync(join(directory, 'setup.json'), 'utf8'), manifest);
child.stdin.write('quit\n'); assert.equal(await exit, 0, output);
} finally {
if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await exit; }
foreign.close(); await running.close(); await conversation?.close(); await relay.close(); rmSync(root, { recursive: true, force: true });
}
});
}