← back to Dw Signup Fulfillment
TK-11377: stop my own test being a fourth writer of the live store
86dd5a0bb38127c82115cbda4b0835581ea0090d · 2026-09-10 09:04:30 -0700 · Steve Abrams
Cody's DTD dissent caught this and it was right. The previous revision of
verification/tk11285/sendonly-filter-test.js wrote its fixture straight to
trade.APPS_PATH - the REAL data/trade-applications.jsonl - and restored it
afterwards with no try/finally. One thrown execFileSync would have left six
synthetic rows (PENDING-A@example.com, APPROVED-A@example.com, ...) sitting in
the live applications store.
That is the exact hazard class this ticket and TK-11285 exist to close, in the
test written to prove the fix. It survived only because .deploy.conf excludes
both /verification and /data/trade-applications.jsonl from rsync, i.e. on a
human remembering to keep an exclude list correct forever. scripts/selftest.js,
which I edited in the same commit, already did it correctly with --file into a
tmpdir fixture; I had the right pattern in front of me and did not use it.
Now drives the CLI's own --file flag against a throwaway mkdtemp fixture,
cleaned up in a finally. Adds a 7th check asserting the real store path is never
even referenced in the output.
Proof: hashed data/trade-applications.jsonl before and after a run - byte
identical. Still non-tautological: reverting the filter fails 3/7 (approved and
rejected both get emailed).
Suite on master: trade-approval 27/27, commitRows 9/9, sendonly-filter 7/7,
DRY_RUN selftest pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Files touched
M verification/tk11285/sendonly-filter-test.js
Diff
commit 86dd5a0bb38127c82115cbda4b0835581ea0090d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 09:04:30 2026 -0700
TK-11377: stop my own test being a fourth writer of the live store
Cody's DTD dissent caught this and it was right. The previous revision of
verification/tk11285/sendonly-filter-test.js wrote its fixture straight to
trade.APPS_PATH - the REAL data/trade-applications.jsonl - and restored it
afterwards with no try/finally. One thrown execFileSync would have left six
synthetic rows (PENDING-A@example.com, APPROVED-A@example.com, ...) sitting in
the live applications store.
That is the exact hazard class this ticket and TK-11285 exist to close, in the
test written to prove the fix. It survived only because .deploy.conf excludes
both /verification and /data/trade-applications.jsonl from rsync, i.e. on a
human remembering to keep an exclude list correct forever. scripts/selftest.js,
which I edited in the same commit, already did it correctly with --file into a
tmpdir fixture; I had the right pattern in front of me and did not use it.
Now drives the CLI's own --file flag against a throwaway mkdtemp fixture,
cleaned up in a finally. Adds a 7th check asserting the real store path is never
even referenced in the output.
Proof: hashed data/trade-applications.jsonl before and after a run - byte
identical. Still non-tautological: reverting the filter fails 3/7 (approved and
rejected both get emailed).
Suite on master: trade-approval 27/27, commitRows 9/9, sendonly-filter 7/7,
DRY_RUN selftest pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
verification/tk11285/sendonly-filter-test.js | 88 +++++++++++++++++-----------
1 file changed, 54 insertions(+), 34 deletions(-)
diff --git a/verification/tk11285/sendonly-filter-test.js b/verification/tk11285/sendonly-filter-test.js
index ce9cf04..7e5d7ff 100644
--- a/verification/tk11285/sendonly-filter-test.js
+++ b/verification/tk11285/sendonly-filter-test.js
@@ -2,41 +2,61 @@
// --send-only must never email an application that has already been decided.
// Shaped from the real prod rows measured 2026-09-10 (TK-11377): 4 approved
// designers sat inside the old filter's target set.
-const fs=require('fs'),path=require('path'),{execFileSync}=require('child_process');
-const ROOT=path.join(__dirname,'..','..');
-const trade=require(path.join(ROOT,'lib','trade'));
-const APPS=trade.APPS_PATH;
-const snap=fs.existsSync(APPS)?fs.readFileSync(APPS):null;
+//
+// SAFETY: this test NEVER touches data/trade-applications.jsonl. It drives the
+// CLI's own --file flag against a throwaway fixture in tmpdir, the way
+// scripts/selftest.js already does. An earlier revision of this file wrote the
+// fixture straight to trade.APPS_PATH and restored it afterwards with no
+// try/finally — one thrown execFileSync would have left six fake rows in the
+// live store. A test that proves a data-safety fix must not itself be a
+// data-safety hole.
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+const { execFileSync } = require('child_process');
-const row=(id,status,extra={})=>({id,email:id.toLowerCase()+'@example.com',business_name:id,
- shopify_customer_id:'700'+id.length,link_status:'linked',link_via:'existing',
- status,created_at:'2026-09-02T00:00:00.000Z',
- decided_at:status==='pending'?null:'2026-09-03T00:00:00.000Z',
- decision:status==='pending'?null:status,assigned_rep:null,...extra});
+const ROOT = path.join(__dirname, '..', '..');
+const CLI = path.join(ROOT, 'scripts', 'recover-stuck-apps.js');
+const FIXTURE = path.join(fs.mkdtempSync(path.join(os.tmpdir(), 'tk11377-')), 'apps.jsonl');
-fs.writeFileSync(APPS,[
- row('PENDING-A','pending'), row('PENDING-B','pending'),
- row('APPROVED-A','approved'), row('APPROVED-B','approved'),
- row('REJECTED-A','rejected'),
- row('ALREADY-EMAILED','pending',{recovery_emailed:true}),
-].map(r=>JSON.stringify(r)).join('\n')+'\n');
+const row = (id, status, extra = {}) => ({
+ id, email: id.toLowerCase() + '@example.com', business_name: id,
+ shopify_customer_id: '700' + id.length, link_status: 'linked', link_via: 'existing',
+ status, created_at: '2026-09-02T00:00:00.000Z',
+ decided_at: status === 'pending' ? null : '2026-09-03T00:00:00.000Z',
+ decision: status === 'pending' ? null : status, assigned_rep: null, ...extra,
+});
-// DRY preview: prints who it WOULD email, sends nothing, writes nothing.
-const out=execFileSync(process.execPath,[path.join(ROOT,'scripts','recover-stuck-apps.js'),'--send-only'],
- {cwd:ROOT,env:{...process.env,DRY_RUN:'1'},encoding:'utf8'});
+let results = [];
+try {
+ fs.writeFileSync(FIXTURE, [
+ row('PENDING-A', 'pending'), row('PENDING-B', 'pending'),
+ row('APPROVED-A', 'approved'), row('APPROVED-B', 'approved'),
+ row('REJECTED-A', 'rejected'),
+ row('ALREADY-EMAILED', 'pending', { recovery_emailed: true }),
+ ].map((r) => JSON.stringify(r)).join('\n') + '\n');
-const emailed=[...out.matchAll(/\[email:designer\]\s+(\S+)/g)].map(m=>m[1]);
-const checks=[
- ['pending applications are still emailed', emailed.includes('pending-a@example.com')&&emailed.includes('pending-b@example.com')],
- ['APPROVED application is NOT emailed', !emailed.some(e=>e.startsWith('approved-'))],
- ['REJECTED application is NOT emailed', !emailed.some(e=>e.startsWith('rejected-'))],
- ['already-emailed row still skipped (idempotent)', !emailed.includes('already-emailed@example.com')],
- ['skipped decided rows are reported, not silent', /SKIPPING 3 already-decided/.test(out)],
- ['exactly 2 designers emailed', emailed.length===2],
-];
-if(snap!==null) fs.writeFileSync(APPS,snap); else { try{fs.unlinkSync(APPS)}catch{} }
-const failed=checks.filter(c=>!c[1]);
-console.log(JSON.stringify({suite:'send-only pending-only filter',emailed,
- results:checks.map(([name,ok])=>({name,verdict:ok?'PASS':'FAIL'})),
- passed:checks.length-failed.length,failed:failed.length},null,2));
-process.exit(failed.length?1:0);
+ // DRY preview against the FIXTURE: sends nothing, writes nothing real.
+ const out = execFileSync(process.execPath, [CLI, '--send-only', '--file', FIXTURE],
+ { cwd: ROOT, env: { ...process.env, DRY_RUN: '1' }, encoding: 'utf8' });
+
+ const emailed = [...out.matchAll(/\[email:designer\]\s+(\S+)/g)].map((m) => m[1]);
+ const checks = [
+ ['pending applications are still emailed', emailed.includes('pending-a@example.com') && emailed.includes('pending-b@example.com')],
+ ['APPROVED application is NOT emailed', !emailed.some((e) => e.startsWith('approved-'))],
+ ['REJECTED application is NOT emailed', !emailed.some((e) => e.startsWith('rejected-'))],
+ ['already-emailed row still skipped (idempotent)', !emailed.includes('already-emailed@example.com')],
+ ['skipped decided rows are reported, not silent', /SKIPPING 3 already-decided/.test(out)],
+ ['exactly 2 designers emailed', emailed.length === 2],
+ ['the real applications store was never opened', !out.includes('data/trade-applications.jsonl')],
+ ];
+ const failed = checks.filter((c) => !c[1]);
+ results = { suite: 'send-only pending-only filter', fixture: FIXTURE, emailed,
+ results: checks.map(([name, ok]) => ({ name, verdict: ok ? 'PASS' : 'FAIL' })),
+ passed: checks.length - failed.length, failed: failed.length };
+ console.log(JSON.stringify(results, null, 2));
+ process.exitCode = failed.length ? 1 : 0;
+} finally {
+ // Always clean up the throwaway dir, even if the CLI threw.
+ try { fs.rmSync(path.dirname(FIXTURE), { recursive: true, force: true }); } catch { /* best effort */ }
+}
← b0c4280 TK-11377: --send-only must not email an already-decided appl
·
back to Dw Signup Fulfillment
·
auto-data-snapshot: 2026-09-10T09:22:46 (1 data files) — ver d0135eb →