Open-source Chrome extension that estimates, on request, how likely a Reddit post is to be promotional. Not affiliated with Reddit.
npm test # run everything once
npm run test:watch # re-run on change
npm run check # typecheck + tests + build
Framework: vitest. DOM tests run in jsdom and opt in with a // @vitest-environment jsdom comment.
| File | Covers |
|---|---|
packages/shared/test/scoring.test.ts |
clamping 0-100, category caps, style-only cap, correlation discount, counter-signals, clear/missing/unclear/unknown disclosure, reach separate from promotion, high upvotes alone never raise the score, comment accusations (+2 max, independent concerns, coordinated -> no points + lower confidence, unverified linked post discounted, author admission), confidence rules, three reasons max |
packages/shared/test/detectors.test.ts |
each local detector: calls to buy/register/download, waitlist, coupon/referral, affiliate URL params, redirect domains, DM requests, “comment interested”, product links vs Reddit/general hosts, repeated naming, story arc, workflow insertion, marketing/testimonial language, clear and unclear disclosure, Brand Affiliate label, limitations/alternatives, useful-without-links, availability notes |
packages/shared/test/schemas.test.ts |
request/response schema validation (example request accepted, missing title, oversized body, out-of-range score, unknown enum values, >3 reasons) |
packages/shared/test/hash.test.ts |
deterministic hashing; content changes rehash; vote changes do not |
apps/api/test/server.test.ts |
health, valid analyze -> schema-valid, cache hit on repeat, 400/413/415/404, CORS allow/deny + preflight, 429 rate limit |
apps/api/test/cache.test.ts |
memory cache expiry/eviction/clear, rate limiter window |
apps/extension/test/adapter.test.ts |
extraction from sanitized fixtures, links/domains, ring mounting with the title slot, stable keys, detail URL detection, end-to-end fixture scores (organic, transparent founder, undisclosed workflow, high-upvote organic) |
apps/extension/test/adapter-detail.test.ts |
comment extraction on a detail page, OP flagging, unsupported accusation stays minimal |
apps/extension/test/scanner.test.ts |
one ring per post, no duplicates on repeated scans, dynamically inserted post detected via MutationObserver, removed posts cleaned up, stop removes everything, IntersectionObserver gating |
apps/extension/test/cache.test.ts |
chrome.storage-style cache: 24 h expiry, TTL change at read time, pruning, clear keeps settings |
apps/extension/test/queue.test.ts |
concurrency limit, duplicate keys, cancellation of pending/running tasks, priority, error isolation |
apps/extension/test/apiClient.test.ts |
validated results, malformed/non-JSON/HTTP errors, unreachable server, max two concurrent, timeout |
apps/extension/test/fixtures/ contains sanitized, synthetic HTML modelled on www.reddit.com markup. None of it is live Reddit content; do not add real posts, usernames or comments to committed fixtures.
| Fixture | Represents |
|---|---|
organic-advice.html |
genuine advice, limitations discussed, no links |
founder-transparent.html |
“I’m the founder” + product link + limitation |
undisclosed-workflow.html |
multi-tool workflow where only one obscure tool is linked (with a referral parameter), no disclosure |
high-upvote-organic.html |
organic PSA with 24k upvotes |
accused-comments.html |
detail page; one unsupported “this is an ad” comment; author answers publicly |
dynamic-insert.html |
feed plus a <template> the scanner test inserts later (scanner is generic; the extension only points it at the main post of a post page) |
The content script prints [PromoLens] lines to the page console at the Verbose level (open DevTools > Console > set the level filter to include “Verbose”): ring attached, queued, extracted, local result, cache lookup, enrich. Warnings appear for anything unexpected.
Two things that look like bugs but are not:
IntersectionObserver for hidden tabs, so no post is ever “near the viewport” until the tab is in the foreground. Switch to the tab and the queue drains.chrome://extensions, content scripts in already-open Reddit tabs can no longer reach the new worker. Reload the Reddit tab too.Build (npm run build), load apps/extension/dist unpacked, then walk through:
[PromoLens] analyse requested lines in the console while scrolling).fetched post page with body and comment counts.<h1> title within ~1 s; related posts further down get nothing.aria-expanded toggles.npm run dev:api, click Check connection: reports connected (provider: mock).MockProvider to return {}): rings keep local results; no errors in the page console beyond a warning.The popup only offers “use my own OpenAI key”. To use apps/api instead (evaluation, other providers, a shared server), open the extension’s service-worker console from chrome://extensions and run:
chrome.storage.local.get("promolens:settings", ({ "promolens:settings": s }) =>
chrome.storage.local.set({ "promolens:settings": { ...s, apiEnabled: true, aiProvider: "local-api", apiBaseUrl: "http://127.0.0.1:8787" } }));
The popup then shows the local-API fields (URL and Check connection) until you switch back to own-key mode.
ANALYSIS_PROVIDER=openai and no key, npm run dev:api fails fast with a clear message.openai:<model>; rings update a second time within ~5 s; the popover reasons quote the post.LOG_RAW_CONTENT unset: the API log shows only routes, statuses, timings and claim counts.chrome://extensions > PromoLens > Details shows only “Read and change your data on www.reddit.com” and storage; nothing else.