// guard.test.mjs — tests del publish-guard (directiva 2026-11-01) import test from 'node:test' import assert from 'node:assert' import { createRequire } from 'node:module' import { readFileSync } from 'node:fs' const require_ = createRequire(import.meta.url) const pg = require_('/srv/cayce-core/node_modules/pg') const env = Object.fromEntries(readFileSync('/srv/cayce-core/.env', 'utf8').split('\n').filter(l => l.includes('=')).map(l => [l.slice(0, l.indexOf('=')), l.slice(l.indexOf('=') + 1)])) const pool = new pg.Pool({ connectionString: env.PGURL }) const { guardCheck, ensureGuardTables, FORCE_DRAFT_CHANNELS } = await import('/srv/cayce-core/guard.mjs') await ensureGuardTables(pool) const clean = async () => { await pool.query("DELETE FROM outbound_drafts WHERE meta->>'test' = '1'") await pool.query("DELETE FROM guard_rejections WHERE content LIKE '%__TEST__%'") } test.after(() => pool.end()) test('R1 marca de hardware wallet bloqueada', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'keep your coins in a Ledger and sleep well __TEST__' }) assert.ok(!v.ok, JSON.stringify(v)); assert.ok(v.reasons.some(r => r.startsWith('marca-bloqueada:ledger')), JSON.stringify(v.reasons)) }) test('R2a enlace de referido', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'good wallets here https://shop.example.com/?ref=cayce __TEST__' }) assert.ok(!v.ok && v.reasons.includes('enlace-afiliado-o-referido'), JSON.stringify(v.reasons)) }) test('R2b hashtag', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'the market never sleeps #bitcoin __TEST__' }) assert.ok(!v.ok && v.reasons.includes('hashtag'), JSON.stringify(v.reasons)) }) test('R2c emoji', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'fees are low again šŸ”„ __TEST__' }) assert.ok(!v.ok && v.reasons.includes('emoji'), JSON.stringify(v.reasons)) }) test('R2d signo de exclamacion', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'fees are low again __TEST__' .replace('again','again!') }) assert.ok(!v.ok && v.reasons.includes('signo-de-exclamacion'), JSON.stringify(v.reasons)) }) test('R3 hecho sin fuente y fecha', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'fees hit 300 sat/vB, the highest since march __TEST__' }) assert.ok(!v.ok && v.reasons.includes('afirmacion-factica-sin-fuente-y-fecha'), JSON.stringify(v.reasons)) }) test('R3 pasa con fuente y fecha', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'fees hit 12 sat/vB on Nov 1 2026 (source: https://hardcap.news/fees) and nobody cared __TEST__' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R4 casi-duplicado 30 dias', async () => { await clean() await clean() await pool.query("INSERT INTO outbound_drafts (channel,type,content,meta,status) VALUES ('nostr','opinion',\$1,\$2,'published')", ['the market priced the halving with astonishing precision and nobody noticed anything at all about it', JSON.stringify({ test: '1' })]) const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'the market priced the halving with astonishing precision and nobody noticed anything at all today __TEST__' }) assert.ok(!v.ok && v.reasons.some(r => r.startsWith('casi-duplicado')), JSON.stringify(v.reasons)) await clean() }) test('R5 tope diario por canal', async () => { await clean() await clean() for (let i = 0; i < 3; i++) await pool.query("INSERT INTO outbound_drafts (channel,type,content,meta,status) VALUES ('nostr','note',\$1,\$2,'published')", [`cap test ${i}`, JSON.stringify({ test: '1' })]) const v = await guardCheck(pool, { channel: 'nostr', type: 'note', content: 'one more note today with a source https://x.example and date Oct 28 2026 __TEST__' }) assert.ok(!v.ok && v.reasons.some(r => r.startsWith('tope-diario:nostr')), JSON.stringify(v.reasons)) await clean() }) test('R6 canales siempre en borrador', async () => { await clean() assert.deepEqual(FORCE_DRAFT_CHANNELS.sort(), ['dm', 'mail', 'proposal']) }) test('metadata (kind 3) exenta de reglas de texto', async () => { const v = await guardCheck(pool, { channel: 'nostr', type: 'contact_list', content: '' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R7 fuente mayor de 72h en canal de noticias', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'fees spiked in august https://example.com/x reported Aug 20 2026 and nobody moved __TEST__' }) assert.ok(!v.ok && v.reasons.some(r => r.startsWith('fuente-mayor-72h')), JSON.stringify(v.reasons)) }) test('R7 pasa con fuente fresca', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'outflows hit 3.64 billion after the reopen on Nov 1 2026 https://www.theblock.co/x — the queue was the story __TEST__' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R7 no aplica a bitcoinhotels', async () => { await clean() const v = await guardCheck(pool, { channel: 'bitcoinhotels', type: 'guide', content: 'step by step guide verified against https://docs.btcpayserver.org/ visited Oct 20 2026 __TEST__' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R8 cierre aforismo corto sin dato', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'the report is dated Nov 1 2026 https://www.theblock.co/x and the numbers hold. Indexes love you while you are liquid __TEST__' }) assert.ok(!v.ok && v.reasons.includes('cierre-aforismo-sin-dato'), JSON.stringify(v.reasons)) }) test('R8 pasa si el cierre trae dato', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'the report is dated Nov 1 2026 https://www.theblock.co/x and the numbers hold. Tron moved 242 million for the Lazarus group __TEST__' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R8 regression: cola (source: ...) no cuenta como cierre', async () => { const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'the report is dated Nov 1 2026 and Tron carried 242 million for the group. The blocklist missed Tron and 242 million dollars noticed. (source: https://www.theblock.co/x, Nov 1 2026) __TEST__' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R8 refinado: cierre con superlativo/record aporta dato', async () => { await clean() const v = await guardCheck(pool, { channel: 'hardcap', type: 'article_note', content: 'A record hack at a major platform triggered the largest daily net outflow on record for Bitcoin ETFs on Nov 1 2026 (source: https://decrypt.co/x, Nov 1 2026) __TEST__' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R1 opendime y seedplate bloqueados', async () => { await clean() const v1 = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'OPENDIME makes bitcoin physical and cheap __TEST__' }) const v2 = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'a SEEDPLATE survives the house fire they said __TEST__' }) assert.ok(!v1.ok && v1.reasons.some(r => r.includes('opendime')), JSON.stringify(v1.reasons)) assert.ok(!v2.ok && v2.reasons.some(r => r.includes('seedplate')), JSON.stringify(v2.reasons)) }) test('R1 voz propia: opinion con marca RECHAZADA', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'Ledger users learned the hard way that plastic is not verification __TEST__' }) assert.ok(!v.ok && v.reasons.some(r => r.startsWith('marca-bloqueada:ledger')), JSON.stringify(v.reasons)) }) test('R1 noticia de terceros: signal con marca + fuente PERMITIDA', async () => { await clean() const v = await guardCheck(pool, { channel: 'hardcap', type: 'signal', content: '2026-11-01 Ā· Ledger confirmed a hardware implant found in reseller units across Asia, per the vendor advisory (source: https://example.com/advisory, Nov 1 2026). Per the advisory, 7 Asian markets pulled affected units within 48 hours.' }) assert.ok(v.ok, JSON.stringify(v.reasons)) }) test('R14a: aforismo + fuente → RECHAZADO por R8 (no por R3)', async () => { const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'Index membership was never a promise.\n\n— fuente: https://example.com/x Ā· 2026-11-02' }) assert.ok(!v.ok && v.reasons.includes('cierre-aforismo-sin-dato'), 'debe rechazar por R8: ' + JSON.stringify(v.reasons)) assert.ok(!v.reasons.some(r => r.includes('afirmacion')), 'R3 NO debe rechazar con fuente') }) test('R14c: texto tema X + fuente tema Y → RECHAZADO por R9', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'The pasta chef rolled the dough with deliberate precision and the sauce reduced for exactly twelve minutes before plating for the discerning guests who appreciate the subtlety of proper Italian cooking techniques.\n\n— fuente: https://theblock.co/taproot-article Ā· 2026-11-02' }) assert.ok(v.reasons.includes('R9-atribucion-no-verificable'), 'debe rechazar por R9: ' + JSON.stringify(v.reasons)) }) test('R14d: pieza correcta sobre tapscript con fuente tapscript → APROBADA', async () => { await clean() const v = await guardCheck(pool, { channel: 'nostr', type: 'opinion', content: 'Multiplication in Tapscript just got cheaper. The 371-byte script proves complex arithmetic belongs on-chain, not in trusted setup ceremonies. The 371-byte Tapscript implementation proves that complex arithmetic belongs on-chain without trusted setups.\n\n— fuente: https://bitcoinmagazine.com/tapscript-multiplication Ā· 2026-11-02' }) assert.ok(v.ok, 'debe aprobar: ' + JSON.stringify(v.reasons)) }) import test from "node:test"; import assert from "node:assert"; test("demo rojo", () => { assert.ok(false, "falla a proposito (demo r19)") })