fix: unwedge CI, and three real defects the broken test harness was hiding #4040
Files changed
Side-by-side diff for claude/bug-finding-mission-gegp06 → main.
Cross-repo impact
See what breaks downstream if this PR merges.
Risk score: calculating…
Refresh in a moment to see the pre-merge risk score for this PR.
Gate checks
1 of 6 failingGateTestDisabled in settingsSkipped
Secret scanAuto-redacted 13 secrets (12a493b)Passed
Security scanStatic rules ran (8 checks, no AI required) — no static findings. AI review unavailable: The platform's AI balance is exhausted, so AI generation is temporarily unavailable. Nothing was lost. You can queue this as a repair for the internal agent from the repository's Health page, or try again once the balance is restored.Skipped
Merge checkMerge conflicts detected — AI auto-resolution will be attempted on mergeFailing
CINo CI workflows are configured for this repositorySkipped
AI ReviewAIAI review approvedPassed
Resolve conflicts manually
GlueCron AI will attempt auto-resolution when you press Merge. If it cannot, or you would rather not wait on it, the manual path below always works.
7 conflicting files between
claude/bug-finding-mission-gegp06 and main:- CLAUDE.md
- apps/api/src/routes/messages.ts
- apps/api/src/routes/sentiment-timeline.ts
- apps/api/tests/ai.test.ts
- apps/api/tests/transactional-email.test.ts
- apps/api/tests/vapron.test.ts
- services/ai-engine/package.json
git fetch origin git checkout claude/bug-finding-mission-gegp06 git merge origin/main # fix the files listed above, then: git add -A && git commit git push origin claude/bug-finding-mission-gegp06
After you push, the merge check re-runs automatically on the next page load and the Merge button unblocks once it passes.
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts
Originally written by @ccantynz-alt on GitHub.
Imported from https://github.com/ccantynz-alt/AlecRae.com/pull/91
Description
A bug-finding pass that started by running the gates instead of trusting the docs. The first finding was that CI was red on
mainfor both required status checks, so no PR could merge at all —bun run lintfailed on two pre-existing errors andbun run testhad 32 failures. The branch protection added in issue #106 to stop main breaking had instead wedged the repo shut; the only reason nobody hit it is thatenforce_adminsis deliberately off (#66), so admin direct-pushes kept landing.Root-causing the test failures turned up the bug underneath, and that bug was hiding two more.
#121 —
@alecrae/ai-engine's export map pointed every subpath'simportcondition at adist/tree that is never built (the package'sbuildscript is a bareecho). Bun resolves thebuncondition, so production and typecheck were clean — but vitest resolvesimport, so 20 tests died at module load. Among them: hard quota enforcement, suppression-list enforcement at send time, andPOST /v1/messages/sendvalidation. Three real protections whose tests had not executed for as long as that map has been in this shape.For the audit trail: issue #29 recorded this as "a pre-existing, unrelated vitest↔
@alecrae/ai-enginedist-resolution gap" and wrote off 6 voice-message tests as unrunnable in the sandbox. That diagnosis was wrong — it was the export map, and all 6 now pass.#122 — monthly email quota was silently unenforced whenever Redis was unavailable.
checkQuota()'s DB fallback countseventsrows of typeemail.queued, and nothing in the codebase ever inserted one — the only reference to that string anywhere was quota.ts's ownSELECT. The fallback counted 0 forever, soallowedwas unconditionallytrueand the 429 reported "0 sent".incrementQuota()early-returned on no-Redis behind the comment "DB counter is updated separately by the existing code", describing code that does not exist. Not a corner case:getRedis()returnsnulluntil ioredis's asyncreadyfires, so the first send after every API restart took this path, and per CLAUDE.md's own infra history the box ran with no redis-server installed at all until 2026-07-14.#123 — plan-tier resolution failed open to PAID tiers in three places, inverting what
plan-gate.ts(#86) exists to do:tierclaim became"starter"and passedrequirePlan("personal");"pro"outright — a transient Postgres blip unlocked all 60requirePlan("pro")mounts (every Claude-backed endpoint) with no spend ceiling;"starter"while every login/register/OAuth path already defaulted to"free", so a user's tier could silently move upward just by refreshing a token.Also rewrote the three vapron test files, which still asserted the guessed tRPC transport that #83 replaced — they weren't merely failing, they encoded known-wrong behaviour as the expectation.
Reported, deliberately not fixed (#124)
Each needs a decision that isn't mine to make unilaterally:
getClientIp()readsCF-Connecting-IPfirst with no trusted-proxy verification anywhere in the repo. Any client reaching the API other than through Cloudflare sets it to a fresh random value per request and gets a fresh bucket, defeating the 10/min brute-force cap on/v1/auth/*and compounding #117. Fixing it requires knowing which proxy is authoritative and whether the origin is reachable off-Cloudflare — getting it wrong breaks real IP attribution.middleware/idempotency()has no in-flight lock, only a post-hoc response cache, and it's mounted onPOST /v1/messages/send. Two concurrent requests with the sameIdempotency-Keyboth send for real. Sequential retries (the documented case) do work.Type of change
Testing
Gates, all from a clean
bun install:bun run lintbun run typecheckbun run test21 new tests. The two behavioural fixes were verified to genuinely catch their bug — I reverted each fix and confirmed the tests fail against the old code, then restored:
tests/quota-db-fallback.test.ts(8) — runs quota.ts with Redis pointed at a closed port. 3 fail pre-fix. The load-bearing one asserts the writer and reader agree on the exact event-type string, since the defect was the two sides disagreeing, not either being wrong alone. Noting honestly: the other 5 document the limit math but do not discriminate pre/post-fix.tests/tier-fail-closed.test.ts(13) — asserts the fail-closed contract at the tier→plan-gate boundary, and pins the DB-vs-API spelling bridge ("professional"→"pro"), since regressing that 403s every paying Pro customer on every Pro-gated route — the opposite failure mode.tests/vapron.test.tsrewritten (15) — covers both transports separately so they can't be swapped again silently.Unit tests added/updated
Integration tests added/updated
Manual testing performed
One thing I could not verify and am flagging rather than glossing: the
email.queuedrow is a new write on the send path. It's fire-and-forget and tested against a mocked DB, but it has not run against live Postgres.Checklist
bun run test)bun run lint && bun run typecheck)bun run docs:checkpasses — 62 docs)Breaking changes
None to any public API. Two behaviour changes worth calling out for review:
"free"instead of a paid tier. Deliberately fail-closed: worst case is a paying customer briefly seeing an upgrade prompt; worst case of the old direction was uncapped AI spend on a free account. Normal resolution paths are unaffected —"professional"→"pro"still works and is now covered by a test.No migration required.
email.queuedalready exists in theemail_event_typeenum.Screenshots
N/A
Generated by Claude Code