[object Object]

← back to Rentv

test: add route+API health smoke-test (regression guard for TK-10356 select-to-live + core routes)

775df4d78bc4309617516f2b58a048f49380d16f · 2026-08-08 07:49:45 -0700 · Steve Abrams

/yoloforever cycle 1 (DTD verdict A, unanimous 5/5). Self-contained node:test suite
that spawns its own server on an ephemeral port (OPEN=1 bypass) and asserts core public
routes, /news/:id permalinks, public + admin APIs, select-to-live active-template shape,
and that all 10 admin dashboards + 10 frontpage version templates stay wired to render.js.
Wired into npm test via --test-concurrency=1 (serializes files: kills the server-spawn vs
PR-DB-integration-test contention that caused a flaky timeout, and reports one exit code so
the guard can't be silently skipped). Cody gate cleared (FIX FIRST -> all 3 fixes applied:
before() kills child on boot-timeout, honest schema-not-liveness test name, single-invocation
no-short-circuit runner). Verified 4x deterministic: 79 pass / 0 fail.

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

Files touched

Diff

commit 775df4d78bc4309617516f2b58a048f49380d16f
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 8 07:49:45 2026 -0700

    test: add route+API health smoke-test (regression guard for TK-10356 select-to-live + core routes)
    
    /yoloforever cycle 1 (DTD verdict A, unanimous 5/5). Self-contained node:test suite
    that spawns its own server on an ephemeral port (OPEN=1 bypass) and asserts core public
    routes, /news/:id permalinks, public + admin APIs, select-to-live active-template shape,
    and that all 10 admin dashboards + 10 frontpage version templates stay wired to render.js.
    Wired into npm test via --test-concurrency=1 (serializes files: kills the server-spawn vs
    PR-DB-integration-test contention that caused a flaky timeout, and reports one exit code so
    the guard can't be silently skipped). Cody gate cleared (FIX FIRST -> all 3 fixes applied:
    before() kills child on boot-timeout, honest schema-not-liveness test name, single-invocation
    no-short-circuit runner). Verified 4x deterministic: 79 pass / 0 fail.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 package.json               |   2 +-
 test/smoke/routes.test.mjs | 155 +++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 156 insertions(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 94ddb4b3..30a707d4 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
     "pr:migrate": "node -e \"require('./src/pr/db').runMigrations({log:console.log}).then(r=>console.log(JSON.stringify(r))).catch(e=>{console.error(e.message);process.exit(1)})\"",
     "pr:worker": "node src/pr/worker.js",
     "pr:seed:ca": "node src/pr/seed/ca-seed.js",
-    "test": "node --test test/pr/*.test.js test/deals/*.test.mjs && node --test test/smoke/*.test.mjs",
+    "test": "node --test --test-concurrency=1 test/pr/*.test.js test/deals/*.test.mjs test/smoke/*.test.mjs",
     "pr:seed:az": "node src/pr/seed/az-seed.js",
     "pr:seed:national": "node src/pr/seed/national-media-seed.js",
     "pr:crawl:media": "node src/pr/tools/daily-media-crawl.js"
diff --git a/test/smoke/routes.test.mjs b/test/smoke/routes.test.mjs
new file mode 100644
index 00000000..f16bd343
--- /dev/null
+++ b/test/smoke/routes.test.mjs
@@ -0,0 +1,155 @@
+// Route + API health smoke-test for the unified RENTV app (server.js).
+// Regression guard authored by the /yoloforever loop (DTD cycle-1 verdict A, unanimous 5/5):
+// locks in TK-10356 (10 admin dashboards + 10 frontpage version templates + select-to-live)
+// plus the core public routes / permalinks / public APIs so a future edit can't silently
+// break what shipped. Spawns its OWN server on an ephemeral port with OPEN=1 (admin bypass)
+// so it never touches the live pm2 :9704 instance and needs no creds.
+// Run: `node --test test/smoke/routes.test.mjs`  (or `npm test`).
+import { test, before, after } from 'node:test';
+import assert from 'node:assert/strict';
+import { spawn } from 'node:child_process';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
+const PORT = 9704 + 1000 + Math.floor(Math.random() * 500); // ephemeral, away from live :9704
+const BASE = `http://127.0.0.1:${PORT}`;
+let child;
+
+// The 10 select-to-live admin dashboards + 10 frontpage version templates (must stay wired).
+const ADMIN_LAYOUTS = ['classic', 'sidebar', 'console', 'cards', 'table', 'analytics', 'split', 'kanban', 'minimal', 'command'];
+const FRONTPAGE_VERSIONS = ['broadsheet', 'dashboard', 'digest', 'feed', 'grid', 'magazine', 'minimal', 'split', 'terminal', 'wire'];
+
+async function get(path, opts = {}) {
+  const r = await fetch(BASE + path, opts);
+  const ct = r.headers.get('content-type') || '';
+  const body = ct.includes('application/json') ? await r.json() : await r.text();
+  return { status: r.status, body, ct };
+}
+
+before(async () => {
+  child = spawn('node', ['server.js'], {
+    cwd: ROOT,
+    env: { ...process.env, PORT: String(PORT), OPEN: '1' }, // OPEN=1 → role:admin bypass (test-only)
+    stdio: ['ignore', 'ignore', 'ignore'],
+  });
+  // Poll /api/health until the server is listening (max ~20s — generous for a loaded box).
+  // If we give up (or fetch throws for any other reason), kill the child BEFORE rethrowing so a
+  // slow boot can't orphan a `node server.js` on the ephemeral port (node:test does not guarantee
+  // after() runs when before() rejects).
+  try {
+    const deadline = Date.now() + 20_000;
+    for (;;) {
+      try {
+        const r = await fetch(BASE + '/api/health');
+        if (r.ok) break;
+      } catch { /* not up yet */ }
+      if (Date.now() > deadline) throw new Error('server did not become healthy in 20s');
+      await new Promise(res => setTimeout(res, 200));
+    }
+  } catch (e) {
+    if (child) child.kill('SIGKILL');
+    throw e;
+  }
+});
+
+after(() => { if (child) child.kill('SIGKILL'); });
+
+// ── Core public routes ─────────────────────────────────────────────────────────
+test('public routes return 200', async () => {
+  for (const p of ['/', '/markets', '/map', '/deals', '/blog', '/search', '/about', '/advertise', '/subscribe']) {
+    const { status } = await get(p);
+    assert.equal(status, 200, `${p} should be 200, got ${status}`);
+  }
+});
+
+// ── Article permalink /news/:id (linked by 3+ templates) ────────────────────────
+test('article permalink /news/:id resolves for a real article', async () => {
+  const { body } = await get('/api/news');
+  assert.ok(Array.isArray(body.items) && body.items.length > 0, '/api/news should return items');
+  const id = body.items[0].id;
+  const { status } = await get('/news/' + encodeURIComponent(id));
+  assert.equal(status, 200, `/news/${id} should be 200`);
+});
+
+// ── Public APIs return the expected shape ───────────────────────────────────────
+test('public APIs return 200 + expected shape', async () => {
+  const health = await get('/api/health');
+  assert.equal(health.status, 200); assert.equal(health.body.ok, true);
+
+  const news = await get('/api/news');
+  assert.equal(news.status, 200);
+  assert.ok(typeof news.body.count === 'number', '/api/news should carry a numeric count');
+
+  const ticker = await get('/api/ticker');
+  assert.equal(ticker.status, 200);
+
+  const posts = await get('/api/posts');
+  assert.equal(posts.status, 200);
+
+  const markets = await get('/api/markets');
+  assert.equal(markets.status, 200);
+
+  const mapArt = await get('/api/map-articles');
+  assert.equal(mapArt.status, 200);
+});
+
+// ── Admin surfaces (served because OPEN=1 → role:admin) ──────────────────────────
+test('admin shells return 200 under admin role', async () => {
+  for (const p of ['/admin', '/versions', '/audience', '/press', '/desk']) {
+    const { status } = await get(p);
+    assert.equal(status, 200, `${p} should be 200 for admin`);
+  }
+});
+
+// Schema/status guard only — this asserts the endpoints respond 200 with the expected KEYS,
+// NOT that the numbers are non-zero. A zeroed-but-well-shaped data regression would still pass
+// here; catching that is the data-pipeline canaries' job, not this smoke test's.
+test('admin data APIs return 200 + expected schema', async () => {
+  const aud = await get('/api/audience');
+  assert.equal(aud.status, 200);
+  assert.ok(aud.body.stats && typeof aud.body.stats.total === 'number', '/api/audience should carry stats.total');
+
+  const pulse = await get('/api/pulse');
+  assert.equal(pulse.status, 200);
+  assert.ok(pulse.body.kpis && typeof pulse.body.kpis.dealCount === 'number', '/api/pulse should carry kpis.dealCount');
+
+  const vids = await get('/api/social/videos');
+  assert.equal(vids.status, 200);
+});
+
+// ── Select-to-live spine (TK-10356) ──────────────────────────────────────────────
+test('select-to-live: active-template API has {admin, frontpage} + whitelisted values', async () => {
+  const { status, body } = await get('/api/admin/active-template');
+  assert.equal(status, 200);
+  assert.ok(typeof body.admin === 'string' && typeof body.frontpage === 'string',
+    'active-template should return {admin, frontpage}');
+  assert.ok(ADMIN_LAYOUTS.includes(body.admin), `active admin '${body.admin}' must be a whitelisted layout`);
+});
+
+test('select-to-live: /admin serves an active dashboard wired to render.js', async () => {
+  const { status, body } = await get('/admin');
+  assert.equal(status, 200);
+  assert.match(body, /render\.js/, '/admin must include the live-data renderer');
+
+  const gallery = await get('/admin/gallery');
+  assert.equal(gallery.status, 200, '/admin/gallery should be 200');
+});
+
+// ── All 10 admin dashboards stay wired to live data ──────────────────────────────
+test('all 10 admin dashboards include render.js (live-wired)', async () => {
+  for (const layout of ADMIN_LAYOUTS) {
+    const { status, body } = await get('/admin/' + layout + '.html');
+    assert.equal(status, 200, `/admin/${layout}.html should be 200`);
+    assert.match(body, /render\.js/, `/admin/${layout}.html must include render.js`);
+  }
+});
+
+// ── All 10 frontpage version templates stay wired ────────────────────────────────
+test('all 10 frontpage version templates include render.js (live-wired)', async () => {
+  for (const v of FRONTPAGE_VERSIONS) {
+    const { status, body } = await get('/versions/' + v + '.html');
+    assert.equal(status, 200, `/versions/${v}.html should be 200`);
+    assert.match(body, /render\.js/, `/versions/${v}.html must include render.js`);
+  }
+});

← cb30389a auto-data-snapshot: 2026-08-08T07:24:16 (8 data files) — dat  ·  back to Rentv  ·  auto-data-snapshot: 2026-08-08T07:55:02 (7 data files) — dat 0dc4b7e0 →