mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge branch 'main' into test/unpin-memory-coverage-10292270866664076558
This commit is contained in:
5 files changed
+204
-64
No files matched your search
@@ -295,14 +295,8 @@ async function artifactExistsSince(filePath, sinceMs) {
|
||||
|
||||
async function verifyMemoryStep({ name, markers, artifacts, outputText, sinceMs }) {
|
||||
const markerMatched = markers.some((marker) => outputText.includes(marker));
|
||||
let artifactMatched = false;
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
if (await artifactExistsSince(artifact, sinceMs)) {
|
||||
artifactMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const artifactResults = await Promise.all(artifacts.map((artifact) => artifactExistsSince(artifact, sinceMs)));
|
||||
const artifactMatched = artifactResults.some(Boolean);
|
||||
|
||||
return {
|
||||
name,
|
||||
|
||||
@@ -182,10 +182,16 @@ async function main() {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const promises = entries.map(async (entry) => {
|
||||
const fullPath = path.join(cadenceDir, entry);
|
||||
logsScanned += 1;
|
||||
const record = await readLogFile(fullPath, cadence, cutoffMs);
|
||||
return record;
|
||||
});
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
logsScanned += entries.length;
|
||||
|
||||
for (const record of results) {
|
||||
if (!record) continue;
|
||||
|
||||
records.push(record);
|
||||
|
||||
+49
-54
@@ -21,9 +21,9 @@ function parsePositiveInteger(value) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
function parseRelayList(value) {
|
||||
function parseStringList(value) {
|
||||
if (!Array.isArray(value)) return null;
|
||||
const parsed = value.map((relay) => String(relay).trim()).filter(Boolean);
|
||||
const parsed = value.map((item) => String(item).trim()).filter(Boolean);
|
||||
return parsed.length > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
@@ -63,11 +63,6 @@ function normalizeStatus(value, fallback) {
|
||||
return normalized === 'active' || normalized === 'all' ? normalized : fallback;
|
||||
}
|
||||
|
||||
function parseRoster(value) {
|
||||
if (!Array.isArray(value)) return null;
|
||||
const parsed = value.map((item) => String(item).trim()).filter(Boolean);
|
||||
return parsed.length > 0 ? parsed : null;
|
||||
}
|
||||
|
||||
export function getTorchConfigPath() {
|
||||
const explicitPath = (process.env.TORCH_CONFIG_PATH || '').trim();
|
||||
@@ -94,10 +89,8 @@ export function parseTorchConfig(raw, configPath = null) {
|
||||
raw,
|
||||
nostrLock: {
|
||||
namespace: typeof nostrLock.namespace === 'string' ? nostrLock.namespace.trim() : null,
|
||||
relays: Array.isArray(nostrLock.relays)
|
||||
? nostrLock.relays.map((relay) => String(relay).trim()).filter(Boolean)
|
||||
: null,
|
||||
relayFallbacks: parseRelayList(nostrLock.relayFallbacks),
|
||||
relays: parseStringList(nostrLock.relays),
|
||||
relayFallbacks: parseStringList(nostrLock.relayFallbacks),
|
||||
ttlSeconds: Number.isFinite(nostrLock.ttlSeconds) && nostrLock.ttlSeconds > 0
|
||||
? Math.floor(nostrLock.ttlSeconds)
|
||||
: null,
|
||||
@@ -105,15 +98,13 @@ export function parseTorchConfig(raw, configPath = null) {
|
||||
publishTimeoutMs: parsePositiveInteger(nostrLock.publishTimeoutMs),
|
||||
minSuccessfulRelayPublishes: parsePositiveInteger(nostrLock.minSuccessfulRelayPublishes),
|
||||
minActiveRelayPool: parsePositiveInteger(nostrLock.minActiveRelayPool),
|
||||
dailyRoster: parseRoster(nostrLock.dailyRoster),
|
||||
weeklyRoster: parseRoster(nostrLock.weeklyRoster),
|
||||
dailyRoster: parseStringList(nostrLock.dailyRoster),
|
||||
weeklyRoster: parseStringList(nostrLock.weeklyRoster),
|
||||
},
|
||||
dashboard: {
|
||||
defaultCadenceView: normalizeCadence(dashboard.defaultCadenceView, 'daily'),
|
||||
defaultStatusView: normalizeStatus(dashboard.defaultStatusView, 'active'),
|
||||
relays: Array.isArray(dashboard.relays)
|
||||
? dashboard.relays.map((relay) => String(relay).trim()).filter(Boolean)
|
||||
: null,
|
||||
relays: parseStringList(dashboard.relays),
|
||||
namespace: typeof dashboard.namespace === 'string' ? dashboard.namespace.trim() : null,
|
||||
hashtag: typeof dashboard.hashtag === 'string' ? dashboard.hashtag.trim() : null,
|
||||
auth: typeof dashboard.auth === 'string' ? dashboard.auth.trim() : null,
|
||||
@@ -124,8 +115,8 @@ export function parseTorchConfig(raw, configPath = null) {
|
||||
weekly: typeof firstPromptByCadence.weekly === 'string' ? firstPromptByCadence.weekly.trim() : null,
|
||||
},
|
||||
paused: {
|
||||
daily: parseRoster(paused.daily) || [],
|
||||
weekly: parseRoster(paused.weekly) || [],
|
||||
daily: parseStringList(paused.daily) || [],
|
||||
weekly: parseStringList(paused.weekly) || [],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -196,6 +187,18 @@ function parseEnvInteger(envValue, envName) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resolveIntegerConfig(envKey, configValue, defaultValue, validator, configLabel) {
|
||||
const envValue = process.env[envKey];
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, envKey);
|
||||
validator(parsed, envKey);
|
||||
return parsed;
|
||||
}
|
||||
const value = configValue || defaultValue;
|
||||
validator(value, `effective ${configLabel}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function getRelays() {
|
||||
const config = loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
||||
@@ -241,54 +244,46 @@ export function getTtl() {
|
||||
|
||||
export function getQueryTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_QUERY_TIMEOUT_MS');
|
||||
assertTimeoutInRange(parsed, 'NOSTR_LOCK_QUERY_TIMEOUT_MS');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.queryTimeoutMs || DEFAULT_QUERY_TIMEOUT_MS;
|
||||
assertTimeoutInRange(value, 'effective query timeout');
|
||||
return value;
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_QUERY_TIMEOUT_MS',
|
||||
config.nostrLock.queryTimeoutMs,
|
||||
DEFAULT_QUERY_TIMEOUT_MS,
|
||||
assertTimeoutInRange,
|
||||
'query timeout',
|
||||
);
|
||||
}
|
||||
|
||||
export function getPublishTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_PUBLISH_TIMEOUT_MS');
|
||||
assertTimeoutInRange(parsed, 'NOSTR_LOCK_PUBLISH_TIMEOUT_MS');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.publishTimeoutMs || DEFAULT_PUBLISH_TIMEOUT_MS;
|
||||
assertTimeoutInRange(value, 'effective publish timeout');
|
||||
return value;
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_PUBLISH_TIMEOUT_MS',
|
||||
config.nostrLock.publishTimeoutMs,
|
||||
DEFAULT_PUBLISH_TIMEOUT_MS,
|
||||
assertTimeoutInRange,
|
||||
'publish timeout',
|
||||
);
|
||||
}
|
||||
|
||||
export function getMinSuccessfulRelayPublishes() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES');
|
||||
assertPositiveCount(parsed, 'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.minSuccessfulRelayPublishes || DEFAULT_MIN_SUCCESSFUL_PUBLISHES;
|
||||
assertPositiveCount(value, 'effective min successful relay publishes');
|
||||
return value;
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES',
|
||||
config.nostrLock.minSuccessfulRelayPublishes,
|
||||
DEFAULT_MIN_SUCCESSFUL_PUBLISHES,
|
||||
assertPositiveCount,
|
||||
'min successful relay publishes',
|
||||
);
|
||||
}
|
||||
|
||||
export function getMinActiveRelayPool() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL;
|
||||
if (envValue) {
|
||||
const parsed = parseEnvInteger(envValue, 'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL');
|
||||
assertPositiveCount(parsed, 'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL');
|
||||
return parsed;
|
||||
}
|
||||
const value = config.nostrLock.minActiveRelayPool || DEFAULT_MIN_ACTIVE_RELAY_POOL;
|
||||
assertPositiveCount(value, 'effective min active relay pool');
|
||||
return value;
|
||||
return resolveIntegerConfig(
|
||||
'NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL',
|
||||
config.nostrLock.minActiveRelayPool,
|
||||
DEFAULT_MIN_ACTIVE_RELAY_POOL,
|
||||
assertPositiveCount,
|
||||
'min active relay pool',
|
||||
);
|
||||
}
|
||||
|
||||
export function getHashtag() {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { summarizeHistory } from '../src/relay-health.mjs';
|
||||
|
||||
function entry(timestamp, healthyRelays, totalRelays = 3) {
|
||||
return {
|
||||
timestamp,
|
||||
summary: {
|
||||
healthyRelays,
|
||||
totalRelays,
|
||||
allRelaysUnhealthy: healthyRelays === 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('summarizeHistory', async (t) => {
|
||||
const now = Date.parse('2026-02-15T12:00:00Z');
|
||||
const minute = 60_000;
|
||||
|
||||
await t.test('calculates correct success rate within window', () => {
|
||||
const history = [
|
||||
entry(new Date(now - 10 * minute).toISOString(), 3, 3), // 100%
|
||||
entry(new Date(now - 20 * minute).toISOString(), 0, 3), // 0%
|
||||
];
|
||||
// Total probes: 6, Healthy: 3. Rate: 0.5
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.sampleCount, 2);
|
||||
assert.equal(result.successRate, 0.5);
|
||||
});
|
||||
|
||||
await t.test('filters out old entries for success rate', () => {
|
||||
const history = [
|
||||
entry(new Date(now - 10 * minute).toISOString(), 3, 3),
|
||||
entry(new Date(now - 90 * minute).toISOString(), 0, 3), // Outside 60m window
|
||||
];
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.sampleCount, 1);
|
||||
assert.equal(result.successRate, 1); // 3/3 = 1
|
||||
});
|
||||
|
||||
await t.test('calculates allDownDurationMinutes correctly', () => {
|
||||
// Last healthy was 30 mins ago
|
||||
const history = [
|
||||
entry(new Date(now - 30 * minute).toISOString(), 3, 3),
|
||||
entry(new Date(now - 10 * minute).toISOString(), 0, 3),
|
||||
];
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.allDownDurationMinutes, 30);
|
||||
});
|
||||
|
||||
await t.test('allDownDurationMinutes considers entries outside window', () => {
|
||||
// Last healthy was 90 mins ago (outside window)
|
||||
const history = [
|
||||
entry(new Date(now - 90 * minute).toISOString(), 3, 3),
|
||||
entry(new Date(now - 10 * minute).toISOString(), 0, 3),
|
||||
];
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.allDownDurationMinutes, 90);
|
||||
});
|
||||
|
||||
await t.test('returns null allDownDurationMinutes if never healthy', () => {
|
||||
const history = [
|
||||
entry(new Date(now - 90 * minute).toISOString(), 0, 3),
|
||||
entry(new Date(now - 10 * minute).toISOString(), 0, 3),
|
||||
];
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.allDownDurationMinutes, null);
|
||||
});
|
||||
|
||||
await t.test('returns 0 allDownDurationMinutes if currently healthy', () => {
|
||||
const history = [
|
||||
entry(new Date(now).toISOString(), 3, 3),
|
||||
];
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.allDownDurationMinutes, 0);
|
||||
});
|
||||
|
||||
await t.test('handles empty history', () => {
|
||||
const result = summarizeHistory([], { nowMs: now, windowMinutes: 60 });
|
||||
assert.equal(result.sampleCount, 0);
|
||||
assert.equal(result.successRate, 1);
|
||||
assert.equal(result.allDownDurationMinutes, null);
|
||||
});
|
||||
|
||||
await t.test('handles malformed entries gracefully', () => {
|
||||
const history = [
|
||||
{}, // No summary, no timestamp
|
||||
{ timestamp: new Date(now - 10 * minute).toISOString() }, // No summary
|
||||
{ summary: { totalRelays: 3 } } // No timestamp
|
||||
];
|
||||
|
||||
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
|
||||
|
||||
// Entry 1: timestamp is undefined -> NaN. NaN >= windowStartMs is false. Skipped.
|
||||
// Entry 2: timestamp valid (10 mins ago). No summary.
|
||||
// summary?.totalRelays || 0 -> 0.
|
||||
// summary?.healthyRelays || 0 -> 0.
|
||||
// Entry 3: timestamp undefined -> NaN. Skipped.
|
||||
|
||||
// recent = [Entry 2]
|
||||
// relayProbeCount = 0 + 0 = 0.
|
||||
// successCount = 0 + 0 = 0.
|
||||
// successRate = 0 > 0 ? ... : 1 -> 1.
|
||||
|
||||
assert.equal(result.sampleCount, 1);
|
||||
assert.equal(result.successRate, 1);
|
||||
|
||||
// lastHealthyAtMs check:
|
||||
// Entry 3: healthyRelays undefined -> 0. Not > 0.
|
||||
// Entry 2: healthyRelays undefined -> 0. Not > 0.
|
||||
// Entry 1: healthyRelays undefined -> 0. Not > 0.
|
||||
// Loop finishes. lastHealthyAtMs = null.
|
||||
// allDownDurationMinutes = null.
|
||||
|
||||
assert.equal(result.allDownDurationMinutes, null);
|
||||
});
|
||||
});
|
||||
@@ -68,4 +68,26 @@ describe('loadTorchConfig (Unit Tests with Mocked FS)', () => {
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs2.existsSync.mock.calls.length, 0);
|
||||
});
|
||||
|
||||
it('returns null for empty string lists (new consistent behavior)', () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({
|
||||
nostrLock: {
|
||||
relays: [],
|
||||
relayFallbacks: []
|
||||
},
|
||||
dashboard: {
|
||||
relays: []
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
const config = loadTorchConfig(mockFs);
|
||||
|
||||
// New behavior: all return null
|
||||
assert.strictEqual(config.nostrLock.relays, null);
|
||||
assert.strictEqual(config.dashboard.relays, null);
|
||||
assert.strictEqual(config.nostrLock.relayFallbacks, null);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user