mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
Signed-off-by: Brother Darryl <7d7dea84f63f17682fa12720e0bb802a127c378d835b3ef416f80ed0ef0f6705@buzz.block.builderlab.xyz>
186 lines
11 KiB
TypeScript
186 lines
11 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { hostname } from 'node:os';
|
|
import { npubEncode } from 'nostr-tools/nip19';
|
|
import { newKey, publicKey } from '../src/protocol.ts';
|
|
|
|
/** Launcher-focused smoke: the installed symlink must behave exactly like the
|
|
* documented `node src/cli.ts` invocation from any directory, keep explicit
|
|
* directories working, keep help public-only, and gate Node before any
|
|
* TypeScript loading. Credential-touching journeys use the established
|
|
* isolated credential fixture via NODE_OPTIONS, never the OS store. */
|
|
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
const launcherSource = join(packageRoot, 'bin', 'beehive.cjs');
|
|
const loader = fileURLToPath(new URL('./isolated-credentials-loader.ts', import.meta.url));
|
|
const nodeBin = dirname(process.execPath);
|
|
const baseEnv = (home: string): NodeJS.ProcessEnv => ({ PATH: `${nodeBin}:/usr/bin:/bin`, HOME: home });
|
|
const isolatedEnv = (home: string, credentials: string): NodeJS.ProcessEnv => ({ ...baseEnv(home), BEEHIVE_TEST_CREDENTIAL_FILE: credentials, NODE_OPTIONS: `--import ${loader}` });
|
|
|
|
async function runLauncher(installed: string, args: string[], env: NodeJS.ProcessEnv, steps: [string, string][], cwd: string, timeoutMs = 30000): Promise<{ code: number | null; output: string; answered: number }> {
|
|
const child = spawn(installed, args, { env, cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
let output = '', cursor = 0, next = 0;
|
|
const timer = setTimeout(() => child.kill('SIGKILL'), timeoutMs);
|
|
child.stdout.on('data', data => {
|
|
output += data.toString();
|
|
if (next < steps.length) {
|
|
const [prompt, answer] = steps[next]!;
|
|
const index = output.indexOf(prompt, cursor);
|
|
if (index !== -1) { cursor = index + prompt.length; next++; child.stdin.write(`${answer}\n`); }
|
|
}
|
|
});
|
|
child.stderr.on('data', data => { output += data.toString(); });
|
|
try {
|
|
const code = await new Promise<number | null>((resolve, reject) => { child.on('error', reject); child.on('exit', resolve); });
|
|
return { code, output: `${output}\n[answered ${next}/${steps.length} prompts]`, answered: next };
|
|
} finally { clearTimeout(timer); child.stdin.end(); }
|
|
}
|
|
|
|
const createJourney = (owner: string): [string, string][] => [
|
|
['Owner PUBLIC npub', npubEncode(publicKey(owner))], ['Management relay URL (', 'wss://example.invalid'],
|
|
['Create host identity', 'yes'],
|
|
];
|
|
|
|
function installInto(root: string, name = 'user bin dir'): string {
|
|
const install = join(root, name);
|
|
mkdirSync(install);
|
|
const installed = join(install, 'beehive');
|
|
symlinkSync(launcherSource, installed);
|
|
return installed;
|
|
}
|
|
|
|
test('installed launcher prints public help from an unrelated directory with spaces and creates no state', { timeout: 20000 }, async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
|
const home = join(root, 'home dir');
|
|
try {
|
|
mkdirSync(home);
|
|
const installed = installInto(root);
|
|
const cwd = join(root, 'unrelated cwd dir');
|
|
mkdirSync(cwd);
|
|
const help = await runLauncher(installed, ['--help'], baseEnv(home), [], cwd);
|
|
assert.equal(help.code, 0, help.output);
|
|
assert.match(help.output, /^Beehive — run and manage your agents/, help.output);
|
|
assert.ok(help.output.includes('beehive host'), help.output);
|
|
assert.ok(help.output.includes('Local Host screen'), help.output);
|
|
assert.ok(!help.output.includes('Enrollment ['), help.output);
|
|
assert.ok(!existsSync(join(home, '.beehive')), 'help must not create state');
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('no-arg setup configures on the displayed default host folder via the isolated credential fixture', { timeout: 30000 }, async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
|
const home = join(root, 'home dir');
|
|
try {
|
|
mkdirSync(home);
|
|
const installed = installInto(root);
|
|
const cwd = join(root, 'unrelated cwd dir');
|
|
mkdirSync(cwd);
|
|
const owner = newKey();
|
|
const credentials = join(root, 'isolated fixture secrets.json');
|
|
const created = await runLauncher(installed, ['setup'], isolatedEnv(home, credentials), createJourney(owner), cwd);
|
|
assert.equal(created.code, 0, created.output);
|
|
assert.equal(created.answered, 3, created.output);
|
|
const folder = join(home, '.beehive', 'host');
|
|
assert.ok(created.output.includes(`Default host folder: ${folder}`), created.output);
|
|
const identity = JSON.parse(readFileSync(join(folder, 'host-identity.json'), 'utf8')) as { version: number; pairing: { host: string; owner: string; label: string }; registration: unknown };
|
|
assert.equal(identity.version, 3);
|
|
assert.equal(identity.pairing.label, hostname().slice(0, 128));
|
|
assert.equal(identity.pairing.owner, publicKey(owner));
|
|
assert.ok(/^[0-9a-f]{64}$/.test(identity.pairing.host));
|
|
assert.equal(identity.registration, null);
|
|
const fixture = readFileSync(credentials, 'utf8');
|
|
assert.ok(fixture.includes(identity.pairing.host), 'host key stored in the isolated fixture');
|
|
assert.equal(Object.keys(JSON.parse(fixture)).length, 1);
|
|
const files = [join(folder, 'host-identity.json'), credentials];
|
|
for (const value of [...files.map(file => readFileSync(file, 'utf8')), created.output]) assert.ok(!value.includes(owner), 'owner secret never persisted or printed');
|
|
// A second default setup must refuse to replace the retained identity.
|
|
const before = readFileSync(join(folder, 'host-identity.json'), 'utf8');
|
|
const second = await runLauncher(installed, ['setup'], isolatedEnv(home, credentials), [
|
|
], cwd);
|
|
assert.equal(second.code, 0, second.output);
|
|
assert.ok(second.output.includes('Identity, owner, relay and approval history retained'), second.output);
|
|
assert.equal(readFileSync(join(folder, 'host-identity.json'), 'utf8'), before, 'retained identity unchanged');
|
|
assert.ok(!existsSync(join(root, 'second request.json')));
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('explicit host directory setup is preserved through the launcher and leaves the default unused', { timeout: 30000 }, async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
|
const home = join(root, 'home dir');
|
|
try {
|
|
mkdirSync(home);
|
|
const installed = installInto(root);
|
|
const cwd = join(root, 'unrelated cwd dir');
|
|
mkdirSync(cwd);
|
|
const owner = newKey();
|
|
const explicit = join(root, 'explicit host dir');
|
|
const credentials = join(root, 'isolated fixture secrets.json');
|
|
const created = await runLauncher(installed, ['setup', explicit], isolatedEnv(home, credentials), createJourney(owner), cwd);
|
|
assert.equal(created.code, 0, created.output);
|
|
assert.ok(!created.output.includes('Default host folder:'), created.output);
|
|
assert.ok(existsSync(join(explicit, 'host-identity.json')), 'identity lands in the explicit directory');
|
|
assert.ok(!existsSync(join(home, '.beehive')), 'default folder untouched');
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('host startup keeps the owner-present guard on both the default folder and explicit directories', { timeout: 20000 }, async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
|
const home = join(root, 'home dir');
|
|
try {
|
|
mkdirSync(home);
|
|
const installed = installInto(root);
|
|
const cwd = join(root, 'unrelated cwd dir');
|
|
mkdirSync(cwd);
|
|
const defaultFolder = join(home, '.beehive', 'host');
|
|
mkdirSync(defaultFolder, { recursive: true });
|
|
writeFileSync(join(defaultFolder, 'host-identity.json'), '{}', { mode: 0o600 });
|
|
const guard = await runLauncher(installed, ['host', 'wss://example.invalid'], isolatedEnv(home, join(root, 'isolated fixture secrets.json')), [], cwd);
|
|
assert.equal(guard.code, 1, guard.output);
|
|
assert.ok(guard.output.includes('Private host requires --owner-present'), guard.output);
|
|
const explicit = join(root, 'explicit host dir');
|
|
mkdirSync(explicit);
|
|
writeFileSync(join(explicit, 'host-identity.json'), '{}', { mode: 0o600 });
|
|
const explicitGuard = await runLauncher(installed, ['host', explicit, 'wss://example.invalid'], isolatedEnv(home, join(root, 'isolated fixture secrets.json')), [], cwd);
|
|
assert.equal(explicitGuard.code, 1, explicitGuard.output);
|
|
assert.ok(explicitGuard.output.includes('Private host requires --owner-present'), explicitGuard.output);
|
|
// Relay-first form parses the flag and resolves the default folder offline.
|
|
const emptyHome = join(root, 'empty home dir');
|
|
mkdirSync(emptyHome);
|
|
const missing = await runLauncher(installed, ['host', 'ws://127.0.0.1:1', '--owner-present'], isolatedEnv(emptyHome, join(root, 'isolated fixture secrets.json')), [], cwd);
|
|
assert.equal(missing.code, 1, missing.output);
|
|
assert.ok(missing.output.includes(join(emptyHome, '.beehive', 'host')), missing.output);
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('launcher Node gate: boundary comparison and a real old-node refusal', { timeout: 20000 }, async () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-launcher-'));
|
|
const home = join(root, 'home dir');
|
|
try {
|
|
mkdirSync(home);
|
|
const probe = spawnSync(process.execPath, ['-e', "const gate = require(process.argv[1]); for (const v of process.argv.slice(2)) process.stdout.write(gate.meetsMinimumNode(v, '22.18.0') + ' ');", launcherSource, '22.18.0', '22.17.9', '18.0.0', '24.15.0', '26.8.1'], { encoding: 'utf8' });
|
|
assert.equal(probe.status, 0, probe.stderr);
|
|
assert.equal(probe.stdout.trim(), 'true false false true true');
|
|
// End-to-end: a PATH "node" reporting 18.9.0 must make the launcher fail
|
|
// fast with the actionable message before any TypeScript is loaded.
|
|
const stubBin = join(root, 'stub node bin');
|
|
mkdirSync(stubBin);
|
|
const patch = join(root, 'old node patch.mjs');
|
|
writeFileSync(patch, "Object.defineProperty(process.versions, 'node', { value: '18.9.0', configurable: true });\n");
|
|
writeFileSync(join(stubBin, 'node'), `#!/bin/sh\nexec '${process.execPath}' --import '${patch}' "$@"\n`);
|
|
chmodSync(join(stubBin, 'node'), 0o755);
|
|
const installed = installInto(root);
|
|
const cwd = join(root, 'unrelated cwd dir');
|
|
mkdirSync(cwd);
|
|
const refused = await runLauncher(installed, [], { PATH: `${stubBin}:${nodeBin}:/usr/bin:/bin`, HOME: home }, [], cwd);
|
|
assert.equal(refused.code, 1, refused.output);
|
|
assert.ok(refused.output.includes('beehive: Node.js 22.18.0 or newer is required'), refused.output);
|
|
assert.ok(refused.output.includes('18.9.0'), refused.output);
|
|
assert.ok(refused.output.includes('nodejs.org'), refused.output);
|
|
assert.ok(!refused.output.includes('Beehive — private host preview'), refused.output);
|
|
} finally { rmSync(root, { recursive: true, force: true }); }
|
|
});
|