feat(events): send the idempotency key, and declare champ.boss.killed (Phase 11a)
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user