feat(agentctl): federation stability and hub ingest journeys

Add runFederationStabilitySoakJourney and runFederationHubIngestJourney for
Phase 650 federation mesh validation.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-01 21:53:11 -04:00
co-authored by Cursor
parent 680632a1d8
commit 31c11586fa
2 changed files with 65 additions and 2 deletions
+2
View File
@@ -29,6 +29,8 @@ Both modes use **`pearcord-ui-flow` `dispatchUiMessage`** — the same handler a
**v0.8.617 (Phase 645):** `runLargeGuildMemberJourney` (seed 120+ members → partial bundle → member page request); IPC `request-member-page`. Bundle: `npm run test:phase645-large-guild-mesh`. See [LARGE_GUILD_MESH.md](../../docs/LARGE_GUILD_MESH.md).
**v0.8.622 (Phase 650):** `runFederationStabilitySoakJourney` (`PEARCORD_FEDERATION_SOAK_MS`), `runFederationHubIngestJourney`; async `exportFederationSyncDiagnostics`. Bundle: `npm run test:phase650-live-federation-mesh`.
**v0.8.621 (Phase 649):** `runFederationMeshSoakJourney` (multi-guild + listing refresh + federation export); `exportFederationSyncDiagnostics`. Bundle: `npm run test:phase649-federation-discovery`. See [FEDERATION_MESH.md](../../docs/FEDERATION_MESH.md).
**v0.8.620 (Phase 648):** `runMultiGuildMeshSoakJourney` (two guilds, switch, multi export); `exportMultiGuildSyncDiagnostics`. Bundle: `npm run test:phase648-multi-guild-mesh`. See [MULTI_GUILD_MESH.md](../../docs/MULTI_GUILD_MESH.md).
+63 -2
View File
@@ -1147,7 +1147,7 @@ class HeadlessSession {
return this.platform.exportMultiGuildSyncDiagnostics()
}
exportFederationSyncDiagnostics () {
async exportFederationSyncDiagnostics () {
if (!this.platform) throw new Error('headless not started')
return this.platform.exportFederationSyncDiagnostics()
}
@@ -1187,11 +1187,72 @@ class HeadlessSession {
syncMesh: false
})
}
const federation = this.platform.exportFederationSyncDiagnostics()
const federation = await this.platform.exportFederationSyncDiagnostics()
await this.refreshView()
return { ...multiOut, listingRefresh, federation }
}
/** Journey: federation stability ticks (CI-safe; use PEARCORD_FEDERATION_SOAK_MS for long soak). */
async runFederationStabilitySoakJourney (guildId) {
if (!this.platform) throw new Error('headless not started')
const gid = guildId || this.platform.guild?.guild?.id
if (!gid) throw new Error('no guild')
const soakMs = Math.max(
3000,
Number(process.env.PEARCORD_FEDERATION_SOAK_MS) || 12000
)
const deadline = Date.now() + soakMs
const ticks = []
while (Date.now() < deadline) {
const scoped = await this.platform.refreshDiscoveryExploreScoped().catch(() => null)
const health = await this.getGuildSyncHealth(gid)
ticks.push({
at: Date.now(),
scoped,
pending: !!health?.pending,
peers: Number(health?.peers) || 0
})
await new Promise((r) => setTimeout(r, 2000))
}
const federation = await this.platform.exportFederationSyncDiagnostics()
await this.refreshView()
return { guildId: gid, soakMs, tickCount: ticks.length, ticks, federation }
}
/** Journey: simulate remote hub post ingest on discovery mesh. */
async runFederationHubIngestJourney () {
if (!this.platform) throw new Error('headless not started')
const g = await this.createGuild({ name: 'Hub Src', publicListing: true })
const gid = g?.guild?.id || this.platform.guild?.guild?.id
const ch = this.platform.activeChannelId
if (!gid || !ch) throw new Error('guild/channel required')
await this.sendGuildMessage(`hub-${Date.now()}`)
const targetGuild = await this.createGuild({ name: 'Hub Sub' })
const subGid = targetGuild?.guild?.id || this.platform.guild?.guild?.id
const targetCh = this.platform.activeChannelId
if (!subGid || !targetCh) throw new Error('subscriber guild required')
await this.platform.announcementHub.subscribe({
subscriberGuildId: subGid,
sourceGuildId: gid,
sourceChannelId: ch,
targetChannelId: targetCh
})
const payload = {
sourceGuildId: gid,
sourceChannelId: ch,
sourceGuildName: 'Hub Src',
sourceChannelName: 'general',
messageId: `hub-msg-${Date.now()}`,
content: 'federation hub ingest smoke',
authorName: 'Smoke',
postedAt: Date.now()
}
const mirrored = await this.platform._ingestAnnouncementHubPost(payload)
const federation = await this.platform.exportFederationSyncDiagnostics()
await this.refreshView()
return { payload, mirrored: mirrored?.length || 0, federation }
}
/** Journey: mesh stability + partition heal + message + diagnostics (production soak). */
async runProductionMeshSoakJourney (guildId) {
if (!this.platform) throw new Error('headless not started')