Repository navigation
fix(audit): show the price a line sold for, not its list price - #759
Conversation
The Details cell rendered every SalesOrder.AddItem row as "at list $X" with X taken from listUnitPriceMinorUnits, so a line sold below, at, or above list all read identically and none of them named the price that was actually charged. On the simulation fixture's SO-1318252C, three of four lines were wrong; the worst read "at list $0.45" for a line sold at $0.20. The branch read unitPriceMinorUnits, used it only as a null guard, then discarded it whenever a list price existed. It shipped this way in #745 and did not drift. An off-list line now renders both numbers — "at $0.20 (list $0.45)" — and no marker word in either direction, because #719's open question 4 treats a markup as not a discount. At-list and no-list-price lines are unchanged. All three keys already existed in en, es and tl; no new strings. The previous AddItem test asserted /\$0\.45/ against that payload, which pinned the defect rather than the behaviour. It is replaced by four tests asserting the cell's literal text, each watched red under a mutation. Closes #758
|
Warning Review limit reachedNext included review available in 27 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthrough
ChangesAudit AddItem price rendering
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to Audit details now display sold and list prices as intended for all supported price cases. No merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review Context for the review: this is a one-branch rendering fix in Two things worth aiming at specifically:
Backend was verified correct against the seeded database — the payload carries sale and list prices as separate, correct fields — so no API or handler change is in scope here. |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== changed files ==='
git diff --name-status HEAD^ HEAD
echo '=== AuditPage diff ==='
git diff --unified=80 HEAD^ HEAD -- web/src/routes/AuditPage.tsx
echo '=== AuditPage test diff ==='
git diff --unified=100 HEAD^ HEAD -- web/src/routes/AuditPage.test.tsx
echo '=== AuditPage outline ==='
ast-grep outline web/src/routes/AuditPage.tsx --items all
echo '=== AuditPage test outline ==='
ast-grep outline web/src/routes/AuditPage.test.tsx --items all
echo '=== price translation keys and renderer references ==='
rg -n -C 4 'detailsAtPrice|detailsAtList|detailsListParen|unitPriceMinorUnits|listUnitPriceMinorUnits' web/srcLength of output: 82857 @mforce No findings. The
The replacement tests constrain the behavior. Each test uses different sale and list values. Each test asserts the full Details cell text. The assertions therefore detect a swapped value, a missing value, an incorrect price order, an incorrect at-list collapse, and the null-list zero-price regression. The above-list test also explicitly rejects direction-marker words. The existing translation keys support this composition in all three locale files. No API or backend change is indicated. ✅ Action performedReview finished.
|
|
@coderabbitai full review Triggering the deep form deliberately, and this is the single deeper pass this PR will take — whatever it returns, the loop stops here. Why this one earns it. The previous clean pass was CodeRabbit's incremental profile, and the diff touches the money class: it decides which of two prices an audit record publishes. An audit row is evidentiary and not re-derivable from anywhere else in the system, so a defect here is expensive to detect later — which is exactly the case the earlier round already proves, since the original bug shipped through review and a full green suite and only surfaced when #721 printed a discount reason under four rows all claiming to have sold at list. Worth aiming at, in rough order of what would hurt most:
Previously verified and not in scope: the backend writes sale and list prices as separate correct fields (checked against the seeded DB — 20 AddItem payloads covering a markup, a discount and a null list), and the before/after screenshots were captured from stacks rebuilt at each revision over one unchanged fixture. |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== PR diff: renderer and tests ==='
git diff --unified=100 HEAD^ HEAD -- web/src/routes/AuditPage.tsx web/src/routes/AuditPage.test.tsx
echo '=== Renderer outline ==='
ast-grep outline web/src/routes/AuditPage.tsx --items all
echo '=== Test outline ==='
ast-grep outline web/src/routes/AuditPage.test.tsx --items all
echo '=== Price fields: declarations and renderer/test use ==='
rg -n -C 5 --glob '*.{ts,tsx}' \
'unitPriceMinorUnits|listUnitPriceMinorUnits|SalesOrder\.AddItem' \
web/src
echo '=== Relevant translation keys in locale resources ==='
rg -n -C 4 \
'detailsAtPrice|detailsAtList|detailsListParen' \
web/src
echo '=== Tests that render AuditPage under non-English locales ==='
rg -n -C 5 --glob '*.{ts,tsx}' \
'AuditPage|AuditDetails|locale|language|i18n' \
web/src/routes/AuditPage.test.tsx web/srcLength of output: 201114 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== changed hunks only ==='
git diff --unified=25 HEAD^ HEAD -- web/src/routes/AuditPage.tsx web/src/routes/AuditPage.test.tsx
echo '=== renderer: relevant lines ==='
rg -n -C 18 --glob 'AuditPage.tsx' \
'SalesOrder\.AddItem|detailsAtPrice|detailsAtList|detailsListParen|unitPriceMinorUnits|listUnitPriceMinorUnits' \
web/src/routes
echo '=== tests: relevant lines ==='
rg -n -C 22 --glob 'AuditPage.test.tsx' \
'addItemEvent|detailsText|SalesOrder\.AddItem|at list|list \$|discount|markup|unitPriceMinorUnits|listUnitPriceMinorUnits' \
web/src/routes
echo '=== API declarations: price field types ==='
rg -n -C 12 --glob 'cluckwork.ts' \
'unitPriceMinorUnits|listUnitPriceMinorUnits|Audit' \
web/src/api
echo '=== translation values ==='
for file in web/src/i18n/en.ts web/src/i18n/es.ts web/src/i18n/tl.ts; do
echo "--- $file ---"
rg -n -C 2 'detailsAtPrice|detailsAtList|detailsListParen' "$file"
done
echo '=== installed language resources ==='
rg -n -C 4 'RESOURCES|TRANSLATED_NAMESPACES|audit' \
web/src/i18n/index.ts web/src/i18n/translations-status.ts 2>/dev/null || trueLength of output: 49093 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo '=== AuditPage localization tests ==='
rg -n -C 12 --glob 'AuditPage.test.tsx' \
'changeLanguage|i18n\.|detailsAtPrice|detailsAtList|detailsListParen|es|tl' \
web/src/routes
echo '=== Audit payload writer, schema, and contract references ==='
rg -n -C 10 \
'SalesOrder\.AddItem|listUnitPriceMinorUnits|unitPriceMinorUnits' \
--glob '*.{cs,json,ts,tsx}' \
. \
-g '!web/src/routes/AuditPage.tsx' \
-g '!web/src/routes/AuditPage.test.tsx' \
| head -n 1200
echo '=== Price value validation at order-write boundaries ==='
rg -n -C 8 \
'unitPriceMinorUnits|expectedListUnitPriceMinorUnits|listUnitPriceMinorUnits' \
--glob '*.cs' \
. \
| head -n 1000Length of output: 201089 @mforce No findings. The equality check is correct for the accepted payload shape. The renderer accepts only numeric JSON values. It compares minor-unit numbers. There is no supported shape where equal accepted values are unequal with The null guard is correct. A null or non-numeric The off-list composition is correct in all installed languages:
The literal space produces the required separation in each case. The four tests constrain the price contract. They reject a swapped field, a missing sale price, a missing list price, reversed price order, an incorrect at-list collapse, and null rendered as The tests use English text. A future implementation that hardcodes the exact English strings could pass them. The reviewed implementation uses the existing translation keys, and the installed translations support the composition correctly.
|
🤖 I have created a release *beep* *boop* --- ## [0.1.0](v0.0.4...v0.1.0) (2026-09-12) ### ⚠ BREAKING CHANGES * log in by farm code, with per-account email identity ([#532](#532)) (#564) ### Features * **accounts:** add Account.Slug (farm code), suspend/reactivate, list-accounts verb ([#531](#531)) ([3fe9754](3fe9754)) * **accounts:** provision additional farms ([#581](#581)) ([006f298](006f298)) * add Aspire local development AppHost ([#567](#567)) ([2c9e6b9](2c9e6b9)) * add configurable worker sale allocation ([#619](#619)) ([0955095](0955095)) * add searchable entity pickers ([#642](#642)) ([60d2053](60d2053)) * **api:** provision-account takes an optional --timezone at creation ([#603](#603)) ([#694](#694)) ([a0aee39](a0aee39)) * **audit:** show the sales-line audit payload as a readable Details column ([#745](#745)) ([#749](#749)) ([d26d389](d26d389)) * **auth:** add ApplicationUser.StepUpLogoutEpoch column ([#338](#338)) ([#554](#554)) ([18306ee](18306ee)) * certify over-cap simulation fixture bands ([#633](#633)) ([a67b2e1](a67b2e1)), closes [#627](#627) * **cli:** rename-account verb to change a farm code ([#732](#732)) ([#733](#733)) ([4b70559](4b70559)) * **customers:** edit existing customer details ([#625](#625)) ([#626](#626)) ([062a55c](062a55c)) * **jobs:** single-runner leader gate for the durable job worker ([#271](#271)) ([#555](#555)) ([4148f9b](4148f9b)) * let owners change user email addresses ([#605](#605)) ([842347b](842347b)) * log in by farm code, with per-account email identity ([#532](#532)) ([#564](#564)) ([68adb62](68adb62)) * **ratelimit:** distributed IP-keyed auth limiters ([#544](#544)) ([#558](#558)) ([ec14972](ec14972)) * **ratelimit:** distributed per-account report concurrency cap with local-ceiling fallback ([#545](#545)) ([#559](#559)) ([1522e4e](1522e4e)) * **sales:** mark discounted lines, total the discount, and show it in the Orders list ([#723](#723), [#724](#724)) ([#741](#741)) ([1a07441](1a07441)) * **sales:** record list, old and new price in the order-line audit payload ([#722](#722)) ([#742](#742)) ([97c866f](97c866f)) * **sales:** refuse an over-ceiling confirm from a Sales user ([#727](#727)) ([#766](#766)) ([8c0792a](8c0792a)) * **sales:** show what each order still owes, and filter the list to unpaid ([#771](#771)) ([ca59d68](ca59d68)) * **sales:** snapshot the list price on the order line and show the discount ([#734](#734)) ([cffed5e](cffed5e)) * **sales:** snapshot the product name and unit in the order-line audit payload ([#747](#747)) ([#748](#748)) ([0481c06](0481c06)) * scope Worker reads to assigned flocks ([#388](#388)) ([#611](#611)) ([5884a9a](5884a9a)) * shared-state ports with Redis + in-process fallback ([#543](#543)) ([#552](#552)) ([f767fa9](f767fa9)) * suspend-account / reactivate-account operator verbs ([#534](#534)) ([#573](#573)) ([d0be26c](d0be26c)) * **tenancy:** write-side tenant guard + single-assignment TenantContext ([#546](#546)) ([#561](#561)) ([f371f1d](f371f1d)) * **web:** dashboard rework — capture-status tiles, 14-day trend, stock as a stacked bar ([#654](#654)) ([396ba23](396ba23)) * **web:** date-range filters on audit and expenses, and the stock lot filter gets its bounded toolbar ([#666](#666), [#667](#667), [#653](#653)) ([94b188f](94b188f)) * **web:** elevation hierarchy and sentence-case labels ([#651](#651), [#652](#652)) ([#661](#661)) ([28db4c7](28db4c7)) * **web:** Expenses and Audit keep a clear-filters control while rows are still showing ([#679](#679)) ([#697](#697)) ([b859982](b859982)) * **web:** expenses filters by a date range like its sibling screens ([#667](#667)) ([f13858f](f13858f)) * **web:** key the farm brand palette per farm ([#586](#586)) ([#600](#600)) ([7183a43](7183a43)) * **web:** let operators forget remembered farms ([#598](#598)) ([577d94e](577d94e)) * **web:** one-line provenance, bounded date filters, and empty states that invite action ([#653](#653), [#655](#655)) ([#668](#668)) ([80b53f4](80b53f4)) * **web:** prefill the farm code from ?farm= and remember it ([#535](#535)) ([#588](#588)) ([b7f5cc6](b7f5cc6)) * **web:** split authenticated routes into lazy chunks ([#620](#620)) ([5089271](5089271)) * **web:** the audit log filters by a date range, and says which window is empty ([#666](#666)) ([63027e0](63027e0)) * **web:** typeset numbers as numbers and refresh the Help glossary ([#650](#650), [#657](#657)) ([af4fe11](af4fe11)) ### Bug fixes * **api:** order same-instant audit events by a durable monotonic key ([#700](#700)) ([8fcf084](8fcf084)) * **api:** print the farm code from bootstrap-admin ([#589](#589)) ([#594](#594)) ([34032ac](34032ac)) * **audit:** show the price a line sold for, not its list price ([#759](#759)) ([e6b37d0](e6b37d0)) * **audit:** store catalog enums by name and guard the add-item transaction shape ([#751](#751)) ([23609ff](23609ff)) * **auth:** reject invalid account claims ([#622](#622)) ([8d6c7fe](8d6c7fe)) * **auth:** require step-up for durable user access ([#360](#360)) ([#607](#607)) ([f767dce](f767dce)) * **ci:** bound the npm audit calls and give the web job room to finish ([#686](#686)) ([153b7a8](153b7a8)) * **ci:** escalate the audit bound to SIGKILL, so it actually bounds ([#686](#686)) ([a0c8f4e](a0c8f4e)) * **ci:** fail closed on invalid vulnerability config ([#621](#621)) ([1690db8](1690db8)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([efb05e6](efb05e6)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([8986d77](8986d77)) * **ci:** remove invalid XML comment from nuget.lockfix.config ([#541](#541)) ([5f1bc0a](5f1bc0a)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([aaf6934](aaf6934)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([64f1f53](64f1f53)) * **i18n:** tl help text names the saleable flag and unit-system setting what their labels call them ([#688](#688)) ([#696](#696)) ([bfd24d7](bfd24d7)) * **infra:** AccountId must be a non-nullable Guid or both tenant write layers refuse ([#673](#673)) ([#695](#695)) ([2470c4e](2470c4e)) * require step-up for flock scope changes ([#609](#609)) ([4151f89](4151f89)) * **sales:** keep a line's discount markers agreeing while its price is edited ([#752](#752)) ([#753](#753)) ([c159b4b](c159b4b)) * **sales:** say which kind of missing list price a line has ([#774](#774)) ([489180e](489180e)) * scope legacy logout to selected farm ([#624](#624)) ([fae8d82](fae8d82)) * **seed:** drain the daily-entry lock sweep so deep simulation fixtures validate ([#644](#644)) ([730fa23](730fa23)), closes [#638](#638) * **tenancy:** AccountId is a concurrency token, so the database refuses a detached cross-tenant write ([#562](#562)) ([4d1dfa3](4d1dfa3)) * **tenancy:** AspNetUserRoles carries a tenant column, so a role write naming another farm's user is refused ([#670](#670)) ([fc0552a](fc0552a)) * **tests:** bump the image-pin allow-list counts for the AppHost LocalPorts tests ([#593](#593)) ([58d3056](58d3056)) * **tests:** the OTLP collector survives a lost port race and ignores traffic that is not an export ([#672](#672), [#676](#676)) ([#677](#677)) ([965c737](965c737)) * **web:** a scoped audit view filtered to nothing names both the record and the range ([#666](#666)) ([41bbfe1](41bbfe1)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Customers, Daily Entry, Flocks, Grades and Products ([#703](#703)) ([#705](#705)) ([85605db](85605db)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Inventory, Expenses, History and Stock ([#703](#703)) ([#706](#706)) ([60a4997](60a4997)) * **web:** an abandoned edit's success no longer hijacks the dialog that replaced it on Users ([#703](#703)) ([#710](#710)) ([778faab](778faab)) * **web:** an abandoned order attempt's success no longer hijacks the dialog that replaced it ([#702](#702)) ([522c699](522c699)) * **web:** capture screens open on the flock you last used, and assigning one no longer guesses ([#646](#646)) ([#699](#699)) ([7f8f317](7f8f317)) * **web:** constrain dialog session helpers to declared scopes ([#715](#715)) ([389e3c8](389e3c8)) * **web:** date validation gets one boundary table instead of one case per review round ([#666](#666)) ([215f830](215f830)) * **web:** keep a paged window and an item panel on the user's newest intent ([#645](#645)) ([d81bccf](d81bccf)) * **web:** keep Sales order panels closed after pending writes ([#711](#711)) ([f0f7492](f0f7492)) * **web:** keep Sales panels closed after pending Open reads ([#716](#716)) ([620411f](620411f)) * **web:** make login take the cross-tab cookie lock so a racing refresh cannot restore the wrong session ([#648](#648)) ([ff18beb](ff18beb)) * **web:** make the entity picker read as a search field and focus it on open ([#736](#736)) ([66ef667](66ef667)), closes [#735](#735) * **web:** page truncated customer and movement tables with usePagedList ([7cfe4d6](7cfe4d6)) * **web:** reconcile Sales line edits with refreshed orders ([#717](#717)) ([d7dd2c9](d7dd2c9)) * **web:** the audit date filter accepts low-numbered years, and its empty state covers every narrowing ([#666](#666)) ([af52d25](af52d25)) * **web:** the audit date filter rejects impossible dates, and its history guard actually guards ([#666](#666)) ([8d51846](8d51846)) * **web:** the expense range bounds are not capped at today, which the month-end default exceeds ([#667](#667)) ([7e01864](7e01864)) * **web:** the help text calls the expiry field what the field calls itself ([#666](#666)) ([2fd1f3c](2fd1f3c)) * **web:** the stock lot date range sits in the bounded toolbar ([#653](#653)) ([43dec5e](43dec5e)) ### Refactoring * **web:** extract SalesPage's dialog-write wrapper into a shared useDialogAction hook ([#703](#703)) ([#704](#704)) ([60ee9d9](60ee9d9)) ### Documentation * add k6 preparation steps to the dev-database fixture runbook ([#643](#643)) ([a4f1f09](a4f1f09)) * add runbook for loading the simulation fixture into a dev database ([#639](#639)) ([2d143b8](2d143b8)) * **agents:** a PR closes its issue from the body, not the title ([#744](#744)) ([39be13c](39be13c)) * **agents:** drop the commit and push gate, and require screenshots on UI changes ([#757](#757)) ([6225172](6225172)) * **agents:** find guards by grepping registry readers; amend issues a PR overtakes ([#580](#580)) ([fe3fde8](fe3fde8)) * **agents:** the Playwright specs have been in CI since 2026-08-08 ([#768](#768)) ([68ee612](68ee612)) * **aspire:** record the second local database and pin the AppHost dashboard ports ([#623](#623)) ([713b941](713b941)) * compress AGENTS.md to one paragraph per rule, and draw the two orders that matter ([#551](#551)) ([997ae8a](997ae8a)) * item 7 names each screen's actual initial filter value ([#666](#666)) ([70a53d8](70a53d8)) * multi-farm tenancy decision record and AGENTS/GLOSSARY sync ([#537](#537)) ([#601](#601)) ([2c34771](2c34771)) * name the scoped filtered-empty key and state the [#653](#653) relationship plainly ([#666](#666)) ([0e93dac](0e93dac)) * note that a PackageReference in Directory.Build.props is invisible to the dependency graph ([4845724](4845724)) * **plans:** commit the [#722](#722) and [#745](#745) design records ([#754](#754)) ([c942fcd](c942fcd)) * record [#579](#579) as won't-fix — suspension is immediate for use, not issuance ([#582](#582)) ([7a3be40](7a3be40)) * record the [#508](#508) audit ordering key and the tracked-file guard lesson ([#701](#701)) ([08964e9](08964e9)) * **runbooks:** add procedure to rename the default farm's code after upgrade ([#731](#731)) ([2f6e242](2f6e242)) * screenshots of the running SPA in the README ([#550](#550)) ([711488a](711488a)) * **sim:** commit the dashboard screenshot, capture the palette matrix, and record the [#651](https://github.com/mforce/cluckwork/issues/651)/[#652](https://github.com/mforce/cluckwork/issues/652) conventions ([#660](#660), [#662](#662), [#663](#663), [#664](#664)) ([#665](#665)) ([930ea30](930ea30)) * specify searchable entity picker ([#641](#641)) ([91d4300](91d4300)) * split the README into audience-scoped docs and adopt repo-template scaffolding ([#548](#548)) ([b3f3fcf](b3f3fcf)) * surface Aspire local development workflow ([#568](#568)) ([a343baa](a343baa)) * **web:** record the per-screen idempotency-key policies and runWrite's refresh contract ([#703](#703)) ([#707](#707)) ([8bee651](8bee651)) * **web:** the date-cap help text covers every stocked item, not only feed ([#666](#666), [#667](#667)) ([c8433c5](c8433c5)) * **web:** the help text claims only what is true of recording, and says nothing about filter caps ([#666](#666), [#667](#667)) ([e2f63d1](e2f63d1)) * **web:** the help text describes the date-range filters that shipped ([#666](#666), [#667](#667)) ([c3275b7](c3275b7)) * **web:** the help text stops describing a cap the filters no longer have ([#666](#666), [#667](#667)) ([49654cd](49654cd)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>




TL;DR
The audit Details cell printed each sales line's list price as though it were the price the line sold for.
AuditDetailsreadunitPriceMinorUnits, used it only as a null guard, then discarded it whenever a list price existed — so everySalesOrder.AddItemrow renderedat list $<list>, whatever it actually sold for.One branch in one component, plus tests. No API, payload, migration or i18n-key change.
The defect, on fixture order
SO-1318252Cat list $0.18❌at $0.15 (list $0.18)at list $0.22❌at $0.30 (list $0.22)at list $0.30✅at list $0.30at list $0.45❌at $0.20 (list $0.45)Three of four were wrong. The worst read as a line selling at $0.45 that sold at $0.20.
Introduced by d26d389 (#749, for #745) — the commit that built the Details column. It shipped this way; it did not drift. It survived review and every test because
at list $0.45is a plausible, correctly formatted number, and nothing on screen contradicted it until #721 began printingDiscount reason: Volumedirectly beneath four rows all claiming to have sold at list.Rendering contract
list === null)at $0.15unit === list)at list $0.30at $0.20 (list $0.45)An off-list line gets no marker word in either direction. #719's open question 4 deliberately treats a markup as not a discount, so the cell states both facts and lets the reader see the direction rather than editorialising. That reasoning is a comment in the code, not just here.
No new translation strings.
detailsAtPrice,detailsAtListanddetailsListParenalready existed in en, es and tl (3/3 each);detailsListParenis the same key theUpdateItembranch already renders in the same position, so per #688 each locale keeps its own existing wording for list price rather than a fresh translation.Reviewer entry points
web/src/routes/AuditPage.tsx— the whole behaviour change is theSalesOrder.AddItemternary, ~6 lines. Everything else in the diff is the comment above it.web/src/routes/AuditPage.test.tsx— the four new cases.One deletion to look at deliberately
The previous test, "renders an AddItem payload as the artboard's summary", asserted
/\$0\.45/against a payload withunitPriceMinorUnits: 40, listUnitPriceMinorUnits: 45. It pinned the defect, not the intended behaviour — it passed precisely because the wrong number was on screen. It is replaced rather than weakened, by four tests asserting the cell's literal full text (toBe, on the cell'stextContent, which is what pins the order of the two prices — a regex on one number is what let this ship).Mutation evidence
Every new test was watched fail before being claimed. Three mutations, each reverted;
git grep -n -e MUTANT -e 'DEBUG-' -- src tests webreturns nothing.money(list)in the off-list branch)expected 'Large Eggs ×240 at list $0.45' to be 'Large Eggs ×240 at $0.20 (list $0.45)'unit === listcollapseexpected 'Large Eggs ×240 at $0.30 (list $0.30)' to be 'Large Eggs ×240 at list $0.30'list === nullshort-circuitingexpected 'Large Eggs ×240 at $0.15 (list $0.00)' to be 'Large Eggs ×240 at $0.15'— M3 reproduces #720's zero-price failure mode exactlyAll four tests are individually load-bearing: no mutation reddened a test that was not aimed at it.
Gates
Web-only — nothing outside
web/changes, so no .NET build and no integration suite.Sibling check
grep -rn listUnitPriceMinorUnits web/srcfinds one other renderer,SalesPage.tsx, which reads the sale and list prices separately and is the screen the issue confirms was already correct. No sibling defect; no coverage added there.Risk
Low. Read-only rendering of a payload field already present on every row, in one component, behind four literal-text tests. No backfill concern: the change is presentational, so historical rows render correctly the moment this ships. The at-list and no-list-price paths are byte-identical to before and pinned as regressions.
Closes #758
Summary by CodeRabbit