Skip to content

Commit 070c86c

Browse files
fix(tests): add relay-only peer nwaku so mesh forms before tests publish
Tests built on `runNodes` (Store, Filter single-node, etc.) start a single nwaku container and a js-waku LightNode. LightNodes never join the gossipsub mesh, so the nwaku ends up with zero mesh peers on the test topic. After nwaku PR #3669 (Jan 2026) the REST endpoint `POST /relay/v1/auto/messages` now returns HTTP 400 with `NoPeersToPublish` instead of silently succeeding in that state, which causes `ServiceNode.sendMessage()` to return `false` and fails every test that seeds the store via REST publish. That broke ~50/56 of the failing assertions in the nwaku `js-waku-node` CI job and the job has been red on every master run since. Fix the test harness rather than reverting the (correct) nwaku change: - `runNodes` now starts a second relay-only `ServiceNode` chained to the primary via `--staticnode`, subscribed to the same shards/topics. Once the gossipsub mesh GRAFTs, REST publishes on the primary have a real mesh peer and succeed. - The peer's lifecycle is bound to the primary through a new private `companion` field + `setCompanion()` getter; existing `tearDownNodes` calls keep working unchanged. - Add `ServiceNode.waitForMeshPeers(pubsubTopics, {timeoutMs})` polling `/admin/v1/peers/mesh`, called before `runNodes` returns so the first publish doesn't race the heartbeat. Doesn't touch `ServiceNodesFleet` (already multi-node); doesn't address the Filter Multiple-Service-Nodes duplication or Peer Exchange compliance timeouts surfaced in the same run — those are separate bugs.
1 parent 0ca94f3 commit 070c86c

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

packages/tests/src/lib/runNodes.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ export async function runNodes<T>(
4545
const { context, networkConfig, createNode, protocols } = options;
4646

4747
const nwaku = new ServiceNode(makeLogFileName(context));
48+
// Mesh peer: gives `nwaku` at least one gossipsub mesh peer so that REST
49+
// publishes (e.g. used by store tests to seed messages) don't fail with
50+
// NoPeersToPublish. Relay-only — no store/filter/lightpush needed.
51+
const nwakuPeer = new ServiceNode(makeLogFileName(context) + "-peer");
4852

4953
const nwakuArgs: Args = {
5054
filter: true,
@@ -94,6 +98,25 @@ export async function runNodes<T>(
9498

9599
await nwaku.start(nwakuArgs, { retries: 3 });
96100

101+
// Spin up a relay-only peer subscribed to the same shards/topics and chain
102+
// it to `nwaku` via --staticnode. Once the mesh forms, REST publishes on
103+
// `nwaku` have at least one mesh peer and succeed. Bind its lifecycle to
104+
// `nwaku` so existing tearDownNodes(nwaku, ...) calls stop both.
105+
const nwakuPeerArgs: Args = {
106+
relay: true,
107+
clusterId: networkConfig.clusterId,
108+
staticnode: await nwaku.getExternalMultiaddr()
109+
};
110+
if (isAutoSharding(networkConfig)) {
111+
nwakuPeerArgs.numShardsInNetwork = networkConfig.numShardsInCluster;
112+
nwakuPeerArgs.contentTopic = options.contentTopics ?? [];
113+
} else if (isStaticSharding(networkConfig) && options.relayShards) {
114+
nwakuPeerArgs.shard = options.relayShards;
115+
nwakuPeerArgs.numShardsInNetwork = 0;
116+
}
117+
await nwakuPeer.start(nwakuPeerArgs, { retries: 3 });
118+
nwaku.setCompanion(nwakuPeer);
119+
97120
log.info("Starting js waku node with :", JSON.stringify(jswakuArgs));
98121
let waku: WakuNode | undefined;
99122
try {
@@ -110,7 +133,15 @@ export async function runNodes<T>(
110133
await waku.dial(await nwaku.getMultiaddrWithId());
111134
await waku.waitForPeers(protocols);
112135

113-
await nwaku.ensureSubscriptions(routingInfos.map((r) => r.pubsubTopic));
136+
const pubsubTopics = routingInfos.map((r) => r.pubsubTopic);
137+
await nwaku.ensureSubscriptions(pubsubTopics);
138+
// Peer must also be subscribed for the gossipsub mesh on `pubsubTopics`
139+
// to form between the two nwaku nodes.
140+
await nwakuPeer.ensureSubscriptions(pubsubTopics);
141+
// Give gossipsub a heartbeat (~1s) to GRAFT both nodes into the mesh
142+
// before any test publishes. Without this, the first publish can still
143+
// race the mesh and fail with NoPeersToPublish.
144+
await nwaku.waitForMeshPeers(pubsubTopics, { timeoutMs: 5000 });
114145

115146
return [nwaku, waku as T];
116147
} else {

packages/tests/src/lib/service_node.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ export class ServiceNode {
6060
private readonly logPath: string;
6161
private restPort?: number;
6262
private args?: Args;
63+
// Optional companion node started by runNodes() to give the primary nwaku
64+
// a gossipsub mesh peer. Its lifecycle is bound to this node's stop().
65+
private companion?: ServiceNode;
6366

6467
public readonly version: NwakuVersion | undefined;
6568

@@ -206,12 +209,58 @@ export class ServiceNode {
206209
public async stop(): Promise<void> {
207210
await this.docker?.stop();
208211
delete this.docker;
212+
if (this.companion) {
213+
await this.companion.stop();
214+
delete this.companion;
215+
}
216+
}
217+
218+
/**
219+
* Used by runNodes() to attach a relay-only mesh peer to this node, so
220+
* that REST `/relay/v1/auto/messages` publishes on this node have at least
221+
* one gossipsub mesh peer and don't fail with NoPeersToPublish.
222+
*/
223+
public setCompanion(node: ServiceNode): void {
224+
this.companion = node;
209225
}
210226

211227
public async waitForLog(msg: string, timeout: number): Promise<void> {
212228
return waitForLine(this.logPath, msg, timeout);
213229
}
214230

231+
/**
232+
* Polls /admin/v1/peers/mesh until every shard corresponding to the given
233+
* pubsub topics has at least one mesh peer. Used by runNodes() to ensure
234+
* the gossipsub mesh is grafted before tests publish.
235+
*/
236+
public async waitForMeshPeers(
237+
pubsubTopics: PubsubTopic[],
238+
options: { timeoutMs?: number; intervalMs?: number } = {}
239+
): Promise<void> {
240+
const timeoutMs = options.timeoutMs ?? 5000;
241+
const intervalMs = options.intervalMs ?? 200;
242+
const deadline = Date.now() + timeoutMs;
243+
244+
while (Date.now() < deadline) {
245+
const mesh = await this.restCall<{ shard: number; peers: unknown[] }[]>(
246+
"/admin/v1/peers/mesh",
247+
"GET",
248+
undefined,
249+
async (response) => {
250+
if (response.status !== 200) return [];
251+
const data = await response.json();
252+
return Array.isArray(data) ? data : [];
253+
}
254+
);
255+
const haveAnyMesh = mesh.some((ps) => (ps.peers?.length ?? 0) > 0);
256+
if (haveAnyMesh) return;
257+
await delay(intervalMs);
258+
}
259+
log.warn(
260+
`Timed out after ${timeoutMs}ms waiting for gossipsub mesh peers on ${pubsubTopics.join(", ")}`
261+
);
262+
}
263+
215264
/**
216265
* Calls nwaku REST API "/admin/v1/peers" to check for known peers. Be aware that it doesn't recognize js-waku as a node
217266
* @throws

0 commit comments

Comments
 (0)