[object Object]

← back to Costa Rica

costa-rica: logo-agent test coverage (15 tests) + saveSession self-heal + fix pre-existing payouts fixture — yoloforever cycle 2

a04f2e1d9f41e5e6c1dcde771b498d20462ced3d · 2026-08-07 16:32:16 -0700 · Steve

- test/logo-agent.test.js: crossover genetics (incl. Cody-C2 boundary-input clamp tests),
  motif-distinctness, full HTTP flow with genetic-inheritance assertion, finalize+error paths;
  ephemeral server, zero artifact leak.
- routes/logo-agent.js: expose _internals for tests (non-breaking; router is a fn) +
  self-healing saveSession (mkdir guard so a removed SESS_DIR can't ENOENT-500 the server).
- test/payouts.test.js: fix pre-existing red fixture (3 bookings CHECKs: has-a-date, stay-order,
  total_reconciles) -> suite 101/101 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit a04f2e1d9f41e5e6c1dcde771b498d20462ced3d
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Aug 7 16:32:16 2026 -0700

    costa-rica: logo-agent test coverage (15 tests) + saveSession self-heal + fix pre-existing payouts fixture — yoloforever cycle 2
    
    - test/logo-agent.test.js: crossover genetics (incl. Cody-C2 boundary-input clamp tests),
      motif-distinctness, full HTTP flow with genetic-inheritance assertion, finalize+error paths;
      ephemeral server, zero artifact leak.
    - routes/logo-agent.js: expose _internals for tests (non-breaking; router is a fn) +
      self-healing saveSession (mkdir guard so a removed SESS_DIR can't ENOENT-500 the server).
    - test/payouts.test.js: fix pre-existing red fixture (3 bookings CHECKs: has-a-date, stay-order,
      total_reconciles) -> suite 101/101 green.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 routes/logo-agent.js    |   7 +-
 test/logo-agent.test.js | 219 ++++++++++++++++++++++++++++++++++++++++++++++++
 test/payouts.test.js    |   6 +-
 3 files changed, 229 insertions(+), 3 deletions(-)

diff --git a/routes/logo-agent.js b/routes/logo-agent.js
index acf8cd7..6daa631 100644
--- a/routes/logo-agent.js
+++ b/routes/logo-agent.js
@@ -134,7 +134,12 @@ function loadSession(sid) {
   if (!fs.existsSync(f)) return null;
   return JSON.parse(fs.readFileSync(f, 'utf8'));
 }
-function saveSession(s) { fs.writeFileSync(path.join(SESS_DIR, `${s.id}.json`), JSON.stringify(s, null, 2)); }
+function saveSession(s) {
+  // Self-heal: re-create SESS_DIR if it was removed (dir is made once at require-time,
+  // so a cleanup/rmdir between boot and now would otherwise ENOENT-500 the next write).
+  fs.mkdirSync(SESS_DIR, { recursive: true });
+  fs.writeFileSync(path.join(SESS_DIR, `${s.id}.json`), JSON.stringify(s, null, 2));
+}
 
 function newRound(component, base) {
   // 3 fresh variants; if we have a locked base genome, keep the other components fixed.
diff --git a/test/logo-agent.test.js b/test/logo-agent.test.js
new file mode 100644
index 0000000..7bda02f
--- /dev/null
+++ b/test/logo-agent.test.js
@@ -0,0 +1,219 @@
+'use strict';
+// Coverage for the logo-agent tournament route (shipped to prod cycle: yoloforever C2).
+// Two layers: (1) pure genetics/SVG invariants via router._internals, (2) full HTTP
+// flow (session -> rank -> lock x6 -> finalize) over an ephemeral express server.
+// Cleans up every file it writes (sessions + finalize artifacts) — zero leak.
+
+const test = require('node:test');
+const assert = require('node:assert');
+const express = require('express');
+const fs = require('fs');
+const path = require('path');
+
+const router = require('../routes/logo-agent');
+const I = router._internals;
+const ROOT = path.join(__dirname, '..');
+const SVG_OUT = path.join(ROOT, 'public', 'img', 'cr-logo.svg');
+const FINAL = path.join(ROOT, 'data', 'logo-agent-final.json');
+const SESS_DIR = path.join(ROOT, 'data', 'logo-agent-sessions');
+
+// ---- ephemeral server helper ----
+let server, base;
+test.before(async () => {
+  const app = express();
+  app.use(express.json());
+  app.use('/api/logo-agent', router);
+  await new Promise((r) => { server = app.listen(0, r); });
+  base = `http://127.0.0.1:${server.address().port}/api/logo-agent`;
+});
+test.after(() => {
+  if (server) server.close();
+  // clean any artifacts this test created
+  try { fs.rmSync(SVG_OUT, { force: true }); } catch {}
+  try { fs.rmSync(FINAL, { force: true }); } catch {}
+  try { fs.rmSync(SESS_DIR, { recursive: true, force: true }); } catch {}
+  // remove empty public/img if we created it
+  try { fs.rmdirSync(path.join(ROOT, 'public', 'img')); } catch {}
+});
+const P = (u, b) => fetch(base + u, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: b ? JSON.stringify(b) : undefined }).then((r) => r.json());
+const G = (u) => fetch(base + u).then((r) => r.json());
+
+// ---- 1. genetics invariants (deterministic assertions on a random op) ----
+test('crossover glyph: motif+container inherited from a parent, weight/dominance within blended bounds', () => {
+  const a = { motif: 'volcano', container: 'circle', weight: 4, dominance: 40 };
+  const b = { motif: 'toucan', container: 'shield', weight: 12, dominance: 90 };
+  for (let i = 0; i < 200; i++) {
+    const c = I.crossover('glyph', a, b);
+    assert.ok([a.motif, b.motif].includes(c.motif), 'motif from a parent');
+    assert.ok([a.container, b.container].includes(c.container), 'container from a parent');
+    assert.ok(c.weight >= 3 && c.weight <= 14, `weight clamped 3..14 got ${c.weight}`);
+    assert.ok(c.dominance >= 30 && c.dominance <= 95, `dominance clamped 30..95 got ${c.dominance}`);
+  }
+});
+
+test('crossover openspace: density stays within clamp bounds', () => {
+  for (let i = 0; i < 100; i++) {
+    const c = I.crossover('openspace', { density: 20 }, { density: 100 });
+    assert.ok(c.density >= 10 && c.density <= 100, `density in 10..100 got ${c.density}`);
+  }
+});
+
+// HOLE 1 (Cody C2): drive the clamp with BOUNDARY inputs so a clamp-constant
+// regression is actually caught — mid-range inputs never approach the clamp.
+test('crossover clamps at the low/high edges (boundary inputs, not mid-range)', () => {
+  for (let i = 0; i < 300; i++) {
+    // low edge: both parents at/below the floor -> jitter must not escape below 3 / 30 / 10
+    const lo = I.crossover('glyph', { motif: 'sun', container: 'none', weight: 3, dominance: 30 },
+                                    { motif: 'sun', container: 'none', weight: 3, dominance: 30 });
+    assert.ok(lo.weight >= 3, `weight floor held, got ${lo.weight}`);
+    assert.ok(lo.dominance >= 30, `dominance floor held, got ${lo.dominance}`);
+    // high edge: both parents at the ceiling -> must not escape above 14 / 95
+    const hi = I.crossover('glyph', { motif: 'sun', container: 'none', weight: 14, dominance: 95 },
+                                    { motif: 'sun', container: 'none', weight: 14, dominance: 95 });
+    assert.ok(hi.weight <= 14, `weight ceiling held, got ${hi.weight}`);
+    assert.ok(hi.dominance <= 95, `dominance ceiling held, got ${hi.dominance}`);
+    // openspace edges
+    assert.ok(I.crossover('openspace', { density: 10 }, { density: 10 }).density >= 10, 'density floor');
+    assert.ok(I.crossover('openspace', { density: 100 }, { density: 100 }).density <= 100, 'density ceiling');
+  }
+});
+
+test('crossover categorical (palette/type/tagline/layout): child value comes from one parent', () => {
+  const pairs = [
+    ['palette', { palette: 'pacific' }, { palette: 'sunset' }],
+    ['typography', { type: 0 }, { type: 3 }],
+    ['tagline', { tagline: 1 }, { tagline: 5 }],
+    ['layout', { layout: 'icon-left' }, { layout: 'icon-only' }],
+  ];
+  for (const [comp, a, b] of pairs) {
+    const key = Object.keys(a)[0];
+    for (let i = 0; i < 50; i++) {
+      const c = I.crossover(comp, a, b);
+      assert.ok([a[key], b[key]].includes(c[key]), `${comp} child ${key} from a parent`);
+    }
+  }
+});
+
+// ---- 2. glyph SVG generation ----
+test('every motif produces a non-empty SVG with a stroke/shape', () => {
+  for (const motif of I.MOTIFS) {
+    const svg = I.buildSvg({ palette: 'rainforest', glyph: { motif, container: 'none', weight: 6 } });
+    assert.match(svg, /<svg[\s\S]*<\/svg>/, `${motif} wrapped in <svg>`);
+    assert.match(svg, /stroke|path|circle|line|ellipse/, `${motif} draws a shape`);
+  }
+});
+
+test('motifs render DISTINCT paths (not just any shape) — catches a motif path swap', () => {
+  // Each motif carries a characteristic signature; assert the RIGHT one renders.
+  const sig = {
+    volcano: 'M14 46 L32 16 L50 46', wave: 'M10 34 Q20 22', monstera: 'M32 12 C14 18',
+    toucan: 'Q52 20 50 30', sun: '<line', coffee: '<ellipse',
+  };
+  const seen = new Set();
+  for (const motif of I.MOTIFS) {
+    const svg = I.buildSvg({ glyph: { motif, container: 'none', weight: 6 } });
+    assert.ok(svg.includes(sig[motif]), `${motif} renders its own signature (${sig[motif]})`);
+    seen.add(I.motifPath(motif, 6));
+  }
+  assert.equal(seen.size, I.MOTIFS.length, 'all motif paths are unique');
+});
+
+test('containers wrap the motif (circle/shield add an outer shape)', () => {
+  const circle = I.buildSvg({ glyph: { motif: 'sun', container: 'circle', weight: 6 } });
+  const shield = I.buildSvg({ glyph: { motif: 'sun', container: 'shield', weight: 6 } });
+  assert.match(circle, /<circle cx="32" cy="32" r="30"/, 'circle container ring');
+  assert.match(shield, /<path d="M32 4/, 'shield container path');
+});
+
+test('assemble() returns a full brand object (svg+palette+type+tagline+layout+density)', () => {
+  const brand = I.assemble({ palette: 'pacific', type: 1, tagline: 2, layout: 'icon-top', density: 60, glyph: { motif: 'wave', container: 'none', weight: 5 } });
+  assert.ok(brand.svg.includes('<svg'));
+  assert.equal(brand.palette.accent, I.PALETTES.pacific.accent);
+  assert.equal(brand.type, I.TYPE[1]);
+  assert.equal(brand.tagline, I.TAGLINES[2]);
+  assert.equal(brand.layout, 'icon-top');
+  assert.equal(brand.density, 60);
+});
+
+// ---- 3. full HTTP tournament flow ----
+test('session/start returns 3 glyph variants with valid svg', async () => {
+  const s = await P('/session/start');
+  assert.ok(s.session_id, 'has session_id');
+  assert.equal(s.component, 'glyph');
+  assert.equal(s.variants.length, 3);
+  assert.match(s.variants[0].svg, /<svg/);
+});
+
+test('rank breeds a new 3rd variant, keeps the top-2, bumps xp/streak', async () => {
+  const s = await P('/session/start');
+  const r = await P('/rank/' + s.session_id, { ranks: [s.variants[0].vid, s.variants[1].vid] });
+  assert.equal(r.variants.length, 3, 'still 3 variants');
+  assert.equal(r.variants[0].vid, s.variants[0].vid, 'champion retained');
+  assert.equal(r.variants[1].vid, s.variants[1].vid, 'runner-up retained');
+  assert.ok(!s.variants.map((v) => v.vid).includes(r.variants[2].vid), '3rd is a fresh bred variant');
+  assert.equal(r.xp, 10);
+  assert.equal(r.round, 2);
+  // HOLE 3 (Cody C2): the bred child must actually INHERIT genes from the two ranked
+  // parents over the HTTP path, not merely have a new vid. glyph is component 1, so the
+  // variants' genomes are exposed via assemble().genome.
+  const p1 = s.variants[0].genome, p2 = s.variants[1].genome, child = r.variants[2].genome;
+  assert.ok([p1.motif, p2.motif].includes(child.motif), 'child motif inherited from a ranked parent');
+  assert.ok([p1.container, p2.container].includes(child.container), 'child container inherited from a ranked parent');
+  const wLo = Math.min(p1.weight, p2.weight) - 1, wHi = Math.max(p1.weight, p2.weight) + 1;
+  assert.ok(child.weight >= Math.max(3, wLo) && child.weight <= Math.min(14, wHi),
+    `child weight blended from parents (${p1.weight},${p2.weight}) -> ${child.weight}`);
+});
+
+test('locking all 6 components finishes the tournament and awards tastemaker', async () => {
+  const s = await P('/session/start');
+  let cur = s;
+  const walked = [s.component];
+  for (let i = 0; i < 6; i++) {
+    const r = await P('/lock/' + s.session_id, { winner_id: cur.variants[0].vid });
+    if (r.done) {
+      assert.ok(r.badges.includes('tastemaker'), 'tastemaker on full completion');
+      assert.ok(r.brand && r.brand.svg.includes('<svg'), 'returns assembled brand');
+      return;
+    }
+    walked.push(r.component);
+    cur = r;
+  }
+  assert.fail('tournament did not complete after 6 locks; walked=' + walked.join(','));
+});
+
+test('lock rejects a winner_id not in the current variants', async () => {
+  const s = await P('/session/start');
+  const r = await P('/lock/' + s.session_id, { winner_id: 'deadbeef' });
+  assert.equal(r.error, 'winner_id not in current variants');
+});
+
+test('rank rejects fewer than 2 ranked vids', async () => {
+  const s = await P('/session/start');
+  const r = await P('/rank/' + s.session_id, { ranks: [s.variants[0].vid] });
+  assert.equal(r.error, 'need at least 2 ranked vids');
+});
+
+// ---- 4. finalize writes the SVG + brand spec + CSS vars ----
+test('finalize writes cr-logo.svg + logo-agent-final.json with CSS vars', async () => {
+  const s = await P('/session/start');
+  let cur = s;
+  for (let i = 0; i < 6; i++) {
+    const r = await P('/lock/' + s.session_id, { winner_id: cur.variants[0].vid });
+    if (r.done) break;
+    cur = r;
+  }
+  const f = await P('/finalize/' + s.session_id);
+  assert.equal(f.ok, true);
+  assert.equal(f.svgPath, '/img/cr-logo.svg');
+  assert.match(f.cssVars, /--cr-accent:/);
+  assert.match(f.cssVars, /--cr-font-display:/);
+  assert.ok(fs.existsSync(SVG_OUT), 'cr-logo.svg written to disk');
+  assert.ok(fs.existsSync(FINAL), 'logo-agent-final.json written to disk');
+  const svg = fs.readFileSync(SVG_OUT, 'utf8');
+  assert.match(svg, /<svg[\s\S]*<\/svg>/, 'written svg is well-formed');
+});
+
+test('state/:sid returns 404 for an unknown session', async () => {
+  const r = await G('/state/nope-not-real');
+  assert.equal(r.error, 'session not found');
+});
diff --git a/test/payouts.test.js b/test/payouts.test.js
index 88c1cc0..9c39ced 100644
--- a/test/payouts.test.js
+++ b/test/payouts.test.js
@@ -39,8 +39,10 @@ async function mkPayoutMethod(hostId, kind, extra = {}) {
 }
 async function mkBooking(hostId, travelerId, { status = 'completed', hostPayout = 36000 } = {}) {
   const { rows: [b] } = await pool.query(
-    `INSERT INTO bookings (code, place_id, host_id, traveler_id, currency, subtotal, total, host_payout, status)
-     VALUES ($1, 1, $2, $3, 'CRC', 36000, 40000, $4, $5) RETURNING id`,
+    // Satisfy the bookings CHECKs: has-a-date (check_in), stay-order (check_out>check_in),
+    // and total_reconciles (total = platform_fee + host_payout, so platform_fee = 40000 - host_payout).
+    `INSERT INTO bookings (code, place_id, host_id, traveler_id, currency, subtotal, total, platform_fee, host_payout, status, check_in, check_out)
+     VALUES ($1, 1, $2, $3, 'CRC', 36000, 40000, 40000 - $4, $4, $5, CURRENT_DATE + 7, CURRENT_DATE + 9) RETURNING id`,
     [`${SENT}-${Math.random().toString(36).slice(2, 8)}`, hostId, travelerId, hostPayout, status]);
   return b.id;
 }

← 8e87509 costa-rica: SAFE/LOCAL security + robustness fixes (C1,C2,R1  ·  back to Costa Rica  ·  costa-rica: tests for the security/robustness fixes (P1,P2,P 6faf459 →