feat: agentctl guild search and thread mesh helpers

Add searchResultsMin wait matcher, connectGuildMeshThread, and document
new headless scenarios.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Raven Scott
2026-05-23 07:03:51 -04:00
co-authored by Cursor
parent 99f49da30a
commit cf23b25c39
3 changed files with 64 additions and 3 deletions
+5 -1
View File
@@ -142,8 +142,12 @@ Bundled scenarios under `apps/pearcord/scenarios/`:
| `agentctl-settings-navigate.json` | `navigate-deep-link``pearcord://settings` (v0.8.258) |
| `agentctl-settings-guild-deep-link.json` | Register + guild + `pearcord://settings?scope=guild&guildId=…` (v0.8.259) |
| `agentctl-composer-channel-mention.json` | Send `@channel` message (v0.8.258) |
| `agentctl-guild-search.json` | Guild-wide `update-search` hit (v0.8.260) |
| `agentctl-thread-mesh.json` | Host thread + reply; guest mesh sync (v0.8.260) |
Scenario step extras (headless + attached `run`): `joinFirstVoice`, `openDmPeer`, `messagesMin`, `messageContentIncludes` on `wait`.
`mesh.connectGuildMeshThread(scenarioPath)` — host steps + invite + `create-thread` + reply; guest must receive `threadReply` token.
Scenario step extras (headless + attached `run`): `joinFirstVoice`, `openDmPeer`, `messagesMin`, `messageContentIncludes`, `searchResultsMin` on `wait`.
## Dependencies
+4
View File
@@ -32,6 +32,10 @@ function matchView (view, spec) {
if (!hay.includes(String(expected))) return false
continue
}
if (key === 'searchResultsMin') {
if ((view?.searchResults || []).length < Number(expected)) return false
continue
}
if (key === 'voiceJoined') {
if (expected && !view?.myVoiceChannelId) return false
if (!expected && view?.myVoiceChannelId) return false
+55 -2
View File
@@ -1,5 +1,6 @@
'use strict'
const fs = require('bare-fs')
const path = require('bare-path')
const os = require('bare-os')
const { HeadlessSession } = require('./headless')
@@ -51,6 +52,58 @@ async function connectGuildMeshGroup (scenarioPath, opts = {}) {
}
}
module.exports = {
connectGuildMeshGroup
/**
* Host + guest: onboard, mesh join, host creates thread from first message and replies.
* @param {string} scenarioPath JSON with hostSteps + threadReply
*/
async function connectGuildMeshThread (scenarioPath, opts = {}) {
const peerCount = Math.max(2, Number(opts.peerCount) || 2)
const raw = fs.readFileSync(scenarioPath, 'utf8')
const spec = JSON.parse(raw)
const replyToken = String(spec.threadReply || opts.threadReply || `thread-mesh-${Date.now()}`)
const base = path.join(os.tmpdir(), `pearcord-agentctl-thread-${Date.now()}`)
const host = new HeadlessSession({ storagePath: path.join(base, 'host') })
const guest = new HeadlessSession({ storagePath: path.join(base, 'guest') })
if (opts.sessions) {
opts.sessions.push(host, guest)
}
await host.start()
await guest.start()
try {
await host.runSteps(spec.hostSteps || spec.steps || [])
const invite = host.lastView?.lastInvite?.code || host.lastView?.inviteCode
if (!invite) {
const created = await host.ipc({ type: 'create-invite' })
const code = created?.lastInvite?.code
if (!code) throw new Error('thread mesh: no invite code')
await guest.ipc({ type: 'join-invite', code })
} else {
await guest.ipc({ type: 'join-invite', code: invite })
}
await waitFor(async () => guest.lastView?.mode === 'guild', 20000)
const rootMsg = (host.lastView?.messages || [])[0]
if (!rootMsg?.id) throw new Error('thread mesh: host has no root message')
await host.ipc({ type: 'create-thread', messageId: rootMsg.id, name: 'Mesh Thread' })
await waitFor(async () => host.lastView?.activeChannel?.type === 'thread', 10000)
await host.ipc({ type: 'send-message', content: replyToken })
const synced = await waitFor(async () => {
const v = guest.lastView
return (v?.messages || []).some((m) => String(m.content || '').includes(replyToken))
}, 30000)
if (!synced) throw new Error('thread mesh: guest did not receive thread reply')
return {
peerCount,
hostPeers: host.lastView?.stats?.peers ?? 0,
guestPeers: guest.lastView?.stats?.peers ?? 0,
replyToken
}
} finally {
await host.close()
await guest.close()
}
}
module.exports = {
connectGuildMeshGroup,
connectGuildMeshThread
}