Skip to content

feat(spend): the spend alarm names the user - #2238

Merged
lilyshen0722 merged 1 commit into
mainfrom
lily/spend-alarm-names-user
Oct 15, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
lily/spend-alarm-names-user

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

What

The $2-a-day spend alarm (#2227) now names the user: each {"alarm":"user_spend"} line carries the username next to the userId.

This is one of Cofounder's conditions for the Pro per-user ceiling of 300 (TASK-268, recorded on the row): whoever is paged sees who, without a database lookup.

  • The lookup runs only for users who crossed the line, so it's rare and costs one query.
  • A failed lookup still logs the id.
  • Paging SRE as well as Sam is a notification-channel change in the apply script, which is SRE's (asked).

Tests

A new arm on in-memory Mongo with the real User and AgentRun models: the alarm line carries username: 'maya'. The spend suite is 5/5; lint and tsc are 0.

🤖 Generated with Claude Code

https://claude.ai/code/session_015NDyNbwmCco62PAAviSL1k

Cofounder's condition on the Pro run ceiling (2026-10-15): the $2-a-day
alarm names the user, so whoever is paged sees who without a lookup. Each
alarm line now carries the username next to the userId, looked up only for
the users who crossed the line; a failed lookup still logs the id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NDyNbwmCco62PAAviSL1k

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Written by Sprint Review, a Commonly agent — gate requested in pod thread 77999.

Bound to 726967855f9467f33efdcad791532d7128ed5f8d. 2 files, +29/−0. Level with main, 0 conflict markers. BASE 5/5. CI 13 pass, 2 skipping, Service Tests (Tier 1 — real DBs) pending. eslint clean; tsc 50, the baseline.

Removing the username from the line reds the new arm, so the feature itself is pinned.

The shared-file worry does not materialise. You flagged that this shares spendAlarmService.ts with #2232 and offered to rebase whichever lands second. I ran git merge-tree on the pair: 0 conflict markers. #2232 touches userSpendForDay's pricing, this touches checkSpend's log line, and they do not overlap — so no rebase contingency is needed for these two heads.

The property you flagged is true, and it is the one thing the arm cannot check

You asked me to look at "a username only, never an email." It holds in the code, verified two ways: the projection is .select('username') (:138), and the string email appears zero times anywhere in the service. The projection is the guard and it is the right guard.

But nothing pins it:

mutation result
username removed from the line 1 failed
a literal email: added to the logged object 5 passed
the failed-lookup catch changed to rethrow 5 passed

The arm asserts with expect.objectContaining({ userId, username }), and objectContaining is a subset matcher — it pins what must be present and is structurally incapable of noticing something extra. So a future .select('username email') with the field added to the line ships green, which is exactly the regression the condition was written to prevent.

The ingredients for a real assertion are already in the fixture: the test creates maya with email: 'maya@example.com', so the negative control exists and simply is not asserted. One line closes it — expect(line).not.toHaveProperty('email'), or an exact toEqual on the whole shape.

Row three is the same gap on the other stated behaviour: "A failed lookup still logs the id" is unpinned too. Inverting the catch to rethrow keeps 5/5, so nothing checks that a lookup failure still produces an alarm line rather than losing it to the scheduler's own catch.

A pattern worth naming, since this is its third appearance in this series. On #2232 the blind spot was toMatchObject({ envs: {} }), where an empty nested object matches anything; here it is objectContaining and an absent key. Both are subset matchers, and both were used to express a negative property — "no environment", "no email". Subset matchers cannot carry a negative. When the requirement is that something must not appear, it wants not.toHaveProperty or an exact-shape assertion; otherwise the test documents the intent without defending it.

Everything else is sound: the lookup runs only when fresh.length so there is no query on a quiet pass, ids are filtered to 24-hex before the $in, and usernames.has() keeps a username: undefined key out of the line when a user has none.

GATE: PASS

@lilyshen0722
lilyshen0722 added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 975e0c3 Oct 15, 2026
17 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