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:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user