Phase 788: mesh reconnect journey and disconnect helper (v0.8.755)
Add disconnectGuildMeshPeers, runGuildMeshReconnectJourney, and matchers meshReconnectReady and meshPeersExact. 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 788 (v0.8.755):** `runGuildMeshReconnectJourney` (2-peer disconnect/reconnect). Bundle: `npm run test:ci-phase788`.
|
||||||
|
|
||||||
**Phase 787 (v0.8.754):** `runGuildMeshMemberRolesJourney` (2-peer custom role assign). Bundle: `npm run test:ci-phase787`.
|
**Phase 787 (v0.8.754):** `runGuildMeshMemberRolesJourney` (2-peer custom role assign). Bundle: `npm run test:ci-phase787`.
|
||||||
|
|
||||||
**Phase 786 (v0.8.753):** `runGuildMeshTypingJourney` (2-peer typing gossip). Bundle: `npm run test:ci-phase786`.
|
**Phase 786 (v0.8.753):** `runGuildMeshTypingJourney` (2-peer typing gossip). Bundle: `npm run test:ci-phase786`.
|
||||||
|
|||||||
@@ -94,6 +94,20 @@ function matchView (view, spec) {
|
|||||||
if ((view?.guildCustomRoles || []).length < Number(expected)) return false
|
if ((view?.guildCustomRoles || []).length < Number(expected)) return false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (key === 'meshReconnectReady') {
|
||||||
|
if (!expected) return true
|
||||||
|
const gid = view?.guild?.id
|
||||||
|
const rail = gid ? view.guildSyncHealthRail?.[gid] : null
|
||||||
|
const peers = Number(view?.stats?.peers) || 0
|
||||||
|
if (peers < 1) return false
|
||||||
|
if (rail?.meshBounded) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (key === 'meshPeersExact') {
|
||||||
|
const n = Number(view?.stats?.peers) || 0
|
||||||
|
if (n !== Number(expected)) return false
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (key === 'voiceChannelPeersMin') {
|
if (key === 'voiceChannelPeersMin') {
|
||||||
const spec = expected && typeof expected === 'object' ? expected : {}
|
const spec = expected && typeof expected === 'object' ? expected : {}
|
||||||
const n = (view?.voiceStatesByChannel?.[spec.channelId] || []).length
|
const n = (view?.voiceStatesByChannel?.[spec.channelId] || []).length
|
||||||
|
|||||||
+41
@@ -4803,6 +4803,47 @@ class HeadlessSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Phase 788: 2-peer mesh disconnect + reconnect; guest recovers sync rail. */
|
||||||
|
async runGuildMeshReconnectJourney () {
|
||||||
|
const {
|
||||||
|
connectGuildMeshPeers,
|
||||||
|
disconnectGuildMeshPeers
|
||||||
|
} = require('./mesh-helpers')
|
||||||
|
const base = path.join(os.tmpdir(), `pearcord-agentctl-reconnect788-${Date.now()}`)
|
||||||
|
const host = new HeadlessSession({ storagePath: path.join(base, 'host') })
|
||||||
|
const guest = new HeadlessSession({ storagePath: path.join(base, 'guest') })
|
||||||
|
await host.start()
|
||||||
|
await guest.start()
|
||||||
|
try {
|
||||||
|
await host.registerUser({ username: 'rchost788', displayName: 'Reconnect Host 788' })
|
||||||
|
await host.createGuild({ name: 'Reconnect Mesh 788', 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 guest.registerUser({ username: 'rcguest788', displayName: 'Reconnect Guest 788' })
|
||||||
|
await guest.ipc({ type: 'join-invite', code })
|
||||||
|
await guest.wait({ mode: 'guild' }, 45000)
|
||||||
|
await connectGuildMeshPeers(host.platform, guest.platform, 45000)
|
||||||
|
await guest.wait({ meshPeersMin: 1 }, 30000)
|
||||||
|
await disconnectGuildMeshPeers(host.platform, guest.platform, 20000)
|
||||||
|
await guest.wait({ meshPeersExact: 0 }, 30000)
|
||||||
|
await connectGuildMeshPeers(host.platform, guest.platform, 45000)
|
||||||
|
if (typeof guest.platform.runOfflineRecoverySync === 'function') {
|
||||||
|
await guest.platform.runOfflineRecoverySync().catch(() => null)
|
||||||
|
}
|
||||||
|
await guest.wait({ meshReconnectReady: true }, 60000)
|
||||||
|
const gv = await guest.refreshView()
|
||||||
|
return {
|
||||||
|
guestPeers: gv.stats?.peers,
|
||||||
|
meshBounded: gv.guildSyncHealthRail?.[gv.guild?.id]?.meshBounded
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await host.close()
|
||||||
|
await guest.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Phase 787: 2-peer mesh — host assigns custom role; guest member list reflects role. */
|
/** Phase 787: 2-peer mesh — host assigns custom role; guest member list reflects role. */
|
||||||
async runGuildMeshMemberRolesJourney () {
|
async runGuildMeshMemberRolesJourney () {
|
||||||
const { connectGuildMeshPeers } = require('./mesh-helpers')
|
const { connectGuildMeshPeers } = require('./mesh-helpers')
|
||||||
|
|||||||
@@ -41,6 +41,29 @@ async function connectGuildMeshPeers (host, guest, timeoutMs = 28000) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drop direct guild mesh links between host and guest (for reconnect journeys).
|
||||||
|
*/
|
||||||
|
async function disconnectGuildMeshPeers (host, guest, timeoutMs = 12000) {
|
||||||
|
if (!host?.guild?.swarm || !guest?.guild?.swarm) {
|
||||||
|
throw new Error('host and guest must be on an active guild mesh')
|
||||||
|
}
|
||||||
|
const hostPk = host.guild.swarm.keyPair.publicKey
|
||||||
|
const guestPk = guest.guild.swarm.keyPair.publicKey
|
||||||
|
host.guild.swarm.leavePeer(guestPk)
|
||||||
|
guest.guild.swarm.leavePeer(hostPk)
|
||||||
|
await host.guild.swarm.flush()
|
||||||
|
await guest.guild.swarm.flush()
|
||||||
|
const ok = await waitFor(async () => {
|
||||||
|
const h = await host.view()
|
||||||
|
const g = await guest.view()
|
||||||
|
return (h.stats?.peers || 0) === 0 && (g.stats?.peers || 0) === 0
|
||||||
|
}, timeoutMs, 350)
|
||||||
|
if (!ok) throw new Error('peers did not disconnect from guild mesh')
|
||||||
|
await sleep(800)
|
||||||
|
return { hostPeers: 0, guestPeers: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cross-join host + two guests on the guild Hyperswarm mesh (3-peer stability tests).
|
* Cross-join host + two guests on the guild Hyperswarm mesh (3-peer stability tests).
|
||||||
*/
|
*/
|
||||||
@@ -83,6 +106,7 @@ module.exports = {
|
|||||||
sleep,
|
sleep,
|
||||||
waitFor,
|
waitFor,
|
||||||
connectGuildMeshPeers,
|
connectGuildMeshPeers,
|
||||||
|
disconnectGuildMeshPeers,
|
||||||
connectGuildMeshTriple,
|
connectGuildMeshTriple,
|
||||||
createIsolatedMeshDht
|
createIsolatedMeshDht
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ function snapshotWaitDiagnostics (view, match) {
|
|||||||
membersWithCustomRoles:
|
membersWithCustomRoles:
|
||||||
(view?.members || []).filter((m) => (m.customRoleIds || []).length > 0)
|
(view?.members || []).filter((m) => (m.customRoleIds || []).length > 0)
|
||||||
.length ?? 0,
|
.length ?? 0,
|
||||||
|
meshBounded: !!(view?.guild?.id && view.guildSyncHealthRail?.[view.guild.id]?.meshBounded),
|
||||||
|
meshRetryCountdownMs:
|
||||||
|
Number(view?.guildSyncHealthRail?.[view?.guild?.id]?.retryCountdownMs) || 0,
|
||||||
sessionReady: !!view?.sessionReady,
|
sessionReady: !!view?.sessionReady,
|
||||||
workerPoll: view?.workerPoll || null,
|
workerPoll: view?.workerPoll || null,
|
||||||
viewBuildStats: view?.viewBuildStats || null
|
viewBuildStats: view?.viewBuildStats || null
|
||||||
|
|||||||
Reference in New Issue
Block a user