← back to Costa Rica

test/logo-agent.test.js

220 lines

'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');
});