diff --git a/scripts/operator_notify_bridge_supervisor.mjs b/scripts/operator_notify_bridge_supervisor.mjs new file mode 100644 index 0000000..0fcd4f5 --- /dev/null +++ b/scripts/operator_notify_bridge_supervisor.mjs @@ -0,0 +1,3 @@ +#!/usr/bin/env node + +import '../plugins/reporting-governance/scripts/operator_notify_bridge_supervisor.mjs'; diff --git a/scripts/test_operator_notify_bridge_supervisor.mjs b/scripts/test_operator_notify_bridge_supervisor.mjs new file mode 100644 index 0000000..a8e17f8 --- /dev/null +++ b/scripts/test_operator_notify_bridge_supervisor.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node + +import assert from 'node:assert/strict'; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import process from 'node:process'; +import { spawnSync } from 'node:child_process'; + +const ROOT_DIR = path.resolve(import.meta.dirname, '..'); +const REPO_SHIM = path.join(ROOT_DIR, 'scripts', 'operator_notify_bridge_supervisor.mjs'); +const PACKAGE_ENTRY = path.join(ROOT_DIR, 'plugins', 'reporting-governance', 'scripts', 'operator_notify_bridge_supervisor.mjs'); + +function createFixture() { + const fixtureRoot = mkdtempSync(path.join(tmpdir(), 'operator-notify-bridge-supervisor-shim-regression-')); + const queueDir = path.join(fixtureRoot, 'queue'); + const spoolDir = path.join(fixtureRoot, 'spool'); + const receiptDir = path.join(fixtureRoot, 'receipts'); + const attemptDir = path.join(fixtureRoot, 'attempts'); + [queueDir, spoolDir, receiptDir, attemptDir].forEach((dir) => mkdirSync(dir, { recursive: true })); + + const queuePath = path.join(queueDir, 'queue-item.json'); + writeFileSync(queuePath, `${JSON.stringify({ + notification_id: 'notify-supervisor-shim-regression-1', + status: 'dispatched', + dispatch_result: { state: 'dispatched', mode: 'spool_only' }, + }, null, 2)}\n`, 'utf8'); + + const spoolPath = path.join(spoolDir, 'notify-supervisor-shim-regression-1-dispatch.json'); + writeFileSync(spoolPath, `${JSON.stringify({ + notification_id: 'notify-supervisor-shim-regression-1', + queue_item_path: queuePath, + dispatch_contract: { + executor: 'message.send', + channel: 'telegram', + target: '864811879', + message: 'watchdog overdue', + }, + }, null, 2)}\n`, 'utf8'); + + return { fixtureRoot, queueDir, spoolDir, receiptDir, attemptDir }; +} + +function run(script, args = []) { + const result = spawnSync(process.execPath, [script, ...args], { + cwd: ROOT_DIR, + encoding: 'utf8', + }); + return { + status: result.status, + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + }; +} + +const tests = []; +function test(name, fn) { tests.push({ name, fn }); } +function printResult(prefix, name, detail = '') { process.stdout.write(`${prefix} ${name}${detail ? ` ${detail}` : ''}\n`); } + +function scrub(value) { + if (Array.isArray(value)) return value.map(scrub); + if (!value || typeof value !== 'object') return value; + + const next = {}; + for (const [key, raw] of Object.entries(value)) { + if (typeof raw === 'string') { + if (key.toLowerCase().includes('path') || key.toLowerCase().endsWith('dir')) { + next[key] = ''; + continue; + } + if (key === 'notificationId' || key === 'notification_id') { + next[key] = ''; + continue; + } + if (key === 'created_at' || key === 'now') { + next[key] = ''; + continue; + } + } + next[key] = scrub(raw); + } + return next; +} + +function normalizeStdout(stdout) { + return scrub(JSON.parse(stdout)); +} + +function buildArgs(fixture) { + return [ + '--queue-dir', fixture.queueDir, + '--spool-dir', fixture.spoolDir, + '--receipt-dir', fixture.receiptDir, + '--dispatcher-script', path.join(ROOT_DIR, 'scripts', 'operator_notify_dispatcher.mjs'), + '--sender-command', `${JSON.stringify(process.execPath)} ${JSON.stringify(path.join(ROOT_DIR, 'scripts', 'operator_notify_sender_binding.mjs'))} --mode shim --attempt-dir ${JSON.stringify(fixture.attemptDir)} --compact`, + '--now', '2026-05-07T10:02:00.000Z', + '--compact', + ]; +} + +test('repo-root shim forwards help text and exits like package entrypoint', () => { + const shim = run(REPO_SHIM, ['--help']); + const pkg = run(PACKAGE_ENTRY, ['--help']); + assert.equal(shim.status, 0); + assert.equal(pkg.status, 0); + assert.equal(shim.stderr, ''); + assert.equal(pkg.stderr, ''); + assert.equal(shim.stdout, pkg.stdout); +}); + +test('repo-root shim forwards args and preserves success payload semantics', () => { + const shimFixture = createFixture(); + const pkgFixture = createFixture(); + try { + const shim = run(REPO_SHIM, buildArgs(shimFixture)); + const pkg = run(PACKAGE_ENTRY, buildArgs(pkgFixture)); + assert.equal(shim.status, 0, shim.stderr); + assert.equal(pkg.status, 0, pkg.stderr); + assert.deepEqual(normalizeStdout(shim.stdout), normalizeStdout(pkg.stdout)); + } finally { + rmSync(shimFixture.fixtureRoot, { recursive: true, force: true }); + rmSync(pkgFixture.fixtureRoot, { recursive: true, force: true }); + } +}); + +test('repo-root shim preserves non-zero exit semantics from package core', () => { + const shim = run(REPO_SHIM, ['--now', 'not-an-iso']); + const pkg = run(PACKAGE_ENTRY, ['--now', 'not-an-iso']); + assert.equal(shim.status, 1); + assert.equal(pkg.status, 1); + assert.equal(shim.stdout, ''); + assert.equal(pkg.stdout, ''); + assert.equal(shim.stderr, pkg.stderr); +}); + +let failures = 0; +for (const { name, fn } of tests) { + try { + fn(); + printResult('ok', name); + } catch (error) { + failures += 1; + printResult('not ok', name, `- ${error instanceof Error ? error.message : String(error)}`); + } +} + +if (failures > 0) { + process.exit(1); +}