Files
buzz/desktop/tests/e2e/agents-everywhere.live.spec.ts
+2 61cc738ee8 feat(desktop+acp): spawn a harness per (agent, community) pair at GUI startup — warm sockets, lazy LLM pool (#2122)
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Signed-off-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: Tyler <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: npub1jh9wn95s0472h86ahapupaf7m6kx4v9sx2n0atj2hltcfer8k06s5n3pyf <95cae996907d7cab9f5dbf43c0f53edeac6ab0b032a6feae4abfd784e467b3f5@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub12gtutshhh76rx0jx697f32f9tffd4hhp3hx58fp4x6u4uemkm7sqf8f757 <5217c5c2f7bfb4333e46d17c98a9255a52dadee18dcd43a43536b95e6776dfa0@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1t2tgm7d8f995uqvmnm8h88sg3wnpp9a5xysjf6dg3tjmgt3ltulqdp8ehr <5a968df9a7494b4e019b9ecf739e088ba61097b4312124e9a88ae5b42e3f5f3e@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Dawn <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: npub1hwqy0rnujtl25dzmlhn8qwux4kr8sjhas3ugltx9j5dm5dwkp2dsqjhytw <bb80478e7c92feaa345bfde6703b86ad86784afd84788facc5951bba35d60a9b@buzz.block.builderlab.xyz>
2026-07-22 13:24:46 -07:00

262 lines
8.1 KiB
TypeScript

import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { expect, test } from "@playwright/test";
import { TwoRelayHarness, type RelaySpec } from "./helpers/twoRelayHarness";
const exec = promisify(execFile);
const enabled = process.env.BUZZ_E2E_AGENTS_EVERYWHERE === "1";
const relayBin = process.env.BUZZ_E2E_RELAY_BIN;
const cliBin = process.env.BUZZ_E2E_CLI_BIN;
const adminBin = process.env.BUZZ_E2E_ADMIN_BIN;
function required(name: string, value: string | undefined): string {
if (!value) throw new Error(`${name} is required for the live gate`);
return value;
}
async function run(
binary: string,
args: string[],
env: NodeJS.ProcessEnv = {},
): Promise<string> {
const { stdout } = await exec(binary, args, {
cwd: "..",
env: { ...process.env, BUZZ_AUTH_TAG: "", ...env },
});
return stdout;
}
function keyField(output: string, label: string): string {
const value = output.match(new RegExp(`^${label}:\\s+(\\S+)$`, "m"))?.[1];
if (!value) throw new Error(`missing ${label} in key output`);
return value;
}
async function processTree(
rootPid: number,
): Promise<Array<{ pid: number; rssKb: number; command: string }>> {
const { stdout } = await exec("ps", ["-axo", "pid=,ppid=,rss=,command="]);
const rows = stdout
.trim()
.split("\n")
.map((line) => {
const match = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(.*)$/);
if (!match) return undefined;
return {
pid: Number(match[1]),
ppid: Number(match[2]),
rssKb: Number(match[3]),
command: match[4],
};
})
.filter((row): row is NonNullable<typeof row> => row !== undefined);
const pids = new Set([rootPid]);
let changed = true;
while (changed) {
changed = false;
for (const row of rows) {
if (pids.has(row.ppid) && !pids.has(row.pid)) {
pids.add(row.pid);
changed = true;
}
}
}
return rows
.filter((row) => pids.has(row.pid))
.map(({ pid, rssKb, command }) => ({ pid, rssKb, command }));
}
async function eventually<T>(fn: () => Promise<T | undefined>): Promise<T> {
const deadline = Date.now() + 30_000;
let lastError: unknown;
while (Date.now() < deadline) {
try {
const value = await fn();
if (value !== undefined) return value;
} catch (error) {
lastError = error;
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
throw lastError ?? new Error("live assertion timed out");
}
test.describe("agents everywhere live two-relay gate", () => {
test.skip(!enabled, "set BUZZ_E2E_AGENTS_EVERYWHERE=1 to run live gate");
test("same agent listens and wakes independently in two communities", async () => {
test.setTimeout(90_000);
const portBase = 20_000 + (process.pid % 5_000) * 2;
const specs: [RelaySpec, RelaySpec] = [
{
name: "relay-a",
ports: {
main: portBase,
health: portBase + 10_000,
metrics: portBase + 20_000,
},
databaseUrl: required(
"BUZZ_E2E_DATABASE_URL",
process.env.BUZZ_E2E_DATABASE_URL,
),
redisUrl: process.env.BUZZ_E2E_REDIS_A ?? "redis://127.0.0.1:6379/11",
},
{
name: "relay-b",
ports: {
main: portBase + 1,
health: portBase + 10_001,
metrics: portBase + 20_001,
},
databaseUrl: required(
"BUZZ_E2E_DATABASE_URL",
process.env.BUZZ_E2E_DATABASE_URL,
),
redisUrl: process.env.BUZZ_E2E_REDIS_B ?? "redis://127.0.0.1:6379/12",
},
];
const harness = await TwoRelayHarness.create(specs);
try {
await harness.startRelays(required("BUZZ_E2E_RELAY_BIN", relayBin));
const senderOutput = await run(required("BUZZ_E2E_ADMIN_BIN", adminBin), [
"generate-key",
]);
const agentOutput = await run(required("BUZZ_E2E_ADMIN_BIN", adminBin), [
"generate-key",
]);
const senderKey = keyField(senderOutput, "Secret key");
const agentKey = keyField(agentOutput, "Secret key");
const agentPubkey = keyField(agentOutput, "Public key");
const channels: Array<{ relay: RelaySpec; id: string }> = [];
for (const relay of specs) {
const relayHttp = `http://127.0.0.1:${relay.ports.main}`;
const senderEnv = {
BUZZ_RELAY_URL: relayHttp,
BUZZ_PRIVATE_KEY: senderKey,
};
const created = JSON.parse(
await run(
required("BUZZ_E2E_CLI_BIN", cliBin),
[
"channels",
"create",
"--name",
`ae-live-${relay.name}-${process.pid}`,
"--type",
"stream",
"--visibility",
"open",
],
senderEnv,
),
);
await run(
required("BUZZ_E2E_CLI_BIN", cliBin),
[
"channels",
"add-member",
"--channel",
created.channel_id,
"--pubkey",
agentPubkey,
"--role",
"member",
],
senderEnv,
);
await run(
required("BUZZ_E2E_CLI_BIN", cliBin),
["users", "set-profile", "--name", "AgentsEverywhereProbe"],
{ BUZZ_RELAY_URL: relayHttp, BUZZ_PRIVATE_KEY: agentKey },
);
channels.push({ relay, id: created.channel_id });
}
const acpChildren = [];
for (const { relay } of channels) {
acpChildren.push(
await harness.startAcp(
`acp-${relay.name}`,
`ws://127.0.0.1:${relay.ports.main}`,
agentKey,
{ BUZZ_ACP_RESPOND_TO: "anyone", BUZZ_ACP_NO_MEMORY: "true" },
),
);
}
await new Promise((resolve) => setTimeout(resolve, 2_000));
if (!acpChildren.every((child) => child.exitCode === null)) {
throw new Error(`ACP listener exited early:\n${await harness.logs()}`);
}
const idleTrees = await Promise.all(
acpChildren.map((child) => {
if (!child.pid) throw new Error("ACP listener has no pid");
return processTree(child.pid);
}),
);
expect(idleTrees.every((tree) => tree.length === 1)).toBe(true);
test.info().annotations.push({
type: "idle-processes",
description: idleTrees
.map(
(tree) =>
`${tree.length} process / ${tree.reduce((sum, row) => sum + row.rssKb, 0)} KiB RSS`,
)
.join("; "),
});
for (const { relay, id } of channels) {
const relayHttp = `http://127.0.0.1:${relay.ports.main}`;
await run(
required("BUZZ_E2E_CLI_BIN", cliBin),
[
"messages",
"send",
"--channel",
id,
"--content",
`@AgentsEverywhereProbe AE-ID:${relay.name}`,
],
{ BUZZ_RELAY_URL: relayHttp, BUZZ_PRIVATE_KEY: senderKey },
);
const messages = await eventually(async () => {
const output = await run(
required("BUZZ_E2E_CLI_BIN", cliBin),
["messages", "get", "--channel", id, "--limit", "20"],
{ BUZZ_RELAY_URL: relayHttp, BUZZ_PRIVATE_KEY: senderKey },
);
const rows = JSON.parse(output) as Array<{ content?: string }>;
return rows.some((row) => row.content === `AE-ACK:${relay.name}`)
? rows
: undefined;
});
expect(
messages.filter((row) => row.content === `AE-ACK:${relay.name}`),
).toHaveLength(1);
}
} catch (error) {
console.error(await harness.logs());
throw error;
} finally {
await harness.stop();
await eventually(async () => {
const survivors = (
await Promise.all(
harness.ownedPids.map(async (pid) => {
try {
process.kill(pid, 0);
return pid;
} catch {
return undefined;
}
}),
)
).filter((pid) => pid !== undefined);
return survivors.length === 0 ? true : undefined;
});
}
});
});