fix(engagement): two defects the Phase 11b live walk found in core #179
Reference in New Issue
Block a user
No description provided.
Delete Branch "fix/engagement-live-walk-core"
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?
Core's half of Phase 11b's acceptance walk. Pairs with Module-uo# (four more defects and the 26th trigger), servuo-plugins# (the market sweep) and docs# (the three decisions).
Both defects here are invisible to a fixture and loud on a real database — which is the argument for the walk being the phase's acceptance rather than a formality.
1. Every module-seeded rule failed to insert
checkSeedRulevalidatesmax_sends_per_hour— Q3's hard ceiling, the thing that keeps a misconfiguration from becoming a mail storm — and then drops it from the normalized rule it returns. The column isNOT NULL, so the first boot with module-uo installed produced 25 of these and seeded nothing:The existing test asserted the rejection of a bad ceiling (
max_sends_per_hour: 0→ error) and never that a good one survives normalization. Those are different bugs and the first test cannot see the second.The new test is written against
engagementRules.db.insert's own column list rather than against the one field, because the next field added to the declaration is the next one that can be forgotten here.2. A rule with a cooldown delivered on exactly ONE of its channels
cooldownsDb.claimis called inside the engine's per-channel loop and its key was(rule, user, subject). So the first channel of a rule claimed the cooldown and every later one came backfalse— reported ascooled, which reads in the log exactly like the cooldown doing its job:inappis ranked first on purpose (CHANNEL_ORDER— sopushcan reference the inbox row it writes), so this is not a coin-flip: a rule naming email and in-app delivers the inbox item and silently never the mail. Core's own seedednews.postrule has that shape — three channels, an hour's cooldown — and so does every one of the sixteen in-universe bodies Phase 11b wrote, each of which was unreachable behind its own in-app twin.The org lead's decision 12:
channeljoins the cooldown key. The alternative — claiming once per user before the channel loop — was refused because it makes the cooldown a per-occasion limit, and an operator who sets "one a day about this house" means one mail and one inbox item, not one of the two. A cooldown is per delivery.The migration, and why it is guarded the way it is
MariaDB has no conditional form of a primary-key change, and
schema.sqlis replayed on every boot — so the unguarded version would come up once and fail the entire schema step on every boot after that. The guard reads the KEY out ofinformation_schemarather than the column's existence, becauseADD COLUMN IF NOT EXISTScan succeed while the key change does not. Same shape as theannounce_job_legsbackfill a few hundred lines up.Rows written before the migration keep
channel = ''and expire on their own interval — one stale cooldown per (rule, user, subject), which is a better trade than dropping the table and letting a storm through the window.Verification
normalized rule is missing "max_sends_per_hour", the engine test with the email row absent.PRIMARY KEY (rule_id, user_id, subject_key, channel)afterwards, and the 26 module rules seeded on the same boot.uo.house.idoc_warningthen wentenqueued: 2, cooled: 0— both channels — and a second transition inside the day wentenqueued: 0, cooled: 2.AI-assisted: written with Claude Code.
🤖 Generated with Claude Code