[object Object]

← 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

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 →