Skip to content

test(ads): cover the house rotation rate in serveAd - #186

Merged
ralyodio merged 1 commit into
masterfrom
test/cover-house-rotation-rate
Aug 3, 2026
Merged

ralyodio merged 1 commit into
masterfrom
test/cover-house-rotation-rate

Conversation

@ralyodio

@ralyodio ralyodio commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #183. Not a fix — it restores one piece of coverage that PR removed.

What happened

#183 de-flaked tests/contract/ads-short-code-serving.test.ts by mocking HOUSE_AD_ROTATION_RATE to 0 for that file. That was the right call: those tests are about the click URL of a paid fill, and the ~10% house-ad coin flip made roughly one CI run in three go red on whichever test lost it. (That's what took down #179 and #183 itself.)

That commit notes rotation is "covered by tests/contract/ads-house-rotation.test.ts, so nothing is lost." It isn't, quite — that file calls houseFill() directly and never goes through serveAd:

import { houseFill } from "@/lib/ads/house";   // never imports serveAd

So it pins what a house ad looks like, not when one is served. With the rate pinned to 0 in the only file that reached it, lib/ads/serve.ts:213 has no test left:

if (Math.random() < HOUSE_AD_ROTATION_RATE) return houseFill(format);

That branch is what keeps the network advertising itself on slots that are already selling. It's a single comparison — the kind of thing that gets dropped or inverted in a refactor and surfaces as a revenue question months later rather than a red build.

Approach

Pinned against serveAd by driving the draw to each side of the threshold, not by sampling a frequency. Sampling a ~10% rate over N fills would mean reintroducing the exact coin flip that made the other file flaky; a controlled draw tests the same behaviour and cannot fail intermittently.

Four tests: draw under the rate → house fill; house fill writes no impression (unmetered — a house ad that metered would bill a publisher for our own promotion); draw exactly on the rate → still paid (the comparison is <); a high draw → paid, which catches an inverted branch that the boundary tests alone would not.

Nothing in ads-short-code-serving.test.ts is touched, so this cannot conflict with #183 or reintroduce its flake.

Verification

Mutation-tested — each mutation applied to lib/ads/serve.ts, suite re-run, source restored:

Mutation Result
< → <= 1 test fails (the boundary)
< → > (inverted) 3 tests fail
branch deleted 2 tests fail
unmodified 4 pass

Full suite on this branch: 1299 passed, 7 skipped. tsc --noEmit clean. The new file runs in ~200ms.

🤖 Generated with Claude Code

#183 de-flaked tests/contract/ads-short-code-serving.test.ts by mocking
HOUSE_AD_ROTATION_RATE to 0 for that file, which was the right call — the
tests there are about the click URL of a paid fill, and the coin flip made
roughly one CI run in three go red on whichever test lost it.

That commit notes rotation is still covered by ads-house-rotation.test.ts.
It isn't, quite: that file calls houseFill() directly and never goes
through serveAd, so it pins what a house ad looks like, not when one is
served. With the rate pinned to 0 in the only file that reached it, the
branch at lib/ads/serve.ts:213 has no test left.

That branch is what keeps the network advertising itself on slots that are
already selling, and it is one comparison — the kind of thing that can be
deleted or inverted in a refactor and show up as a revenue question months
later, not a red build.

So it is pinned here against serveAd, by driving the draw to each side of
the threshold rather than sampling a ~10% frequency. Sampling would mean
reintroducing exactly the coin flip that made the other file flaky; a
controlled draw tests the same behaviour and cannot fail intermittently.

Verified by mutation: '<' to '<=' fails 1 test, inverting the comparison
fails 3, deleting the branch fails 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

ThreatCrush Security Scan

52 finding(s)

HIGH/CRITICAL: 10 | MEDIUM: 42

Severity Rule Location
HIGH secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
HIGH js-ssrf-outbound-request lib/sp/platforms/facebook.ts:115
HIGH secret-generic-credential lib/sp/platforms/linkedin.ts:25
HIGH js-ssrf-outbound-request lib/sp/platforms/telegram.ts:63
HIGH js-ssrf-outbound-request lib/sp/platforms/threads.ts:138
HIGH manifest-typosquat package.json:59
HIGH secret-generic-credential tests/contract/coinpay.test.ts:4
HIGH secret-generic-credential tests/contract/posthog-integration.test.ts:13
HIGH secret-generic-credential tests/lead-campaign.test.ts:16
MEDIUM js-unescaped-html-sink app/(app)/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM sql-template-interpolation app/(app)/projects/[id]/autoblog/actions.tsx:96
MEDIUM js-unescaped-html-sink app/(app)/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM sql-template-interpolation app/(app)/projects/[id]/autoblog/setup/form.tsx:504
MEDIUM sql-template-interpolation app/(app)/projects/[id]/uptime/monitor-actions.tsx:28
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM sql-template-interpolation app/actions/admin.ts:114
MEDIUM sql-template-interpolation app/actions/orgs.ts:328
MEDIUM sql-template-interpolation app/api/lx/keywords/regenerate/route.ts:59
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:196
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:223
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:279
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM sql-template-interpolation lib/audit/checks/security.ts:48
MEDIUM redos-nested-quantifier lib/careers/jobs.ts:139
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:130
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:93
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:367
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:379
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:380
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:1340
MEDIUM sql-template-interpolation lib/lx/articleGen.ts:1362
MEDIUM sql-template-interpolation lib/lx/guestPostGen.ts:109
MEDIUM tls-verification-disabled lib/onion.ts:47
MEDIUM sql-template-interpolation lib/sp/platforms/linkedin.ts:177
MEDIUM sql-template-interpolation scripts/delete-archived-projects.mjs:97
MEDIUM sql-template-interpolation scripts/delete-archived-projects.mjs:102

…and 2 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 9a18a3e into master Aug 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant