Phase 652: 3-peer partition heal live and mesh churn scale soak journeys.

Add runPartitionHeal3PeerLiveJourney and runMeshChurnScaleSoakJourney for CI and live mesh validation.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-01 22:07:57 -04:00
co-authored by Cursor
parent 9298004693
commit d7b4685ede
3 changed files with 113 additions and 1 deletions
+2
View File
@@ -39,6 +39,8 @@ Both modes use **`pearcord-ui-flow` `dispatchUiMessage`** — the same handler a
**v0.8.619 (Phase 647):** `runProductionMeshSoakJourney` (stability + heal + message + diagnostics); `exportPartitionHealDiagnostics`. Bundle: `npm run test:phase647-production-mesh`. See [PRODUCTION_MESH.md](../../docs/PRODUCTION_MESH.md).
**v0.8.624 (Phase 652):** `runPartitionHeal3PeerLiveJourney`, `runMeshChurnScaleSoakJourney` (`PEARCORD_MESH_CHURN_SOAK_MS`). Bundle: `npm run test:phase652-live-mesh-churn`. See [MESH_CHURN_SCALE.md](../../docs/MESH_CHURN_SCALE.md).
**v0.8.618 (Phase 646):** `runPartitionHealJourney` (split-brain presence → heal → diagnostics); IPC `run-partition-heal-sync`. Bundle: `npm run test:phase646-partition-heal`. See [PARTITION_HEAL.md](../../docs/PARTITION_HEAL.md).
`AgentClient` resolves RPC replies before treating `{ event: "state" | "sidecar" }` push lines as events (fixes `wait-event` responses that include an `event` field).
+12
View File
@@ -1219,6 +1219,18 @@ class HeadlessSession {
return { guildId: gid, soakMs, tickCount: ticks.length, ticks, federation }
}
/** Journey: 3-peer mesh connect + partition heal (live scoped discovery). */
async runPartitionHeal3PeerLiveJourney (scenarioPath, opts = {}) {
const { runPartitionHeal3PeerLiveJourney: runLive } = require('./mesh')
return runLive(scenarioPath, opts)
}
/** Journey: mesh churn scale soak (topic pool + gossip lane ticks). */
async runMeshChurnScaleSoakJourney (scenarioPath, opts = {}) {
const { runMeshChurnScaleSoakJourney: runSoak } = require('./mesh')
return runSoak(scenarioPath, opts)
}
/** Journey: simulate remote hub post ingest on discovery mesh. */
async runFederationHubIngestJourney () {
if (!this.platform) throw new Error('headless not started')
+99 -1
View File
@@ -216,8 +216,106 @@ async function connectGuildMeshTripleGroup (scenarioPath, opts = {}) {
}
}
/**
* 3-peer live mesh: connect, split-brain presence, partition heal with scoped discovery.
*/
async function runPartitionHeal3PeerLiveJourney (scenarioPath, opts = {}) {
const raw = fs.existsSync(scenarioPath) ? JSON.parse(fs.readFileSync(scenarioPath, 'utf8')) : null
const base = path.join(os.tmpdir(), `pearcord-agentctl-heal3-${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') })
if (opts.sessions) opts.sessions.push(host, guest1, guest2)
await host.start()
await guest1.start()
await guest2.start()
try {
if (raw?.hostSteps || raw?.steps) {
await host.runSteps(raw.hostSteps || raw.steps)
} else if (scenarioPath) {
await host.runScenario(scenarioPath)
} else {
await host.registerUser({ username: 'heal3_host', displayName: 'Heal3 Host' })
await host.createGuild({ name: 'Heal3 Live' })
}
const invite = await resolveHostInviteCode(host)
await ensureGuestOnboarded(guest1)
await ensureGuestOnboarded(guest2)
await guest1.ipc({ type: 'join-invite', code: invite })
await guest2.ipc({ type: 'join-invite', code: invite })
await waitFor(async () => guest1.lastView?.mode === 'guild', 20000)
await waitFor(async () => guest2.lastView?.mode === 'guild', 20000)
await connectGuildMeshTriple(
host.platform,
guest1.platform,
guest2.platform,
opts.meshTimeoutMs || 50000
)
const gid = host.platform.guild?.guild?.id
if (!gid) throw new Error('3-peer heal: no guild id')
const journey = await host.runPartitionHealJourney(gid)
const meshDiag = host.platform.exportMeshReplicationDiagnostics(gid)
return {
peerCount: 3,
guildId: gid,
heal: journey.heal,
discoveryScoped: journey.heal?.discoveryScoped === true,
meshDiag,
hostPeers: (await host.platform.view()).stats?.peers ?? 0
}
} finally {
await host.close()
await guest1.close()
await guest2.close()
}
}
/**
* CI-safe mesh churn ticks (topic pool + gossip lanes); optional long soak via env.
*/
async function runMeshChurnScaleSoakJourney (scenarioPath, opts = {}) {
const raw = fs.existsSync(scenarioPath) ? JSON.parse(fs.readFileSync(scenarioPath, 'utf8')) : null
const base = path.join(os.tmpdir(), `pearcord-agentctl-churn-${Date.now()}`)
const host = new HeadlessSession({ storagePath: path.join(base, 'host') })
await host.start()
try {
if (raw?.hostSteps || raw?.steps) {
await host.runSteps(raw.hostSteps || raw.steps)
} else if (scenarioPath) {
await host.runScenario(scenarioPath)
} else {
await host.registerUser({ username: 'churn_host', displayName: 'Churn Host' })
await host.createGuild({ name: 'Churn Scale' })
}
const gid = host.platform.guild?.guild?.id
if (!gid) throw new Error('churn soak: no guild')
const soakMs = Math.max(
2000,
Number(process.env.PEARCORD_MESH_CHURN_SOAK_MS) || Number(opts.soakMs) || 6000
)
const deadline = Date.now() + soakMs
const ticks = []
while (Date.now() < deadline) {
await host.platform._refreshGuildSyncHealthCounts(gid).catch(() => {})
const diag = host.platform.exportMeshReplicationDiagnostics(gid)
ticks.push({
at: Date.now(),
topicPool: diag?.guildTopicPool || null,
lanes: diag?.gossipLaneCounts || null
})
await new Promise((r) => setTimeout(r, 1500))
}
await host.refreshView()
return { guildId: gid, soakMs, tickCount: ticks.length, ticks }
} finally {
await host.close()
}
}
module.exports = {
connectGuildMeshGroup,
connectGuildMeshThread,
connectGuildMeshTripleGroup
connectGuildMeshTripleGroup,
runPartitionHeal3PeerLiveJourney,
runMeshChurnScaleSoakJourney
}