Phase 786: agentctl guild typing mesh journey (v0.8.753)

Add typingUsersIncludes matcher, runGuildMeshTypingJourney for 2-peer gossip,
and wait-diagnostics typingUserCount checkpoint.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-06-03 21:36:10 -04:00
co-authored by Cursor
parent b9a16f06cd
commit 2714838133
4 changed files with 66 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 786 (v0.8.753):** `runGuildMeshTypingJourney` (2-peer typing gossip). Bundle: `npm run test:ci-phase786`.
**Phase 785 (v0.8.752):** `runGuildMeshVoiceOccupancyJourney` (2-peer voice occupancy). Bundle: `npm run test:ci-phase785`.
**Phase 784 (v0.8.751):** `runGuildMeshUnreadJourney` (2-peer channel unread). Bundle: `npm run test:ci-phase784`.
+5
View File
@@ -78,6 +78,11 @@ function matchView (view, spec) {
if (count < Number(spec.min ?? 1)) return false
continue
}
if (key === 'typingUsersIncludes') {
const uid = String(expected || '')
if (!(view?.typingUsers || []).includes(uid)) return false
continue
}
if (key === 'voiceChannelPeersMin') {
const spec = expected && typeof expected === 'object' ? expected : {}
const n = (view?.voiceStatesByChannel?.[spec.channelId] || []).length
+58
View File
@@ -4745,6 +4745,64 @@ class HeadlessSession {
return { memberCount, presencePeers, guildId: v.guild?.id }
}
/** Phase 786: 2-peer mesh — host typing visible on guest in same channel. */
async runGuildMeshTypingJourney () {
const { connectGuildMeshPeers } = require('./mesh-helpers')
const { RPC } = require('pearcord-shared')
const base = path.join(os.tmpdir(), `pearcord-agentctl-typing786-${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: 'typhost786', displayName: 'Typing Host 786' })
await host.createGuild({ name: 'Typing Mesh 786', 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: 'typguest786', displayName: 'Typing Guest 786' })
await guest.ipc({ type: 'join-invite', code })
await guest.wait({ mode: 'guild' }, 45000)
const chId = (await host.refreshView()).activeChannelId
if (!chId) throw new Error('active channel missing')
await guest.ipc({ type: 'select-channel', channelId: chId })
await guest.wait({ activeChannelId: chId }, 30000)
await connectGuildMeshPeers(host.platform, guest.platform, 45000)
await new Promise((r) => setTimeout(r, 2000))
const hostUserId = (await host.refreshView()).user?.id
if (!hostUserId) throw new Error('host user id missing')
await host.platform.signalTyping?.()
if (host.platform.guild?.gossipTyping) {
await host.platform.guild
.gossipTyping({
userId: hostUserId,
channelId: chId,
guildId: host.platform.guild.guild.id,
at: Date.now()
})
.catch(() => {})
} else if (host.platform.guild?.simulateGossip) {
await host.platform.guild.simulateGossip(RPC.TYPING_START, {
userId: hostUserId,
channelId: chId,
guildId: host.platform.guild.guild.id,
at: Date.now()
})
}
await guest.wait({ typingUsersIncludes: hostUserId }, 60000)
const gv = await guest.refreshView()
return {
channelId: chId,
hostUserId,
guestTyping: gv.typingUsers || []
}
} finally {
await host.close()
await guest.close()
}
}
/** Phase 785: 2-peer mesh — host in voice visible on guest voiceStatesByChannel. */
async runGuildMeshVoiceOccupancyJourney () {
const { connectGuildMeshPeers } = require('./mesh-helpers')
+1
View File
@@ -34,6 +34,7 @@ function snapshotWaitDiagnostics (view, match) {
voicePeerCount: view?.myVoiceChannelId
? (view?.voiceStatesByChannel?.[view.myVoiceChannelId] || []).length
: 0,
typingUserCount: view?.typingUsers?.length ?? 0,
sessionReady: !!view?.sessionReady,
workerPoll: view?.workerPoll || null,
viewBuildStats: view?.viewBuildStats || null