feat(cli): websocket client + MCP tool integration
broker-client: full WS client with hello handshake + ack, auto-reconnect with exponential backoff (1s → 30s capped), in-memory outbound queue (max 100) during reconnect, 500-entry push buffer for check_messages. MCP tool integration: - send_message: "slug:target" prefix or single-mesh fast path - check_messages: drains push buffers across all clients - set_status: fans manual override across all connected meshes - set_summary: stubbed (broker protocol extension needed) - list_peers: stubbed — lists connected mesh slugs + statuses manager module holds Map<meshId, BrokerClient>, starts on MCP server boot for every joined mesh in ~/.claudemesh/config.json. new CLI command: seed-test-mesh injects a mesh row for dev testing. also fixes a broker-side hello race: handleHello sent hello_ack before the caller closure assigned presenceId, so clients sending right after the ack hit the no_hello check. Fix: return presenceId, caller sets closure var, THEN sends hello_ack. Queue drain is fire-and-forget now. round-trip verified: two clients, A→B, push received with correct senderPubkey + ciphertext. 44/44 broker tests still pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
81
apps/cli/scripts/roundtrip.ts
Normal file
81
apps/cli/scripts/roundtrip.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* End-to-end round-trip: two BrokerClient instances talking via the
|
||||
* broker. Runs against a live broker + seeded DB.
|
||||
*
|
||||
* Reads /tmp/cli-seed.json (output of broker's scripts/seed-test-mesh.ts),
|
||||
* connects peer A and peer B, sends a message from A to B, waits for
|
||||
* the push on B, asserts receipt + sender pubkey.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { BrokerClient } from "../src/ws/client";
|
||||
import type { JoinedMesh } from "../src/state/config";
|
||||
|
||||
const seed = JSON.parse(readFileSync("/tmp/cli-seed.json", "utf-8")) as {
|
||||
meshId: string;
|
||||
peerA: { memberId: string; pubkey: string };
|
||||
peerB: { memberId: string; pubkey: string };
|
||||
};
|
||||
|
||||
const brokerUrl = process.env.BROKER_WS_URL ?? "ws://localhost:7900/ws";
|
||||
const meshA: JoinedMesh = {
|
||||
meshId: seed.meshId,
|
||||
memberId: seed.peerA.memberId,
|
||||
slug: "rt-a",
|
||||
name: "roundtrip-a",
|
||||
pubkey: seed.peerA.pubkey,
|
||||
secretKey: "stub",
|
||||
brokerUrl,
|
||||
joinedAt: new Date().toISOString(),
|
||||
};
|
||||
const meshB: JoinedMesh = { ...meshA, memberId: seed.peerB.memberId, slug: "rt-b", pubkey: seed.peerB.pubkey };
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const a = new BrokerClient(meshA, { debug: true });
|
||||
const b = new BrokerClient(meshB, { debug: true });
|
||||
|
||||
let received: string | null = null;
|
||||
let receivedSender: string | null = null;
|
||||
b.onPush((msg) => {
|
||||
received = Buffer.from(msg.ciphertext, "base64").toString("utf-8");
|
||||
receivedSender = msg.senderPubkey;
|
||||
console.log(`[b] push: "${received}" from ${receivedSender}`);
|
||||
});
|
||||
|
||||
console.log("[rt] connecting A + B…");
|
||||
await Promise.all([a.connect(), b.connect()]);
|
||||
console.log(`[rt] A: ${a.status}, B: ${b.status}`);
|
||||
|
||||
console.log("[rt] A → B …");
|
||||
const result = await a.send(seed.peerB.pubkey, "hello from A", "now");
|
||||
console.log("[rt] send result:", result);
|
||||
|
||||
// Wait up to 3s for the push to land.
|
||||
for (let i = 0; i < 30 && !received; i++) {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
}
|
||||
|
||||
a.close();
|
||||
b.close();
|
||||
|
||||
if (!received) {
|
||||
console.error("✗ FAIL: no push received");
|
||||
process.exit(1);
|
||||
}
|
||||
if (received !== "hello from A") {
|
||||
console.error(`✗ FAIL: body mismatch: "${received}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (receivedSender !== seed.peerA.pubkey) {
|
||||
console.error(`✗ FAIL: sender mismatch: "${receivedSender}"`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("✓ round-trip PASSED");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("✗ FAIL:", e instanceof Error ? e.message : e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user