[object Object]

← back to CelebritySignatures

Verify recolored wear artwork through both fulfillment draft paths

c90a83ceb08ba7278f4d132f352c540c6ae195be · 2026-09-09 07:58:22 -0700 · Steve Abrams

Files touched

Diff

commit c90a83ceb08ba7278f4d132f352c540c6ae195be
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 9 07:58:22 2026 -0700

    Verify recolored wear artwork through both fulfillment draft paths
---
 test/wear-artwork-flow.test.mjs | 141 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 141 insertions(+)

diff --git a/test/wear-artwork-flow.test.mjs b/test/wear-artwork-flow.test.mjs
new file mode 100644
index 0000000..343c220
--- /dev/null
+++ b/test/wear-artwork-flow.test.mjs
@@ -0,0 +1,141 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { mkdtemp, mkdir, readFile, writeFile, copyFile, rm } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawn, execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+import { createServer } from 'node:net';
+import { once } from 'node:events';
+import { buildPayload } from '../scripts/submit-pod-draft-printify.mjs';
+
+// Run real draft producers against synthetic payment/artwork responses in an
+// isolated project. No project .env, real orders, or external APIs are used.
+const ROOT = fileURLToPath(new URL('..', import.meta.url));
+const exec = promisify(execFile);
+const artwork = 'https://fixture.invalid/signature.svg';
+const recipient = { name: 'Fixture Buyer', address: { line1: '1 Fixture Lane',
+  city: 'Fixture', state: 'CA', country: 'US', postal_code: '90001' } };
+const fixtureOrder = id => ({ id, status: 'pending_payment', mode: 'test', provider: 'printify',
+  email: 'fixture@example.invalid', stripeSession: `cs_test_${id}`, pod_submitted: false,
+  items: [{ qid: 'QFixture', signature_name: 'Fixture', garment: 'polo', garment_label: 'Polo',
+    color: 'black', size: 'M', placement: 'left_chest' }] });
+const env = { PATH: process.env.PATH, STRIPE_TEST_SECRET_KEY: 'sk_test_fixture',
+  WEAR_SALES_LIVE: '0', STRIPE_LIVE_ENABLED: '0', WEAR_STRIPE_LIVE_ENABLED: '0' };
+
+async function freePort() {
+  const socket = createServer();
+  socket.listen(0, '127.0.0.1');
+  await once(socket, 'listening');
+  const port = socket.address().port;
+  await new Promise(resolve => socket.close(resolve));
+  return port;
+}
+
+test('black-polo draft artwork survives both producers, cache hits, serving, payload, and retries', { timeout: 60000 }, async t => {
+  const root = await mkdtemp(join(tmpdir(), 'tk10286-artwork-'));
+  t.after(() => rm(root, { recursive: true, force: true }));
+  const template = JSON.parse(await readFile(join(ROOT, 'data/wear-templates.json'), 'utf8'));
+  for (const producer of ['success', 'reconciler']) {
+    await t.test(producer, async () => {
+      const work = join(root, producer);
+      await mkdir(join(work, 'data'), { recursive: true });
+      await mkdir(join(work, 'scripts'));
+      await copyFile(join(ROOT, 'server.js'), join(work, 'server.mjs'));
+      await copyFile(join(ROOT, 'scripts/reconcile-wear-orders.mjs'), join(work, 'scripts/reconcile-wear-orders.mjs'));
+      const put = (name, data) => writeFile(join(work, 'data', name), JSON.stringify(data));
+      await put('wear-templates.json', template);
+      await put('wear-orders.json', [fixtureOrder(91001), fixtureOrder(91002)]);
+      await put('celebrity_signatures.json', [{ wikidata: 'https://www.wikidata.org/wiki/QFixture',
+        full_name: 'Fixture', category: 'Politics', signature_image_url: artwork,
+        usable_in_commercial_collage: 'yes', risk_level: 'low', death_date: '1800-01-01' }]);
+      const interceptor = join(work, 'intercept.mjs');
+      await writeFile(interceptor, `
+import { appendFileSync } from 'node:fs';
+globalThis.fetch = async (input, options = {}) => {
+  const url = String(input), method = options.method || 'GET';
+  if (method !== 'GET') throw new Error('External mutation blocked: ' + method);
+  appendFileSync(${JSON.stringify(join(work, 'requests.jsonl'))}, JSON.stringify({url,method})+'\\n');
+  if (url === ${JSON.stringify(artwork)}) return new Response('<svg xmlns="http://www.w3.org/2000/svg" width="20" height="10"><rect x="5" y="3" width="10" height="4" fill="black"/></svg>');
+  const m = /^https:\\/\\/api\\.stripe\\.com\\/v1\\/checkout\\/sessions\\/cs_test_(91001|91002)$/.exec(url);
+  if (m) return Response.json({ id: 'cs_test_'+m[1], status:'complete', payment_status:'paid', metadata:{order_id:m[1]}, shipping_details:${JSON.stringify(recipient)} });
+  throw new Error('External network blocked: ' + url);
+};
+`);
+      const reconcile = () => exec(process.execPath, ['--import', interceptor,
+        join(work, 'scripts/reconcile-wear-orders.mjs'), '--apply'], { env, timeout: 15000 });
+      if (producer === 'reconciler') await reconcile();
+      const port = await freePort();
+      const child = spawn(process.execPath, ['--import', interceptor, join(work, 'server.mjs')],
+        { env: { ...env, PORT: String(port) }, stdio: ['ignore', 'pipe', 'pipe'] });
+      let stderr = '';
+      child.stderr.on('data', c => { stderr += c; });
+      try {
+        await new Promise((resolve, reject) => {
+          const timer = setTimeout(() => reject(new Error('server startup timeout: '+stderr)), 10000);
+          child.stdout.on('data', c => { if (String(c).includes('CelebritySignatures →')) { clearTimeout(timer); resolve(); } });
+          child.once('error', e => { clearTimeout(timer); reject(e); });
+          child.once('exit', code => { clearTimeout(timer); reject(new Error('server exited '+code+': '+stderr)); });
+        });
+        const base = `http://127.0.0.1:${port}`;
+        if (producer === 'success') {
+          for (const id of [91001, 91002]) {
+            const response = await fetch(`${base}/wear-success?sid=cs_test_${id}`);
+            assert.equal(response.status, 200);
+            assert.match(await response.text(), /Order received/);
+          }
+        }
+        const draftPath = join(work, 'data/pod-order-drafts.jsonl');
+        const before = await readFile(draftPath, 'utf8');
+        const drafts = before.trim().split('\n').map(JSON.parse);
+        assert.deepEqual(drafts.map(d => d.orderId), ['91001.0', '91002.0']);
+        assert.equal(drafts[0].design_image_url, drafts[1].design_image_url, 'cache hit uses same artifact');
+        for (const draft of drafts) {
+          assert.equal(draft.provider, 'printify');
+          const image = new URL(draft.design_image_url);
+          assert.equal(image.origin, 'https://celebsignatures.com');
+          assert.match(image.pathname, /^\/assets\/recolored-signatures\/[a-f0-9]{24}\.png$/);
+          const payload = buildPayload(template, draft);
+          assert.equal(payload.external_id, `celebsig-wear-${draft.orderId}`);
+          assert.equal(payload.line_items[0].variant_id, template.garments.find(g => g.id === 'polo').printify.variants['black/M']);
+          assert.equal(payload.line_items[0].print_areas.left_chest[0].src, image.href);
+          const served = await fetch(base + image.pathname);
+          assert.equal(served.status, 200);
+          assert.match(served.headers.get('content-type'), /image\/png/);
+          const png = Buffer.from(await served.arrayBuffer());
+          assert.deepEqual(png, await readFile(join(work, 'public', image.pathname)));
+          assert.equal(png.subarray(1, 4).toString(), 'PNG');
+        }
+        const pixels = await exec('convert', [join(work, 'public', new URL(drafts[0].design_image_url).pathname), 'txt:-']);
+        assert.match(pixels.stdout, /10,5:.*#F5F3EEFF/i, 'opaque signature pixel has light ink');
+        assert.match(pixels.stdout, /0,0:.*#F5F3EE00/i, 'transparent background preserved');
+        if (producer === 'success') {
+          for (const id of [91001, 91002]) await fetch(`${base}/wear-success?sid=cs_test_${id}`);
+        } else await reconcile();
+        assert.equal(await readFile(draftPath, 'utf8'), before, 'retry must not append duplicate drafts');
+        const orders = JSON.parse(await readFile(join(work, 'data/wear-orders.json'), 'utf8'));
+        assert.ok(orders.every(o => o.status === 'paid' && o.pod_submitted === true));
+        const ordersBefore = await readFile(join(work, 'data/wear-orders.json'), 'utf8');
+        const blocked = await fetch(`${base}/api/wear-checkout`, { method: 'POST',
+          headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({
+            email: 'fixture@example.invalid', items: [{ qid: 'QFixture', garment: 'polo', color: 'black', size: 'M' }] }) });
+        assert.equal((await blocked.json()).comingSoon, true, 'disabled sales stop before payment');
+        const unpaid = await fetch(`${base}/wear-success?sid=invalid`);
+        assert.match(await unpaid.text(), /Order not completed/);
+        assert.equal(await readFile(join(work, 'data/wear-orders.json'), 'utf8'), ordersBefore);
+        assert.equal(await readFile(draftPath, 'utf8'), before, 'negative paths do not change the queue');
+        const requests = (await readFile(join(work, 'requests.jsonl'), 'utf8')).trim().split('\n').map(JSON.parse);
+        assert.equal(requests.filter(r => r.url === artwork).length, 1, 'second order uses cached artwork');
+        assert.ok(requests.every(r => r.method === 'GET'));
+        assert.doesNotMatch(stderr, /recolorSignatureToLight failed/);
+      } finally {
+        if (child.exitCode === null && child.signalCode === null) {
+          const exited = once(child, 'exit');
+          child.kill('SIGTERM');
+          await exited;
+        }
+      }
+    });
+  }
+});

← c9d72f3 wear: fix recolored signature URL being relative, not absolu  ·  back to CelebritySignatures  ·  wear: persist sort selection to localStorage, matching densi 2dd7c66 →