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-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-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-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
|
## Dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ function matchView (view, spec) {
|
|||||||
if (!hay.includes(String(expected))) return false
|
if (!hay.includes(String(expected))) return false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (key === 'searchResultsMin') {
|
||||||
|
if ((view?.searchResults || []).length < Number(expected)) return false
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (key === 'voiceJoined') {
|
if (key === 'voiceJoined') {
|
||||||
if (expected && !view?.myVoiceChannelId) return false
|
if (expected && !view?.myVoiceChannelId) return false
|
||||||
if (!expected && view?.myVoiceChannelId) return false
|
if (!expected && view?.myVoiceChannelId) return false
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
|
const fs = require('bare-fs')
|
||||||
const path = require('bare-path')
|
const path = require('bare-path')
|
||||||
const os = require('bare-os')
|
const os = require('bare-os')
|
||||||
const { HeadlessSession } = require('./headless')
|
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