← 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
M routes/logo-agent.jsA test/logo-agent.test.jsM test/payouts.test.js
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 →