Files
buzz/desktop/tests/e2e/agents-everywhere.live.spec.ts
T
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08: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;
});
}
});
});