← back to Goldleafwallpaper
scripts/ig-poster/test-cncp-alert.js
112 lines
'use strict';
/*
* test-cncp-alert.js — NEGATIVE-TEST-FIRST proof for the IG-poster alert arm.
*
* CLAUDE.md TK-11431 amendment 3: "a check ships with a negative test proving it
* goes RED on an injected fault, or it does not ship. A positive-only test on a
* detector proves nothing." The defect this replaces was invisible to exactly a
* positive-only test, because fetch() RESOLVES on HTTP 404/500 — so the ONLY
* case that mattered (non-2xx) looked identical to success.
*
* Runs four cases against LOCAL STUB servers — it never touches the real CNCP
* board, never posts a real card, and never loads post-next.js (so it cannot
* post to Instagram):
* 1. NEGATIVE non-2xx (stub returns 404) -> MUST report delivered:false
* 2. NEGATIVE non-2xx (stub returns 500) -> MUST report delivered:false
* 3. NEGATIVE transport (nothing listening) -> MUST report delivered:false
* 4. POSITIVE 2xx (stub returns 200) -> MUST report delivered:true
* Plus: the shared sender's delivery RECEIPT is asserted for both a failed and a
* successful send, since that receipt is the durable, producer-unsuppressable
* signal the whole design rests on.
*
* The seam is CNCP_URL (honoured by _shared/cncp_post.sh). No plist sets it, so
* a scheduled run always targets the real board.
*
* Run: node scripts/ig-poster/test-cncp-alert.js ($0 local)
*/
const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const { postCncpCard } = require('./cncp-alert');
// The stub MUST live in its own process: postCncpCard is synchronous
// (execFileSync), so an in-process http server would never get a chance to
// answer and every case — including the 200 control — would fail as a timeout.
// That is itself a measuring-the-wrong-thing trap, so it is designed out.
function stub(status) {
return new Promise((resolve, reject) => {
const code = `const http=require('http');const s=http.createServer((q,r)=>{r.writeHead(${status});r.end('{}')});`
+ `s.listen(0,'127.0.0.1',()=>console.log(s.address().port));`;
const p = spawn(process.execPath, ['-e', code], { stdio: ['ignore', 'pipe', 'inherit'] });
let buf = '';
p.stdout.on('data', (d) => {
buf += d;
if (buf.includes('\n')) resolve({ srv: { close: () => p.kill() }, port: Number(buf.trim()) });
});
p.on('error', reject);
});
}
function freePort() {
return new Promise((resolve) => {
const srv = http.createServer();
srv.listen(0, '127.0.0.1', () => { const p = srv.address().port; srv.close(() => resolve(p)); });
});
}
// Receipts go to a THROWAWAY skills-shaped dir so the real shared receipt log is
// not polluted with test rows (alert_receipt.sh prefers $SKILL when it is a dir
// whose parent is named "skills").
const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'goldleaf-alert-test-'));
const SKILL = path.join(TMP, 'skills', 'goldleaf-ig-poster-test');
fs.mkdirSync(SKILL, { recursive: true });
process.env.SKILL = SKILL;
const RECEIPTS = path.join(SKILL, 'data', 'alert-delivery.jsonl');
const receipts = () => (fs.existsSync(RECEIPTS)
? fs.readFileSync(RECEIPTS, 'utf8').split('\n').filter(Boolean).map(JSON.parse) : []);
let failures = 0;
function check(name, cond, detail) {
console.log(`${cond ? 'PASS' : 'FAIL'} ${name}${detail ? ' :: ' + detail : ''}`);
if (!cond) failures++;
}
(async () => {
console.log(`receipts -> ${RECEIPTS}\n`);
// --- 1 & 2: NEGATIVE, non-2xx. The case the old code got wrong. ---
for (const code of [404, 500]) {
const { srv, port } = await stub(code);
process.env.CNCP_URL = `http://127.0.0.1:${port}`;
const r = postCncpCard('alert://test', 'injected fault: stub returns ' + code);
srv.close();
check(`NEGATIVE non-2xx ${code} reports NOT delivered`, r.delivered === false, JSON.stringify(r));
check(`NEGATIVE non-2xx ${code} names the HTTP code in its reason`,
String(r.error).includes(String(code)), r.error);
}
const afterFail = receipts().slice(-1)[0];
check('NEGATIVE non-2xx writes a receipt with ok:false',
!!afterFail && afterFail.ok === false && afterFail.channel === 'cncp', JSON.stringify(afterFail));
// --- 3: NEGATIVE, transport failure (nothing listening). ---
const dead = await freePort();
process.env.CNCP_URL = `http://127.0.0.1:${dead}`;
const r3 = postCncpCard('alert://test', 'injected fault: nothing listening');
check('NEGATIVE transport failure reports NOT delivered', r3.delivered === false, JSON.stringify(r3));
check('NEGATIVE transport failure explains itself', /transport|reaching|CNCP/i.test(String(r3.error)), r3.error);
// --- 4: POSITIVE, 2xx. The success path must still report success. ---
const { srv: okSrv, port: okPort } = await stub(200);
process.env.CNCP_URL = `http://127.0.0.1:${okPort}`;
const r4 = postCncpCard('alert://test', 'control: stub returns 200');
okSrv.close();
check('POSITIVE 2xx reports delivered', r4.delivered === true, JSON.stringify(r4));
const afterOk = receipts().slice(-1)[0];
check('POSITIVE 2xx writes a receipt with ok:true',
!!afterOk && afterOk.ok === true && afterOk.channel === 'cncp', JSON.stringify(afterOk));
console.log(`\n${failures === 0 ? 'ALL PASS' : failures + ' FAILURE(S)'} · receipts written: ${receipts().length} · cost: $0 (local)`);
process.exit(failures === 0 ? 0 : 1);
})();