From 922cfe71364ad74d4f3994fbcc5254ce12627a7a Mon Sep 17 00:00:00 2001 From: Raven Scott Date: Thu, 4 Jun 2026 03:36:54 -0400 Subject: [PATCH] Phase 827: guild 4-peer mesh message fanout (v0.8.794) Add connectGuildMeshQuad, runGuildMeshQuadMessageJourney, and meshQuadReady matcher for host + three guest cross-join fanout tests. Co-authored-by: Cursor --- README.md | 2 ++ assert.js | 10 +++++++ headless.js | 78 +++++++++++++++++++++++++++++++++++++++++++++++++ mesh-helpers.js | 40 +++++++++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/README.md b/README.md index c22268d..689845c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Agent control plane for Pearcord — send the same IPC messages the UI sends, read `view` snapshots, and run scripted journeys against a **live worker** or an in-process **headless** session. +**Phase 827 (v0.8.794):** `runGuildMeshQuadMessageJourney` (4-peer mesh message fanout). Bundle: `npm run test:ci-phase827`. + **Phase 826 (v0.8.793):** `runGuildMeshTripleChurnRejoinJourney` (3-peer single-peer churn rejoin). Bundle: `npm run test:ci-phase826`. **Phase 825 (v0.8.792):** `runGuildMeshTripleRecoveryBurstJourney` (3-peer composite recovery burst). Bundle: `npm run test:ci-phase825`. diff --git a/assert.js b/assert.js index b3a256d..a42dd95 100644 --- a/assert.js +++ b/assert.js @@ -102,6 +102,16 @@ function matchView (view, spec) { if (rail?.meshBounded) return false continue } + if (key === 'meshQuadReady') { + if (!expected) return true + const peers = Number(view?.stats?.peers) || 0 + if (peers < 2) return false + if (view?.meshStabilityStats?.pushHalted) return false + const gid = view?.guild?.id + const rail = gid ? view.guildSyncHealthRail?.[gid] : null + if (rail?.meshBounded) return false + continue + } if (key === 'presencePeersMin') { if ((view?.presence?.peers || []).length < Number(expected)) return false continue diff --git a/headless.js b/headless.js index a73d87c..64dcb42 100644 --- a/headless.js +++ b/headless.js @@ -7153,6 +7153,84 @@ class HeadlessSession { } } + /** Phase 827: 4-peer mesh — host message visible on all three guests after quad connect. */ + async runGuildMeshQuadMessageJourney () { + const { connectGuildMeshQuad } = require('./mesh-helpers') + const token = `quad827-${Date.now()}` + const base = path.join(os.tmpdir(), `pearcord-agentctl-quad827-${Date.now()}`) + const host = new HeadlessSession({ storagePath: path.join(base, 'host') }) + const guest1 = new HeadlessSession({ storagePath: path.join(base, 'guest1') }) + const guest2 = new HeadlessSession({ storagePath: path.join(base, 'guest2') }) + const guest3 = new HeadlessSession({ storagePath: path.join(base, 'guest3') }) + await host.start() + await guest1.start() + await guest2.start() + await guest3.start() + try { + await host.registerUser({ username: 'quadhost827', displayName: 'Quad Host 827' }) + await host.createGuild({ name: 'Quad Mesh 827', publicListing: false }) + await host.wait({ mode: 'guild' }, 45000) + const invite = await host.platform.createInvite().catch(() => null) + const code = invite?.shareCode || invite?.code + if (!code) throw new Error('host invite missing') + await guest1.registerUser({ username: 'quadguest827a', displayName: 'Quad Guest 827a' }) + await guest2.registerUser({ username: 'quadguest827b', displayName: 'Quad Guest 827b' }) + await guest3.registerUser({ username: 'quadguest827c', displayName: 'Quad Guest 827c' }) + await guest1.ipc({ type: 'join-invite', code }) + await guest2.ipc({ type: 'join-invite', code }) + await guest3.ipc({ type: 'join-invite', code }) + await guest1.wait({ mode: 'guild' }, 45000) + await guest2.wait({ mode: 'guild' }, 45000) + await guest3.wait({ mode: 'guild' }, 45000) + const chId = (await host.refreshView()).activeChannelId + if (!chId) throw new Error('active channel missing') + for (const sess of [guest1, guest2, guest3]) { + await sess.ipc({ type: 'select-channel', channelId: chId }) + await sess.wait({ activeChannelId: chId }, 30000) + } + const mesh = await connectGuildMeshQuad( + host.platform, + guest1.platform, + guest2.platform, + guest3.platform, + 55000 + ) + const quadWait = { meshQuadReady: true, membersMin: 4 } + await host.wait(quadWait, 30000) + await guest1.wait(quadWait, 30000) + await guest2.wait(quadWait, 30000) + await guest3.wait(quadWait, 30000) + await host.sendGuildMessage(token) + for (const p of [host.platform, guest1.platform, guest2.platform, guest3.platform]) { + if (typeof p.requestGuildSyncFanout === 'function') { + p.requestGuildSyncFanout() + } + } + const msgWait = { meshQuadReady: true, activeChannelMessageIncludes: token } + await guest1.wait(msgWait, 90000) + await guest2.wait(msgWait, 90000) + await guest3.wait(msgWait, 90000) + const hv = await host.refreshView() + const g1v = await guest1.refreshView() + const g2v = await guest2.refreshView() + const g3v = await guest3.refreshView() + return { + token, + channelId: chId, + hostPeers: mesh.hostPeers ?? hv.stats?.peers ?? 0, + guest1Peers: mesh.guest1Peers ?? g1v.stats?.peers ?? 0, + guest2Peers: mesh.guest2Peers ?? g2v.stats?.peers ?? 0, + guest3Peers: mesh.guest3Peers ?? g3v.stats?.peers ?? 0, + memberCount: (hv.members || []).length + } + } finally { + await host.close() + await guest1.close() + await guest2.close() + await guest3.close() + } + } + /** Phase 826: 3-peer mesh — drop one guest, survivors stay synced, rejoin + recovery burst. */ async runGuildMeshTripleChurnRejoinJourney () { const { diff --git a/mesh-helpers.js b/mesh-helpers.js index 98e7847..3f29fc5 100644 --- a/mesh-helpers.js +++ b/mesh-helpers.js @@ -213,6 +213,45 @@ async function reconnectGuildMeshTripleOnePeer ( } } +/** + * Cross-join host + three guests on the guild Hyperswarm mesh (4-peer fanout tests). + */ +async function connectGuildMeshQuad ( + host, + guest1, + guest2, + guest3, + timeoutMs = 50000 +) { + const peers = [host, guest1, guest2, guest3] + for (const p of peers) { + if (!p?.guild?.swarm) throw new Error('all peers must be on an active guild mesh') + } + const keys = peers.map((p) => p.guild.swarm.keyPair.publicKey) + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + if (i === j) continue + peers[i].guild.swarm.joinPeer(keys[j]) + } + } + for (const p of peers) { + await p.guild.swarm.flush() + } + const ok = await waitFor(async () => { + const views = await Promise.all(peers.map((p) => p.view())) + return views.every((v) => (v.stats?.peers || 0) >= 2) + }, timeoutMs, 450) + if (!ok) throw new Error('4-peer mesh did not connect') + await sleep(1800) + const views = await Promise.all(peers.map((p) => p.view())) + return { + hostPeers: views[0].stats?.peers, + guest1Peers: views[1].stats?.peers, + guest2Peers: views[2].stats?.peers, + guest3Peers: views[3].stats?.peers + } +} + /** @returns {null} isolated DHT wiring is optional for headless IPC smokes */ function createIsolatedMeshDht () { return null @@ -227,5 +266,6 @@ module.exports = { disconnectGuildMeshTriple, disconnectGuildMeshTripleOnePeer, reconnectGuildMeshTripleOnePeer, + connectGuildMeshQuad, createIsolatedMeshDht }