[object Object]

← back to Rentv 2026

test(adslots): +124 coverage (buildInventory sources/dedup, reserve soft-hold lifecycle, exact price ladder, admin gating) + replace a pricing tautology with live-value asserts

f709ec783e9c6c85428d73a6ab82529294b23ed0 · 2026-08-10 11:27:19 -0700 · Steve Abrams

Graph-engineering pass under TK-10364: 6 new zero-dep test files raise the ad-system.cjs adslots suite 23->147 (0 fail). No production code changed. Contrarian(Cody)-gated: fixed a false-confidence tautology in pricing (assert.ok(200<250) -> live suggested_price asserts), pinned the intentional admin-can-unsell vs public-reserve-cannot asymmetry, and clarified the quoted_monthly=0 known-bug guard.

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

Files touched

Diff

commit f709ec783e9c6c85428d73a6ab82529294b23ed0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 11:27:19 2026 -0700

    test(adslots): +124 coverage (buildInventory sources/dedup, reserve soft-hold lifecycle, exact price ladder, admin gating) + replace a pricing tautology with live-value asserts
    
    Graph-engineering pass under TK-10364: 6 new zero-dep test files raise the ad-system.cjs adslots suite 23->147 (0 fail). No production code changed. Contrarian(Cody)-gated: fixed a false-confidence tautology in pricing (assert.ok(200<250) -> live suggested_price asserts), pinned the intentional admin-can-unsell vs public-reserve-cannot asymmetry, and clarified the quoted_monthly=0 known-bug guard.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 test/adslots/admin.test.mjs             |  16 +
 test/adslots/gating.test.mjs            | 358 ++++++++++++++++++
 test/adslots/inventory-build.test.mjs   | 589 ++++++++++++++++++++++++++++++
 test/adslots/inventory-sources.test.mjs | 503 ++++++++++++++++++++++++++
 test/adslots/pricing.test.mjs           | 623 ++++++++++++++++++++++++++++++++
 test/adslots/reserve-lifecycle.test.mjs | 485 +++++++++++++++++++++++++
 test/adslots/reserve-state.test.mjs     | 369 +++++++++++++++++++
 7 files changed, 2943 insertions(+)

diff --git a/test/adslots/admin.test.mjs b/test/adslots/admin.test.mjs
index e20ecad4..44ae15cb 100644
--- a/test/adslots/admin.test.mjs
+++ b/test/adslots/admin.test.mjs
@@ -53,6 +53,22 @@ test('admin/video: sets status/price/advertiser and persists to ad-state.json',
   assert.equal(readState(DATA).videos['yt-a1'].status, 'sold', 'persisted to disk');
 });
 
+test('admin/video: admin CAN override a sold slot back to available (intentional asymmetry vs public reserve)', () => {
+  // Contract pin: the PUBLIC reserve path guards `if (cur.status !== 'sold')` so a buyer
+  // inquiry can't downgrade a sold slot (see reserve-state.test.mjs). The ADMIN path is
+  // deliberately UNGUARDED — an admin must be able to un-sell a slot when a deal falls
+  // through. This test locks that asymmetry so nobody "fixes" it by copying the reserve
+  // guard onto the admin write (which would break legitimate un-selling).
+  const DATA = tmp();
+  fs.writeFileSync(path.join(DATA, 'ad-state.json'),
+    JSON.stringify({ videos: { 'yt-sold1': { status: 'sold', advertiser: 'ACME' } } }));
+  const { status, body } = hit(mount(DATA), 'POST /api/ad/admin/video',
+    { body: { uid: 'yt-sold1', status: 'available' } });
+  assert.equal(status, 200);
+  assert.equal(body.state.status, 'available', 'admin override un-sells the slot');
+  assert.equal(readState(DATA).videos['yt-sold1'].status, 'available', 'persisted to disk');
+});
+
 test('admin/video: an invalid status is ignored, not written', () => {
   const DATA = tmp();
   hit(mount(DATA), 'POST /api/ad/admin/video', { body: { uid: 'yt-a1', status: 'sold' } });
diff --git a/test/adslots/gating.test.mjs b/test/adslots/gating.test.mjs
new file mode 100644
index 00000000..9ef63a18
--- /dev/null
+++ b/test/adslots/gating.test.mjs
@@ -0,0 +1,358 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/gating.test.mjs — ADMIN GATING + CREATIVE-MERGE edges
+// Coverage areas:
+//  1. fail-safe adminOnly (no opts.adminOnly passed) → all admin routes 403
+//  2. pickGate for POST /api/ad/admin/site-template:
+//     (a) PUBLIC/OPEN unset + no adminOnly → fail-safe blocks (403)
+//     (b) process.env.OPEN='1' → pick allowed without admin (200)
+//     env save/restore via try/finally to prevent leakage
+//  3. creative-merge on GET /api/ad/rate-card: click_url/advertiser/flight +
+//     no-creative-entry leaves placement unchanged
+//  4. creative-merge on GET /api/ad/slot/:id via Object.assign:
+//     click_url/advertiser/flight, no-creative-entry unchanged, unknown id → 404
+//
+// Zero new npm deps. ESM .mjs, node:test + node:assert.
+// OWNED BY: cre-agent (TK-10364 PartD). Do NOT edit this file from other agents.
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+// ── harness helpers ──────────────────────────────────────────────────────────
+
+// mountChain: preserves ALL handlers (middleware + final) per route key.
+// Used when we need to run the full middleware chain (gating tests).
+// mountChain WITHOUT adminOnly in opts → module uses the fail-safe gate.
+function mountChain(DATA, opts = {}) {
+  const routes = {};
+  const reg = m => (p, ...handlers) => {
+    routes[`${m} ${p}`] = handlers; // store full chain
+  };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB: path.join(DATA, '__nopub__'), ...opts });
+  return routes;
+}
+
+// hitChain: runs the full handler chain (middleware → final handler) in order.
+// Middleware calls next() to proceed; if a middleware sends a response, the
+// chain stops. Returns { status, body }.
+function hitChain(routes, key, { query = {}, body = {}, params = {} } = {}) {
+  const handlers = routes[key];
+  if (!handlers || handlers.length === 0) throw new Error(`No handlers for route: ${key}`);
+  const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+  let out = { status: 200, body: null };
+  let responded = false;
+  const res = {
+    set() { return this; },
+    status(c) { out.status = c; return this; },
+    json(o) { out.body = o; responded = true; return this; },
+  };
+  // Run middleware chain; each calls next() or terminates with json()
+  let i = 0;
+  function next() {
+    if (responded) return;
+    const h = handlers[i++];
+    if (h) h(req, res, next);
+  }
+  next();
+  return out;
+}
+
+// mount (last-handler only, for non-gating tests matching existing convention)
+function mount(DATA, opts = {}) {
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB: path.join(DATA, '__nopub__'), ...opts });
+  return routes;
+}
+function hit(routes, key, { query = {}, body = {}, params = {} } = {}) {
+  const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+  let out = { status: 200, body: null };
+  const res = { set() { return this; }, status(c) { out.status = c; return this; }, json(o) { out.body = o; return this; } };
+  routes[key](req, res);
+  return out;
+}
+
+function tmpData(files = {}) {
+  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adgating-'));
+  for (const [f, o] of Object.entries(files)) {
+    fs.writeFileSync(path.join(dir, f), JSON.stringify(o));
+  }
+  return dir;
+}
+
+// ════════════════════════════════════════════════════════════════════════════
+// 1. FAIL-SAFE adminOnly — mount WITHOUT opts.adminOnly
+//    All three admin routes must return 403 {ok:false,error:'admin gate not wired'}
+// ════════════════════════════════════════════════════════════════════════════
+
+test('fail-safe: POST /api/ad/admin/video returns 403 when no adminOnly provided', () => {
+  const DATA = tmpData();
+  const routes = mountChain(DATA); // no adminOnly in opts
+  const out = hitChain(routes, 'POST /api/ad/admin/video', { body: { uid: 'yt-test' } });
+  assert.equal(out.status, 403, 'must be 403 (fail-CLOSED)');
+  assert.equal(out.body.ok, false);
+  assert.equal(out.body.error, 'admin gate not wired');
+});
+
+test('fail-safe: POST /api/ad/admin/placement returns 403 when no adminOnly provided', () => {
+  const DATA = tmpData();
+  const routes = mountChain(DATA);
+  const out = hitChain(routes, 'POST /api/ad/admin/placement', { body: { id: 'home-leaderboard' } });
+  assert.equal(out.status, 403, 'must be 403 (fail-CLOSED)');
+  assert.equal(out.body.ok, false);
+  assert.equal(out.body.error, 'admin gate not wired');
+});
+
+test('fail-safe: GET /api/ad/admin/reservations returns 403 when no adminOnly provided', () => {
+  const DATA = tmpData();
+  const routes = mountChain(DATA);
+  const out = hitChain(routes, 'GET /api/ad/admin/reservations');
+  assert.equal(out.status, 403, 'must be 403 (fail-CLOSED)');
+  assert.equal(out.body.ok, false);
+  assert.equal(out.body.error, 'admin gate not wired');
+});
+
+// ════════════════════════════════════════════════════════════════════════════
+// 2a. pickGate — PUBLIC/OPEN unset + no adminOnly → fail-safe blocks (403)
+//     save/restore process.env.PUBLIC + process.env.OPEN via try/finally
+// ════════════════════════════════════════════════════════════════════════════
+
+test('pickGate (2a): PUBLIC/OPEN unset + no adminOnly → fail-safe blocks site-template pick (403)', () => {
+  const savedPUBLIC = process.env.PUBLIC;
+  const savedOPEN = process.env.OPEN;
+  try {
+    delete process.env.PUBLIC;
+    delete process.env.OPEN;
+    const DATA = tmpData();
+    const routes = mountChain(DATA); // no adminOnly
+    const out = hitChain(routes, 'POST /api/ad/admin/site-template', { body: { chosen: 'a' } });
+    assert.equal(out.status, 403, 'pickGate falls through to fail-safe adminOnly → 403');
+    assert.equal(out.body.ok, false);
+    assert.equal(out.body.error, 'admin gate not wired');
+  } finally {
+    if (savedPUBLIC === undefined) delete process.env.PUBLIC; else process.env.PUBLIC = savedPUBLIC;
+    if (savedOPEN === undefined) delete process.env.OPEN; else process.env.OPEN = savedOPEN;
+  }
+});
+
+// ════════════════════════════════════════════════════════════════════════════
+// 2b. pickGate — process.env.OPEN='1' → pick bypasses adminOnly (200)
+//     the chosen template is persisted and verifiable
+//     save/restore via try/finally
+// ════════════════════════════════════════════════════════════════════════════
+
+test('pickGate (2b): process.env.OPEN=1 → pick allowed without adminOnly, chosen persists (200)', () => {
+  const savedPUBLIC = process.env.PUBLIC;
+  const savedOPEN = process.env.OPEN;
+  try {
+    delete process.env.PUBLIC;
+    process.env.OPEN = '1';
+    const DATA = tmpData();
+    const routes = mountChain(DATA); // no adminOnly — OPEN bypasses it
+    const out = hitChain(routes, 'POST /api/ad/admin/site-template', { body: { chosen: 'b' } });
+    assert.equal(out.status, 200, 'OPEN=1 → pickGate sets req.role=admin and calls next()');
+    assert.equal(out.body.ok, true);
+    assert.equal(out.body.chosen, 'b');
+    // verify chosen persisted to site-template.json
+    const persisted = JSON.parse(fs.readFileSync(path.join(DATA, 'site-template.json'), 'utf8'));
+    assert.equal(persisted.chosen, 'b', 'chosen written to disk');
+  } finally {
+    if (savedPUBLIC === undefined) delete process.env.PUBLIC; else process.env.PUBLIC = savedPUBLIC;
+    if (savedOPEN === undefined) delete process.env.OPEN; else process.env.OPEN = savedOPEN;
+  }
+});
+
+test('pickGate (2b): process.env.PUBLIC=1 → pick also allowed without adminOnly (200)', () => {
+  const savedPUBLIC = process.env.PUBLIC;
+  const savedOPEN = process.env.OPEN;
+  try {
+    process.env.PUBLIC = '1';
+    delete process.env.OPEN;
+    const DATA = tmpData();
+    const routes = mountChain(DATA);
+    const out = hitChain(routes, 'POST /api/ad/admin/site-template', { body: { chosen: 'c' } });
+    assert.equal(out.status, 200, 'PUBLIC=1 → pickGate bypasses adminOnly');
+    assert.equal(out.body.chosen, 'c');
+  } finally {
+    if (savedPUBLIC === undefined) delete process.env.PUBLIC; else process.env.PUBLIC = savedPUBLIC;
+    if (savedOPEN === undefined) delete process.env.OPEN; else process.env.OPEN = savedOPEN;
+  }
+});
+
+// ════════════════════════════════════════════════════════════════════════════
+// 2c. Confirm env does NOT leak between tests — after both pickGate tests,
+//     PUBLIC and OPEN must be unset (or restored to original values).
+//     Run this as an explicit canary test.
+// ════════════════════════════════════════════════════════════════════════════
+
+test('env-leak canary: PUBLIC and OPEN are not leaked from pickGate tests', () => {
+  // Both env vars should be either absent or equal to whatever they were
+  // before this test file ran. Since the parent process doesn't set them,
+  // they should be undefined here. This test will fail if try/finally broke.
+  assert.equal(process.env.PUBLIC, undefined, 'PUBLIC must not leak');
+  assert.equal(process.env.OPEN, undefined, 'OPEN must not leak');
+});
+
+// ════════════════════════════════════════════════════════════════════════════
+// 3. CREATIVE-MERGE on GET /api/ad/rate-card
+//    Goes BEYOND the one existing test (ratecard.test.mjs line 76):
+//    - click_url merges correctly (verified separately above, but here
+//      we focus on advertiser + flight as independent fields)
+//    - no creative entry → placement returned UNCHANGED (all original fields intact)
+// ════════════════════════════════════════════════════════════════════════════
+
+test('rate-card merge: flight and advertiser from creatives merge onto matching placement', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'sidebar-right', name: 'SR', type: 'banner', price_monthly: 900, status: 'available' },
+    ] },
+    'ad-state.json': { videos: {}, creatives: {
+      'sidebar-right': {
+        creative_url: '/side.png',
+        click_url: 'https://sponsor.example/landing',
+        status: 'sold',
+        advertiser: 'UrbanEdge Realty',
+        flight: '2026-09',
+      },
+    } },
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { status, body } = hit(routes, 'GET /api/ad/rate-card');
+  assert.equal(status, 200);
+  const sr = body.placements.find(p => p.id === 'sidebar-right');
+  assert.equal(sr.click_url, 'https://sponsor.example/landing', 'click_url merges');
+  assert.equal(sr.advertiser, 'UrbanEdge Realty', 'advertiser merges');
+  assert.equal(sr.flight, '2026-09', 'flight merges');
+  assert.equal(sr.status, 'sold', 'status merges');
+  assert.equal(sr.creative_url, '/side.png', 'creative_url merges');
+});
+
+test('rate-card merge: placement with NO creative entry is returned completely unchanged', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'footer-banner', name: 'FB', type: 'banner', price_monthly: 500, status: 'available' },
+      { id: 'nl-top', name: 'NL', type: 'newsletter', price_monthly: 1200, status: 'available' },
+    ] },
+    'ad-state.json': { videos: {}, creatives: {
+      // only nl-top has a creative; footer-banner has none
+      'nl-top': { creative_url: '/nl.gif', status: 'sold', advertiser: 'PropTech Co', flight: '2026-10' },
+    } },
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { status, body } = hit(routes, 'GET /api/ad/rate-card');
+  assert.equal(status, 200);
+  const fb = body.placements.find(p => p.id === 'footer-banner');
+  // no creative → these fields must remain as they were in ad-slots.json
+  assert.equal(fb.status, 'available', 'status untouched');
+  assert.equal(fb.creative_url, undefined, 'creative_url absent (no creative seeded)');
+  assert.equal(fb.click_url, undefined, 'click_url absent');
+  assert.equal(fb.advertiser, undefined, 'advertiser absent');
+  assert.equal(fb.flight, undefined, 'flight absent');
+});
+
+test('rate-card merge: multiple placements — only the matched one changes', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'hero-top', name: 'HT', type: 'banner', price_monthly: 4000, status: 'available' },
+      { id: 'mid-article', name: 'MA', type: 'banner', price_monthly: 700, status: 'available' },
+    ] },
+    'ad-state.json': { videos: {}, creatives: {
+      'hero-top': { creative_url: '/hero.jpg', click_url: 'https://hero.example', status: 'sold', advertiser: 'CREFirm', flight: '2026-Q4' },
+    } },
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { body } = hit(routes, 'GET /api/ad/rate-card');
+  const hero = body.placements.find(p => p.id === 'hero-top');
+  const mid = body.placements.find(p => p.id === 'mid-article');
+  // hero-top should have all merged fields
+  assert.equal(hero.status, 'sold');
+  assert.equal(hero.advertiser, 'CREFirm');
+  assert.equal(hero.flight, '2026-Q4');
+  assert.equal(hero.click_url, 'https://hero.example');
+  // mid-article should be untouched
+  assert.equal(mid.status, 'available');
+  assert.equal(mid.advertiser, undefined);
+  assert.equal(mid.flight, undefined);
+  assert.equal(mid.click_url, undefined);
+});
+
+// ════════════════════════════════════════════════════════════════════════════
+// 4. CREATIVE-MERGE on GET /api/ad/slot/:id (Object.assign merge)
+//    Goes BEYOND existing ratecard.test.mjs (which only tested creative_url + status).
+//    Here: click_url / advertiser / flight individually, plus no-creative unchanged,
+//    and the unknown-id → 404 path.
+// ════════════════════════════════════════════════════════════════════════════
+
+test('slot/:id merge: click_url from creative merges into slot via Object.assign', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'news-sidebar', name: 'NS', type: 'banner', price_monthly: 600, status: 'available' },
+    ] },
+    'ad-state.json': { videos: {}, creatives: {
+      'news-sidebar': { click_url: 'https://click.example' },
+    } },
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { status, body } = hit(routes, 'GET /api/ad/slot/:id', { params: { id: 'news-sidebar' } });
+  assert.equal(status, 200);
+  assert.equal(body.ok, true);
+  assert.equal(body.slot.click_url, 'https://click.example', 'click_url merges via Object.assign');
+});
+
+test('slot/:id merge: advertiser and flight from creative merge into slot', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'deal-wire-banner', name: 'DW', type: 'banner', price_monthly: 800, status: 'available' },
+    ] },
+    'ad-state.json': { videos: {}, creatives: {
+      'deal-wire-banner': { advertiser: 'Apex Capital', flight: '2026-11', status: 'sold' },
+    } },
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { status, body } = hit(routes, 'GET /api/ad/slot/:id', { params: { id: 'deal-wire-banner' } });
+  assert.equal(status, 200);
+  assert.equal(body.slot.advertiser, 'Apex Capital', 'advertiser merges');
+  assert.equal(body.slot.flight, '2026-11', 'flight merges');
+  assert.equal(body.slot.status, 'sold', 'status merges');
+});
+
+test('slot/:id merge: placement with NO creative entry returns unchanged (no extra fields)', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'market-intel-top', name: 'MI', type: 'banner', price_monthly: 1100, status: 'available' },
+    ] },
+    'ad-state.json': { videos: {}, creatives: {} }, // no entry for market-intel-top
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { status, body } = hit(routes, 'GET /api/ad/slot/:id', { params: { id: 'market-intel-top' } });
+  assert.equal(status, 200);
+  assert.equal(body.ok, true);
+  // slot should match the original placement exactly (Object.assign({}, p, {}) = p clone)
+  assert.equal(body.slot.id, 'market-intel-top');
+  assert.equal(body.slot.status, 'available', 'status untouched');
+  assert.equal(body.slot.advertiser, undefined, 'no advertiser injected');
+  assert.equal(body.slot.click_url, undefined, 'no click_url injected');
+  assert.equal(body.slot.flight, undefined, 'no flight injected');
+  assert.equal(body.slot.price_monthly, 1100, 'original price intact');
+});
+
+test('slot/:id: unknown slot id returns 404 with ok:false and error message', () => {
+  const DATA = tmpData({
+    'ad-slots.json': { placements: [
+      { id: 'known-slot', name: 'KS', type: 'banner', price_monthly: 500, status: 'available' },
+    ] },
+  });
+  const routes = mount(DATA, { adminOnly: (_q, _r, n) => n && n() });
+  const { status, body } = hit(routes, 'GET /api/ad/slot/:id', { params: { id: 'completely-unknown' } });
+  assert.equal(status, 404);
+  assert.equal(body.ok, false);
+  assert.equal(body.error, 'no such slot');
+});
diff --git a/test/adslots/inventory-build.test.mjs b/test/adslots/inventory-build.test.mjs
new file mode 100644
index 00000000..cb99ea75
--- /dev/null
+++ b/test/adslots/inventory-build.test.mjs
@@ -0,0 +1,589 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/inventory-build.test.mjs  —  TK-10364 PartA (cre-agent)
+//
+// Covers the genuinely-uncovered branches in buildInventory() and its helpers
+// (src/ad-system.cjs lines 66-160) that cycles 1-4 and the other four test files
+// in this directory still leave open:
+//
+//   1. originals.json: direct field assertions (uid=orig-<id>, source='original',
+//      kind='brand', cat='RENTV Originals', mp4, view_url=v.mp4, plays=0, date=null)
+//   2. vimeo cat field: kind==='episode' → cat='CRE Talk', else → 'CRE Talk · Clips'
+//      (the exact string literals in line 122 — never previously asserted)
+//   3. vimeo items[] middle array: items from vim.items (not episodes or clips) merge in
+//   4. vimeo embed fallback: absent v.embed → 'https://player.vimeo.com/video/<id>'
+//   5. vimeo thumb_local priority: thumb_local wins over thumb when both present
+//   6. vimeo view_url from v.url field (v.url || null, line 124)
+//   7. scanDir: explicit tolerance of a named-but-absent directory (no throw)
+//   8. sponsorship null defaults on every item (advertiser:null, flight:null,
+//      updated_at:null) when no ad-state entry exists — applied via the overlay loop
+//
+// Harness matches the repo zero-dep style exactly: node:test, node:assert/strict,
+// ESM .mjs, fake-Express mount() + synchronous hit(), mkdtempSync for fixtures.
+// No new npm deps. Does NOT edit src/ad-system.cjs.
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+// ── minimal fake-Express harness (matches repo style exactly) ────────────────
+function mount(DATA, PUB) {
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB: PUB || path.join(DATA, '__nopub__'), adminOnly: (_q, _r, n) => n && n() });
+  return routes;
+}
+
+function hit(routes, key, { query = {}, body = {}, params = {} } = {}) {
+  const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+  let out = { status: 200, body: null };
+  const res = {
+    set() { return this; },
+    status(c) { out.status = c; return this; },
+    json(o) { out.body = o; return this; },
+  };
+  routes[key](req, res);
+  return out;
+}
+
+// ── fixture helpers ───────────────────────────────────────────────────────────
+const MP4_BYTES = Buffer.from('mp4placeholder');
+
+// Build a throwaway DATA dir with all baseline fixture files so readJSON
+// never falls back to {} on an unrelated source file.
+function makeData(overrides = {}) {
+  const DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'adslots-build-'));
+  const defaults = {
+    'videos.json':        { items: [] },
+    'videos-pinned.json': { items: [] },
+    'originals.json':     { items: [] },
+    'vimeo-library.json': {},
+    'ad-slots.json':      { placements: [] },
+  };
+  const merged = Object.assign({}, defaults, overrides);
+  for (const [f, obj] of Object.entries(merged)) {
+    fs.writeFileSync(path.join(DATA, f), JSON.stringify(obj));
+  }
+  return DATA;
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 1. originals.json: direct field assertions
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('originals: uid is orig-<id>, source=original, kind=brand, cat=RENTV Originals', () => {
+  const DATA = makeData({
+    'originals.json': {
+      items: [{ id: 'bspot1', title: 'Brand Spot One', mp4: '/videos/bspot1.mp4', thumb: '/t/bspot1.jpg', desc: 'First brand spot.' }],
+    },
+  });
+  const routes = mount(DATA);
+  const { status, body } = hit(routes, 'GET /api/ad/inventory');
+
+  assert.equal(status, 200);
+  const item = body.items.find(i => i.uid === 'orig-bspot1');
+  assert.ok(item, 'originals item must appear with uid prefix "orig-"');
+  assert.equal(item.source, 'original', 'source must be "original"');
+  assert.equal(item.kind, 'brand', 'kind must be "brand"');
+  assert.equal(item.cat, 'RENTV Originals', 'cat must be exactly "RENTV Originals"');
+  assert.equal(item.title, 'Brand Spot One');
+  assert.equal(item.desc, 'First brand spot.');
+});
+
+test('originals: mp4 field and view_url both equal v.mp4 from the fixture', () => {
+  const DATA = makeData({
+    'originals.json': {
+      items: [{ id: 'o7', title: 'Brand Seven', mp4: '/videos/originals/o7.mp4' }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'orig-o7');
+  assert.ok(item, 'orig-o7 must appear');
+  assert.equal(item.mp4, '/videos/originals/o7.mp4', 'mp4 must equal v.mp4');
+  assert.equal(item.view_url, '/videos/originals/o7.mp4', 'view_url must also equal v.mp4 (line 109)');
+});
+
+test('originals: plays=0 and date=null regardless of source data', () => {
+  // originals.json items carry no plays or date — the module hard-codes plays:0, date:null
+  const DATA = makeData({
+    'originals.json': {
+      items: [{ id: 'oX', title: 'Any Spot', mp4: '/oX.mp4' }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'orig-oX');
+  assert.ok(item);
+  assert.equal(item.plays, 0, 'originals always have plays=0');
+  assert.equal(item.date, null, 'originals always have date=null');
+});
+
+test('originals: multiple items all get distinct orig-<id> uids', () => {
+  const DATA = makeData({
+    'originals.json': {
+      items: [
+        { id: 'a1', title: 'Alpha', mp4: '/a1.mp4' },
+        { id: 'b2', title: 'Beta',  mp4: '/b2.mp4' },
+        { id: 'c3', title: 'Gamma', mp4: '/c3.mp4' },
+      ],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const origItems = body.items.filter(i => i.source === 'original');
+  assert.equal(origItems.length, 3, 'all three originals appear');
+  const uids = origItems.map(i => i.uid).sort();
+  assert.deepEqual(uids, ['orig-a1', 'orig-b2', 'orig-c3'], 'uid prefix is orig- for all');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 2. vimeo cat field: kind==='episode' → 'CRE Talk', else → 'CRE Talk · Clips'
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('vimeo cat: episode kind produces cat="CRE Talk" (exact string)', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      episodes: [{ id: 'e1', kind: 'episode', title: 'Full Episode', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-e1');
+  assert.ok(item, 'vimeo episode must appear');
+  assert.equal(item.cat, 'CRE Talk', 'episode cat must be exactly "CRE Talk"');
+  assert.equal(item.kind, 'episode', 'kind must be preserved as "episode"');
+});
+
+test('vimeo cat: non-episode kind produces cat="CRE Talk · Clips" (exact string with middle dot)', () => {
+  // The middle-dot character in "CRE Talk · Clips" (U+00B7) is load-bearing.
+  const DATA = makeData({
+    'vimeo-library.json': {
+      clips: [{ id: 'c1', title: 'Short Clip', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-c1');
+  assert.ok(item, 'vimeo clip must appear');
+  assert.equal(item.cat, 'CRE Talk · Clips', 'clip cat must be exactly "CRE Talk · Clips" (U+00B7 middle dot)');
+});
+
+test('vimeo cat: item with explicit kind="clip" also routes to "CRE Talk · Clips"', () => {
+  // v.kind exists but is not 'episode' → else branch
+  const DATA = makeData({
+    'vimeo-library.json': {
+      items: [{ id: 'i1', kind: 'clip', title: 'Explicit Clip Kind', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-i1');
+  assert.ok(item, 'vimeo item with kind=clip must appear');
+  assert.equal(item.cat, 'CRE Talk · Clips', 'explicit clip kind also routes to Clips category');
+});
+
+test('vimeo cat: both episode and clip in same call produce their respective cats', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      episodes: [{ id: 'ep1', kind: 'episode', title: 'Episode One', plays: 0 }],
+      clips:    [{ id: 'cl1', title: 'Clip One', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const ep = body.items.find(i => i.uid === 'vimeo-ep1');
+  const cl = body.items.find(i => i.uid === 'vimeo-cl1');
+  assert.ok(ep && cl, 'both vimeo items must appear');
+  assert.equal(ep.cat, 'CRE Talk', 'episode gets CRE Talk');
+  assert.equal(cl.cat, 'CRE Talk · Clips', 'clip gets CRE Talk · Clips');
+  assert.notEqual(ep.cat, cl.cat, 'episode and clip categories must differ');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 3. vimeo items[] middle array (vim.items, distinct from episodes and clips)
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('vimeo items[]: entries in vim.items array appear in inventory', () => {
+  // The module does: [].concat(vim.episodes||[], vim.items||[], vim.clips||[])
+  // vim.items is the middle array — not episodes, not clips.
+  const DATA = makeData({
+    'vimeo-library.json': {
+      items: [
+        { id: 'm1', kind: 'clip', title: 'Items Array Entry One', plays: 0 },
+        { id: 'm2', kind: 'episode', title: 'Items Array Episode', plays: 0 },
+      ],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const m1 = body.items.find(i => i.uid === 'vimeo-m1');
+  const m2 = body.items.find(i => i.uid === 'vimeo-m2');
+  assert.ok(m1, 'vim.items[0] must appear in inventory');
+  assert.ok(m2, 'vim.items[1] must appear in inventory');
+  assert.equal(m1.source, 'vimeo');
+  assert.equal(m1.cat, 'CRE Talk · Clips', 'clip from items[] array routes to Clips category');
+  assert.equal(m2.cat, 'CRE Talk', 'episode from items[] array routes to CRE Talk category');
+});
+
+test('vimeo items[]: malformed entry (id=null) inside vim.items is skipped', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      items: [
+        { id: null, title: 'Null in items array' },
+        { id: 'mOK', kind: 'clip', title: 'Valid item', plays: 0 },
+      ],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const vimItems = body.items.filter(i => i.source === 'vimeo');
+  assert.equal(vimItems.length, 1, 'null-id row in vim.items is skipped');
+  assert.equal(vimItems[0].uid, 'vimeo-mOK');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 4. vimeo embed fallback URL construction
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('vimeo embed: absent v.embed falls back to https://player.vimeo.com/video/<id>', () => {
+  // Line 123: embed: v.embed || `https://player.vimeo.com/video/${v.id}`
+  const DATA = makeData({
+    'vimeo-library.json': {
+      episodes: [{ id: '12345', kind: 'episode', title: 'No Embed Field', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-12345');
+  assert.ok(item, 'vimeo item must appear');
+  assert.equal(item.embed, 'https://player.vimeo.com/video/12345',
+    'embed must fall back to canonical Vimeo player URL built from v.id');
+});
+
+test('vimeo embed: explicit v.embed wins over the fallback', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      clips: [{ id: '99999', title: 'Has Embed', embed: 'https://player.vimeo.com/video/99999?h=abc123', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-99999');
+  assert.ok(item);
+  assert.equal(item.embed, 'https://player.vimeo.com/video/99999?h=abc123',
+    'explicit v.embed must be preserved as-is, not overwritten by fallback');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 5. vimeo thumb_local priority over thumb
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('vimeo thumb: thumb_local wins over thumb when both are present (line 123)', () => {
+  // Line 123: thumb: v.thumb_local || v.thumb
+  const DATA = makeData({
+    'vimeo-library.json': {
+      episodes: [{
+        id: 'tloc1', kind: 'episode', title: 'Has Both Thumbs', plays: 0,
+        thumb_local: '/local/cache/tloc1.jpg',
+        thumb: 'https://vimeo.com/remote/tloc1.jpg',
+      }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-tloc1');
+  assert.ok(item, 'vimeo item must appear');
+  assert.equal(item.thumb, '/local/cache/tloc1.jpg',
+    'thumb_local must win over thumb when both are present');
+});
+
+test('vimeo thumb: falls back to thumb when thumb_local is absent', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      clips: [{
+        id: 'tremote1', title: 'Remote Thumb Only', plays: 0,
+        thumb: 'https://vimeo.com/remote/tremote1.jpg',
+      }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-tremote1');
+  assert.ok(item);
+  assert.equal(item.thumb, 'https://vimeo.com/remote/tremote1.jpg',
+    'when thumb_local absent, thumb fallback is used');
+});
+
+test('vimeo thumb: both absent → thumb is falsy (null or undefined)', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      clips: [{ id: 'tnone', title: 'No Thumbs', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-tnone');
+  assert.ok(item);
+  assert.ok(!item.thumb, 'when both thumb_local and thumb are absent, thumb is falsy');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 6. vimeo view_url from v.url field
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('vimeo view_url: set to v.url when present (line 124)', () => {
+  // Line 124: view_url: v.url || null
+  const DATA = makeData({
+    'vimeo-library.json': {
+      episodes: [{
+        id: 'url1', kind: 'episode', title: 'Has URL', plays: 0,
+        url: 'https://vimeo.com/url1',
+      }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-url1');
+  assert.ok(item);
+  assert.equal(item.view_url, 'https://vimeo.com/url1',
+    'view_url must be set to v.url when present');
+});
+
+test('vimeo view_url: null when v.url is absent', () => {
+  const DATA = makeData({
+    'vimeo-library.json': {
+      clips: [{ id: 'nourl', title: 'No URL field', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'vimeo-nourl');
+  assert.ok(item);
+  assert.equal(item.view_url, null, 'view_url is null when v.url is absent (v.url || null)');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 7. scanDir: explicit tolerance of a named-but-absent directory
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('scanDir: absent news-wire dir does not throw — inventory returns 0 news-wire items', () => {
+  // PUB exists but vid/news-wire/ subdir does not. scanDir catches the ENOENT
+  // from readdirSync and returns [] (line 74: catch { /* dir may not exist */ }).
+  const PUB = fs.mkdtempSync(path.join(os.tmpdir(), 'adslots-pub-'));
+  const DATA = makeData();
+  // Explicitly do NOT create PUB/vid/news-wire — it's intentionally absent.
+  // PUB/social-videos also absent.
+  const routes = mount(DATA, PUB);
+
+  let threw = false;
+  let body = null;
+  try {
+    const result = hit(routes, 'GET /api/ad/inventory');
+    body = result.body;
+  } catch (e) {
+    threw = true;
+  }
+
+  assert.equal(threw, false, 'buildInventory must NOT throw when PUB subdirs are absent');
+  assert.ok(body && body.ok === true, 'response must have ok:true despite absent dirs');
+  const nwItems = body.items.filter(i => i.source === 'news-wire');
+  assert.equal(nwItems.length, 0, 'zero news-wire items when dir is absent');
+  const socItems = body.items.filter(i => i.source === 'social');
+  assert.equal(socItems.length, 0, 'zero social items when dir is absent');
+});
+
+test('scanDir: one dir present, other absent — only present dir contributes items', () => {
+  const PUB = fs.mkdtempSync(path.join(os.tmpdir(), 'adslots-pub2-'));
+  const DATA = makeData();
+  // Create only news-wire, leave social-videos absent.
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'deal.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  assert.ok(body.ok);
+  const nwItems  = body.items.filter(i => i.source === 'news-wire');
+  const socItems = body.items.filter(i => i.source === 'social');
+  assert.equal(nwItems.length, 1, 'news-wire item present when dir exists');
+  assert.equal(socItems.length, 0, 'no social items when dir is absent');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// 8. sponsorship block null defaults on every item
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('sponsorship overlay: every item gets sponsorship block even with no ad-state.json', () => {
+  // Lines 148-158: loop applies overlay to every item. No ad-state.json →
+  // loadState() returns { videos:{}, updated_at:null }. Every item should
+  // get the default sponsorship object with status='available'.
+  const DATA = makeData({
+    'videos.json': {
+      items: [
+        { id: 'sp1', yt: 'SP1', title: 'Sponsored Test', views: 0 },
+      ],
+    },
+    'originals.json': {
+      items: [{ id: 'spO1', title: 'Original Sponsor', mp4: '/o.mp4' }],
+    },
+    'vimeo-library.json': {
+      clips: [{ id: 'spV1', title: 'Vimeo Sponsor', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  for (const item of body.items) {
+    assert.ok(item.sponsorship, `item ${item.uid} must have a sponsorship block`);
+    assert.equal(item.sponsorship.status, 'available',
+      `item ${item.uid} sponsorship.status defaults to 'available'`);
+    assert.equal(item.sponsorship.advertiser, null,
+      `item ${item.uid} sponsorship.advertiser defaults to null`);
+    assert.equal(item.sponsorship.flight, null,
+      `item ${item.uid} sponsorship.flight defaults to null`);
+    assert.equal(item.sponsorship.updated_at, null,
+      `item ${item.uid} sponsorship.updated_at defaults to null`);
+    assert.ok(typeof item.sponsorship.price_monthly === 'number',
+      `item ${item.uid} sponsorship.price_monthly is a number`);
+    assert.ok(typeof item.suggested_price === 'number',
+      `item ${item.uid} suggested_price is a number`);
+  }
+});
+
+test('sponsorship overlay: status/advertiser/flight/updated_at from ad-state overlay onto specific uid', () => {
+  // Verify the overlay reads from the correct uid key and applies all four
+  // fields, not just status.
+  const DATA = makeData({
+    'videos.json': {
+      items: [
+        { id: 'ovA', yt: 'OVA', title: 'Overlay Alpha', views: 0 },
+        { id: 'ovB', yt: 'OVB', title: 'Overlay Beta',  views: 0 },
+      ],
+    },
+    'ad-state.json': {
+      videos: {
+        'yt-ovA': {
+          status: 'reserved',
+          price_monthly: 800,
+          advertiser: 'Acme Real Estate',
+          flight: '2026-09',
+          updated_at: '2026-08-10T00:00:00.000Z',
+        },
+      },
+      updated_at: null,
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const ovA = body.items.find(i => i.uid === 'yt-ovA');
+  const ovB = body.items.find(i => i.uid === 'yt-ovB');
+  assert.ok(ovA && ovB, 'both items must appear');
+
+  // ovA: all fields from state overlay
+  assert.equal(ovA.sponsorship.status, 'reserved');
+  assert.equal(ovA.sponsorship.price_monthly, 800, 'state price_monthly wins');
+  assert.equal(ovA.sponsorship.advertiser, 'Acme Real Estate');
+  assert.equal(ovA.sponsorship.flight, '2026-09');
+  assert.equal(ovA.sponsorship.updated_at, '2026-08-10T00:00:00.000Z');
+
+  // ovB: no state entry → all defaults
+  assert.equal(ovB.sponsorship.status, 'available');
+  assert.equal(ovB.sponsorship.advertiser, null);
+  assert.equal(ovB.sponsorship.flight, null);
+  assert.equal(ovB.sponsorship.updated_at, null);
+  // price_monthly falls back to suggestPrice (youtube, 0 plays → 600)
+  assert.equal(ovB.sponsorship.price_monthly, 600);
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Integration: full buildInventory() with all sources + vimeo cat verification
+// ─────────────────────────────────────────────────────────────────────────────
+
+test('buildInventory integration: youtube + pinned-only + original + vimeo (ep+items+clip) in one call', () => {
+  const DATA = makeData({
+    'videos.json': {
+      fetched_at: '2026-08-10',
+      items: [{ id: 'ytI', yt: 'YTI', title: 'YouTube Item', views: 50 }],
+    },
+    'videos-pinned.json': {
+      items: [
+        { id: 'ytI', title: 'Same As YT', thumb: null, embed: null }, // deduped
+        { id: 'pin1', title: 'Pinned Only', thumb: null, embed: null }, // new, pinned:true
+      ],
+    },
+    'originals.json': {
+      items: [{ id: 'origI', title: 'The Original', mp4: '/origI.mp4', desc: 'An original.' }],
+    },
+    'vimeo-library.json': {
+      episodes: [{ id: 'vEp', kind: 'episode', title: 'Full Episode', plays: 200 }],
+      items:    [{ id: 'vIt', kind: 'clip',    title: 'Items Clip',   plays: 30 }],
+      clips:    [{ id: 'vCl', title: 'Loose Clip', plays: 0 }],
+    },
+  });
+  const routes = mount(DATA);
+  const { status, body } = hit(routes, 'GET /api/ad/inventory');
+
+  assert.equal(status, 200);
+  // 1 yt + 1 pinned-only + 1 orig + 3 vimeo = 6 items
+  assert.equal(body.count, 6, '1 youtube + 1 pinned-only + 1 original + 3 vimeo = 6');
+  assert.equal(body.summary.total, 6);
+
+  // Source counts
+  const bySrc = body.summary.by_source;
+  assert.equal(bySrc.youtube,  2, '2 youtube items (ytI from videos.json + pin1 pinned-only)');
+  assert.equal(bySrc.original, 1);
+  assert.equal(bySrc.vimeo,    3, '3 vimeo items (episode + items-clip + loose-clip)');
+
+  // Pinned behavior
+  const ytI  = body.items.find(i => i.uid === 'yt-ytI');
+  const pin1 = body.items.find(i => i.uid === 'yt-pin1');
+  assert.ok(ytI,  'yt-ytI from videos.json appears');
+  assert.ok(pin1, 'yt-pin1 pinned-only item appears');
+  assert.ok(!ytI.pinned, 'yt-ytI (from videos.json) does not carry pinned flag');
+  assert.equal(pin1.pinned, true, 'pin1 pinned-only item carries pinned:true');
+
+  // Original fields
+  const orig = body.items.find(i => i.uid === 'orig-origI');
+  assert.ok(orig);
+  assert.equal(orig.cat, 'RENTV Originals');
+  assert.equal(orig.mp4, '/origI.mp4');
+  assert.equal(orig.view_url, '/origI.mp4');
+
+  // Vimeo cat assignments
+  const vEp = body.items.find(i => i.uid === 'vimeo-vEp');
+  const vIt = body.items.find(i => i.uid === 'vimeo-vIt');
+  const vCl = body.items.find(i => i.uid === 'vimeo-vCl');
+  assert.ok(vEp && vIt && vCl, 'all three vimeo items present');
+  assert.equal(vEp.cat, 'CRE Talk',                'episode → CRE Talk');
+  assert.equal(vIt.cat, 'CRE Talk · Clips',   'items clip → CRE Talk · Clips');
+  assert.equal(vCl.cat, 'CRE Talk · Clips',   'loose clip → CRE Talk · Clips');
+
+  // Sponsorship block present on all items
+  assert.equal(body.items.every(i => i.sponsorship && typeof i.sponsorship.price_monthly === 'number'), true,
+    'every item must have a sponsorship block with a numeric price_monthly');
+});
diff --git a/test/adslots/inventory-sources.test.mjs b/test/adslots/inventory-sources.test.mjs
new file mode 100644
index 00000000..1f6141a2
--- /dev/null
+++ b/test/adslots/inventory-sources.test.mjs
@@ -0,0 +1,503 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/inventory-sources.test.mjs
+//
+// Closes video-inventory source/dedup coverage gaps that cycles 1-3 missed
+// (TK-10364 LaneA — cre-agent). Production code (src/ad-system.cjs) is frozen;
+// this file adds tests ONLY.
+//
+// Gap coverage:
+//   1. scanDir + news-wire + social sources (lines 128-144)
+//   2. titleize() (line 77)
+//   3. suggestPrice social/news-wire base tiers (lines 58-59)
+//   4. pinned dedup: same uid appears in both videos.json and videos-pinned.json
+//   5. vimeo seenV dedup (line 119) + malformed-row skips (yt line 86, vimeo line 117)
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+// ── fake-Express harness (matches repo style exactly) ────────────────────────
+// PUB is passed as a real dir for the scanDir-source tests in this file.
+function mount(DATA, PUB) {
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB, adminOnly: (_q, _r, n) => n && n() });
+  return routes;
+}
+
+function hit(routes, key, { query = {}, body = {}, params = {} } = {}) {
+  const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+  let out = { status: 200, body: null };
+  const res = { set() { return this; }, status(c) { out.status = c; return this; }, json(o) { out.body = o; return this; } };
+  routes[key](req, res);
+  return out;
+}
+
+// ── helpers: build isolated temp DATA + PUB dirs ──────────────────────────────
+// Each call returns a fresh unique pair so parallel runs never collide.
+function makeDirs() {
+  const DATA = fs.mkdtempSync(path.join(os.tmpdir(), 'adslots-src-data-'));
+  const PUB  = fs.mkdtempSync(path.join(os.tmpdir(), 'adslots-src-pub-'));
+  return { DATA, PUB };
+}
+
+// Write a JSON fixture into DATA.
+function wj(DATA, name, obj) {
+  fs.writeFileSync(path.join(DATA, name), JSON.stringify(obj));
+}
+
+// Minimal placeholder content for video/poster files (content doesn't matter —
+// scanDir only calls readdirSync and checks the file extension).
+const MP4_BYTES = Buffer.from('mp4placeholder');
+const JPG_BYTES = Buffer.from('jpgplaceholder');
+
+// ── Baseline empty fixtures so readJSON never throws for unrelated source files ─
+function writeBaseFixtures(DATA) {
+  wj(DATA, 'videos.json',        { items: [] });
+  wj(DATA, 'videos-pinned.json', { items: [] });
+  wj(DATA, 'originals.json',     { items: [] });
+  wj(DATA, 'vimeo-library.json', {});
+  wj(DATA, 'ad-slots.json',      { placements: [] });
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// GAP 1: scanDir reads news-wire and social directories from PUB
+// ─────────────────────────────────────────────────────────────────────────────
+test('scanDir news-wire: mp4 in PUB/vid/news-wire appears in inventory with correct uid/url/poster', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  // Create PUB/vid/news-wire/deal-close.mp4 + matching .jpg poster
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'deal-close.mp4'), MP4_BYTES);
+  fs.writeFileSync(path.join(nwDir, 'deal-close.jpg'), JPG_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { status, body } = hit(routes, 'GET /api/ad/inventory');
+
+  assert.equal(status, 200);
+  const item = body.items.find(i => i.uid === 'newswire-deal-close');
+  assert.ok(item, 'news-wire item must appear in inventory with uid prefix "newswire-"');
+  assert.equal(item.source, 'news-wire');
+  assert.equal(item.kind, 'deal-wire');
+  assert.equal(item.mp4, '/vid/news-wire/deal-close.mp4', 'mp4 must be rooted public path');
+  assert.equal(item.view_url, '/vid/news-wire/deal-close.mp4', 'view_url matches mp4 for news-wire');
+  assert.equal(item.thumb, '/vid/news-wire/deal-close.jpg', 'poster path uses base + .jpg');
+});
+
+test('scanDir social: mp4 in PUB/social-videos appears in inventory with correct uid/url', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  // Create PUB/social-videos/instagram-spot.mp4
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(socDir, 'instagram-spot.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { status, body } = hit(routes, 'GET /api/ad/inventory');
+
+  assert.equal(status, 200);
+  const item = body.items.find(i => i.uid === 'social-instagram-spot');
+  assert.ok(item, 'social item must appear in inventory with uid prefix "social-"');
+  assert.equal(item.source, 'social');
+  assert.equal(item.kind, 'social');
+  assert.equal(item.mp4, '/social-videos/instagram-spot.mp4');
+  assert.equal(item.view_url, '/social-videos/instagram-spot.mp4');
+  assert.equal(item.thumb, null, 'social items have no auto-poster (thumb is null)');
+});
+
+test('scanDir: news-wire and social both appear together when both dirs populated', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'tower-deal.mp4'), MP4_BYTES);
+  fs.writeFileSync(path.join(socDir, 'fb-clip.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const uids = body.items.map(i => i.uid);
+  assert.ok(uids.includes('newswire-tower-deal'), 'news-wire item present');
+  assert.ok(uids.includes('social-fb-clip'), 'social item present');
+  assert.equal(body.summary.by_source['news-wire'], 1);
+  assert.equal(body.summary.by_source['social'], 1);
+});
+
+test('scanDir: non-mp4 files in news-wire/social dirs are ignored', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'readme.txt'), Buffer.from('not a video'));
+  fs.writeFileSync(path.join(nwDir, 'thumb.jpg'), JPG_BYTES);
+  fs.writeFileSync(path.join(nwDir, 'real.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const nwItems = body.items.filter(i => i.source === 'news-wire');
+  assert.equal(nwItems.length, 1, 'only the .mp4 file produces an inventory item');
+  assert.equal(nwItems[0].uid, 'newswire-real');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// GAP 2: titleize() — file stem to display title
+// ─────────────────────────────────────────────────────────────────────────────
+test('titleize: hyphen-and-underscore file stem converts to title case (no extension)', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  // Use news-wire to exercise titleize via the live path
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  // "big-deal_closes.mp4" → title "Big Deal Closes"
+  fs.writeFileSync(path.join(nwDir, 'big-deal_closes.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'newswire-big-deal_closes');
+  assert.ok(item, 'item must be found');
+  assert.equal(item.title, 'Big Deal Closes', 'hyphens and underscores become spaces, each word capitalised');
+});
+
+test('titleize: all-lowercase single word becomes capitalised', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(socDir, 'promo.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'social-promo');
+  assert.ok(item);
+  assert.equal(item.title, 'Promo');
+});
+
+test('titleize: already-mixed-case stem (e.g. camelCase via hyphens) capitalises each word', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  // "cre-market-update_q2-2026.mp4" → "Cre Market Update Q2 2026"
+  fs.writeFileSync(path.join(nwDir, 'cre-market-update_q2-2026.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'newswire-cre-market-update_q2-2026');
+  assert.ok(item);
+  assert.equal(item.title, 'Cre Market Update Q2 2026');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// GAP 3: suggestPrice base tiers for social (200) and news-wire (250)
+// plays=0 so no bump; result rounds to nearest $50 (already exact at 200/250)
+// ─────────────────────────────────────────────────────────────────────────────
+test('suggestPrice news-wire: base 250, plays 0, suggested_price = 250', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'deal-spot.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'newswire-deal-spot');
+  assert.ok(item, 'news-wire item must be present');
+  assert.equal(item.suggested_price, 250, 'news-wire base 250, plays 0, no bump → 250');
+  assert.equal(item.suggested_price % 50, 0, 'must be a $50 multiple');
+});
+
+test('suggestPrice social: base 200, plays 0, suggested_price = 200', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(socDir, 'ig-reel.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'social-ig-reel');
+  assert.ok(item, 'social item must be present');
+  assert.equal(item.suggested_price, 200, 'social base 200, plays 0, no bump → 200');
+  assert.equal(item.suggested_price % 50, 0, 'must be a $50 multiple');
+});
+
+test('suggestPrice: news-wire base 250 is lower than social base 200 — no, 250 > 200 (tier ordering check)', () => {
+  // Explicit ordering assertion so the matrix is self-documenting in test output.
+  // news-wire 250 > social 200: deal-wire micro-spots price higher than generated socials.
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  const nwDir  = path.join(PUB, 'vid', 'news-wire');
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(nwDir,  { recursive: true });
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir,  'nw.mp4'),  MP4_BYTES);
+  fs.writeFileSync(path.join(socDir, 'soc.mp4'), MP4_BYTES);
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const nw  = body.items.find(i => i.uid === 'newswire-nw');
+  const soc = body.items.find(i => i.uid === 'social-soc');
+  assert.ok(nw  && soc, 'both items present');
+  assert.ok(nw.suggested_price > soc.suggested_price, 'news-wire (250) prices above social (200)');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// GAP 4: pinned dedup — same uid in both videos.json and videos-pinned.json
+// ─────────────────────────────────────────────────────────────────────────────
+test('pinned dedup: uid in both videos.json and videos-pinned.json appears exactly once', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  // a1 in both; p9 only in pinned
+  wj(DATA, 'videos.json', {
+    fetched_at: '2026-08-01',
+    items: [
+      { id: 'a1', yt: 'YT1', title: 'Review A', views: 0 },
+    ],
+  });
+  wj(DATA, 'videos-pinned.json', {
+    items: [
+      { id: 'a1', title: 'Review A Pinned', thumb: null, embed: null }, // same uid
+      { id: 'p9', title: 'Pinned Exclusive', thumb: null, embed: null }, // new
+    ],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const a1Items = body.items.filter(i => i.uid === 'yt-a1');
+  assert.equal(a1Items.length, 1, 'yt-a1 must appear exactly once (deduped from pinned)');
+
+  const p9 = body.items.find(i => i.uid === 'yt-p9');
+  assert.ok(p9, 'yt-p9 (only in pinned) must be added to inventory');
+  assert.equal(p9.pinned, true, 'p9 must carry pinned:true');
+});
+
+test('pinned dedup: the videos.json version wins (no pinned:true flag) when uid is in both', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  wj(DATA, 'videos.json', {
+    fetched_at: '2026-08-01',
+    items: [{ id: 'a1', yt: 'YT1', title: 'Review A', views: 600 }],
+  });
+  wj(DATA, 'videos-pinned.json', {
+    items: [{ id: 'a1', title: 'Review A Pinned', thumb: null, embed: null }],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const item = body.items.find(i => i.uid === 'yt-a1');
+  assert.ok(item);
+  // The first-pass item (from videos.json) was pushed before pinned loop runs;
+  // the pinned loop skips it. So pinned:true should NOT be on the surviving item.
+  assert.ok(!item.pinned, 'surviving item came from videos.json — pinned flag must not be set');
+  // plays came from videos.json (600 views), not pinned (0)
+  assert.equal(item.plays, 600, 'plays value from videos.json survives');
+});
+
+test('pinned dedup: multiple new pinned items all appear, all tagged pinned:true', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  wj(DATA, 'videos.json', { items: [] }); // empty — nothing from YT source
+  wj(DATA, 'videos-pinned.json', {
+    items: [
+      { id: 'p1', title: 'Pin One',   thumb: null, embed: null },
+      { id: 'p2', title: 'Pin Two',   thumb: null, embed: null },
+      { id: 'p3', title: 'Pin Three', thumb: null, embed: null },
+    ],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const pinned = body.items.filter(i => i.pinned === true);
+  assert.equal(pinned.length, 3, 'all three pinned items added');
+  const uids = pinned.map(i => i.uid).sort();
+  assert.deepEqual(uids, ['yt-p1', 'yt-p2', 'yt-p3']);
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// GAP 5a: vimeo seenV dedup — duplicate vimeo id collapses to one item
+// ─────────────────────────────────────────────────────────────────────────────
+test('vimeo seenV dedup: duplicate vimeo id across episodes/clips/items collapses to one inventory item', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  // v99 appears in both episodes and clips (simulates a re-run or cross-list dupe)
+  wj(DATA, 'vimeo-library.json', {
+    episodes: [{ id: 'v99', kind: 'episode', title: 'CRE Ep Dupe', plays: 50 }],
+    clips:    [{ id: 'v99', title: 'CRE Clip Dupe', plays: 30 }],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const dupes = body.items.filter(i => i.uid === 'vimeo-v99');
+  assert.equal(dupes.length, 1, 'vimeo-v99 must appear exactly once despite being in two lists');
+  // The first occurrence (episodes) wins
+  assert.equal(dupes[0].kind, 'episode', 'first occurrence (from episodes array) is retained');
+});
+
+test('vimeo seenV dedup: three-way dupe across episodes+clips+items collapses to one', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  wj(DATA, 'vimeo-library.json', {
+    episodes: [{ id: 'vX', kind: 'episode', title: 'EP', plays: 10 }],
+    items:    [{ id: 'vX', kind: 'clip',    title: 'IT', plays: 20 }],
+    clips:    [{ id: 'vX', title: 'CL', plays: 30 }],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const matches = body.items.filter(i => i.uid === 'vimeo-vX');
+  assert.equal(matches.length, 1, 'three-way vimeo dupe collapses to one item');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// GAP 5b: malformed-row skips
+//   youtube: v.id == null or v.id === '' skipped (line 86)
+//   vimeo:   v.id == null or v.id === '' skipped (line 117)
+// ─────────────────────────────────────────────────────────────────────────────
+test('youtube malformed rows: id=null and id="" are skipped, valid rows still appear', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  wj(DATA, 'videos.json', {
+    items: [
+      { id: null,  title: 'No ID Null' },           // must be skipped
+      { id: '',    title: 'No ID Empty' },           // must be skipped
+      { id: 'ok1', title: 'Valid YT', views: 0 },   // must appear
+    ],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const ytItems = body.items.filter(i => i.source === 'youtube');
+  assert.equal(ytItems.length, 1, 'only the row with a real id survives');
+  assert.equal(ytItems[0].uid, 'yt-ok1');
+});
+
+test('vimeo malformed rows: id=null and id="" are skipped, valid rows still appear', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  wj(DATA, 'vimeo-library.json', {
+    episodes: [
+      { id: null,  kind: 'episode', title: 'Null ID ep' },     // must be skipped
+      { id: '',    kind: 'episode', title: 'Empty ID ep' },    // must be skipped
+      { id: 'v5', kind: 'episode', title: 'Good ep', plays: 0 }, // must appear
+    ],
+    clips: [
+      { id: null, title: 'Null clip' },                         // must be skipped
+    ],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const vimItems = body.items.filter(i => i.source === 'vimeo');
+  assert.equal(vimItems.length, 1, 'only one vimeo item with a real id survives');
+  assert.equal(vimItems[0].uid, 'vimeo-v5');
+});
+
+test('malformed rows: missing id property (undefined) is treated as null — skipped', () => {
+  const { DATA, PUB } = makeDirs();
+  writeBaseFixtures(DATA);
+
+  // { title: 'No id key' } has v.id === undefined, which == null is TRUE
+  wj(DATA, 'videos.json', {
+    items: [
+      { title: 'No id key at all' },                    // v.id === undefined → skip
+      { id: 'good', yt: 'YTG', title: 'Fine', views: 0 },
+    ],
+  });
+
+  const routes = mount(DATA, PUB);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+
+  const ytItems = body.items.filter(i => i.source === 'youtube');
+  assert.equal(ytItems.length, 1, 'row with missing id property skipped');
+  assert.equal(ytItems[0].uid, 'yt-good');
+});
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Integration: combined fixture — all sources in one inventory call
+// ─────────────────────────────────────────────────────────────────────────────
+test('combined: all six sources appear when all dirs and fixture files are populated', () => {
+  const { DATA, PUB } = makeDirs();
+
+  // YouTube
+  wj(DATA, 'videos.json', {
+    items: [{ id: 'yt1', title: 'YT Review', views: 0 }],
+  });
+  wj(DATA, 'videos-pinned.json', { items: [] });
+
+  // Originals
+  wj(DATA, 'originals.json', { items: [{ id: 'o1', title: 'Brand Spot', mp4: '/o1.mp4' }] });
+
+  // Vimeo
+  wj(DATA, 'vimeo-library.json', {
+    episodes: [{ id: 'v1', kind: 'episode', title: 'CRE Ep', plays: 0 }],
+    clips:    [{ id: 'v2', title: 'CRE Clip', plays: 0 }],
+  });
+
+  // News-wire
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'combined-nw.mp4'), MP4_BYTES);
+
+  // Social
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(socDir, 'combined-soc.mp4'), MP4_BYTES);
+
+  // Ad slots (needed for rate-card, not inventory, but write it clean)
+  wj(DATA, 'ad-slots.json', { placements: [] });
+
+  const routes = mount(DATA, PUB);
+  const { status, body } = hit(routes, 'GET /api/ad/inventory');
+
+  assert.equal(status, 200);
+  assert.equal(body.count, 6, '1 yt + 1 orig + 2 vimeo + 1 news-wire + 1 social = 6');
+
+  const bySrc = body.summary.by_source;
+  assert.equal(bySrc['youtube'],   1);
+  assert.equal(bySrc['original'],  1);
+  assert.equal(bySrc['vimeo'],     2);
+  assert.equal(bySrc['news-wire'], 1);
+  assert.equal(bySrc['social'],    1);
+  assert.equal(body.summary.total, 6);
+});
diff --git a/test/adslots/pricing.test.mjs b/test/adslots/pricing.test.mjs
new file mode 100644
index 00000000..558cdf0c
--- /dev/null
+++ b/test/adslots/pricing.test.mjs
@@ -0,0 +1,623 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/pricing.test.mjs  —  TK-10364 PartC (cre-agent)
+//
+// Exhaustive coverage of TWO specific areas in src/ad-system.cjs:
+//
+//   A. suggestPrice(v) heuristic (lines 52-64)
+//      - per-source base prices: vimeo+episode=900, vimeo(other)=450,
+//        youtube=600, original=750, news-wire=250, social=200,
+//        unknown/default=300
+//      - plays bump ladder EXACT boundaries:
+//          plays<=25   → +0
+//          plays=26    → +100  (first hit of >25 tier)
+//          plays=100   → +100  (100 is NOT >100, falls to >25 tier)
+//          plays=101   → +200  (first hit of >100 tier)
+//          plays=500   → +200  (500 is NOT >500, falls to >100 tier)
+//          plays=501   → +400  (first hit of >500 tier)
+//      - rounding: Math.round((base+bump)/50)*50 — all results are $50 multiples
+//      - plays field source per source type:
+//          youtube: v.views (mapped to plays in buildInventory line 92)
+//          vimeo: v.plays (line 124)
+//          original: hardcoded 0 (line 109)
+//
+//   B. Admin price-override PRECEDENCE in buildInventory (lines 149-157)
+//      - ov.price_monthly != null  → item.sponsorship.price_monthly = OVERRIDE
+//        (literal 0 wins because 0 != null is TRUE)
+//      - ov.price_monthly == null (null or missing/undefined) → falls back to
+//        suggestPrice
+//      - item.suggested_price is ALWAYS the heuristic, never the override
+//
+// suggestPrice is closure-private (not exported). All tests reach it through
+// GET /api/ad/inventory and read item.suggested_price (always the heuristic)
+// and item.sponsorship.price_monthly (override when set).
+//
+// Harness: zero new npm deps, ESM .mjs, node:test + node:assert/strict.
+// Matches the fake-Express pattern in the other test files exactly.
+// plays for originals are hardcoded 0 in the source, so youtube/vimeo fixtures
+// are used for arbitrary plays values.
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+// ── fake-Express harness (matches repo style exactly) ────────────────────────
+
+function mount(DATA, PUB) {
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, {
+    DATA,
+    PUB: PUB || path.join(DATA, '__nopub__'),
+    adminOnly: (_q, _r, n) => n && n(),
+  });
+  return routes;
+}
+
+function hit(routes, key, { query = {}, body = {}, params = {} } = {}) {
+  const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+  let out = { status: 200, body: null };
+  const res = {
+    set() { return this; },
+    status(c) { out.status = c; return this; },
+    json(o) { out.body = o; return this; },
+  };
+  routes[key](req, res);
+  return out;
+}
+
+// Creates a throwaway temp DATA dir with optional pre-seeded JSON files.
+function tmpData(files = {}) {
+  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adpricing-'));
+  for (const [f, o] of Object.entries(files)) {
+    fs.writeFileSync(path.join(dir, f), JSON.stringify(o));
+  }
+  return dir;
+}
+
+// Builds a minimal videos.json with one YouTube item at the given plays count.
+// In buildInventory, youtube items map v.views → plays (line 92).
+function ytFixture(id, views) {
+  return { fetched_at: '2026-08-10', items: [{ id, yt: 'YT' + id, title: 'Review ' + id, views }] };
+}
+
+// Builds a minimal vimeo-library.json with one episode at the given plays count.
+function vimeoEpFixture(id, plays) {
+  return { episodes: [{ id, kind: 'episode', title: 'CRE Ep ' + id, plays }] };
+}
+
+// Builds a minimal vimeo-library.json with one clip at the given plays count.
+function vimeoClipFixture(id, plays) {
+  return { clips: [{ id, title: 'CRE Clip ' + id, plays }] };
+}
+
+// Helper: GET /api/ad/inventory, find an item by uid, return it.
+function getItem(routes, uid) {
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+  return body.items.find(i => i.uid === uid);
+}
+
+// ═════════════════════════════════════════════════════════════════════════════
+// SECTION A: suggestPrice — per-source BASE prices (no plays)
+// ═════════════════════════════════════════════════════════════════════════════
+
+// ── A1: youtube base = 600 ───────────────────────────────────────────────────
+
+test('suggestPrice: youtube base = 600, plays=0, no bump, rounds to 600', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('yt0', 0) });
+  const item = getItem(mount(DATA), 'yt-yt0');
+  assert.ok(item, 'youtube item must appear in inventory');
+  assert.equal(item.suggested_price, 600, 'youtube base 600 + 0 plays bump = 600');
+  assert.equal(item.suggested_price % 50, 0, 'result is a $50 multiple');
+});
+
+// ── A2: vimeo episode base = 900 ─────────────────────────────────────────────
+
+test('suggestPrice: vimeo episode base = 900, plays=0, no bump, rounds to 900', () => {
+  const DATA = tmpData({ 'vimeo-library.json': vimeoEpFixture('ep0', 0) });
+  const item = getItem(mount(DATA), 'vimeo-ep0');
+  assert.ok(item, 'vimeo episode item must appear in inventory');
+  assert.equal(item.suggested_price, 900, 'vimeo episode base 900 + 0 plays bump = 900');
+  assert.equal(item.suggested_price % 50, 0, 'result is a $50 multiple');
+});
+
+// ── A3: vimeo clip (non-episode) base = 450 ──────────────────────────────────
+
+test('suggestPrice: vimeo clip base = 450, plays=0, no bump, rounds to 450', () => {
+  const DATA = tmpData({ 'vimeo-library.json': vimeoClipFixture('cl0', 0) });
+  const item = getItem(mount(DATA), 'vimeo-cl0');
+  assert.ok(item, 'vimeo clip item must appear in inventory');
+  assert.equal(item.suggested_price, 450, 'vimeo clip base 450 + 0 plays bump = 450');
+  assert.equal(item.suggested_price % 50, 0, 'result is a $50 multiple');
+});
+
+// ── A4: original base = 750 ─────────────────────────────────────────────────
+
+test('suggestPrice: original base = 750, plays hardcoded 0, rounds to 750', () => {
+  const DATA = tmpData({
+    'originals.json': { items: [{ id: 'orig1', title: 'Brand Spot', mp4: '/orig1.mp4' }] },
+  });
+  const item = getItem(mount(DATA), 'orig-orig1');
+  assert.ok(item, 'original item must appear in inventory');
+  assert.equal(item.suggested_price, 750, 'original base 750 + 0 plays (hardcoded) = 750');
+  assert.equal(item.suggested_price % 50, 0, 'result is a $50 multiple');
+});
+
+// ── A5: news-wire base = 250 (scanDir source — plays always 0) ───────────────
+
+test('suggestPrice: news-wire base = 250, plays=0 (hardcoded), rounds to 250', () => {
+  const PUB = fs.mkdtempSync(path.join(os.tmpdir(), 'adprice-pub-nw-'));
+  const DATA = tmpData({
+    'videos.json':        { items: [] },
+    'videos-pinned.json': { items: [] },
+    'originals.json':     { items: [] },
+    'vimeo-library.json': {},
+    'ad-slots.json':      { placements: [] },
+  });
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'deal-spot.mp4'), Buffer.from('mp4'));
+
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB, adminOnly: (_q, _r, n) => n && n() });
+
+  const item = getItem(routes, 'newswire-deal-spot');
+  assert.ok(item, 'news-wire item must appear in inventory');
+  assert.equal(item.suggested_price, 250, 'news-wire base 250 + 0 plays bump = 250');
+  assert.equal(item.suggested_price % 50, 0, 'result is a $50 multiple');
+});
+
+// ── A6: social base = 200 (scanDir source — plays always 0) ─────────────────
+
+test('suggestPrice: social base = 200, plays=0 (hardcoded), rounds to 200', () => {
+  const PUB = fs.mkdtempSync(path.join(os.tmpdir(), 'adprice-pub-soc-'));
+  const DATA = tmpData({
+    'videos.json':        { items: [] },
+    'videos-pinned.json': { items: [] },
+    'originals.json':     { items: [] },
+    'vimeo-library.json': {},
+    'ad-slots.json':      { placements: [] },
+  });
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(socDir, 'ig-spot.mp4'), Buffer.from('mp4'));
+
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB, adminOnly: (_q, _r, n) => n && n() });
+
+  const item = getItem(routes, 'social-ig-spot');
+  assert.ok(item, 'social item must appear in inventory');
+  assert.equal(item.suggested_price, 200, 'social base 200 + 0 plays bump = 200');
+  assert.equal(item.suggested_price % 50, 0, 'result is a $50 multiple');
+});
+
+// ── A7: unknown/default source base = 300 ────────────────────────────────────
+// The default base (let base=300) is used when v.source matches none of the
+// known values. We cannot reach this through the standard inventory sources
+// (all six sources are typed). However, the heuristic is exercised transitively
+// by the source tests above. We document the base value here via the combined
+// tier table tests below (section B) which use youtube as the plays vehicle.
+// This test checks that the 5 known sources' bases produce the correct ordering:
+// social(200) < news-wire(250) < default(300) < vimeo-clip(450) <
+// youtube(600) < original(750) < vimeo-episode(900).
+
+test('suggestPrice: source base ordering asserted on LIVE inventory values (social<news-wire<youtube<original<vimeo-episode)', () => {
+  // Regression guard on the REAL heuristic output, not hardcoded literals: build a
+  // fixture item per source (incl. a real PUB dir for the filesystem-scan sources
+  // news-wire + social, which __nopub__ can't reach) and read suggested_price back
+  // from GET /api/ad/inventory. If any source base changes in suggestPrice(), the
+  // exact-value asserts below fail — a literal `200 < 250` never would.
+  const DATA = tmpData({
+    'videos.json': ytFixture('yt0', 0),                                            // youtube  → 600
+    'originals.json': { items: [{ id: 'o0', title: 'Brand Spot', mp4: '/o0.mp4' }] }, // original → 750
+    'vimeo-library.json': { episodes: [{ id: 'v0', kind: 'episode', title: 'Ep', plays: 0 }] }, // vimeo ep → 900
+  });
+  const PUB = fs.mkdtempSync(path.join(os.tmpdir(), 'adprice-pub-'));
+  const nwDir = path.join(PUB, 'vid', 'news-wire');
+  const socDir = path.join(PUB, 'social-videos');
+  fs.mkdirSync(nwDir, { recursive: true });
+  fs.mkdirSync(socDir, { recursive: true });
+  fs.writeFileSync(path.join(nwDir, 'nw.mp4'), Buffer.from('mp4'));   // news-wire → 250
+  fs.writeFileSync(path.join(socDir, 'soc.mp4'), Buffer.from('mp4')); // social    → 200
+
+  const { body } = hit(mount(DATA, PUB), 'GET /api/ad/inventory');
+  const price = uid => body.items.find(i => i.uid === uid).suggested_price;
+  const soc = price('social-soc'), nw = price('newswire-nw'), yt = price('yt-yt0');
+  const orig = price('orig-o0'), ep = price('vimeo-v0');
+  // exact per-source bases (pins the numbers to the heuristic)
+  assert.equal(soc, 200, 'social base');
+  assert.equal(nw, 250, 'news-wire base');
+  assert.equal(yt, 600, 'youtube base');
+  assert.equal(orig, 750, 'original base');
+  assert.equal(ep, 900, 'vimeo-episode base');
+  // ordering derived from the LIVE returned values (fails if any base is reordered)
+  assert.ok(soc < nw && nw < yt && yt < orig && orig < ep, 'live source-base ordering holds');
+});
+
+// ═════════════════════════════════════════════════════════════════════════════
+// SECTION B: suggestPrice — plays bump ladder, EXACT boundary assertions
+//
+// Ladder: plays>500 → +400; plays>100 → +200; plays>25 → +100; else → +0
+// All using youtube as the plays vehicle (v.views maps to plays on line 92).
+// Base=600 (youtube). Final = Math.round((600+bump)/50)*50.
+// ═════════════════════════════════════════════════════════════════════════════
+
+// ── B1: plays = 25 (NOT >25, bump=0) ────────────────────────────────────────
+// Math.round((600+0)/50)*50 = 600
+
+test('plays bump: plays=25 is NOT >25, bump=0; youtube → 600', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('p25', 25) });
+  const item = getItem(mount(DATA), 'yt-p25');
+  assert.ok(item);
+  assert.equal(item.suggested_price, 600, 'plays=25: boundary excluded, bump=0, 600+0=600');
+  assert.equal(item.suggested_price % 50, 0);
+});
+
+// ── B2: plays = 26 (first hit of >25 tier, bump=+100) ───────────────────────
+// Math.round((600+100)/50)*50 = 700
+
+test('plays bump: plays=26 IS >25, bump=+100; youtube → 700', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('p26', 26) });
+  const item = getItem(mount(DATA), 'yt-p26');
+  assert.ok(item);
+  assert.equal(item.suggested_price, 700, 'plays=26: first >25 hit, bump=100, 600+100=700');
+  assert.equal(item.suggested_price % 50, 0);
+});
+
+// ── B3: plays = 100 (NOT >100, falls to >25 tier, bump=+100) ────────────────
+// Math.round((600+100)/50)*50 = 700
+
+test('plays bump: plays=100 is NOT >100 (falls to >25 tier), bump=+100; youtube → 700', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('p100', 100) });
+  const item = getItem(mount(DATA), 'yt-p100');
+  assert.ok(item);
+  assert.equal(item.suggested_price, 700,
+    'plays=100: NOT >100 (boundary excluded), falls to >25 tier, bump=100, 600+100=700');
+  assert.equal(item.suggested_price % 50, 0);
+});
+
+// ── B4: plays = 101 (first hit of >100 tier, bump=+200) ─────────────────────
+// Math.round((600+200)/50)*50 = 800
+
+test('plays bump: plays=101 IS >100, bump=+200; youtube → 800', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('p101', 101) });
+  const item = getItem(mount(DATA), 'yt-p101');
+  assert.ok(item);
+  assert.equal(item.suggested_price, 800, 'plays=101: first >100 hit, bump=200, 600+200=800');
+  assert.equal(item.suggested_price % 50, 0);
+});
+
+// ── B5: plays = 500 (NOT >500, falls to >100 tier, bump=+200) ───────────────
+// Math.round((600+200)/50)*50 = 800
+
+test('plays bump: plays=500 is NOT >500 (falls to >100 tier), bump=+200; youtube → 800', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('p500', 500) });
+  const item = getItem(mount(DATA), 'yt-p500');
+  assert.ok(item);
+  assert.equal(item.suggested_price, 800,
+    'plays=500: NOT >500 (boundary excluded), falls to >100 tier, bump=200, 600+200=800');
+  assert.equal(item.suggested_price % 50, 0);
+});
+
+// ── B6: plays = 501 (first hit of >500 tier, bump=+400) ─────────────────────
+// Math.round((600+400)/50)*50 = 1000
+
+test('plays bump: plays=501 IS >500, bump=+400; youtube → 1000', () => {
+  const DATA = tmpData({ 'videos.json': ytFixture('p501', 501) });
+  const item = getItem(mount(DATA), 'yt-p501');
+  assert.ok(item);
+  assert.equal(item.suggested_price, 1000, 'plays=501: first >500 hit, bump=400, 600+400=1000');
+  assert.equal(item.suggested_price % 50, 0);
+});
+
+// ── B7: Full ladder table for vimeo episode (base=900) ───────────────────────
+// Verifies the bump logic is source-agnostic; only the base differs.
+// plays=0→900, plays=26→1000, plays=101→1100, plays=501→1300
+
+test('plays bump: full ladder via vimeo episode (base=900): 0→900, 26→1000, 101→1100, 501→1300', () => {
+  const cases = [
+    [0,   900,  'plays=0 → no bump → 900+0=900'],
+    [26,  1000, 'plays=26 → +100 → 900+100=1000'],
+    [101, 1100, 'plays=101 → +200 → 900+200=1100'],
+    [501, 1300, 'plays=501 → +400 → 900+400=1300'],
+  ];
+  for (const [plays, expected, label] of cases) {
+    const id = 'ep' + plays;
+    const DATA = tmpData({ 'vimeo-library.json': vimeoEpFixture(id, plays) });
+    const item = getItem(mount(DATA), 'vimeo-' + id);
+    assert.ok(item, 'vimeo episode item must appear: ' + label);
+    assert.equal(item.suggested_price, expected, label);
+    assert.equal(item.suggested_price % 50, 0, 'must be $50 multiple: ' + label);
+  }
+});
+
+// ── B8: Full ladder table for vimeo clip (base=450) ──────────────────────────
+// plays=0→450, plays=26→550, plays=101→650, plays=501→850
+
+test('plays bump: full ladder via vimeo clip (base=450): 0→450, 26→550, 101→650, 501→850', () => {
+  const cases = [
+    [0,   450, 'plays=0 → no bump → 450+0=450'],
+    [26,  550, 'plays=26 → +100 → 450+100=550'],
+    [101, 650, 'plays=101 → +200 → 450+200=650'],
+    [501, 850, 'plays=501 → +400 → 450+400=850'],
+  ];
+  for (const [plays, expected, label] of cases) {
+    const id = 'cl' + plays;
+    const DATA = tmpData({ 'vimeo-library.json': vimeoClipFixture(id, plays) });
+    const item = getItem(mount(DATA), 'vimeo-' + id);
+    assert.ok(item, 'vimeo clip item must appear: ' + label);
+    assert.equal(item.suggested_price, expected, label);
+    assert.equal(item.suggested_price % 50, 0, 'must be $50 multiple: ' + label);
+  }
+});
+
+// ── B9: All six bump-boundary plays values via youtube in one fixture ─────────
+// Single inventory call asserting all boundary values simultaneously.
+
+test('plays bump: all six boundary values in one youtube fixture (plays 25/26/100/101/500/501)', () => {
+  const DATA = tmpData({
+    'videos.json': {
+      fetched_at: '2026-08-10',
+      items: [
+        { id: 'b25',  yt: 'YTb25',  title: 'B25',  views: 25  },
+        { id: 'b26',  yt: 'YTb26',  title: 'B26',  views: 26  },
+        { id: 'b100', yt: 'YTb100', title: 'B100', views: 100 },
+        { id: 'b101', yt: 'YTb101', title: 'B101', views: 101 },
+        { id: 'b500', yt: 'YTb500', title: 'B500', views: 500 },
+        { id: 'b501', yt: 'YTb501', title: 'B501', views: 501 },
+      ],
+    },
+  });
+  const routes = mount(DATA);
+  const { body } = hit(routes, 'GET /api/ad/inventory');
+  const byUid = Object.fromEntries(body.items.map(i => [i.uid, i.suggested_price]));
+
+  // Each boundary assertion with exact expected value and clear label
+  assert.equal(byUid['yt-b25'],  600,  'plays=25: NOT >25, bump=0, 600+0=600');
+  assert.equal(byUid['yt-b26'],  700,  'plays=26: IS >25, bump=100, 600+100=700');
+  assert.equal(byUid['yt-b100'], 700,  'plays=100: NOT >100 (falls to >25), bump=100, 600+100=700');
+  assert.equal(byUid['yt-b101'], 800,  'plays=101: IS >100, bump=200, 600+200=800');
+  assert.equal(byUid['yt-b500'], 800,  'plays=500: NOT >500 (falls to >100), bump=200, 600+200=800');
+  assert.equal(byUid['yt-b501'], 1000, 'plays=501: IS >500, bump=400, 600+400=1000');
+
+  // Confirm all are $50 multiples
+  for (const [uid, price] of Object.entries(byUid)) {
+    assert.equal(price % 50, 0, uid + ' suggested_price must be a $50 multiple');
+  }
+});
+
+// ── B10: Rounding — values that are already exact multiples of $50 ────────────
+// Since all (base+bump) combos in the ladder happen to land on exact $50 multiples
+// (200, 250, 300, 450, 550, 600, 650, 700, 750, 800, 850, 900, 1000, 1100, 1300),
+// the rounding is a no-op for all defined combinations. Verify this is preserved.
+
+test('rounding: Math.round((base+bump)/50)*50 — all defined base+bump combos are already $50 multiples', () => {
+  // (base, bump) pairs from the matrix:
+  const pairs = [
+    [200, 0],   // social
+    [250, 0],   // news-wire
+    [300, 0],   // default
+    [300, 100], [300, 200], [300, 400], // default + bumps
+    [450, 0],   [450, 100], [450, 200], [450, 400], // vimeo clip
+    [600, 0],   [600, 100], [600, 200], [600, 400], // youtube
+    [750, 0],   // original (plays hardcoded 0)
+    [900, 0],   [900, 100], [900, 200], [900, 400], // vimeo episode
+  ];
+  for (const [base, bump] of pairs) {
+    const raw = base + bump;
+    const rounded = Math.round(raw / 50) * 50;
+    assert.equal(rounded, raw,
+      `(${base}+${bump}=${raw}) is already a $50 multiple — rounding is a no-op`);
+    assert.equal(rounded % 50, 0, `${rounded} divisible by 50`);
+  }
+});
+
+// ═════════════════════════════════════════════════════════════════════════════
+// SECTION C: Admin price-override PRECEDENCE in buildInventory
+//
+// Line 152: price_monthly: (ov.price_monthly != null ? ov.price_monthly : suggestPrice(it))
+// Line 157: it.suggested_price = suggestPrice(it)  — ALWAYS the heuristic
+// ═════════════════════════════════════════════════════════════════════════════
+
+// ── C1: Explicit non-zero override wins ───────────────────────────────────────
+
+test('override precedence: explicit price_monthly=1234 wins; suggested_price = heuristic (600)', () => {
+  const DATA = tmpData({
+    'videos.json': ytFixture('oa', 0),
+    'ad-state.json': {
+      videos: { 'yt-oa': { status: 'available', price_monthly: 1234, updated_at: null } },
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'yt-oa');
+  assert.ok(item, 'yt-oa must appear in inventory');
+
+  // Override wins for sponsorship.price_monthly
+  assert.equal(item.sponsorship.price_monthly, 1234,
+    'sponsorship.price_monthly must use the state override (1234)');
+
+  // suggested_price is always the heuristic
+  // youtube base=600, views=0 → no bump → 600
+  assert.equal(item.suggested_price, 600,
+    'suggested_price always reflects heuristic (youtube 0 plays → 600), never the override');
+
+  // The two fields must differ
+  assert.notEqual(item.sponsorship.price_monthly, item.suggested_price,
+    'sponsorship.price_monthly and suggested_price must differ when override is set');
+});
+
+// ── C2: Literal zero override wins (0 != null is TRUE) ───────────────────────
+// This is the critical "0 wins" case specified in the task.
+// The condition `ov.price_monthly != null` — with loose inequality —
+// treats 0 as NOT null (0 != null is TRUE), so price_monthly=0 is a valid
+// explicit override that must win over the heuristic.
+
+test('override precedence: price_monthly=0 (literal zero) wins because 0 != null is TRUE', () => {
+  const DATA = tmpData({
+    'videos.json': ytFixture('ozero', 500), // high plays so heuristic > 0
+    'ad-state.json': {
+      videos: { 'yt-ozero': { status: 'available', price_monthly: 0, updated_at: null } },
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'yt-ozero');
+  assert.ok(item, 'yt-ozero must appear in inventory');
+
+  // 0 wins as the override (0 != null is TRUE in the source condition)
+  assert.equal(item.sponsorship.price_monthly, 0,
+    'price_monthly=0 wins as override — 0 != null is TRUE');
+
+  // Heuristic: youtube base=600, plays=500 (NOT >500) → +200 → 800
+  assert.equal(item.suggested_price, 800,
+    'suggested_price is the heuristic (youtube, plays=500 falls to >100 tier, 600+200=800)');
+
+  // They must differ: 0 vs 800
+  assert.notEqual(item.sponsorship.price_monthly, item.suggested_price,
+    'zero override vs heuristic 800 — they must differ');
+});
+
+// ── C3: null override falls back to suggestPrice ─────────────────────────────
+
+test('override precedence: price_monthly=null falls back to suggestPrice', () => {
+  const DATA = tmpData({
+    'videos.json': ytFixture('onull', 0),
+    'ad-state.json': {
+      videos: { 'yt-onull': { status: 'available', price_monthly: null, updated_at: null } },
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'yt-onull');
+  assert.ok(item, 'yt-onull must appear in inventory');
+
+  // null override → falls back to heuristic
+  // youtube base=600, plays=0 → 600
+  assert.equal(item.sponsorship.price_monthly, 600,
+    'null price_monthly falls back to suggestPrice (youtube 0 plays → 600)');
+
+  // suggested_price equals sponsorship.price_monthly when no override
+  assert.equal(item.suggested_price, item.sponsorship.price_monthly,
+    'suggested_price and sponsorship.price_monthly both equal heuristic when override is null');
+});
+
+// ── C4: Missing price_monthly key (undefined) falls back to suggestPrice ──────
+
+test('override precedence: missing price_monthly key (undefined) falls back to suggestPrice', () => {
+  const DATA = tmpData({
+    'videos.json': ytFixture('oundef', 0),
+    'ad-state.json': {
+      videos: { 'yt-oundef': { status: 'reserved', updated_at: null } },
+      // price_monthly key entirely absent → ov.price_monthly === undefined
+      // undefined == null is TRUE → falls back to suggestPrice
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'yt-oundef');
+  assert.ok(item, 'yt-oundef must appear in inventory');
+
+  // undefined == null is TRUE → falls back to heuristic
+  assert.equal(item.sponsorship.price_monthly, 600,
+    'missing price_monthly key falls back to suggestPrice (youtube 0 plays → 600)');
+
+  assert.equal(item.suggested_price, 600,
+    'suggested_price also reflects heuristic');
+});
+
+// ── C5: No state entry at all (ov = {}) falls back to suggestPrice ────────────
+
+test('override precedence: no state entry for uid (ov={}) falls back to suggestPrice', () => {
+  const DATA = tmpData({
+    'videos.json': ytFixture('onostate', 0),
+    // ad-state.json absent → loadState returns {videos:{}} → ov = {}
+  });
+  const item = getItem(mount(DATA), 'yt-onostate');
+  assert.ok(item, 'yt-onostate must appear in inventory');
+
+  assert.equal(item.sponsorship.price_monthly, 600,
+    'no state entry → ov={} → price_monthly falls back to suggestPrice (youtube 0 plays → 600)');
+  assert.equal(item.suggested_price, 600, 'heuristic: youtube 0 plays → 600');
+});
+
+// ── C6: suggested_price is ALWAYS the heuristic, never the override ───────────
+// Even when the override is an unusual value ($9999), suggested_price stays heuristic.
+
+test('override precedence: suggested_price is ALWAYS the heuristic (never the override)', () => {
+  const DATA = tmpData({
+    'videos.json': ytFixture('oalways', 101), // plays=101 → >100 tier → +200 → 800
+    'ad-state.json': {
+      videos: { 'yt-oalways': { status: 'available', price_monthly: 9999, updated_at: null } },
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'yt-oalways');
+  assert.ok(item, 'yt-oalways must appear in inventory');
+
+  // Override wins for sponsorship
+  assert.equal(item.sponsorship.price_monthly, 9999,
+    'sponsorship.price_monthly uses the 9999 override');
+
+  // suggested_price: youtube base=600, plays=101 → IS >100, bump=+200 → 800
+  assert.equal(item.suggested_price, 800,
+    'suggested_price is always heuristic (youtube, plays=101, >100 tier, 600+200=800)');
+
+  // They must never be equal in this case
+  assert.notEqual(item.suggested_price, item.sponsorship.price_monthly,
+    'suggested_price (heuristic) never equals the override');
+});
+
+// ── C7: Override with vimeo episode — heuristic still tracks source correctly ─
+
+test('override precedence: vimeo episode with override; suggested_price = episode heuristic', () => {
+  const DATA = tmpData({
+    'vimeo-library.json': { episodes: [{ id: 'oep', kind: 'episode', title: 'CRE Ep', plays: 501 }] },
+    'ad-state.json': {
+      videos: { 'vimeo-oep': { status: 'available', price_monthly: 500, updated_at: null } },
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'vimeo-oep');
+  assert.ok(item, 'vimeo-oep must appear in inventory');
+
+  // Override: 500
+  assert.equal(item.sponsorship.price_monthly, 500,
+    'sponsorship.price_monthly uses override (500)');
+
+  // Heuristic: vimeo episode base=900, plays=501 IS >500 → bump=+400 → 1300
+  assert.equal(item.suggested_price, 1300,
+    'suggested_price is heuristic (vimeo episode, plays=501, >500 tier, 900+400=1300)');
+});
+
+// ── C8: price_monthly=0 override plus suggested_price shows both fields together ─
+// Explicitly confirms the dual-field contract: sponsorship.price_monthly=0 (override),
+// suggested_price=heuristic. Buyers see the deal price; admin sees the heuristic.
+
+test('override precedence: price_monthly=0 and suggested_price=heuristic coexist on same item', () => {
+  // vimeo clip, plays=26 → >25 tier → +100 → 450+100=550
+  const DATA = tmpData({
+    'vimeo-library.json': { clips: [{ id: 'ozeroep', title: 'CRE Clip', plays: 26 }] },
+    'ad-state.json': {
+      videos: { 'vimeo-ozeroep': { status: 'available', price_monthly: 0, updated_at: null } },
+      updated_at: null,
+    },
+  });
+  const item = getItem(mount(DATA), 'vimeo-ozeroep');
+  assert.ok(item, 'vimeo-ozeroep must appear in inventory');
+
+  assert.equal(item.sponsorship.price_monthly, 0,
+    'price_monthly=0 override wins (0 != null is TRUE in the source condition)');
+  assert.equal(item.suggested_price, 550,
+    'suggested_price = heuristic (vimeo clip, plays=26, >25 tier, 450+100=550)');
+
+  // Both fields present and distinct
+  assert.notEqual(item.sponsorship.price_monthly, item.suggested_price,
+    '0 (override) differs from 550 (heuristic)');
+});
diff --git a/test/adslots/reserve-lifecycle.test.mjs b/test/adslots/reserve-lifecycle.test.mjs
new file mode 100644
index 00000000..897a00c5
--- /dev/null
+++ b/test/adslots/reserve-lifecycle.test.mjs
@@ -0,0 +1,485 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/reserve-lifecycle.test.mjs  —  TK-10364 PartB (cre-agent)
+//
+// Covers POST /api/ad/reserve (src/ad-system.cjs lines 213-241) side-effects and
+// input normalisation NOT yet covered by the existing three test files
+// (inventory.test.mjs, reserve-state.test.mjs, inventory-sources.test.mjs,
+// admin.test.mjs, ratecard.test.mjs).
+//
+// Specifically adds:
+//   1. target_name truncation (.slice(0,160))
+//   2. advertiser truncation (.slice(0,120))
+//   3. months='abc' non-numeric string → 1 (NaN fallback)
+//   4. months=-5 below-minimum non-zero parseInt → 1 (max(1,...) clamp)
+//   5. months=1 at lower boundary (no clamp, stored as 1)
+//   6. months=24 at upper boundary (no clamp, stored as 24)
+//   7. APPEND semantics: two sequential reserves both appear in jsonl (not overwrite)
+//   8. Recorded row carries 'at' ISO timestamp and 'ip' field
+//   9. x-forwarded-for header extraction: first-IP + trim from multi-value header
+//  10. req.ip fallback when x-forwarded-for is absent
+//  11. quoted_monthly=0 stores as null (Number(0)||null — falsy-zero documented behavior)
+//  12. fresh-target video: st.videos[id] starts absent → entry created with status=reserved
+//  13. fresh-target placement: st.creatives[id] starts absent → entry created with status=reserved
+//  14. Double-reserve on same video target: second reserve still shows reserved (idempotent)
+//  15. kind defaults to 'placement' for any value other than 'video'
+//  16. start field length truncated at 7 chars in recorded row
+//
+// Harness: zero-dep, fake-Express, synchronous — identical to repo style.
+// Each test uses a fresh temp DATA dir for full isolation.
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+// ── minimal fake-Express harness (matches repo style exactly) ────────────────
+function mount(DATA) {
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB: path.join(DATA, '__nopub__'), adminOnly: (_q, _r, n) => n && n() });
+  return routes;
+}
+
+// hit() accepts an optional `headers` map so x-forwarded-for tests can supply it.
+function hit(routes, key, { query = {}, body = {}, params = {}, headers = {}, ip = '127.0.0.1' } = {}) {
+  const req = { query, body, params, headers, ip };
+  let out = { status: 200, body: null };
+  const res = {
+    set() { return this; },
+    status(c) { out.status = c; return this; },
+    json(o) { out.body = o; return this; },
+  };
+  routes[key](req, res);
+  return out;
+}
+
+// Throwaway temp DATA dir, optionally pre-seeding named JSON files.
+function tmpData(files = {}) {
+  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adlc-'));
+  for (const [f, o] of Object.entries(files)) {
+    fs.writeFileSync(path.join(dir, f), JSON.stringify(o));
+  }
+  return dir;
+}
+
+// Read and parse the first (or only) line of the reservations jsonl.
+function readReservations(DATA) {
+  const raw = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  return raw.split('\n').filter(Boolean).map(l => JSON.parse(l));
+}
+
+// Minimal valid reserve body — video kind on a fresh target.
+const BASE_VIDEO = {
+  email: 'buyer@example.com',
+  target_id: 'yt-v1',
+  kind: 'video',
+  advertiser: 'ACME Corp',
+  months: 3,
+};
+
+// Minimal valid reserve body — placement kind.
+const BASE_PLACEMENT = {
+  email: 'buyer@example.com',
+  target_id: 'home-banner',
+  kind: 'placement',
+  advertiser: 'ACME Corp',
+  months: 1,
+};
+
+// ── 1. target_name truncation ─────────────────────────────────────────────────
+// Line 220: target_name: esc(b.target_name).slice(0, 160)
+test('field truncation: target_name longer than 160 chars is sliced to 160', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const longName = 'N'.repeat(200);
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, target_name: longName },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.target_name.length, 160, 'target_name sliced to 160 chars');
+  assert.equal(rec.target_name, 'N'.repeat(160));
+});
+
+test('field truncation: target_name exactly 160 chars is stored unchanged', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const exactName = 'X'.repeat(160);
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, target_name: exactName },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.target_name.length, 160);
+  assert.equal(rec.target_name, exactName);
+});
+
+// ── 2. advertiser truncation ──────────────────────────────────────────────────
+// Line 223: advertiser: esc(b.advertiser).slice(0, 120)
+test('field truncation: advertiser longer than 120 chars is sliced to 120', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const longAdv = 'A'.repeat(150);
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, advertiser: longAdv },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.advertiser.length, 120, 'advertiser sliced to 120 chars');
+});
+
+test('field truncation: advertiser exactly 120 chars is stored unchanged', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const exactAdv = 'B'.repeat(120);
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, advertiser: exactAdv },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.advertiser.length, 120);
+  assert.equal(rec.advertiser, exactAdv);
+});
+
+// ── 3. months='abc' non-numeric string → 1 ───────────────────────────────────
+// parseInt('abc', 10) = NaN; NaN || 1 = 1; Math.max(1, Math.min(24, 1)) = 1
+test('months clamp: non-numeric string "abc" stores as 1 (NaN fallback)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, months: 'abc' },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.months, 1, '"abc" parses as NaN, falls back to 1');
+});
+
+// ── 4. months=-5 below-minimum non-zero parseInt → 1 ─────────────────────────
+// parseInt(-5, 10) = -5; -5 is truthy so -5 || 1 = -5; Math.max(1, Math.min(24, -5)) = 1
+test('months clamp: months=-5 (below minimum, non-zero) clamps to 1 via Math.max', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, months: -5 },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.months, 1, 'negative months clamps to 1 via Math.max(1,...)');
+});
+
+// ── 5. months=1 at lower boundary ────────────────────────────────────────────
+test('months clamp: months=1 at lower boundary stores as 1 (no clamp applied)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, months: 1 },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.months, 1, 'months=1 is the minimum, stored as-is');
+});
+
+// ── 6. months=24 at upper boundary ───────────────────────────────────────────
+test('months clamp: months=24 at upper boundary stores as 24 (no clamp applied)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, months: 24 },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.months, 24, 'months=24 is the maximum, stored as-is');
+});
+
+// ── 7. APPEND semantics: two sequential reserves both appear in jsonl ─────────
+// Line 233: fs.appendFileSync — must APPEND not overwrite.
+// After two reserve calls on the SAME target, both rows are in the file.
+test('append semantics: two sequential reserves both appear in ad-reservations.jsonl (not overwritten)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, advertiser: 'First Buyer' },
+  });
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, advertiser: 'Second Buyer' },
+  });
+
+  const rows = readReservations(DATA);
+  assert.equal(rows.length, 2, 'both rows must be present — appendFileSync, not writeFileSync');
+  assert.equal(rows[0].advertiser, 'First Buyer', 'first row is the first reserve');
+  assert.equal(rows[1].advertiser, 'Second Buyer', 'second row is the second reserve');
+});
+
+test('append semantics: three reserves on the same target produce three jsonl rows', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  for (let i = 1; i <= 3; i++) {
+    hit(routes, 'POST /api/ad/reserve', {
+      body: { ...BASE_VIDEO, advertiser: `Buyer${i}` },
+    });
+  }
+
+  const rows = readReservations(DATA);
+  assert.equal(rows.length, 3, 'three separate reservation records accumulated');
+  assert.deepEqual(rows.map(r => r.advertiser), ['Buyer1', 'Buyer2', 'Buyer3']);
+});
+
+// ── 8. Recorded row carries 'at' ISO timestamp and 'ip' field ─────────────────
+// Line 217: at: nowISO() — must be a valid ISO 8601 string.
+// Line 229: ip: (req.headers['x-forwarded-for'] || req.ip || '').toString().split(',')[0].trim()
+test('recorded row: at field is a valid ISO 8601 timestamp', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const before = Date.now();
+  hit(routes, 'POST /api/ad/reserve', { body: { ...BASE_VIDEO } });
+  const after = Date.now();
+
+  const [rec] = readReservations(DATA);
+  assert.ok(typeof rec.at === 'string' && rec.at.length > 0, 'at field is a non-empty string');
+  const ts = new Date(rec.at).getTime();
+  assert.ok(!Number.isNaN(ts), 'at parses as a valid date');
+  assert.ok(ts >= before && ts <= after + 100, 'at timestamp is within the test window');
+});
+
+test('recorded row: ip field is present and non-empty for default req.ip', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', { body: { ...BASE_VIDEO }, ip: '10.0.0.1' });
+
+  const [rec] = readReservations(DATA);
+  assert.ok(typeof rec.ip === 'string', 'ip field is a string');
+  assert.equal(rec.ip, '10.0.0.1', 'ip extracted from req.ip when no x-forwarded-for header');
+});
+
+// ── 9. x-forwarded-for header extraction ─────────────────────────────────────
+// Line 229: ip: (req.headers['x-forwarded-for'] || req.ip || '').toString().split(',')[0].trim()
+test('ip extraction: x-forwarded-for single value stored as-is (trimmed)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO },
+    headers: { 'x-forwarded-for': '203.0.113.42' },
+    ip: '127.0.0.1',
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.ip, '203.0.113.42', 'x-forwarded-for single IP stored correctly');
+});
+
+test('ip extraction: x-forwarded-for with multiple IPs stores only the first (client IP)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  // Proxy chains produce comma-separated values: client, proxy1, proxy2
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO },
+    headers: { 'x-forwarded-for': '203.0.113.7, 10.1.2.3, 10.4.5.6' },
+    ip: '10.99.0.1',
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.ip, '203.0.113.7', 'only the first IP from x-forwarded-for chain is stored');
+});
+
+test('ip extraction: x-forwarded-for with leading/trailing spaces around first IP is trimmed', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO },
+    headers: { 'x-forwarded-for': '  198.51.100.1 , 10.0.0.1' },
+    ip: '127.0.0.1',
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.ip, '198.51.100.1', 'first IP is trimmed of surrounding spaces');
+});
+
+// ── 10. req.ip fallback ───────────────────────────────────────────────────────
+test('ip extraction: falls back to req.ip when x-forwarded-for header is absent', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  // headers empty — no x-forwarded-for
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO },
+    headers: {},
+    ip: '192.0.2.55',
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.ip, '192.0.2.55', 'req.ip used as fallback when no forwarded header');
+});
+
+// ── 11. quoted_monthly=0 stores as null ───────────────────────────────────────
+// Line 228: quoted_monthly: Number(b.quoted_monthly) || null
+// Number(0) = 0; 0 || null = null. Zero is a falsy value so it collapses to null.
+// This is a documented behavioral characteristic: a price of $0/month cannot be
+// distinguished from "no price quoted" in the stored row. Captured as a regression
+// test so any future change to this line must be deliberate.
+// KNOWN-BUG GUARD (not a contract): if $0/month should ever be a real quote, change
+// ad-system.cjs line ~228 to `b.quoted_monthly != null ? Number(b.quoted_monthly) : null`
+// and flip this assertion to expect 0. Until then, null is the intentional current output.
+test('quoted_monthly=0 stores as null: Number(0)||null is null (falsy-zero documented behavior)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, quoted_monthly: 0 },
+  });
+
+  const [rec] = readReservations(DATA);
+  // Number(0) || null === null — zero is falsy in JS; this IS the current behavior.
+  assert.equal(rec.quoted_monthly, null,
+    'quoted_monthly=0 stores as null due to Number(0)||null falsy coercion (regression guard)');
+});
+
+// ── 12. fresh-target video: st.videos[id] starts absent ──────────────────────
+// Line 237: st.videos = st.videos || {}; const cur = st.videos[rec.target_id] || {};
+// When the target has never been seen, there is no prior entry. After reserve,
+// the entry is created with status='reserved'.
+test('fresh video target: reserve creates the st.videos[id] entry from scratch', () => {
+  // No pre-seeded ad-state.json at all — file does not exist.
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const { status } = hit(routes, 'POST /api/ad/reserve', { body: { ...BASE_VIDEO } });
+  assert.equal(status, 200);
+
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  assert.ok(state.videos, 'videos key exists after first reserve');
+  const entry = state.videos[BASE_VIDEO.target_id];
+  assert.ok(entry, 'entry for target_id exists');
+  assert.equal(entry.status, 'reserved', 'freshly created entry has status=reserved');
+  assert.ok(typeof entry.updated_at === 'string', 'updated_at is set on fresh entry');
+});
+
+// ── 13. fresh-target placement: st.creatives[id] starts absent ───────────────
+// Line 238 else-branch: st.creatives = st.creatives || {}; const cur = st.creatives[id] || {}
+test('fresh placement target: reserve creates the st.creatives[id] entry from scratch', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const { status } = hit(routes, 'POST /api/ad/reserve', { body: { ...BASE_PLACEMENT } });
+  assert.equal(status, 200);
+
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  assert.ok(state.creatives, 'creatives key exists after first placement reserve');
+  const entry = state.creatives[BASE_PLACEMENT.target_id];
+  assert.ok(entry, 'entry for placement target_id exists');
+  assert.equal(entry.status, 'reserved', 'freshly created placement entry has status=reserved');
+});
+
+// ── 14. Double-reserve on same video target: second stays reserved ────────────
+// Pre-seeded as 'available'; first reserve → 'reserved'; second reserve → stays 'reserved'.
+// The guard is `if (cur.status !== 'sold')` — reserved is not 'sold', so the second
+// reserve re-sets status='reserved' (idempotent; updated_at refreshes each time).
+test('double-reserve on same video target: second reserve leaves status=reserved (idempotent)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', { body: { ...BASE_VIDEO, advertiser: 'First' } });
+  hit(routes, 'POST /api/ad/reserve', { body: { ...BASE_VIDEO, advertiser: 'Second' } });
+
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  assert.equal(state.videos[BASE_VIDEO.target_id].status, 'reserved',
+    'double-reserve does not escalate or corrupt status — stays reserved');
+
+  // Both inquiry rows still appended to the jsonl (the state guard only stops downgrade,
+  // not the recording of the inquiry itself).
+  const rows = readReservations(DATA);
+  assert.equal(rows.length, 2, 'both inquiries are recorded even when target is already reserved');
+});
+
+// ── 15. kind defaults to 'placement' for any value other than 'video' ─────────
+// Line 218: kind: b.kind === 'video' ? 'video' : 'placement'
+// Any value that is not exactly the string 'video' routes to the creatives branch.
+test('kind normalisation: kind="VIDEO" (wrong case) routes to placement, not video', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, kind: 'VIDEO' }, // uppercase — not === 'video'
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.kind, 'placement', 'kind is case-sensitive: "VIDEO" != "video" → placement');
+
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  // must have written to creatives, NOT videos
+  assert.ok(!state.videos || !state.videos[BASE_VIDEO.target_id],
+    'wrong-case kind writes to creatives (not videos) branch');
+  assert.ok(state.creatives && state.creatives[BASE_VIDEO.target_id],
+    'entry created in creatives for wrong-case kind');
+});
+
+test('kind normalisation: kind undefined defaults to "placement"', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  const body = { ...BASE_VIDEO };
+  delete body.kind;
+  hit(routes, 'POST /api/ad/reserve', { body });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.kind, 'placement', 'missing kind defaults to placement');
+});
+
+test('kind normalisation: kind="banner" (non-video non-empty string) routes to placement', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_PLACEMENT, kind: 'banner' },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.kind, 'placement', '"banner" kind normalises to placement');
+});
+
+// ── 16. start field length truncated at 7 chars ───────────────────────────────
+// Line 222: start: esc(b.start).slice(0, 7)
+// A full ISO date "2026-08-15" has 10 chars; slice(0,7) yields "2026-08".
+test('field truncation: start longer than 7 chars is sliced to 7 (YYYY-MM)', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, start: '2026-08-15' }, // 10-char ISO date
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.start.length, 7, 'start sliced to 7 chars');
+  assert.equal(rec.start, '2026-08', 'full ISO date truncated to YYYY-MM');
+});
+
+test('field truncation: start exactly 7 chars (YYYY-MM) is stored unchanged', () => {
+  const DATA = tmpData();
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...BASE_VIDEO, start: '2026-09' },
+  });
+
+  const [rec] = readReservations(DATA);
+  assert.equal(rec.start, '2026-09', 'YYYY-MM start stored as-is');
+});
diff --git a/test/adslots/reserve-state.test.mjs b/test/adslots/reserve-state.test.mjs
new file mode 100644
index 00000000..b19e932e
--- /dev/null
+++ b/test/adslots/reserve-state.test.mjs
@@ -0,0 +1,369 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/reserve-state.test.mjs  —  TK-10364 LaneB
+//
+// Covers the reserve() soft-hold state machine (src/ad-system.cjs lines 213-241)
+// and the sponsorship price_monthly override path (lines 146-160).
+//
+// Five suites:
+//   1. video reserve soft-hold — writes ad-state.json + shows in GET inventory
+//   2. sold-is-not-downgraded (video branch) — pre-sold slot stays sold
+//   3. placement reserve soft-hold — kind:'placement' writes creatives branch
+//   4. sold-is-not-downgraded (placement branch)
+//   5. field clamping — months 99->24, missing->1; quoted_monthly non-numeric->null;
+//      target_id slice; note slice
+//   6. price_monthly override in inventory sponsorship overlay
+//
+// Harness: zero-dep, fake-Express, synchronous — matches repo style.
+// PUB is a nonexistent subdir of tmpData so scanDir returns empty (no .mp4s).
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+// ── minimal fake-Express harness ─────────────────────────────────────────────
+function mount(DATA) {
+  const routes = {};
+  const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; };
+  const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+  mountAdSystem(app, { DATA, PUB: path.join(DATA, '__nopub__'), adminOnly: (_q, _r, n) => n && n() });
+  return routes;
+}
+
+function hit(routes, key, { query = {}, body = {}, params = {} } = {}) {
+  const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+  let out = { status: 200, body: null };
+  const res = {
+    set() { return this; },
+    status(c) { out.status = c; return this; },
+    json(o) { out.body = o; return this; },
+  };
+  routes[key](req, res);
+  return out;
+}
+
+// Creates a throwaway temp DATA dir, optionally pre-seeding named JSON files.
+function tmpData(files = {}) {
+  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'adres-'));
+  for (const [f, o] of Object.entries(files)) {
+    fs.writeFileSync(path.join(dir, f), JSON.stringify(o));
+  }
+  return dir;
+}
+
+// Minimal videos.json fixture so inventory always contains 'yt-a1'.
+const BASE_VIDEOS = {
+  fetched_at: '2026-08-10',
+  items: [{ id: 'a1', yt: 'YTa1', title: 'Review A', views: 0 }],
+};
+
+// Minimal valid reserve body for yt-a1 (video kind).
+const VALID_VIDEO_BODY = {
+  email: 'buyer@example.com',
+  target_id: 'yt-a1',
+  kind: 'video',
+  advertiser: 'ACME Corp',
+  months: 3,
+};
+
+// Minimal valid reserve body for a placement kind.
+const VALID_PLACEMENT_BODY = {
+  email: 'buyer@example.com',
+  target_id: 'home-leaderboard',
+  kind: 'placement',
+  advertiser: 'ACME Corp',
+  months: 1,
+};
+
+// ── 1. video reserve soft-hold ────────────────────────────────────────────────
+// Lines 236-237: if kind==='video' and cur.status!=='sold', set
+// status='reserved' and updated_at, write ad-state.json.
+// Verified BOTH by reading the state file and by the follow-up GET /api/ad/inventory.
+
+test('video reserve: ad-state.json written with status=reserved and updated_at', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  const before = Date.now();
+  const { status, body } = hit(routes, 'POST /api/ad/reserve', { body: VALID_VIDEO_BODY });
+  assert.equal(status, 200, 'reserve returns 200');
+  assert.equal(body.ok, true);
+
+  // Read ad-state.json directly to confirm the write.
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  const slot = state.videos && state.videos['yt-a1'];
+  assert.ok(slot, 'videos[yt-a1] entry exists in ad-state.json');
+  assert.equal(slot.status, 'reserved', 'status is reserved');
+  assert.ok(typeof slot.updated_at === 'string' && slot.updated_at.length > 0, 'updated_at is set');
+  // updated_at must be an ISO timestamp on or after the call
+  const ts = new Date(slot.updated_at).getTime();
+  assert.ok(!Number.isNaN(ts), 'updated_at parses as a date');
+  assert.ok(ts >= before, 'updated_at is not in the past relative to the test');
+});
+
+test('video reserve: GET /api/ad/inventory reflects sponsorship.status === reserved', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  // Perform the reserve.
+  hit(routes, 'POST /api/ad/reserve', { body: VALID_VIDEO_BODY });
+
+  // Follow-up inventory call.
+  const inv = hit(routes, 'GET /api/ad/inventory').body;
+  const item = inv.items.find(i => i.uid === 'yt-a1');
+  assert.ok(item, 'yt-a1 appears in inventory');
+  assert.equal(item.sponsorship.status, 'reserved', 'inventory sponsorship.status is reserved');
+});
+
+// ── 2. sold is NOT downgraded — video branch ─────────────────────────────────
+// Line 237 guard: `if (cur.status !== 'sold')` — a sold slot must stay sold.
+
+test('video reserve: pre-sold slot stays sold after a reserve attempt', () => {
+  const DATA = tmpData({
+    'videos.json': BASE_VIDEOS,
+    'ad-state.json': {
+      videos: { 'yt-a1': { status: 'sold', updated_at: '2026-01-01T00:00:00.000Z' } },
+      updated_at: null,
+    },
+  });
+  const routes = mount(DATA);
+
+  const { status, body } = hit(routes, 'POST /api/ad/reserve', { body: VALID_VIDEO_BODY });
+  // The route still returns 200 (reservation intent recorded) — the guard only
+  // prevents the soft-hold STATE from being downgraded.
+  assert.equal(status, 200, 'still returns 200 for a sold slot');
+  assert.equal(body.ok, true);
+
+  // Confirm state file still shows sold.
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  assert.equal(state.videos['yt-a1'].status, 'sold', 'status remains sold (not clobbered by reserve)');
+  assert.equal(state.videos['yt-a1'].updated_at, '2026-01-01T00:00:00.000Z', 'updated_at unchanged for sold slot');
+});
+
+// ── 3. placement reserve soft-hold ───────────────────────────────────────────
+// Line 238 else-branch: kind!='video' → st.creatives[target_id].status = 'reserved'.
+// NOTE: the placement branch does NOT set cur.updated_at (unlike the video branch
+// which does). This is an observable behavioral difference — documented here, not
+// flagged as a bug since the source was likely intentional (placements are
+// creatives-config objects, not time-sensitive soft-holds in the same sense).
+
+test('placement reserve: ad-state.json written with creatives[home-leaderboard].status=reserved', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  const { status, body } = hit(routes, 'POST /api/ad/reserve', { body: VALID_PLACEMENT_BODY });
+  assert.equal(status, 200);
+  assert.equal(body.ok, true);
+
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  const slot = state.creatives && state.creatives['home-leaderboard'];
+  assert.ok(slot, 'creatives[home-leaderboard] entry exists');
+  assert.equal(slot.status, 'reserved', 'placement status is reserved');
+  // Placement branch intentionally does NOT set updated_at (line 238 vs 237).
+  // We assert the absence here so any future source change that adds it will
+  // require this test to be deliberately updated.
+  assert.equal(slot.updated_at, undefined, 'placement branch: updated_at NOT set (by design — line 238)');
+});
+
+test('placement reserve: GET /api/ad/inventory does NOT expose placements (creatives are rate-card items, not video inventory)', () => {
+  // The GET /api/ad/inventory endpoint returns only video-source items via
+  // buildInventory(). Placement creatives live in a separate creatives subtree
+  // in ad-state.json and are not surfaced through this endpoint. This test
+  // documents that contract so it's explicit rather than implicit.
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', { body: VALID_PLACEMENT_BODY });
+
+  const inv = hit(routes, 'GET /api/ad/inventory').body;
+  // None of the items should have uid 'home-leaderboard'
+  const placementItem = inv.items.find(i => i.uid === 'home-leaderboard');
+  assert.equal(placementItem, undefined, 'placement creative does not appear in video inventory');
+});
+
+// ── 4. sold is NOT downgraded — placement branch ─────────────────────────────
+
+test('placement reserve: pre-sold placement slot stays sold', () => {
+  const DATA = tmpData({
+    'videos.json': BASE_VIDEOS,
+    'ad-state.json': {
+      creatives: { 'home-leaderboard': { status: 'sold' } },
+      updated_at: null,
+    },
+  });
+  const routes = mount(DATA);
+
+  const { status } = hit(routes, 'POST /api/ad/reserve', { body: VALID_PLACEMENT_BODY });
+  assert.equal(status, 200);
+
+  const state = JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+  assert.equal(state.creatives['home-leaderboard'].status, 'sold', 'placement status stays sold (not clobbered)');
+});
+
+// ── 5. field clamping ─────────────────────────────────────────────────────────
+// Line 221: months = Math.max(1, Math.min(24, parseInt(b.months, 10) || 1))
+//   - 99 → min(24, 99) = 24
+//   - 0  → parseInt(0)=0 → 0||1=1 → max(1,min(24,1))=1
+//   - missing/undefined → parseInt(undefined,10)=NaN → NaN||1=1 → 1
+//   - 'abc' → NaN → 1
+// Line 228: quoted_monthly = Number(b.quoted_monthly) || null
+//   - non-numeric string → NaN → null
+//   - numeric string '500' → 500
+// Line 219: target_id sliced to 80 chars
+// Line 227: note sliced to 600 chars
+
+test('field clamping: months=99 is stored as 24 (upper clamp)', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...VALID_VIDEO_BODY, months: 99 },
+  });
+
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.months, 24, 'months 99 clamped to 24');
+});
+
+test('field clamping: months missing defaults to 1 (lower clamp / NaN fallback)', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  const body = { ...VALID_VIDEO_BODY };
+  delete body.months;
+  hit(routes, 'POST /api/ad/reserve', { body });
+
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.months, 1, 'missing months defaults to 1');
+});
+
+test('field clamping: months=0 clamps to 1 (NaN-or-zero fallback)', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...VALID_VIDEO_BODY, months: 0 },
+  });
+
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.months, 1, 'months=0 clamps to 1 (0||1 fallback then max(1,...))');
+});
+
+test('field clamping: quoted_monthly non-numeric string stores as null', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...VALID_VIDEO_BODY, quoted_monthly: 'not-a-number' },
+  });
+
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.quoted_monthly, null, 'non-numeric quoted_monthly stores as null');
+});
+
+test('field clamping: quoted_monthly numeric string stores as number', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...VALID_VIDEO_BODY, quoted_monthly: '750' },
+  });
+
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.quoted_monthly, 750, 'numeric string quoted_monthly coerced to number');
+});
+
+test('field clamping: target_id longer than 80 chars is sliced to 80', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  const long_id = 'x'.repeat(100);
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...VALID_VIDEO_BODY, target_id: long_id },
+  });
+
+  // The reserve will succeed (jsonl appended). Check the stored target_id.
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.target_id.length, 80, 'target_id sliced to 80 chars');
+});
+
+test('field clamping: note longer than 600 chars is sliced to 600', () => {
+  const DATA = tmpData({ 'videos.json': BASE_VIDEOS });
+  const routes = mount(DATA);
+
+  const long_note = 'n'.repeat(700);
+  hit(routes, 'POST /api/ad/reserve', {
+    body: { ...VALID_VIDEO_BODY, note: long_note },
+  });
+
+  const line = fs.readFileSync(path.join(DATA, 'ad-reservations.jsonl'), 'utf8').trim();
+  const rec = JSON.parse(line);
+  assert.equal(rec.note.length, 600, 'note sliced to 600 chars');
+});
+
+// ── 6. price_monthly override in inventory sponsorship overlay ────────────────
+// Line 152: price_monthly: (ov.price_monthly != null ? ov.price_monthly : suggestPrice(it))
+// Pre-seed ad-state.json with a custom price_monthly on yt-a1; confirm the
+// inventory response uses that value as sponsorship.price_monthly, while
+// suggested_price still reflects the heuristic.
+
+test('price_monthly override: custom value wins over suggestPrice in sponsorship overlay', () => {
+  const DATA = tmpData({
+    'videos.json': BASE_VIDEOS,
+    'ad-state.json': {
+      videos: {
+        'yt-a1': {
+          status: 'available',
+          price_monthly: 1234,
+          updated_at: null,
+        },
+      },
+      updated_at: null,
+    },
+  });
+  const routes = mount(DATA);
+
+  const inv = hit(routes, 'GET /api/ad/inventory').body;
+  const item = inv.items.find(i => i.uid === 'yt-a1');
+  assert.ok(item, 'yt-a1 appears in inventory');
+
+  // The override must win.
+  assert.equal(item.sponsorship.price_monthly, 1234, 'sponsorship.price_monthly uses the state override (1234)');
+
+  // suggested_price is always the heuristic, independent of the override.
+  // yt-a1 has views:0 → youtube base 600, no plays bump → 600.
+  assert.equal(item.suggested_price, 600, 'suggested_price reflects heuristic (youtube base, 0 plays)');
+
+  // Confirm they differ — the whole point of having both fields.
+  assert.notEqual(item.sponsorship.price_monthly, item.suggested_price,
+    'sponsorship.price_monthly and suggested_price differ when override is set');
+});
+
+test('price_monthly: null price_monthly in state falls back to suggestPrice', () => {
+  // ov.price_monthly != null is the gate (line 152). null in state → suggestPrice.
+  const DATA = tmpData({
+    'videos.json': BASE_VIDEOS,
+    'ad-state.json': {
+      videos: {
+        'yt-a1': { status: 'available', price_monthly: null, updated_at: null },
+      },
+      updated_at: null,
+    },
+  });
+  const routes = mount(DATA);
+
+  const inv = hit(routes, 'GET /api/ad/inventory').body;
+  const item = inv.items.find(i => i.uid === 'yt-a1');
+  assert.equal(item.sponsorship.price_monthly, item.suggested_price,
+    'null price_monthly in state falls back to suggestPrice');
+});

← 423cfa3d test(adslots): cover ad-system admin write paths (video/plac  ·  back to Rentv 2026  ·  fix(adslots): 5 ad-system.cjs correctness fixes + regression fc7ec801 →