mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge pull request #260 from PR0M3TH3AN/jules-perf-async-config-1969443791138192182
⚡ Async Config Loading Optimization
This commit is contained in:
13 files changed
+215
-146
No files matched your search
+6
-15
@@ -7,20 +7,11 @@ import { getRoster } from './roster.mjs';
|
||||
import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr } from './utils.mjs';
|
||||
|
||||
export async function cmdCheck(cadence, deps = {}) {
|
||||
const {
|
||||
getRelaysFn = getRelays,
|
||||
getNamespaceFn = getNamespace,
|
||||
loadTorchConfigFn = loadTorchConfig,
|
||||
getRosterFn = getRoster,
|
||||
queryLocksFn = queryLocks,
|
||||
todayDateStrFn = todayDateStr,
|
||||
} = deps;
|
||||
|
||||
const relays = getRelaysFn();
|
||||
const namespace = getNamespaceFn();
|
||||
const dateStr = todayDateStrFn();
|
||||
const config = loadTorchConfigFn();
|
||||
export async function cmdCheck(cadence) {
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const config = await loadTorchConfig();
|
||||
const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || [];
|
||||
|
||||
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
@@ -31,7 +22,7 @@ export async function cmdCheck(cadence, deps = {}) {
|
||||
|
||||
const locks = await queryLocksFn(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = getRosterFn(cadence);
|
||||
const roster = await getRoster(cadence);
|
||||
|
||||
const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])];
|
||||
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
||||
|
||||
+3
-3
@@ -7,8 +7,8 @@ import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr, nowUnix } from './utils.mjs';
|
||||
|
||||
export async function cmdList(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const cadences = cadence ? [cadence] : ['daily', 'weekly'];
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function cmdList(cadence) {
|
||||
);
|
||||
}
|
||||
|
||||
const roster = getRoster(c);
|
||||
const roster = await getRoster(c);
|
||||
const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean));
|
||||
const unknownLockedAgents = [...lockedAgents].filter((agent) => !roster.includes(agent));
|
||||
const available = roster.filter((a) => !lockedAgents.has(a));
|
||||
|
||||
+4
-4
@@ -14,10 +14,10 @@ import { todayDateStr, nowUnix } from './utils.mjs';
|
||||
import { ExitError } from './errors.mjs';
|
||||
|
||||
export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const ttl = getTtl();
|
||||
const ttl = await getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const roster = getRoster(cadence);
|
||||
const roster = await getRoster(cadence);
|
||||
if (!roster.includes(agent)) {
|
||||
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
|
||||
+23
-14
@@ -22,9 +22,10 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
// Resolve package root relative to this file (src/dashboard.mjs)
|
||||
// this file is in <root>/src/dashboard.mjs, so '..' goes to <root>
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const auth = getDashboardAuth();
|
||||
const auth = await getDashboardAuth();
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
// Basic Auth check
|
||||
if (auth) {
|
||||
const authHeader = req.headers.authorization;
|
||||
@@ -181,20 +182,28 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;
|
||||
console.log(`Dashboard running at ${listenUrl}`);
|
||||
if (auth) {
|
||||
console.log('Authentication: enabled (Basic Auth)');
|
||||
} else {
|
||||
console.warn('Authentication: DISABLED (Dashboard is public)');
|
||||
} catch (err) {
|
||||
console.error('Dashboard Server Error:', err);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500);
|
||||
res.end('Internal Server Error');
|
||||
}
|
||||
}
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
return server;
|
||||
return new Promise((resolve, reject) => {
|
||||
server.listen(port, host, () => {
|
||||
const listenUrl = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}/dashboard/`;
|
||||
console.log(`Dashboard running at ${listenUrl}`);
|
||||
if (auth) {
|
||||
console.log('Authentication: enabled (Basic Auth)');
|
||||
} else {
|
||||
console.warn('Authentication: DISABLED (Dashboard is public)');
|
||||
}
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
resolve(server);
|
||||
});
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
+15
-15
@@ -75,10 +75,10 @@ export async function cmdCheck(cadence, deps = {}) {
|
||||
quiet = false,
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = getDateStr();
|
||||
const config = loadTorchConfig();
|
||||
const config = await loadTorchConfig();
|
||||
const pausedAgents = config.scheduler.paused[cadence] || [];
|
||||
|
||||
if (!quiet) {
|
||||
@@ -99,7 +99,7 @@ export async function cmdCheck(cadence, deps = {}) {
|
||||
|
||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = getRoster(cadence);
|
||||
const roster = await getRoster(cadence);
|
||||
const rosterSet = new Set(roster);
|
||||
|
||||
const excludedAgentsSet = new Set([...lockedAgents, ...pausedAgents, ...completedAgents]);
|
||||
@@ -180,11 +180,11 @@ export async function cmdLock(agent, cadence, optionsOrDryRun = false, deps = {}
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const hashtag = getHashtag();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const hashtag = await getHashtag();
|
||||
const dateStr = getDateStr();
|
||||
const ttl = getTtl();
|
||||
const ttl = await getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
@@ -193,7 +193,7 @@ export async function cmdLock(agent, cadence, optionsOrDryRun = false, deps = {}
|
||||
error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const roster = getRoster(cadence);
|
||||
const roster = await getRoster(cadence);
|
||||
if (!roster.includes(agent)) {
|
||||
error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
@@ -319,8 +319,8 @@ export async function cmdList(cadence, deps = {}) {
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const dateStr = getDateStr();
|
||||
const cadences = cadence ? [cadence] : [...VALID_CADENCES];
|
||||
|
||||
@@ -365,7 +365,7 @@ export async function cmdList(cadence, deps = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
const roster = getRoster(c);
|
||||
const roster = await getRoster(c);
|
||||
const rosterSet = new Set(roster);
|
||||
const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean));
|
||||
const unknownLockedAgents = [...lockedAgents].filter((agent) => !rosterSet.has(agent));
|
||||
@@ -413,9 +413,9 @@ export async function cmdComplete(agent, cadence, optionsOrDryRun = false, deps
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const hashtag = getHashtag();
|
||||
const relays = await getRelays();
|
||||
const namespace = await getNamespace();
|
||||
const hashtag = await getHashtag();
|
||||
const dateStr = getDateStr();
|
||||
const now = nowUnix();
|
||||
|
||||
|
||||
+7
-7
@@ -306,11 +306,11 @@ export async function queryLocks(relays, cadence, dateStr, namespace, deps = {})
|
||||
|
||||
const pool = poolFactory();
|
||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||
const queryTimeoutMs = getQueryTimeoutMsFn();
|
||||
const fallbackRelays = getRelayFallbacksFn().filter((relay) => !relays.includes(relay));
|
||||
const queryTimeoutMs = await getQueryTimeoutMsFn();
|
||||
const fallbackRelays = (await getRelayFallbacksFn()).filter((relay) => !relays.includes(relay));
|
||||
const healthConfig = buildRelayHealthConfig({
|
||||
...deps,
|
||||
minActiveRelayPool: getMinActiveRelayPoolFn(),
|
||||
minActiveRelayPool: await getMinActiveRelayPoolFn(),
|
||||
});
|
||||
|
||||
const runQuery = async (relaySet, phase) => {
|
||||
@@ -419,13 +419,13 @@ export async function publishLock(relays, event, deps = {}) {
|
||||
const attemptId = diagnostics.attemptId || process.env.SCHEDULER_LOCK_ATTEMPT_ID || '1';
|
||||
|
||||
const pool = poolFactory();
|
||||
const publishTimeoutMs = getPublishTimeoutMsFn();
|
||||
const minSuccesses = getMinSuccessfulRelayPublishesFn();
|
||||
const fallbackRelays = getRelayFallbacksFn().filter((relay) => !relays.includes(relay));
|
||||
const publishTimeoutMs = await getPublishTimeoutMsFn();
|
||||
const minSuccesses = await getMinSuccessfulRelayPublishesFn();
|
||||
const fallbackRelays = (await getRelayFallbacksFn()).filter((relay) => !relays.includes(relay));
|
||||
const maxAttempts = Math.max(1, Math.floor(retryAttempts));
|
||||
const healthConfig = buildRelayHealthConfig({
|
||||
...deps,
|
||||
minActiveRelayPool: getMinActiveRelayPoolFn(),
|
||||
minActiveRelayPool: await getMinActiveRelayPoolFn(),
|
||||
});
|
||||
|
||||
const publishOnce = async () => {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { loadTorchConfig, _resetTorchConfigCache } from '../torch-config.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const configPath = path.resolve(process.cwd(), 'torch-config.json');
|
||||
const originalConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath) : null;
|
||||
|
||||
// Ensure a config file exists for benchmark
|
||||
if (!fs.existsSync(configPath)) {
|
||||
fs.writeFileSync(configPath, JSON.stringify({
|
||||
nostrLock: {
|
||||
relays: ["wss://relay.damus.io"],
|
||||
namespace: "test-ns"
|
||||
}
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
const ITERATIONS = 1000;
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
async function run() {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
_resetTorchConfigCache();
|
||||
try {
|
||||
const res = await loadTorchConfig();
|
||||
// Use res to prevent dead code elimination if JIT is smart (though side effects exist)
|
||||
if (!res) throw new Error("Failed");
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const end = process.hrtime.bigint();
|
||||
const duration = Number(end - start) / 1e6; // ms
|
||||
|
||||
console.log(`[ASYNC] Loaded config ${ITERATIONS} times in ${duration.toFixed(2)}ms`);
|
||||
console.log(`[ASYNC] Average: ${(duration / ITERATIONS).toFixed(4)}ms per load`);
|
||||
|
||||
// Cleanup
|
||||
if (!originalConfig && fs.existsSync(configPath)) {
|
||||
fs.unlinkSync(configPath);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
+10
-3
@@ -196,15 +196,22 @@ async function appendHistory(historyPath, entry) {
|
||||
export async function runRelayHealthCheck(options = {}) {
|
||||
const {
|
||||
cadence = 'daily',
|
||||
relays = getRelays(),
|
||||
namespace = getNamespace(),
|
||||
timeoutMs = 6000,
|
||||
historyPath = path.resolve(process.cwd(), 'task-logs', 'relay-health', `${cadence}.jsonl`),
|
||||
allRelaysDownMinutes = 10,
|
||||
minSuccessRate = 0.7,
|
||||
windowMinutes = 60,
|
||||
} = options;
|
||||
|
||||
let {
|
||||
relays,
|
||||
namespace,
|
||||
historyPath,
|
||||
} = options;
|
||||
|
||||
if (!relays) relays = await getRelays();
|
||||
if (!namespace) namespace = await getNamespace();
|
||||
if (!historyPath) historyPath = path.resolve(process.cwd(), 'task-logs', 'relay-health', `${cadence}.jsonl`);
|
||||
|
||||
const relayResults = [];
|
||||
for (const relay of relays) {
|
||||
const relayResult = {
|
||||
|
||||
+2
-2
@@ -94,8 +94,8 @@ function parseEnvRoster(value) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function getRoster(cadence) {
|
||||
const config = loadTorchConfig();
|
||||
export async function getRoster(cadence) {
|
||||
const config = await loadTorchConfig();
|
||||
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
|
||||
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
|
||||
const canonical = loadCanonicalRoster();
|
||||
|
||||
+28
-26
@@ -127,18 +127,20 @@ export function _resetTorchConfigCache() {
|
||||
cachedConfig = null;
|
||||
}
|
||||
|
||||
export function loadTorchConfig(fileSystem = fs) {
|
||||
export async function loadTorchConfig(fileSystem = fs) {
|
||||
if (cachedConfig) return cachedConfig;
|
||||
|
||||
const configPath = getTorchConfigPath();
|
||||
let raw = {};
|
||||
|
||||
if (fileSystem.existsSync(configPath)) {
|
||||
try {
|
||||
raw = JSON.parse(fileSystem.readFileSync(configPath, 'utf8'));
|
||||
} catch (err) {
|
||||
try {
|
||||
const content = await fileSystem.promises.readFile(configPath, 'utf8');
|
||||
raw = JSON.parse(content);
|
||||
} catch (err) {
|
||||
if (err.code !== 'ENOENT') {
|
||||
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
||||
}
|
||||
// If file missing (ENOENT), we use empty object defaults
|
||||
}
|
||||
|
||||
cachedConfig = parseTorchConfig(raw, configPath);
|
||||
@@ -199,8 +201,8 @@ function resolveIntegerConfig(envKey, configValue, defaultValue, validator, conf
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getRelays() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getRelays() {
|
||||
const config = await loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
||||
if (envRelays) {
|
||||
return parseEnvRelayList(envRelays, 'NOSTR_LOCK_RELAYS');
|
||||
@@ -214,8 +216,8 @@ export function getRelays() {
|
||||
return DEFAULT_RELAYS;
|
||||
}
|
||||
|
||||
export function getRelayFallbacks() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getRelayFallbacks() {
|
||||
const config = await loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAY_FALLBACKS;
|
||||
if (envRelays) {
|
||||
return parseEnvRelayList(envRelays, 'NOSTR_LOCK_RELAY_FALLBACKS');
|
||||
@@ -223,14 +225,14 @@ export function getRelayFallbacks() {
|
||||
return config.nostrLock.relayFallbacks || [];
|
||||
}
|
||||
|
||||
export function getNamespace() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getNamespace() {
|
||||
const config = await loadTorchConfig();
|
||||
const namespace = (process.env.NOSTR_LOCK_NAMESPACE || config.nostrLock.namespace || DEFAULT_NAMESPACE).trim();
|
||||
return namespace || DEFAULT_NAMESPACE;
|
||||
}
|
||||
|
||||
export function getTtl() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getTtl() {
|
||||
const config = await loadTorchConfig();
|
||||
const envTtl = process.env.NOSTR_LOCK_TTL;
|
||||
if (envTtl) {
|
||||
const parsed = parseInt(envTtl, 10);
|
||||
@@ -242,8 +244,8 @@ export function getTtl() {
|
||||
return DEFAULT_TTL;
|
||||
}
|
||||
|
||||
export function getQueryTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getQueryTimeoutMs() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_QUERY_TIMEOUT_MS',
|
||||
config.nostrLock.queryTimeoutMs,
|
||||
@@ -253,8 +255,8 @@ export function getQueryTimeoutMs() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getPublishTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getPublishTimeoutMs() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_PUBLISH_TIMEOUT_MS',
|
||||
config.nostrLock.publishTimeoutMs,
|
||||
@@ -264,8 +266,8 @@ export function getPublishTimeoutMs() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getMinSuccessfulRelayPublishes() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getMinSuccessfulRelayPublishes() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES',
|
||||
config.nostrLock.minSuccessfulRelayPublishes,
|
||||
@@ -275,8 +277,8 @@ export function getMinSuccessfulRelayPublishes() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getMinActiveRelayPool() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getMinActiveRelayPool() {
|
||||
const config = await loadTorchConfig();
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL',
|
||||
config.nostrLock.minActiveRelayPool,
|
||||
@@ -286,8 +288,8 @@ export function getMinActiveRelayPool() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getHashtag() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getHashtag() {
|
||||
const config = await loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_HASHTAG;
|
||||
if (envValue) {
|
||||
return envValue.trim();
|
||||
@@ -295,12 +297,12 @@ export function getHashtag() {
|
||||
if (config.dashboard.hashtag) {
|
||||
return config.dashboard.hashtag;
|
||||
}
|
||||
const namespace = getNamespace();
|
||||
const namespace = await getNamespace();
|
||||
return `${namespace}-agent-lock`;
|
||||
}
|
||||
|
||||
export function getDashboardAuth() {
|
||||
const config = loadTorchConfig();
|
||||
export async function getDashboardAuth() {
|
||||
const config = await loadTorchConfig();
|
||||
const envValue = process.env.TORCH_DASHBOARD_AUTH;
|
||||
if (envValue) {
|
||||
return envValue.trim();
|
||||
|
||||
@@ -5,15 +5,17 @@ import http from 'node:http';
|
||||
import { cmdDashboard } from '../src/dashboard.mjs';
|
||||
|
||||
test('Dashboard File Access Security', async (t) => {
|
||||
const testPort = 4175;
|
||||
let testPort = 0;
|
||||
let server;
|
||||
|
||||
t.beforeEach(async () => {
|
||||
server = await cmdDashboard(testPort);
|
||||
server = await cmdDashboard(0);
|
||||
testPort = server.address().port;
|
||||
});
|
||||
|
||||
t.afterEach(() => {
|
||||
if (server) server.close();
|
||||
t.afterEach((done) => {
|
||||
if (server) server.close(done);
|
||||
else done();
|
||||
});
|
||||
|
||||
function get(path) {
|
||||
|
||||
@@ -7,83 +7,96 @@ describe('loadTorchConfig (Unit Tests with Mocked FS)', () => {
|
||||
_resetTorchConfigCache();
|
||||
});
|
||||
|
||||
it('loads valid config correctly', () => {
|
||||
it('loads valid config correctly', async () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({
|
||||
nostrLock: { namespace: 'unit-test-ns' }
|
||||
}))
|
||||
promises: {
|
||||
readFile: mock.fn(async () => JSON.stringify({
|
||||
nostrLock: { namespace: 'unit-test-ns' }
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const config = loadTorchConfig(mockFs);
|
||||
const config = await loadTorchConfig(mockFs);
|
||||
|
||||
assert.strictEqual(config.nostrLock.namespace, 'unit-test-ns');
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1);
|
||||
// loadTorchConfig no longer calls existsSync on the passed fs, only readFile (and catches ENOENT)
|
||||
// Actually, getTorchConfigPath calls existsSync on REAL fs.
|
||||
// So checking mockFs.existsSync is wrong if loadTorchConfig doesn't call it.
|
||||
// In my change, loadTorchConfig calls await fileSystem.promises.readFile.
|
||||
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
|
||||
});
|
||||
|
||||
it('returns default config when file is missing', () => {
|
||||
it('returns default config when file is missing', async () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => false),
|
||||
readFileSync: mock.fn()
|
||||
promises: {
|
||||
readFile: mock.fn(async () => {
|
||||
const err = new Error('ENOENT');
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
const config = loadTorchConfig(mockFs);
|
||||
const config = await loadTorchConfig(mockFs);
|
||||
|
||||
assert.strictEqual(config.nostrLock.namespace, null);
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 0);
|
||||
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
|
||||
});
|
||||
|
||||
it('throws error on malformed JSON', () => {
|
||||
it('throws error on malformed JSON', async () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => '{ invalid json }')
|
||||
promises: {
|
||||
readFile: mock.fn(async () => '{ invalid json }')
|
||||
}
|
||||
};
|
||||
|
||||
assert.throws(() => {
|
||||
loadTorchConfig(mockFs);
|
||||
await assert.rejects(async () => {
|
||||
await loadTorchConfig(mockFs);
|
||||
}, /Failed to parse/);
|
||||
});
|
||||
|
||||
it('caches the config and ignores subsequent fs calls', () => {
|
||||
it('caches the config and ignores subsequent fs calls', async () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({ nostrLock: { namespace: 'cached' } }))
|
||||
promises: {
|
||||
readFile: mock.fn(async () => JSON.stringify({ nostrLock: { namespace: 'cached' } }))
|
||||
}
|
||||
};
|
||||
|
||||
const config1 = loadTorchConfig(mockFs);
|
||||
const config1 = await loadTorchConfig(mockFs);
|
||||
assert.strictEqual(config1.nostrLock.namespace, 'cached');
|
||||
|
||||
// Second call with a different "fs" (or same) should not trigger fs operations
|
||||
const mockFs2 = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({ nostrLock: { namespace: 'changed' } }))
|
||||
promises: {
|
||||
readFile: mock.fn(async () => JSON.stringify({ nostrLock: { namespace: 'changed' } }))
|
||||
}
|
||||
};
|
||||
|
||||
const config2 = loadTorchConfig(mockFs2);
|
||||
const config2 = await loadTorchConfig(mockFs2);
|
||||
|
||||
assert.strictEqual(config2.nostrLock.namespace, 'cached');
|
||||
assert.strictEqual(config1, config2);
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs2.existsSync.mock.calls.length, 0);
|
||||
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs2.promises.readFile.mock.calls.length, 0);
|
||||
});
|
||||
|
||||
it('returns null for empty string lists (new consistent behavior)', () => {
|
||||
it('returns null for empty string lists (new consistent behavior)', async () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({
|
||||
nostrLock: {
|
||||
relays: [],
|
||||
relayFallbacks: []
|
||||
},
|
||||
dashboard: {
|
||||
relays: []
|
||||
}
|
||||
}))
|
||||
promises: {
|
||||
readFile: mock.fn(async () => JSON.stringify({
|
||||
nostrLock: {
|
||||
relays: [],
|
||||
relayFallbacks: []
|
||||
},
|
||||
dashboard: {
|
||||
relays: []
|
||||
}
|
||||
}))
|
||||
}
|
||||
};
|
||||
|
||||
const config = loadTorchConfig(mockFs);
|
||||
const config = await loadTorchConfig(mockFs);
|
||||
|
||||
// New behavior: all return null
|
||||
assert.strictEqual(config.nostrLock.relays, null);
|
||||
|
||||
+14
-14
@@ -78,22 +78,22 @@ describe('torch-config', () => {
|
||||
_resetTorchConfigCache();
|
||||
});
|
||||
|
||||
it('throws fatal error for invalid relay URL', () => {
|
||||
it('throws fatal error for invalid relay URL', async () => {
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify({ nostrLock: { relays: ['https://bad'] } }));
|
||||
process.env.TORCH_CONFIG_PATH = tempConfigPath;
|
||||
assert.throws(() => loadTorchConfig(), /Invalid relay URL in nostrLock\.relays/);
|
||||
await assert.rejects(() => loadTorchConfig(), /Invalid relay URL in nostrLock\.relays/);
|
||||
});
|
||||
|
||||
it('throws fatal error for invalid timeout range', () => {
|
||||
it('throws fatal error for invalid timeout range', async () => {
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify({ nostrLock: { queryTimeoutMs: 50 } }));
|
||||
process.env.TORCH_CONFIG_PATH = tempConfigPath;
|
||||
assert.throws(() => loadTorchConfig(), /Invalid nostrLock\.queryTimeoutMs/);
|
||||
await assert.rejects(() => loadTorchConfig(), /Invalid nostrLock\.queryTimeoutMs/);
|
||||
});
|
||||
|
||||
it('throws error for malformed JSON', () => {
|
||||
it('throws error for malformed JSON', async () => {
|
||||
fs.writeFileSync(badConfigPath, '{ invalid json }');
|
||||
process.env.TORCH_CONFIG_PATH = badConfigPath;
|
||||
assert.throws(() => loadTorchConfig(), /Failed to parse/);
|
||||
await assert.rejects(() => loadTorchConfig(), /Failed to parse/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -125,28 +125,28 @@ describe('torch-config', () => {
|
||||
_resetTorchConfigCache();
|
||||
});
|
||||
|
||||
it('returns default relays', () => {
|
||||
assert.deepStrictEqual(getRelays(), DEFAULT_RELAYS);
|
||||
it('returns default relays', async () => {
|
||||
assert.deepStrictEqual(await getRelays(), DEFAULT_RELAYS);
|
||||
});
|
||||
|
||||
it('validates env knob values', () => {
|
||||
it('validates env knob values', async () => {
|
||||
process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS = '99';
|
||||
assert.throws(() => getQueryTimeoutMs(), /NOSTR_LOCK_QUERY_TIMEOUT_MS/);
|
||||
await assert.rejects(() => getQueryTimeoutMs(), /NOSTR_LOCK_QUERY_TIMEOUT_MS/);
|
||||
|
||||
process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS = '5000';
|
||||
process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS = 'abc';
|
||||
assert.throws(() => getPublishTimeoutMs(), /NOSTR_LOCK_PUBLISH_TIMEOUT_MS/);
|
||||
await assert.rejects(() => getPublishTimeoutMs(), /NOSTR_LOCK_PUBLISH_TIMEOUT_MS/);
|
||||
|
||||
process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS = '8000';
|
||||
process.env.NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES = '0';
|
||||
assert.throws(() => getMinSuccessfulRelayPublishes(), /NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES/);
|
||||
await assert.rejects(() => getMinSuccessfulRelayPublishes(), /NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES/);
|
||||
|
||||
process.env.NOSTR_LOCK_RELAY_FALLBACKS = 'https://bad';
|
||||
assert.throws(() => getRelayFallbacks(), /NOSTR_LOCK_RELAY_FALLBACKS/);
|
||||
await assert.rejects(() => getRelayFallbacks(), /NOSTR_LOCK_RELAY_FALLBACKS/);
|
||||
|
||||
process.env.NOSTR_LOCK_RELAY_FALLBACKS = '';
|
||||
process.env.NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL = '0';
|
||||
assert.throws(() => getMinActiveRelayPool(), /NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL/);
|
||||
await assert.rejects(() => getMinActiveRelayPool(), /NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user