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).
|
`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
|
## Quick start
|
||||||
|
|
||||||
### 1. Start Pearcord with the control server
|
### 1. Start Pearcord with the control server
|
||||||
|
|||||||
@@ -144,6 +144,20 @@ function matchView (view, spec) {
|
|||||||
if ((view?.activeChannelOverwrites || []).length < Number(expected)) return false
|
if ((view?.activeChannelOverwrites || []).length < Number(expected)) return false
|
||||||
continue
|
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)
|
const actual = getPath(view, key)
|
||||||
if (
|
if (
|
||||||
expected &&
|
expected &&
|
||||||
@@ -176,6 +190,9 @@ function matchSidecarEvent (evt, spec) {
|
|||||||
const msg = payload.message || payload.record?.msg || ''
|
const msg = payload.message || payload.record?.msg || ''
|
||||||
if (!String(msg).includes(String(spec.messageIncludes))) return false
|
if (!String(msg).includes(String(spec.messageIncludes))) return false
|
||||||
}
|
}
|
||||||
|
if (spec.automodBlocked) {
|
||||||
|
if (payload.result?.ok !== false) return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-1
@@ -90,7 +90,22 @@ class HeadlessSession {
|
|||||||
const deadline = Date.now() + timeoutMs
|
const deadline = Date.now() + timeoutMs
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
await this.refreshView()
|
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))
|
await new Promise((r) => setTimeout(r, 50))
|
||||||
}
|
}
|
||||||
throw new Error(`headless wait timeout: ${JSON.stringify(match)}`)
|
throw new Error(`headless wait timeout: ${JSON.stringify(match)}`)
|
||||||
@@ -102,6 +117,25 @@ class HeadlessSession {
|
|||||||
async runSteps (steps) {
|
async runSteps (steps) {
|
||||||
const results = []
|
const results = []
|
||||||
for (const step of steps || []) {
|
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) {
|
if (step.ipc) {
|
||||||
const view = await this.ipc(step.ipc)
|
const view = await this.ipc(step.ipc)
|
||||||
results.push({ ipc: step.ipc, view })
|
results.push({ ipc: step.ipc, view })
|
||||||
|
|||||||
Reference in New Issue
Block a user