← back to Dw Signup Fulfillment
TK-11285: fix a clobber bug in my own commitRows merge, and test it
f849a46e02fae0a3d86cbab0a514944ed8ec06f3 · 2026-09-10 07:54:15 -0700 · Steve Abrams
Self-review caught a real defect in bef6f4f. The customer_id_conflict branch did
`patch.shopify_customer_id = undefined` to skip the field, but an undefined value
still spreads: {...current, ...patch} then ERASES the id the service resolved, and
JSON.stringify drops the key entirely. The guard meant to protect the service's
write was deleting it. Use `delete` instead.
Adds verification/tk11285/commit-rows-test.js — 9 focused checks on the merge:
concurrent intake survives, a service decision is never overwritten, the service's
customer id wins a conflict, receipts are kept so no duplicate letter, a vanished
row is not resurrected, untouched rows stay byte-identical.
Proven non-tautological: reintroducing the bug fails 2/9 (cust=undefined, key
erased); with the fix 9/9 PASS exit 0.
Also gates the CLI behind require.main === module so commitRows is testable.
CLI behaviour unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon
Files touched
M scripts/recover-stuck-apps.jsA verification/tk11285/commit-rows-test.js
Diff
commit f849a46e02fae0a3d86cbab0a514944ed8ec06f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 07:54:15 2026 -0700
TK-11285: fix a clobber bug in my own commitRows merge, and test it
Self-review caught a real defect in bef6f4f. The customer_id_conflict branch did
`patch.shopify_customer_id = undefined` to skip the field, but an undefined value
still spreads: {...current, ...patch} then ERASES the id the service resolved, and
JSON.stringify drops the key entirely. The guard meant to protect the service's
write was deleting it. Use `delete` instead.
Adds verification/tk11285/commit-rows-test.js — 9 focused checks on the merge:
concurrent intake survives, a service decision is never overwritten, the service's
customer id wins a conflict, receipts are kept so no duplicate letter, a vanished
row is not resurrected, untouched rows stay byte-identical.
Proven non-tautological: reintroducing the bug fails 2/9 (cust=undefined, key
erased); with the fix 9/9 PASS exit 0.
Also gates the CLI behind require.main === module so commitRows is testable.
CLI behaviour unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEDp3MXiKVJ5GDKaQCHZon
---
scripts/recover-stuck-apps.js | 10 ++++-
verification/tk11285/commit-rows-test.js | 65 ++++++++++++++++++++++++++++++++
2 files changed, 73 insertions(+), 2 deletions(-)
diff --git a/scripts/recover-stuck-apps.js b/scripts/recover-stuck-apps.js
index 06e185d..ddadff4 100644
--- a/scripts/recover-stuck-apps.js
+++ b/scripts/recover-stuck-apps.js
@@ -97,7 +97,9 @@ function commitRows(touched) {
// Never clobber a customer id the service resolved itself.
if (f === 'shopify_customer_id' && current[f] && String(current[f]) !== String(t[f])) {
skipped.push({ id: t.id, reason: 'customer_id_conflict' });
- patch.shopify_customer_id = undefined;
+ // delete, never `= undefined`: an undefined value still spreads and would
+ // erase the id the service resolved.
+ delete patch.shopify_customer_id;
continue;
}
patch[f] = t[f];
@@ -242,4 +244,8 @@ async function main() {
}
}
-main().catch((e) => { console.error('recover-stuck-apps error:', e.message); process.exit(1); });
+if (require.main === module) {
+ main().catch((e) => { console.error('recover-stuck-apps error:', e.message); process.exit(1); });
+} else {
+ module.exports = { commitRows, readRows, writeRows, LINK_FIELDS, RECEIPT_FIELDS };
+}
diff --git a/verification/tk11285/commit-rows-test.js b/verification/tk11285/commit-rows-test.js
new file mode 100644
index 0000000..3bac18f
--- /dev/null
+++ b/verification/tk11285/commit-rows-test.js
@@ -0,0 +1,65 @@
+'use strict';
+// Focused tests for commitRows() in scripts/recover-stuck-apps.js — the merge that
+// stops this out-of-process writer from clobbering the live service's writes.
+const fs = require('fs');
+const path = require('path');
+const ROOT = path.join(__dirname, '..', '..');
+const trade = require(path.join(ROOT, 'lib', 'trade'));
+const { commitRows } = require(path.join(ROOT, 'scripts', 'recover-stuck-apps.js'));
+
+const APPS = trade.APPS_PATH;
+const write = rows => fs.writeFileSync(APPS, rows.map(r => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : ''));
+const read = () => fs.readFileSync(APPS, 'utf8').split('\n').filter(Boolean).map(l => JSON.parse(l));
+const row = (o) => ({ id: o.id, email: o.id + '@e.com', status: 'pending', created_at: '2026-09-01T00:00:00.000Z', shopify_customer_id: null, ...o });
+
+const results = [];
+const check = (name, pass, detail) => { results.push({ name, verdict: pass ? 'PASS' : 'FAIL', detail }); };
+
+// 1. a row that arrived after our snapshot is preserved
+write([row({ id: 'A' })]);
+const snapshotA = read();
+write([row({ id: 'A' }), row({ id: 'NEW' })]); // service appended concurrently
+snapshotA[0].shopify_customer_id = '111'; snapshotA[0].link_status = 'linked';
+commitRows(snapshotA);
+check('new intake appended during the window survives', read().some(r => r.id === 'NEW'), read().map(r => r.id).join(','));
+check('our linkage still applied', read().find(r => r.id === 'A').shopify_customer_id === '111');
+
+// 2. a decision made during the window is not overwritten
+write([row({ id: 'B' })]);
+const snapB = read();
+write([row({ id: 'B', status: 'approved', decision: 'approved', shopify_customer_id: '222' })]);
+snapB[0].shopify_customer_id = '999'; snapB[0].link_status = 'linked'; snapB[0].recovery_emailed = true;
+commitRows(snapB);
+const b = read().find(r => r.id === 'B');
+check('service decision preserved', b.status === 'approved', 'status=' + b.status);
+check('service customer id preserved (not clobbered by our stale link)', b.shopify_customer_id === '222', 'cust=' + b.shopify_customer_id);
+check('email receipt still recorded (no duplicate letter later)', b.recovery_emailed === true);
+
+// 3. customer_id conflict must NOT erase the id the service resolved
+// (regression: `patch.x = undefined` still spreads and deletes the key)
+write([row({ id: 'C' })]);
+const snapC = read();
+write([row({ id: 'C', shopify_customer_id: '333' })]); // service resolved it first
+snapC[0].shopify_customer_id = '444'; // we resolved a different one
+commitRows(snapC);
+const c = read().find(r => r.id === 'C');
+check('conflicting customer id: service value kept', c.shopify_customer_id === '333', 'cust=' + c.shopify_customer_id);
+check('conflicting customer id: key not erased', 'shopify_customer_id' in c && c.shopify_customer_id != null, JSON.stringify(c));
+
+// 4. a row we touched that has since vanished is not resurrected
+write([row({ id: 'D' })]);
+const snapD = read();
+write([]); // row gone
+commitRows(snapD);
+check('vanished row is not resurrected', read().length === 0, 'rows=' + read().length);
+
+// 5. rows we never touched are left byte-identical
+write([row({ id: 'E' }), row({ id: 'UNTOUCHED', business_name: 'keep me' })]);
+const snapE = read();
+commitRows([snapE[0]]);
+const u = read().find(r => r.id === 'UNTOUCHED');
+check('untouched rows preserved verbatim', JSON.stringify(u) === JSON.stringify(snapE[1]));
+
+const failures = results.filter(r => r.verdict === 'FAIL');
+console.log(JSON.stringify({ suite: 'commitRows', total: results.length, passed: results.length - failures.length, failed: failures.length, results }, null, 2));
+process.exit(failures.length ? 1 : 0);
← 251724b TK-10836: Option D complete — the $0.00 free-shipping rate W
·
back to Dw Signup Fulfillment
·
TK-10836: contrarian overturns my Option D headline; correct 944582a →