Bridges third-party status pages into Webex spaces via RSS polling and inbound webhooks (Statuspage / Status.io / Uptime Kuma / generic). Includes an interactive Webex bot (websocket transport) that lets space members register sources with an Adaptive Card instead of hand-editing config/feeds.json: help, add, list, webhook <key>, remove <key>. Ships with an atomic JSON store (per-file mutex, tmp+rename), parallel RSS polling, and unit tests via node:test. All secrets are sourced from environment variables (see .env.example); no credentials in the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
52 lines
1.9 KiB
JavaScript
52 lines
1.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { readJSON, writeJSON } from '../src/atomicJson.js';
|
|
|
|
function tmpFile(name) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atomicjson-'));
|
|
return path.join(dir, name);
|
|
}
|
|
|
|
test('writeJSON persists and readJSON reads it back', async () => {
|
|
const file = tmpFile('a.json');
|
|
await writeJSON(file, { hello: 'world', n: 1 });
|
|
assert.deepEqual(readJSON(file), { hello: 'world', n: 1 });
|
|
});
|
|
|
|
test('concurrent writeJSON calls all land, none corrupt the target', async () => {
|
|
const file = tmpFile('b.json');
|
|
const writers = [];
|
|
for (let i = 0; i < 25; i++) {
|
|
writers.push(writeJSON(file, { i, at: Date.now() }));
|
|
}
|
|
await Promise.all(writers);
|
|
// File must be valid JSON with one of the written objects.
|
|
const parsed = readJSON(file);
|
|
assert.equal(typeof parsed.i, 'number');
|
|
assert.ok(parsed.i >= 0 && parsed.i < 25);
|
|
});
|
|
|
|
test('writeJSON removes tmp files on rename failure', async () => {
|
|
const file = tmpFile('c.json');
|
|
await writeJSON(file, { ok: true });
|
|
|
|
// Force a rename failure by pointing at a path whose parent does not exist.
|
|
const bad = path.join(file, 'nested', 'not', 'there.json');
|
|
await assert.rejects(writeJSON(bad, { x: 1 }));
|
|
|
|
// No tmp files should be left in the parent dir of `file`.
|
|
const dir = path.dirname(file);
|
|
const stray = fs.readdirSync(dir).filter(n => n.endsWith('.tmp'));
|
|
assert.deepEqual(stray, []);
|
|
});
|
|
|
|
test('writes to different paths do not block each other', async () => {
|
|
const f1 = tmpFile('d.json');
|
|
const f2 = tmpFile('e.json');
|
|
await Promise.all([writeJSON(f1, { a: 1 }), writeJSON(f2, { b: 2 })]);
|
|
assert.deepEqual(readJSON(f1), { a: 1 });
|
|
assert.deepEqual(readJSON(f2), { b: 2 });
|
|
});
|