Merge branch 'main' into perf/async-memory-save-18240015948353785856

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-18 09:48:42 -05:00
commit 0dfbf3f86f
46 files changed
+1755 -633

No files matched your search

@@ -0,0 +1,18 @@
{
"cadence": "daily",
"operation": "retrieve",
"runId": "2026-02-17T16-35-41Z",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "todo-triage-agent",
"promptPath": "src/prompts/daily/todo-triage-agent.md",
"promptIntent": "Required startup + artifacts + memory + issue capture",
"query": "scheduler | daily | agent | todo-triage-agent | intent | Required startup + artifacts + memory + issue capture",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 1
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,20 @@
{
"cadence": "daily",
"operation": "store",
"runId": "2026-02-17T16-36-25Z",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "todo-triage-agent",
"promptPath": "src/prompts/daily/todo-triage-agent.md",
"promptIntent": "Required startup + artifacts + memory + issue capture",
"events": 1,
"sourceFile": "memory-updates/2026-02-17T16-36-25Z__todo-triage-agent.md"
},
"outputs": {
"storedCount": 1,
"summaries": [
"# Memory Update - 2026-02-17T16-36-25Z\n\nRun at 2026-02-17T16-36-25Z. Scanned codebase for TODO/FIXME/XXX. Found 0 actionable items. False positives in mktemp usage and documentation."
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
+91 -21
View File
@@ -117,7 +117,7 @@
"importance": 0.25,
"embedding_id": null,
"created_at": 1771370050385,
"last_seen": 1771370148086,
"last_seen": 1771384958445,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
@@ -140,7 +140,7 @@
"importance": 0.25,
"embedding_id": null,
"created_at": 1771370084740,
"last_seen": 1771370148086,
"last_seen": 1771384958445,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
@@ -163,7 +163,7 @@
"importance": 0.25,
"embedding_id": null,
"created_at": 1771370148079,
"last_seen": 1771370148086,
"last_seen": 1771384958445,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
@@ -196,12 +196,12 @@
}
],
[
"f8711702-8dc2-474b-9142-0798f41209f3",
"dacc99c5-1ba1-438f-87d3-cbc39ca7f899",
{
"schema_version": 1,
"id": "f8711702-8dc2-474b-9142-0798f41209f3",
"agent_id": "scheduler-memory-daily",
"session_id": "session-mlrc78ft",
"id": "dacc99c5-1ba1-438f-87d3-cbc39ca7f899",
"agent_id": "design-system-audit-agent",
"session_id": "session-mlrf4pzf",
"type": "batch_window",
"content": "Memory retrieval seed for daily :: scheduler memory retrieval",
"summary": "Memory retrieval seed for daily :: scheduler memory retrieval",
@@ -212,8 +212,8 @@
],
"importance": 0.4,
"embedding_id": null,
"created_at": 1771377118841,
"last_seen": 1771377118848,
"created_at": 1771382040459,
"last_seen": 1771382040466,
"source": "scheduler-retrieve",
"ttl_days": null,
"merged_into": null,
@@ -221,25 +221,95 @@
}
],
[
"782a835a-88d1-4307-8154-599a09bcdfb6",
"c683cf59-af31-49e5-98c5-41994c8ffc93",
{
"schema_version": 1,
"id": "782a835a-88d1-4307-8154-599a09bcdfb6",
"agent_id": "scheduler-memory-weekly",
"session_id": "session-mlrc8az1",
"id": "c683cf59-af31-49e5-98c5-41994c8ffc93",
"agent_id": "design-system-audit-agent",
"session_id": "session-mlrf7alk",
"type": "batch_window",
"content": "Store memory event A for weekly :: scheduler memory store\nStore memory event B for weekly :: scheduler memory store",
"summary": "Store memory event A for weekly :: scheduler memory store Store memory event B for weekly :: scheduler memory store",
"content": "# Design System Audit Report\n\nDate: 2026-02-18T02:34:58.298Z\n\n## 1. Inline Styles\nFound 8 instances.\n### Top Offending Files\n- landing/index.html: 8\n\n### Sample Snippets\n- `style=\"font-size: 2rem; margin-bottom: 2rem; color: var(--text-strong);\"` in landing/index.html\n- `style=\"margin-top: 1rem;\"` in landing/index.html\n- `style=\"margin-top: 4rem; text-align: center; width: 100%; max-width: 600px; padding-top: 2rem; border-top: 1px solid var(--border);\"` in landing/index.html\n- `style=\"font-size: 1.25rem; margin-bottom: 0.5rem; color: var(--text-strong);\"` in landing/index.html\n- `style=\"color: var(--muted); margin-bottom: 1.5rem; font-size: 0.95rem;\"` in landing/index.html\n\n## 2. Unsanctioned Hex Colors\nFound 6 instances.\n### Top Offending Colors\n- #039: 1\n- #0d1117: 1\n- #e6edf3: 1\n- #a5d6ff: 1\n- #ff4d4d: 1\n- #ef4444: 1\n\n### Sample Locations\n- #039 in dashboard/domUtils.js\n- #0d1117 in landing/index.html\n- #e6edf3 in landing/index.html\n- #a5d6ff in landing/index.html\n- #ff4d4d in landing/index.html\n\n## 3. Raw Lengths\n(Skipped: requires AST parsing)\n\n## Next Steps\n- Review unsanctioned colors and replace with tokens.\n- Refactor inline styles to CSS classes.",
"summary": "# Design System Audit Report\n\nDate: 2026-02-18T02:34:58.298Z\n\n## 1. Inline Styles\nFound 8 instances.\n### Top Offending Files\n- landing/index.html: 8\n\n### Sample Snippets\n- `style=\"font-size: 2rem; margin-bottom: 2rem; color: var(--text-strong);\"` in landing/index.html\n- `style=\"…",
"tags": [
"scheduler",
"weekly",
"store"
"daily",
"store",
"insight"
],
"importance": 0.55,
"importance": 0.8,
"embedding_id": null,
"created_at": 1771377168781,
"last_seen": 1771377168782,
"source": "scheduler-store",
"created_at": 1771382160489,
"last_seen": 1771382160489,
"source": "agent-output",
"ttl_days": null,
"merged_into": null,
"pinned": false
}
],
[
"71791fc3-49a7-40f2-a8e5-c93e8b19f32b",
{
"schema_version": 1,
"id": "71791fc3-49a7-40f2-a8e5-c93e8b19f32b",
"agent_id": "agent-telemetry",
"session_id": "s-telemetry",
"type": "batch_window",
"content": "customer token ABC123 secret",
"summary": "customer token ABC123 secret",
"tags": [
"incident"
],
"importance": 0.25,
"embedding_id": null,
"created_at": 1771384842151,
"last_seen": 1771384958445,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
"pinned": false
}
],
[
"ca60066d-1d01-43de-a597-573d9826d101",
{
"schema_version": 1,
"id": "ca60066d-1d01-43de-a597-573d9826d101",
"agent_id": "agent-telemetry",
"session_id": "s-telemetry",
"type": "batch_window",
"content": "customer token ABC123 secret",
"summary": "customer token ABC123 secret",
"tags": [
"incident"
],
"importance": 0.25,
"embedding_id": null,
"created_at": 1771384870483,
"last_seen": 1771384958445,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
"pinned": false
}
],
[
"b95b047e-6bc6-4342-a535-8d311ebc7f1a",
{
"schema_version": 1,
"id": "b95b047e-6bc6-4342-a535-8d311ebc7f1a",
"agent_id": "agent-telemetry",
"session_id": "s-telemetry",
"type": "batch_window",
"content": "customer token ABC123 secret",
"summary": "customer token ABC123 secret",
"tags": [
"incident"
],
"importance": 0.25,
"embedding_id": null,
"created_at": 1771384958439,
"last_seen": 1771384958445,
"source": "ingest",
"ttl_days": null,
"merged_into": null,
"pinned": false
@@ -0,0 +1,18 @@
{
"cadence": "daily",
"operation": "retrieve",
"runId": "session-mlrf4pzf",
"servicePath": "src/services/memory/index.js#getRelevantMemories",
"inputs": {
"agentId": "design-system-audit-agent",
"promptPath": "",
"promptIntent": "scheduler memory retrieval",
"query": "scheduler | daily | agent | design-system-audit-agent | intent | scheduler memory retrieval",
"events": 1
},
"outputs": {
"ingestedCount": 1,
"retrievedCount": 1
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_RETRIEVED
@@ -0,0 +1,20 @@
{
"cadence": "daily",
"operation": "store",
"runId": "session-mlrf7alk",
"servicePath": "src/services/memory/index.js#ingestEvents",
"inputs": {
"agentId": "design-system-audit-agent",
"promptPath": "",
"promptIntent": "scheduler memory store",
"events": 1,
"sourceFile": "memory-update.md"
},
"outputs": {
"storedCount": 1,
"summaries": [
"# Design System Audit Report\n\nDate: 2026-02-18T02:34:58.298Z\n\n## 1. Inline Styles\nFound 8 instances.\n### Top Offending Files\n- landing/index.html: 8\n\n### Sample Snippets\n- `style=\"font-size: 2rem; margin-bottom: 2rem; color: var(--text-strong);\"` in landing/index.html\n- `style=\"…"
]
},
"status": "ok"
}
@@ -0,0 +1 @@
MEMORY_STORED
+4
View File
@@ -0,0 +1,4 @@
# TODO Issues Report - 2026-02-17T16-36-25Z
No actionable TODOs found in the codebase (src/, docs/, bin/, scripts/) as of 2026-02-17T16-36-25Z.
Matches found were false positives (mktemp XXXXXX templates or documentation).
+73 -2
View File
@@ -1,3 +1,74 @@
src/test_logs/TEST_LOG_2026-02-15T23-20-19Z.md:11:- Result: Created CONTEXT, TODO, DECISIONS, TEST_LOG files.
src/test_logs/TEST_LOG_2026-02-15T18-00-00Z.md:10:- Command: Scan for TODOs
src/test_logs/TEST_LOG_2026-02-15T18-00-00Z.md:11: `git grep -n -E "TODO|FIXME|XXX" -- '*.js' '*.mjs' 'docs/*' | sed -n '1,200p' > artifacts/todos.txt`
src/context/CONTEXT_2026-02-15T18-00-00Z.md:11:Scan the repository for TODO/FIXME/XXX markers and triage them.
src/decisions/DECISIONS_2026-02-15T18-00-00Z.md:11:No actionable TODOs were found in the codebase. The scan returned only documentation references and test data.
src/decisions/DECISIONS_2026-02-15T18-00-00Z.md:14:The goal is to triage *actionable* TODOs. Since none exist, no changes or issues are required.
src/todo/TODO_manual-jules-1771293773.md:1:# TODO for Scheduler Update Agent
src/todo/TODO_2026-02-14T13-30-00Z.md:1:# Decomposition TODO
src/todo/TODO_2026-02-14T06-04-19Z.md:1:# Audit Run TODO
src/todo/2026-02-13-dom-migration-todo.md:1:# TODO — Rendering Migration (Example)
src/todo/TODO_2026-02-17T02-04-31Z.md:1:# TODO for Scheduler Update Agent
src/todo/TODO_2026-02-17T02-04-05Z.md:1:# TODO for Scheduler Update Agent
src/todo/TODO_2026-02-15T01-22-33Z.md:1:# TODO for Perf Agent Run 2026-02-15T01-22-33Z
src/todo/TODO_2026-02-15T18-00-00Z.md:14:- Scan repository for TODOs.
src/todo/TODO_2026-02-16.md:1:# TODO: perf-agent (2026-02-16)
src/todo/TODO_2026-02-14.md:1:# TODO Checklist
src/todo/TODO_2026-02-15T01:03:31Z.md:8:# TODO Update - 2026-02-15T01:03:31Z
src/todo/TODO_2026-02-14T23-00-00Z.md:1:# TODO: dashboard/index.html innerHTML Migration
src/prompts/scheduler-flow.md:11:- `src/todo/TODO_<timestamp>.md`
src/prompts/daily/decompose-agent.md:32:- `src/todo/TODO_<timestamp>.md` — checklist of extraction items (2–3 blocks) and status.
src/prompts/daily/decompose-agent.md:75:Add these items to `src/todo/TODO_<timestamp>.md`.
src/prompts/daily/decompose-agent.md:266:4. Read file, identify 2–3 cohesive blocks, document in `src/context/CONTEXT_<timestamp>.md` and `src/todo/TODO_<timestamp>.md`.
src/prompts/daily/const-refactor-agent.md:40:- `src/todo/TODO_<timestamp>.md` — List of duplicated constants to canonicalize and status for each.
src/prompts/daily/audit-agent.md:29:- `src/todo/TODO_<timestamp>.md` — checklist for the audit run and follow-ups.
src/prompts/daily/audit-agent.md:121: - If automated, add a follow-up task in `src/todo/TODO_<timestamp>.md` or open issues for high-priority items with labels `perf`, `lint`, or `security` as appropriate.
src/prompts/daily/scheduler-update-agent.md:89: - `src/todo/TODO_<timestamp>.md`
src/prompts/daily/perf-agent.md:50:- `src/todo/TODO_<timestamp>.md` — tasks (Done / Blocked).
src/prompts/daily/innerhtml-migration-agent.md:31:- `src/todo/TODO_<timestamp>.md` — checklist of assignments in the file and status.
src/prompts/daily/innerhtml-migration-agent.md:66: Add this list to `src/todo/TODO_<timestamp>.md` as items to migrate.
src/prompts/daily/docs-code-investigator.md:22: - Focus on files > ~200 LOC, or files that contain `TODO|FIXME|XXX`.
src/prompts/daily/docs-code-investigator.md:23:- Priority: files containing `TODO|FIXME|XXX` or >200 lines. shared helpers and core modules are high-value places to document.
src/prompts/daily/docs-code-investigator.md:39:git grep -n -E "TODO|FIXME|XXX" -- . | sed -n '1,200p'
src/prompts/daily/docs-code-investigator.md:42:- Is large (> 200 LOC) or contains `TODO` / `FIXME` / `XXX`.
src/prompts/daily/docs-code-investigator.md:80:- Note unreachable code, TODOs, or suspicious `try/catch` with swallowing errors.
src/prompts/daily/docs-code-investigator.md:206:git grep -n -E "TODO|FIXME|XXX" -- . | sed -n '1,200p'
src/prompts/daily/todo-triage-agent.md:13:Mission: keep the codebase’s TODO/FIXME backlog actionable by scanning for TODO-style comments, classifying them by effort/risk, and either (a) fixing **trivial, safe** items via small PRs or (b) converting non-trivial or sensitive items into well-formed GitHub issues. Every change must be small, safe, traceable, and reviewable.
src/prompts/daily/todo-triage-agent.md:29: - Scanning the repo for TODO/FIXME/XXX markers in source/docs.
src/prompts/daily/todo-triage-agent.md:36: - Refactors or feature work disguised as “TODO fixes”.
src/prompts/daily/todo-triage-agent.md:40: - Deleting TODOs without resolving the underlying need.
src/prompts/daily/todo-triage-agent.md:45:1. Visibility — Produce a current TODO inventory file each run.
src/prompts/daily/todo-triage-agent.md:46:2. Safety — No risky areas are modified; sensitive TODOs become issues.
src/prompts/daily/todo-triage-agent.md:50:5. Minimal disruption — Avoid churn; don’t reformat files just to touch TODOs.
src/prompts/daily/todo-triage-agent.md:58:- No security-sensitive fixes. If the TODO mentions (or touches) cryptography,
src/prompts/daily/todo-triage-agent.md:61:- No mission drift. Do not expand TODO triage into general cleanup/refactors.
src/prompts/daily/todo-triage-agent.md:62:- One logical change per commit; don’t bundle unrelated TODOs in one PR unless
src/prompts/daily/todo-triage-agent.md:68:P0 Security/safety TODOs → issue only, label clearly.
src/prompts/daily/todo-triage-agent.md:69:P1 Trivial correctness/docs TODOs → small PR if safe and < 1 hour.
src/prompts/daily/todo-triage-agent.md:70:P2 Medium/large engineering TODOs → issue with next-step recommendation.
src/prompts/daily/todo-triage-agent.md:71:P3 Cosmetic/low-value TODOs → issue or leave (don’t churn) unless policy
src/prompts/daily/todo-triage-agent.md:89: `git grep -n -E "TODO|FIXME|XXX" -- '*.js' '*.html' 'docs/*' \
src/prompts/daily/todo-triage-agent.md:95: For each TODO/FIXME/XXX entry:
src/prompts/daily/todo-triage-agent.md:97: - file, line number, exact TODO first line
src/prompts/daily/todo-triage-agent.md:130:2) Implement the smallest change that resolves the TODO.
src/prompts/daily/todo-triage-agent.md:135: - `fix(ai): <short description> (TODO)`
src/prompts/daily/todo-triage-agent.md:136: - `chore(ai): <short description> (TODO)` for docs-only changes
src/prompts/daily/todo-triage-agent.md:138: - Summary of the TODO and what changed
src/prompts/daily/todo-triage-agent.md:144:- `fix(ai): <short description> (TODO)`
src/prompts/daily/todo-triage-agent.md:152:- any sensitive/security-related TODOs
src/prompts/daily/todo-triage-agent.md:155:- `todo: <first line of the TODO>`
src/prompts/daily/todo-triage-agent.md:160:- What you believe the TODO intends
src/prompts/daily/todo-triage-agent.md:170:DE-DUPING & STALE TODOs
src/prompts/daily/todo-triage-agent.md:172:- If multiple TODOs refer to the same underlying problem, consolidate into one
src/prompts/daily/todo-triage-agent.md:174:- Do not delete TODOs as “stale” unless you can prove the need is resolved.
src/prompts/daily/todo-triage-agent.md:195:- 0–N small PRs for trivial, safe TODOs
src/prompts/daily/todo-triage-agent.md:196:- 0–N GitHub issues for medium/large/sensitive TODOs
src/prompts/daily/content-audit-agent.md:43:- `src/todo/TODO_<timestamp>.md` — checklist of pages and claims to verify, with statuses.
src/prompts/daily/deps-security-agent.md:44:- `src/todo/TODO_<timestamp>.md` — upgrade tasks and statuses.
src/prompts/daily/test-audit-agent.md:42:- `src/todo/TODO_<timestamp>.md` — checklist of specific tests/issues to investigate/fix.
src/prompts/daily/test-audit-agent.md:164: 1. **Test-fix PR** — add missing assertions, replace `setTimeout` sleeps with `waitFor`, or mock timers. Include `src/context/CONTEXT_<timestamp>.md`, `src/todo/TODO_<timestamp>.md`, `src/test_logs/TEST_LOG_<timestamp>.md`.
docs/agent-handoffs/incidents/goose-hermit-etxtbsy-fix.patch:70:+ HERMIT_TMP=$(mktemp ~/.config/goose/mcp-hermit/bin/.hermit-download.XXXXXX)
docs/agent-handoffs/incidents/goose-hermit-etxtbsy-fix.patch:91:+ HERMIT_INIT_COPY=$(mktemp /tmp/.hermit-init-copy.XXXXXX)
docs/agent-handoffs/incidents/2026-02-15-hermit-text-file-busy.md:48:HERMIT_TMP=$(mktemp ~/.config/goose/mcp-hermit/bin/.hermit-download.XXXXXX)
docs/agent-handoffs/incidents/2026-02-15-hermit-text-file-busy.md:57:HERMIT_INIT_COPY=$(mktemp /tmp/.hermit-init-copy.XXXXXX)
docs/proposals/prompt-packages.md:26: * `todo-triage-agent`: Manage TODOs.
scripts/agent/verify-run-artifacts.mjs:15: expectedPrefix: 'TODO_',
test/verify-run-artifacts.test.mjs:46: await fs.writeFile(path.join(root, 'src', 'todo', 'TODO_1.md'), `${metadata}\n\nPending tasks:\n- finish checks\n`, 'utf8');
@@ -0,0 +1,3 @@
# Memory Update - 2026-02-17T16-36-25Z
Run at 2026-02-17T16-36-25Z. Scanned codebase for TODO/FIXME/XXX. Found 0 actionable items. False positives in mktemp usage and documentation.
+41
View File
@@ -0,0 +1,41 @@
import { RelayHealthManager } from '../src/lock-ops.mjs';
const healthManager = new RelayHealthManager();
// Setup mock relays
const relays = [];
for (let i = 0; i < 100; i++) {
relays.push(`wss://relay-${i}.example.com`);
}
const config = {
rollingWindowSize: 10,
failureThreshold: 3,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 60000,
snapshotIntervalMs: 10000,
minActiveRelayPool: 5,
};
// Populate some metrics
for (const relay of relays) {
// Simulate some outcomes
for (let i = 0; i < 5; i++) {
healthManager.recordOutcome(relay, Math.random() > 0.1, null, Math.random() * 200, config);
}
}
// Benchmark rankRelays
const start = performance.now();
const iterations = 10000;
for (let i = 0; i < iterations; i++) {
healthManager.rankRelays(relays, config);
}
const end = performance.now();
const duration = end - start;
console.log(`RankRelays x ${iterations}: ${duration.toFixed(2)}ms`);
console.log(`Average per call: ${(duration / iterations).toFixed(4)}ms`);
+44
View File
@@ -0,0 +1,44 @@
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import crypto from 'node:crypto';
const COUNT = 30000;
const DUPLICATE_WINDOW_MS = 1000 * 60 * 60; // 1 hour
function createMemory(i) {
return {
id: `mem-${i}`,
agent_id: 'agent-1',
session_id: 's1',
type: 'event',
content: `memory content ${i}`,
summary: `memory summary ${i}`,
tags: ['tag-a', 'tag-b'],
importance: 0.5,
embedding_id: null,
created_at: Date.now() - Math.floor(Math.random() * 10 * 24 * 60 * 60 * 1000), // Random time in last 10 days
last_seen: Date.now(),
source: 'ingest',
ttl_days: null,
merged_into: null,
pinned: false,
};
}
const memories = Array.from({ length: COUNT }, (_, i) => createMemory(i));
// Mock embedding function
const getEmbedding = (memory) => {
// Generate a pseudo-random embedding based on ID to be deterministic but varied
const val = parseInt(memory.id.split('-')[1]) % 100;
return [val, 100 - val];
};
console.time('createLifecyclePlan');
await createLifecyclePlan(memories, {
now: Date.now(),
retentionMs: 30 * 24 * 60 * 60 * 1000,
duplicateWindowMs: DUPLICATE_WINDOW_MS,
getEmbedding,
generateSummary: async () => JSON.stringify({ summary: 'merged', importance: 0.5 }),
});
console.timeEnd('createLifecyclePlan');
+135
View File
@@ -0,0 +1,135 @@
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import crypto from 'node:crypto';
import { performance } from 'node:perf_hooks';
const ITERATIONS = 3;
const GROUPS = 100;
const GROUP_SIZE = 5;
const LATENCY_MS = 10;
function createMemory(overrides = {}) {
return {
schema_version: 1,
id: overrides.id ?? crypto.randomUUID(),
agent_id: overrides.agent_id ?? 'agent-1',
session_id: overrides.session_id ?? 's1',
type: overrides.type ?? 'event',
content: overrides.content ?? 'default content',
summary: overrides.summary ?? 'default summary',
tags: overrides.tags ?? [],
importance: overrides.importance ?? 0.5,
embedding_id: overrides.embedding_id ?? null,
created_at: overrides.created_at ?? Date.now(),
last_seen: overrides.last_seen ?? Date.now(),
source: overrides.source ?? 'ingest',
ttl_days: overrides.ttl_days ?? null,
merged_into: overrides.merged_into ?? null,
pinned: overrides.pinned ?? false,
};
}
function generateMemories() {
const memories = [];
const now = Date.now();
for (let i = 0; i < GROUPS; i++) {
const groupId = `group-${i}`;
for (let j = 0; j < GROUP_SIZE; j++) {
memories.push(createMemory({
id: `${groupId}-${j}`,
content: `Memory ${j} for group ${groupId}`,
summary: `Summary ${j} for group ${groupId}`,
tags: ['benchmark', groupId],
created_at: now - (j * 100), // Close in time
last_seen: now,
importance: 0.5,
}));
}
}
// Add some random noise memories that won't group
for (let i = 0; i < 50; i++) {
memories.push(createMemory({
id: `noise-${i}`,
content: `Noise memory ${i}`,
tags: [`noise-${i}`], // Unique tags to prevent grouping
created_at: now - (i * 100000), // Spread out
}));
}
return memories;
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function runBenchmark() {
console.log(`Setting up benchmark: ${GROUPS} groups of ${GROUP_SIZE} memories + noise.`);
console.log(`Mock latency: ${LATENCY_MS}ms per group condensation.`);
const memories = generateMemories();
// Prepare embeddings
const groupEmbeddings = new Map();
for (let i = 0; i < GROUPS; i++) {
const vec = new Array(GROUPS).fill(0);
vec[i] = 1;
groupEmbeddings.set(`group-${i}`, vec);
}
const getEmbeddingWrapper = (memory) => {
const groupIdTag = memory.tags.find(t => t.startsWith('group-'));
if (groupIdTag && groupEmbeddings.has(groupIdTag)) {
return groupEmbeddings.get(groupIdTag);
}
// Return a unique random vector for noise to prevent grouping
// But since they have unique tags, getEmbedding won't even be called for similarity check
// if tag overlap check fails first?
// groupNearDuplicates logic:
// ... if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
// So if tags don't overlap, it continues.
// So embedding doesn't matter for noise if tags are unique.
return [Math.random()];
};
const options = {
retentionMs: 30 * 24 * 60 * 60 * 1000, // 30 days
duplicateWindowMs: 1000 * 60 * 60, // 1 hour
similarityThreshold: 0.99,
getEmbedding: getEmbeddingWrapper,
generateSummary: async (prompt) => {
await delay(LATENCY_MS);
return JSON.stringify({
summary: 'Condensed summary based on prompt.',
importance: 0.8,
});
},
// Mock templateDir to avoid file reading issues if paths are tricky,
// but loadMemoryPromptTemplates reads from file system.
// It should work if run from root.
};
console.log(`Starting ${ITERATIONS} iterations...`);
const times = [];
for (let i = 0; i < ITERATIONS; i++) {
const start = performance.now();
const plan = await createLifecyclePlan(memories, options);
const duration = performance.now() - start;
// Verify grouping happened
const condensedCount = plan.condensedGroups.length;
// We expect exactly GROUPS condensed groups.
if (condensedCount !== GROUPS) {
console.warn(`Warning: Expected ${GROUPS} condensed groups, but got ${condensedCount}. Check grouping logic.`);
}
times.push(duration);
console.log(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`Average execution time: ${avg.toFixed(2)}ms`);
}
runBenchmark().catch(console.error);
-61
View File
@@ -1,61 +0,0 @@
import {
loadTorchConfig,
getRelays,
getNamespace,
} from './torch-config.mjs';
import { getRoster } from './roster.mjs';
import { queryLocks } from './lock-ops.mjs';
import { todayDateStr } from './utils.mjs';
export async function cmdCheck(cadence, deps = {}) {
const {
loadTorchConfigFn = loadTorchConfig,
getRelaysFn = getRelays,
getNamespaceFn = getNamespace,
getRosterFn = getRoster,
queryLocksFn = queryLocks,
todayDateStrFn = todayDateStr,
} = deps;
const relays = await getRelaysFn();
const namespace = await getNamespaceFn();
const dateStr = todayDateStrFn();
const config = await loadTorchConfigFn();
const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || [];
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
console.error(`Relays: ${relays.join(', ')}`);
if (pausedAgents.length > 0) {
console.error(`Paused agents: ${pausedAgents.join(', ')}`);
}
const locks = await queryLocksFn(relays, cadence, dateStr, namespace);
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
const roster = await getRosterFn(cadence);
const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])];
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
const available = roster.filter((a) => !excludedAgents.includes(a));
const result = {
namespace,
cadence,
date: dateStr,
locked: lockedAgents.sort(),
paused: pausedAgents.sort(),
excluded: excludedAgents.sort(),
available: available.sort(),
lockCount: locks.length,
unknownLockedAgents: unknownLockedAgents.sort(),
locks: locks.map((l) => ({
agent: l.agent,
eventId: l.eventId,
createdAt: l.createdAtIso,
expiresAt: l.expiresAtIso,
platform: l.platform,
})),
};
console.log(JSON.stringify(result, null, 2));
return result;
}
-122
View File
@@ -1,122 +0,0 @@
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
import {
getRelays,
getNamespace,
getTtl,
} from './torch-config.mjs';
import {
RACE_CHECK_DELAY_MS,
KIND_APP_DATA,
} from './constants.mjs';
import { getRoster } from './roster.mjs';
import { queryLocks, publishLock } from './lock-ops.mjs';
import { todayDateStr, nowUnix } from './utils.mjs';
import { ExitError } from './errors.mjs';
export async function cmdLock(agent, cadence, dryRun = false) {
const relays = await getRelays();
const namespace = await getNamespace();
const dateStr = todayDateStr();
const ttl = await getTtl();
const now = nowUnix();
const expiresAt = now + ttl;
console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
console.error(`Relays: ${relays.join(', ')}`);
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(', ')}`);
throw new ExitError(1, 'Agent not in roster');
}
console.error('Step 1: Checking for existing locks...');
const existingLocks = await queryLocks(relays, cadence, dateStr, namespace);
const conflicting = existingLocks.filter((l) => l.agent === agent);
if (conflicting.length > 0) {
const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0];
console.error(
`LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` +
`(created ${earliest.createdAtIso}, platform: ${earliest.platform})`,
);
console.log('LOCK_STATUS=denied');
console.log('LOCK_REASON=already_locked');
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
throw new ExitError(3, 'Lock denied');
}
console.error('Step 2: Generating ephemeral keypair...');
const sk = generateSecretKey();
const pk = getPublicKey(sk);
console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
console.error('Step 3: Building lock event...');
const event = finalizeEvent(
{
kind: KIND_APP_DATA,
created_at: now,
tags: [
['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`],
['t', `${namespace}-agent-lock`],
['t', `${namespace}-lock-${cadence}`],
['t', `${namespace}-lock-${cadence}-${dateStr}`],
['expiration', String(expiresAt)],
],
content: JSON.stringify({
agent,
cadence,
status: 'started',
namespace,
date: dateStr,
platform: process.env.AGENT_PLATFORM || 'unknown',
lockedAt: new Date(now * 1000).toISOString(),
expiresAt: new Date(expiresAt * 1000).toISOString(),
}),
},
sk,
);
console.error(` Event ID: ${event.id}`);
if (dryRun) {
console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
console.error('RACE CHECK: won (dry run — no real contention possible)');
} else {
console.error('Step 4: Publishing to relays...');
await publishLock(relays, event);
console.error('Step 5: Race check...');
await new Promise((resolve) => setTimeout(resolve, RACE_CHECK_DELAY_MS));
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
const racingLocks = postLocks
.filter((l) => l.agent === agent)
.sort((a, b) => a.createdAt - b.createdAt);
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
const winner = racingLocks[0];
console.error(
`RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`,
);
console.log('LOCK_STATUS=race_lost');
console.log('LOCK_REASON=earlier_claim_exists');
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
throw new ExitError(3, 'Race check lost');
}
console.error('RACE CHECK: won');
}
console.log('LOCK_STATUS=ok');
console.log(`LOCK_EVENT_ID=${event.id}`);
console.log(`LOCK_PUBKEY=${pk}`);
console.log(`LOCK_AGENT=${agent}`);
console.log(`LOCK_CADENCE=${cadence}`);
console.log(`LOCK_DATE=${dateStr}`);
console.log(`LOCK_EXPIRES=${expiresAt}`);
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
return { status: 'ok', eventId: event.id };
}
@@ -0,0 +1,16 @@
# Context for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
## Goal
Scan the repository for TODO/FIXME/XXX markers and triage them.
## Scope
src/, docs/, bin/, scripts/ directories.
## Constraints
Only fix trivial items. Report others.
## Active Prompt
src/prompts/daily/todo-triage-agent.md
@@ -0,0 +1,12 @@
---
agent: design-system-audit-agent
cadence: daily
session: session-2026-02-18T02:35:22Z
run-start: 2026-02-18T02:35:22Z
prompt: src/prompts/daily/design-system-audit-agent.md
---
# Context
- Goal: Audit design system compliance.
- Scope: Daily audit of src/ and dashboard/.
- Constraints: Non-blocking, reporting only.
+36 -17
View File
@@ -7,6 +7,13 @@ import { fileURLToPath } from 'node:url';
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
import { getDashboardAuth, parseTorchConfig } from './torch-config.mjs';
const SECURITY_HEADERS = {
'Content-Security-Policy': "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' wss:; object-src 'none'; base-uri 'self';",
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Referrer-Policy': 'strict-origin-when-cross-origin'
};
function timingSafeCompare(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
@@ -30,7 +37,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
if (auth) {
const authHeader = req.headers.authorization;
if (!authHeader) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Authentication required');
return;
}
@@ -48,7 +55,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
}
if (!isValid) {
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.writeHead(401, { ...SECURITY_HEADERS, 'WWW-Authenticate': 'Basic realm="TORCH Dashboard"' });
res.end('Invalid credentials');
return;
}
@@ -60,7 +67,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
// Redirect / to /dashboard/
if (pathname === '/' || pathname === '/dashboard') {
res.writeHead(302, { 'Location': '/dashboard/' });
res.writeHead(302, { ...SECURITY_HEADERS, 'Location': '/dashboard/' });
res.end();
return;
}
@@ -105,31 +112,43 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
const rawConfig = JSON.parse(configContent);
const safeConfig = parseTorchConfig(rawConfig);
// Security: Sanitize sensitive fields
if (safeConfig.dashboard) {
delete safeConfig.dashboard.auth;
}
delete safeConfig.configPath;
// Security: Whitelist only fields required by the dashboard frontend
const publicConfig = {
dashboard: safeConfig.dashboard ? { ...safeConfig.dashboard } : {},
nostrLock: safeConfig.nostrLock
? {
namespace: safeConfig.nostrLock.namespace,
relays: safeConfig.nostrLock.relays,
}
: {},
};
res.writeHead(200, { 'Content-Type': 'application/json' });
// Remove sensitive dashboard auth
if (publicConfig.dashboard.auth) {
delete publicConfig.dashboard.auth;
}
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': 'application/json' });
res.end(JSON.stringify(safeConfig, null, 2));
return;
} catch (err) {
console.error('Error parsing torch-config.json:', err);
res.writeHead(500);
res.writeHead(500, SECURITY_HEADERS);
res.end('Internal Server Error');
return;
}
} else {
res.writeHead(404);
res.writeHead(404, SECURITY_HEADERS);
res.end('Not Found');
return;
}
}
// Security check: prevent directory traversal
const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), '');
let filePath = path.join(packageRoot, safePath);
// Resolve path relative to packageRoot.
// We strip the leading slash from pathname (which comes from URL) to treat it as relative.
const relativePath = pathname.replace(/^\//, '');
let filePath = path.resolve(packageRoot, relativePath);
// Security check: restrict access to allowed paths
const allowedPaths = [
@@ -146,7 +165,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
});
if (!isAllowed) {
res.writeHead(403);
res.writeHead(403, SECURITY_HEADERS);
res.end('Forbidden');
return;
}
@@ -160,7 +179,7 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
// Check if file exists and is a file
if (!fileStat || !fileStat.isFile()) {
res.writeHead(404);
res.writeHead(404, SECURITY_HEADERS);
res.end('Not Found');
return;
}
@@ -181,12 +200,12 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
case '.md': contentType = 'text/markdown'; break;
}
res.writeHead(200, { 'Content-Type': contentType });
res.writeHead(200, { ...SECURITY_HEADERS, 'Content-Type': contentType });
fs.createReadStream(filePath).pipe(res);
} catch (err) {
console.error('Dashboard Server Error:', err);
if (!res.headersSent) {
res.writeHead(500);
res.writeHead(500, SECURITY_HEADERS);
res.end('Internal Server Error');
}
}
@@ -0,0 +1,13 @@
# Decisions for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
## Decision
No changes required.
## Rationale
No actionable TODOs were found in the codebase. The scan returned only documentation references and test data (mktemp usage).
Agent: todo-triage-agent
Prompt: src/prompts/daily/todo-triage-agent.md
@@ -0,0 +1,11 @@
---
agent: design-system-audit-agent
cadence: daily
session: session-2026-02-18T02:35:22Z
run-start: 2026-02-18T02:35:22Z
prompt: src/prompts/daily/design-system-audit-agent.md
---
# Decisions
- Decision: Use automated script for audit.
- Rationale: No existing tool covers all requirements.
+102 -39
View File
@@ -73,10 +73,27 @@ function relayListLabel(relays) {
return relays.join(', ');
}
const PUBLISH_ERROR_CODES = {
TIMEOUT: 'publish_timeout',
DNS: 'dns_resolution',
TCP: 'tcp_connect_timeout',
TLS: 'tls_handshake',
WEBSOCKET: 'websocket_open_failure',
NETWORK: 'network_timeout',
CONNECTION_RESET: 'connection_reset',
RELAY_UNAVAILABLE: 'relay_unavailable',
PERMANENT: 'permanent_validation_error',
};
const PUBLISH_FAILURE_CATEGORIES = {
QUORUM_FAILURE: 'relay_publish_quorum_failure',
NON_RETRYABLE: 'relay_publish_non_retryable',
};
function classifyPublishError(message) {
const normalized = String(message || '').toLowerCase();
if (normalized.includes('publish timed out after') || normalized.includes('publish timeout')) {
return 'publish_timeout';
return PUBLISH_ERROR_CODES.TIMEOUT;
}
if (
normalized.includes('enotfound')
@@ -84,14 +101,14 @@ function classifyPublishError(message) {
|| normalized.includes('getaddrinfo')
|| (normalized.includes('dns') && normalized.includes('websocket'))
) {
return 'dns_resolution';
return PUBLISH_ERROR_CODES.DNS;
}
if (
normalized.includes('connect etimedout')
|| normalized.includes('tcp connect timed out')
|| normalized.includes('connect timeout')
) {
return 'tcp_connect_timeout';
return PUBLISH_ERROR_CODES.TCP;
}
if (
normalized.includes('tls')
@@ -99,20 +116,20 @@ function classifyPublishError(message) {
|| normalized.includes('certificate')
|| normalized.includes('handshake')
) {
return 'tls_handshake';
return PUBLISH_ERROR_CODES.TLS;
}
if (
normalized.includes('websocket')
|| normalized.includes('bad response')
|| normalized.includes('unexpected server response')
) {
return 'websocket_open_failure';
return PUBLISH_ERROR_CODES.WEBSOCKET;
}
if (normalized.includes('timed out') || normalized.includes('timeout') || normalized.includes('etimedout')) {
return 'network_timeout';
return PUBLISH_ERROR_CODES.NETWORK;
}
if (normalized.includes('econnreset') || normalized.includes('connection reset') || normalized.includes('socket hang up')) {
return 'connection_reset';
return PUBLISH_ERROR_CODES.CONNECTION_RESET;
}
if (
normalized.includes('unavailable')
@@ -122,21 +139,21 @@ function classifyPublishError(message) {
|| normalized.includes('enotfound')
|| normalized.includes('503')
) {
return 'relay_unavailable';
return PUBLISH_ERROR_CODES.RELAY_UNAVAILABLE;
}
return 'permanent_validation_error';
return PUBLISH_ERROR_CODES.PERMANENT;
}
function isTransientPublishCategory(category) {
return [
'publish_timeout',
'dns_resolution',
'tcp_connect_timeout',
'tls_handshake',
'websocket_open_failure',
'network_timeout',
'connection_reset',
'relay_unavailable',
PUBLISH_ERROR_CODES.TIMEOUT,
PUBLISH_ERROR_CODES.DNS,
PUBLISH_ERROR_CODES.TCP,
PUBLISH_ERROR_CODES.TLS,
PUBLISH_ERROR_CODES.WEBSOCKET,
PUBLISH_ERROR_CODES.NETWORK,
PUBLISH_ERROR_CODES.CONNECTION_RESET,
PUBLISH_ERROR_CODES.RELAY_UNAVAILABLE,
].includes(category);
}
@@ -188,6 +205,8 @@ export class RelayHealthManager {
constructor() {
this.metricsByRelay = new Map();
this.lastSnapshotAt = 0;
this._metricsVersion = 0;
this._sortedCache = null;
}
ensureMetrics(relay, config) {
@@ -203,31 +222,72 @@ export class RelayHealthManager {
lastResultAt: null,
};
this.metricsByRelay.set(relay, metrics);
this._metricsVersion += 1;
}
return metrics;
}
rankRelays(relays, config, nowMs = Date.now()) {
const summaries = relays.map((relay) => {
const metrics = this.ensureMetrics(relay, config);
const summary = summarizeRelayMetrics(metrics, nowMs);
return {
relay,
summary,
score: computeRelayScore(summary),
};
});
for (const relay of relays) {
this.ensureMetrics(relay, config);
}
return summaries.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1;
if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) {
if (a.summary.averageLatencyMs === null) return 1;
if (b.summary.averageLatencyMs === null) return -1;
return a.summary.averageLatencyMs - b.summary.averageLatencyMs;
if (
!this._sortedCache
|| this._sortedCache.version !== this._metricsVersion
|| nowMs >= this._sortedCache.validUntil
) {
let minQuarantineUntil = Infinity;
const entries = [];
for (const metrics of this.metricsByRelay.values()) {
const summary = summarizeRelayMetrics(metrics, nowMs);
const score = computeRelayScore(summary);
if (metrics.quarantineUntil > nowMs && metrics.quarantineUntil < minQuarantineUntil) {
minQuarantineUntil = metrics.quarantineUntil;
}
entries.push({
relay: metrics.relay,
summary,
score,
metrics,
});
}
return a.relay.localeCompare(b.relay);
});
entries.sort((a, b) => {
if (b.score !== a.score) return b.score - a.score;
if (a.summary.quarantined !== b.summary.quarantined) return a.summary.quarantined ? 1 : -1;
if (a.summary.averageLatencyMs !== b.summary.averageLatencyMs) {
if (a.summary.averageLatencyMs === null) return 1;
if (b.summary.averageLatencyMs === null) return -1;
return a.summary.averageLatencyMs - b.summary.averageLatencyMs;
}
return a.relay.localeCompare(b.relay);
});
this._sortedCache = {
version: this._metricsVersion,
validUntil: minQuarantineUntil,
entries,
};
}
const requestedSet = new Set(relays);
return this._sortedCache.entries
.filter((entry) => requestedSet.has(entry.relay))
.map((entry) => {
const quarantineRemainingMs = entry.metrics.quarantineUntil > nowMs
? entry.metrics.quarantineUntil - nowMs
: 0;
return {
relay: entry.relay,
score: entry.score,
summary: {
...entry.summary,
quarantineRemainingMs,
},
};
});
}
prioritizeRelays(relays, config, nowMs = Date.now()) {
@@ -275,6 +335,7 @@ export class RelayHealthManager {
metrics.quarantineUntil = nowMs + metrics.cooldownMs;
metrics.cooldownMs = Math.min(config.maxQuarantineCooldownMs, Math.floor(metrics.cooldownMs * 1.5));
}
this._metricsVersion += 1;
}
collectSnapshot(relays, config, nowMs = Date.now()) {
@@ -300,6 +361,8 @@ export class RelayHealthManager {
reset() {
this.metricsByRelay.clear();
this.lastSnapshotAt = 0;
this._metricsVersion = 0;
this._sortedCache = null;
}
}
@@ -495,7 +558,7 @@ export class LockPublisher {
let lastAttemptState = null;
const retryTimeline = [];
const overallStartedAt = Date.now();
let terminalFailureCategory = 'relay_publish_non_retryable';
let terminalFailureCategory = PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
for (let attemptNumber = 1; attemptNumber <= this.maxAttempts; attemptNumber += 1) {
const startedAtMs = Date.now();
@@ -583,11 +646,11 @@ export class LockPublisher {
determineFailureCategory(hasTransientFailure, hasPermanentFailure, attemptNumber) {
if (hasTransientFailure && !hasPermanentFailure && attemptNumber >= this.maxAttempts) {
return 'relay_publish_quorum_failure';
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
} else if (hasPermanentFailure) {
return 'relay_publish_non_retryable';
return PUBLISH_FAILURE_CATEGORIES.NON_RETRYABLE;
} else {
return 'relay_publish_quorum_failure';
return PUBLISH_FAILURE_CATEGORIES.QUORUM_FAILURE;
}
}
+20 -10
View File
@@ -5,6 +5,7 @@ import crypto from 'node:crypto';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';
import { DEFAULT_RELAYS } from './constants.mjs';
import { ensureDir } from './utils.mjs';
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
@@ -36,12 +37,6 @@ function getPaths(root, installDirName) {
};
}
function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
function copyDir(src, dest) {
if (fs.existsSync(src)) {
fs.cpSync(src, dest, { recursive: true });
@@ -152,11 +147,26 @@ async function interactiveInit(cwd) {
}
}
async function resolveConfiguration(cwd, mockAnswers) {
if (mockAnswers) {
return mockAnswers;
function validateInstallDir(dir) {
if (dir === '.') return;
// Strict validation to prevent command injection
// Only allow alphanumeric, hyphens, underscores, slashes, and periods.
if (!/^[a-zA-Z0-9_\-\/\.]+$/.test(dir)) {
throw new Error(`Invalid directory name: "${dir}". Only alphanumeric characters, hyphens, underscores, slashes, and periods are allowed.`);
}
return await interactiveInit(cwd);
}
async function resolveConfiguration(cwd, mockAnswers) {
let config;
if (mockAnswers) {
config = mockAnswers;
} else {
config = await interactiveInit(cwd);
}
validateInstallDir(config.installDir);
return config;
}
function ensureInstallDirectory(paths, force, installDir) {
+17 -7
View File
@@ -53,20 +53,20 @@ const FALLBACK_ROSTER = {
let cachedCanonicalRoster = null;
function loadCanonicalRoster() {
function loadCanonicalRoster(fsModule = fs) {
if (cachedCanonicalRoster) return cachedCanonicalRoster;
let rosterPath = ROSTER_FILE;
// Prefer user-managed roster if present
if (fs.existsSync(USER_ROSTER_FILE)) {
if (fsModule.existsSync(USER_ROSTER_FILE)) {
rosterPath = USER_ROSTER_FILE;
} else if (fs.existsSync(CWD_ROSTER_FILE)) {
} else if (fsModule.existsSync(CWD_ROSTER_FILE)) {
rosterPath = CWD_ROSTER_FILE;
}
try {
const parsed = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
const parsed = JSON.parse(fsModule.readFileSync(rosterPath, 'utf8'));
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
const weekly = Array.isArray(parsed.weekly)
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
@@ -86,6 +86,11 @@ function loadCanonicalRoster() {
return cachedCanonicalRoster;
}
/** @internal */
export function _resetRosterCache() {
cachedCanonicalRoster = null;
}
function parseEnvRoster(value) {
if (!value) return null;
return value
@@ -94,11 +99,16 @@ function parseEnvRoster(value) {
.filter(Boolean);
}
export async function getRoster(cadence) {
const config = await loadTorchConfig();
export async function getRoster(cadence, deps = {}) {
const {
loadTorchConfig: loadTorchConfigFn = loadTorchConfig,
fs: fsModule = fs,
} = deps;
const config = await loadTorchConfigFn();
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
const canonical = loadCanonicalRoster();
const canonical = loadCanonicalRoster(fsModule);
const dailyFromConfig = config.nostrLock.dailyRoster;
const weeklyFromConfig = config.nostrLock.weeklyRoster;
+1
View File
@@ -7,6 +7,7 @@ import { getMemoryPruneMode, isMemoryIngestEnabled } from './feature-flags.js';
import fs from 'node:fs';
import path from 'node:path';
import util from 'node:util';
import { ensureDir } from '../../utils.mjs';
const MEMORY_FILE_PATH = path.join(process.cwd(), '.scheduler-memory', 'memory-store.json');
const debug = util.debuglog('torch-memory');
+21 -15
View File
@@ -67,22 +67,29 @@ function evaluateCandidate(memory, policy) {
* @param {{ getEmbedding?: (memory: import('./schema.js').MemoryRecord) => number[] | null | undefined, similarityThreshold: number, duplicateWindowMs: number }} options
*/
function groupNearDuplicates(memories, options) {
// Sort by created_at to enable early exit in the inner loop
const sortedMemories = [...memories].sort((a, b) => a.created_at - b.created_at);
const groups = [];
const consumed = new Set();
for (let i = 0; i < memories.length; i += 1) {
const base = memories[i];
for (let i = 0; i < sortedMemories.length; i += 1) {
const base = sortedMemories[i];
if (consumed.has(base.id) || base.merged_into) continue;
const baseEmbedding = options.getEmbedding?.(base);
const group = [base];
for (let j = i + 1; j < memories.length; j += 1) {
const probe = memories[j];
for (let j = i + 1; j < sortedMemories.length; j += 1) {
const probe = sortedMemories[j];
// Optimization: Since sorted by created_at, if the time difference exceeds the window,
// all subsequent items will also be outside the window.
if (probe.created_at - base.created_at > options.duplicateWindowMs) break;
if (consumed.has(probe.id) || probe.merged_into || probe.id === base.id) continue;
const withinWindow = Math.abs(base.created_at - probe.created_at) <= options.duplicateWindowMs;
if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
if (!hasTagOverlap(base.tags, probe.tags)) continue;
const similarity = cosineSimilarity(baseEmbedding, options.getEmbedding?.(probe));
if (similarity >= options.similarityThreshold) {
@@ -189,12 +196,13 @@ export async function createLifecyclePlan(memories, options) {
});
const templates = loadMemoryPromptTemplates({ templateDir: options.templateDir });
const condensedGroups = [];
const condensedGroups = await Promise.all(
groups.map((group) => condenseGroup(group, options.generateSummary, templates.condense))
);
const mergedIntoMap = new Map();
for (const group of groups) {
const condensed = await condenseGroup(group, options.generateSummary, templates.condense);
condensedGroups.push(condensed);
for (const condensed of condensedGroups) {
for (const mergedId of condensed.mergedIds) {
mergedIntoMap.set(mergedId, condensed.merged.id);
}
@@ -265,9 +273,7 @@ export async function createLifecyclePlan(memories, options) {
* @param {Awaited<ReturnType<typeof createLifecyclePlan>>} plan
*/
export async function applyLifecycleActions(repository, plan) {
const results = [];
for (const decision of plan.actions) {
const results = await Promise.all(plan.actions.map(async (decision) => {
if (decision.merged_into && typeof repository.markMerged === 'function') {
await repository.markMerged(decision.id, decision.merged_into);
}
@@ -280,8 +286,8 @@ export async function applyLifecycleActions(repository, plan) {
await repository.keepMemory?.(decision.id, decision.reason);
}
results.push(decision);
}
return decision;
}));
return {
applied: results,
+15 -9
View File
@@ -31,17 +31,20 @@ function computeRecencyDecay(timestamp, now, halfLifeMs) {
return Math.exp(-(age / halfLifeMs));
}
function textMatchSimilarity(query, memory) {
const normalizedQuery = String(query ?? '').trim().toLowerCase();
if (!normalizedQuery) return 0;
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
const haystack = `${memory.summary} ${memory.content}`.toLowerCase();
if (haystack.includes(normalizedQuery)) return 1;
function textMatchSimilarity(queryTerms, queryRegexes, memory) {
if (!queryTerms || queryTerms.length === 0) return 0;
const queryTerms = normalizedQuery.split(/\s+/).filter(Boolean);
if (queryTerms.length === 0) return 0;
let matchedTerms = 0;
for (const regex of queryRegexes) {
if (regex.test(memory.summary) || regex.test(memory.content)) {
matchedTerms++;
}
}
const matchedTerms = queryTerms.filter((term) => haystack.includes(term)).length;
return matchedTerms / queryTerms.length;
}
@@ -102,8 +105,11 @@ async function buildSimilarityIndex(memories, params) {
}
}
const queryTerms = normalizedQuery.toLowerCase().split(/\s+/).filter(Boolean);
const queryRegexes = queryTerms.map((t) => new RegExp(escapeRegExp(t), 'i'));
for (const memory of memories) {
const lexicalScore = textMatchSimilarity(normalizedQuery, memory);
const lexicalScore = textMatchSimilarity(queryTerms, queryRegexes, memory);
if (lexicalScore > 0) {
similarityById.set(memory.id, lexicalScore);
}
@@ -0,0 +1,10 @@
# Test Logs for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
- Command: Scan for TODOs
Result: 0 actionable items found.
Agent: todo-triage-agent
Prompt: src/prompts/daily/todo-triage-agent.md
@@ -0,0 +1,11 @@
---
agent: design-system-audit-agent
cadence: daily
session: session-2026-02-18T02:35:22Z
run-start: 2026-02-18T02:35:22Z
prompt: src/prompts/daily/design-system-audit-agent.md
---
# Test Log
- Command: npm run lint
- Result: Passed (with warnings)
+13
View File
@@ -0,0 +1,13 @@
# Todo List for Run 2026-02-17T16-36-25Z
- Agent: todo-triage-agent
- Cadence: daily
- Run-start: 2026-02-17T16-36-25Z
## Completed
- [x] Scan codebase for TODOs (todo-triage-agent)
- [x] Triage findings (0 found)
## Pending
None.
Ref: src/prompts/daily/todo-triage-agent.md
+12
View File
@@ -0,0 +1,12 @@
---
agent: design-system-audit-agent
cadence: daily
session: session-2026-02-18T02:35:22Z
run-start: 2026-02-18T02:35:22Z
prompt: src/prompts/daily/design-system-audit-agent.md
---
# Todo
## Pending Tasks
- [ ] Review unsanctioned colors.
+8
View File
@@ -1,3 +1,5 @@
import fs from 'node:fs';
export function todayDateStr() {
return new Date().toISOString().slice(0, 10);
}
@@ -34,3 +36,9 @@ export function detectPlatform() {
if (process.env.GOOSE_API_KEY || process.env.GOOSE_SESSION_ID) return 'goose';
return null;
}
export function ensureDir(dir) {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
}
@@ -0,0 +1,20 @@
# Agent Run: todo-triage-agent
- **Date:** 2026-02-17T16-36-25Z
- **Cadence:** daily
- **Platform:** jules
- **Status:** completed
## Steps
1. Lock acquired.
2. Memory retrieved.
3. Codebase scanned (0 actionable items found).
4. Artifacts updated (todo_issues.md, CONTEXT, TODO, DECISIONS, TEST_LOG).
5. Memory stored.
6. Verification passed.
7. Completion published.
## Results
- No actionable TODOs found in src/, docs/, bin/, scripts/.
- Memory updated.
- No changes required.
@@ -0,0 +1,45 @@
---
agent: design-system-audit-agent
status: completed
platform: linux
---
# Design System Audit Report
Date: 2026-02-18T02:34:58.298Z
## 1. Inline Styles
Found 8 instances.
### Top Offending Files
- landing/index.html: 8
### Sample Snippets
- `style="font-size: 2rem; margin-bottom: 2rem; color: var(--text-strong);"` in landing/index.html
- `style="margin-top: 1rem;"` in landing/index.html
- `style="margin-top: 4rem; text-align: center; width: 100%; max-width: 600px; padding-top: 2rem; border-top: 1px solid var(--border);"` in landing/index.html
- `style="font-size: 1.25rem; margin-bottom: 0.5rem; color: var(--text-strong);"` in landing/index.html
- `style="color: var(--muted); margin-bottom: 1.5rem; font-size: 0.95rem;"` in landing/index.html
## 2. Unsanctioned Hex Colors
Found 6 instances.
### Top Offending Colors
- #039: 1
- #0d1117: 1
- #e6edf3: 1
- #a5d6ff: 1
- #ff4d4d: 1
- #ef4444: 1
### Sample Locations
- #039 in dashboard/domUtils.js
- #0d1117 in landing/index.html
- #e6edf3 in landing/index.html
- #a5d6ff in landing/index.html
- #ff4d4d in landing/index.html
## 3. Raw Lengths
(Skipped: requires AST parsing)
## Next Steps
- Review unsanctioned colors and replace with tokens.
- Refactor inline styles to CSS classes.
-187
View File
@@ -1,187 +0,0 @@
import { describe, it, mock, afterEach, after } from 'node:test';
import assert from 'node:assert';
import { cmdCheck } from '../src/cmd-check.mjs';
// Mocks
const mockLoadTorchConfig = mock.fn(() => ({
scheduler: {
paused: {
daily: [],
weekly: [],
},
},
}));
const mockGetRelays = mock.fn(() => ['wss://mock-relay']);
const mockGetNamespace = mock.fn(() => 'mock-namespace');
const mockGetRoster = mock.fn(() => []);
const mockQueryLocks = mock.fn(async () => []);
const mockTodayDateStr = mock.fn(() => '2023-10-27');
const deps = {
loadTorchConfigFn: mockLoadTorchConfig,
getRelaysFn: mockGetRelays,
getNamespaceFn: mockGetNamespace,
getRosterFn: mockGetRoster,
queryLocksFn: mockQueryLocks,
todayDateStrFn: mockTodayDateStr,
};
describe('cmdCheck', () => {
// Capture console output
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
let consoleLogOutput = [];
let consoleErrorOutput = [];
const mockConsoleLog = mock.fn((msg) => {
consoleLogOutput.push(msg);
});
const mockConsoleError = mock.fn((msg) => {
consoleErrorOutput.push(msg);
});
// Setup console mocks
console.log = mockConsoleLog;
console.error = mockConsoleError;
afterEach(() => {
// Reset mocks
mockLoadTorchConfig.mock.resetCalls();
mockGetRelays.mock.resetCalls();
mockGetNamespace.mock.resetCalls();
mockGetRoster.mock.resetCalls();
mockQueryLocks.mock.resetCalls();
mockTodayDateStr.mock.resetCalls();
// Clear captured output
consoleLogOutput = [];
consoleErrorOutput = [];
});
// Restore console after all tests
after(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
});
it('correctly reports available agents when no locks exist', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2']);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.strictEqual(result.cadence, 'daily');
assert.strictEqual(result.namespace, 'mock-namespace');
assert.strictEqual(result.date, '2023-10-27');
assert.deepStrictEqual(result.available, ['agent1', 'agent2']);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.paused, []);
assert.deepStrictEqual(result.excluded, []);
assert.strictEqual(result.lockCount, 0);
// Verify console output was JSON
assert.strictEqual(consoleLogOutput.length, 1);
const logOutput = JSON.parse(consoleLogOutput[0]);
assert.deepStrictEqual(logOutput.available, ['agent1', 'agent2']);
});
it('correctly identifies locked agents', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2', 'agent3']);
mockQueryLocks.mock.mockImplementation(async () => [
{ agent: 'agent1', eventId: 'evt1', createdAtIso: '2023-10-27T00:00:00Z', expiresAtIso: '2023-10-27T01:00:00Z', platform: 'linux' }
]);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, ['agent2', 'agent3']);
assert.deepStrictEqual(result.locked, ['agent1']);
assert.deepStrictEqual(result.excluded, ['agent1']);
assert.strictEqual(result.lockCount, 1);
assert.strictEqual(result.locks[0].agent, 'agent1');
});
it('correctly identifies paused agents', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2', 'agent3']);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: ['agent2'] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, ['agent1', 'agent3']);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.paused, ['agent2']);
assert.deepStrictEqual(result.excluded, ['agent2']);
});
it('handles agents that are both locked and paused', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2']);
mockQueryLocks.mock.mockImplementation(async () => [
{ agent: 'agent1', eventId: 'evt1' }
]);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: ['agent1'] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, ['agent2']);
assert.deepStrictEqual(result.locked, ['agent1']);
assert.deepStrictEqual(result.paused, ['agent1']);
// excluded should contain unique agents
assert.deepStrictEqual(result.excluded, ['agent1']);
});
it('identifies unknown locked agents (not in roster)', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1']);
mockQueryLocks.mock.mockImplementation(async () => [
{ agent: 'agent1', eventId: 'evt1' },
{ agent: 'unknown-agent', eventId: 'evt2' }
]);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, []);
assert.deepStrictEqual(result.locked, ['agent1', 'unknown-agent']);
assert.deepStrictEqual(result.unknownLockedAgents, ['unknown-agent']);
assert.deepStrictEqual(result.excluded, ['agent1', 'unknown-agent']);
});
it('handles weekly cadence', async () => {
mockGetRoster.mock.mockImplementation(() => ['weekly1', 'weekly2']);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { weekly: ['weekly2'] } }
}));
const result = await cmdCheck('weekly', deps);
assert.strictEqual(result.cadence, 'weekly');
assert.deepStrictEqual(result.available, ['weekly1']);
assert.deepStrictEqual(result.paused, ['weekly2']);
});
it('handles empty roster', async () => {
mockGetRoster.mock.mockImplementation(() => []);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, []);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.paused, []);
});
});
+116
View File
@@ -0,0 +1,116 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { cmdDashboard } from '../src/dashboard.mjs';
const tempConfigPath = path.resolve('torch-config-exposure-test.json');
const SENSITIVE_CONFIG = {
nostrLock: {
namespace: 'test-namespace',
relays: ['wss://relay.example.com'],
dailyRoster: ['user1', 'user2'], // Sensitive roster
weeklyRoster: ['user3'],
ttlSeconds: 3600,
queryTimeoutMs: 5000,
publishTimeoutMs: 5000,
minSuccessfulRelayPublishes: 2,
minActiveRelayPool: 1,
},
dashboard: {
auth: 'secret-auth-string', // stripped by auth check
relays: ['wss://dashboard.relay.example.com'],
},
scheduler: {
paused: {
daily: ['user1'],
},
firstPromptByCadence: {
daily: 'some prompt',
},
},
};
test('Dashboard Configuration Exposure Security', async (t) => {
// Setup temp config
fs.writeFileSync(tempConfigPath, JSON.stringify(SENSITIVE_CONFIG, null, 2));
// Override env to use this config
const originalEnvPath = process.env.TORCH_CONFIG_PATH;
const originalAuth = process.env.TORCH_DASHBOARD_AUTH;
process.env.TORCH_CONFIG_PATH = tempConfigPath;
// Disable auth via env so we can access the endpoint without credentials
// The vulnerability is about exposure of *other* fields.
// We need to bypass the basic auth middleware to test the JSON response.
// However, cmdDashboard checks config.dashboard.auth too.
// If we want to test that auth is stripped from JSON, we should enable auth but provide credentials.
// OR we can just test the sanitization logic which runs regardless of how we got there?
// No, the endpoint logic runs after auth check.
// To keep it simple: we want to verify that IF we can access the endpoint, the sensitive data is gone.
// We can simulate an authorized request or disable auth.
// If we disable auth in config, we can access it.
// Let's modify the config for the test to NOT have auth, so we can access it freely.
// We want to verify that things like 'dailyRoster' are stripped.
const configForTest = { ...SENSITIVE_CONFIG };
delete configForTest.dashboard.auth;
fs.writeFileSync(tempConfigPath, JSON.stringify(configForTest, null, 2));
let server;
let serverPort;
// Cleanup function
t.after(() => {
if (server) server.close();
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
if (originalEnvPath) process.env.TORCH_CONFIG_PATH = originalEnvPath;
else delete process.env.TORCH_CONFIG_PATH;
});
// Start server
await new Promise((resolve) => {
cmdDashboard(0).then((srv) => {
server = srv;
serverPort = server.address().port;
resolve();
});
});
// Fetch config
const responseBody = await new Promise((resolve, reject) => {
http.get(`http://localhost:${serverPort}/torch-config.json`, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve(data));
}).on('error', reject);
});
let exposedConfig;
try {
exposedConfig = JSON.parse(responseBody);
} catch (e) {
assert.fail(`Failed to parse JSON response: ${responseBody}`);
}
// 1. Assert sensitive fields are NOT present
assert.equal(exposedConfig.nostrLock?.dailyRoster, undefined, 'dailyRoster should NOT be exposed');
assert.equal(exposedConfig.nostrLock?.weeklyRoster, undefined, 'weeklyRoster should NOT be exposed');
assert.equal(exposedConfig.nostrLock?.relayFallbacks, undefined, 'relayFallbacks should NOT be exposed');
assert.equal(exposedConfig.scheduler, undefined, 'scheduler config should NOT be exposed');
assert.equal(exposedConfig.raw, undefined, 'raw config should NOT be exposed');
// 2. Assert required fields ARE present
assert.ok(exposedConfig.dashboard, 'dashboard config should be present');
// Note: cmdDashboard loads from file, parseTorchConfig normalizes keys.
// check dashboard.relays
assert.deepEqual(exposedConfig.dashboard.relays, ['wss://dashboard.relay.example.com'], 'dashboard relays should be present');
assert.ok(exposedConfig.nostrLock, 'nostrLock config should be present');
assert.equal(exposedConfig.nostrLock.namespace, 'test-namespace', 'nostrLock namespace should be present');
assert.deepEqual(exposedConfig.nostrLock.relays, ['wss://relay.example.com'], 'nostrLock relays should be present');
});
@@ -0,0 +1,95 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { cmdDashboard } from '../src/dashboard.mjs';
test('Dashboard Path Traversal Security Fix', async (t) => {
let testPort = 0;
let server;
t.beforeEach(async () => {
// Start dashboard on random port
server = await cmdDashboard(0);
testPort = server.address().port;
});
t.afterEach((done) => {
if (server) server.close(done);
else done();
});
function get(path) {
return new Promise((resolve, reject) => {
const req = http.get({
host: '127.0.0.1',
port: testPort,
path: path
}, (res) => {
// Consume data to avoid memory leaks
res.resume();
resolve(res);
});
req.on('error', reject);
});
}
// --- Valid Access ---
await t.test('allows access to valid dashboard index', async () => {
const res = await get('/dashboard/index.html');
assert.strictEqual(res.statusCode, 200, 'Should allow access to dashboard index');
});
// --- Traversal Attempts ---
const traversalPayloads = [
'/../../etc/passwd',
'/dashboard/../../etc/passwd',
'/dashboard/../../../etc/passwd',
'/../package.json',
'/dashboard/../package.json',
'/dashboard/%2e%2e/package.json', // URL Encoded ..
'/dashboard/%2e%2e%2fpackage.json', // URL Encoded ../
'/..%2fpackage.json',
'/%2e%2e%2fpackage.json',
'/dashboard/..%5cpackage.json', // Encoded backslash
'/dashboard/..%252fpackage.json', // Double encoded
'/dashboard/....//package.json', // Weird dots
'/dashboard/.../package.json', // Three dots
'//etc/passwd', // Double slash
'/dashboard//../../etc/passwd',
];
for (const payload of traversalPayloads) {
await t.test(`blocks traversal attempt: ${payload}`, async () => {
const res = await get(payload);
// Depending on how it's resolved, it might be 403 (Forbidden) or 404 (Not Found)
// If it escapes allowed paths, it should be 403.
// If it stays within allowed paths but file doesn't exist, 404.
// But we want to ensure it doesn't return 200 for sensitive files if they existed.
// Since we don't know if /etc/passwd exists on test environment, we assume 403 or 404 is safe.
// However, for package.json (which exists in root), it MUST be 403 because it's not in allowed paths.
// If the payload targets a file outside allowed paths (like package.json), it MUST be 403.
// If the payload targets a file that doesn't exist but is outside allowed paths, it MUST be 403.
// The current logic returns 403 if not in allowedPaths.
assert.notStrictEqual(res.statusCode, 200, `Should not return 200 for ${payload}`);
// If it targets package.json, we expect 403 because it exists but is restricted.
// EXCEPTION: If the payload uses encoded slashes (%2f) that are NOT decoded by URL parser,
// it is treated as a filename, not a separator. Thus it looks for a file with %2f in name,
// which doesn't exist -> 404. This is SAFE.
// So for package.json payloads, we accept 403 (blocked traversal) or 404 (failed traversal/not found).
if (payload.includes('package.json')) {
assert.ok([403, 404].includes(res.statusCode), `Should return 403 or 404 for ${payload}, got ${res.statusCode}`);
}
});
}
// --- Allowed Traversal (within allowed scope) ---
await t.test('allows traversal within allowed paths', async () => {
// /dashboard/../dashboard/index.html -> /dashboard/index.html (Allowed)
const res = await get('/dashboard/../dashboard/index.html');
assert.strictEqual(res.statusCode, 200, 'Should allow traversal that stays within allowed paths');
});
});
+32
View File
@@ -69,4 +69,36 @@ test('Dashboard File Access Security', async (t) => {
// path.normalize collapses this to /package.json, which is then blocked
assert.strictEqual(res.statusCode, 403);
});
await t.test('responses include security headers', async () => {
const res = await get('/dashboard/index.html');
assert.strictEqual(res.statusCode, 200);
const headers = res.headers;
// Content-Security-Policy
assert.ok(headers['content-security-policy'], 'CSP header missing');
const csp = headers['content-security-policy'];
assert.match(csp, /default-src 'self'/, 'CSP: missing default-src self');
assert.match(csp, /script-src 'self' 'unsafe-inline' https:\/\/cdn\.jsdelivr\.net/, 'CSP: missing script-src');
assert.match(csp, /object-src 'none'/, 'CSP: missing object-src none');
// X-Content-Type-Options
assert.strictEqual(headers['x-content-type-options'], 'nosniff', 'X-Content-Type-Options mismatch');
// X-Frame-Options
assert.strictEqual(headers['x-frame-options'], 'DENY', 'X-Frame-Options mismatch');
// Referrer-Policy
assert.strictEqual(headers['referrer-policy'], 'strict-origin-when-cross-origin', 'Referrer-Policy mismatch');
});
await t.test('error responses include security headers', async () => {
const res = await get('/dashboard/nonexistent.html');
assert.strictEqual(res.statusCode, 404);
const headers = res.headers;
assert.strictEqual(headers['x-content-type-options'], 'nosniff');
assert.strictEqual(headers['x-frame-options'], 'DENY');
});
});
+109
View File
@@ -0,0 +1,109 @@
import { test } from 'node:test';
import assert from 'node:assert';
import { cmdInit } from '../src/ops.mjs';
import fs from 'node:fs';
import path from 'node:path';
const TEST_DIR = 'test_validation_env';
test('cmdInit should validate install directory name', async (t) => {
if (fs.existsSync(TEST_DIR)) {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
}
fs.mkdirSync(TEST_DIR);
const mockPkg = { name: "host", scripts: {} };
fs.writeFileSync(path.join(TEST_DIR, 'package.json'), JSON.stringify(mockPkg));
await t.test('should reject directory with semicolon', async () => {
await assert.rejects(async () => {
await cmdInit(false, TEST_DIR, {
installDir: 'torch; echo pwned',
namespace: 'ns',
relays: []
});
}, /Invalid directory name/);
});
await t.test('should reject directory with spaces', async () => {
await assert.rejects(async () => {
await cmdInit(false, TEST_DIR, {
installDir: 'torch dir',
namespace: 'ns',
relays: []
});
}, /Invalid directory name/);
});
await t.test('should reject directory with quotes', async () => {
await assert.rejects(async () => {
await cmdInit(false, TEST_DIR, {
installDir: 'torch"dir',
namespace: 'ns',
relays: []
});
}, /Invalid directory name/);
});
await t.test('should reject directory with backticks', async () => {
await assert.rejects(async () => {
await cmdInit(false, TEST_DIR, {
installDir: 'torch`dir',
namespace: 'ns',
relays: []
});
}, /Invalid directory name/);
});
await t.test('should reject directory with $', async () => {
await assert.rejects(async () => {
await cmdInit(false, TEST_DIR, {
installDir: 'torch$dir',
namespace: 'ns',
relays: []
});
}, /Invalid directory name/);
});
await t.test('should accept valid directory names', async () => {
try {
await cmdInit(false, TEST_DIR, {
installDir: 'valid-dir_123',
namespace: 'ns',
relays: []
});
} catch (e) {
// It might fail because of other initialization steps, but shouldn't fail validation
assert.notMatch(e.message, /Invalid directory name/);
}
});
await t.test('should accept "."', async () => {
try {
await cmdInit(false, TEST_DIR, {
installDir: '.',
namespace: 'ns',
relays: []
});
} catch (e) {
assert.notMatch(e.message, /Invalid directory name/);
}
});
await t.test('should accept nested paths with slashes', async () => {
try {
await cmdInit(false, TEST_DIR, {
installDir: 'path/to/torch',
namespace: 'ns',
relays: []
});
} catch (e) {
assert.notMatch(e.message, /Invalid directory name/);
}
});
// Cleanup
if (fs.existsSync(TEST_DIR)) {
fs.rmSync(TEST_DIR, { recursive: true, force: true });
}
});
+15 -4
View File
@@ -138,11 +138,22 @@ test('applyLifecycleActions executes keep/archive/delete and merge markers', asy
policyLogs: [],
});
assert.deepEqual(calls, [
['keepMemory', 'k1', 'keep reason'],
['markMerged', 'a1', 'k1'],
// Sort calls to verify presence regardless of execution order
const sortedCalls = [...calls].sort((a, b) => a[0].localeCompare(b[0]) || a[1].localeCompare(b[1]));
const expectedCalls = [
['archiveMemory', 'a1', 'archive reason'],
['deleteMemory', 'd1', 'delete reason'],
]);
['keepMemory', 'k1', 'keep reason'],
['markMerged', 'a1', 'k1'],
];
assert.deepEqual(sortedCalls, expectedCalls);
// Verify that markMerged happens before archiveMemory for the same ID
const markIndex = calls.findIndex((c) => c[0] === 'markMerged' && c[1] === 'a1');
const archiveIndex = calls.findIndex((c) => c[0] === 'archiveMemory' && c[1] === 'a1');
assert.ok(markIndex !== -1, 'markMerged was called');
assert.ok(archiveIndex !== -1, 'archiveMemory was called');
assert.ok(markIndex < archiveIndex, 'markMerged should precede archiveMemory');
assert.equal(result.applied.length, 3);
});
+71
View File
@@ -69,4 +69,75 @@ describe('RelayHealthManager', () => {
manager.reset();
assert.strictEqual(manager.metricsByRelay.size, 0);
});
it('should cache ranked results and invalidate on outcome', () => {
const manager = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 2,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
minActiveRelayPool: 1,
};
const now = Date.now();
manager.recordOutcome('wss://r1', true, null, 100, config, now);
manager.recordOutcome('wss://r2', true, null, 200, config, now);
const result1 = manager.rankRelays(['wss://r1', 'wss://r2'], config, now);
// Internal cache should be populated
assert.ok(manager._sortedCache);
assert.strictEqual(manager._sortedCache.version, manager._metricsVersion);
const result2 = manager.rankRelays(['wss://r1', 'wss://r2'], config, now);
// Should be same objects (deep equality check on content, but references might differ due to map)
// Actually rankRelays returns new objects always.
assert.deepStrictEqual(result1, result2);
// Modify outcome
manager.recordOutcome('wss://r1', false, 'timeout', 1000, config, now);
// Version should increase
assert.ok(manager._sortedCache.version < manager._metricsVersion);
const result3 = manager.rankRelays(['wss://r1', 'wss://r2'], config, now);
// Cache should be updated
assert.strictEqual(manager._sortedCache.version, manager._metricsVersion);
assert.notDeepStrictEqual(result1, result3);
});
it('should invalidate cache when time passes quarantine expiry', () => {
const manager = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 1, // fails immediately
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
minActiveRelayPool: 1,
};
let now = 10000;
manager.recordOutcome('wss://r1', false, 'fail', 100, config, now);
// r1 is quarantined until 11000
const result1 = manager.rankRelays(['wss://r1'], config, now);
assert.strictEqual(result1[0].summary.quarantined, true);
assert.strictEqual(manager._sortedCache.validUntil, 11000);
// Advance time but before expiry
now = 10500;
const result2 = manager.rankRelays(['wss://r1'], config, now);
// Cache should still be valid (version same, now < validUntil)
// Quarantine remaining should be less
assert.strictEqual(result2[0].summary.quarantined, true);
assert.strictEqual(result2[0].summary.quarantineRemainingMs, 500);
// Advance time past expiry
now = 11001;
const result3 = manager.rankRelays(['wss://r1'], config, now);
// Cache should be invalidated because now >= validUntil
assert.strictEqual(result3[0].summary.quarantined, false);
assert.strictEqual(result3[0].summary.quarantineRemainingMs, 0);
// validUntil should be Infinity (no quarantined relays)
assert.strictEqual(manager._sortedCache.validUntil, Infinity);
});
});
+152 -31
View File
@@ -13,38 +13,159 @@ function entry(timestamp, healthyRelays, totalRelays = 3) {
};
}
test('raises all-relays-down duration alert after threshold', () => {
test('evaluateAlertThresholds', async (t) => {
const now = Date.parse('2026-02-15T12:00:00Z');
const history = [
entry('2026-02-15T11:30:00Z', 1),
entry('2026-02-15T11:45:00Z', 0),
entry('2026-02-15T11:55:00Z', 0),
entry('2026-02-15T12:00:00Z', 0),
];
const minute = 60_000;
const current = history[history.length - 1];
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.1,
windowMinutes: 60,
}, { nowMs: now });
await t.test('Happy Path: No alerts when thresholds are not met', () => {
const history = [
entry(new Date(now - 10 * minute).toISOString(), 3),
entry(new Date(now).toISOString(), 3),
];
const current = history[history.length - 1];
assert.ok(result.alerts.some((alert) => alert.type === 'all_relays_down_duration'));
});
test('raises success-rate alert when rolling health falls below threshold', () => {
const history = [
entry('2026-02-15T11:50:00Z', 3),
entry('2026-02-15T11:55:00Z', 0),
entry('2026-02-15T12:00:00Z', 0),
];
const current = history[history.length - 1];
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 999,
minSuccessRate: 0.5,
windowMinutes: 30,
}, { nowMs: Date.parse('2026-02-15T12:00:00Z') });
assert.ok(result.alerts.some((alert) => alert.type === 'relay_success_rate_below_threshold'));
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.5,
windowMinutes: 60,
}, { nowMs: now });
assert.equal(result.alerts.length, 0);
});
await t.test('All Relays Down: Triggers alert when duration equals threshold', () => {
// 10 minutes down exactly
const history = [
entry(new Date(now - 10 * minute).toISOString(), 3), // healthy 10 mins ago
entry(new Date(now - 5 * minute).toISOString(), 0),
entry(new Date(now).toISOString(), 0),
];
const current = history[history.length - 1];
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.1,
windowMinutes: 60,
}, { nowMs: now });
const alert = result.alerts.find(a => a.type === 'all_relays_down_duration');
assert.ok(alert);
assert.equal(alert.actualMinutes, 10);
});
await t.test('All Relays Down: Does NOT trigger alert just below threshold', () => {
// 9 minutes down
const history = [
entry(new Date(now - 9 * minute).toISOString(), 3), // healthy 9 mins ago
entry(new Date(now).toISOString(), 0),
];
const current = history[history.length - 1];
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.1,
windowMinutes: 60,
}, { nowMs: now });
const alert = result.alerts.find(a => a.type === 'all_relays_down_duration');
assert.equal(alert, undefined);
});
await t.test('Success Rate: Triggers alert when rate is strictly below threshold', () => {
const history = [
entry(new Date(now).toISOString(), 1, 4), // 25% healthy
];
const current = history[history.length - 1];
// Threshold 0.3 > 0.25 -> Alert
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 999,
minSuccessRate: 0.3,
windowMinutes: 60,
}, { nowMs: now });
const alert = result.alerts.find(a => a.type === 'relay_success_rate_below_threshold');
assert.ok(alert);
assert.equal(alert.actual, 0.25);
});
await t.test('Success Rate: Does NOT trigger alert when rate equals threshold', () => {
const history = [
entry(new Date(now).toISOString(), 1, 4), // 25% healthy
];
const current = history[history.length - 1];
// Threshold 0.25 == 0.25 -> No Alert
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 999,
minSuccessRate: 0.25,
windowMinutes: 60,
}, { nowMs: now });
const alert = result.alerts.find(a => a.type === 'relay_success_rate_below_threshold');
assert.equal(alert, undefined);
});
await t.test('Success Rate: Does NOT trigger alert when rate is above threshold', () => {
const history = [
entry(new Date(now).toISOString(), 3, 4), // 75% healthy
];
const current = history[history.length - 1];
// Threshold 0.5 < 0.75 -> No Alert
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 999,
minSuccessRate: 0.5,
windowMinutes: 60,
}, { nowMs: now });
const alert = result.alerts.find(a => a.type === 'relay_success_rate_below_threshold');
assert.equal(alert, undefined);
});
await t.test('Multiple Alerts: Both conditions met', () => {
// Down for 20 mins (threshold 10) AND rate 0 (threshold 0.5)
const history = [
entry(new Date(now - 20 * minute).toISOString(), 3), // Healthy 20m ago
entry(new Date(now - 10 * minute).toISOString(), 0),
entry(new Date(now).toISOString(), 0),
];
const current = history[history.length - 1];
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.5,
windowMinutes: 60,
}, { nowMs: now });
assert.equal(result.alerts.length, 2);
assert.ok(result.alerts.some(a => a.type === 'all_relays_down_duration'));
assert.ok(result.alerts.some(a => a.type === 'relay_success_rate_below_threshold'));
});
await t.test('Edge Case: Empty history (should not crash)', () => {
const history = [];
const current = entry(new Date(now).toISOString(), 3); // current result is healthy
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.5,
windowMinutes: 60,
}, { nowMs: now });
assert.equal(result.alerts.length, 0);
});
await t.test('Edge Case: Empty history but current result is unhealthy', () => {
const history = [];
const current = entry(new Date(now).toISOString(), 0); // current result is unhealthy
const result = evaluateAlertThresholds(current, history, {
allRelaysDownMinutes: 10,
minSuccessRate: 0.5,
windowMinutes: 60,
}, { nowMs: now });
assert.equal(result.alerts.length, 0);
});
});
+116 -106
View File
@@ -2,122 +2,132 @@ import { test } from 'node:test';
import assert from 'node:assert/strict';
import { summarizeHistory } from '../src/relay-health.mjs';
function entry(timestamp, healthyRelays, totalRelays = 3) {
function createEntry(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;
test('summarizeHistory - basic statistics calculation', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
// Chronological order (oldest to newest)
createEntry('2024-01-01T11:45:00Z', 0, 3), // 0% healthy
createEntry('2024-01-01T11:50:00Z', 1, 3), // 33% healthy
createEntry('2024-01-01T11:55:00Z', 3, 3), // 100% healthy
];
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 });
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 2);
assert.equal(result.successRate, 0.5);
});
assert.equal(result.sampleCount, 3);
// Total probes: 9, Total healthy: 4. Rate: 4/9
assert.equal(result.successRate, 4 / 9);
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);
});
// Last healthy was at 11:55 (5 minutes ago)
assert.equal(result.allDownDurationMinutes, 5);
});
test('summarizeHistory - handles empty history', () => {
const result = summarizeHistory([], { nowMs: Date.now() });
assert.equal(result.sampleCount, 0);
assert.equal(result.successRate, 1); // Default when sampleCount is 0
assert.equal(result.allDownDurationMinutes, null);
});
test('summarizeHistory - respects time window for stats', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
// Chronological order
createEntry('2024-01-01T10:00:00Z', 0, 3), // Outside 60m window (120m ago)
createEntry('2024-01-01T11:55:00Z', 3, 3), // Inside 60m window (5m ago)
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1);
assert.equal(result.successRate, 1); // Only the recent healthy entry counts
});
test('summarizeHistory - allDownDurationMinutes considers outside window entries', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
// Chronological order
createEntry('2024-01-01T10:00:00Z', 3, 3), // Outside window, healthy
createEntry('2024-01-01T11:55:00Z', 0, 3), // Inside window, unhealthy
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1); // Only recent one counted
assert.equal(result.successRate, 0); // 0/3 = 0
// Last healthy was at 10:00 (120 minutes ago)
assert.equal(result.allDownDurationMinutes, 120);
});
test('summarizeHistory - allDownDurationMinutes is null if never healthy', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
createEntry('2024-01-01T11:55:00Z', 0, 3),
createEntry('2024-01-01T10:00:00Z', 0, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, null);
});
test('summarizeHistory - allDownDurationMinutes is 0 if currently healthy', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
createEntry('2024-01-01T12:00:00Z', 3, 3),
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.allDownDurationMinutes, 0);
});
test('summarizeHistory - handles malformed entries gracefully', () => {
const now = Date.now();
const history = [
{}, // Missing everything
{ timestamp: 'invalid-date' },
{ timestamp: new Date(now).toISOString(), summary: null },
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
// Should calculate stats safely (ignoring malformed or treating as 0)
assert.equal(typeof result.successRate, 'number');
assert.equal(result.sampleCount, 1); // Only the valid timestamp one is counted in 'recent'
// That one has summary: null, so totalRelays=0, healthyRelays=0.
// relayProbeCount = 0. So successRate = 1.
assert.equal(result.successRate, 1);
});
test('summarizeHistory - handles valid timestamp but missing summary', () => {
const now = Date.parse('2024-01-01T12:00:00Z');
const history = [
{ timestamp: '2024-01-01T11:55:00Z' } // Missing summary
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 1);
assert.equal(result.successRate, 1); // totalRelays defaults to 0 -> relayProbeCount=0 -> rate=1
assert.equal(result.allDownDurationMinutes, null); // healthyRelays defaults to 0 -> never healthy
});
test('summarizeHistory - healthy entry with invalid timestamp results in NaN duration', () => {
const now = Date.now();
const history = [
{ timestamp: 'invalid-date', summary: { healthyRelays: 1 } }
];
const result = summarizeHistory(history, { nowMs: now });
assert.equal(Number.isNaN(result.allDownDurationMinutes), true);
});
+139
View File
@@ -0,0 +1,139 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import { getRoster, _resetRosterCache } from '../src/roster.mjs';
describe('Roster (Unit Tests with Dependency Injection)', () => {
const CWD = process.cwd();
const USER_ROSTER_FILE = path.resolve(CWD, 'torch/roster.json');
const CWD_ROSTER_FILE = path.resolve(CWD, 'roster.json');
const mockFs = {
existsSync: mock.fn(),
readFileSync: mock.fn(),
};
const mockLoadTorchConfig = mock.fn();
const originalConsoleError = console.error;
beforeEach(() => {
_resetRosterCache();
mockFs.existsSync.mock.resetCalls();
mockFs.readFileSync.mock.resetCalls();
mockLoadTorchConfig.mock.resetCalls();
console.error = mock.fn();
// 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(() => '{}');
// Clear env vars
delete process.env.NOSTR_LOCK_DAILY_ROSTER;
delete process.env.NOSTR_LOCK_WEEKLY_ROSTER;
});
afterEach(() => {
mock.reset();
console.error = originalConsoleError;
});
it('returns fallback roster when no configuration or files exist', async () => {
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const roster = await getRoster('daily', deps);
assert.ok(Array.isArray(roster));
assert.ok(roster.length > 0);
assert.ok(roster.includes('audit-agent'));
});
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) => {
if (filepath === USER_ROSTER_FILE) {
return JSON.stringify({ daily: ['user-agent-daily'], weekly: ['user-agent-weekly'] });
}
throw new Error('ENOENT');
});
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const weekly = await getRoster('weekly', deps);
assert.deepStrictEqual(daily, ['user-agent-daily']);
assert.deepStrictEqual(weekly, ['user-agent-weekly']);
assert.strictEqual(mockFs.readFileSync.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) => {
if (filepath === CWD_ROSTER_FILE) {
return JSON.stringify({ daily: ['cwd-agent-daily'], weekly: ['cwd-agent-weekly'] });
}
throw new Error('ENOENT');
});
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['cwd-agent-daily']);
});
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 }');
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
// Should catch error and return fallback
const daily = await getRoster('daily', deps);
assert.ok(daily.includes('audit-agent'));
// Should have tried to read file
assert.strictEqual(mockFs.readFileSync.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: [] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.ok(daily.includes('audit-agent'));
});
it('prioritizes environment variables over file and config', async () => {
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'] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['env-agent-daily']);
});
it('prioritizes config over file', async () => {
mockLoadTorchConfig.mock.mockImplementation(async () => ({
nostrLock: { dailyRoster: ['config-agent-daily'] }
}));
// Setup file to exist
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['config-agent-daily']);
});
it('handles multiple items in env var roster', async () => {
process.env.NOSTR_LOCK_DAILY_ROSTER = 'agent1, agent2,agent3 ';
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
assert.deepStrictEqual(daily, ['agent1', 'agent2', 'agent3']);
});
});
+56 -2
View File
@@ -1,8 +1,30 @@
import { describe, it } from 'node:test';
import { describe, it, after } from 'node:test';
import assert from 'node:assert';
import { getIsoWeekStr } from '../src/utils.mjs';
import { getIsoWeekStr, todayDateStr, nowUnix } from '../src/utils.mjs';
describe('Date Utilities', () => {
describe('todayDateStr', () => {
it('returns current date in YYYY-MM-DD format', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
// 2023-10-25 12:00:00 UTC
const date = new Date('2023-10-25T12:00:00Z');
t.mock.timers.setTime(date.getTime());
assert.strictEqual(todayDateStr(), '2023-10-25');
});
});
describe('nowUnix', () => {
it('returns current unix timestamp', (t) => {
t.mock.timers.enable({ apis: ['Date'] });
const timeMs = 1700000000000;
t.mock.timers.setTime(timeMs);
// 1700000000000 / 1000 = 1700000000
assert.strictEqual(nowUnix(), 1700000000);
});
});
describe('getIsoWeekStr', () => {
it('returns correct ISO week for simple cases', () => {
assert.strictEqual(getIsoWeekStr('2023-01-02'), '2023-W01'); // Mon
@@ -34,3 +56,35 @@ describe('Date Utilities', () => {
});
});
});
describe('File Utilities', () => {
describe('ensureDir', () => {
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-utils-'));
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
it('creates directory if it does not exist', () => {
const targetDir = path.join(tempBase, 'new-dir');
assert.strictEqual(fs.existsSync(targetDir), false);
ensureDir(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
});
it('does nothing if directory already exists', () => {
const targetDir = path.join(tempBase, 'existing-dir');
fs.mkdirSync(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
ensureDir(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
});
it('creates nested directories recursively', () => {
const targetDir = path.join(tempBase, 'nested', 'deep', 'dir');
assert.strictEqual(fs.existsSync(targetDir), false);
ensureDir(targetDir);
assert.strictEqual(fs.existsSync(targetDir), true);
});
});
});