fix(engagement): claim the seed guard atomically, and let a rule name a digest body
#181
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/engagement-seed-claim-and-digest"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The two defects Phase 13's acceptance walk found (docs#203). Neither was visible to any test, and both are in code the workstream shipped.
1 · The one-shot rule-group guard was not a guard
moduleSeeds.seedRuleGroupandcoreRules.seedGroupboth read their settings stamp, inserted the whole group, and wrote the stamp after the loop. Two instances booting in the same moment both read "absent" and both insert. The walk ended up with 52 module rules where module-uo ships 26, under one stamp — anddocker compose up --scale app=2and a rolling restart both start two instances on purpose, so this is ordinary rather than exotic.settings.dbgainsclaim(key, value)— the sameINSERT IGNOREasseedDefault, reporting its ownaffectedRows, so exactly one caller can win a key. The atomicity is the PRIMARY KEY's: no transaction, no lock, the same bargainengagementWorker's row claim already makes. Both seeders now claim before inserting.The trade is unchanged, only its order. A process that dies mid-loop leaves the group stamped and partly seeded; the missing rules are an operator's visit to the "new rule" form. Both functions' own comments already say why that is the better failure — "a duplicate rule is two mails per event", which is precisely what the old ordering produced.
Why nothing caught it. The stubs supplied
getandsetover aMap, which cannot race, so they agreed with the bug — Phase 4a'sfoundRowsfinding in a different costume. The fake is nowclaim-shaped and decides without yielding, exactly as the table does. Both suites gained a test that runs two seeders underPromise.all:Verified by reverting the fix — the module test then reports every rule inserted twice:
2 · A rule that names a
digestbody could not be savedregistries.jscheckSeedRulepermitsdigestin as many words — it is the bodyteamDigestWorkerrenders for a rule whose email channel an individual set to digest mode, so it never appears inchannelsand never could — andMODULE_API.md§2.4 tells modules they may pointtemplate_keysatnotify.digest.engagementRules.model.jsthen rejected any key that was not one of the rule's channels.So every rule shipping a digest body was unsaveable from the Rules screen: core's own Team and news rules (
coreRules.jslines 62, 84, 138) and sixteen of module-uo's. An operator who opened House — decay warning and pressed Save got a 400 naming a key they had never typed, and the only remedy was deleting the digest body — silently dropping digest support from that rule.The two validators now agree. Anything that is neither a channel nor
digestis still refused, and there is a test for each direction.No MODULE_API bump
No member is added, removed or changed. The documented contract (§2.4: "a rule may point
template_keysatnotify.event/inapp.event/notify.digest") is what the code now honours rather than anything new, and the seeding change is core-internal.Gates
npm test: 1555 pass, 1 fail — and that one is a local-checkout artifact, not this change.engagement-triggers.jsonis unmodified in git; the manifest test compares the file on disk (CRLF here, viacore.autocrlf) against freshly generated output (LF). Regenerating it produces a byte-identical file modulo line endings (git diffreports the path with no content diff), so it is green on CI's Linux checkout.routes:manifest --check: up to date (243 routes) — this adds no route.🤖 Generated with Claude Code