← back to Designer Wallcoverings
Stock new cadence drafts with verified inventory and scoped credentials
480c254a4dbce8f809e68c87ecd8efcf35fff9eb · 2026-09-11 08:34:12 -0700 · Steve Abrams
Files touched
M shopify/scripts/cadence/cadence-import.jsA shopify/scripts/cadence/tests/inventory-create-path.test.mjs
Diff
commit 480c254a4dbce8f809e68c87ecd8efcf35fff9eb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 08:34:12 2026 -0700
Stock new cadence drafts with verified inventory and scoped credentials
---
shopify/scripts/cadence/cadence-import.js | 135 ++++++++++++++-------
.../cadence/tests/inventory-create-path.test.mjs | 93 ++++++++++++++
2 files changed, 187 insertions(+), 41 deletions(-)
diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 5780d252..d06555e8 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -116,8 +116,16 @@ const RETAIL = c => Math.round((c / 0.65 / 0.85) * 100) / 100;
const SAMPLE_PRICE = '4.25'; // correct price for the Sample variant (Steve 2026-06-11)
// ---- creds ----
-const env = fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8');
-const TOKEN = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+// Inventory writes require write_inventory. Prefer the existing full-access credential
+// even when the hourly wrapper exports the narrow admin token (TK-11426).
+function readToken(file, key) {
+ try { return (fs.readFileSync(file, 'utf8').match(new RegExp('^' + key + '=(.*)$', 'm')) || [])[1]?.replace(/['"]/g, '').trim(); }
+ catch { return undefined; }
+}
+const TOKEN = process.env.SHOPIFY_FULL_ACCESS_TOKEN
+ || readToken(path.resolve(__dirname, '../../../.env'), 'SHOPIFY_FULL_ACCESS_TOKEN')
+ || process.env.SHOPIFY_ADMIN_TOKEN
+ || readToken(os.homedir() + '/Projects/secrets-manager/.env', 'SHOPIFY_ADMIN_TOKEN');
const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
const DATADIR = path.join(__dirname, 'data');
const CURSOR = path.join(DATADIR, 'cadence-cursor.json');
@@ -843,38 +851,77 @@ function createdTodayForTable(table) {
// TK-10965: also select price + parent product tags/vendor so the inventory-stamp
// guard can decide per-variant whether positive stock is safe (a $0 / quote-only
// SELLABLE variant must get 0, never 2026, or it becomes $0-orderable).
-const INV_LOOKUP = `query($q:String!){productVariants(first:250,query:$q){edges{node{sku price inventoryItem{id} product{tags vendor}}}}}`;
+async function assertInventoryScopes() {
+ const r = await gqlRetry(`query{currentAppInstallation{accessScopes{handle}}}`, {});
+ const scopes = r?.json?.data?.currentAppInstallation?.accessScopes;
+ if (r?.status !== 200 || r.json?.errors?.length || !Array.isArray(scopes)
+ || !scopes.some(s => s.handle === 'write_inventory')
+ || !scopes.some(s => s.handle === 'read_inventory')) {
+ throw new Error('selected Shopify credential must grant read_inventory and write_inventory');
+ }
+}
+const INV_LOOKUP = `query($q:String!,$loc:ID!){productVariants(first:250,query:$q){pageInfo{hasNextPage} edges{node{sku price inventoryItem{id inventoryLevel(locationId:$loc){id location{id}}} product{tags vendor}}}}}`;
+const INV_ACTIVATE = `mutation($id:ID!,$loc:ID!){inventoryActivate(inventoryItemId:$id,locationId:$loc,available:0){inventoryLevel{id item{id} location{id}} userErrors{field message}}}`;
const INV_SET = `mutation($input:InventorySetQuantitiesInput!){inventorySetQuantities(input:$input){userErrors{field message code}}}`;
+const INV_VERIFY = `query($ids:[ID!]!,$loc:ID!){nodes(ids:$ids){... on InventoryItem{id inventoryLevel(locationId:$loc){id location{id} quantities(names:["on_hand"]){name quantity}}}}}`;
async function setInventory2026(skus) {
- if (!skus.length) return { set: 0, errors: [] };
- const map = new Map();
- for (let i=0;i<skus.length;i+=40) {
- const batch = skus.slice(i, i+40);
- const q = batch.map(s => `sku:"${String(s).replace(/"/g,'\\"')}"`).join(' OR ');
- const r = await gqlRetry(INV_LOOKUP, { q });
- for (const e of (r.json?.data?.productVariants?.edges || [])) {
- if (e.node.sku && e.node.inventoryItem?.id) {
- map.set(e.node.sku, {
- inventoryItemId: e.node.inventoryItem.id,
- variant: { title: e.node.sku, price: e.node.price }, // sku carries -Sample suffix → guard's isSellableVariant reads it
- product: { tags: e.node.product?.tags || [], vendor: e.node.product?.vendor || '' },
- });
- }
- }
+ const errors = [], map = new Map(); let set = 0;
+ const fail = message => { throw new Error(message); };
+ function data(r, key) {
+ if (r?.status !== 200 || !r.json || (r.json.errors && (!Array.isArray(r.json.errors) || r.json.errors.length))) fail(`${key}: HTTP/GraphQL failure ${JSON.stringify(r)}`);
+ const value = r.json.data?.[key];
+ if (value == null) fail(`${key}: missing result`);
+ return value;
}
- // Per-variant quantity via the guard: 2026 for normal priced variants, 0 for the
- // $0/quote-only sellable class (the zero-price-orderable defect this closes).
- const pairs = skus.filter(s => map.has(s)).map(s => {
- const m = map.get(s);
- return { inventoryItemId: m.inventoryItemId, locationId: INV_LOCATION, quantity: safeStampQuantity(m.variant, m.product) };
- });
- const errors = [];
- for (let i=0;i<pairs.length;i+=250) {
- const r = await gqlRetry(INV_SET, { input: { name:'on_hand', reason:'correction', ignoreCompareQuantity:true, quantities: pairs.slice(i,i+250) } });
- const ue = r.json?.data?.inventorySetQuantities?.userErrors || [];
- if (ue.length) errors.push(...ue);
+ function mutation(r, key) {
+ const value = data(r, key);
+ if (!Array.isArray(value.userErrors) || value.userErrors.length) fail(`${key}: ${JSON.stringify(value.userErrors ?? 'missing userErrors')}`);
+ return value;
+ }
+ function levelAtLocation(level) {
+ return level && typeof level.id === 'string' && level.id.length && level.location?.id === INV_LOCATION;
}
- return { set: pairs.length, mapped: map.size, errors };
+ try {
+ if (!Array.isArray(skus) || skus.some(s => typeof s !== 'string' || !s.trim()) || new Set(skus).size !== skus.length) fail('invalid or duplicate requested SKU');
+ if (!skus.length) return { set: 0, mapped: 0, errors };
+ const itemIds = new Set();
+ // Resolve the entire requested set before making any inventory mutation.
+ for (let i=0;i<skus.length;i+=40) {
+ const batch = skus.slice(i, i+40);
+ const q = batch.map(s => `sku:"${s.replace(/\\/g,'\\\\').replace(/"/g,'\\"')}"`).join(' OR ');
+ const variants = data(await gqlRetry(INV_LOOKUP, { q, loc: INV_LOCATION }), 'productVariants');
+ if (!Array.isArray(variants.edges) || variants.pageInfo?.hasNextPage !== false) fail('incomplete SKU lookup');
+ for (const edge of variants.edges) {
+ const n = edge?.node;
+ if (!n || !batch.includes(n.sku) || map.has(n.sku) || !/^gid:\/\/shopify\/InventoryItem\/\d+$/.test(n.inventoryItem?.id || '') || itemIds.has(n.inventoryItem.id)) fail('unexpected, duplicate or invalid SKU/item mapping');
+ if (n.price == null || !n.product || !Array.isArray(n.product.tags) || typeof n.product.vendor !== 'string') fail(`missing stock guard metadata: ${n.sku}`);
+ if (!Object.hasOwn(n.inventoryItem, 'inventoryLevel')) fail(`missing inventory level field: ${n.sku}`);
+ const level = n.inventoryItem.inventoryLevel;
+ if (level !== null && !levelAtLocation(level)) fail(`wrong inventory location: ${n.sku}`);
+ itemIds.add(n.inventoryItem.id);
+ map.set(n.sku, { id:n.inventoryItem.id, active:level !== null, quantity:safeStampQuantity({title:n.sku,price:n.price}, n.product) });
+ }
+ }
+ if (map.size !== skus.length || skus.some(s => !map.has(s))) fail('missing requested SKU');
+ for (const sku of skus) {
+ const item = map.get(sku);
+ try {
+ if (!item.active) {
+ // Activate at zero: guarded positive stock is applied only by the following set.
+ const activated = mutation(await gqlRetry(INV_ACTIVATE, { id:item.id, loc:INV_LOCATION }), 'inventoryActivate');
+ if (!levelAtLocation(activated.inventoryLevel) || activated.inventoryLevel.item?.id !== item.id) fail(`activation identity/location mismatch: ${sku}`);
+ }
+ mutation(await gqlRetry(INV_SET, { input:{name:'on_hand',reason:'correction',ignoreCompareQuantity:true,quantities:[{inventoryItemId:item.id,locationId:INV_LOCATION,quantity:item.quantity}]} }), 'inventorySetQuantities');
+ const nodes = data(await gqlRetry(INV_VERIFY, {ids:[item.id],loc:INV_LOCATION}), 'nodes');
+ const node = Array.isArray(nodes) && nodes.length === 1 ? nodes[0] : null;
+ const quantities = node?.inventoryLevel?.quantities;
+ const onHand = Array.isArray(quantities) ? quantities.filter(q => q.name === 'on_hand') : [];
+ if (node?.id !== item.id || !levelAtLocation(node.inventoryLevel) || onHand.length !== 1 || onHand[0].quantity !== item.quantity) fail(`inventory readback mismatch: ${sku}`);
+ set++;
+ } catch (e) { errors.push({sku,message:e.message}); }
+ }
+ } catch (e) { errors.push({message:e.message}); }
+ return { set, mapped:map.size, errors };
}
// ---- PRE-PUSH DEDUP GUARD (Steve 2026-06-12, DTD-picked structural fix) ----
@@ -954,7 +1001,7 @@ async function createProduct(table, row, payload) {
appendRestore({ table, mfr_sku: row.mfr_sku, dw_sku: row.dw_sku, shopify_product_id: num, action: 'created', activated, published, sampleOnly: !!payload.sampleOnly, status: activated ? 'ACTIVE' : 'DRAFT' });
// sample-only variants are inventory-untracked ($4.25 sample, no stock) → do NOT feed them to
// the inventory=2026 setter (it only applies to tracked roll/sample variants of priced products).
- const invSkus = activated && !payload.sampleOnly ? [row.dw_sku, `${row.dw_sku}-Sample`] : [];
+ const invSkus = !payload.sampleOnly ? [row.dw_sku, `${row.dw_sku}-Sample`] : [];
// showroom-only vendors are excluded from Trending auto-membership (addressable-not-discoverable). TK-11186.
return { ok:true, pid, activated, published, skus: invSkus, showroom: isShowroomVendor(payload.input?.vendor) };
}
@@ -1002,7 +1049,7 @@ async function verifyActivatedMedia(pids) {
// Export internals for unit/inspection harnesses (no behavior change when run as a script;
// the main IIFE below only runs on direct execution via the require.main guard).
-module.exports = { gate, selectSkus, buildInput, buildSampleOnlyInput, brandFilterClause, scrubBanned, bannedBrandReason, verifyActivatedMedia };
+module.exports = { gate, selectSkus, buildInput, buildSampleOnlyInput, brandFilterClause, scrubBanned, bannedBrandReason, verifyActivatedMedia, setInventory2026, assertInventoryScopes };
// ---- main ----
if (require.main === module) (async () => {
@@ -1018,6 +1065,11 @@ if (require.main === module) (async () => {
process.exit(ok === cases.length ? 0 : 1);
}
if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+ // Fail before the first canonical/product write if inventory cannot be completed.
+ if (COMMIT && !GATE_ONLY) {
+ try { await assertInventoryScopes(); }
+ catch (e) { console.error('inventory preflight failed:', e.message); process.exitCode = 1; return; }
+ }
const all = gate();
console.log(`\n=== VENDOR GATE (slot=${SLOT}) ===`);
for (const r of all) console.log(` ${r.ready?'✅':'⛔'} ${r.vendor.padEnd(18)} net-new costed=${String(r.netnew).padStart(5)} ${r.why}`);
@@ -1055,7 +1107,7 @@ if (require.main === module) (async () => {
for (let i=0;i<picks;i++) picked.push(ready[(start+i)%ready.length]);
console.log(`\n=== ${COMMIT?'LIVE COMMIT':'DRY-RUN'} — slot ${SLOT}: lead=${ready[start].vendor} · ${picked.length}/${ready.length} READY vendor(s) × up to ${SKUS_PER_VENDOR} SKUs (cursor idx ${cur.idx}→${(start+picked.length)%ready.length}) ===`);
- const plan = []; let created=0, failed=0, linked=0, held=0, activatedCount=0; const activatedSkus=[]; const publishedProductIds=[]; const activatedPids=[]; let hitMax=false, hitCap=false;
+ const plan = []; let created=0, failed=0, linked=0, held=0, activatedCount=0; const inventorySkus=[]; const publishedProductIds=[]; const activatedPids=[]; let hitMax=false, hitCap=false;
if (MAX_PRODUCTS) console.log(`(--max ${MAX_PRODUCTS} products this run)${ACTIVATE?' --activate: readiness-passing → ACTIVE + inventory 2026':''}`);
for (const r of picked) {
if (hitMax || hitCap) break;
@@ -1110,7 +1162,7 @@ if (require.main === module) (async () => {
const res = await createProduct(r.cfg.table, row, payload);
if (res.held) { held++; /* HELD already logged inside createProduct; counted, never published */ }
else if (res.ok && res.action === 'linked-existing') { linked++; console.log(` ↪ ${row.dw_sku} already on Shopify (${res.status}) → backfilled link ${String(res.pid).replace(/.*\//,'')}, skipped create (dedup guard)`); }
- else if (res.ok) { created++; vendorCreated++; vChanged++; if (res.activated) activatedCount++; if (res.activated && res.skus) activatedSkus.push(...res.skus); if (res.activated && res.pid) activatedPids.push(res.pid); if (res.published && res.pid && !res.showroom) publishedProductIds.push(res.pid); if(created%10===0) process.stdout.write(`\r created ${created}…`); }
+ else if (res.ok) { created++; vendorCreated++; vChanged++; if (res.activated) activatedCount++; if (res.skus) inventorySkus.push(...res.skus); if (res.activated && res.pid) activatedPids.push(res.pid); if (res.published && res.pid && !res.showroom) publishedProductIds.push(res.pid); if(created%10===0) process.stdout.write(`\r created ${created}…`); }
else { failed++; console.log(` ✗ ${row.dw_sku} ${JSON.stringify(res.err).slice(0,140)}`); }
if (res.stop) { console.log(`\n⛔ DAILY_VARIANT_LIMIT reached — stopping (resets in ~24h). created ${created} this run.`); hitCap=true; break; }
// dailyCap per-create stop (PJ): once this vendor created its remaining daily allowance THIS
@@ -1122,11 +1174,12 @@ if (require.main === module) (async () => {
// alert reads completed_at, so even a 0-change slot counts as "this vendor ran".
if (run) run.finish({ status: 'ok', skusFetched: vFetched, skusChanged: vChanged });
}
- // auto-activate-to-live: set inventory=2026 on BOTH variants of every product that went ACTIVE.
- if (COMMIT && ACTIVATE && activatedSkus.length) {
- console.log(`\nsetting inventory=2026 on ${activatedSkus.length} variant(s) (${activatedSkus.length/2} activated products) at Ventura Blvd…`);
- try { const inv = await setInventory2026(activatedSkus); console.log(` inventory set: ${inv.set} pairs, mapped ${inv.mapped}, errors ${inv.errors.length}`); if (inv.errors.length) console.log(' inv errors:', JSON.stringify(inv.errors).slice(0,200)); }
- catch (e) { console.warn(' inventory-set failed (products are ACTIVE but inventory not 2026 — re-run inventory-set-2026.js):', e.message); }
+ // Stock every newly created priced product, including the hourly DRAFT path.
+ // Sample-only products remain untracked; publication/rotation behavior is unchanged.
+ if (COMMIT && inventorySkus.length) {
+ console.log(`\nsetting inventory=2026 on ${inventorySkus.length} variant(s) (${inventorySkus.length/2} new priced products) at Ventura Blvd…`);
+ try { const inv = await setInventory2026(inventorySkus); console.log(` inventory set: ${inv.set} pairs, mapped ${inv.mapped}, errors ${inv.errors.length}`); if (inv.errors.length || inv.set !== new Set(inventorySkus).size) { process.exitCode = 1; console.error(' inventory incomplete:', JSON.stringify(inv)); } }
+ catch (e) { process.exitCode = 1; console.error(' inventory-set failed (created products have unconfirmed inventory — re-run inventory-set-2026.js):', e.message); }
}
// TK-10040 image-gate sweep: demote any activated product whose async image upload never landed.
let imageDemoted = 0;
@@ -1152,7 +1205,7 @@ if (require.main === module) (async () => {
const planFile = path.join(DATADIR, `cadence-plan-${SLOT}-${TODAY}.json`);
fs.mkdirSync(DATADIR,{recursive:true}); fs.writeFileSync(planFile, JSON.stringify(plan,null,1));
// activatedCount counts every product that went ACTIVE (incl. sample-only, whose untracked
- // sample returns no inventory skus — so it can't be derived from activatedSkus.length/2).
+ // sample returns no inventory skus — so it can't be derived from inventorySkus.length/2).
const activatedN = activatedCount;
console.log(`\n\n${COMMIT?`CREATED ${created} (${ACTIVATE?activatedN+' ACTIVE / '+(created-activatedN)+' DRAFT':'all DRAFT'}), linked-existing ${linked} (dedup guard), held ${held} (banned-Brand denylist), failed ${failed}`:`DRY-RUN planned ${plan.length} products${ACTIVATE?` (${plan.filter(p=>p.willActivate).length} would activate)`:''}${plan.some(p=>p.held)?` — ${plan.filter(p=>p.held).length} HELD (banned-Brand denylist)`:''}`} → ${planFile}`);
if (COMMIT) console.log(`restore map: ${RESTORE_FILE} (batch ${BATCH_ID})`);
@@ -1168,5 +1221,5 @@ if (require.main === module) (async () => {
execFileSync('node', [path.join(__dirname, 'compute-price-coverage.js'), '--quiet'], { stdio: 'ignore' });
console.log('price-coverage recomputed.');
} catch (e) { console.warn('price-coverage recompute skipped:', e.message); }
- if (hitCap) process.exit(3); // resumable signal: daily variant cap hit, runner resumes next slot
+ if (hitCap && !process.exitCode) process.exit(3); // resumable signal: daily variant cap hit, runner resumes next slot
})();
diff --git a/shopify/scripts/cadence/tests/inventory-create-path.test.mjs b/shopify/scripts/cadence/tests/inventory-create-path.test.mjs
new file mode 100644
index 00000000..4d4ea6e4
--- /dev/null
+++ b/shopify/scripts/cadence/tests/inventory-create-path.test.mjs
@@ -0,0 +1,93 @@
+import fs from 'node:fs';import vm from 'node:vm';import assert from 'node:assert/strict';import crypto from 'node:crypto';import {safeStampQuantity} from '../../lib/inventory-stamp-guard.mjs';
+const dir=new URL('./',import.meta.url),source=fs.readFileSync(new URL('../cadence-import.js',dir),'utf8');
+const loc='gid://shopify/Location/5795643504',id='gid://shopify/InventoryItem/1';const clone=x=>JSON.parse(JSON.stringify(x));
+const results=[];async function test(name,fn){try{await fn();results.push({name,verdict:'PASS'});}catch(e){results.push({name,verdict:'FAIL',reason:e.stack});}}
+function fixture(options={}){
+ const state={active:options.active??false,quantity:options.quantity??0,calls:[],rows:options.rows??[{sku:'FIXTURE',price:options.price??'10',product:{tags:options.tags??[],vendor:options.vendor??'Fixture'}}]};
+ function level(){return {id:'gid://shopify/InventoryLevel/1',location:{id:loc},item:{id},quantities:[{name:'on_hand',quantity:state.quantity}]};}
+ const gqlRetry=async(q,v)=>{
+ const stage=q.includes('productVariants')?'lookup':q.includes('inventoryActivate')?'activate':q.includes('inventorySetQuantities')?'set':'verify';state.calls.push({stage,variables:clone(v)});
+ if(options.failure?.stage===stage){const mode=options.failure.mode;if(mode==='throw')throw Error('synthetic transport');if(mode==='http')return {status:500,json:{data:{}}};if(mode==='top')return {status:200,json:{errors:[{message:'synthetic GraphQL rejection'}],data:{}}};if(mode==='null')return {status:200,json:{data:{[stage==='activate'?'inventoryActivate':stage==='set'?'inventorySetQuantities':stage==='lookup'?'productVariants':'nodes']:null}}};if(mode==='reject')return {status:200,json:{data:{[stage==='activate'?'inventoryActivate':'inventorySetQuantities']:{userErrors:[{message:'rejected'}]}}}};}
+ let data;
+ if(stage==='lookup')data={productVariants:{pageInfo:{hasNextPage:!!options.truncated},edges:state.rows.map((row,i)=>({node:{...row,inventoryItem:{id:options.badLookupId?'wrong':options.sharedId?id:`gid://shopify/InventoryItem/${i+1}`,inventoryLevel:state.active?{...level(),location:{id:options.wrongLookupLocation?'wrong':loc}}:null}}}))}};
+ if(stage==='activate'){
+ assert.equal(v.id,id);assert.equal(v.loc,loc);assert.equal(state.active,false,'must not activate active item');assert(q.includes('available:0'),'activation must start at zero');state.active=true;state.quantity=0;
+ data={inventoryActivate:{userErrors:[],inventoryLevel:{...level(),item:{id:options.wrongActivationId?'wrong':id},location:{id:options.wrongActivationLocation?'wrong':loc}}}};
+ }
+ if(stage==='set'){
+ assert.equal(state.active,true,'cannot set unstocked item');assert.equal(v.input.quantities.length,1);const pair=v.input.quantities[0];assert.equal(pair.inventoryItemId,id);assert.equal(pair.locationId,loc);assert.equal(v.input.name,'on_hand');state.quantity=pair.quantity;data={inventorySetQuantities:{userErrors:[]}};
+ }
+ if(stage==='verify'){
+ assert.deepEqual(Array.from(v.ids),[id]);assert.equal(v.loc,loc);const l=level();if(options.verifyMismatch)l.quantities[0].quantity++;if(options.wrongVerifyLocation)l.location.id='wrong';data={nodes:[{id:options.wrongVerifyId?'wrong':id,inventoryLevel:l}]};
+ }
+ if(options.failure?.stage===stage&&options.failure.mode==='missingErrors')delete data[stage==='activate'?'inventoryActivate':'inventorySetQuantities'].userErrors;
+ return {status:200,json:{data}};
+ };
+ const c=vm.createContext({safeStampQuantity,INV_LOCATION:loc,gqlRetry});vm.runInContext(source.slice(source.indexOf('const INV_LOOKUP ='),source.indexOf('// ---- PRE-PUSH DEDUP GUARD')),c);return {state,run:skus=>c.setInventory2026(skus??state.rows.map(r=>r.sku))};
+}
+for(const active of [false,true])await test(active?'already-active-no-activation':'inactive-activate-before-set',async()=>{const f=fixture({active}),r=await f.run();assert.equal(r.set,1);assert.equal(r.errors.length,0);assert.equal(f.state.quantity,2026);assert.deepEqual(f.state.calls.map(x=>x.stage),active?['lookup','set','verify']:['lookup','activate','set','verify']);});
+for(const stage of ['lookup','activate','set','verify'])for(const mode of ['null','http','top','throw',...(['activate','set'].includes(stage)?['reject','missingErrors']:[])])await test(`${stage}-${mode}-not-confirmed`,async()=>{const f=fixture({failure:{stage,mode}}),r=await f.run();assert.equal(r.set,0);assert(r.errors.length);if(['lookup','activate'].includes(stage))assert(!f.state.calls.some(c=>c.stage==='set'));});
+for(const key of ['badLookupId','wrongLookupLocation','wrongActivationId','wrongActivationLocation','wrongVerifyId','wrongVerifyLocation','verifyMismatch','truncated'])await test(key,async()=>{const f=fixture({[key]:true,active:key==='wrongLookupLocation'}),r=await f.run();assert.equal(r.set,0);assert(r.errors.length);if(!key.includes('Verify')&&key!=='verifyMismatch')assert(!f.state.calls.some(c=>c.stage==='set'));});
+await test('missing-SKU-blocks-all-mutations',async()=>{const f=fixture();const r=await f.run(['MISSING']);assert.equal(r.set,0);assert(r.errors.length);assert.equal(f.state.calls.length,1);});
+await test('duplicate-request-blocks-all',async()=>{const f=fixture();const r=await f.run(['FIXTURE','FIXTURE']);assert(r.errors.length);assert.equal(f.state.calls.length,0);});
+await test('duplicate-returned-SKU-blocks-all',async()=>{const f=fixture({rows:[{sku:'FIXTURE',price:'10',product:{tags:[],vendor:'F'}},{sku:'FIXTURE',price:'10',product:{tags:[],vendor:'F'}}]});const r=await f.run(['FIXTURE']);assert(r.errors.length);assert.equal(r.set,0);assert.equal(f.state.calls.length,1);});
+await test('duplicate-inventory-ID-blocks-all',async()=>{const f=fixture({sharedId:true,rows:[{sku:'ONE',price:'10',product:{tags:[],vendor:'F'}},{sku:'TWO',price:'10',product:{tags:[],vendor:'F'}}]});const r=await f.run();assert(r.errors.length);assert.equal(r.set,0);assert.equal(f.state.calls.length,1);});
+await test('retry-idempotent',async()=>{const f=fixture();assert.equal((await f.run()).set,1);assert.equal((await f.run()).set,1);assert.equal(f.state.calls.filter(c=>c.stage==='activate').length,1);assert.equal(f.state.quantity,2026);});
+for(const [name,options,expected] of [['priced',{},2026],['zero',{price:'0'},0],['nan',{price:'bad'},0],['quote',{tags:['quote-only']},0],['needs-price',{tags:['Needs-Price']},0],['vendor',{vendor:'Fentucci Naturals'},0],['sample',{rows:[{sku:'FIXTURE-Sample',price:'4.25',product:{tags:['quote-only'],vendor:'Fentucci Naturals'}}]},2026]])await test('actual-guard-'+name,async()=>{const f=fixture(options),r=await f.run();assert.equal(r.set,1);assert.equal(r.errors.length,0);assert.equal(f.state.quantity,expected);});
+await test('retry-after-unconfirmed-set-does-not-reactivate',async()=>{const options={failure:{stage:'set',mode:'null'}};const f=fixture(options);assert.equal((await f.run()).set,0);delete options.failure;assert.equal((await f.run()).set,1);assert.equal(f.state.calls.filter(c=>c.stage==='activate').length,1);});
+await test('empty-input-noop',async()=>{const f=fixture();const r=await f.run([]);assert.equal(r.set,0);assert.equal(r.errors.length,0);assert.equal(f.state.calls.length,0);});
+await test('partial-success-counts-only-confirmed-item',async()=>{
+ let quantity=0;const c=vm.createContext({safeStampQuantity,INV_LOCATION:loc,gqlRetry:async(q,v)=>{
+ if(q.includes('productVariants'))return {status:200,json:{data:{productVariants:{pageInfo:{hasNextPage:false},edges:[1,2].map(n=>({node:{sku:'SKU'+n,price:'10',product:{tags:[],vendor:'Fixture'},inventoryItem:{id:'gid://shopify/InventoryItem/'+n,inventoryLevel:{id:'level'+n,location:{id:loc}}}}}))}}}};
+ if(q.includes('inventorySetQuantities')){const pair=v.input.quantities[0];if(pair.inventoryItemId.endsWith('/2'))return {status:200,json:{data:{inventorySetQuantities:null}}};quantity=pair.quantity;return {status:200,json:{data:{inventorySetQuantities:{userErrors:[]}}}};}
+ assert.equal(v.ids[0],id);return {status:200,json:{data:{nodes:[{id,inventoryLevel:{id:'level1',location:{id:loc},quantities:[{name:'on_hand',quantity}]}}]}}};
+ }});vm.runInContext(source.slice(source.indexOf('const INV_LOOKUP ='),source.indexOf('// ---- PRE-PUSH DEDUP GUARD')),c);const r=await c.setInventory2026(['SKU1','SKU2']);assert.equal(r.set,1);assert.equal(r.mapped,2);assert.equal(r.errors.length,1);assert.equal(r.errors[0].sku,'SKU2');
+});
+await test('41-SKU-two-lookup-chunks-exact-independent-success',async()=>{
+ const items=new Map(Array.from({length:41},(_,i)=>['gid://shopify/InventoryItem/'+(i+1),{sku:'SKU'+(i+1),active:false,quantity:0}])),calls=[];
+ const level=(iid,item)=>({id:'gid://shopify/InventoryLevel/'+iid.split('/').pop(),item:{id:iid},location:{id:loc},quantities:[{name:'on_hand',quantity:item.quantity}]});
+ const c=vm.createContext({safeStampQuantity,INV_LOCATION:loc,gqlRetry:async(q,v)=>{
+ const stage=q.includes('productVariants')?'lookup':q.includes('inventoryActivate')?'activate':q.includes('inventorySetQuantities')?'set':'verify';calls.push({stage,variables:clone(v)});let data;
+ if(stage==='lookup'){assert.equal(v.loc,loc);const requested=Array.from(v.q.matchAll(/sku:"([^"]+)"/g),m=>m[1]);assert(requested.length<=40);data={productVariants:{pageInfo:{hasNextPage:false},edges:Array.from(items,([iid,item])=>({node:{sku:item.sku,price:'10',product:{tags:[],vendor:'Fixture'},inventoryItem:{id:iid,inventoryLevel:item.active?level(iid,item):null}}})).filter(e=>requested.includes(e.node.sku))}};}
+ if(stage==='activate'){assert.equal(v.loc,loc);const item=items.get(v.id);assert(item);assert.equal(item.active,false);assert(q.includes('available:0'));item.active=true;data={inventoryActivate:{userErrors:[],inventoryLevel:level(v.id,item)}};}
+ if(stage==='set'){assert.equal(v.input.quantities.length,1);const pair=v.input.quantities[0],item=items.get(pair.inventoryItemId);assert(item);assert.equal(pair.locationId,loc);assert.equal(item.active,true);assert.equal(pair.quantity,2026);item.quantity=pair.quantity;data={inventorySetQuantities:{userErrors:[]}};}
+ if(stage==='verify'){assert.equal(v.loc,loc);assert.equal(v.ids.length,1);data={nodes:v.ids.map(iid=>{const item=items.get(iid);assert(item);return {id:iid,inventoryLevel:level(iid,item)};})};}
+ return {status:200,json:{data}};
+ }});vm.runInContext(source.slice(source.indexOf('const INV_LOOKUP ='),source.indexOf('// ---- PRE-PUSH DEDUP GUARD')),c);
+ const r=await c.setInventory2026(Array.from(items.values(),x=>x.sku));assert.equal(r.set,41);assert.equal(r.mapped,41);assert.equal(r.errors.length,0);assert.equal(calls.filter(x=>x.stage==='lookup').length,2);assert.equal(calls.length,125);assert.equal(new Set(calls.filter(x=>x.stage==='set').map(x=>x.variables.input.quantities[0].inventoryItemId)).size,41);assert(Array.from(items.values()).every(x=>x.active&&x.quantity===2026));
+});
+await test('activation-generic-UserError-schema-selection',()=>{const q=source.match(/const INV_ACTIVATE = `([^`]+)`;/)[1];const fields=q.match(/userErrors\{([^}]+)\}/)[1].trim().split(/\s+/);assert.deepEqual(fields,['field','message']);});
+const caller=source.slice(source.indexOf(' // Stock every newly created priced product,'),source.indexOf(' // TK-10040 image-gate sweep:'));
+async function callerRun(fn){const process={exitCode:0},logs=[];const c=vm.createContext({COMMIT:true,ACTIVATE:false,inventorySkus:['FIXTURE'],setInventory2026:fn,process,console:{log:(...a)=>logs.push(a),error:(...a)=>logs.push(a)}});await vm.runInContext('(async()=>{'+caller+'})()',c);return {process,logs};}
+await test('actual-caller-real-helper-failure-nonzero',async()=>{const f=fixture({failure:{stage:'set',mode:'null'}});const c=await callerRun(()=>f.run());assert.equal(c.process.exitCode,1);assert(c.logs.some(x=>String(x[0]).includes('inventory incomplete')));});
+await test('actual-caller-thrown-failure-nonzero',async()=>{const c=await callerRun(async()=>{throw Error('fixture')});assert.equal(c.process.exitCode,1);assert(c.logs.some(x=>String(x[0]).includes('inventory-set failed')));});
+await test('actual-caller-underreported-count-nonzero',async()=>assert.equal((await callerRun(async()=>({set:0,mapped:1,errors:[]}))).process.exitCode,1));
+await test('actual-caller-success-zero',async()=>{const f=fixture();assert.equal((await callerRun(()=>f.run())).process.exitCode,0);});
+
+const scopesBlock=source.slice(source.indexOf('async function assertInventoryScopes()'),source.indexOf('const INV_LOOKUP ='));
+for (const [name,response,allowed] of [
+ ['inventory-scopes-ok',{status:200,json:{data:{currentAppInstallation:{accessScopes:[{handle:'read_inventory'},{handle:'write_inventory'}]}}}},true],
+ ['inventory-scopes-narrow',{status:200,json:{data:{currentAppInstallation:{accessScopes:[{handle:'read_products'}]}}}},false],
+ ['inventory-scopes-null',{status:200,json:{data:null}},false],
+ ['inventory-scopes-HTTP',{status:401,json:{}},false],
+ ['inventory-scopes-top-errors',{status:200,json:{errors:[{message:'denied'}],data:null}},false]
+]) await test(name,async()=>{const c=vm.createContext({gqlRetry:async()=>response});vm.runInContext(scopesBlock,c);if(allowed)await c.assertInventoryScopes();else await assert.rejects(c.assertInventoryScopes());});
+await test('actual-main-stops-before-DB-without-inventory-permissions',async()=>{
+ let gateCalls=0;const module={};const proc={exitCode:0};const c=vm.createContext({require:{main:module},module,flag:()=>false,TOKEN:'fixture',COMMIT:true,GATE_ONLY:false,assertInventoryScopes:async()=>{throw Error('no scopes');},gate:()=>{gateCalls++;throw Error('must not reach DB');},console:{error(){}},process:proc});
+ await vm.runInContext(source.slice(source.indexOf('if (require.main === module)')),c);assert.equal(gateCalls,0);assert.equal(proc.exitCode,1);
+});
+const createBlock=source.slice(source.indexOf('async function createProduct('),source.indexOf('// TK-10040 IMAGE-GATE'));
+for(const [sampleOnly,active] of [[false,false],[false,true],[true,false],[true,true]])await test(`actual-create-queue-sampleOnly=${sampleOnly}-active=${active}`,async()=>{
+ const calls=[];const c=vm.createContext({bannedBrandReason:()=>null,findExistingProduct:async()=>null,logPriceToDb:()=>calls.push('price'),gqlRetry:async()=>({status:200,json:{data:{productSet:{product:{id:'gid://shopify/Product/1'},userErrors:[]}}}}),PRODUCTSET:'fixture',psql:()=>calls.push('DB'),publishToChannels:async()=>{calls.push('publish');return {published:true};},appendRestore:()=>{},isShowroomVendor:()=>false,console});
+ vm.runInContext(createBlock,c);const r=await c.createProduct('fixture',{dw_sku:'FIXTURE',mfr_sku:'fixture'},{input:{vendor:'Fixture',metafields:[]},sampleOnly,willActivate:active,retail:10});
+ assert.equal(r.ok,true);assert.equal(r.activated,active);assert.deepEqual(Array.from(r.skus),sampleOnly?[]:['FIXTURE','FIXTURE-Sample']);assert.equal(calls.includes('publish'),active);
+});
+const credsBlock=source.slice(source.indexOf('function readToken('),source.indexOf('const STORE ='))+';globalThis.selected=TOKEN;';
+for(const [name,environment,files,want] of [
+ ['env-full',{SHOPIFY_FULL_ACCESS_TOKEN:'envfull',SHOPIFY_ADMIN_TOKEN:'narrow'},{},'envfull'],
+ ['repo-full-over-wrapper-narrow',{SHOPIFY_ADMIN_TOKEN:'narrow'},{'/repo/.env':'SHOPIFY_FULL_ACCESS_TOKEN=repofull'},'repofull'],
+ ['fallback-narrow',{}, {'/home/Projects/secrets-manager/.env':'SHOPIFY_ADMIN_TOKEN=narrow'},'narrow'],
+ ['missing-all',{}, {},undefined]
+])await test('credential-'+name,()=>{const c=vm.createContext({process:{env:environment},fs:{readFileSync:p=>{if(!(p in files))throw Error('ENOENT');return files[p];}},path:{resolve:()=>'/repo/.env'},os:{homedir:()=>'/home'},__dirname:'/repo/shopify/scripts/cadence'});vm.runInContext(credsBlock,c);assert.equal(c.selected,want);});
+
+const report={correlation:'TK-11426-adoption',passed:results.filter(r=>r.verdict==='PASS').length,failed:results.filter(r=>r.verdict==='FAIL').length,results,scope:'Offline extracted actual candidate helper and actual caller block; stateful fake GraphQL. No live/schema/runtime verification.',sourceSHA:crypto.createHash('sha256').update(source).digest('hex')};console.log(JSON.stringify(report));process.exitCode=report.failed?1:0;
← 187088fe auto-data-snapshot: 2026-09-11T08:16:47 (2 data files) — DW-
·
back to Designer Wallcoverings
·
TK-11240: repoint Other Colorways module off title-match ont 2e8e01e5 →