← back to Dw Signup Fulfillment

verification/tk11285/sendonly-filter-test.js

63 lines

'use strict';
// --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.
//
// 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 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');

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,
});

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');

  // 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 */ }
}