From 9e343093169629907a5383ba03f73e2f30dd4c5d Mon Sep 17 00:00:00 2001 From: Arseniy Klempner Date: Mon, 13 Oct 2025 09:24:03 -0700 Subject: [PATCH] feat: add command to test dev env --- packages/run/scripts/start.ts | 3 + packages/run/scripts/test.ts | 118 +++++++++++++++++++++++++++++++ packages/run/src/cli.ts | 4 +- packages/run/src/test-client.ts | 116 ++++++++++++++++++++++++++++++ packages/run/tests/basic.spec.ts | 65 +++++------------ 5 files changed, 256 insertions(+), 50 deletions(-) create mode 100644 packages/run/scripts/test.ts create mode 100644 packages/run/src/test-client.ts diff --git a/packages/run/scripts/start.ts b/packages/run/scripts/start.ts index ead7b3b799..ea21510fb6 100755 --- a/packages/run/scripts/start.ts +++ b/packages/run/scripts/start.ts @@ -167,6 +167,9 @@ try { const isPublished = __dirname.includes("dist"); const cmdPrefix = isPublished ? "npx @waku/run" : "npm run"; + process.stdout.write( + ` ${colors.cyan}${cmdPrefix} test${colors.reset} - Test network with a message\n` + ); process.stdout.write( ` ${colors.cyan}${cmdPrefix} logs${colors.reset} - View logs\n` ); diff --git a/packages/run/scripts/test.ts b/packages/run/scripts/test.ts new file mode 100644 index 0000000000..c52ca2b9b3 --- /dev/null +++ b/packages/run/scripts/test.ts @@ -0,0 +1,118 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +import { Protocols } from "@waku/sdk"; + +import { WakuTestClient } from "../src/test-client.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +// In development: scripts are in packages/run/scripts +// In published package: scripts are in node_modules/@waku/run/dist/scripts +const packageRoot = __dirname.includes("dist") + ? join(__dirname, "..", "..") + : join(__dirname, ".."); + +interface Colors { + reset: string; + cyan: string; + green: string; + red: string; + yellow: string; +} + +// ANSI color codes +const colors: Colors = { + reset: "\x1b[0m", + cyan: "\x1b[36m", + green: "\x1b[32m", + red: "\x1b[31m", + yellow: "\x1b[33m" +}; + +async function main(): Promise { + let client: WakuTestClient | null = null; + + try { + // Check if containers are running + const output: string = execSync("docker compose ps --quiet", { + cwd: packageRoot, + encoding: "utf-8" + }).trim(); + + if (!output) { + process.stderr.write( + `${colors.red}✗${colors.reset} No nodes running. Start with: ${colors.cyan}npx @waku/run start${colors.reset}\n` + ); + process.exit(1); + } + + process.stdout.write( + `${colors.cyan}Testing local Waku network...${colors.reset}\n\n` + ); + + // Step 1: Create client + process.stdout.write( + `${colors.cyan}→${colors.reset} Creating Waku light node...\n` + ); + client = new WakuTestClient(); + + // Step 2: Start and connect + process.stdout.write(`${colors.cyan}→${colors.reset} Starting node...\n`); + await client.start(); + + // Step 3: Wait for peers + process.stdout.write( + `${colors.cyan}→${colors.reset} Waiting for peers...\n` + ); + await client.waku!.waitForPeers([Protocols.LightPush]); + const connectedPeers = client.waku!.libp2p.getPeers().length; + process.stdout.write( + `${colors.green}✓${colors.reset} Connected to ${connectedPeers} peer(s)\n` + ); + + // Step 4: Send test message + process.stdout.write( + `${colors.cyan}→${colors.reset} Sending lightpush message...\n` + ); + const result = await client.sendTestMessage("Test from @waku/run"); + + if (result.success) { + process.stdout.write( + `${colors.green}✓${colors.reset} Message sent successfully to ${result.messagesSent} peer(s)\n` + ); + process.stdout.write( + `\n${colors.green}✓ All tests passed!${colors.reset}\n` + ); + process.stdout.write( + `${colors.cyan}The local Waku network is working correctly.${colors.reset}\n` + ); + } else { + process.stderr.write( + `${colors.red}✗${colors.reset} Failed to send message: ${result.error || "Unknown error"}\n` + ); + process.stderr.write( + ` Sent: ${result.messagesSent}, Failed: ${result.failures}\n` + ); + process.exit(1); + } + } catch (error: unknown) { + const err = error as { message?: string }; + process.stderr.write( + `${colors.red}✗${colors.reset} Test failed: ${err.message || String(error)}\n` + ); + process.exit(1); + } finally { + if (client) { + await client.stop(); + } + } +} + +main().catch((error) => { + process.stderr.write(`Unexpected error: ${String(error)}\n`); + process.exit(1); +}); diff --git a/packages/run/src/cli.ts b/packages/run/src/cli.ts index c47f6424dc..55fe2161c1 100644 --- a/packages/run/src/cli.ts +++ b/packages/run/src/cli.ts @@ -13,7 +13,8 @@ const scriptMap: Record = { start: join(__dirname, "..", "scripts", "start.js"), stop: join(__dirname, "..", "scripts", "stop.js"), info: join(__dirname, "..", "scripts", "info.js"), - logs: join(__dirname, "..", "scripts", "logs.js") + logs: join(__dirname, "..", "scripts", "logs.js"), + test: join(__dirname, "..", "scripts", "test.js") }; if (!command || !scriptMap[command]) { @@ -24,6 +25,7 @@ if (!command || !scriptMap[command]) { process.stderr.write(" stop Stop the local Waku network\n"); process.stderr.write(" info Show connection info for running network\n"); process.stderr.write(" logs View logs from running network\n"); + process.stderr.write(" test Test the network by sending a message\n"); process.exit(1); } diff --git a/packages/run/src/test-client.ts b/packages/run/src/test-client.ts new file mode 100644 index 0000000000..3bc23e1611 --- /dev/null +++ b/packages/run/src/test-client.ts @@ -0,0 +1,116 @@ +import { createEncoder } from "@waku/core"; +import type { LightNode } from "@waku/interfaces"; +import { createLightNode } from "@waku/sdk"; +import { createRoutingInfo } from "@waku/utils"; + +import { NODE1_PEER_ID, NODE2_PEER_ID } from "./constants.js"; + +export interface WakuTestClientOptions { + node1Port?: string; + node2Port?: string; + clusterId?: number; + numShardsInCluster?: number; + contentTopic?: string; +} + +export interface TestResult { + success: boolean; + connectedPeers: number; + messagesSent: number; + failures: number; + error?: string; +} + +export class WakuTestClient { + public waku: LightNode | null = null; + private options: Required; + + public constructor(options: WakuTestClientOptions = {}) { + this.options = { + node1Port: options.node1Port || process.env.NODE1_WS_PORT || "60000", + node2Port: options.node2Port || process.env.NODE2_WS_PORT || "60001", + clusterId: options.clusterId ?? 0, + numShardsInCluster: options.numShardsInCluster ?? 8, + contentTopic: options.contentTopic || "/waku-run/1/test/proto" + }; + } + + /** + * Create and start the Waku light node + */ + public async start(): Promise { + const { node1Port, node2Port, clusterId, numShardsInCluster } = + this.options; + + const networkConfig = { + clusterId, + numShardsInCluster + }; + + this.waku = await createLightNode({ + defaultBootstrap: false, + bootstrapPeers: [ + `/ip4/127.0.0.1/tcp/${node1Port}/ws/p2p/${NODE1_PEER_ID}`, + `/ip4/127.0.0.1/tcp/${node2Port}/ws/p2p/${NODE2_PEER_ID}` + ], + networkConfig, + numPeersToUse: 2, + libp2p: { + filterMultiaddrs: false + } + }); + + await this.waku.start(); + } + + /** + * Send a test message via lightpush + */ + public async sendTestMessage( + payload: string = "Hello Waku!" + ): Promise { + if (!this.waku) { + throw new Error("Waku node not started. Call start() first."); + } + + try { + const { contentTopic, clusterId, numShardsInCluster } = this.options; + const networkConfig = { clusterId, numShardsInCluster }; + + const routingInfo = createRoutingInfo(networkConfig, { contentTopic }); + const encoder = createEncoder({ contentTopic, routingInfo }); + + const result = await this.waku.lightPush.send(encoder, { + payload: new TextEncoder().encode(payload) + }); + + const connectedPeers = this.waku.libp2p.getPeers().length; + + return { + success: + result.successes.length > 0 && (result.failures?.length || 0) === 0, + connectedPeers, + messagesSent: result.successes.length, + failures: result.failures?.length || 0 + }; + } catch (error) { + return { + success: false, + connectedPeers: this.waku.libp2p.getPeers().length, + messagesSent: 0, + failures: 0, + error: error instanceof Error ? error.message : String(error) + }; + } + } + + /** + * Stop the Waku node + */ + public async stop(): Promise { + if (this.waku) { + await this.waku.stop(); + this.waku = null; + } + } +} diff --git a/packages/run/tests/basic.spec.ts b/packages/run/tests/basic.spec.ts index 2b42255fbc..c1837a8828 100644 --- a/packages/run/tests/basic.spec.ts +++ b/packages/run/tests/basic.spec.ts @@ -1,17 +1,14 @@ import { execSync } from "child_process"; -import { createEncoder } from "@waku/core"; -import type { LightNode } from "@waku/interfaces"; -import { createLightNode, Protocols } from "@waku/sdk"; -import { createRoutingInfo } from "@waku/utils"; +import { Protocols } from "@waku/sdk"; import { expect } from "chai"; -import { NODE1_PEER_ID, NODE2_PEER_ID } from "../src/constants.js"; +import { WakuTestClient } from "../src/test-client.js"; describe("Waku Run - Basic Test", function () { this.timeout(90000); - let waku: LightNode; + let client: WakuTestClient; before(async function () { // Step 1: Start the nodes @@ -65,8 +62,8 @@ describe("Waku Run - Basic Test", function () { after(async function () { // Step 4: Stop the nodes - if (waku) { - await waku.stop(); + if (client) { + await client.stop(); } execSync("docker compose down", { stdio: "inherit" @@ -74,59 +71,29 @@ describe("Waku Run - Basic Test", function () { }); it("should connect to both nodes and send lightpush message to both peers", async function () { - // Step 2: Connect to nodes via js-waku - const node1Port = process.env.NODE1_WS_PORT || "60000"; - const node2Port = process.env.NODE2_WS_PORT || "60001"; - - // Static peer IDs from --nodekey configuration - // cspell:ignore nodekey - const peer1 = NODE1_PEER_ID; - const peer2 = NODE2_PEER_ID; - - const networkConfig = { - clusterId: 0, - numShardsInCluster: 8 - }; - - waku = await createLightNode({ - defaultBootstrap: false, - bootstrapPeers: [ - `/ip4/127.0.0.1/tcp/${node1Port}/ws/p2p/${peer1}`, - `/ip4/127.0.0.1/tcp/${node2Port}/ws/p2p/${peer2}` - ], - networkConfig, - numPeersToUse: 2, // Use both peers for sending - libp2p: { - filterMultiaddrs: false - } + // Step 2: Connect to nodes via js-waku using WakuTestClient + client = new WakuTestClient({ + contentTopic: "/test/1/basic/proto" }); - await waku.start(); + await client.start(); // Wait for both peers to be connected - await waku.waitForPeers([Protocols.LightPush]); - - // Verify we're connected to both peers - const connectedPeers = waku.libp2p.getPeers(); - expect(connectedPeers.length).to.equal( + await client.waku!.waitForPeers([Protocols.LightPush]); + const connectedPeers = client.waku!.libp2p.getPeers().length; + expect(connectedPeers).to.equal( 2, "Should be connected to both nwaku nodes" ); // Step 3: Send lightpush message - it should be sent to both peers - const contentTopic = "/test/1/basic/proto"; - const routingInfo = createRoutingInfo(networkConfig, { contentTopic }); - const encoder = createEncoder({ contentTopic, routingInfo }); + const result = await client.sendTestMessage("Hello Waku!"); - const result = await waku.lightPush.send(encoder, { - payload: new TextEncoder().encode("Hello Waku!") - }); - - // With numPeersToUse=2, the message should be sent to both peers - expect(result.successes.length).to.equal( + expect(result.success).to.be.true; + expect(result.messagesSent).to.equal( 2, "Message should be sent to both peers" ); - expect(result.failures?.length || 0).to.equal(0, "Should have no failures"); + expect(result.failures).to.equal(0, "Should have no failures"); }); });