← back to CelebritySignatures
Verify recolored wear artwork through both fulfillment draft paths
c90a83ceb08ba7278f4d132f352c540c6ae195be · 2026-09-09 07:58:22 -0700 · Steve Abrams
Files touched
A test/wear-artwork-flow.test.mjs
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 →