Merge branch 'main' into test/upload-validation-15691315589845316357

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-21 14:13:15 -05:00
commit 67ebd6a504
33 files changed
+9337 -1079

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
import { test, after } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { cmdBackup, listBackups } from '../src/cmd-backup.mjs';
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-backup-'));
// Helper to setup dummy state files
function setupStateFiles(cwd) {
const memoryDir = path.join(cwd, '.scheduler-memory');
const logsDir = path.join(cwd, 'task-logs', 'daily');
fs.mkdirSync(memoryDir, { recursive: true });
fs.mkdirSync(logsDir, { recursive: true });
fs.writeFileSync(path.join(memoryDir, 'memory-store.json'), '{"dummy": "memory"}');
fs.writeFileSync(path.join(logsDir, '.scheduler-run-state.json'), '{"dummy": "state"}');
}
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
test('cmdBackup creates backup directory and manifest', async () => {
const cwd = path.join(tempBase, 'scenario-1');
fs.mkdirSync(cwd, { recursive: true });
setupStateFiles(cwd);
const { backupDir } = await cmdBackup({ cwd });
assert.ok(fs.existsSync(backupDir), 'Backup directory created');
assert.ok(fs.existsSync(path.join(backupDir, 'backup-manifest.json')), 'Manifest created');
const manifestContent = JSON.parse(fs.readFileSync(path.join(backupDir, 'backup-manifest.json'), 'utf8'));
assert.strictEqual(manifestContent.backupDir, backupDir);
assert.ok(manifestContent.createdAt);
// Verify files are copied
const backupMemory = path.join(backupDir, '.scheduler-memory__memory-store.json');
assert.ok(fs.existsSync(backupMemory), 'Memory file copied');
// captured should contain the 2 files
assert.strictEqual(manifestContent.captured.length, 2);
});
test('cmdBackup handles missing files gracefully', async () => {
const cwd = path.join(tempBase, 'scenario-2');
fs.mkdirSync(cwd, { recursive: true });
// Do NOT setup files
const { manifest } = await cmdBackup({ cwd });
assert.strictEqual(manifest.captured.length, 0);
assert.strictEqual(manifest.skipped.length, 2);
assert.strictEqual(manifest.skipped[0].reason, 'not found');
});
test('cmdBackup respects output override', async () => {
const cwd = path.join(tempBase, 'scenario-3');
fs.mkdirSync(cwd, { recursive: true });
setupStateFiles(cwd);
const outputDir = 'custom-backup-location';
const { backupDir } = await cmdBackup({ cwd, output: outputDir });
const expectedPath = path.resolve(cwd, outputDir);
assert.strictEqual(backupDir, expectedPath);
assert.ok(fs.existsSync(expectedPath));
assert.ok(fs.existsSync(path.join(expectedPath, 'backup-manifest.json')), 'Manifest in custom output');
});
test('listBackups lists created backups', async () => {
const cwd = path.join(tempBase, 'scenario-4');
fs.mkdirSync(cwd, { recursive: true });
setupStateFiles(cwd);
// Create two backups
await cmdBackup({ cwd });
// Ensure we have a slight delay or just rely on execution time for timestamp diff
// Windows/Filesystem might have low resolution, but listBackups sorts by ISO string in manifest
await new Promise(r => setTimeout(r, 100));
await cmdBackup({ cwd });
const backups = await listBackups({ cwd });
assert.strictEqual(backups.length, 2);
// Verify sorting (newest first)
const t0 = new Date(backups[0].createdAt).getTime();
const t1 = new Date(backups[1].createdAt).getTime();
assert.ok(t0 >= t1, 'Backups sorted newest first');
// Verify properties
assert.ok(backups[0].id);
assert.ok(backups[0].backupDir);
assert.ok(Array.isArray(backups[0].captured));
});
+316
View File
@@ -0,0 +1,316 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { cmdProposal } from '../src/cmd-proposal.mjs';
import { ExitError } from '../src/errors.mjs';
describe('cmdProposal', () => {
const noop = () => {};
describe('create', () => {
it('should create a proposal successfully', async () => {
const logs = [];
const errors = [];
const mockCreate = async (args) => {
return { id: 'prop-123', status: 'open', ...args };
};
const mockReadFile = async () => 'file-content';
const deps = {
createProposal: mockCreate,
readFile: mockReadFile,
log: (msg) => logs.push(msg),
error: (msg) => errors.push(msg),
};
const args = {
agent: 'test-agent',
target: 'src/foo.js',
contentFile: 'new-content.txt',
reason: 'fix bug',
};
await cmdProposal('create', args, deps);
assert.strictEqual(logs.length, 1);
assert.strictEqual(errors.length, 0);
const output = JSON.parse(logs[0]);
assert.strictEqual(output.id, 'prop-123');
assert.strictEqual(output.newContent, 'file-content');
assert.strictEqual(output.agent, 'test-agent');
});
it('should throw if arguments are missing', async () => {
const errors = [];
const deps = {
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('create', {}, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing arguments'
);
assert.ok(errors[0].includes('Usage:'));
});
it('should throw if file read fails', async () => {
const errors = [];
const mockReadFile = async () => { throw new Error('File not found'); };
const deps = {
readFile: mockReadFile,
log: noop,
error: (msg) => errors.push(msg),
};
const args = {
agent: 'test-agent',
target: 'src/foo.js',
contentFile: 'missing.txt',
reason: 'fix bug',
};
await assert.rejects(
() => cmdProposal('create', args, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'File read error'
);
assert.ok(errors[0].includes('Failed to read content file'));
});
it('should throw if createProposal fails', async () => {
const errors = [];
const mockCreate = async () => { throw new Error('DB error'); };
const mockReadFile = async () => 'content';
const deps = {
createProposal: mockCreate,
readFile: mockReadFile,
log: noop,
error: (msg) => errors.push(msg),
};
const args = {
agent: 'test-agent',
target: 'src/foo.js',
contentFile: 'content.txt',
reason: 'fix bug',
};
await assert.rejects(
() => cmdProposal('create', args, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Proposal creation failed'
);
assert.ok(errors[0].includes('Failed to create proposal'));
});
});
describe('list', () => {
it('should list all proposals', async () => {
const logs = [];
const mockList = async () => [{ id: '1', status: 'open' }, { id: '2', status: 'closed' }];
const deps = {
listProposals: mockList,
log: (msg) => logs.push(msg),
error: noop,
};
await cmdProposal('list', {}, deps);
assert.strictEqual(logs.length, 1);
const output = JSON.parse(logs[0]);
assert.strictEqual(output.length, 2);
});
it('should filter proposals by status', async () => {
const logs = [];
const mockList = async () => [{ id: '1', status: 'open' }, { id: '2', status: 'closed' }];
const deps = {
listProposals: mockList,
log: (msg) => logs.push(msg),
error: noop,
};
await cmdProposal('list', { status: 'open' }, deps);
assert.strictEqual(logs.length, 1);
const output = JSON.parse(logs[0]);
assert.strictEqual(output.length, 1);
assert.strictEqual(output[0].id, '1');
});
it('should handle list failure', async () => {
const errors = [];
const mockList = async () => { throw new Error('List error'); };
const deps = {
listProposals: mockList,
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('list', {}, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'List failed'
);
assert.ok(errors[0].includes('Failed to list proposals'));
});
});
describe('apply', () => {
it('should apply a proposal', async () => {
const logs = [];
const mockApply = async (id) => ({ id, status: 'applied' });
const deps = {
applyProposal: mockApply,
log: (msg) => logs.push(msg),
error: noop,
};
await cmdProposal('apply', { id: 'prop-1' }, deps);
assert.strictEqual(logs.length, 1);
const output = JSON.parse(logs[0]);
assert.strictEqual(output.status, 'applied');
assert.strictEqual(output.id, 'prop-1');
});
it('should throw if id is missing', async () => {
const errors = [];
const deps = {
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('apply', {}, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing id'
);
assert.ok(errors[0].includes('Usage:'));
});
it('should handle apply failure', async () => {
const errors = [];
const mockApply = async () => { throw new Error('Apply error'); };
const deps = {
applyProposal: mockApply,
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('apply', { id: 'prop-1' }, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Apply failed'
);
assert.ok(errors[0].includes('Failed to apply proposal'));
});
});
describe('reject', () => {
it('should reject a proposal', async () => {
const logs = [];
const mockReject = async (id, reason) => ({ id, status: 'rejected', reason });
const deps = {
rejectProposal: mockReject,
log: (msg) => logs.push(msg),
error: noop,
};
await cmdProposal('reject', { id: 'prop-1', reason: 'bad code' }, deps);
assert.strictEqual(logs.length, 1);
const output = JSON.parse(logs[0]);
assert.strictEqual(output.status, 'rejected');
assert.strictEqual(output.reason, 'bad code');
});
it('should throw if arguments are missing', async () => {
const errors = [];
const deps = {
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('reject', { id: 'prop-1' }, deps), // missing reason
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing arguments'
);
});
it('should handle reject failure', async () => {
const errors = [];
const mockReject = async () => { throw new Error('Reject error'); };
const deps = {
rejectProposal: mockReject,
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('reject', { id: 'prop-1', reason: 'bad' }, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Reject failed'
);
assert.ok(errors[0].includes('Failed to reject proposal'));
});
});
describe('show', () => {
it('should show a proposal', async () => {
const logs = [];
const mockGet = async (id) => ({ id, content: 'stuff' });
const deps = {
getProposal: mockGet,
log: (msg) => logs.push(msg),
error: noop,
};
await cmdProposal('show', { id: 'prop-1' }, deps);
assert.strictEqual(logs.length, 1);
const output = JSON.parse(logs[0]);
assert.strictEqual(output.id, 'prop-1');
assert.strictEqual(output.content, 'stuff');
});
it('should throw if id is missing', async () => {
const errors = [];
const deps = {
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('show', {}, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Missing id'
);
});
it('should handle show failure', async () => {
const errors = [];
const mockGet = async () => { throw new Error('Show error'); };
const deps = {
getProposal: mockGet,
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('show', { id: 'prop-1' }, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Show failed'
);
assert.ok(errors[0].includes('Failed to show proposal'));
});
});
describe('unknown subcommand', () => {
it('should throw ExitError for unknown subcommand', async () => {
const errors = [];
const deps = {
log: noop,
error: (msg) => errors.push(msg),
};
await assert.rejects(
() => cmdProposal('foo', {}, deps),
(err) => err instanceof ExitError && err.code === 1 && err.message === 'Unknown subcommand'
);
assert.ok(errors[0].includes('Unknown proposal subcommand'));
});
});
});
+40
View File
@@ -1,6 +1,7 @@
import test from 'node:test';
import assert from 'node:assert';
import http from 'node:http';
import crypto from 'node:crypto';
import { cmdDashboard } from '../src/dashboard.mjs';
import { _resetTorchConfigCache } from '../src/torch-config.mjs';
@@ -147,4 +148,43 @@ test('Dashboard Authentication', async (t) => {
_resetTorchConfigCache();
}
});
await t.test('validates correctly using scrypt hashed credentials', async () => {
// Generate a valid scrypt hash
const password = 'admin:hashedpassword';
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64);
const storedAuth = `scrypt:${salt.toString('hex')}:${hash.toString('hex')}`;
process.env.TORCH_DASHBOARD_AUTH = storedAuth;
_resetTorchConfigCache();
server = await cmdDashboard(0);
testPort = server.address().port;
try {
const makeRequest = (credentials) => {
const auth = Buffer.from(credentials).toString('base64');
return new Promise((resolve) => {
http.get({
host: '127.0.0.1',
port: testPort,
path: '/dashboard/',
headers: { 'Authorization': `Basic ${auth}` }
}, resolve);
});
};
// Correct password
const resCorrect = await makeRequest(password);
assert.notStrictEqual(resCorrect.statusCode, 401, 'correct hashed credentials must not be rejected');
// Incorrect password
const resIncorrect = await makeRequest('admin:wrong');
assert.strictEqual(resIncorrect.statusCode, 401, 'incorrect hashed credentials must be rejected');
} finally {
delete process.env.TORCH_DASHBOARD_AUTH;
_resetTorchConfigCache();
}
});
});
+3 -3
View File
@@ -91,7 +91,7 @@ test('cmdInit should validate install directory name', async (t) => {
relays: []
});
} catch (e) {
assert.notMatch(e.message, /Invalid directory name/);
assert.doesNotMatch(e.message, /Invalid directory name/);
}
});
});
@@ -105,7 +105,7 @@ test('cmdInit should validate install directory name', async (t) => {
relays: []
});
} catch (e) {
assert.notMatch(e.message, /Invalid directory name/);
assert.doesNotMatch(e.message, /Invalid directory name/);
}
});
});
@@ -119,7 +119,7 @@ test('cmdInit should validate install directory name', async (t) => {
relays: []
});
} catch (e) {
assert.notMatch(e.message, /Invalid directory name/);
assert.doesNotMatch(e.message, /Invalid directory name/);
}
});
});
-192
View File
@@ -1,192 +0,0 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import { publishLock, _resetRelayHealthState } from '../src/lock-ops.mjs';
function createScriptedPool(scriptedOutcomesByAttempt, relaysSeen) {
let publishCallCount = 0;
return {
publish: (relays) => {
publishCallCount += 1;
relaysSeen.push({ publishAttempt: publishCallCount, relays: [...relays] });
const attemptOutcomes = scriptedOutcomesByAttempt[publishCallCount - 1] || {};
return relays.map((relay) => {
const outcome = attemptOutcomes[relay] || { ok: true };
return outcome.ok ? Promise.resolve('ok') : Promise.reject(new Error(outcome.message));
});
},
close: () => {},
getPublishCallCount: () => publishCallCount,
};
}
const parseTelemetry = (logs) => logs.map((line) => JSON.parse(line));
describe('publishLock retry behavior', { concurrency: false }, () => {
beforeEach(() => {
_resetRelayHealthState();
});
afterEach(() => {
mock.timers.reset();
});
it('succeeds immediately on first attempt without retries', async () => {
const telemetryLogs = [];
const relaysSeen = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
const event = { id: 'evt-immediate' };
const result = await publishLock(['wss://relay-a'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result, event);
assert.strictEqual(mockPool.getPublishCallCount(), 1);
assert.deepStrictEqual(relaysSeen, [{ publishAttempt: 1, relays: ['wss://relay-a'] }]);
const telemetry = parseTelemetry(telemetryLogs);
assert.strictEqual(telemetry.filter((entry) => entry.event === 'lock_publish_retry').length, 0);
const metEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(metEvent);
assert.strictEqual(metEvent.publishAttempt, 1);
});
it('retries a transient failure then succeeds within retry budget with bounded jitter', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const telemetryLogs = [];
const relaysSeen = [];
const sleepCalls = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'connection reset by peer' } },
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
const result = await publishLock(['wss://relay-a'], { id: 'evt-retry-success' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 100,
retryCapDelayMs: 500,
randomFn: () => 0.25,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result.id, 'evt-retry-success');
assert.deepStrictEqual(sleepCalls, [25]);
assert.strictEqual(mockPool.getPublishCallCount(), 2);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 1);
assert.strictEqual(retryEvents[0].publishAttempt, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[0].nextDelayMs >= 0 && retryEvents[0].nextDelayMs <= 100);
});
it('returns terminal relay_publish_quorum_failure after exhausting retry budget', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const telemetryLogs = [];
const relaysSeen = [];
const sleepCalls = [];
const randomValues = [0.1, 0.9];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
], relaysSeen);
await assert.rejects(
() => publishLock(['wss://relay-a'], { id: 'evt-exhausted' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 100,
retryCapDelayMs: 250,
randomFn: () => randomValues.shift() ?? 0,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_quorum_failure/,
);
assert.strictEqual(mockPool.getPublishCallCount(), 3);
assert.deepStrictEqual(sleepCalls, [10, 180]);
assert.deepStrictEqual(relaysSeen.map((entry) => entry.publishAttempt), [1, 2, 3]);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 2);
assert.strictEqual(retryEvents[0].publishAttempt, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[0].nextDelayMs >= 0 && retryEvents[0].nextDelayMs <= 100);
assert.strictEqual(retryEvents[1].publishAttempt, 2);
assert.strictEqual(retryEvents[1].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[1].nextDelayMs >= retryEvents[0].nextDelayMs);
assert.ok(retryEvents[1].nextDelayMs <= 200);
const failedEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(failedEvent);
assert.strictEqual(failedEvent.errorCategory, 'relay_publish_quorum_failure');
assert.strictEqual(failedEvent.attempts, 3);
});
it('short-circuits non-retryable failures without extra attempts', async () => {
const telemetryLogs = [];
const relaysSeen = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'validation failed: invalid signature' } },
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
await assert.rejects(
() => publishLock(['wss://relay-a'], { id: 'evt-permanent' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_non_retryable/,
);
assert.strictEqual(mockPool.getPublishCallCount(), 1);
assert.deepStrictEqual(relaysSeen, [{ publishAttempt: 1, relays: ['wss://relay-a'] }]);
const telemetry = parseTelemetry(telemetryLogs);
assert.strictEqual(telemetry.filter((entry) => entry.event === 'lock_publish_retry').length, 0);
const failedEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(failedEvent);
assert.strictEqual(failedEvent.errorCategory, 'relay_publish_non_retryable');
assert.notStrictEqual(failedEvent.errorCategory, 'relay_publish_quorum_failure');
});
});
-319
View File
@@ -2,7 +2,6 @@ import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import {
parseLockEvent,
publishLock,
queryLocks,
_resetRelayHealthState,
} from '../src/lock-ops.mjs';
@@ -111,322 +110,4 @@ describe('lock-ops', () => {
assert.strictEqual(locks[0].agent, 'agent1');
});
});
describe('publishLock', () => {
it('retries transient failures and succeeds on a later attempt', async () => {
const telemetry = [];
const sleepCalls = [];
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
if (publishCalls === 1) {
return relays.map(() => Promise.reject(new Error('connection reset by peer')));
}
return relays.map(() => Promise.resolve('ok'));
},
close: () => {},
};
const event = { id: 'evt-retry' };
const result = await publishLock(['wss://primary-a', 'wss://primary-b'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 500,
retryCapDelayMs: 8_000,
randomFn: () => 0.5,
sleepFn: async (ms) => {
sleepCalls.push(ms);
},
telemetryLogger: (line) => {
telemetry.push(JSON.parse(line));
},
healthLogger: () => {},
diagnostics: {
attemptId: 'attempt-1',
correlationId: 'corr-1',
},
});
assert.strictEqual(result, event);
assert.strictEqual(publishCalls, 2);
assert.deepStrictEqual(sleepCalls, [250]);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 2);
assert.deepStrictEqual(retryEvents[0], {
event: 'lock_publish_retry',
correlationId: 'corr-1',
attemptId: 'attempt-1',
publishAttempt: 1,
relayUrl: 'wss://primary-a',
errorCategory: 'connection_reset',
elapsedMs: retryEvents[0].elapsedMs,
nextDelayMs: 250,
});
assert.deepStrictEqual(retryEvents[1], {
event: 'lock_publish_retry',
correlationId: 'corr-1',
attemptId: 'attempt-1',
publishAttempt: 1,
relayUrl: 'wss://primary-b',
errorCategory: 'connection_reset',
elapsedMs: retryEvents[1].elapsedMs,
nextDelayMs: 250,
});
assert.ok(Number.isInteger(retryEvents[0].elapsedMs));
assert.ok(Number.isInteger(retryEvents[1].elapsedMs));
const metEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(metEvent);
assert.equal(metEvent.correlationId, 'corr-1');
assert.equal(metEvent.attemptId, 'attempt-1');
assert.equal(metEvent.publishAttempt, 2);
assert.ok(Array.isArray(metEvent.retryTimeline));
});
it('fails after retry budget is exhausted for persistent transient failures', async () => {
let publishCalls = 0;
const sleepCalls = [];
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map(() => Promise.reject(new Error('relay unavailable')));
},
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://primary-down'], { id: 'evt-exhausted' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
randomFn: () => 1,
sleepFn: async (ms) => {
sleepCalls.push(ms);
},
telemetryLogger: () => {},
healthLogger: () => {},
}),
/Failed relay publish quorum in publish phase: 0\/1 successful \(required=1, timeout=200ms, attempts=3,/,
);
assert.strictEqual(publishCalls, 3);
assert.deepStrictEqual(sleepCalls, [500, 1000]);
});
it('fails without retry for persistent validation failures', async () => {
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map(() => Promise.reject(new Error('validation failed: invalid signature')));
},
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://primary-bad'], { id: 'evt-permanent' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
sleepFn: async () => {
throw new Error('should not sleep for permanent failures');
},
telemetryLogger: () => {},
healthLogger: () => {},
}),
/Failed relay publish quorum in publish phase: 0\/1 successful \(required=1, timeout=200ms, attempts=4,/,
);
assert.strictEqual(publishCalls, 1);
});
it('includes per-relay reason categories and retry metadata in failure diagnostics', async () => {
const telemetry = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
if (relay.includes('dns')) return Promise.reject(new Error('getaddrinfo ENOTFOUND relay.example'));
if (relay.includes('tcp')) return Promise.reject(new Error('connect ETIMEDOUT 1.2.3.4:443'));
if (relay.includes('tls')) return Promise.reject(new Error('TLS handshake failure'));
if (relay.includes('ws')) return Promise.reject(new Error('websocket: unexpected server response'));
return Promise.reject(new Error('[publish:primary] Publish timed out after 200ms'));
}),
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://dns', 'wss://tcp', 'wss://tls', 'wss://ws', 'relay-timeout'], { id: 'evt-diag' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetry.push(JSON.parse(line)),
healthLogger: () => {},
diagnostics: { attemptId: 'attempt-42', correlationId: 'corr-42' },
}),
);
const summaryEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(summaryEvent);
const failureEvents = telemetry.filter((entry) => entry.event === 'lock_publish_failure');
const failureText = failureEvents.map((entry) => `${entry.relayUrl}:${entry.reason}`).join('\n');
assert.match(failureText, /wss:\/\/dns:dns_resolution/);
assert.match(failureText, /wss:\/\/tcp:tcp_connect_timeout/);
assert.match(failureText, /wss:\/\/tls:tls_handshake/);
assert.match(failureText, /wss:\/\/ws:websocket_open_failure/);
assert.match(failureText, /relay-timeout:publish_timeout/);
assert.deepStrictEqual(summaryEvent.reasonDistribution, {
dns_resolution: 1,
tcp_connect_timeout: 1,
tls_handshake: 1,
websocket_open_failure: 1,
publish_timeout: 1,
});
});
it('returns success when mixed relay outcomes still satisfy quorum', async () => {
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map((relay) => (
relay === 'wss://relay-ok'
? Promise.resolve('ok')
: Promise.reject(new Error('validation failed: rejected event'))
));
},
close: () => {},
};
const event = { id: 'evt-mixed' };
const result = await publishLock(['wss://relay-ok', 'wss://relay-bad'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
sleepFn: async () => {
throw new Error('should not sleep when quorum is satisfied');
},
healthLogger: () => {},
});
assert.strictEqual(result, event);
assert.strictEqual(publishCalls, 1);
});
it('uses fallback relays to satisfy min success quorum', async () => {
const mockPool = {
publish: (relays) => relays.map((relay) => (
relay.includes('fallback-ok') ? Promise.resolve('ok') : Promise.reject(new Error('offline'))
)),
close: () => {},
};
const event = { id: 'evt-2' };
const result = await publishLock(['wss://primary-bad'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => ['wss://fallback-ok'],
getMinActiveRelayPoolFn: () => 1,
healthLogger: () => {},
});
assert.strictEqual(result, event);
});
it('quarantines repeatedly failing relays and still reaches quorum with one healthy relay', async () => {
const callOrder = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
callOrder.push(relay);
if (relay === 'wss://healthy') {
return Promise.resolve('ok');
}
return Promise.reject(new Error('offline'));
}),
close: () => {},
};
const event = { id: 'evt-health' };
for (let i = 0; i < 3; i += 1) {
await publishLock(['wss://bad', 'wss://healthy'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
failureThreshold: 2,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
}
callOrder.length = 0;
await publishLock(['wss://bad', 'wss://healthy'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
failureThreshold: 2,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
assert.deepStrictEqual(callOrder, ['wss://healthy']);
});
it('uses fallback and min active pool to reintroduce quarantined relay when needed', async () => {
const attempts = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
attempts.push(relay);
if (relay === 'wss://fallback-healthy') {
return Promise.resolve('ok');
}
return Promise.reject(new Error('relay timeout'));
}),
close: () => {},
};
const event = { id: 'evt-quorum' };
await publishLock(['wss://primary-a', 'wss://primary-b'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => ['wss://fallback-healthy'],
getMinActiveRelayPoolFn: () => 2,
retryAttempts: 1,
failureThreshold: 1,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
assert.ok(attempts.includes('wss://fallback-healthy'));
});
});
});
+508
View File
@@ -0,0 +1,508 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import { publishLock } from '../src/lock-publisher.mjs';
import { defaultHealthManager } from '../src/relay-health-manager.mjs';
function _resetRelayHealthState() {
defaultHealthManager.reset();
}
function createScriptedPool(scriptedOutcomesByAttempt, relaysSeen) {
let publishCallCount = 0;
return {
publish: (relays) => {
publishCallCount += 1;
relaysSeen.push({ publishAttempt: publishCallCount, relays: [...relays] });
const attemptOutcomes = scriptedOutcomesByAttempt[publishCallCount - 1] || {};
return relays.map((relay) => {
const outcome = attemptOutcomes[relay] || { ok: true };
return outcome.ok ? Promise.resolve('ok') : Promise.reject(new Error(outcome.message));
});
},
close: () => {},
getPublishCallCount: () => publishCallCount,
};
}
const parseTelemetry = (logs) => logs.map((line) => JSON.parse(line));
describe('LockPublisher', () => {
beforeEach(() => {
_resetRelayHealthState();
});
afterEach(() => {
mock.timers.reset();
});
describe('publishLock basic and retry logic', () => {
it('retries transient failures and succeeds on a later attempt', async () => {
const telemetry = [];
const sleepCalls = [];
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
if (publishCalls === 1) {
return relays.map(() => Promise.reject(new Error('connection reset by peer')));
}
return relays.map(() => Promise.resolve('ok'));
},
close: () => {},
};
const event = { id: 'evt-retry' };
const result = await publishLock(['wss://primary-a', 'wss://primary-b'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 500,
retryCapDelayMs: 8_000,
randomFn: () => 0.5,
sleepFn: async (ms) => {
sleepCalls.push(ms);
},
telemetryLogger: (line) => {
telemetry.push(JSON.parse(line));
},
healthLogger: () => {},
diagnostics: {
attemptId: 'attempt-1',
correlationId: 'corr-1',
},
});
assert.strictEqual(result, event);
assert.strictEqual(publishCalls, 2);
assert.deepStrictEqual(sleepCalls, [250]);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 2);
assert.deepStrictEqual(retryEvents[0], {
event: 'lock_publish_retry',
correlationId: 'corr-1',
attemptId: 'attempt-1',
publishAttempt: 1,
relayUrl: 'wss://primary-a',
errorCategory: 'connection_reset',
elapsedMs: retryEvents[0].elapsedMs,
nextDelayMs: 250,
});
assert.deepStrictEqual(retryEvents[1], {
event: 'lock_publish_retry',
correlationId: 'corr-1',
attemptId: 'attempt-1',
publishAttempt: 1,
relayUrl: 'wss://primary-b',
errorCategory: 'connection_reset',
elapsedMs: retryEvents[1].elapsedMs,
nextDelayMs: 250,
});
});
it('fails after retry budget is exhausted for persistent transient failures', async () => {
let publishCalls = 0;
const sleepCalls = [];
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map(() => Promise.reject(new Error('relay unavailable')));
},
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://primary-down'], { id: 'evt-exhausted' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
randomFn: () => 1,
sleepFn: async (ms) => {
sleepCalls.push(ms);
},
telemetryLogger: () => {},
healthLogger: () => {},
}),
/Failed relay publish quorum in publish phase: 0\/1 successful \(required=1, timeout=200ms, attempts=3,/,
);
assert.strictEqual(publishCalls, 3);
assert.deepStrictEqual(sleepCalls, [500, 1000]);
});
it('fails without retry for persistent validation failures', async () => {
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map(() => Promise.reject(new Error('validation failed: invalid signature')));
},
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://primary-bad'], { id: 'evt-permanent' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
sleepFn: async () => {
throw new Error('should not sleep for permanent failures');
},
telemetryLogger: () => {},
healthLogger: () => {},
}),
/Failed relay publish quorum in publish phase: 0\/1 successful \(required=1, timeout=200ms, attempts=4,/,
);
assert.strictEqual(publishCalls, 1);
});
it('includes per-relay reason categories and retry metadata in failure diagnostics', async () => {
const telemetry = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
if (relay.includes('dns')) return Promise.reject(new Error('getaddrinfo ENOTFOUND relay.example'));
if (relay.includes('tcp')) return Promise.reject(new Error('connect ETIMEDOUT 1.2.3.4:443'));
if (relay.includes('tls')) return Promise.reject(new Error('TLS handshake failure'));
if (relay.includes('ws')) return Promise.reject(new Error('websocket: unexpected server response'));
return Promise.reject(new Error('[publish:primary] Publish timed out after 200ms'));
}),
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://dns', 'wss://tcp', 'wss://tls', 'wss://ws', 'relay-timeout'], { id: 'evt-diag' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetry.push(JSON.parse(line)),
healthLogger: () => {},
diagnostics: { attemptId: 'attempt-42', correlationId: 'corr-42' },
}),
);
const summaryEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(summaryEvent);
const failureEvents = telemetry.filter((entry) => entry.event === 'lock_publish_failure');
const failureText = failureEvents.map((entry) => `${entry.relayUrl}:${entry.reason}`).join('\n');
assert.match(failureText, /wss:\/\/dns:dns_resolution/);
assert.match(failureText, /wss:\/\/tcp:tcp_connect_timeout/);
assert.match(failureText, /wss:\/\/tls:tls_handshake/);
assert.match(failureText, /wss:\/\/ws:websocket_open_failure/);
assert.match(failureText, /relay-timeout:publish_timeout/);
assert.deepStrictEqual(summaryEvent.reasonDistribution, {
dns_resolution: 1,
tcp_connect_timeout: 1,
tls_handshake: 1,
websocket_open_failure: 1,
publish_timeout: 1,
});
});
it('returns success when mixed relay outcomes still satisfy quorum', async () => {
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map((relay) => (
relay === 'wss://relay-ok'
? Promise.resolve('ok')
: Promise.reject(new Error('validation failed: rejected event'))
));
},
close: () => {},
};
const event = { id: 'evt-mixed' };
const result = await publishLock(['wss://relay-ok', 'wss://relay-bad'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
sleepFn: async () => {
throw new Error('should not sleep when quorum is satisfied');
},
healthLogger: () => {},
});
assert.strictEqual(result, event);
assert.strictEqual(publishCalls, 1);
});
it('uses fallback relays to satisfy min success quorum', async () => {
const mockPool = {
publish: (relays) => relays.map((relay) => (
relay.includes('fallback-ok') ? Promise.resolve('ok') : Promise.reject(new Error('offline'))
)),
close: () => {},
};
const event = { id: 'evt-2' };
const result = await publishLock(['wss://primary-bad'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => ['wss://fallback-ok'],
getMinActiveRelayPoolFn: () => 1,
healthLogger: () => {},
});
assert.strictEqual(result, event);
});
it('quarantines repeatedly failing relays and still reaches quorum with one healthy relay', async () => {
const callOrder = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
callOrder.push(relay);
if (relay === 'wss://healthy') {
return Promise.resolve('ok');
}
return Promise.reject(new Error('offline'));
}),
close: () => {},
};
const event = { id: 'evt-health' };
for (let i = 0; i < 3; i += 1) {
await publishLock(['wss://bad', 'wss://healthy'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
failureThreshold: 2,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
}
callOrder.length = 0;
await publishLock(['wss://bad', 'wss://healthy'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
failureThreshold: 2,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
assert.deepStrictEqual(callOrder, ['wss://healthy']);
});
it('uses fallback and min active pool to reintroduce quarantined relay when needed', async () => {
const attempts = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
attempts.push(relay);
if (relay === 'wss://fallback-healthy') {
return Promise.resolve('ok');
}
return Promise.reject(new Error('relay timeout'));
}),
close: () => {},
};
const event = { id: 'evt-quorum' };
await publishLock(['wss://primary-a', 'wss://primary-b'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => ['wss://fallback-healthy'],
getMinActiveRelayPoolFn: () => 2,
retryAttempts: 1,
failureThreshold: 1,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
assert.ok(attempts.includes('wss://fallback-healthy'));
});
});
describe('scripted retry scenarios', () => {
it('succeeds immediately on first attempt without retries', async () => {
const telemetryLogs = [];
const relaysSeen = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
const event = { id: 'evt-immediate' };
const result = await publishLock(['wss://relay-a'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result, event);
assert.strictEqual(mockPool.getPublishCallCount(), 1);
assert.deepStrictEqual(relaysSeen, [{ publishAttempt: 1, relays: ['wss://relay-a'] }]);
const telemetry = parseTelemetry(telemetryLogs);
assert.strictEqual(telemetry.filter((entry) => entry.event === 'lock_publish_retry').length, 0);
const metEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(metEvent);
assert.strictEqual(metEvent.publishAttempt, 1);
});
it('retries a transient failure then succeeds within retry budget with bounded jitter', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const telemetryLogs = [];
const relaysSeen = [];
const sleepCalls = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'connection reset by peer' } },
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
const result = await publishLock(['wss://relay-a'], { id: 'evt-retry-success' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 100,
retryCapDelayMs: 500,
randomFn: () => 0.25,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result.id, 'evt-retry-success');
assert.deepStrictEqual(sleepCalls, [25]);
assert.strictEqual(mockPool.getPublishCallCount(), 2);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 1);
assert.strictEqual(retryEvents[0].publishAttempt, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[0].nextDelayMs >= 0 && retryEvents[0].nextDelayMs <= 100);
});
it('returns terminal relay_publish_quorum_failure after exhausting retry budget', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const telemetryLogs = [];
const relaysSeen = [];
const sleepCalls = [];
const randomValues = [0.1, 0.9];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
], relaysSeen);
await assert.rejects(
() => publishLock(['wss://relay-a'], { id: 'evt-exhausted' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 100,
retryCapDelayMs: 250,
randomFn: () => randomValues.shift() ?? 0,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_quorum_failure/,
);
assert.strictEqual(mockPool.getPublishCallCount(), 3);
assert.deepStrictEqual(sleepCalls, [10, 180]);
assert.deepStrictEqual(relaysSeen.map((entry) => entry.publishAttempt), [1, 2, 3]);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 2);
assert.strictEqual(retryEvents[0].publishAttempt, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[0].nextDelayMs >= 0 && retryEvents[0].nextDelayMs <= 100);
assert.strictEqual(retryEvents[1].publishAttempt, 2);
assert.strictEqual(retryEvents[1].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[1].nextDelayMs >= retryEvents[0].nextDelayMs);
assert.ok(retryEvents[1].nextDelayMs <= 200);
const failedEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(failedEvent);
assert.strictEqual(failedEvent.errorCategory, 'relay_publish_quorum_failure');
assert.strictEqual(failedEvent.attempts, 3);
});
it('short-circuits non-retryable failures without extra attempts', async () => {
const telemetryLogs = [];
const relaysSeen = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'validation failed: invalid signature' } },
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
await assert.rejects(
() => publishLock(['wss://relay-a'], { id: 'evt-permanent' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_non_retryable/,
);
assert.strictEqual(mockPool.getPublishCallCount(), 1);
assert.deepStrictEqual(relaysSeen, [{ publishAttempt: 1, relays: ['wss://relay-a'] }]);
const telemetry = parseTelemetry(telemetryLogs);
assert.strictEqual(telemetry.filter((entry) => entry.event === 'lock_publish_retry').length, 0);
const failedEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(failedEvent);
assert.strictEqual(failedEvent.errorCategory, 'relay_publish_non_retryable');
assert.notStrictEqual(failedEvent.errorCategory, 'relay_publish_quorum_failure');
});
});
});
+13
View File
@@ -77,3 +77,16 @@ test('blocks decrypted/session-sensitive data from durable promotion without san
assert.equal(events[1].durable_eligible, true);
assert.equal(cache.getMetrics().runtime_sensitive_events_blocked, 1);
});
test('prunes cache entries based on predicate', () => {
const cache = createMemoryCache();
cache.set('key1', 'value1');
cache.set('key2', 'value2');
cache.set('other', 'value3');
cache.prune((key) => key.startsWith('key'));
assert.equal(cache.get('key1'), undefined);
assert.equal(cache.get('key2'), undefined);
assert.equal(cache.get('other'), 'value3');
});
+69 -29
View File
@@ -5,8 +5,9 @@ import { getRoster, _resetRosterCache, _setRosterDependencies, _restoreRosterDep
// Mock dependencies
const mockFs = {
existsSync: mock.fn(),
readFileSync: mock.fn(),
promises: {
readFile: mock.fn(),
},
};
const mockLoadTorchConfig = mock.fn();
@@ -25,18 +26,18 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
console.error = mockConsoleError;
_resetRosterCache();
mockFs.existsSync.mock.resetCalls();
mockFs.readFileSync.mock.resetCalls();
mockFs.promises.readFile.mock.resetCalls();
mockLoadTorchConfig.mock.resetCalls();
// Default: config returns empty nostrLock
mockLoadTorchConfig.mock.mockImplementation(async () => ({ nostrLock: {} }));
// Default: fs.existsSync returns false (no files found)
mockFs.existsSync.mock.mockImplementation(() => false);
// Default: fs.readFileSync returns empty object
mockFs.readFileSync.mock.mockImplementation(() => '{}');
// Default: fs.promises.readFile returns ENOENT
mockFs.promises.readFile.mock.mockImplementation(async () => {
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
// Inject mocks
_setRosterDependencies({
@@ -63,11 +64,20 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
});
it('loads internal module roster if user/cwd rosters are missing', async () => {
// User/CWD missing
mockFs.existsSync.mock.mockImplementation(() => false);
// When reading file, if it's not USER or CWD (implied by default ENOENT), check ROSTER_FILE logic?
// Wait, the logic tries USER -> CWD -> ROSTER.
// So if first two fail (default), third one should succeed for this test.
// When reading file, return internal roster content
mockFs.readFileSync.mock.mockImplementation(() => {
// We need to verify that it eventually reads internal roster (ROSTER_FILE).
// But since we don't know the exact path of ROSTER_FILE easily in test (it depends on import.meta.url),
// we can just say "if not USER or CWD, return content".
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === USER_ROSTER_FILE || filepath === CWD_ROSTER_FILE) {
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
}
return JSON.stringify({ daily: ['internal-daily'], weekly: ['internal-weekly'] });
});
@@ -76,12 +86,13 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
});
it('loads roster from torch/roster.json (User Roster) if present', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation((filepath) => {
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === USER_ROSTER_FILE) {
return JSON.stringify({ daily: ['user-agent-daily'], weekly: ['user-agent-weekly'] });
}
throw new Error('ENOENT');
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
const daily = await getRoster('daily');
@@ -89,16 +100,21 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
assert.deepStrictEqual(daily, ['user-agent-daily']);
assert.deepStrictEqual(weekly, ['user-agent-weekly']);
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1); // Should be cached after first call
// Should be cached after first call
// Note: getRoster calls loadCanonicalRoster each time but it caches result internally.
// However, since we call getRoster twice, and result is cached, readFile should be called once.
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
});
it('loads roster from roster.json (CWD Roster) if present and torch/roster.json is missing', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === CWD_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation((filepath) => {
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === CWD_ROSTER_FILE) {
return JSON.stringify({ daily: ['cwd-agent-daily'], weekly: ['cwd-agent-weekly'] });
}
throw new Error('ENOENT');
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
const daily = await getRoster('daily');
@@ -106,20 +122,32 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
});
it('falls back to default if roster file is malformed', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => '{ invalid json }');
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === USER_ROSTER_FILE) {
return '{ invalid json }';
}
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
// Should catch error and return fallback
const daily = await getRoster('daily');
assert.ok(daily.includes('audit-agent'));
// Should have tried to read file
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1);
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
});
it('falls back to default if roster file is missing daily/weekly arrays', async () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ other: [] }));
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === USER_ROSTER_FILE) {
return JSON.stringify({ other: [] });
}
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
const daily = await getRoster('daily');
assert.ok(daily.includes('audit-agent'));
@@ -129,8 +157,14 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
process.env.NOSTR_LOCK_DAILY_ROSTER = 'env-agent-daily';
// Setup file to exist
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === USER_ROSTER_FILE) {
return JSON.stringify({ daily: ['file-agent'] });
}
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
const daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['env-agent-daily']);
@@ -142,8 +176,14 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
}));
// Setup file to exist
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
mockFs.promises.readFile.mock.mockImplementation(async (filepath) => {
if (filepath === USER_ROSTER_FILE) {
return JSON.stringify({ daily: ['file-agent'] });
}
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
});
const daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['config-agent-daily']);