feat(events): send the idempotency key, and declare champ.boss.killed (Phase 11a)
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / server-tests (pull_request) Successful in 26s
PR Checks / frozen-manifest (pull_request) Successful in 39s

The website's half of protocol 6.

Every event-driven write now carries the step's idempotency key, and `uo.broadcast`
stops being un-retryable. Phase 9 shipped it answering `retry: false` to everything
including a 503 from a shard that was merely restarting, with a comment naming the
line that would change when the wire could refuse a repeat. This is that line: it
defers to `sidecarFailure`, the same helper its two siblings already used, so the
hand-rolled variant that forced every outcome terminal is gone rather than re-tuned.

One verb was less idempotent than its own id made it look. Both keyed verbs post
under a run-scoped id and a repeat replaces — but `news.add` with `announce: true`
makes the criers proclaim the title on every post, so a retry replaced the article
silently and proclaimed it again. The key stops the second proclamation.

`champ.boss.killed` is mapped to the `champs` feature (rule 2 would otherwise fail
it closed to admin), with `damagers` a nested `staff` field rule: the kill is public
because a champion falling is what the board is for, the ranked roll of who was
strong enough to fell it is not. `uo.champ.boss_killed` is declared as a trigger —
which is what makes it usable as an event PHASE CONDITION, since a condition is
written over a trigger firing — and it carries `damagerCount`, never a damager name,
because a trigger variable reaches mail an operator may address to every subscriber.

Its seeded rule is its own group, `champ-boss-killed-v1`: `triggers-v1` is stamped
once under a settings guard, so appending a 27th entry would have reached fresh
installs and nothing else. It also ships email+inapp and NOT push, and the comment
says why — no trigger in this module is also a registered stream, so no engagement
rule here can push. That is pre-existing in twenty rules and flagged rather than
fixed; this one declines to be the twenty-first.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-04 14:57:26 -05:00
parent cf60932c85
commit dc13515927
10 changed files with 501 additions and 64 deletions

View File

@@ -133,30 +133,57 @@ test('a broadcast spends the one budget dimension the module declares', () => {
assert.equal(byId('uo.news.post').cost, undefined)
})
// ── uo.broadcast: attempted exactly once ───────────────────────────────────
// ── uo.broadcast: retried, because protocol 6 made that safe ───────────────
test('a broadcast is never retried, whatever the sidecar says', async () => {
test('a broadcast is retried on a transient failure and never on a permanent one', async () => {
const broadcast = byId('uo.broadcast')
// Every failure this transport can produce: no route to the sidecar, a data
// refusal, a bad token, a protocol mismatch, a shard that is not connected and
// a shard that timed out. The last two are genuinely transient, and this is
// the trade being taken knowingly — a lost announcement is cheaper than one
// delivered twice to everyone online.
for (const status of [0, 400, 401, 403, 409, 503, 504]) {
// Wave 1 asserted the opposite of this — every failure terminal, including the
// two that are plainly transient — because nothing on the wire could stop a
// retry announcing to everyone twice. Protocol 6 puts an idempotency key on the
// command and the shard refuses the repeat, so the trade that test recorded is
// no longer one that has to be made.
//
// 425 is the new status in this list: `bridge.busy`, the shard saying a command
// under this key is still in flight. Transient by construction.
const TRANSIENT = new Set([0, 425, 503, 504])
for (const status of [0, 400, 401, 403, 409, 425, 503, 504]) {
uoLinkClient.adminBroadcast = async () => ({ ok: false, status, error: `status ${status}` })
const result = await broadcast.perform({ runId: 7, params: { text: 'hear ye' }, verify: false })
assert.equal(result.ok, false)
assert.equal(result.retry, false, `a ${status} must not be retried`)
// The clause belongs only where a retry was genuinely given up. On a
// permanent status it would explain the wrong thing.
if (!actions.PERMANENT_STATUSES.has(status)) {
assert.match(result.error, /announce twice/, 'a discarded retry must say why')
} else {
assert.doesNotMatch(result.error, /announce twice/, `a ${status} was never retryable`)
}
assert.equal(result.retry, TRANSIENT.has(status), `a ${status} retries iff it is transient`)
}
})
test('every write carries the step idempotency key, unchanged', async () => {
// The key is what makes the retry above safe, so a verb that dropped it would
// silently restore the wave-1 hazard while every other assertion still passed.
// Asserted per verb rather than once, because each builds its own body.
const KEY = 'a'.repeat(40)
const seen = {}
uoLinkClient.adminBroadcast = async (body) => { seen.broadcast = body; return { ok: true } }
uoLinkClient.postTownCrier = async (body) => { seen.crier = body; return { ok: true } }
uoLinkClient.postNews = async (body) => { seen.news = body; return { ok: true } }
await byId('uo.broadcast').perform({
runId: 7, idempotencyKey: KEY, params: { text: 'hear ye' }, verify: false,
})
await byId('uo.towncrier.post').perform({
runId: 7, idempotencyKey: KEY, params: { lines: 'hear ye' }, verify: false,
})
await byId('uo.news.post').perform({
runId: 7, idempotencyKey: KEY, params: { title: 'A thing', body: 'happened' }, verify: false,
})
assert.equal(seen.broadcast.idempotencyKey, KEY)
assert.equal(seen.crier.idempotencyKey, KEY)
assert.equal(seen.news.idempotencyKey, KEY)
// The two keyed verbs post under an id DERIVED from the key. Both travel: the
// id is what makes a repeat replace, the key is what stops it re-announcing.
assert.equal(seen.crier.id, `evt-${KEY}`)
assert.equal(seen.news.id, `evt-${KEY}`)
})
test("the shard's own words reach the run log, not just a status code", async () => {
// **The rig found this.** The sidecar refuses a broadcast with
// `{"reason":"admin write plane disabled"}` and `legError` looks for