[object Object]

← back to Wallco Ai

marketplace ai/generate-colorways: dedicated smoke test

a1c0a711771fedf5ba21bbd6d8074febef1d690c · 2026-05-13 14:53:38 -0700 · SteveStudio2

tests/marketplace/ai-colorways.test.js — 6 cases by default (validation,
mock shape, cost-tracker wrapper sanity, DB column existence) and a 7th
opt-in REAL Gemini round-trip when WALLCO_TEST_REAL=1. Verifies:
  - PNG written to public/marketplace/uploads/
  - row persisted with ai_generated=true
  - cost-tracker logGemini called (smoke-loadable)

Files touched

Diff

commit a1c0a711771fedf5ba21bbd6d8074febef1d690c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 14:53:38 2026 -0700

    marketplace ai/generate-colorways: dedicated smoke test
    
    tests/marketplace/ai-colorways.test.js — 6 cases by default (validation,
    mock shape, cost-tracker wrapper sanity, DB column existence) and a 7th
    opt-in REAL Gemini round-trip when WALLCO_TEST_REAL=1. Verifies:
      - PNG written to public/marketplace/uploads/
      - row persisted with ai_generated=true
      - cost-tracker logGemini called (smoke-loadable)
---
 tests/marketplace/ai-colorways.test.js | 148 +++++++++++++++++++++++++++++++++
 1 file changed, 148 insertions(+)

diff --git a/tests/marketplace/ai-colorways.test.js b/tests/marketplace/ai-colorways.test.js
new file mode 100644
index 0000000..86a7cec
--- /dev/null
+++ b/tests/marketplace/ai-colorways.test.js
@@ -0,0 +1,148 @@
+// Dedicated test suite for /api/marketplace/ai/generate-colorways.
+//
+//   node tests/marketplace/ai-colorways.test.js
+//
+// By default only the cheap paths run (validation + mock mode + cost-tracker
+// wrapper sanity). To exercise the REAL Gemini call (~30s, ~6× $0.001 = $0.006),
+// set WALLCO_TEST_REAL=1:
+//
+//   WALLCO_TEST_REAL=1 node tests/marketplace/ai-colorways.test.js
+//
+// The real-mode test cleans up after itself by deleting any ai_generated
+// colorways it created for the test pattern.
+
+'use strict';
+const assert = require('assert');
+const http = require('http');
+const fs = require('fs');
+const path = require('path');
+const os = require('os');
+
+const HOST = process.env.WALLCO_HOST || '127.0.0.1';
+const PORT = process.env.WALLCO_PORT || 9792;
+const RUN_REAL = process.env.WALLCO_TEST_REAL === '1';
+const TEST_SLUG = process.env.WALLCO_TEST_SLUG || 'wildflower-reverie-astrid-mauve';
+
+function req(method, urlPath, body) {
+  return new Promise((resolve, reject) => {
+    const opts = { hostname: HOST, port: PORT, path: urlPath, method, headers: { 'content-type': 'application/json' }, timeout: 180_000 };
+    const r = http.request(opts, (res) => {
+      let buf = '';
+      res.on('data', (c) => (buf += c));
+      res.on('end', () => resolve({ status: res.statusCode, body: buf, json: tryJson(buf) }));
+    });
+    r.on('error', reject);
+    r.on('timeout', () => { r.destroy(new Error('timeout')); });
+    if (body) r.write(JSON.stringify(body));
+    r.end();
+  });
+}
+function tryJson(s) { try { return JSON.parse(s); } catch (_) { return null; } }
+
+const tests = [];
+function t(name, fn) { tests.push({ name, fn }); }
+
+// ── Required-input validation ───────────────────────────────────────────────
+t('POST without patternId/patternSlug/imageUrl → 400', async () => {
+  const r = await req('POST', '/api/marketplace/ai/generate-colorways', {});
+  assert.strictEqual(r.status, 400);
+  assert.ok(/patternId|patternSlug|imageUrl/.test(r.json.error || ''));
+});
+
+// ── Mock mode (no Gemini call) ──────────────────────────────────────────────
+t('mock=true returns 6 colorways with stable shape', async () => {
+  const r = await req('POST', '/api/marketplace/ai/generate-colorways', {
+    patternSlug: TEST_SLUG, mock: true,
+  });
+  assert.strictEqual(r.status, 200);
+  assert.strictEqual(r.json.status, 'mock');
+  assert.ok(Array.isArray(r.json.colorways));
+  assert.strictEqual(r.json.colorways.length, 6);
+  for (const c of r.json.colorways) {
+    assert.ok(c.name && typeof c.name === 'string', 'colorway.name required');
+    assert.ok(Array.isArray(c.hexPalette) && c.hexPalette.length >= 3, 'hexPalette[]');
+    assert.ok(c.description && typeof c.description === 'string', 'description');
+    assert.strictEqual(c.ai_generated, false, 'mock should not flag ai_generated');
+  }
+});
+
+t('mock=true with unknown pattern slug still returns 6 (uses defaults)', async () => {
+  const r = await req('POST', '/api/marketplace/ai/generate-colorways', {
+    patternSlug: 'no-such-pattern-xyzzy', mock: true,
+  });
+  assert.strictEqual(r.status, 200);
+  assert.strictEqual(r.json.colorways.length, 6);
+});
+
+// ── cost-tracker wrapper sanity (offline) ───────────────────────────────────
+t('cost-tracker logGemini wrapper module loads', async () => {
+  const wrapperPath = path.join(os.homedir(), '.claude/skills/cost-tracker/scripts/log-gemini.js');
+  assert.ok(fs.existsSync(wrapperPath), 'log-gemini.js wrapper must exist at ' + wrapperPath);
+  const mod = require(wrapperPath);
+  assert.strictEqual(typeof mod.logGemini, 'function');
+  // Smoke: call it with a synthetic Gemini response — no-op if usageMetadata absent
+  mod.logGemini({}, { app: 'wallco-ai-test', note: 'unit-test no-op' });
+});
+
+t('mp_pattern_colorways table has ai_generated column', async () => {
+  // Indirect check via the /api/marketplace/patterns/:slug payload — colorway
+  // objects come back from the DB. We just want to make sure the API works.
+  const r = await req('GET', `/api/marketplace/patterns/${TEST_SLUG}`);
+  assert.strictEqual(r.status, 200);
+  assert.ok(r.json.pattern && r.json.pattern.id, 'pattern lookup failed');
+  assert.ok(Array.isArray(r.json.colorways), 'colorways[] expected');
+});
+
+// ── Real Gemini call — opt-in only (costs ~$0.006 + ~30s) ───────────────────
+if (RUN_REAL) {
+  t('REAL Gemini: generates ≥1 ai_generated colorway, writes PNG to uploads/, persists row', async () => {
+    // Snapshot existing colorway ids so we can identify newly-created rows.
+    const before = await req('GET', `/api/marketplace/patterns/${TEST_SLUG}`);
+    assert.strictEqual(before.status, 200);
+    const beforeIds = new Set((before.json.colorways || []).map(c => c.id));
+
+    const r = await req('POST', '/api/marketplace/ai/generate-colorways', {
+      patternSlug: TEST_SLUG,
+    });
+    assert.strictEqual(r.status, 200, `expected 200, got ${r.status}: ${r.body.slice(0, 200)}`);
+    assert.strictEqual(r.json.status, 'ok');
+    assert.ok(Array.isArray(r.json.colorways) && r.json.colorways.length >= 1,
+      `expected ≥1 colorway, got ${r.json.colorways?.length || 0}`);
+
+    const uploadsDir = path.join(__dirname, '..', '..', 'public', 'marketplace', 'uploads');
+    for (const c of r.json.colorways) {
+      assert.strictEqual(c.ai_generated, true, 'colorway should be flagged ai_generated');
+      assert.ok(c.image_url && c.image_url.startsWith('/marketplace/uploads/'),
+        'image_url must point under /marketplace/uploads/');
+      const filename = c.image_url.replace('/marketplace/uploads/', '');
+      const absPath = path.join(uploadsDir, filename);
+      assert.ok(fs.existsSync(absPath), `PNG missing on disk: ${absPath}`);
+      const stat = fs.statSync(absPath);
+      assert.ok(stat.size > 10_000, `PNG suspiciously small (${stat.size}b): ${absPath}`);
+      assert.ok(Array.isArray(c.hex_palette) && c.hex_palette.length >= 1, 'hex_palette[] required');
+    }
+
+    // Verify the rows landed in the DB
+    const after = await req('GET', `/api/marketplace/patterns/${TEST_SLUG}`);
+    assert.strictEqual(after.status, 200);
+    const newRows = (after.json.colorways || []).filter(c => !beforeIds.has(c.id));
+    assert.ok(newRows.length >= 1, 'no new colorway rows persisted to mp_pattern_colorways');
+  });
+}
+
+(async () => {
+  console.log(`Running ${tests.length} test(s) against ${HOST}:${PORT}${RUN_REAL ? ' (incl. REAL Gemini)' : ' (mock only — set WALLCO_TEST_REAL=1 to exercise Gemini)'}\n`);
+  let pass = 0, fail = 0;
+  for (const test of tests) {
+    try {
+      await test.fn();
+      console.log('✓', test.name);
+      pass++;
+    } catch (err) {
+      console.error('✗', test.name, '—', err.message);
+      fail++;
+    }
+  }
+  console.log(`\n${pass}/${tests.length} ai-colorways tests passed`);
+  if (fail) process.exit(1);
+})();

← faa24dd marketplace YOLO loop · overnight milestone 2 — parallel-tab  ·  back to Wallco Ai  ·  footer: add visible phone, DW corp address, and © 2026 line 2ac2375 →