feat(mesh): Phase 830 quad composite recovery burst journey

Add disconnectGuildMeshQuad for full 4-peer mesh teardown and
runGuildMeshQuadRecoveryBurstJourney: disconnect all peers, reconnect,
then offline recovery plus sync burst on host and three guests with
meshQuadRecoveryBurstReady verification.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-04 03:55:21 -04:00
co-authored by Cursor
parent 1ea2a2c426
commit e21b7a3e51
3 changed files with 153 additions and 0 deletions
+2
View File
@@ -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 830 (v0.8.797):** `runGuildMeshQuadRecoveryBurstJourney` (4-peer composite recovery burst). Bundle: `npm run test:ci-phase830`.
**Phase 829 (v0.8.796):** `runGuildMeshQuadChurnRejoinJourney` (4-peer single-peer churn rejoin). Bundle: `npm run test:ci-phase829`.
**Phase 828 (v0.8.795):** `runGuildMeshQuadSyncBurstJourney` (4-peer sync burst). Bundle: `npm run test:ci-phase828`.
+117
View File
@@ -7153,6 +7153,123 @@ class HeadlessSession {
}
}
/** Phase 830: 4-peer mesh — full disconnect/reconnect then offline recovery + sync burst on all peers. */
async runGuildMeshQuadRecoveryBurstJourney () {
const {
connectGuildMeshQuad,
disconnectGuildMeshQuad
} = require('./mesh-helpers')
const token = `recovery830-${Date.now()}`
const base = path.join(os.tmpdir(), `pearcord-agentctl-recovery830-${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: 'rechost830', displayName: 'Recovery Host 830' })
await host.createGuild({ name: 'Recovery Burst Quad 830', 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: 'recguest830a', displayName: 'Recovery Guest 830a' })
await guest2.registerUser({ username: 'recguest830b', displayName: 'Recovery Guest 830b' })
await guest3.registerUser({ username: 'recguest830c', displayName: 'Recovery Guest 830c' })
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 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)
const msgWait = { activeChannelMessageIncludes: token }
await guest1.wait(msgWait, 90000)
await guest2.wait(msgWait, 90000)
await guest3.wait(msgWait, 90000)
await disconnectGuildMeshQuad(
host.platform,
guest1.platform,
guest2.platform,
guest3.platform,
20000
)
for (const sess of [guest1, guest2, guest3]) {
await sess.wait({ meshPeersExact: 0 }, 30000)
}
await connectGuildMeshQuad(
host.platform,
guest1.platform,
guest2.platform,
guest3.platform,
55000
)
for (const sess of [host, guest1, guest2, guest3]) {
const gid = sess.platform.guild?.guild?.id
if (gid && typeof sess.platform._refreshGuildSyncHealthCounts === 'function') {
await sess.platform._refreshGuildSyncHealthCounts(gid).catch(() => null)
}
if (typeof sess.platform.runOfflineRecoverySync === 'function') {
await sess.platform.runOfflineRecoverySync().catch(() => null)
}
if (typeof sess.platform.requestGuildSyncBurst === 'function') {
sess.platform.requestGuildSyncBurst({ clearPushHalt: true })
} else if (typeof sess.platform.requestGuildSyncFanout === 'function') {
sess.platform.requestGuildSyncFanout()
}
}
const readyWait = {
meshQuadRecoveryBurstReady: true,
meshPushNotHalted: true
}
const msgReady = { ...readyWait, activeChannelMessageIncludes: token }
await host.wait(msgReady, 90000)
await guest1.wait(msgReady, 90000)
await guest2.wait(msgReady, 90000)
await guest3.wait(msgReady, 90000)
const hv = await host.refreshView()
const g1v = await guest1.refreshView()
const g2v = await guest2.refreshView()
const g3v = await guest3.refreshView()
return {
token,
hostPeers: hv.stats?.peers,
guest1Peers: g1v.stats?.peers,
guest2Peers: g2v.stats?.peers,
guest3Peers: g3v.stats?.peers,
guest1Outbox: g1v.guildSyncHealth?.pendingGossipCount,
guest2Outbox: g2v.guildSyncHealth?.pendingGossipCount,
guest3Outbox: g3v.guildSyncHealth?.pendingGossipCount,
guest3PushHalted: g3v.meshStabilityStats?.pushHalted
}
} finally {
await host.close()
await guest1.close()
await guest2.close()
await guest3.close()
}
}
/** Phase 829: 4-peer mesh — drop one guest, survivors stay synced, rejoin + recovery burst. */
async runGuildMeshQuadChurnRejoinJourney () {
const {
+34
View File
@@ -252,6 +252,39 @@ async function connectGuildMeshQuad (
}
}
/**
* Drop all direct guild mesh links in a 4-peer group (for quad reconnect journeys).
*/
async function disconnectGuildMeshQuad (
host,
guest1,
guest2,
guest3,
timeoutMs = 18000
) {
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.leavePeer(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) === 0)
}, timeoutMs, 350)
if (!ok) throw new Error('4-peer mesh did not disconnect')
await sleep(800)
return { hostPeers: 0, guest1Peers: 0, guest2Peers: 0, guest3Peers: 0 }
}
/**
* Drop one peer from a 4-peer guild mesh; the other three should remain connected.
* @param {number} dropIndex 0=host, 1=guest1, 2=guest2, 3=guest3
@@ -358,6 +391,7 @@ module.exports = {
disconnectGuildMeshTripleOnePeer,
reconnectGuildMeshTripleOnePeer,
connectGuildMeshQuad,
disconnectGuildMeshQuad,
disconnectGuildMeshQuadOnePeer,
reconnectGuildMeshQuadOnePeer,
createIsolatedMeshDht