// 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 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() 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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)) })