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 <[email protected]>
This commit is contained in:
@@ -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.
|
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 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`.
|
**Phase 825 (v0.8.792):** `runGuildMeshTripleRecoveryBurstJourney` (3-peer composite recovery burst). Bundle: `npm run test:ci-phase825`.
|
||||||
|
|||||||
@@ -102,6 +102,16 @@ function matchView (view, spec) {
|
|||||||
if (rail?.meshBounded) return false
|
if (rail?.meshBounded) return false
|
||||||
continue
|
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 (key === 'presencePeersMin') {
|
||||||
if ((view?.presence?.peers || []).length < Number(expected)) return false
|
if ((view?.presence?.peers || []).length < Number(expected)) return false
|
||||||
continue
|
continue
|
||||||
|
|||||||
+78
@@ -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. */
|
/** Phase 826: 3-peer mesh — drop one guest, survivors stay synced, rejoin + recovery burst. */
|
||||||
async runGuildMeshTripleChurnRejoinJourney () {
|
async runGuildMeshTripleChurnRejoinJourney () {
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -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 */
|
/** @returns {null} isolated DHT wiring is optional for headless IPC smokes */
|
||||||
function createIsolatedMeshDht () {
|
function createIsolatedMeshDht () {
|
||||||
return null
|
return null
|
||||||
@@ -227,5 +266,6 @@ module.exports = {
|
|||||||
disconnectGuildMeshTriple,
|
disconnectGuildMeshTriple,
|
||||||
disconnectGuildMeshTripleOnePeer,
|
disconnectGuildMeshTripleOnePeer,
|
||||||
reconnectGuildMeshTripleOnePeer,
|
reconnectGuildMeshTripleOnePeer,
|
||||||
|
connectGuildMeshQuad,
|
||||||
createIsolatedMeshDht
|
createIsolatedMeshDht
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user