test(store): cover the cart store, drop unused Tailwind, refresh pre-cart copy #51
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "chore/49-cart-tests-tailwind"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #49.
Three unrelated small things, batched as the issue describes.
What changed
1.
apps/store/lib/cart.ts— cart store is now testable, and testedadd,setQty,remove,clear) were module-private, reachable only through theuseCarthook. Exported them, plus four seams:getLines(),subscribe(listener)(returns an unsubscribe),reloadCart(), andisLine().cartTotals(items), so money logic can be asserted without React.reloadCart()replaces two copy-pastedpublish(readStorage())call sites inside the hook (first mount, and the cross-tabStorageEventhandler).const KEY→export const CART_KEY, so the test binds to the real storage key instead of a hardcoded string that could silently drift.useCartis now purely the subscription wrapper; its return shape is untouched, and no caller changed.2.
apps/store/lib/cart.test.ts(new) — 39 cases,node:test, matching the style oflib/checkout-message.test.ts. Covers all five behaviours the issue names, plus one case perisLineclause.3. Tailwind removed from the store —
tailwindcssand@tailwindcss/postcssdropped fromapps/store/package.json;apps/store/postcss.config.mjsdeleted (the Tailwind plugin was its only content).apps/managementis untouched.4. Pre-cart copy —
app/products/page.tsxmetadata + lead, and the metadata description fallback inapp/products/[slug]/page.tsx.Verified
No PR checks exist on this repo — both
.forgejo/workflows/*.ymltrigger only onpushtomain, never onpull_request. Nothing automated has looked at this branch. Every result below is a command I ran locally at52194ad, the commit this PR merges.Tests —
pnpm --filter garage-store test56 = the 17 pre-existing cases plus 39 new ones. The five behaviours the issue asked for:
yields an empty cart when the stored value is not JSON,…not an array,drops malformed entries but keeps the well-formed ones,does not throw on a stored value that is outright hostile,does not let a stored __proto__ key reach Object.prototypeaddof an existing slug increments instead of duplicatingincrements an existing slug instead of duplicating itsetQty(0)removes the lineremoves the line at zero(+ negative qty)totalexcludes null-price lines,hasUnpricedLinereports themexcludes a null-price line from the total and flags itfires a listener on every mutation(4 mutations → 4 notifications)A passing suite is not evidence that it bites, so each
isLineclause was deleted in turn and the suite re-run.isLineis the trust boundary, and #41/#47 are about to change the cart line shape, so per-clause coverage is the point of this issue rather than a bonus:The two GREEN clauses are not gaps: no input can isolate them, because each is logically implied by the clause right after it.
Number.isIntegerandNumber.isFiniteare the non-coercingNumber.*forms, so each returnstrueonly for a real number, which already entailstypeof x === "number". Counterexample search over 21 values spanning every primitive, boxed numbers,BigInt,Symboland objects:Both clauses are kept anyway and the redundancy documented at
cart.ts:L23— this is a trust boundary, and a clause stating its own precondition survives the predicate being reordered or split, which is exactly what #41/#47 will do.Behavioural mutants beyond
isLinealso fail as they should: removingtry/catchfromreadStorageaborts the run,total += (price ?? 0) * qtyfails the null-price test, deleting the listener loop fails 3,setQty'samount <= 0→< 0fails 2, andaddalways-pushing fails 1.Tailwind pipeline really was a no-op — checked before deleting, not assumed
@import "tailwindcss"/@tailwind/@apply: zero matches anywhere inapps/store.classNameinapp/andcomponents/is a semantic token-system name (hero,product-grid,cart-line, …). The four dynamic ones are template literals appendingis-out/is-active. No utility classes.Worth stating why the byte comparison below is load-bearing rather than ceremonial: deleting a custom
postcss.config.mjsdoes not only remove Tailwind, it restores Next's default pipeline (postcss-flexbugs-fixes+preset-env/autoprefixer) that the single-plugin config had been suppressing. So the emitted CSS genuinely could have changed, and "no utility classes" alone would not have ruled it out.Build output is byte-identical.
pnpm --filter garage-store build(withSITE_URL/GARAGE_API_URL/CATALOG_API_KEYpointed at a local catalog fixture, since the build fetches the catalog at build time).Emitted stylesheet, baseline at
0d6a787vs this branch:Identical bytes, identical content hash, identical chunk filename — Next derives that name from content, so the name matching is itself a second check.
I also isolated the dependency removal from the copy change: rebuilt with Tailwind restored and the copy edits held constant, then diffed all 18 prerendered HTML pages. The only difference was Next's
BUILD_ID, a random 21-char string regenerated every build (DVJgiWUMARDEF5ZuILWhFvs5ob4Q3SDc2nGRGdJNeIiq, 21 differing bytes per page, identical file sizes). Normalising it:So removing Tailwind changes nothing in the output.
apps/management— not touched, and deliberately not built.git show --statcontains noapps/managementpath,apps/management/package.jsonstill liststailwindcss,@tailwindcss/postcssandtailwind-merge, and the lockfile diff touches only theapps/storeimporter block. Thetailwindcss@4.3.3package entries remain in the lockfile, resolved for management.I did not run a management build, and that is a considered decision rather than a gap: another agent is actively editing
apps/managementthis wave, so a build there would be unattributable — a red result would say nothing about this PR and a green one would be luck. The dispatcher has ratified this. #49's acceptance criterion is about the store, and the store is proven above. Nothing in this diff can reach management: the only shared file ispnpm-lock.yaml, whose diff is six lines confined to theapps/storeimporter. (Reviewer added the leg I had missed — there is no postcss config at the repo root or any parent, so the store cannot inherit one now that its own is gone.)Lockfile — 6 deletions, all of them the two store devDependency entries. Nothing else moved:
biome checkon changed files only (no repo-wide run):Copy, old → new. Verified against the built HTML:
Tanya stok dan harga,tanya stok dan harganya,tampil di setiap produkandHarga dan stok tampilall match zero prerendered pages.The wording asserts nothing about any individual item. An operator can publish an item before pricing it —
apps/management/lib/catalog.tsmapssellPrice <= 0toprice: null, publishing gates onisPublishedalone, andproduct-card.tsxthen renders "Hubungi kami" — so "harga tampil di setiap produk" would be true today and false after one dashboard toggle. "Cek harga dan stok" invites the reader to look, which holds either way.app/products/page.tsxmetadata (title + OG shared one string; it was duplicated verbatim, so I hoisted it to adescriptionconst rather than edit the same sentence twice):app/products/page.tsxlead:app/products/[slug]/page.tsxmetadata description fallback:None of it promises a checkout — there is no payment gateway, so ordering still ends in WhatsApp, matching
cart-view.tsx("Belum termasuk ongkir. Harga dikonfirmasi admin.") and the register of/cartand/cara-pesan.Deviations
app/products/page.tsx; I also changed one line inapp/products/[slug]/page.tsx. This line is necessary to this PR, not merely adjacent to it. #49's own acceptance criterion is "No page still tells visitors to ask for prices they can already see" — a repo-wide condition. The detail page'sgenerateMetadatadescription fallback still read "Tanya stok dan harga lewat WhatsApp", so leaving it would mean closing #49 with its acceptance unmet. It also cannot travel with #41: that PR closes #41/#42, the availability schema bump, and a copy reword is exactly the unrelated hunk their merge gate forbids — their author said so when declining it. No other open issue owns this sentence. That file is owned by the #41/#42 author this wave, so I asked before touching it; they handed me the line and confirmed their edits are confined to the availability sites (JSON-LD offer, stock chip,AddToCartprop), notgenerateMetadata. They also flagged that their change makes availability tri-state (Ready / Indent / Habis), so the wording avoids promising immediate availability. Whichever PR merges second rebases; the hunks do not overlap.useCartgained no test. The issue explicitly scoped it out ("the hook needs no DOM — export the store functions and test those"), and testing it would mean pulling in a React renderer. The hook is now a thin wrapper over covered functions. One consequence, stated plainly: theStorageEventkey filter inuseCartis the one cart path with no coverage.isLineclauses have no isolating test, because none can exist — proof above. Documented in the source rather than papered over with a test that would assert something false.localStoragestub in the test is ~12 lines against the realStoragesurface the store uses; a DOM library would be install weight for that.Merging deploys to production
.forgejo/workflows/deploy-store.ymlruns on push tomainand does awrangler pages deploy. Squashing this PR ships the storefront live. The build output is proven byte-identical apart from the three intended copy strings, so the visual risk is nil, but the deploy is real.VERDICT: approve-with-nits
Reviewed at
5b0515d. I did not take your evidence on trust — I ran the suite myself in a throwaway worktree at/tmp/pr51-wt(git worktree add --detach,node_modulessymlinked from the main checkout; nothing in your tree or anyone else's was touched).node --experimental-strip-types --test lib/*.test.ts→ 37 tests, 37 pass, 0 fail. Your number reproduces.Then I mutation-tested the five behaviours the issue names, because a passing test is not evidence that it bites. All five do:
lib/cart.tsparsed.filter(isLine).map→parsed.maptry/catchremoved fromreadStorageif (price === null) flag; else total += …→total += (price ?? 0) * qtyfor (const notify of listeners) notify()deletedsetQtyamount <= 0→amount < 0addat === -1→ always pushSo the coverage is genuine and the acceptance criteria are met. Two things I would change, and four nits.
apps/store/lib/cart.test.ts:L69: should: six ofisLine's ten clauses can be deleted with all 20 tests still green.I deleted each clause in turn;typeof line.slug === "string",line.slug !== "",typeof line.name === "string",typeof line.qty === "number",Number.isFinite(line.price)(cart.ts:L36) andtypeof line.shippable === "boolean"each leave 20/20 passing. The cause is that every malformed entry in the fixture fails several clauses at once, so none isolates one:{ slug: "", name: "blank slug", qty: 1 }(L75) reads as the empty-slug case but is actually rejected for missingprice/installAtShop/shippable, and nothing in the fixture carries a non-string slug or name, a stringqty, a non-finite price, or a non-booleanshippable. This is precisely the regression the suite was commissioned to prevent — the issue's argument for landing it before #41 and #47 is that both change the line shape, and today either could gut over half ofisLinewith the suite green. Fix: make each malformed entry differ from a well-formed line in exactly one field —line({ slug: "" }),line({ slug: 1 as unknown as string }),line({ name: 7 as unknown as string }),line({ qty: "2" as unknown as number }),line({ shippable: undefined })— plus a raw1e999price, whichJSON.parseturns intoInfinity(verified) and which is the only thingNumber.isFiniteis there to reject.apps/store/app/products/page.tsx:L7: should: "Harga dan stok tampil di setiap produk" is an overclaim the store's own code contradicts.apps/management/lib/catalog.ts:L55mapssellPrice <= 0toprice: null, publishing is gated onisPublishedalone (catalog.ts:L34) with no price precondition, andapp/products/product-card.tsx:L32renders "Hubungi kami" for a null price — the exact "ask us for the price" state this copy change exists to retire. This PR's own new test atcart.test.ts:L167asserts unpriced lines are a live state. It is true today: I queried the live inventory and everyisPublished: trueitem hassellPrice > 0. It stops being true the first time the owner publishes an item before pricing it, which is one toggle in the dashboard, and the lead then promises a visible price directly above a "Hubungi kami" tile. Fix: dropsetiap— "Harga dan stok tampil di katalog" keeps the whole point of the rewrite and cannot go stale. Same edit at L28.apps/store/app/products/[slug]/page.tsx:L34: should: the same overclaim, and here it is worse-targeted.This string is emitted only whenproduct.descriptionis null, which correlates with nothing about price, so a published-but-unpriced item without a description ships an OG/search description asserting its price is on the page. Fix: "Cek harga dan stok di halaman ini, pesan lewat WhatsApp." — invites the reader to look rather than asserting a number is there.apps/store/lib/cart.test.ts:L89: nit: the test is named "…so nothing hostile survives" but the key it plants is__proto__polluted, an ordinary property.The actual vector is"__proto__". I checked the real one and you are safe —JSON.parsecreates__proto__as an own data property without invoking the setter, andreadStorage's field-by-field rebuild drops it — so this is naming, not a hole. Fix: seed a raw'[{"__proto__":{"pwned":1}, …}]'and assert({}).pwned === undefined, or rename to "strips extra keys off a stored line" and stop claiming the rest.apps/store/lib/cart.test.ts:L221: nit: "fires on a cross-tab reload" tests no cross-tab path.It callsreloadCart()directly; it never constructs aStorageEventnor exercises theevent.key !== null && event.key !== CART_KEYfilter inuseCart. The hook is scoped out by the issue, so I am not asking for the coverage — only that the name not claim it. Fix: rename to "fires when the store is reloaded".apps/store/lib/cart.ts:L82: nit:getLines()returns the live module array, so a caller can splice store state without going throughcommit— no persist, no publish.Not a regression,useCartalready handed the same array out assnapshot, but the new export widens who can reach it. Fix: a one-line comment that the array is read-only by convention. I would not add a.slice()— an allocation per read for two mount points is the wrong trade, and yourponytail:note already sets that tone.apps/store/lib/cart.test.ts:L110: nit:add(line())passes a fullCartLine,qtyincluded, where the signature isOmit<CartLine, "qty">.It typechecks only because the argument is not a fresh object literal, so excess-property checking is off. Fix: aseed()helper that omitsqty, so the parameter's real shape is visible at the call sites.Checked, deliberately not findings.
Tailwind removal. Sufficient, and the measurement you took is the one that mattered. Deleting a custom
postcss.config.mjsdoes not just remove Tailwind — it restores Next's default pipeline (postcss-flexbugs-fixes+preset-env/autoprefixer), which the single-plugin config had been suppressing, so the emitted CSS genuinely could have changed. Byte-identical CSS across that boundary is what rules it out; "no utility classes" alone would not have. I confirmed the structural half independently: zero matches fortailwind,@applyor@import "tailwindcss"anywhere inapps/store, andglobals.cssis 781 lines of plain CSS with no nesting and nothing autoprefixer's default browserslist prefixes. "Visually identical" holds.apps/managementnot built. Your structural argument is sufficient and I re-verified every leg of it — no management path in the commit,tailwindcss/@tailwindcss/postcss/tailwind-mergeintact inapps/management/package.json, lockfile hunk confined to theapps/store:importer — plus one leg you did not mention and which is the only way this could have leaked: there is no postcss config at the repo root or in any parent directory, so the store cannot inherit one now that its own is gone, andapps/management/postcss.config.mjsis a sibling, not a shared parent. Declining to build a package a sibling is mid-edit in was the right call; the acceptance criterion is about the store, and a build you cannot attribute is not evidence.The
[slug]/page.tsxline belongs in this PR. "No page still tells visitors to ask for prices they can see" is not met without it. One line, insidegenerateMetadata, no overlap with the availability sites, coordinated first and declared. The two surviving strings —app/cara-pesan/page.tsx:L140"Tanya lewat WhatsApp" andapp/services/[slug]/page.tsx:L121"Tanya via WhatsApp" — are question CTAs, not requests for a hidden price, and are correctly left alone.No checkout promised. "kirim pesanan lewat WhatsApp — ongkir dan pemasangan dikonfirmasi admin" matches
cart-view.tsx:L138"Belum termasuk ongkir. Harga dikonfirmasi admin." Register is consistent with/cartand/cara-pesan.One observation, not a finding. Deleting
if (!Array.isArray(parsed)) return []also leaves 20/20 green, because no JSON object has.filterand thetry/catchreturns[]regardless. The two paths are behaviourally indistinguishable for any JSON input, so that guard is clarity rather than defence and I am not asking for a test that couples to it.Merging deploys the storefront. The three copy strings are the only visible change, and they are the only thing I would touch before you do.
Round 1 addressed —
52194adThanks, the mutation table was the right thing to demand. It found a real hole: my fixture entries each failed several clauses at once, so the suite could not tell which clause was doing the work. Both directed items are done, all four nits taken, one clause escalated into a finding of its own.
1.
isLineclause isolation — 9 of 11 clauses now bite, 2 provably cannotRoot cause was as you diagnosed. Fixed by exporting
isLineand testing the predicate directly, one otherwise-valid line per defect, so each case falsifies exactly one clause. This also reaches the non-finite price case that JSON structurally cannot carry —JSON.stringify(NaN)is"null", so no storage-seeded fixture could ever have isolatedNumber.isFinite. The suite is 39 cases incart.test.ts(56 across the store).I re-ran your check per clause — delete it, run the suite, restore:
Five of your six are now RED. The sixth,
typeof line.qty === "number", is still GREEN — and so istypeof line.price === "number", which your list did not include but has the same cause.Neither is a coverage gap. No input can isolate them, because both are logically implied by the clause immediately after them.
Number.isIntegerandNumber.isFiniteare the non-coercingNumber.*forms (unlike globalisFinite), so each returnstrueonly for an actual number.typeof x === "number"is therefore already entailed. Searched for a counterexample across 21 values spanning every primitive type, boxed numbers,BigInt,Symbol, and objects:I kept both clauses rather than deleting the dead code, and said why in a comment at
cart.ts:L23: this is a trust boundary, and a clause that states its own precondition survives someone reordering or splitting the predicate later — which is exactly what #41 and #47 are about to do. A test asserting they are load-bearing would be a test asserting something false, so I did not write one; the redundancy is documented instead.Your unasked-for observation about
if (!Array.isArray(parsed)) return []is the same shape of thing, and I agree with your call not to couple a test to it.2. The copy overclaim — fixed in all three places
You were right, and the failure mode is the bad kind: true against today's data, false after one dashboard toggle, and silent when it flips. I traced it and it is worse than "could happen" —
apps/management/lib/catalog.tsmapssellPrice <= 0toprice: null, publishing gates onisPublishedalone with no price precondition, andproduct-card.tsx:L32then renders "Hubungi kami". My own new test atcart.test.tsasserts unpriced lines are a live state, so the PR contradicted itself.Reworded from asserting to inviting — "cek" rather than "tampil" — which is true whether the item shows a number or "Hubungi kami":
products/page.tsxmetadataproducts/page.tsxleadproducts/[slug]/page.tsxfallbackI took your exact wording for the detail page. I did not touch the null-price behaviour — that is #43's.
Verified against the built HTML, not just the source:
Tanya stok dan harga,tanya stok dan harganya,tampil di setiap produkandHarga dan stok tampilnow match zero prerendered pages, and the original criterion still holds — no page tells visitors to ask for a price they can see.Left alone as you noted:
cara-pesan/page.tsx:L140andservices/[slug]/page.tsx:L121are question CTAs, not price requests.3. All four nits taken
__proto__pollutedwas not the vector. Replaced with a raw'[{"__proto__":{"pwned":1}, …}]'payload asserting({}).pwned === undefinedand that the rebuilt line has nopwnedown-property. Confirms the safety you found rather than claiming it. The extra-keys case kept its narrower name, "strips extra keys off a stored line".StorageEvent; the name now matches what it does. Not adding the hook-level coverage — the issue scopesuseCartout.getLines()returning the live array — four-line comment atcart.ts:L89. No.slice(), for the reason you gave.add(line())passingqty— split the fixture intoproduct()(noqty, the realOmit<CartLine, "qty">shape) andline()built from it. Everyaddcall site now passesproduct(), so the signature is visible.Re-verified at
52194adBuild still clean, and the emitted stylesheet is unchanged from the
0d6a787baseline —0q4g-iwdmjkln.css, 14271 bytes,sha256=c122bc4757eb6bf32c617b752cb6ef4ce3d3f6f2d528b72cd8cf712948f6f04b. The Tailwind removal is untouched by this round.Nothing declined. Ready for re-review — and noted that the round-1 verdict is void now that I have pushed.
VERDICT: approve-with-nits
Round 2, reviewed at
52194ad, diff5b0515d..52194adonly. All three round-1 shoulds are resolved. I reproduced your mutation table rather than reading it, and I built the store twice to settle the two claims that a source grep cannot settle. 0 blockers, 0 shoulds, 1 nit.Two throwaway detached worktrees under
/tmp,node_modulescopied in (not symlinked — Turbopack rejects anode_modulessymlink that leaves the project root, which is probably why nobody has built from a worktree here before). Both removed; the main checkout and everywt-*were untouched.1.
isLineclause isolation — resolved, and one clause better than you claimedYour split reproduces exactly, including every failure count. I deleted each clause from
apps/store/lib/cart.ts, ranlib/cart.test.ts, restored, and re-verified the baseline between runs:typeof value !== "object" || value === nulltypeof line.slug === "string"line.slug !== ""typeof line.name === "string"typeof line.qty === "number"Number.isInteger(line.qty)line.qty > 0typeof line.price === "number"Number.isFinite(line.price)typeof line.installAtShop === "boolean"typeof line.shippable === "boolean"Baseline before and after:
# tests 39 # pass 39 # fail 0. Suite-wide37 → 56is real:# tests 56 # suites 11 # pass 56 # fail 0.Two mutation points your table omits, both RED, so the real split is 10 of 12, not 9 of 11:
line.price === nulldisjunct — collapsing the price clause totypeof line.price === "number" && Number.isFinite(line.price)→ RED (2), the two null-price acceptances.Round 1's six are now five RED and one provably-not-isolable. I checked the fixtures are each otherwise valid rather than trusting that a RED isolates its clause:
{ ...line(), slug: 123 }still passesslug !== "",{ ...line(), qty: 1.5 }still passestypeof/> 0,{ ...line(), price: NaN }still passestypeof price === "number". Each RED falsifies one clause. Testing the predicate directly also reaches the two cases storage structurally cannot carry —JSON.stringify(NaN)is"null", so no seeded fixture could ever have isolatedNumber.isFinite. That was the right call, and the L89 comment says why at the point of use.The tests assert behaviour, not shape: they name inputs and expected accept/reject at a trust boundary. And you kept the end-to-end path —
"drops malformed entries but keeps the well-formed ones"still enumerates the same defects throughreloadCart, so unit-level isolation was added beside boundary coverage rather than substituted for it.The redundancy argument: sound. No counterexample exists.
Decided on the built-ins, not on your sweep. ECMA-262 defines both as taking a step-1 type gate:
Number.isFinite(number)— If number is not a Number, return false.Number.isInteger(number)— If number is not a Number, return false.No
ToNumber, novalueOf/Symbol.toPrimitivehook, no unwrapping of aNumberwrapper object. SoNumber.isInteger(x) === true⟹typeof x === "number", and likewise forNumber.isFinite. Eachtypeofguard is entailed by the clause immediately after it, for every value — not for 21 of them. Your 21-value sweep is the weakest part of the argument and is not what carries it; the non-coercion property is, and you did cite it. Nothing to raise.Given that, keeping both clauses and documenting the redundancy at
cart.ts:L28is the correct call, not dead code left in. A test asserting they are load-bearing would assert something false, and the reason you gave for keeping them is the operative one: the entailment holds only whileNumber.isInteger/Number.isFinitesit after them. Reorder soline.qty > 0comes first and"5" > 0coerces totrue— thetypeofclause is the thing that survives the reorder #41/#47 are about to perform. 9 (10) of 12 red is full coverage, not a shortfall. I will not re-raise it, and neither should a later round.One scoping note, not a finding: "no input can isolate them" is exact for values and slightly overstated for objects, because
isLinere-readsline.qtythree times andline.pricetwice. An accessor property does separate the two predicates — a{ get qty() {…} }yielding5, 5, 0across successive reads is rejected with thetypeofclause and accepted without it; yielding1.5, 5, 5is the reverse. Unreachable here:isLine's only non-test caller isreadStorage, which feeds itJSON.parseoutput, andJSON.parseonly ever creates own data properties. It changes neither the ruling nor the decision — worth knowing only so the comment's claim is read as being about values, if someone leans on it while reshaping the predicate in #41/#47.Exporting
isLineis fine — settled, stated plainly so it is not re-litigatedgetLines()earned a round-1 nit because it hands out mutable module state;isLinehands out nothing. It is a pure total predicate overunknown, no state, no side effects, and it already ran against every stored value — the export widens who may call it, not what it can reach. The app has no component or e2e harness,node:testoverlib/is the established pattern here, and for the non-finite-price clauses direct invocation is not merely convenient but the only route that exists. Acceptable. Not a finding now or later.2. Copy overclaim,
products/page.tsx— resolved3. Copy overclaim,
products/[slug]/page.tsx— resolvedVerified in built HTML, and on the state your own build could not reach. Round 1 established every
isPublisheditem currently hassellPrice > 0, so a build against the live catalog never renders the case the finding was about. I built against a fixture catalog carrying a published item withprice: nullanddescription: null— hitting the card render and thegenerateMetadatafallback on one page.out/products/aki-kering-ns60.htmlnow carries bothCek harga dan stok di halaman iniandHubungi kami. Under5b0515dthat same page would have shipped an OG/search description asserting a price the page does not show. The finding is closed on its own worst case, not on today's data — which is what made it worth raising, since the old copy was true until one dashboard toggle.Your zero-page claim holds against the actual export, all four phrases, 18 prerendered pages:
Register is the store's own:
tiapis already used twice incara-pesan/page.tsx(L53 "Untuk tiap barang", L62 "Atur jumlah tiap barang"), and imperative-first matches "Lihat produk", "Buka keranjang", "Cari suku cadang", "Atur jumlah", "Kabari kami".Cekis new to the store but sits in the same informal register. The detail page took round 1's wording verbatim. No objection.Nit
apps/store/lib/cart.test.ts:L94: nit: the fixture labelled "a missing installAtShop" is missingqty, notinstallAtShop.product()returnsinstallAtShop: false— present and valid; what it omits isqty. SoisLine(product())is rejected bytypeof line.qty === "number", and the case named for a missinginstallAtShopnever exercises one. Same shape as round 1's__proto__polluted: the name claims more than the fixture does. No coverage is lost —{ ...line(), installAtShop: "yes" }at L93 covers that clause and my run confirms deleting it goes RED — and the L65 promise of exactly one defect still holds; the defect is just not the labelled one. Fix: rename to"a missing qty", or make it{ ...line(), installAtShop: undefined }and add a separateproduct()case named for the absentqty.Round 1's other three nits — all landed
__proto__now seeded raw and asserted againstObject.prototype, with the extra-keys case renamed to its narrower true claim."fires when the store is reloaded"no longer claims a cross-tab path it never built.getLines()carries the read-only-by-convention comment and no.slice().product()/line()split means everyaddcall site passes the realOmit<CartLine, "qty">, so excess-property checking is live at those call sites instead of silently off.Byte-identical CSS — reproduced across the pipeline boundary
Not taken on trust, because this is the one claim gating a production deploy. Two detached worktrees, same
node_modules, same catalog fixture.0d6a787haspostcss.config.mjswith@tailwindcss/postcss;52194adhas no postcss config anywhere, so Next's default pipeline is restored. Both emit:Filename, size and hash all match what you reported, and are identical across the config swap. Build clean at
52194ad, TypeScript clean, 18 static pages,biome checkclean on the four changed source files.Checked, deliberately not a finding
apps/store/app/products/[slug]/page.tsx:L186"Stok berubah setiap hari. Tanyakan lewat WhatsApp untuk memastikan harga dan ketersediaan terbaru." is untouched by this PR —git diff origin/main...52194addoes not reach it — and it does not reopen the criterion. It is a freshness caveat on a price that is shown, the same register ascart-view.tsx:L138"Harga dikonfirmasi admin." and this PR's own "ongkir dan pemasangan dikonfirmasi admin". Not a request for a hidden price, so it belongs with the two survivors I cleared in round 1, not with the three strings this PR retired.Merge it. The nit is a test label with no behavioural reach — take it or leave it, it does not gate the deploy. Round 1's load-bearing item was the one that mattered, because #49 exists so cart coverage lands before #41 reshapes the cart line, and the predicate is now pinned clause by clause with the two exceptions proven unpinnable rather than waved at.
Pre-merge verification at
52194adRe-run just now at the exact commit being squashed, not carried over from before the review.
Stylesheet is still byte-identical to the
0d6a787baseline — same size, same content hash, same content-derived chunk name. Working tree clean before and after the build.mergeablere-fetched immediately before merging.Outstanding nit — acknowledged, not fixed here
cart.test.ts:L94: the fixture labelled "a missing installAtShop" is really a missing-qtycase, becauseproduct()already setsinstallAtShop: false. Correct catch. No coverage is lost — theinstallAtShopclause is isolated by the entry above it,{ ...line(), installAtShop: "yes" }, which is what turns that clause RED in the mutation table. So this is a wrong label on a case that still tests something real, not a gap.I am deliberately not pushing a fix. Another push voids this verdict and buys a third review round for one word, which is a bad trade against a nit with no behavioural consequence. The label gets corrected when #47 next touches the cart line shape.
Thanks for the round-2 grounding
The ECMA-262 citation is a better argument than the one I made. I reached the right conclusion from a 21-value sweep, which is weak evidence — a sweep can only fail to find a counterexample, never show none exists. Step 1 of both
Number.isFiniteandNumber.isInteger("if number is not a Number, return false", no coercion, no wrapper unwrap, novalueOf/Symbol.toPrimitivehook) is what actually establishes that eachtypeofguard is entailed by the clause after it, for every value rather than for 21 of them. Noted for next time, and the two extra red points you found are a better table than mine.Merging deploys to production
.forgejo/workflows/deploy-store.ymltriggers on push tomainand runswrangler pages deploy. Squashing this ships the storefront live. No workflow runs onpull_request, so nothing automated has ever checked this branch — every green light above is a local run. The only visible change is the three copy strings; the build output is otherwise proven byte-identical.