mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge branch 'main' into chore/remove-dead-code-cmd-check-6250725036284174249
This commit is contained in:
41 files changed
+1335
-340
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
|
||||
@@ -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,26 @@
|
||||
}
|
||||
],
|
||||
[
|
||||
"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
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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.
|
||||
@@ -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`);
|
||||
@@ -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');
|
||||
@@ -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);
|
||||
@@ -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.
|
||||
+19
-7
@@ -105,14 +105,24 @@ 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;
|
||||
// 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,
|
||||
}
|
||||
: {},
|
||||
};
|
||||
|
||||
// Remove sensitive dashboard auth
|
||||
if (publicConfig.dashboard.auth) {
|
||||
delete publicConfig.dashboard.auth;
|
||||
}
|
||||
delete safeConfig.configPath;
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(safeConfig, null, 2));
|
||||
res.end(JSON.stringify(publicConfig, null, 2));
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('Error parsing torch-config.json:', err);
|
||||
@@ -128,8 +138,10 @@ export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT, host = '127.0.
|
||||
}
|
||||
|
||||
// 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 = [
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-6
@@ -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 });
|
||||
|
||||
+17
-7
@@ -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;
|
||||
|
||||
|
||||
@@ -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');
|
||||
@@ -29,9 +30,7 @@ function loadMemoryStore() {
|
||||
function saveMemoryStore(store) {
|
||||
try {
|
||||
const dir = path.dirname(MEMORY_FILE_PATH);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
ensureDir(dir);
|
||||
const entries = [...store.entries()];
|
||||
fs.writeFileSync(MEMORY_FILE_PATH, JSON.stringify(entries, null, 2), 'utf8');
|
||||
} catch (err) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
+116
-106
@@ -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);
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
+36
-2
@@ -1,6 +1,8 @@
|
||||
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 fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getIsoWeekStr, ensureDir } from '../src/utils.mjs';
|
||||
|
||||
describe('Date Utilities', () => {
|
||||
describe('getIsoWeekStr', () => {
|
||||
@@ -34,3 +36,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user