Files
buzz/beehive/test/goose.test.ts
2026-09-10 03:15:26 -04:00

107 lines
8.4 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, realpathSync, writeFileSync, readFileSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { setTimeout as delay } from 'node:timers/promises';
import { terminal } from './local-terminal.ts';
import { installationSlots } from '../src/slots.ts';
import { host, bindingFingerprint } from '../src/host.ts';
import { relay } from '../src/relay.ts';
import { connect } from '../src/client.ts';
import { newKey, publicKey, message, type Message } from '../src/protocol.ts';
import { writePrivate, readPrivate } from '../src/storage.ts';
import { profileRevision } from '../src/profiles.ts';
import { gooseModels, prepareAgent } from '../src/acp.ts';
test('Goose native model evidence rejects absent, ambiguous, unadvertised and mismatched acknowledgements', () => {
const value = { configOptions: [{ configId: 'model', category: 'model', currentValue: 'a', options: [{ value: 'a' }] }] };
assert.deepEqual(gooseModels(value, 'a'), ['a']);
for (const bad of [{}, { models: { currentModelId: 'a' } }, { configOptions: [...value.configOptions, ...value.configOptions] }, { configOptions: [{ ...value.configOptions[0], options: [] }] }]) assert.throws(() => gooseModels(bad, 'a'));
assert.throws(() => gooseModels(value, 'b'));
});
test('actual setup entry Goose/new/reuse/hidden standby and selected binding ACP Restart preserve identity/profile/sibling', { timeout: 35000 }, async () => {
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'bh-goose-'))), source = join(dir, 'source');
const secret = newKey(), identity = join(dir, 'owner.json'); writePrivate(identity, { secret });
const runner = join(dir, 'goose-fixture.ts');
writeFileSync(runner, `#!${realpathSync(process.execPath)}\nif (process.argv[2] !== 'acp') throw Error('Expected goose acp');\nawait import(${JSON.stringify(pathToFileURL(resolve('test/conversation-harness-fixture.ts')).href)});\n`, { mode: 0o700 });
writeFileSync(join(dir, 'mode'), 'ok');
const steps = (name: string) => [
{ prompt: 'Host name: ', answer: name }, { prompt: 'Setup [', answer: '3' },
{ prompt: 'Absolute installed Goose executable (runs acp): ', answer: runner },
{ prompt: 'Allowed workspace (absolute directory): ', answer: dir },
{ prompt: 'Additional allowed workspace (blank for none): ', answer: '' },
{ prompt: 'Locally configured Goose provider ID: ', answer: 'fixture-provider' },
{ prompt: 'Operator-approved compatible exact model IDs (comma-separated): ', answer: 'goose-a,goose-b' },
{ prompt: 'Purpose [', answer: 'diagnostic' },
];
let output = await terminal(['setup', source, identity], [...steps('source'),
{ prompt: 'Create a NEW agent identity', answer: 'yes' },
{ prompt: 'Add another independent NEW agent', answer: 'yes' },
{ prompt: 'Add another independent NEW agent', answer: 'no' },
]);
assert.match(output, /Goose owns provider credentials/); assert.ok(!output.includes('auth databricks'));
assert.match(output, /Start uses an ACP greeting probe, not yet a Buzz relay conversation agent/);
assert.match(output, /explicit local-setup .*action normal/);
assert.match(output, /no provider login or community admission has been verified/);
const slots = installationSlots(source), X = slots[0]!, Y = slots[1]!;
const state = () => readPrivate(X.path) as any;
const originalY = readFileSync(Y.path), originalManifest = readFileSync(join(source, 'setup.json'));
output += await terminal(['setup', source], [{ prompt: 'Local action [', answer: 'reuse' }, { prompt: 'Existing agent public key: ', answer: X.agent }]);
assert.deepEqual(readFileSync(join(source, 'setup.json')), originalManifest);
const root = join(dir, 'genesis.json'); writePrivate(root, state().assignment.genesis);
const target = join(dir, 'target');
output += await terminal(['setup', target, identity], [...steps('target'),
{ prompt: 'Create a NEW agent identity', answer: 'import-standby' },
{ prompt: 'Local public genesis file (no key): ', answer: root },
{ prompt: `Import exact ${X.agent}`, answer: 'yes' },
{ prompt: 'Agent private key (64 hex; hidden, never passed as an argument): ', answer: X.setup.agentSecret! },
{ prompt: 'Add another independent NEW agent', answer: 'no' },
], process.platform === 'darwin');
for (const key of [secret, X.setup.agentSecret!, Y.setup.agentSecret!]) assert.ok(!output.includes(key));
for (const path of [join(source, 'setup.json'), join(target, 'setup.json'), X.path]) assert.equal(statSync(path).mode & 0o777, 0o600);
assert.equal(statSync(join(source, 'service-home')).mode & 0o777, 0o700);
const plan = prepareAgent({ executable: runner, args: ['acp'], workspace: dir, home: X.setup.serviceHome!, configDirectory: X.setup.serviceHome!, databricksHost: '', harness: 'goose', provider: 'fixture-provider', model: 'goose-a' });
assert.equal(plan.env.GOOSE_MODEL, 'goose-a'); assert.equal(plan.env.BUZZ_AGENT_CONFIG_DIR, undefined); assert.equal(plan.env.BUZZ_PRIVATE_KEY, undefined);
const server = await relay(0, publicKey(secret), 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(source, url), standby = await host(target, url), seen: Message[] = [];
const client = connect(url, secret, m => seen.push(m)); await client.ready;
async function request(type: 'start' | 'restart' | 'stop' | 'save', body = {}, hostname = 'source') {
const revision = hostname === 'source' ? state().revision : 0;
const m = message(type, hostname, X.agent, revision, body); client.send(m);
for (let n = 0; n < 400; n++) { const receipt = seen.find(e => e.type === 'receipt' && e.body.operation === m.id); if (receipt) return String(receipt.body.result); await delay(20); }
throw Error(`Missing ${type} receipt`);
}
try {
assert.notEqual(await request('start', {}, 'target'), 'accepted');
const instructions = 'GOOSE-PROFILE: keep behavior independent of provider.';
const revision = profileRevision('Goose behavior', null, instructions);
const selection = { model: 'goose-a', workspace: dir, profile: revision, behavior: { name: 'Goose behavior', parent: null, revision, instructions }, harnessSetup: { id: 'default', fingerprint: bindingFingerprint(X.setup) } };
client.send(message('profile', 'profiles', 'profiles', 0, selection.behavior));
assert.match(await request('save', selection), /saved/);
assert.equal(await request('start'), 'accepted'); const actual = state().actual;
assert.match(actual.evidence.session, /^goose-/); assert.equal(actual.selection.model, 'goose-a');
assert.equal(readFileSync(join(dir, 'received-system-instructions'), 'utf8'), instructions);
assert.match(await request('save', { ...selection, model: 'goose-b' }), /saved/);
assert.deepEqual(state().actual, actual);
assert.equal(await request('restart'), 'accepted');
assert.notEqual(state().actual.evidence.session, actual.evidence.session); assert.equal(state().actual.evidence.model, 'goose-b');
assert.equal(state().actual.selection.behavior.revision, revision); assert.equal(state().binding.agent, X.agent);
assert.deepEqual(state().runs[actual.run], actual);
assert.notEqual(await request('save', { ...selection, model: 'unknown' }), 'saved; running configuration unchanged');
assert.notEqual(await request('save', { ...selection, harnessSetup: { id: 'unknown', fingerprint: selection.harnessSetup.fingerprint } }), 'saved; running configuration unchanged');
assert.notEqual(await request('save', { ...selection, harnessSetup: { id: 'default', fingerprint: '0'.repeat(64) } }), 'saved; running configuration unchanged');
const running = state().actual; writeFileSync(join(dir, 'mode'), 'wrong-model');
assert.notEqual(await request('restart'), 'accepted'); assert.deepEqual(state().actual, running);
rmSync(X.setup.serviceHome!, { recursive: true });
assert.equal(await request('stop'), 'accepted');
assert.deepEqual(readFileSync(Y.path), originalY); assert.deepEqual(readFileSync(join(source, 'setup.json')), originalManifest);
} finally {
await h.close(); await standby.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 });
}
});