← back to Designer Wallcoverings
TK-11857 R1/R4: price-integrity gate fails closed on non-boolean orderable; shopify.ts uses real product_type
76f11f71037c82b0c74a6924be26768383cbab70 · 2026-09-22 13:35:26 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7
Files touched
M DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.tsM DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.tsM DW-Programming/ImportNewSkufromURL/lib/shopify.ts
Diff
commit 76f11f71037c82b0c74a6924be26768383cbab70
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Sep 22 13:35:26 2026 -0700
TK-11857 R1/R4: price-integrity gate fails closed on non-boolean orderable; shopify.ts uses real product_type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3ChReG53fwpNgUESv4SY7
---
.../__tests__/price-integrity-gate.test.ts | 76 ++++++++++++++++++++++
.../lib/price-integrity-gate.ts | 14 +++-
DW-Programming/ImportNewSkufromURL/lib/shopify.ts | 9 ++-
3 files changed, 96 insertions(+), 3 deletions(-)
diff --git a/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts b/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
index cb1b8ce1..1bec3712 100644
--- a/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
+++ b/DW-Programming/ImportNewSkufromURL/__tests__/price-integrity-gate.test.ts
@@ -202,6 +202,31 @@ describe('assertPriceIntegrity', () => {
const r = assertPriceIntegrity(clean);
expect(r.warnings).toHaveLength(0);
});
+
+ // ---- 15/16/17. NEGATIVE class C (empty/missing variants = fail-OPEN) --------
+ // TK-11357 (vp-engineering review 2026-09-14): the gate ran to completion on a
+ // zero-variant input having checked nothing and returned ok:true — a malformed
+ // payload sailed straight through. These prove it now fails CLOSED whether the
+ // array is empty, the field is absent, or the field is a non-array. (RED-goes-
+ // on-injected-fault, CLAUDE.md TK-11431 rule 3.)
+ it('15. empty variants array → ok:false, class C empty-variants (was fail-open)', () => {
+ const r = assertPriceIntegrity({ dwSku: 'DWX-1', netCost: 30, variants: [] });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'C' && v.code === 'empty-variants')).toBe(true);
+ });
+
+ it('16. missing variants field → ok:false, class C empty-variants', () => {
+ // Cast: exercises the runtime guard against a caller that omits `variants`.
+ const r = assertPriceIntegrity({ dwSku: 'DWX-1', netCost: 30 } as unknown as PriceIntegrityInput);
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.code === 'empty-variants')).toBe(true);
+ });
+
+ it('17. non-array variants → ok:false, class C empty-variants (no throw)', () => {
+ const r = assertPriceIntegrity({ dwSku: 'DWX-1', netCost: 30, variants: null } as unknown as PriceIntegrityInput);
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.code === 'empty-variants')).toBe(true);
+ });
});
describe('enforcePriceIntegrity', () => {
@@ -224,3 +249,54 @@ describe('enforcePriceIntegrity', () => {
expect((caught as PriceIntegrityError).violations.length).toBeGreaterThan(0);
});
});
+
+// ============================================================================
+// TK-11857 R1 — FAIL-CLOSED on a NON-BOOLEAN `orderable`.
+// The VariantCheck type says boolean, but untyped JS callers (import-queue-runner.js)
+// can pass undefined / 1 / 'true' at runtime. The old strict `=== true` fail-OPENED:
+// a $0 variant with a non-boolean orderable SKIPPED the Class-C block. These are the
+// negative tests (CLAUDE.md TK-11431 amendment 3): they INJECT that fault and prove
+// the gate goes RED. Revert `!== false` back to `=== true` and both go GREEN→RED.
+// ============================================================================
+describe('assertPriceIntegrity — R1 non-boolean orderable fails CLOSED', () => {
+ it('R1a. sellable $0 with orderable:undefined → ok:false, class C (was fail-open)', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-R1a',
+ netCost: 30,
+ // orderable omitted → undefined at runtime, exactly what an untyped JS caller sends
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 0 } as any,
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'C' && v.code === 'zero-price-orderable')).toBe(true);
+ });
+
+ it('R1b. sellable $2 (cost unknown) with orderable:1 (truthy non-bool) → ok:false, class B floor', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-R1b',
+ netCost: null,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 2, orderable: 1 as any },
+ ],
+ });
+ expect(r.ok).toBe(false);
+ expect(r.violations.some(v => v.class === 'B' && v.code === 'below-absolute-floor')).toBe(true);
+ });
+
+ it('R1c. explicitly-non-orderable ($0 orderable:false) is still NOT blocked (no over-block)', () => {
+ const r = assertPriceIntegrity({
+ dwSku: 'DWX-R1c',
+ netCost: 30,
+ variants: [
+ { role: 'sample', price: 4.25, orderable: false },
+ { role: 'sellable', price: 59, orderable: true },
+ { role: 'sample', price: 0, orderable: false }, // genuinely non-orderable $0 → fine
+ ],
+ });
+ // no class-C zero-price-orderable finding for the explicitly non-orderable $0 variant
+ expect(r.violations.some(v => v.code === 'zero-price-orderable')).toBe(false);
+ });
+});
diff --git a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
index 2e980b1e..44f9f7c9 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/price-integrity-gate.ts
@@ -419,7 +419,15 @@ export function assertPriceIntegrity(input: PriceIntegrityInput): GateResult {
// standard DW import. Samples already have their own floor enforced above
// ('sample-below-vendor-floor', the vendor-declared price). The absolute
// floor is an anomaly guard for SELLABLES, where no legitimate item is $2.
- if (v.role === 'sellable' && v.orderable === true && price > 0 && price < absoluteFloor) {
+ // TK-11857 R1 — FAIL CLOSED on a non-boolean `orderable`. The VariantCheck type
+ // says boolean, but untyped JS callers (e.g. scripts/import-queue-runner.js) can
+ // pass 1 / 'true' / undefined at runtime; strict `=== true` would SKIP this block
+ // for them (fail-OPEN — the inverse of CLAUDE.md TK-11431 rule 1, "an unmeasured
+ // input is never PASS"). `!== false` treats anything not explicitly false as
+ // orderable, so the guard still fires. A genuinely non-orderable variant MUST
+ // pass orderable:false. (Latent today — all enumerated call sites pass a strict
+ // boolean — proven by the negative test in __tests__/price-integrity-gate.test.ts.)
+ if (v.role === 'sellable' && v.orderable !== false && price > 0 && price < absoluteFloor) {
findings.push({
class: 'B',
code: 'below-absolute-floor',
@@ -436,7 +444,9 @@ export function assertPriceIntegrity(input: PriceIntegrityInput): GateResult {
// ---- Class C: any orderable variant (any role) MUST be priced > 0 — BLOCK ----
// Diagnose honestly: a NEGATIVE price is a different fault than a $0/NaN
// price and gets its own code so the report isn't misleading.
- if (v.orderable === true && !(price > 0)) {
+ // TK-11857 R1 — FAIL CLOSED on non-boolean `orderable` (see the Class-B comment
+ // above): `!== false` so an unmeasured orderable still gets the $0/negative block.
+ if (v.orderable !== false && !(price > 0)) {
if (price < 0) {
findings.push({
class: 'C',
diff --git a/DW-Programming/ImportNewSkufromURL/lib/shopify.ts b/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
index 60d47cb1..48546f47 100644
--- a/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
+++ b/DW-Programming/ImportNewSkufromURL/lib/shopify.ts
@@ -841,10 +841,17 @@ async function createProductWithMetafield(dto: ProductDTO, priceUnit: string, un
// TK-00134: brand-aware banned-word sweep on every customer-facing field
const { cleanWallpaper } = require('./clean-wallpaper');
+ // TK-11857 R4 — use the REAL product type instead of a hardcoded 'Wallcovering'.
+ // A Fabric imported via this single-product path was stored as 'Wallcovering' (the
+ // weight guard already reads dto.specs?.product_type at ~L2027; the batch path
+ // derives PRODUCT_TYPE from product.product_type). Mirror both: real type when
+ // present, same banned-word "Wallpaper"->"Wallcovering" sweep as the batch path,
+ // default 'Wallcovering' when absent.
+ const resolvedProductType = (dto.specs?.product_type || 'Wallcovering').replace(/\bWallpaper\b/gi, 'Wallcovering');
const input = {
title: cleanWallpaper(enhancedTitle),
descriptionHtml: cleanWallpaper(enhancedDescription),
- productType: 'Wallcovering', // banned word "Wallpaper" removed per standing rule
+ productType: resolvedProductType, // TK-11857 R4: real type (was hardcoded 'Wallcovering')
vendor,
tags: statusTags.map((t: string) => cleanWallpaper(t)),
status: productStatus,
← 8196c654 TK-11786: free-samples anchored title match + block-nonpurch
·
back to Designer Wallcoverings
·
TK-11783: strip hardcoded Google OAuth client secret from sh 82c219e2 →