feat(agentctl): expectIpcError, ban wait, automod dry-run assert keys
Support headless moderation scenarios: IPC error expectations, DB ban wait, automodBlocked sidecar match, and view wait keys for automod/timeouts. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -17,6 +17,8 @@ Both modes use **`pearcord-ui-flow` `dispatchUiMessage`** — the same handler a
|
||||
|
||||
`AgentClient` resolves RPC replies before treating `{ event: "state" | "sidecar" }` push lines as events (fixes `wait-event` responses that include an `event` field).
|
||||
|
||||
**v0.8.390 (Phase 427):** `expectIpcError` step, `wait.banForUserId` (DB ban row), `waitEvent` with `automodBlocked`, assert keys `automodEnabled`, `automodBlockedKeywordsMin`, `memberTimeoutFor`. Bundle: `npm run test:agentctl-phase427-moderation`.
|
||||
|
||||
## Quick start
|
||||
|
||||
### 1. Start Pearcord with the control server
|
||||
|
||||
@@ -144,6 +144,20 @@ function matchView (view, spec) {
|
||||
if ((view?.activeChannelOverwrites || []).length < Number(expected)) return false
|
||||
continue
|
||||
}
|
||||
if (key === 'automodEnabled') {
|
||||
if (!!view?.automodConfig?.enabled !== !!expected) return false
|
||||
continue
|
||||
}
|
||||
if (key === 'automodBlockedKeywordsMin') {
|
||||
if ((view?.automodConfig?.blockedKeywords || []).length < Number(expected)) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (key === 'memberTimeoutFor') {
|
||||
if (!view?.memberTimeouts?.[String(expected)]) return false
|
||||
continue
|
||||
}
|
||||
const actual = getPath(view, key)
|
||||
if (
|
||||
expected &&
|
||||
@@ -176,6 +190,9 @@ function matchSidecarEvent (evt, spec) {
|
||||
const msg = payload.message || payload.record?.msg || ''
|
||||
if (!String(msg).includes(String(spec.messageIncludes))) return false
|
||||
}
|
||||
if (spec.automodBlocked) {
|
||||
if (payload.result?.ok !== false) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
+35
-1
@@ -90,7 +90,22 @@ class HeadlessSession {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (Date.now() < deadline) {
|
||||
await this.refreshView()
|
||||
if (matchView(this._lastView, match)) return this._lastView
|
||||
if (match?.banForUserId) {
|
||||
const guildId = this._lastView?.guild?.id
|
||||
const userId = String(match.banForUserId)
|
||||
if (guildId) {
|
||||
const ban = await this.platform.db.get(COLLECTIONS.BANS, { guildId, userId })
|
||||
if (ban) {
|
||||
const rest = { ...match }
|
||||
delete rest.banForUserId
|
||||
if (!Object.keys(rest).length || matchView(this._lastView, rest)) {
|
||||
return this._lastView
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (matchView(this._lastView, match)) {
|
||||
return this._lastView
|
||||
}
|
||||
await new Promise((r) => setTimeout(r, 50))
|
||||
}
|
||||
throw new Error(`headless wait timeout: ${JSON.stringify(match)}`)
|
||||
@@ -102,6 +117,25 @@ class HeadlessSession {
|
||||
async runSteps (steps) {
|
||||
const results = []
|
||||
for (const step of steps || []) {
|
||||
if (step.expectIpcError) {
|
||||
const spec =
|
||||
step.expectIpcError && typeof step.expectIpcError === 'object'
|
||||
? step.expectIpcError
|
||||
: {}
|
||||
const payload = spec.ipc || spec
|
||||
let err = null
|
||||
try {
|
||||
await this.ipc(payload)
|
||||
} catch (e) {
|
||||
err = e
|
||||
}
|
||||
if (!err) throw new Error(`expected ipc error for ${payload?.type}`)
|
||||
const msg = String(err.message || err)
|
||||
if (spec.messageIncludes && !msg.includes(String(spec.messageIncludes))) {
|
||||
throw new Error(`expected "${spec.messageIncludes}" in: ${msg}`)
|
||||
}
|
||||
results.push({ expectIpcError: payload, error: msg })
|
||||
}
|
||||
if (step.ipc) {
|
||||
const view = await this.ipc(step.ipc)
|
||||
results.push({ ipc: step.ipc, view })
|
||||
|
||||
Reference in New Issue
Block a user