Remove superseded Nostr implementation

This commit is contained in:
thePR0M3TH3AN committed 2026-09-27 16:59:32 -04:00
1 parent 87001555ea
commit 056fab8af4
802 files changed
+43 -104020

No files matched your search

@@ -1 +0,0 @@
Lock backend error (relay_publish_quorum_failure) after 3 attempt(s). Prompt not executed. Recommended auto-remediation: Likely backend/relay connectivity issue during lock acquisition. Retry window: 60 minute(s) (max deferrals: 3). Retry command: npm run scheduler:daily Run health check: npm run lock:health -- --cadence daily Review incident runbook: docs/agent-handoffs/learnings/2026-02-15-relay-health-preflight-job.md
@@ -1 +0,0 @@
Prompt not executed; unable to read prompt file at <PROMPT_PATH>: ENOENT: no such file or directory, open '<PROMPT_PATH>'
-34
View File
@@ -1,34 +0,0 @@
import fs from 'fs';
import path from 'path';
import { test } from 'node:test';
import assert from 'node:assert';
const DIST_DIR = path.resolve('dist');
test('Build artifacts verification', { skip: !fs.existsSync(DIST_DIR) && 'dist/ not found — run npm run build first' }, async (t) => {
await t.test('dist directory exists', () => {
assert.strictEqual(fs.existsSync(DIST_DIR), true, 'dist directory should exist');
});
await t.test('Critical files exist in dist', () => {
const criticalFiles = [
'index.html',
'dashboard/index.html',
'src/docs/TORCH.md',
'assets/logo.svg',
'src/constants.mjs',
'src/prompts/META_PROMPTS.md'
];
criticalFiles.forEach(file => {
const filePath = path.join(DIST_DIR, file);
assert.strictEqual(fs.existsSync(filePath), true, `File ${file} should exist in dist`);
});
});
await t.test('TORCH.md content is not empty', () => {
const torchMdPath = path.join(DIST_DIR, 'src/docs/TORCH.md');
const content = fs.readFileSync(torchMdPath, 'utf8');
assert.ok(content.length > 0, 'TORCH.md should not be empty');
});
});
-108
View File
@@ -1,108 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert';
import { parseArgs } from '../src/cli-parser.mjs';
import { DEFAULT_DASHBOARD_PORT } from '../src/constants.mjs';
test('parseArgs should parse basic command', () => {
const args = parseArgs(['check', '--cadence', 'daily']);
assert.strictEqual(args.command, 'check');
assert.strictEqual(args.cadence, 'daily');
});
test('parseArgs should parse agent and cadence', () => {
const args = parseArgs(['lock', '--agent', 'my-agent', '--cadence', 'weekly']);
assert.strictEqual(args.command, 'lock');
assert.strictEqual(args.agent, 'my-agent');
assert.strictEqual(args.cadence, 'weekly');
});
test('parseArgs should parse equals sign syntax', () => {
const args = parseArgs(['lock', '--agent=my-agent', '--cadence=weekly']);
assert.strictEqual(args.command, 'lock');
assert.strictEqual(args.agent, 'my-agent');
assert.strictEqual(args.cadence, 'weekly');
});
test('parseArgs should parse dry-run flag', () => {
const args = parseArgs(['lock', '--dry-run']);
assert.strictEqual(args.dryRun, true);
});
test('parseArgs should parse port with default', () => {
const args = parseArgs(['dashboard']);
assert.strictEqual(args.port, DEFAULT_DASHBOARD_PORT);
});
test('parseArgs should parse custom port', () => {
const args = parseArgs(['dashboard', '--port', '8080']);
assert.strictEqual(args.port, 8080);
});
test('parseArgs should parse ignore-logs flag', () => {
const args = parseArgs(['check', '--ignore-logs']);
assert.strictEqual(args.ignoreLogs, true);
});
test('parseArgs should parse memory flags', () => {
const args = parseArgs([
'list-memories',
'--id', '123',
'--type', 'retrieve',
'--tags', 'a,b',
'--pinned', 'true',
'--limit', '10',
'--offset', '5'
]);
assert.strictEqual(args.id, '123');
assert.strictEqual(args.type, 'retrieve');
assert.deepStrictEqual(args.tags, ['a', 'b']);
assert.strictEqual(args.pinned, true);
assert.strictEqual(args.limit, 10);
assert.strictEqual(args.offset, 5);
});
test('parseArgs should parse memory flags with equals', () => {
const args = parseArgs([
'list-memories',
'--id=123',
'--type=retrieve',
'--tags=a,b',
'--pinned=false',
'--limit=10',
'--offset=5'
]);
assert.strictEqual(args.id, '123');
assert.strictEqual(args.type, 'retrieve');
assert.deepStrictEqual(args.tags, ['a', 'b']);
assert.strictEqual(args.pinned, false);
assert.strictEqual(args.limit, 10);
assert.strictEqual(args.offset, 5);
});
test('parseArgs should parse memory subcommands and event contract flags', () => {
const args = parseArgs([
'memory',
'add',
'--agent', 'codex',
'--topic', 'memory',
'--message', 'hello',
'--run-id', 'run_1',
'--events-dir', 'memory/events',
'--schema-path', 'memory/schema/memory-event.schema.json',
'--created-at', '2026-03-01T15:20:11Z',
'--source', 'scheduler',
'--project', 'TORCH',
]);
assert.strictEqual(args.command, 'memory');
assert.strictEqual(args.subcommand, 'add');
assert.strictEqual(args.agent, 'codex');
assert.strictEqual(args.topic, 'memory');
assert.strictEqual(args.message, 'hello');
assert.strictEqual(args.runId, 'run_1');
assert.strictEqual(args.eventsDir, 'memory/events');
assert.strictEqual(args.schemaPath, 'memory/schema/memory-event.schema.json');
assert.strictEqual(args.createdAt, '2026-03-01T15:20:11Z');
assert.strictEqual(args.source, 'scheduler');
assert.strictEqual(args.project, 'TORCH');
});
-65
View File
@@ -1,65 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { cmdInit } from '../src/ops.mjs';
import { runDoctorChecks, cmdDoctor } from '../src/cmd-doctor.mjs';
const MOCK_CONFIG = {
installDir: 'torch',
namespace: 'doctor-test-namespace',
hashtag: 'doctor-test-hashtag',
relays: ['wss://relay.damus.io'],
};
test('runDoctorChecks reports failure when TORCH is not installed', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-missing-'));
const report = runDoctorChecks({ cwd: root, nodeVersion: '22.10.0' });
assert.equal(report.ok, false);
assert.ok(report.checks.some((check) => check.id === 'install-detection' && check.status === 'fail'));
});
test('runDoctorChecks passes core checks after init', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-healthy-'));
await cmdInit(false, root, MOCK_CONFIG);
// Simulate post-init dependency install for health check.
await fs.mkdir(path.join(root, 'torch', 'node_modules'), { recursive: true });
const report = runDoctorChecks({ cwd: root, nodeVersion: '22.10.0' });
assert.equal(report.ok, true);
assert.ok(report.checks.some((check) => check.id === 'memory-hook' && check.status === 'pass'));
assert.ok(report.checks.some((check) => check.id === 'install-files' && check.status === 'pass'));
});
test('runDoctorChecks fails when memory hook is missing from existing AGENTS.md', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-hook-missing-'));
await cmdInit(false, root, MOCK_CONFIG);
await fs.writeFile(path.join(root, 'AGENTS.md'), '# AGENTS\nNo memory section.\n', 'utf8');
const report = runDoctorChecks({ cwd: root, nodeVersion: '22.10.0' });
assert.equal(report.ok, false);
const memoryCheck = report.checks.find((check) => check.id === 'memory-hook');
assert.ok(memoryCheck);
assert.equal(memoryCheck.status, 'fail');
});
test('cmdDoctor emits JSON report when requested', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-json-'));
await cmdInit(false, root, MOCK_CONFIG);
const logs = [];
const report = cmdDoctor({
cwd: root,
json: true,
nodeVersion: '22.10.0',
log: (line) => logs.push(String(line)),
});
assert.ok(report.summary.passed >= 1);
assert.equal(logs.length, 1);
assert.doesNotThrow(() => JSON.parse(logs[0]));
});
-57
View File
@@ -1,57 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { cmdList } from '../src/cmd-list.mjs';
describe('cmdList', () => {
it('should list active locks and identify unknown agents', async () => {
const logs = [];
const mockLocks = [
{
agent: 'agent-1',
createdAt: 1000,
expiresAt: 4600,
eventId: 'event-1',
platform: 'linux',
},
{
agent: 'unknown-agent',
createdAt: 2000,
eventId: 'event-2',
platform: 'mac',
},
];
const deps = {
getRelays: async () => ['wss://relay.example.com'],
getNamespace: async () => 'test-namespace',
queryLocks: async () => mockLocks,
getRoster: async () => ['agent-1', 'agent-2'],
todayDateStr: () => '2023-10-27',
nowUnix: () => 3000, // agent-1 age: 2000s (~33m), unknown-agent age: 1000s (~17m)
log: (...args) => logs.push(args.join(' ')),
error: (...args) => logs.push(args.join(' ')),
};
await cmdList('daily', deps);
const output = logs.join('\n');
// Verify header
assert.match(output, /Active test-namespace daily locks/);
// Verify agents listed
assert.match(output, /agent-1/);
assert.match(output, /unknown-agent/);
// Verify unknown warning
assert.match(output, /Warning: lock events found with non-roster agent names: unknown-agent/);
// Verify summary stats
// Locked: 2 (agent-1 and unknown-agent both count as locked agents) / Roster size 2
assert.match(output, /Locked: 2\/2/);
// Verify available
// agent-2 is in roster and not in lockedAgents
assert.match(output, /Available: agent-2/);
});
});
-135
View File
@@ -1,135 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { cmdMemoryAdd, cmdMemoryBuild, cmdMemoryVerify } from '../src/cmd-memory.mjs';
import { ExitError } from '../src/errors.mjs';
const SCHEMA_PATH = 'memory/schema/memory-event.schema.json';
async function withTempProject(fn) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-memory-cmd-'));
const cwd = process.cwd();
process.chdir(root);
try {
await fs.mkdir('memory/events', { recursive: true });
await fs.mkdir('memory/schema', { recursive: true });
const schemaSource = path.resolve(cwd, SCHEMA_PATH);
await fs.copyFile(schemaSource, path.join(root, SCHEMA_PATH));
await fn(root);
} finally {
process.chdir(cwd);
}
}
async function writeEvent(root, event) {
const compact = event.created_at.replace(/[-:]/g, '').replace('.000', '');
const year = event.created_at.slice(0, 4);
const month = event.created_at.slice(5, 7);
const day = event.created_at.slice(8, 10);
const dir = path.join(root, 'memory', 'events', year, month, day);
await fs.mkdir(dir, { recursive: true });
const filePath = path.join(dir, `${compact}_${event.id}.json`);
await fs.writeFile(filePath, `${JSON.stringify(event, null, 2)}\n`, 'utf8');
}
test('memory add writes one append-only event file with schema-valid shape', async () => {
await withTempProject(async () => {
const createdAt = '2026-03-01T15:20:11Z';
await cmdMemoryAdd({
subcommand: 'add',
agent: 'codex',
topic: 'memory',
message: 'Append-only event created.',
createdAt,
runId: 'run_abc123',
tags: ['architecture', 'memory'],
source: 'test',
});
const eventDir = path.resolve('memory/events/2026/03/01');
const files = await fs.readdir(eventDir);
assert.strictEqual(files.length, 1);
const json = JSON.parse(await fs.readFile(path.join(eventDir, files[0]), 'utf8'));
assert.strictEqual(json.project, 'TORCH');
assert.strictEqual(json.created_at, createdAt);
assert.strictEqual(json.agent, 'codex');
assert.ok(json.id.includes('-7'));
});
});
test('memory build renders deterministic markdown sorted by created_at then id', async () => {
await withTempProject(async (root) => {
await writeEvent(root, {
id: '0195505d-0000-7000-8000-000000000002',
created_at: '2026-03-01T15:20:11Z',
project: 'TORCH',
agent: 'codex',
topic: 'memory',
content: 'second',
});
await writeEvent(root, {
id: '0195505d-0000-7000-8000-000000000001',
created_at: '2026-03-01T15:20:11Z',
project: 'TORCH',
agent: 'codex',
topic: 'memory',
content: 'first',
});
await cmdMemoryBuild({ subcommand: 'build' });
const rendered = await fs.readFile('memory_update.md', 'utf8');
const firstPos = rendered.indexOf('000000000001');
const secondPos = rendered.indexOf('000000000002');
assert.ok(firstPos > 0);
assert.ok(secondPos > firstPos);
assert.ok(rendered.startsWith('<!-- AUTO-GENERATED: DO NOT EDIT. Source: memory/events -->'));
});
});
test('memory verify fails when generated output is stale', async () => {
await withTempProject(async (root) => {
await writeEvent(root, {
id: '0195505d-0000-7000-8000-000000000001',
created_at: '2026-03-01T15:20:11Z',
project: 'TORCH',
agent: 'codex',
topic: 'memory',
content: 'expected output',
});
await fs.writeFile('memory_update.md', 'manually edited\n', 'utf8');
await assert.rejects(
async () => cmdMemoryVerify({ subcommand: 'verify' }),
(error) => error instanceof ExitError && error.code === 4 && /out of date|missing generated-file header/i.test(error.message),
);
});
});
test('memory verify fails on duplicate event ids', async () => {
await withTempProject(async (root) => {
const sharedId = '0195505d-0000-7000-8000-000000000001';
await writeEvent(root, {
id: sharedId,
created_at: '2026-03-01T15:20:11Z',
project: 'TORCH',
agent: 'codex',
topic: 'memory',
content: 'first',
});
await writeEvent(root, {
id: sharedId,
created_at: '2026-03-01T15:20:12Z',
project: 'TORCH',
agent: 'codex',
topic: 'memory',
content: 'second',
});
await assert.rejects(
async () => cmdMemoryVerify({ subcommand: 'verify' }),
(error) => error instanceof ExitError && error.code === 4 && /duplicate id/i.test(error.message),
);
});
});
-145
View File
@@ -1,145 +0,0 @@
import { test, after } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { cmdInit } from '../src/ops.mjs';
import { cmdRemove } from '../src/cmd-remove.mjs';
const MOCK_CONFIG = {
installDir: 'torch',
namespace: 'test-namespace',
relays: ['wss://relay.damus.io']
};
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-ops-remove-'));
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
test('cmdRemove reports nothing when no TORCH installation exists', async () => {
const projectRoot = path.join(tempBase, 'empty-project');
fs.mkdirSync(projectRoot, { recursive: true });
// Create a basic package.json so it looks like a real project
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({ name: 'my-app' }));
// Should complete without error — just reports nothing found
await cmdRemove(true, projectRoot);
// Project should be untouched
assert.ok(fs.existsSync(path.join(projectRoot, 'package.json')), 'package.json still exists');
});
test('cmdRemove with --force removes torch/ directory', async () => {
const projectRoot = path.join(tempBase, 'project-force');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Verify init created the expected artifacts
assert.ok(fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ exists before remove');
assert.ok(fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'torch-config.json exists before remove');
assert.ok(fs.existsSync(path.join(projectRoot, '.torch')), '.torch/ exists before remove');
await cmdRemove(true, projectRoot);
// All TORCH artifacts should be gone
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ removed');
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'torch-config.json removed');
assert.ok(!fs.existsSync(path.join(projectRoot, '.torch')), '.torch/ removed');
});
test('cmdRemove removes runtime artifacts (task-logs, .scheduler-memory)', async () => {
const projectRoot = path.join(tempBase, 'project-runtime');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Simulate runtime artifacts that the scheduler would create
fs.mkdirSync(path.join(projectRoot, 'task-logs', 'daily'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, 'task-logs', 'daily', 'run.md'), '# log');
fs.mkdirSync(path.join(projectRoot, '.scheduler-memory'), { recursive: true });
fs.writeFileSync(path.join(projectRoot, '.scheduler-memory', 'memory-store.json'), '{}');
await cmdRemove(true, projectRoot);
assert.ok(!fs.existsSync(path.join(projectRoot, 'task-logs')), 'task-logs/ removed');
assert.ok(!fs.existsSync(path.join(projectRoot, '.scheduler-memory')), '.scheduler-memory/ removed');
});
test('cmdRemove removes src/proposals/ and cleans empty src/', async () => {
const projectRoot = path.join(tempBase, 'project-proposals');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// src/proposals/ is created by cmdInit
assert.ok(fs.existsSync(path.join(projectRoot, 'src', 'proposals')), 'src/proposals/ exists before remove');
await cmdRemove(true, projectRoot);
assert.ok(!fs.existsSync(path.join(projectRoot, 'src', 'proposals')), 'src/proposals/ removed');
// src/ should also be removed since it's now empty
assert.ok(!fs.existsSync(path.join(projectRoot, 'src')), 'empty src/ removed');
});
test('cmdRemove cleans torch:* scripts from host package.json', async () => {
const projectRoot = path.join(tempBase, 'project-scripts');
fs.mkdirSync(projectRoot, { recursive: true });
// Create a host package.json with existing user scripts
fs.writeFileSync(path.join(projectRoot, 'package.json'), JSON.stringify({
name: 'my-app',
scripts: {
test: 'jest',
build: 'webpack'
}
}, null, 2));
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Verify init injected torch scripts
const pkgBefore = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
assert.ok('torch:dashboard' in pkgBefore.scripts, 'torch:dashboard injected');
assert.ok('torch:check' in pkgBefore.scripts, 'torch:check injected');
await cmdRemove(true, projectRoot);
// User scripts should remain, torch scripts should be gone
const pkgAfter = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
assert.strictEqual(pkgAfter.scripts.test, 'jest', 'user test script preserved');
assert.strictEqual(pkgAfter.scripts.build, 'webpack', 'user build script preserved');
assert.ok(!('torch:dashboard' in pkgAfter.scripts), 'torch:dashboard removed');
assert.ok(!('torch:check' in pkgAfter.scripts), 'torch:check removed');
assert.ok(!('torch:lock' in pkgAfter.scripts), 'torch:lock removed');
assert.ok(!('torch:health' in pkgAfter.scripts), 'torch:health removed');
assert.ok(!('torch:memory:list' in pkgAfter.scripts), 'torch:memory:list removed');
assert.ok(!('torch:memory:inspect' in pkgAfter.scripts), 'torch:memory:inspect removed');
});
test('cmdRemove cancels when user declines confirmation', async () => {
const projectRoot = path.join(tempBase, 'project-cancel');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Decline the confirmation
await cmdRemove(false, projectRoot, { confirm: false });
// Everything should still exist
assert.ok(fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ still exists after cancel');
assert.ok(fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'config still exists after cancel');
});
test('cmdRemove confirmed via mockAnswers removes artifacts', async () => {
const projectRoot = path.join(tempBase, 'project-confirm');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
// Confirm the removal
await cmdRemove(false, projectRoot, { confirm: true });
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch')), 'torch/ removed after confirm');
assert.ok(!fs.existsSync(path.join(projectRoot, 'torch-config.json')), 'config removed after confirm');
});
-150
View File
@@ -1,150 +0,0 @@
import test from 'node:test';
import assert from 'node:assert';
import http from 'node:http';
import { cmdDashboard } from '../src/dashboard.mjs';
import { _resetTorchConfigCache } from '../src/torch-config.mjs';
test('Dashboard Authentication', async (t) => {
let testPort = 0;
let server;
t.beforeEach(async () => {
// We don't start server here because some tests need env vars set BEFORE starting
});
t.afterEach((done) => {
if (server) server.close(done);
else done();
server = null;
});
await t.test('returns 401 when auth is required but missing', async () => {
process.env.TORCH_DASHBOARD_AUTH = 'admin:password';
_resetTorchConfigCache();
server = await cmdDashboard(0);
testPort = server.address().port;
try {
const res = await new Promise((resolve) => {
http.get(`http://127.0.0.1:${testPort}/dashboard/`, resolve);
});
assert.strictEqual(res.statusCode, 401);
assert.strictEqual(res.headers['www-authenticate'], 'Basic realm="TORCH Dashboard"');
} finally {
delete process.env.TORCH_DASHBOARD_AUTH;
_resetTorchConfigCache();
}
});
await t.test('returns 401 when malformed auth header is provided', async () => {
process.env.TORCH_DASHBOARD_AUTH = 'admin:password';
_resetTorchConfigCache();
server = await cmdDashboard(0);
testPort = server.address().port;
try {
const res = await new Promise((resolve) => {
http.get({
host: '127.0.0.1',
port: testPort,
path: '/dashboard/',
headers: { 'Authorization': 'Basic' }
}, resolve);
});
assert.strictEqual(res.statusCode, 401);
} finally {
delete process.env.TORCH_DASHBOARD_AUTH;
_resetTorchConfigCache();
}
});
await t.test('returns 200 when valid credentials are provided', async () => {
process.env.TORCH_DASHBOARD_AUTH = 'admin:password';
_resetTorchConfigCache();
server = await cmdDashboard(0);
testPort = server.address().port;
try {
const auth = Buffer.from('admin:password').toString('base64');
const res = await new Promise((resolve) => {
http.get({
host: '127.0.0.1',
port: testPort,
path: '/dashboard/',
headers: { 'Authorization': `Basic ${auth}` }
}, resolve);
});
// We expect 200 (if dashboard/index.html exists) or 404 (if not), but not 401
assert.notStrictEqual(res.statusCode, 401);
} finally {
delete process.env.TORCH_DASHBOARD_AUTH;
_resetTorchConfigCache();
}
});
await t.test('returns 401 when password has different byte length than stored credential', async () => {
// Regression test for timingSafeCompare: previously the comparison result
// was discarded and `false` was returned unconditionally for unequal-length
// inputs, making the timing-safe branch dead code. Verify that a prefix of
// the correct password (shorter) and an extended version (longer) are both
// rejected.
process.env.TORCH_DASHBOARD_AUTH = 'admin:secret';
_resetTorchConfigCache();
server = await cmdDashboard(0);
testPort = server.address().port;
try {
const makeRequest = (credentials) => {
const auth = Buffer.from(credentials).toString('base64');
return new Promise((resolve) => {
http.get({
host: '127.0.0.1',
port: testPort,
path: '/dashboard/',
headers: { 'Authorization': `Basic ${auth}` }
}, resolve);
});
};
// Shorter than correct password — different byte length
const resShorter = await makeRequest('admin:secre');
assert.strictEqual(resShorter.statusCode, 401, 'shorter password must be rejected');
// Longer than correct password — different byte length
const resLonger = await makeRequest('admin:secret_extra');
assert.strictEqual(resLonger.statusCode, 401, 'longer password must be rejected');
// Correct credentials still work
const resCorrect = await makeRequest('admin:secret');
assert.notStrictEqual(resCorrect.statusCode, 401, 'correct credentials must not be rejected');
} finally {
delete process.env.TORCH_DASHBOARD_AUTH;
_resetTorchConfigCache();
}
});
await t.test('returns 200 when auth is disabled', async () => {
delete process.env.TORCH_DASHBOARD_AUTH;
_resetTorchConfigCache();
server = await cmdDashboard(0);
testPort = server.address().port;
try {
const res = await new Promise((resolve) => {
http.get(`http://127.0.0.1:${testPort}/dashboard/`, resolve);
});
assert.notStrictEqual(res.statusCode, 401);
} finally {
_resetTorchConfigCache();
}
});
});
-114
View File
@@ -1,114 +0,0 @@
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;
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');
});
-125
View File
@@ -1,125 +0,0 @@
import test from 'node:test';
import assert from 'node:assert';
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { cmdDashboard } from '../src/dashboard.mjs';
import { _resetTorchConfigCache } from '../src/torch-config.mjs';
const CONFIG_FILE = 'torch-config-test.json';
// Helper to fetch JSON
function fetchJson(url, auth) {
return new Promise((resolve, reject) => {
const options = {
headers: {}
};
if (auth) {
options.headers['Authorization'] = `Basic ${Buffer.from(auth).toString('base64')}`;
}
http.get(url, options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => resolve({ statusCode: res.statusCode, body: data }));
}).on('error', reject);
});
}
test('Dashboard Configuration Security', async (t) => {
process.env.TORCH_CONFIG_PATH = CONFIG_FILE;
const configPath = path.resolve(process.cwd(), CONFIG_FILE);
let server;
// Ensure clean state
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
_resetTorchConfigCache();
t.afterEach((done) => {
if (server) {
server.close(done);
server = null;
} else {
done();
}
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
});
t.after(() => {
delete process.env.TORCH_CONFIG_PATH;
});
// Scenario 1: Unauthenticated access (no auth configured), sensitive fields in config
await t.test('sanitizes config when auth is disabled', async () => {
const sensitiveConfig = {
dashboard: {
relays: ["wss://public.relay.com"]
},
super_secret_key: "DO_NOT_LEAK",
nostrLock: {
relays: ["wss://lock.relay.com"]
}
};
fs.writeFileSync(configPath, JSON.stringify(sensitiveConfig, null, 2));
_resetTorchConfigCache();
server = await cmdDashboard(0);
const testPort = server.address().port;
try {
const { statusCode, body } = await fetchJson(`http://127.0.0.1:${testPort}/torch-config.json`);
assert.strictEqual(statusCode, 200);
const json = JSON.parse(body);
// Check for leaks
assert.strictEqual(json.super_secret_key, undefined, 'Unknown fields should be stripped');
// Check for valid fields
assert.deepStrictEqual(json.dashboard.relays, sensitiveConfig.dashboard.relays);
assert.deepStrictEqual(json.nostrLock.relays, sensitiveConfig.nostrLock.relays);
// Check that configPath is not leaked
assert.strictEqual(json.configPath, undefined, 'configPath should not be exposed');
} finally {
// server closed by afterEach
}
});
// Scenario 2: Authenticated access, auth credentials in config
await t.test('sanitizes auth credentials even when authenticated', async () => {
const authConfig = {
dashboard: {
auth: "admin:password123",
relays: ["wss://secure.relay.com"]
}
};
fs.writeFileSync(configPath, JSON.stringify(authConfig, null, 2));
_resetTorchConfigCache();
server = await cmdDashboard(0);
const testPort = server.address().port;
try {
// 1. Verify 401 without auth
const res401 = await fetchJson(`http://127.0.0.1:${testPort}/torch-config.json`);
assert.strictEqual(res401.statusCode, 401);
// 2. Verify 200 with auth
const { statusCode, body } = await fetchJson(`http://127.0.0.1:${testPort}/torch-config.json`, "admin:password123");
assert.strictEqual(statusCode, 200);
const json = JSON.parse(body);
// Check for leaks
assert.strictEqual(json.dashboard.auth, undefined, 'Auth credentials should be stripped');
// Check for valid fields
assert.deepStrictEqual(json.dashboard.relays, authConfig.dashboard.relays);
} finally {
// server closed by afterEach
}
});
});
@@ -1,95 +0,0 @@
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');
});
});
-109
View File
@@ -1,109 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import { cmdDashboard } from '../src/dashboard.mjs';
test('Dashboard File Access Security', async (t) => {
let testPort = 0;
let server;
t.beforeEach(async () => {
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) => {
http.get({
host: '127.0.0.1',
port: testPort,
path: path
}, (res) => {
// Consume data to avoid memory leaks
res.resume();
resolve(res);
});
});
}
await t.test('allows access to dashboard assets', async () => {
const res = await get('/dashboard/index.html');
assert.strictEqual(res.statusCode, 200);
});
await t.test('allows access to global assets', async () => {
// assuming favicon.svg exists
const res = await get('/assets/favicon.svg');
assert.strictEqual(res.statusCode, 200);
});
await t.test('allows access to landing page', async () => {
const res = await get('/landing/index.html');
assert.strictEqual(res.statusCode, 200);
});
await t.test('blocks access to package.json', async () => {
const res = await get('/package.json');
assert.strictEqual(res.statusCode, 403);
});
await t.test('blocks access to src/dashboard.mjs', async () => {
const res = await get('/src/dashboard.mjs');
assert.strictEqual(res.statusCode, 403);
});
await t.test('blocks access to random root files', async () => {
const res = await get('/README.md');
assert.strictEqual(res.statusCode, 403);
});
await t.test('allows access to torch-config.json via special handler', async () => {
const res = await get('/torch-config.json');
// It exists in root, so it should return 200
assert.strictEqual(res.statusCode, 200);
});
await t.test('blocks directory traversal attempts', async () => {
const res = await get('/dashboard/../../package.json');
// 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' 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');
});
});
-31
View File
@@ -1,31 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { ExitError } from '../src/errors.mjs';
describe('ExitError', () => {
it('should capture the code and message correctly', () => {
const code = 1;
const message = 'Test error message';
const error = new ExitError(code, message);
assert.strictEqual(error.code, code);
assert.strictEqual(error.message, message);
});
it('should be an instance of Error and ExitError', () => {
const error = new ExitError(1, 'message');
assert.ok(error instanceof Error);
assert.ok(error instanceof ExitError);
});
it('should have a stack trace', () => {
const error = new ExitError(1, 'message');
assert.ok(error.stack);
});
it('should have the correct name', () => {
const error = new ExitError(1, 'message');
assert.strictEqual(error.name, 'ExitError');
});
});
-6
View File
@@ -1,6 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('fail fixture stays red', () => {
assert.equal(1, 2);
});
-6
View File
@@ -1,6 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
test('pass fixture stays green', () => {
assert.equal(2 + 2, 4);
});
-48
View File
@@ -1,48 +0,0 @@
export const FIXED_NOW = 1_700_000_000_000;
export const SYNTHETIC_EVENTS = [
{
agent_id: 'agent-alpha',
content: 'Incident INC-101 resolved after restarting api service',
timestamp: FIXED_NOW - 5_000,
tags: ['incident', 'ops'],
metadata: { session_id: 'session-1' },
},
{
agent_id: 'agent-alpha',
content: 'Customer email [email protected] confirmed recovery',
timestamp: FIXED_NOW - 3_000,
tags: ['incident', 'customer'],
metadata: { session_id: 'session-1' },
},
{
agent_id: 'agent-alpha',
content: 'Follow-up task created for postmortem',
timestamp: FIXED_NOW - 2_000,
tags: ['ops', 'task'],
metadata: { session_id: 'session-1' },
},
];
export const EXPECTED_SUMMARY = 'INC-101 resolved, customer contact [redacted:email], postmortem task queued';
export const EXPECTED_TAGS = ['incident', 'ops', 'customer', 'task'];
export const BASE_MEMORY_RECORD = {
schema_version: 1,
id: 'memory-1',
agent_id: 'agent-alpha',
session_id: 'session-1',
type: 'event',
content: 'Incident INC-101 resolved after restarting api service',
summary: 'Incident resolved',
tags: ['incident', 'ops'],
importance: 0.7,
embedding_id: null,
created_at: FIXED_NOW - 5_000,
last_seen: FIXED_NOW - 2_000,
source: 'ingest',
ttl_days: 30,
merged_into: null,
pinned: false,
};
-69
View File
@@ -1,69 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createProposal, validateProposal } from '../src/services/governance/index.js';
import path from 'node:path';
import fs from 'node:fs/promises';
test('createProposal should throw error for disallowed target', async () => {
const agent = 'test-agent';
const target = 'secrets/passwords.txt';
const newContent = 'new content';
await assert.rejects(
createProposal({ agent, target, newContent, reason: 'test' }),
{
message: /Target secrets\/passwords.txt is not in an allowed directory/
}
);
});
test('validateProposal should return invalid for disallowed target', async () => {
const id = 'test-disallowed-proposal';
const dir = path.resolve(process.cwd(), 'src/proposals', id);
await fs.mkdir(dir, { recursive: true });
const meta = {
id,
author: 'test-agent',
target: 'secrets/passwords.txt',
reason: 'test',
timestamp: new Date().toISOString(),
status: 'pending'
};
await fs.writeFile(path.join(dir, 'meta.json'), JSON.stringify(meta));
await fs.writeFile(path.join(dir, 'new.md'), 'Shared contract (required):\nRequired startup + artifacts + memory + issue capture');
try {
const result = await validateProposal(id);
assert.strictEqual(result.valid, false);
assert.strictEqual(result.reason, 'Target not in allowed directories.');
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
test('validateProposal should return valid for allowed target', async () => {
const id = 'test-allowed-proposal';
const dir = path.resolve(process.cwd(), 'src/proposals', id);
await fs.mkdir(dir, { recursive: true });
const meta = {
id,
author: 'test-agent',
target: 'src/prompts/daily/test.md',
reason: 'test',
timestamp: new Date().toISOString(),
status: 'pending'
};
await fs.writeFile(path.join(dir, 'meta.json'), JSON.stringify(meta));
await fs.writeFile(path.join(dir, 'new.md'), 'Shared contract (required):\nRequired startup + artifacts + memory + issue capture');
try {
const result = await validateProposal(id);
assert.strictEqual(result.valid, true);
} finally {
await fs.rm(dir, { recursive: true, force: true });
}
});
-374
View File
@@ -1,374 +0,0 @@
/**
* Governance: Prompt Versioning and Rollback Tests
*
* Scenarios tested:
* SCN-proposal-create : createProposal writes correct on-disk structure
* SCN-proposal-list : listProposals returns all proposals; status filter works
* SCN-proposal-reject : rejectProposal marks status and records reason
* SCN-archive-naming : applyProposal archives with <base>_<timestamp>_<hash>.md + sidecar
* SCN-version-list : listPromptVersions returns versions newest-first with metadata
* SCN-rollback-latest : rollbackPrompt --strategy latest restores most-recent archive
* SCN-rollback-hash : rollbackPrompt --strategy <fragment> restores the matching archive
* SCN-rollback-no-archive: rollbackPrompt falls through to git when no archive exists
*/
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import fsp from 'node:fs/promises';
import path from 'node:path';
import { createHash } from 'node:crypto';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const CWD = process.cwd();
const PROPOSALS_DIR = path.join(CWD, 'src/proposals');
const HISTORY_DIR = path.join(CWD, '.torch/prompt-history');
// A real daily prompt that is guaranteed to exist (passes allowlist)
const TEST_TARGET = 'src/prompts/daily/audit-agent.md';
const ABS_TARGET = path.join(CWD, TEST_TARGET);
let originalPromptContent = null;
const createdProposalIds = [];
// Read governance functions fresh each time (they're already cached by Node's ESM cache,
// but that's fine — we need stable CWD-based paths for integration tests).
const {
createProposal,
listProposals,
getProposal,
rejectProposal,
applyProposal,
listPromptVersions,
rollbackPrompt,
} = await import('../src/services/governance/index.js');
// ---------------------------------------------------------------------------
// Setup / Teardown
// ---------------------------------------------------------------------------
before(async () => {
// Save original prompt content so we can restore it after applyProposal tests
originalPromptContent = await fsp.readFile(ABS_TARGET, 'utf8');
});
after(async () => {
// Restore original prompt content if a test changed it
if (originalPromptContent !== null) {
try {
const current = await fsp.readFile(ABS_TARGET, 'utf8');
if (current !== originalPromptContent) {
await fsp.writeFile(ABS_TARGET, originalPromptContent);
}
} catch {
// best-effort
}
}
// Remove test proposals written during tests
for (const id of createdProposalIds) {
const dir = path.join(PROPOSALS_DIR, id);
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ }
}
// Remove test archives written during tests (keep real ones, only delete test-* prefixed)
const archiveDir = path.join(HISTORY_DIR, path.dirname(TEST_TARGET));
try {
const files = await fsp.readdir(archiveDir);
for (const f of files) {
if (f.startsWith('__test__')) {
fs.rmSync(path.join(archiveDir, f), { force: true });
}
}
} catch { /* ignore */ }
});
// ---------------------------------------------------------------------------
// SCN-proposal-create
// ---------------------------------------------------------------------------
test('SCN-proposal-create: createProposal writes meta.json, new.md, change.diff', async () => {
// Given: a valid target path and new content
const newContent = originalPromptContent + '\n<!-- test marker -->';
const agent = '__test-agent__';
// When: a proposal is created
const { id } = await createProposal({
agent,
target: TEST_TARGET,
newContent,
reason: 'Test proposal for SCN-proposal-create',
});
createdProposalIds.push(id);
// Then: the proposal directory contains the required files
const proposalDir = path.join(PROPOSALS_DIR, id);
assert.ok(fs.existsSync(path.join(proposalDir, 'meta.json')), 'meta.json exists');
assert.ok(fs.existsSync(path.join(proposalDir, 'new.md')), 'new.md exists');
assert.ok(fs.existsSync(path.join(proposalDir, 'change.diff')), 'change.diff exists');
const meta = JSON.parse(fs.readFileSync(path.join(proposalDir, 'meta.json'), 'utf8'));
assert.equal(meta.author, agent, 'author recorded');
assert.equal(meta.target, TEST_TARGET, 'target recorded');
assert.equal(meta.status, 'pending', 'initial status is pending');
assert.equal(meta.reason, 'Test proposal for SCN-proposal-create', 'reason recorded');
const savedContent = fs.readFileSync(path.join(proposalDir, 'new.md'), 'utf8');
assert.equal(savedContent, newContent, 'new.md contains proposed content');
});
// ---------------------------------------------------------------------------
// SCN-proposal-list
// ---------------------------------------------------------------------------
test('SCN-proposal-list: listProposals returns created proposal; status filter works', async () => {
// Given: at least one pending proposal exists (created in previous test)
// When: listing all proposals
const all = await listProposals();
assert.ok(Array.isArray(all), 'listProposals returns an array');
assert.ok(all.length >= 1, 'at least one proposal returned');
const testProposals = all.filter(p => p.author === '__test-agent__');
assert.ok(testProposals.length >= 1, 'test proposal is in the list');
assert.equal(testProposals[0].status, 'pending', 'status is pending');
// When: filtering by status that returns nothing
const rejected = all.filter(p => p.status === 'rejected' && p.author === '__test-agent__');
assert.equal(rejected.length, 0, 'no rejected proposals from our test agent yet');
});
// ---------------------------------------------------------------------------
// SCN-proposal-reject
// ---------------------------------------------------------------------------
test('SCN-proposal-reject: rejectProposal marks proposal with rejected status and reason', async () => {
// Given: an existing pending proposal
const newContent = originalPromptContent + '\n<!-- reject-test -->';
const { id } = await createProposal({
agent: '__test-agent__',
target: TEST_TARGET,
newContent,
reason: 'Will be rejected',
});
createdProposalIds.push(id);
// When: the proposal is rejected
const rejectionReason = 'Does not meet EXIT CRITERIA standard';
await rejectProposal(id, rejectionReason);
// Then: the meta.json reflects rejected status
const proposal = await getProposal(id);
assert.equal(proposal.meta.status, 'rejected', 'status is rejected');
assert.equal(proposal.meta.rejectionReason, rejectionReason, 'rejection reason recorded');
assert.ok(proposal.meta.rejectedAt, 'rejectedAt timestamp set');
});
// ---------------------------------------------------------------------------
// SCN-archive-naming: applyProposal creates <base>_<ts>_<hash>.md + .meta.json sidecar
// ---------------------------------------------------------------------------
test('SCN-archive-naming: applyProposal archives with timestamp_hash filename and sidecar', async () => {
// Given: a valid pending proposal for a real prompt file
const newContent = originalPromptContent.replace(
/^(# )/m,
'# [VERSIONING TEST MARKER] '
);
// Ensure newContent still passes contract validation
const hasContract = /Shared contract \(required\):/i.test(newContent) &&
/Required startup \+ artifacts \+ memory \+ issue capture/i.test(newContent);
if (!hasContract) {
// Skip: the prompt doesn't have required headers; not a governance test failure
return;
}
const { id } = await createProposal({
agent: '__test-agent__',
target: TEST_TARGET,
newContent,
reason: 'Archive naming test',
});
createdProposalIds.push(id);
const expectedHash = createHash('sha256').update(originalPromptContent).digest('hex');
// When: the proposal is applied
await applyProposal(id);
// Then: find the archive whose sidecar references this specific proposal ID
// (There may be archives from prior test runs with the same hash; we must find ours.)
const archiveDir = path.join(HISTORY_DIR, path.dirname(TEST_TARGET));
const baseName = path.basename(TEST_TARGET, '.md');
const files = fs.readdirSync(archiveDir);
const sidecarFiles = files.filter(f => f.startsWith(baseName + '_') && f.endsWith('.meta.json'));
let archiveFile = null;
let sidecar = null;
for (const sf of sidecarFiles) {
try {
const s = JSON.parse(fs.readFileSync(path.join(archiveDir, sf), 'utf8'));
if (s.proposalId === id) {
sidecar = s;
archiveFile = sf.replace('.meta.json', '.md');
break;
}
} catch { /* skip unreadable sidecars */ }
}
assert.ok(archiveFile, `archive sidecar found for proposal ${id}`);
assert.ok(fs.existsSync(path.join(archiveDir, archiveFile)), 'archive .md file exists');
// Filename must contain a timestamp segment (not just hash)
const withoutPrefix = archiveFile.slice(baseName.length + 1, -3); // strip base_ and .md
const parts = withoutPrefix.split('_');
assert.ok(parts.length >= 2, 'filename has timestamp + hash parts');
assert.equal(sidecar.proposalId, id, 'sidecar references the proposal ID');
assert.equal(sidecar.author, '__test-agent__', 'sidecar records author');
assert.equal(sidecar.hash, expectedHash, 'sidecar records SHA-256 hash');
assert.ok(sidecar.archivedAt, 'sidecar records archivedAt timestamp');
// Applied content is the new content
const applied = fs.readFileSync(ABS_TARGET, 'utf8');
assert.equal(applied, newContent, 'new content was written to target file');
});
// ---------------------------------------------------------------------------
// SCN-version-list: listPromptVersions returns metadata sorted newest first
// ---------------------------------------------------------------------------
test('SCN-version-list: listPromptVersions returns versions newest-first with metadata', async () => {
// Given: two manually created archive files with different timestamps
const archiveDir = path.join(HISTORY_DIR, path.dirname(TEST_TARGET));
await fsp.mkdir(archiveDir, { recursive: true });
const baseName = '__test__versioning-probe';
const ts1 = '2026-01-10T00-00-00-000Z';
const ts2 = '2026-01-20T00-00-00-000Z';
const hash1 = 'aaa000';
const hash2 = 'bbb111';
const file1 = `${baseName}_${ts1}_${hash1}.md`;
const file2 = `${baseName}_${ts2}_${hash2}.md`;
fs.writeFileSync(path.join(archiveDir, file1), 'old version');
fs.writeFileSync(path.join(archiveDir, file2), 'newer version');
fs.writeFileSync(
path.join(archiveDir, file2.replace('.md', '.meta.json')),
JSON.stringify({ proposalId: 'test-prop', author: 'test-agent', reason: 'v2', hash: hash2, archivedAt: '2026-01-20T00:00:00.000Z' })
);
// When: listing versions for the probe target
const probeTarget = `src/prompts/daily/${baseName}.md`;
const versions = await listPromptVersions(probeTarget);
// Then: sorted newest first (ts2 > ts1)
assert.ok(versions.length >= 2, 'at least 2 versions returned');
assert.equal(versions[0].filename, file2, 'newest version is first');
assert.equal(versions[1].filename, file1, 'older version is second');
// Metadata from sidecar is surfaced
assert.equal(versions[0].author, 'test-agent', 'author from sidecar');
assert.equal(versions[0].reason, 'v2', 'reason from sidecar');
assert.equal(versions[0].hash, hash2, 'hash from sidecar');
// Version without sidecar still has hash extracted from filename
assert.ok(versions[1].hash, 'hash extracted from filename for legacy entry');
});
// ---------------------------------------------------------------------------
// SCN-rollback-latest
// NOTE: Uses a dedicated probe target (not an actual prompt) so there is no
// interference from archives created by other tests or the scheduler.
// ---------------------------------------------------------------------------
// Probe target: a path inside the allowed subpath structure but with a name
// that will not conflict with real prompts. rollbackPrompt only needs to
// write to this path; the file is cleaned up in after().
const ROLLBACK_PROBE_TARGET = 'src/prompts/daily/__rollback-probe__.md';
const ABS_ROLLBACK_PROBE = path.join(CWD, ROLLBACK_PROBE_TARGET);
test('SCN-rollback-latest: rollbackPrompt restores the most recently archived content', async () => {
// Given: two manually created archive files for the probe target; newest is distinctive
const archiveDir = path.join(HISTORY_DIR, path.dirname(ROLLBACK_PROBE_TARGET));
await fsp.mkdir(archiveDir, { recursive: true });
const baseName = path.basename(ROLLBACK_PROBE_TARGET, '.md');
const olderTs = '2025-06-01T00-00-00-000Z';
const newerTs = '2025-12-01T00-00-00-000Z';
const olderHash = '__older-hash-scn-latest__';
const newerHash = '__newer-hash-scn-latest__';
const olderContent = '<!-- SCN-rollback-latest: older -->';
const newerContent = '<!-- SCN-rollback-latest: newer -->';
const olderFile = `${baseName}_${olderTs}_${olderHash}.md`;
const newerFile = `${baseName}_${newerTs}_${newerHash}.md`;
await fsp.writeFile(path.join(archiveDir, olderFile), olderContent);
await fsp.writeFile(path.join(archiveDir, olderFile.replace('.md', '.meta.json')),
JSON.stringify({ archivedAt: '2025-06-01T00:00:00.000Z', hash: olderHash }));
await fsp.writeFile(path.join(archiveDir, newerFile), newerContent);
await fsp.writeFile(path.join(archiveDir, newerFile.replace('.md', '.meta.json')),
JSON.stringify({ archivedAt: '2025-12-01T00:00:00.000Z', hash: newerHash }));
try {
// When: rolling back with 'latest' strategy
const result = await rollbackPrompt(ROLLBACK_PROBE_TARGET, 'latest');
// Then: target file contains the newer archive's content
assert.ok(result.success, 'rollback reported success');
assert.equal(result.source, 'archive', 'restored from archive, not git');
assert.equal(result.restored, newerFile, 'restored from newer archive');
const restoredContent = await fsp.readFile(ABS_ROLLBACK_PROBE, 'utf8');
assert.equal(restoredContent, newerContent, 'target file contains newer archived content');
} finally {
// Cleanup probe artifacts
for (const f of [olderFile, olderFile.replace('.md', '.meta.json'),
newerFile, newerFile.replace('.md', '.meta.json')]) {
try { await fsp.unlink(path.join(archiveDir, f)); } catch { /* ignore */ }
}
try { await fsp.unlink(ABS_ROLLBACK_PROBE); } catch { /* ignore */ }
}
});
// ---------------------------------------------------------------------------
// SCN-rollback-hash
// ---------------------------------------------------------------------------
test('SCN-rollback-hash: rollbackPrompt --strategy <fragment> restores matching archive', async () => {
// Given: one archive with a distinctive hash fragment for the probe target
const archiveDir = path.join(HISTORY_DIR, path.dirname(ROLLBACK_PROBE_TARGET));
await fsp.mkdir(archiveDir, { recursive: true });
const baseName = path.basename(ROLLBACK_PROBE_TARGET, '.md');
const specificHash = 'cafecafe1234scn';
const specificContent = '<!-- SCN-rollback-hash: specific content -->';
const specificTs = '2025-09-15T00-00-00-000Z';
const specificFile = `${baseName}_${specificTs}_${specificHash}.md`;
await fsp.writeFile(path.join(archiveDir, specificFile), specificContent);
await fsp.writeFile(path.join(archiveDir, specificFile.replace('.md', '.meta.json')),
JSON.stringify({ archivedAt: '2025-09-15T00:00:00.000Z', hash: specificHash }));
try {
// When: rolling back with the specific hash fragment
const result = await rollbackPrompt(ROLLBACK_PROBE_TARGET, 'cafecafe1234scn');
// Then: the correct archive was selected
assert.ok(result.success, 'rollback succeeded');
assert.equal(result.source, 'archive', 'restored from archive');
assert.equal(result.restored, specificFile, 'correct archive file selected');
const restoredContent = await fsp.readFile(ABS_ROLLBACK_PROBE, 'utf8');
assert.equal(restoredContent, specificContent, 'content matches the specific archive');
} finally {
for (const f of [specificFile, specificFile.replace('.md', '.meta.json')]) {
try { await fsp.unlink(path.join(archiveDir, f)); } catch { /* ignore */ }
}
try { await fsp.unlink(ABS_ROLLBACK_PROBE); } catch { /* ignore */ }
}
});
-109
View File
@@ -1,109 +0,0 @@
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 });
}
});
-415
View File
@@ -1,415 +0,0 @@
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import { cmdCheck, cmdLock, cmdList, cmdComplete } from '../src/lib.mjs';
describe('src/lib.mjs', () => {
// Common mocks
const mockRelays = ['wss://relay.test'];
const mockNamespace = 'test-ns';
const mockTtl = 3600;
const mockRoster = ['agent1', 'agent2', 'agent3'];
const mockDateStr = new Date().toISOString().slice(0, 10);
let logs = [];
let errors = [];
const mockLog = (...args) => logs.push(args.join(' '));
const mockError = (...args) => errors.push(args.join(' '));
const baseDeps = {
getRelays: () => mockRelays,
getNamespace: () => mockNamespace,
getTtl: () => mockTtl,
getRoster: () => mockRoster,
loadTorchConfig: () => ({ scheduler: { paused: { daily: [], weekly: [] } } }),
getDateStr: () => mockDateStr,
log: mockLog,
error: mockError,
readdir: async () => [],
};
beforeEach(() => {
logs = [];
errors = [];
});
describe('cmdCheck', () => {
it('returns correct structure with empty locks', async () => {
const deps = {
...baseDeps,
queryLocks: async () => [],
};
const result = await cmdCheck('daily', deps);
assert.strictEqual(result.namespace, mockNamespace);
assert.strictEqual(result.cadence, 'daily');
assert.strictEqual(result.date, mockDateStr);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.available, mockRoster.sort());
assert.strictEqual(result.lockCount, 0);
});
it('identifies locked agents', async () => {
const locks = [
{ agent: 'agent1', eventId: 'ev1', createdAtIso: 'iso1', platform: 'test' }
];
const deps = {
...baseDeps,
queryLocks: async () => locks,
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.locked, ['agent1']);
assert.deepStrictEqual(result.available, ['agent2', 'agent3']);
assert.strictEqual(result.lockCount, 1);
});
it('excludes paused agents', async () => {
const deps = {
...baseDeps,
queryLocks: async () => [],
loadTorchConfig: () => ({ scheduler: { paused: { daily: ['agent2'], weekly: [] } } }),
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.paused, ['agent2']);
assert.deepStrictEqual(result.excluded, ['agent2']);
assert.deepStrictEqual(result.available, ['agent1', 'agent3']);
});
it('identifies unknown locked agents', async () => {
const locks = [
{ agent: 'unknown-agent', eventId: 'ev1' }
];
const deps = {
...baseDeps,
queryLocks: async () => locks,
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.unknownLockedAgents, ['unknown-agent']);
assert.deepStrictEqual(result.locked, ['unknown-agent']);
// unknown agents are not in roster, so available should be full roster (since no roster agent is locked)
assert.deepStrictEqual(result.available, mockRoster.sort());
});
});
describe('cmdLock', () => {
const mockGenerateSecretKey = () => new Uint8Array(32);
const mockGetPublicKey = () => 'mock-pubkey';
const mockFinalizeEvent = (t) => ({ ...t, id: 'mock-id', sig: 'mock-sig' });
it('successfully locks an available agent', async () => {
let published = false;
const deps = {
...baseDeps,
queryLocks: async () => [], // No existing locks, and race check returns empty
publishLock: async () => { published = true; },
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: mockFinalizeEvent,
raceCheckDelayMs: 1, // Fast race check
};
const result = await cmdLock('agent1', 'daily', false, deps);
assert.strictEqual(result.status, 'ok');
assert.strictEqual(result.eventId, 'mock-id');
assert.strictEqual(published, true);
assert.match(logs.join('\n'), /LOCK_STATUS=ok/);
});
it('fails if agent is not in roster', async () => {
const deps = { ...baseDeps };
await assert.rejects(
async () => await cmdLock('unknown', 'daily', false, deps),
(err) => err.code === 1 && err.message === 'Agent not in roster'
);
});
it('fails if agent is already locked', async () => {
const existingLocks = [
{ agent: 'agent1', eventId: 'existing', createdAt: 100, createdAtIso: 'time' }
];
const deps = {
...baseDeps,
queryLocks: async () => existingLocks,
};
await assert.rejects(
async () => await cmdLock('agent1', 'daily', false, deps),
(err) => err.code === 3 && err.message === 'Lock denied'
);
assert.match(logs.join('\n'), /LOCK_STATUS=denied/);
});
it('fails if agent is already completed', async () => {
const existingLocks = [
{ agent: 'agent1', eventId: 'done-id', status: 'completed' }
];
const deps = {
...baseDeps,
queryLocks: async () => existingLocks,
};
await assert.rejects(
async () => await cmdLock('agent1', 'daily', false, deps),
(err) => err.code === 3 && err.message === 'Task already completed'
);
assert.match(logs.join('\n'), /LOCK_STATUS=denied/);
assert.match(logs.join('\n'), /LOCK_REASON=already_completed/);
});
it('fails if race check is lost', async () => {
let callCount = 0;
const deps = {
...baseDeps,
queryLocks: async () => {
callCount++;
if (callCount === 1) return []; // First check: no locks
// Second check (race): found an earlier lock
return [{
agent: 'agent1',
eventId: 'winner',
createdAt: 100, // old timestamp
createdAtIso: 'old-time'
}, {
agent: 'agent1',
eventId: 'mock-id',
createdAt: 200, // our timestamp (mocked event)
}];
},
publishLock: async () => {},
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: mockFinalizeEvent,
raceCheckDelayMs: 1,
};
await assert.rejects(
async () => await cmdLock('agent1', 'daily', false, deps),
(err) => err.code === 3 && err.message === 'Race check lost'
);
assert.match(logs.join('\n'), /LOCK_STATUS=race_lost/);
});
it('handles race condition with identical timestamps using eventId tie-breaker (we win)', async () => {
let callCount = 0;
const deps = {
...baseDeps,
queryLocks: async () => {
callCount++;
if (callCount === 1) return [];
return [{
agent: 'agent1',
eventId: 'aaaaa', // competitor (higher ID)
createdAt: 100,
createdAtIso: 'time'
}, {
agent: 'agent1',
eventId: 'mock-id', // our ID (we need to ensure mockFinalizeEvent returns this or we control comparison)
// The test mockFinalizeEvent returns 'mock-id'.
// 'mock-id' > 'aaaaa'. So we would lose if competitor is 'aaaaa'.
// To win, competitor must be 'zzzzz'.
// Let's re-check the logic.
// sort((a, b) => ... || a.id.localeCompare(b.id))
// If we are 'mock-id', and competitor is 'zzzzz'.
// 'mock-id'.localeCompare('zzzzz') is negative. So we come first.
// racingLocks[0] is us. We win.
createdAt: 100,
}];
},
publishLock: async () => {},
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: (t) => ({ ...t, id: 'mock-id' }),
raceCheckDelayMs: 1,
};
// Competitor is 'aaaaa'. We are 'mock-id'. 'aaaaa' comes first. We lose.
// Wait, I want a test where we WIN.
// So competitor should be 'zzzzz'.
deps.queryLocks = async () => {
callCount++;
if (callCount === 1) return [];
return [{
agent: 'agent1',
eventId: 'zzzzz', // competitor (higher ID)
createdAt: 100,
createdAtIso: 'time'
}, {
agent: 'agent1',
eventId: 'mock-id', // our ID
createdAt: 100,
}];
};
const result = await cmdLock('agent1', 'daily', false, deps);
assert.strictEqual(result.status, 'ok');
assert.match(logs.join('\n'), /LOCK_STATUS=ok/);
});
it('handles race condition with identical timestamps using eventId tie-breaker (we lose)', async () => {
let callCount = 0;
const deps = {
...baseDeps,
queryLocks: async () => {
callCount++;
if (callCount === 1) return [];
return [{
agent: 'agent1',
eventId: 'aaaaa', // competitor (lower ID)
createdAt: 100,
createdAtIso: 'time'
}, {
agent: 'agent1',
eventId: 'mock-id', // our ID ('m' > 'a')
createdAt: 100,
}];
},
publishLock: async () => {},
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: (t) => ({ ...t, id: 'mock-id' }),
raceCheckDelayMs: 1,
};
await assert.rejects(
async () => await cmdLock('agent1', 'daily', false, deps),
(err) => err.code === 3 && err.message === 'Race check lost'
);
assert.match(logs.join('\n'), /LOCK_STATUS=race_lost/);
});
it('dry run does not publish', async () => {
let published = false;
const deps = {
...baseDeps,
queryLocks: async () => [],
publishLock: async () => { published = true; },
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: mockFinalizeEvent,
};
const result = await cmdLock('agent1', 'daily', true, deps);
assert.strictEqual(result.status, 'ok');
assert.strictEqual(published, false);
assert.match(errors.join('\n'), /\[DRY RUN\]/);
});
});
describe('cmdList', () => {
it('lists active locks', async () => {
const locks = [
{ agent: 'agent1', eventId: 'ev1', createdAt: Date.now()/1000 - 60, createdAtIso: 'iso1', platform: 'test' }
];
const deps = {
...baseDeps,
queryLocks: async () => locks,
};
await cmdList('daily', deps);
const output = logs.join('\n');
assert.match(output, /Active test-ns daily locks/);
assert.match(output, /agent1/);
assert.match(output, /Locked: 1\/3/);
});
it('warns about unknown agents', async () => {
const locks = [
{ agent: 'unknown', eventId: 'ev1', createdAt: Date.now()/1000 }
];
const deps = {
...baseDeps,
queryLocks: async () => locks,
};
await cmdList('daily', deps);
const output = logs.join('\n');
assert.match(output, /Warning: lock events found with non-roster agent names: unknown/);
});
it('handles no locks', async () => {
const deps = {
...baseDeps,
queryLocks: async () => [],
};
await cmdList('daily', deps);
const output = logs.join('\n');
assert.match(output, /\(no active locks\)/);
});
});
describe('cmdComplete', () => {
const mockGenerateSecretKey = () => new Uint8Array(32);
const mockGetPublicKey = () => 'mock-pubkey';
const mockFinalizeEvent = (t) => ({ ...t, id: 'mock-id-complete' });
it('successfully completes an active lock', async () => {
let publishedEvent = null;
const locks = [
{ agent: 'agent1', eventId: 'lock-id', createdAt: 100, createdAtIso: 'iso1', status: 'started' }
];
const deps = {
...baseDeps,
queryLocks: async () => locks,
publishLock: async (relays, evt) => { publishedEvent = evt; },
generateSecretKey: mockGenerateSecretKey,
getPublicKey: mockGetPublicKey,
finalizeEvent: mockFinalizeEvent,
};
const result = await cmdComplete('agent1', 'daily', false, deps);
assert.strictEqual(result.status, 'completed');
assert.strictEqual(result.eventId, 'mock-id-complete');
assert.ok(publishedEvent);
// Verify no expiration tag
const expTag = publishedEvent.tags.find(t => t[0] === 'expiration');
assert.strictEqual(expTag, undefined);
// Verify content
const content = JSON.parse(publishedEvent.content);
assert.strictEqual(content.status, 'completed');
assert.strictEqual(content.startedAt, 'iso1');
assert.match(logs.join('\n'), /LOCK_STATUS=completed/);
});
it('fails if no active lock found', async () => {
const deps = {
...baseDeps,
queryLocks: async () => [], // No locks
};
await assert.rejects(
async () => await cmdComplete('agent1', 'daily', false, deps),
(err) => err.code === 1 && err.message === 'No active lock found'
);
});
it('detects already completed task', async () => {
const locks = [
{ agent: 'agent1', eventId: 'done-id', status: 'completed' }
];
const deps = {
...baseDeps,
queryLocks: async () => locks,
};
const result = await cmdComplete('agent1', 'daily', false, deps);
assert.strictEqual(result.status, 'completed');
assert.strictEqual(result.eventId, 'done-id');
assert.match(logs.join('\n'), /LOCK_STATUS=completed/);
});
});
});
@@ -1,219 +0,0 @@
import { afterEach, beforeEach, describe, it, mock } from 'node:test';
import assert from 'node:assert';
import { publishLock, _resetRelayHealthState } from '../src/lock-ops.mjs';
const RELAYS = ['wss://relay-a', 'wss://relay-b', 'wss://relay-c'];
function parseTelemetry(lines) {
return lines.map((line) => JSON.parse(line));
}
function createDynamicPool(outcomeForAttempt) {
let publishCallCount = 0;
return {
publish(relays) {
publishCallCount += 1;
return relays.map((relay) => {
const outcome = outcomeForAttempt({
relay,
attempt: publishCallCount,
nowMs: Date.now(),
});
if (outcome.ok) {
return Promise.resolve('ok');
}
return Promise.reject(new Error(outcome.message));
});
},
close() {},
getPublishCallCount() {
return publishCallCount;
},
};
}
describe('ci-resilience lock acquisition under unstable relays', { concurrency: false }, () => {
const healthStabilityOverrides = {
failureThreshold: 999,
rollingWindowSize: 100,
};
beforeEach(() => {
_resetRelayHealthState();
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
});
afterEach(() => {
mock.timers.reset();
});
it('recovers after all relays are down for an initial outage window', async () => {
const telemetryLogs = [];
const sleepCalls = [];
const initialOutageMs = 3_000;
const pool = createDynamicPool(({ relay, nowMs }) => {
if (nowMs < initialOutageMs) {
return { ok: false, message: 'relay unavailable during outage' };
}
if (relay === 'wss://relay-c') {
return { ok: true };
}
return { ok: false, message: 'relay unavailable during outage recovery' };
});
const result = await publishLock(RELAYS, { id: 'evt-outage-recovery' }, {
poolFactory: () => pool,
getPublishTimeoutMsFn: () => 25,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 8,
retryBaseDelayMs: 1_000,
retryCapDelayMs: 1_000,
randomFn: () => 1,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
...healthStabilityOverrides,
});
assert.strictEqual(result.id, 'evt-outage-recovery');
assert.strictEqual(pool.getPublishCallCount(), 4);
assert.deepStrictEqual(sleepCalls, [1_000, 1_000, 1_000]);
const telemetry = parseTelemetry(telemetryLogs);
const quorumMet = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(quorumMet);
assert.strictEqual(quorumMet.publishAttempt, 4);
assert.ok(quorumMet.totalElapsedMs >= initialOutageMs);
assert.ok(quorumMet.totalElapsedMs < 6_000);
const retriesByRelay = telemetry
.filter((entry) => entry.event === 'lock_publish_retry')
.reduce((acc, entry) => {
acc[entry.relayUrl] = (acc[entry.relayUrl] || 0) + 1;
return acc;
}, {});
assert.strictEqual(retriesByRelay['wss://relay-a'], 3);
assert.strictEqual(retriesByRelay['wss://relay-b'], 3);
assert.strictEqual(retriesByRelay['wss://relay-c'], 3);
});
it('survives intermittent timeout spikes and reaches quorum with bounded retries', async () => {
const telemetryLogs = [];
const sleepCalls = [];
const pool = createDynamicPool(({ relay, attempt }) => {
if (relay === 'wss://relay-a') {
return { ok: true };
}
if (relay === 'wss://relay-b' && (attempt === 1 || attempt === 3)) {
return { ok: false, message: 'publish timed out after 25ms (spike)' };
}
return { ok: true };
});
const result = await publishLock(['wss://relay-a', 'wss://relay-b'], { id: 'evt-timeout-spikes' }, {
poolFactory: () => pool,
getPublishTimeoutMsFn: () => 25,
getMinSuccessfulRelayPublishesFn: () => 2,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
retryBaseDelayMs: 200,
retryCapDelayMs: 200,
randomFn: () => 1,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
...healthStabilityOverrides,
});
assert.strictEqual(result.id, 'evt-timeout-spikes');
assert.strictEqual(pool.getPublishCallCount(), 2);
assert.deepStrictEqual(sleepCalls, [200]);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-b');
assert.strictEqual(retryEvents[0].errorCategory, 'publish_timeout');
const quorumMet = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(quorumMet);
assert.strictEqual(quorumMet.publishAttempt, 2);
assert.ok(quorumMet.totalElapsedMs <= 1_000);
});
it('handles staggered relay recovery and never exceeds retry budget', async () => {
const telemetryLogs = [];
const sleepCalls = [];
const pool = createDynamicPool(({ relay, attempt }) => {
if (attempt <= 2) {
return { ok: false, message: 'connect timeout during outage' };
}
if (attempt === 3) {
if (relay === 'wss://relay-a') return { ok: true };
return { ok: false, message: 'connect timeout during staged recovery' };
}
if (relay === 'wss://relay-a' || relay === 'wss://relay-b') {
return { ok: true };
}
return { ok: false, message: 'relay unavailable' };
});
const retryAttempts = 6;
const result = await publishLock(RELAYS, { id: 'evt-staggered-recovery' }, {
poolFactory: () => pool,
getPublishTimeoutMsFn: () => 25,
getMinSuccessfulRelayPublishesFn: () => 2,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts,
retryBaseDelayMs: 300,
retryCapDelayMs: 300,
randomFn: () => 1,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
...healthStabilityOverrides,
});
assert.strictEqual(result.id, 'evt-staggered-recovery');
assert.strictEqual(pool.getPublishCallCount(), 4);
assert.ok(pool.getPublishCallCount() <= retryAttempts);
assert.deepStrictEqual(sleepCalls, [300, 300, 300]);
const telemetry = parseTelemetry(telemetryLogs);
const quorumMet = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(quorumMet);
assert.strictEqual(quorumMet.publishAttempt, 4);
assert.ok(quorumMet.totalElapsedMs < 3_000);
const retriesByRelay = telemetry
.filter((entry) => entry.event === 'lock_publish_retry')
.reduce((acc, entry) => {
acc[entry.relayUrl] = (acc[entry.relayUrl] || 0) + 1;
return acc;
}, {});
assert.strictEqual(retriesByRelay['wss://relay-a'], 2);
assert.strictEqual(retriesByRelay['wss://relay-b'], 3);
assert.strictEqual(retriesByRelay['wss://relay-c'], 3);
});
});
-192
View File
@@ -1,192 +0,0 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import { publishLock, _resetRelayHealthState } from '../src/lock-ops.mjs';
function createScriptedPool(scriptedOutcomesByAttempt, relaysSeen) {
let publishCallCount = 0;
return {
publish: (relays) => {
publishCallCount += 1;
relaysSeen.push({ publishAttempt: publishCallCount, relays: [...relays] });
const attemptOutcomes = scriptedOutcomesByAttempt[publishCallCount - 1] || {};
return relays.map((relay) => {
const outcome = attemptOutcomes[relay] || { ok: true };
return outcome.ok ? Promise.resolve('ok') : Promise.reject(new Error(outcome.message));
});
},
close: () => {},
getPublishCallCount: () => publishCallCount,
};
}
const parseTelemetry = (logs) => logs.map((line) => JSON.parse(line));
describe('publishLock retry behavior', { concurrency: false }, () => {
beforeEach(() => {
_resetRelayHealthState();
});
afterEach(() => {
mock.timers.reset();
});
it('succeeds immediately on first attempt without retries', async () => {
const telemetryLogs = [];
const relaysSeen = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
const event = { id: 'evt-immediate' };
const result = await publishLock(['wss://relay-a'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result, event);
assert.strictEqual(mockPool.getPublishCallCount(), 1);
assert.deepStrictEqual(relaysSeen, [{ publishAttempt: 1, relays: ['wss://relay-a'] }]);
const telemetry = parseTelemetry(telemetryLogs);
assert.strictEqual(telemetry.filter((entry) => entry.event === 'lock_publish_retry').length, 0);
const metEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(metEvent);
assert.strictEqual(metEvent.publishAttempt, 1);
});
it('retries a transient failure then succeeds within retry budget with bounded jitter', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const telemetryLogs = [];
const relaysSeen = [];
const sleepCalls = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'connection reset by peer' } },
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
const result = await publishLock(['wss://relay-a'], { id: 'evt-retry-success' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 100,
retryCapDelayMs: 500,
randomFn: () => 0.25,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result.id, 'evt-retry-success');
assert.deepStrictEqual(sleepCalls, [25]);
assert.strictEqual(mockPool.getPublishCallCount(), 2);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 1);
assert.strictEqual(retryEvents[0].publishAttempt, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[0].nextDelayMs >= 0 && retryEvents[0].nextDelayMs <= 100);
});
it('returns terminal relay_publish_quorum_failure after exhausting retry budget', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout'] });
const telemetryLogs = [];
const relaysSeen = [];
const sleepCalls = [];
const randomValues = [0.1, 0.9];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
{ 'wss://relay-a': { ok: false, message: 'relay unavailable' } },
], relaysSeen);
await assert.rejects(
() => publishLock(['wss://relay-a'], { id: 'evt-exhausted' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 100,
retryCapDelayMs: 250,
randomFn: () => randomValues.shift() ?? 0,
sleepFn: async (delayMs) => {
sleepCalls.push(delayMs);
mock.timers.tick(delayMs);
},
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_quorum_failure/,
);
assert.strictEqual(mockPool.getPublishCallCount(), 3);
assert.deepStrictEqual(sleepCalls, [10, 180]);
assert.deepStrictEqual(relaysSeen.map((entry) => entry.publishAttempt), [1, 2, 3]);
const telemetry = parseTelemetry(telemetryLogs);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 2);
assert.strictEqual(retryEvents[0].publishAttempt, 1);
assert.strictEqual(retryEvents[0].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[0].nextDelayMs >= 0 && retryEvents[0].nextDelayMs <= 100);
assert.strictEqual(retryEvents[1].publishAttempt, 2);
assert.strictEqual(retryEvents[1].relayUrl, 'wss://relay-a');
assert.ok(retryEvents[1].nextDelayMs >= retryEvents[0].nextDelayMs);
assert.ok(retryEvents[1].nextDelayMs <= 200);
const failedEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(failedEvent);
assert.strictEqual(failedEvent.errorCategory, 'relay_publish_quorum_failure');
assert.strictEqual(failedEvent.attempts, 3);
});
it('short-circuits non-retryable failures without extra attempts', async () => {
const telemetryLogs = [];
const relaysSeen = [];
const mockPool = createScriptedPool([
{ 'wss://relay-a': { ok: false, message: 'validation failed: invalid signature' } },
{ 'wss://relay-a': { ok: true } },
], relaysSeen);
await assert.rejects(
() => publishLock(['wss://relay-a'], { id: 'evt-permanent' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_non_retryable/,
);
assert.strictEqual(mockPool.getPublishCallCount(), 1);
assert.deepStrictEqual(relaysSeen, [{ publishAttempt: 1, relays: ['wss://relay-a'] }]);
const telemetry = parseTelemetry(telemetryLogs);
assert.strictEqual(telemetry.filter((entry) => entry.event === 'lock_publish_retry').length, 0);
const failedEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(failedEvent);
assert.strictEqual(failedEvent.errorCategory, 'relay_publish_non_retryable');
assert.notStrictEqual(failedEvent.errorCategory, 'relay_publish_quorum_failure');
});
});
-432
View File
@@ -1,432 +0,0 @@
import { describe, it, beforeEach } from 'node:test';
import assert from 'node:assert';
import {
parseLockEvent,
publishLock,
queryLocks,
_resetRelayHealthState,
} from '../src/lock-ops.mjs';
describe('lock-ops', () => {
beforeEach(() => {
_resetRelayHealthState();
});
describe('parseLockEvent', () => {
it('parses a valid lock event correctly', () => {
const event = {
id: 'mock-event-id',
pubkey: 'mock-pubkey',
created_at: 1678886400,
tags: [['d', 'mock-d-tag'], ['expiration', '1678890000']],
content: JSON.stringify({
agent: 'test-agent',
cadence: 'daily',
status: 'started',
date: '2023-03-15',
platform: 'test-platform',
}),
};
const result = parseLockEvent(event);
assert.strictEqual(result.dTag, 'mock-d-tag');
assert.strictEqual(result.expiresAt, 1678890000);
assert.strictEqual(result.agent, 'test-agent');
assert.strictEqual(result.platform, 'test-platform');
});
it('returns nulls when content is invalid JSON', () => {
const event = {
id: 'mock-id',
pubkey: 'mock-pubkey',
created_at: 1678886400,
tags: [],
content: 'invalid json',
};
const result = parseLockEvent(event);
assert.strictEqual(result.agent, null);
assert.strictEqual(result.cadence, null);
});
it('returns nulls when required content fields are missing', () => {
const event = {
id: 'mock-id',
pubkey: 'mock-pubkey',
created_at: 1678886400,
tags: [],
content: JSON.stringify({}),
};
const result = parseLockEvent(event);
assert.strictEqual(result.agent, null);
assert.strictEqual(result.cadence, null);
});
it('returns nulls when content is an array (not object)', () => {
const event = {
id: 'mock-id',
pubkey: 'mock-pubkey',
created_at: 1678886400,
tags: [],
content: JSON.stringify(['not', 'an', 'object']),
};
const result = parseLockEvent(event);
assert.strictEqual(result.agent, null);
});
});
describe('queryLocks', () => {
it('falls back to fallback relays when primary query fails', async () => {
const mockPool = {
querySync: async (relays) => {
if (relays.includes('wss://primary')) {
throw new Error('primary unavailable');
}
return [{
id: 'id-1',
pubkey: 'pk',
created_at: Math.floor(Date.now() / 1000),
tags: [['d', 't']],
content: JSON.stringify({ agent: 'agent1' }),
}];
},
close: () => {},
};
const locks = await queryLocks(
['wss://primary'],
'daily',
'2026-02-14',
'torch',
{
poolFactory: () => mockPool,
getQueryTimeoutMsFn: () => 200,
getRelayFallbacksFn: () => ['wss://fallback'],
errorLogger: () => {},
healthLogger: () => {},
},
);
assert.strictEqual(locks.length, 1);
assert.strictEqual(locks[0].agent, 'agent1');
});
});
describe('publishLock', () => {
it('retries transient failures and succeeds on a later attempt', async () => {
const telemetry = [];
const sleepCalls = [];
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
if (publishCalls === 1) {
return relays.map(() => Promise.reject(new Error('connection reset by peer')));
}
return relays.map(() => Promise.resolve('ok'));
},
close: () => {},
};
const event = { id: 'evt-retry' };
const result = await publishLock(['wss://primary-a', 'wss://primary-b'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
retryBaseDelayMs: 500,
retryCapDelayMs: 8_000,
randomFn: () => 0.5,
sleepFn: async (ms) => {
sleepCalls.push(ms);
},
telemetryLogger: (line) => {
telemetry.push(JSON.parse(line));
},
healthLogger: () => {},
diagnostics: {
attemptId: 'attempt-1',
correlationId: 'corr-1',
},
});
assert.strictEqual(result, event);
assert.strictEqual(publishCalls, 2);
assert.deepStrictEqual(sleepCalls, [250]);
const retryEvents = telemetry.filter((entry) => entry.event === 'lock_publish_retry');
assert.strictEqual(retryEvents.length, 2);
assert.deepStrictEqual(retryEvents[0], {
event: 'lock_publish_retry',
correlationId: 'corr-1',
attemptId: 'attempt-1',
publishAttempt: 1,
relayUrl: 'wss://primary-a',
errorCategory: 'connection_reset',
elapsedMs: retryEvents[0].elapsedMs,
nextDelayMs: 250,
});
assert.deepStrictEqual(retryEvents[1], {
event: 'lock_publish_retry',
correlationId: 'corr-1',
attemptId: 'attempt-1',
publishAttempt: 1,
relayUrl: 'wss://primary-b',
errorCategory: 'connection_reset',
elapsedMs: retryEvents[1].elapsedMs,
nextDelayMs: 250,
});
assert.ok(Number.isInteger(retryEvents[0].elapsedMs));
assert.ok(Number.isInteger(retryEvents[1].elapsedMs));
const metEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(metEvent);
assert.equal(metEvent.correlationId, 'corr-1');
assert.equal(metEvent.attemptId, 'attempt-1');
assert.equal(metEvent.publishAttempt, 2);
assert.ok(Array.isArray(metEvent.retryTimeline));
});
it('fails after retry budget is exhausted for persistent transient failures', async () => {
let publishCalls = 0;
const sleepCalls = [];
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map(() => Promise.reject(new Error('relay unavailable')));
},
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://primary-down'], { id: 'evt-exhausted' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
randomFn: () => 1,
sleepFn: async (ms) => {
sleepCalls.push(ms);
},
telemetryLogger: () => {},
healthLogger: () => {},
}),
/Failed relay publish quorum in publish phase: 0\/1 successful \(required=1, timeout=200ms, attempts=3,/,
);
assert.strictEqual(publishCalls, 3);
assert.deepStrictEqual(sleepCalls, [500, 1000]);
});
it('fails without retry for persistent validation failures', async () => {
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map(() => Promise.reject(new Error('validation failed: invalid signature')));
},
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://primary-bad'], { id: 'evt-permanent' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 4,
sleepFn: async () => {
throw new Error('should not sleep for permanent failures');
},
telemetryLogger: () => {},
healthLogger: () => {},
}),
/Failed relay publish quorum in publish phase: 0\/1 successful \(required=1, timeout=200ms, attempts=4,/,
);
assert.strictEqual(publishCalls, 1);
});
it('includes per-relay reason categories and retry metadata in failure diagnostics', async () => {
const telemetry = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
if (relay.includes('dns')) return Promise.reject(new Error('getaddrinfo ENOTFOUND relay.example'));
if (relay.includes('tcp')) return Promise.reject(new Error('connect ETIMEDOUT 1.2.3.4:443'));
if (relay.includes('tls')) return Promise.reject(new Error('TLS handshake failure'));
if (relay.includes('ws')) return Promise.reject(new Error('websocket: unexpected server response'));
return Promise.reject(new Error('[publish:primary] Publish timed out after 200ms'));
}),
close: () => {},
};
await assert.rejects(
() => publishLock(['wss://dns', 'wss://tcp', 'wss://tls', 'wss://ws', 'relay-timeout'], { id: 'evt-diag' }, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetry.push(JSON.parse(line)),
healthLogger: () => {},
diagnostics: { attemptId: 'attempt-42', correlationId: 'corr-42' },
}),
);
const summaryEvent = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(summaryEvent);
const failureEvents = telemetry.filter((entry) => entry.event === 'lock_publish_failure');
const failureText = failureEvents.map((entry) => `${entry.relayUrl}:${entry.reason}`).join('\n');
assert.match(failureText, /wss:\/\/dns:dns_resolution/);
assert.match(failureText, /wss:\/\/tcp:tcp_connect_timeout/);
assert.match(failureText, /wss:\/\/tls:tls_handshake/);
assert.match(failureText, /wss:\/\/ws:websocket_open_failure/);
assert.match(failureText, /relay-timeout:publish_timeout/);
assert.deepStrictEqual(summaryEvent.reasonDistribution, {
dns_resolution: 1,
tcp_connect_timeout: 1,
tls_handshake: 1,
websocket_open_failure: 1,
publish_timeout: 1,
});
});
it('returns success when mixed relay outcomes still satisfy quorum', async () => {
let publishCalls = 0;
const mockPool = {
publish: (relays) => {
publishCalls += 1;
return relays.map((relay) => (
relay === 'wss://relay-ok'
? Promise.resolve('ok')
: Promise.reject(new Error('validation failed: rejected event'))
));
},
close: () => {},
};
const event = { id: 'evt-mixed' };
const result = await publishLock(['wss://relay-ok', 'wss://relay-bad'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 3,
sleepFn: async () => {
throw new Error('should not sleep when quorum is satisfied');
},
healthLogger: () => {},
});
assert.strictEqual(result, event);
assert.strictEqual(publishCalls, 1);
});
it('uses fallback relays to satisfy min success quorum', async () => {
const mockPool = {
publish: (relays) => relays.map((relay) => (
relay.includes('fallback-ok') ? Promise.resolve('ok') : Promise.reject(new Error('offline'))
)),
close: () => {},
};
const event = { id: 'evt-2' };
const result = await publishLock(['wss://primary-bad'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => ['wss://fallback-ok'],
getMinActiveRelayPoolFn: () => 1,
healthLogger: () => {},
});
assert.strictEqual(result, event);
});
it('quarantines repeatedly failing relays and still reaches quorum with one healthy relay', async () => {
const callOrder = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
callOrder.push(relay);
if (relay === 'wss://healthy') {
return Promise.resolve('ok');
}
return Promise.reject(new Error('offline'));
}),
close: () => {},
};
const event = { id: 'evt-health' };
for (let i = 0; i < 3; i += 1) {
await publishLock(['wss://bad', 'wss://healthy'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
failureThreshold: 2,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
}
callOrder.length = 0;
await publishLock(['wss://bad', 'wss://healthy'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
failureThreshold: 2,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
assert.deepStrictEqual(callOrder, ['wss://healthy']);
});
it('uses fallback and min active pool to reintroduce quarantined relay when needed', async () => {
const attempts = [];
const mockPool = {
publish: (relays) => relays.map((relay) => {
attempts.push(relay);
if (relay === 'wss://fallback-healthy') {
return Promise.resolve('ok');
}
return Promise.reject(new Error('relay timeout'));
}),
close: () => {},
};
const event = { id: 'evt-quorum' };
await publishLock(['wss://primary-a', 'wss://primary-b'], event, {
poolFactory: () => mockPool,
getPublishTimeoutMsFn: () => 200,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => ['wss://fallback-healthy'],
getMinActiveRelayPoolFn: () => 2,
retryAttempts: 1,
failureThreshold: 1,
quarantineCooldownMs: 60_000,
healthLogger: () => {},
});
assert.ok(attempts.includes('wss://fallback-healthy'));
});
});
});
-66
View File
@@ -1,66 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert';
import { getCompletedAgents } from '../src/lock-utils.mjs';
test('getCompletedAgents', async (t) => {
await t.test('returns empty set if directory read fails (non-ENOENT)', async () => {
const deps = {
readdir: async () => { throw new Error('Permission denied'); },
getDateStr: () => '2026-02-24',
getIsoWeek: () => '2026-W09'
};
const completed = await getCompletedAgents('daily', 'logs', deps);
assert.strictEqual(completed.size, 0);
});
await t.test('returns empty set if directory does not exist (ENOENT)', async () => {
const error = new Error('ENOENT');
error.code = 'ENOENT';
const deps = {
readdir: async () => { throw error; },
getDateStr: () => '2026-02-24',
getIsoWeek: () => '2026-W09'
};
const completed = await getCompletedAgents('daily', 'logs', deps);
assert.strictEqual(completed.size, 0);
});
await t.test('identifies daily completed agents correctly', async () => {
const files = [
'2026-02-24T10-00-00Z__agent-a__completed.md',
'2026-02-24T11-00-00Z__agent-b__failed.md', // failed, not completed
'2026-02-23T10-00-00Z__agent-c__completed.md', // yesterday
'invalid-format.md'
];
const deps = {
readdir: async () => files,
getDateStr: () => '2026-02-24',
getIsoWeek: () => '2026-W09'
};
const completed = await getCompletedAgents('daily', 'logs', deps);
assert.ok(completed.has('agent-a'));
assert.ok(!completed.has('agent-b'));
assert.ok(!completed.has('agent-c'));
assert.strictEqual(completed.size, 1);
});
await t.test('identifies weekly completed agents correctly', async () => {
const files = [
'2026-02-24T10-00-00Z__agent-a__completed.md', // this week (W09)
'2026-02-17T10-00-00Z__agent-b__completed.md', // last week (W08)
];
const deps = {
readdir: async () => files,
getDateStr: () => '2026-02-24',
getIsoWeek: (date) => {
if (date === '2026-02-24') return '2026-W09';
if (date === '2026-02-17') return '2026-W08';
return '2026-W09'; // Default for current
}
};
const completed = await getCompletedAgents('weekly', 'logs', deps);
assert.ok(completed.has('agent-a'));
assert.ok(!completed.has('agent-b'));
assert.strictEqual(completed.size, 1);
});
});
-98
View File
@@ -1,98 +0,0 @@
import { mock, describe, it, before, after, beforeEach } from 'node:test';
import assert from 'node:assert';
// Mock process.exit
const originalExit = process.exit;
let exitMock;
// Mock console
const originalError = console.error;
let errorMock;
const originalLog = console.log;
let logMock;
describe('main dispatch', () => {
let main;
before(async () => {
exitMock = mock.fn();
process.exit = exitMock;
errorMock = mock.fn();
console.error = errorMock;
logMock = mock.fn();
console.log = logMock;
// We import main
// Since we cannot mock modules easily in this environment (mock.module missing),
// we will rely on testing the error paths of the handlers, which don't trigger side effects.
const mod = await import('../src/lib.mjs');
main = mod.main;
});
after(() => {
process.exit = originalExit;
console.error = originalError;
console.log = originalLog;
mock.reset();
});
beforeEach(() => {
exitMock.mock.resetCalls();
errorMock.mock.resetCalls();
logMock.mock.resetCalls();
});
it('should exit with code 1 if no command provided', async () => {
await main([]);
assert.strictEqual(exitMock.mock.callCount(), 1);
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
});
it('should exit with code 1 if unknown command provided', async () => {
await main(['unknown']);
assert.strictEqual(exitMock.mock.callCount(), 1);
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
const unknownMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('Unknown command'));
assert.ok(unknownMsg);
});
it('should exit with code 1 if check command misses cadence', async () => {
await main(['check']);
assert.strictEqual(exitMock.mock.callCount(), 1);
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('required for check'));
assert.ok(missingMsg);
});
it('should exit with code 1 if lock command misses agent', async () => {
await main(['lock']);
assert.strictEqual(exitMock.mock.callCount(), 1);
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('required for lock'));
assert.ok(missingMsg);
});
it('should exit with code 1 if complete command misses agent', async () => {
await main(['complete']);
assert.strictEqual(exitMock.mock.callCount(), 1);
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('required for complete'));
assert.ok(missingMsg);
});
it('should exit with code 1 if proposal command misses subcommand', async () => {
await main(['proposal']);
assert.strictEqual(exitMock.mock.callCount(), 1);
assert.strictEqual(exitMock.mock.calls[0].arguments[0], 1);
const missingMsg = errorMock.mock.calls.find(c => c.arguments[0].includes('Missing subcommand'));
assert.ok(missingMsg);
});
});
-176
View File
@@ -1,176 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
getRelevantMemories,
ingestEvents,
inspectMemory,
listMemories,
markMemoryMerged,
memoryStats,
pinMemory,
runPruneCycle,
triggerPruneDryRun,
unpinMemory,
} from '../src/services/memory/index.js';
test('listMemories, inspectMemory, pinMemory, and unpinMemory support admin hooks', async () => {
const records = [
{ id: 'm1', agent_id: 'agent-a', type: 'event', tags: ['ops'], pinned: false, last_seen: 10, merged_into: null },
{ id: 'm2', agent_id: 'agent-a', type: 'summary', tags: ['ops', 'prod'], pinned: true, last_seen: 20, merged_into: null },
{ id: 'm3', agent_id: 'agent-b', type: 'event', tags: ['dev'], pinned: false, last_seen: 5, merged_into: 'm2' },
];
const repository = {
async listMemories() {
return records;
},
async getMemoryById(id) {
return records.find((record) => record.id === id) ?? null;
},
async setPinned(id, pinned) {
const index = records.findIndex((record) => record.id === id);
if (index < 0) return null;
records[index] = { ...records[index], pinned };
return records[index];
},
};
const listed = await listMemories({ agent_id: 'agent-a', tags: ['ops'], includeMerged: false }, { repository });
assert.deepEqual(listed.map((entry) => entry.id), ['m2', 'm1']);
const inspected = await inspectMemory('m2', { repository });
assert.equal(inspected?.id, 'm2');
const pinned = await pinMemory('m1', { repository });
assert.equal(pinned?.pinned, true);
const unpinned = await unpinMemory('m2', { repository });
assert.equal(unpinned?.pinned, false);
});
test('triggerPruneDryRun returns candidate metadata without deleting records', async () => {
const repository = {
async listPruneCandidates() {
return [
{ id: 'old-1', agent_id: 'agent-z', type: 'event', pinned: false, last_seen: 1, merged_into: null },
];
},
async listMemories() {
return [];
},
};
const dryRun = await triggerPruneDryRun({ repository, retentionMs: 1000 });
assert.equal(dryRun.dryRun, true);
assert.equal(dryRun.candidateCount, 1);
assert.deepEqual(dryRun.candidates[0], {
id: 'old-1',
agent_id: 'agent-z',
type: 'event',
pinned: false,
last_seen: 1,
merged_into: null,
});
});
test('memoryStats reports counts/rates and retrieval telemetry remains redaction-safe', async () => {
const telemetry = [];
await ingestEvents([
{
agent_id: 'agent-telemetry',
content: 'customer token ABC123 secret',
timestamp: Date.now(),
tags: ['incident'],
metadata: { session_id: 's-telemetry' },
},
]);
const retrieved = await getRelevantMemories({
agent_id: 'agent-telemetry',
query: 'show customer token ABC123 secret',
emitTelemetry: (event, payload) => telemetry.push({ event, payload }),
});
assert.ok(retrieved.length >= 1);
const retrievalEvent = telemetry.find((entry) => entry.event === 'memory:retrieved');
assert.ok(retrievalEvent);
assert.equal('query' in retrievalEvent.payload, false);
assert.equal(retrievalEvent.payload.query_present, true);
assert.ok(retrievalEvent.payload.query_length > 0);
const stats = await memoryStats({ windowMs: 60 * 60 * 1000 });
assert.ok(stats.totals.total >= 1);
assert.ok(Number.isFinite(stats.rates.archivedRate));
assert.ok(Number.isFinite(stats.rates.deletedRate));
assert.ok(stats.indexSizeEstimateBytes >= 0);
assert.ok(Number.isFinite(stats.ingestThroughputPerMinute));
});
test('runPruneCycle respects prune feature-flag modes', async () => {
const repository = {
async listPruneCandidates() {
return [
{ id: 'old-1', agent_id: 'agent-z', type: 'event', pinned: false, last_seen: 1, merged_into: null },
];
},
};
const dryRunResult = await runPruneCycle({
repository,
env: { TORCH_MEMORY_PRUNE_ENABLED: 'dry-run' },
retentionMs: 1000,
});
assert.equal(dryRunResult.mode, 'dry-run');
assert.equal(dryRunResult.pruned.length, 0);
assert.equal(dryRunResult.candidates.length, 1);
const offResult = await runPruneCycle({
repository,
env: { TORCH_MEMORY_PRUNE_ENABLED: 'false' },
retentionMs: 1000,
});
assert.equal(offResult.mode, 'off');
assert.equal(offResult.pruned.length, 0);
});
test('markMemoryMerged delegates to repository and clears cache', async () => {
let callCount = 0;
const repository = {
async markMerged(id, _mergedInto) {
callCount++;
return id === 'exists';
}
};
const result1 = await markMemoryMerged('exists', 'target', { repository });
assert.equal(result1, true);
assert.equal(callCount, 1);
const result2 = await markMemoryMerged('missing', 'target', { repository });
assert.equal(result2, false);
assert.equal(callCount, 2);
// Verify cache clearing
const telemetry = [];
const emitTelemetry = (event, payload) => telemetry.push({ event, payload });
// 1. Populate cache
await getRelevantMemories({ agent_id: 'cache-test', emitTelemetry });
const hit1 = telemetry.findLast(t => t.event === 'memory:retrieved');
assert.equal(hit1.payload.cache_hit, false);
// 2. Hit cache
await getRelevantMemories({ agent_id: 'cache-test', emitTelemetry });
const hit2 = telemetry.findLast(t => t.event === 'memory:retrieved');
assert.equal(hit2.payload.cache_hit, true);
// 3. Clear cache via side-effect
await markMemoryMerged('exists', 'target', { repository });
// 4. Miss cache
await getRelevantMemories({ agent_id: 'cache-test', emitTelemetry });
const hit3 = telemetry.findLast(t => t.event === 'memory:retrieved');
assert.equal(hit3.payload.cache_hit, false);
});
-79
View File
@@ -1,79 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createMemoryCache } from '../src/services/memory/cache.js';
test('stores runtime events in signer/session namespace and clears scope', () => {
const cache = createMemoryCache();
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 });
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's2' }, { id: 2 });
assert.deepEqual(
cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).map((event) => event.id),
[1],
);
assert.deepEqual(
cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's2' }).map((event) => event.id),
[2],
);
cache.clearScope({ signer_id: 'alice', session_id: 's1' });
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's2' }).length, 1);
});
test('expires runtime events and records expiration metrics', () => {
const originalNow = Date.now;
try {
const start = originalNow.call(Date);
Date.now = () => start;
const cache = createMemoryCache();
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 }, 1);
// Advance mocked clock past the 1ms TTL
Date.now = () => start + 10;
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
const metrics = cache.getMetrics();
assert.equal(metrics.runtime_events_expired, 1);
} finally {
Date.now = originalNow;
}
});
test('enforces LRU bounds and tracks eviction metrics', () => {
const cache = createMemoryCache({ maxNamespaces: 1, maxEventsPerNamespace: 2, maxTotalEvents: 2 });
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 1 });
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 2 });
cache.setRuntimeEvent({ signer_id: 'alice', session_id: 's1' }, { id: 3 });
const ids = cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).map((event) => event.id);
assert.deepEqual(ids, [2, 3]);
cache.setRuntimeEvent({ signer_id: 'bob', session_id: 's2' }, { id: 4 });
assert.equal(cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' }).length, 0);
const metrics = cache.getMetrics();
assert.ok(metrics.runtime_events_lru_evicted >= 3);
});
test('blocks decrypted/session-sensitive data from durable promotion without sanitizer pass', () => {
const cache = createMemoryCache();
cache.setRuntimeEvent(
{ signer_id: 'alice', session_id: 's1' },
{ id: 1, classification: 'decrypted', sanitized_for_durable: false },
);
cache.setRuntimeEvent(
{ signer_id: 'alice', session_id: 's1' },
{ id: 2, classification: 'decrypted', sanitized_for_durable: true },
);
const events = cache.getRecentRuntimeEvents({ signer_id: 'alice', session_id: 's1' });
assert.equal(events[0].durable_eligible, false);
assert.equal(events[1].durable_eligible, true);
assert.equal(cache.getMetrics().runtime_sensitive_events_blocked, 1);
});
-41
View File
@@ -1,41 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createEmbedderAdapter, embedText } from '../src/services/memory/embedder.js';
test('createEmbedderAdapter selects in-memory backend and supports vector CRUD', async () => {
const adapter = createEmbedderAdapter({ backend: 'inmemory' });
// Manually provide a vector to verify storage mechanism works even if default embedder is disabled
const vector = [0.1, 0.2, 0.3];
await adapter.upsertVector({
id: 'vec-1',
vector,
metadata: { agent_id: 'a1', context_excerpt: 'hello world' },
});
const results = await adapter.queryVector({
vector,
k: 1,
filter: { agent_id: 'a1' },
});
assert.equal(results.length, 1);
assert.equal(results[0].id, 'vec-1');
assert.equal(results[0].metadata.context_excerpt, 'hello world');
const deleted = await adapter.deleteVector('vec-1');
assert.equal(deleted, true);
});
test('createEmbedderAdapter reads backend from environment', async () => {
process.env.MEMORY_VECTOR_BACKEND = 'local';
const adapter = createEmbedderAdapter();
const vector = await embedText('env selected', { adapter });
assert.ok(Array.isArray(vector));
// Default embedding is disabled, so vector should be empty
assert.equal(vector.length, 0);
delete process.env.MEMORY_VECTOR_BACKEND;
});
-28
View File
@@ -1,28 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
getMemoryPruneMode,
isMemoryEnabled,
isMemoryIngestEnabled,
isMemoryPruneEnabled,
} from '../src/services/memory/feature-flags.js';
test('memory feature flags parse defaults and canary allow-lists', () => {
assert.equal(isMemoryEnabled({}), true);
assert.equal(isMemoryEnabled({ TORCH_MEMORY_ENABLED: 'false' }), false);
assert.equal(isMemoryIngestEnabled('agent-a', {}), true);
assert.equal(isMemoryIngestEnabled('agent-a', { TORCH_MEMORY_INGEST_ENABLED: 'false' }), false);
assert.equal(isMemoryIngestEnabled('agent-a', { TORCH_MEMORY_INGEST_ENABLED: 'agent-a,agent-b' }), true);
assert.equal(isMemoryIngestEnabled('agent-c', { TORCH_MEMORY_INGEST_ENABLED: 'agent-a,agent-b' }), false);
});
test('memory prune flag supports off, dry-run, and active modes', () => {
assert.equal(getMemoryPruneMode({}), 'active');
assert.equal(getMemoryPruneMode({ TORCH_MEMORY_PRUNE_ENABLED: 'false' }), 'off');
assert.equal(getMemoryPruneMode({ TORCH_MEMORY_PRUNE_ENABLED: 'dry-run' }), 'dry-run');
assert.equal(getMemoryPruneMode({ TORCH_MEMORY_PRUNE_ENABLED: 'true' }), 'active');
assert.equal(isMemoryPruneEnabled({ TORCH_MEMORY_PRUNE_ENABLED: 'dry-run' }), false);
assert.equal(isMemoryPruneEnabled({ TORCH_MEMORY_PRUNE_ENABLED: 'true' }), true);
});
-440
View File
@@ -1,440 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
estimateTokenCount,
formatMemoryBlock,
formatMemoriesForPrompt,
renderPromptWithMemoryContext,
} from '../src/services/memory/formatter.js';
function createMemory(overrides = {}) {
return {
schema_version: 1,
id: overrides.id ?? 'm1',
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 ?? 1700000000000,
last_seen: overrides.last_seen ?? 1700000000000,
source: overrides.source ?? 'ingest',
ttl_days: overrides.ttl_days ?? null,
merged_into: overrides.merged_into ?? null,
pinned: overrides.pinned ?? false,
};
}
test('estimateTokenCount returns heuristic count', () => {
assert.equal(estimateTokenCount(''), 0);
assert.equal(estimateTokenCount(null), 0);
assert.equal(estimateTokenCount(undefined), 0);
assert.equal(estimateTokenCount('abcd'), 1);
assert.equal(estimateTokenCount('abcde'), 2);
assert.equal(estimateTokenCount('A'.repeat(400)), 100);
});
test('formatMemoryBlock converts memory record to display block', () => {
const memory = createMemory({
id: 'm1',
summary: 'The quick brown fox',
content: 'Jumps over the lazy dog',
importance: 0.856,
created_at: 1700000000000,
tags: ['fox', 'dog']
});
const block = formatMemoryBlock(memory);
assert.equal(block.id, 'm1');
assert.equal(block.summary, 'The quick brown fox');
assert.equal(block.short_context, 'Jumps over the lazy dog');
assert.equal(block.importance, '0.86');
assert.equal(block.timestamp, '2023-11-14T22:13:20.000Z');
assert.equal(block.tags, 'fox,dog');
});
test('formatMemoryBlock handles missing optional fields and default clamping', () => {
const memory = {
id: 'm-minimal',
created_at: 1700000000000,
};
const block = formatMemoryBlock(memory);
assert.equal(block.id, 'm-minimal');
assert.equal(block.summary, '');
assert.equal(block.short_context, '');
assert.equal(block.importance, '0.00');
assert.equal(block.tags, '');
});
test('formatMemoryBlock respects custom clamping options', () => {
const memory = createMemory({
summary: '1234567890',
content: 'abcdefghij',
});
const block = formatMemoryBlock(memory, {
maxSummaryChars: 5,
maxContextChars: 3
});
// clampString adds ellipsis if length > maxChars.
// if length > maxChars, it takes maxChars - 1 and adds …
assert.equal(block.summary, '1234…');
assert.equal(block.short_context, 'ab…');
});
test('formatMemoryBlock handles edge cases for clamping', () => {
const memory = createMemory({
summary: 'exact',
content: 'over',
});
// Exactly maxChars
const blockExact = formatMemoryBlock(memory, { maxSummaryChars: 5, maxContextChars: 3 });
assert.equal(blockExact.summary, 'exact');
assert.equal(blockExact.short_context, 'ov…');
// Negative or zero maxChars
const blockZero = formatMemoryBlock(memory, { maxSummaryChars: 0, maxContextChars: -1 });
assert.equal(blockZero.summary, '');
assert.equal(blockZero.short_context, '');
});
test('formatMemoriesForPrompt enforces token budget and k limit', () => {
const memories = [
createMemory({ id: 'm1', summary: 'one', importance: 0.9 }),
createMemory({ id: 'm2', summary: 'two', importance: 0.8 }),
createMemory({ id: 'm3', summary: 'three', importance: 0.7 }),
];
// Limit k to 2
const resultK = formatMemoriesForPrompt(memories, { k: 2 });
assert.equal(resultK.blocks.length, 2);
assert.equal(resultK.blocks[0].id, 'm1');
assert.equal(resultK.blocks[1].id, 'm2');
assert.equal(resultK.truncated, false);
// Limit token budget
// A single block line looks like: [m1, 2023-11-14T22:13:20.000Z, 0.90, one, default content, ]
// Tokens (chars/4): ~70 chars -> 18 tokens
const resultBudget = formatMemoriesForPrompt(memories, { tokenBudget: 25 });
assert.equal(resultBudget.blocks.length, 1);
assert.ok(resultBudget.usedTokens <= 25);
assert.equal(resultBudget.truncated, true);
});
test('formatMemoriesForPrompt handles aggressive budget fitting', () => {
const memory = createMemory({
id: 'm1',
summary: 'very long summary that will need clamping'.repeat(10),
content: 'very long content that will need clamping'.repeat(10),
});
// Very small budget should force fitting
const result = formatMemoriesForPrompt([memory], { tokenBudget: 15 });
assert.equal(result.blocks.length, 1);
assert.ok(result.usedTokens <= 15);
assert.ok(result.blocks[0].summary.length <= 8 || result.blocks[0].summary.includes('…'));
});
test('renderPromptWithMemoryContext injects context when enabled', async () => {
const memoryService = {
async getRelevantMemories() {
return [createMemory({ id: 'm-ctx', summary: 'relevant info' })];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
const output = await renderPromptWithMemoryContext(params);
assert.match(output, /\[CONTEXT — Relevant memories\]/);
assert.match(output, /relevant info/);
assert.match(output, /Base prompt/);
});
test('renderPromptWithMemoryContext returns base prompt when disabled', async () => {
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService: {},
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'false' }
};
const output = await renderPromptWithMemoryContext(params);
assert.equal(output, 'Base prompt');
});
test('renderPromptWithMemoryContext handles missing query or service', async () => {
const params = {
basePrompt: 'Base prompt',
userRequest: '',
agentContext: null,
agent_id: 'agent-1',
memoryService: null,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
const output = await renderPromptWithMemoryContext(params);
assert.equal(output, 'Base prompt');
});
test('renderPromptWithMemoryContext handles circular agentContext gracefully', async () => {
const circular = {};
circular.self = circular;
let capturedQuery = '';
const memoryService = {
async getRelevantMemories({ query }) {
capturedQuery = query;
return [];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agentContext: circular,
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
const output = await renderPromptWithMemoryContext(params);
// It should not throw.
// The query should contain the string representation of the circular object.
// String(circular) is "[object Object]"
assert.ok(capturedQuery.includes('[object Object]'), 'Query should contain the stringified circular object fallback');
assert.match(output, /Base prompt/);
});
// Added tests below
test('renderPromptWithMemoryContext handles agentContext as array of strings', async () => {
let capturedQuery = '';
const memoryService = {
async getRelevantMemories({ query }) {
capturedQuery = query;
return [];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agentContext: ['context1', 'context2'],
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
await renderPromptWithMemoryContext(params);
assert.ok(capturedQuery.includes('context1'));
assert.ok(capturedQuery.includes('context2'));
});
test('renderPromptWithMemoryContext handles agentContext as plain object', async () => {
let capturedQuery = '';
const memoryService = {
async getRelevantMemories({ query }) {
capturedQuery = query;
return [];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agentContext: { key: 'value' },
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
await renderPromptWithMemoryContext(params);
assert.ok(capturedQuery.includes('{"key":"value"}'));
});
test('renderPromptWithMemoryContext handles agentContext as primitives', async () => {
let capturedQuery = '';
const memoryService = {
async getRelevantMemories({ query }) {
capturedQuery = query;
return [];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agentContext: 12345,
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
await renderPromptWithMemoryContext(params);
assert.ok(capturedQuery.includes('12345'));
});
test('renderPromptWithMemoryContext calls updateMemoryUsage for retrieved memories', async () => {
const updatedIds = [];
const memoryService = {
async getRelevantMemories() {
return [
createMemory({ id: 'm1' }),
createMemory({ id: 'm2' })
];
},
async updateMemoryUsage(id) {
updatedIds.push(id);
}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
await renderPromptWithMemoryContext(params);
assert.deepEqual(updatedIds, ['m1', 'm2']);
});
test('renderPromptWithMemoryContext handles empty memories gracefully', async () => {
const memoryService = {
async getRelevantMemories() {
return [];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
const output = await renderPromptWithMemoryContext(params);
assert.equal(output, 'Base prompt');
});
test('renderPromptWithMemoryContext handles null/undefined memories gracefully', async () => {
const memoryService = {
async getRelevantMemories() {
return null;
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
const output = await renderPromptWithMemoryContext(params);
assert.equal(output, 'Base prompt');
});
test('renderPromptWithMemoryContext respects TORCH_MEMORY_ENABLED=false', async () => {
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService: {},
env: {
TORCH_MEMORY_ENABLED: 'false',
TORCH_MEMORY_RETRIEVAL_ENABLED: 'true'
}
};
const output = await renderPromptWithMemoryContext(params);
assert.equal(output, 'Base prompt');
});
test('renderPromptWithMemoryContext respects retrieval allowlist', async () => {
const memoryService = {
async getRelevantMemories() { return [createMemory({summary: 'Should not see'})]; }
};
// agent-2 is not in the allowlist
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-2',
memoryService,
env: {
TORCH_MEMORY_ENABLED: 'true',
TORCH_MEMORY_RETRIEVAL_ENABLED: 'agent-1'
}
};
const output = await renderPromptWithMemoryContext(params);
assert.equal(output, 'Base prompt');
// agent-1 is in the allowlist
const paramsAllowed = {
...params,
agent_id: 'agent-1'
};
const outputAllowed = await renderPromptWithMemoryContext(paramsAllowed);
assert.match(outputAllowed, /\[CONTEXT — Relevant memories\]/);
});
test('renderPromptWithMemoryContext passes k and tokenBudget', async () => {
const memoryService = {
async getRelevantMemories({ k }) {
assert.equal(k, 3, 'Should pass k=3');
// Return more memories than k to verify formatter truncation if needed,
// but getRelevantMemories is responsible for limiting too.
// Here we just check if it gets called with k.
return [
createMemory({id: 'm1'}),
createMemory({id: 'm2'}),
createMemory({id: 'm3'}),
createMemory({id: 'm4'}),
];
},
async updateMemoryUsage() {}
};
const params = {
basePrompt: 'Base prompt',
userRequest: 'Hello',
agent_id: 'agent-1',
memoryService,
retrievalK: 3,
tokenBudget: 50, // Small budget
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }
};
const output = await renderPromptWithMemoryContext(params);
// With small budget, it might truncate m3 or m2.
assert.match(output, /m1/);
});
-146
View File
@@ -1,146 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { getRelevantMemories } from '../src/services/memory/index.js';
function createMockMemory(id) {
return {
id,
content: `content for ${id}`,
agent_id: 'agent-1',
created_at: Date.now(),
last_seen: Date.now(),
};
}
test('getRelevantMemories uses cached results when available', async (t) => {
const cachedMemory = createMockMemory('cached-1');
const cacheGetMock = t.mock.fn((key) => {
if (key.includes('test-query')) return [cachedMemory];
return null;
});
const cacheSetMock = t.mock.fn();
const mockCache = {
get: cacheGetMock,
set: cacheSetMock,
};
const repository = {
listMemories: t.mock.fn(async () => []),
updateMemoryUsage: async () => {},
};
const result = await getRelevantMemories({
agent_id: 'agent-1',
query: 'test-query',
cache: mockCache,
repository,
});
assert.equal(result.length, 1);
assert.equal(result[0].id, 'cached-1');
assert.equal(cacheGetMock.mock.callCount(), 1);
// Should NOT call listMemories on cache hit
assert.equal(repository.listMemories.mock.callCount(), 0);
// Should NOT call cache.set on cache hit
assert.equal(cacheSetMock.mock.callCount(), 0);
});
test('getRelevantMemories calls ranker and updates cache on miss', async (t) => {
const memories = [createMockMemory('m1'), createMockMemory('m2')];
const repository = {
listMemories: t.mock.fn(async () => memories),
updateMemoryUsage: t.mock.fn(async () => {}),
};
// Mock ranker to reverse the list
const ranker = t.mock.fn(async (source) => [...source].reverse());
const cacheSetMock = t.mock.fn();
const mockCache = {
get: () => null,
set: cacheSetMock,
};
const result = await getRelevantMemories({
agent_id: 'agent-1',
query: 'test-query',
repository,
ranker,
cache: mockCache,
});
assert.equal(result.length, 2);
assert.equal(result[0].id, 'm2');
assert.equal(result[1].id, 'm1');
// Verify ranker called with correct source
assert.equal(ranker.mock.callCount(), 1);
const [sourceArg] = ranker.mock.calls[0].arguments;
assert.deepEqual(sourceArg, memories);
// Verify cache updated
assert.equal(cacheSetMock.mock.callCount(), 1);
const [keyArg, valueArg] = cacheSetMock.mock.calls[0].arguments;
assert.ok(typeof keyArg === 'string');
assert.deepEqual(valueArg, result); // Should cache the ranked result
// Verify memory usage updated
assert.equal(repository.updateMemoryUsage.mock.callCount(), 2);
});
test('getRelevantMemories passes correct parameters to ranker', async (t) => {
const repository = {
listMemories: async () => [],
updateMemoryUsage: async () => {},
};
const ranker = t.mock.fn(async () => []);
const mockCache = { get: () => null, set: () => {} };
const params = {
agent_id: 'agent-1',
query: 'my query',
tags: ['a', 'b'],
k: 5,
repository,
ranker,
cache: mockCache,
};
await getRelevantMemories(params);
assert.equal(ranker.mock.callCount(), 1);
const [, queryParams] = ranker.mock.calls[0].arguments;
assert.equal(queryParams.agent_id, 'agent-1');
assert.equal(queryParams.query, 'my query');
assert.deepEqual(queryParams.tags, ['a', 'b']);
assert.equal(queryParams.k, 5);
// Ensure injected deps are NOT in queryParams
assert.equal(queryParams.repository, undefined);
assert.equal(queryParams.ranker, undefined);
assert.equal(queryParams.cache, undefined);
});
test('getRelevantMemories triggers updateMemoryUsage on repository', async (t) => {
const m1 = createMockMemory('m1');
const repository = {
listMemories: async () => [m1],
updateMemoryUsage: t.mock.fn(async () => {}),
};
const ranker = async (source) => source; // identity ranker
const mockCache = { get: () => null, set: () => {} };
await getRelevantMemories({
agent_id: 'agent-1',
repository,
ranker,
cache: mockCache,
});
// updateMemoryUsage helper calls repository.updateMemoryUsage for each memory
assert.equal(repository.updateMemoryUsage.mock.callCount(), 1);
const [idArg] = repository.updateMemoryUsage.mock.calls[0].arguments;
assert.equal(idArg, 'm1');
});
-165
View File
@@ -1,165 +0,0 @@
import { test, describe, beforeEach } from 'node:test';
import assert from 'node:assert/strict';
import { ingestEvents, getRelevantMemories } from '../src/services/memory/index.js';
describe('ingestEvents', () => {
let repository;
let telemetryEvents = [];
let metricsEvents = [];
let storedMemories = [];
let storedFingerprints = new Map();
let linkedEmbeddings = new Map();
const mockOptions = {
embedText: async (_text) => Array(128).fill(0.1), // Deterministic embedding
emitTelemetry: (event, payload) => telemetryEvents.push({ event, payload }),
metrics: {
emit: (metric, payload) => metricsEvents.push({ metric, payload }),
},
// Mock environment to ensure enabled by default
env: {
TORCH_MEMORY_ENABLED: 'true',
TORCH_MEMORY_INGEST_ENABLED: 'true',
},
now: 1600000000000, // Fixed time
};
beforeEach(() => {
telemetryEvents = [];
metricsEvents = [];
storedMemories = [];
storedFingerprints = new Map();
linkedEmbeddings = new Map();
repository = {
async insertMemory(memory) {
storedMemories.push(memory);
return memory;
},
async hasIngestionFingerprint(key) {
return storedFingerprints.has(key);
},
async storeIngestionFingerprint(key, payload) {
storedFingerprints.set(key, payload);
},
async linkEmbedding(id, embedding) {
linkedEmbeddings.set(id, embedding);
},
// Minimal implementation for getRelevantMemories to work for cache check
async listMemories() {
return storedMemories;
},
async updateMemoryUsage() {},
};
});
test('happy path: successfully ingests events', async () => {
const events = [
{
agent_id: 'agent-1',
content: 'User logged in',
timestamp: mockOptions.now - 1000,
metadata: { session_id: 'session-1', source: 'test' },
},
{
agent_id: 'agent-1',
content: 'User clicked button',
timestamp: mockOptions.now - 500,
metadata: { session_id: 'session-1', source: 'test' },
},
];
const result = await ingestEvents(events, { ...mockOptions, repository });
assert.equal(result.length, 1); // Should be chunked into one memory
assert.equal(storedMemories.length, 1);
assert.equal(storedMemories[0].agent_id, 'agent-1');
assert.match(storedMemories[0].content, /User logged in/);
assert.match(storedMemories[0].content, /User clicked button/);
// Verify telemetry
const ingestedEvent = telemetryEvents.find((e) => e.event === 'memory:ingested');
assert.ok(ingestedEvent, 'Should emit memory:ingested telemetry');
assert.equal(ingestedEvent.payload.agent_id, 'agent-1');
assert.equal(ingestedEvent.payload.chunk_events, 2);
// Verify metrics
const ingestedMetric = metricsEvents.find((e) => e.metric === 'memory_ingested_total');
assert.ok(ingestedMetric, 'Should emit memory_ingested_total metric');
assert.equal(ingestedMetric.payload.count, 1);
// Verify embedding link
assert.ok(linkedEmbeddings.has(storedMemories[0].id), 'Should link embedding');
});
test('skips ingestion when feature flag is disabled', async () => {
const events = [{ agent_id: 'agent-1', content: 'test', timestamp: Date.now() }];
const options = {
...mockOptions,
repository,
env: { TORCH_MEMORY_INGEST_ENABLED: 'false' },
};
const result = await ingestEvents(events, options);
assert.equal(result.length, 0);
assert.equal(storedMemories.length, 0);
const skippedEvent = telemetryEvents.find((e) => e.event === 'memory:ingest_skipped');
assert.ok(skippedEvent, 'Should emit memory:ingest_skipped telemetry');
assert.equal(skippedEvent.payload.reason, 'flag_disabled');
});
test('clears cache after ingestion', async () => {
const agentId = 'agent-cache-test';
// 1. Populate cache
await getRelevantMemories({ agent_id: agentId, repository, ...mockOptions });
// 2. Verify cache hit (by checking telemetry from a second call)
telemetryEvents = []; // Reset telemetry
await getRelevantMemories({ agent_id: agentId, repository, ...mockOptions });
const hitEvent = telemetryEvents.find(e => e.event === 'memory:retrieved');
assert.ok(hitEvent.payload.cache_hit, 'Should hit cache before ingestion');
// 3. Ingest event
await ingestEvents(
[{ agent_id: agentId, content: 'new event', timestamp: mockOptions.now }],
{ ...mockOptions, repository }
);
// 4. Verify cache miss
telemetryEvents = []; // Reset telemetry
await getRelevantMemories({ agent_id: agentId, repository, ...mockOptions });
const missEvent = telemetryEvents.find(e => e.event === 'memory:retrieved');
assert.equal(missEvent.payload.cache_hit, false, 'Should miss cache after ingestion (cache cleared)');
});
test('skips duplicate windows', async () => {
const events = [{ agent_id: 'agent-dedupe', content: 'repeat', timestamp: mockOptions.now }];
// First ingestion
await ingestEvents(events, { ...mockOptions, repository });
assert.equal(storedMemories.length, 1);
// Second ingestion (should be duplicate)
await ingestEvents(events, { ...mockOptions, repository });
assert.equal(storedMemories.length, 1, 'Should not insert duplicate memory');
});
test('propagates repository errors', async () => {
const errorRepository = {
...repository,
async insertMemory() {
throw new Error('Database connection failed');
},
};
const events = [{ agent_id: 'agent-err', content: 'fail', timestamp: mockOptions.now }];
await assert.rejects(
async () => ingestEvents(events, { ...mockOptions, repository: errorRepository }),
/Database connection failed/
);
});
});
-108
View File
@@ -1,108 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { ingestMemoryWindow } from '../src/services/memory/ingestor.js';
test('ingestMemoryWindow gathers sources, redacts pii, chunks, embeds, links, and emits telemetry', async () => {
const inserted = [];
const linked = [];
const telemetry = [];
const upserted = [];
const repository = {
async insertMemory(memory) {
inserted.push(memory);
return memory;
},
async linkEmbedding(memoryId, embedding) {
linked.push({ memoryId, embedding });
},
};
const runtimeCache = {
getRecentRuntimeEvents() {
return [
{ agent_id: 'agent-1', content: 'runtime event [email protected]', timestamp: 1100, tags: ['runtime'], metadata: {} },
];
},
};
const logSource = {
async getEvents() {
return [
{ agent_id: 'agent-1', content: 'call me at 555-555-1212', timestamp: 1200, tags: ['log'], metadata: {} },
];
},
};
const records = await ingestMemoryWindow(
{
events: [
{ agent_id: 'agent-1', content: 'hello', timestamp: 1000, tags: ['direct'], metadata: { session_id: 's1' } },
],
runtimeCache,
logSource,
windowStart: 900,
windowEnd: 2000,
},
{
repository,
maxChunkChars: 40,
embedText: (text) => [text.length],
emitTelemetry: (event, payload) => telemetry.push({ event, payload }),
embedderAdapter: {
async embedText(text) {
return [text.length];
},
async upsertVector(entry) {
upserted.push(entry);
},
async queryVector() {
return [];
},
async deleteVector() {
return false;
},
},
now: 2000,
}
);
assert.equal(records.length, 2);
assert.equal(inserted.length, 2);
assert.equal(linked.length, 2);
assert.equal(telemetry.length, 2);
assert.equal(upserted.length, 2);
assert.ok(inserted.some((item) => item.content.includes('[redacted:email]')));
assert.ok(inserted.some((item) => item.content.includes('[redacted:phone]')));
assert.ok(telemetry.every((entry) => entry.event === 'memory:ingested'));
assert.ok(linked.every((entry) => typeof entry.embedding.metadata?.context_excerpt === 'string'));
assert.ok(linked.every((entry) => entry.embedding.metadata.context_excerpt.length > 0));
});
test('ingestMemoryWindow dedupes overlapping windows by hashed content + agent + timestamp bucket', async () => {
const inserted = [];
const repository = {
async insertMemory(memory) {
inserted.push(memory);
return memory;
},
};
const input = {
events: [
{ agent_id: 'agent-2', content: 'stable content', timestamp: 10_000, tags: ['a'], metadata: {} },
{ agent_id: 'agent-2', content: 'stable content two', timestamp: 10_200, tags: ['b'], metadata: {} },
],
windowStart: 10_000,
windowEnd: 10_500,
};
const first = await ingestMemoryWindow(input, { repository, now: 10_500 });
const second = await ingestMemoryWindow(input, { repository, now: 10_500 });
assert.equal(first.length, 1);
assert.equal(second.length, 0);
assert.equal(inserted.length, 1);
});
-141
View File
@@ -1,141 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { getRelevantMemories, pinMemory, unpinMemory } from '../src/services/memory/index.js';
test('pinMemory pins the memory and clears the cache', async () => {
const records = [
{ id: 'm1', agent_id: 'agent-a', type: 'event', tags: ['ops'], pinned: false, last_seen: 10, merged_into: null },
];
let listMemoriesCallCount = 0;
const repository = {
async listMemories() {
listMemoriesCallCount++;
return records;
},
async getMemoryById(id) {
return records.find((record) => record.id === id) ?? null;
},
async setPinned(id, pinned) {
const index = records.findIndex((record) => record.id === id);
if (index < 0) return null;
records[index] = { ...records[index], pinned };
return records[index];
},
async updateMemoryUsage() { return null; }
};
// 1. Populate cache
await getRelevantMemories({ agent_id: 'agent-a', repository });
assert.equal(listMemoriesCallCount, 1, 'Repository should be queried initially');
// 2. Verify cache hit
await getRelevantMemories({ agent_id: 'agent-a', repository });
assert.equal(listMemoriesCallCount, 1, 'Repository should NOT be queried on cache hit');
// 3. Pin memory
const pinned = await pinMemory('m1', { repository });
assert.ok(pinned);
assert.equal(pinned.pinned, true);
assert.equal(records[0].pinned, true);
// 4. Verify cache miss (cleared)
await getRelevantMemories({ agent_id: 'agent-a', repository });
assert.equal(listMemoriesCallCount, 2, 'Repository should be queried again after cache clear');
});
test('pinMemory returns null for non-existent ID but still clears cache', async () => {
const records = [];
let listMemoriesCallCount = 0;
const repository = {
async listMemories() {
listMemoriesCallCount++;
return records;
},
async setPinned(_id, _pinned) {
return null;
},
async updateMemoryUsage() { return null; }
};
// 1. Populate cache (use different agent_id to avoid cache collision with previous test)
await getRelevantMemories({ agent_id: 'agent-b', repository });
assert.equal(listMemoriesCallCount, 1);
// 2. Pin non-existent memory
const result = await pinMemory('non-existent', { repository });
assert.equal(result, null);
// 3. Verify cache cleared
await getRelevantMemories({ agent_id: 'agent-b', repository });
assert.equal(listMemoriesCallCount, 2, 'Cache should be cleared even if ID not found');
});
test('unpinMemory unpins the memory and clears the cache', async () => {
const records = [
{ id: 'm1', agent_id: 'agent-c', type: 'event', tags: ['ops'], pinned: true, last_seen: 10, merged_into: null },
];
let listMemoriesCallCount = 0;
const repository = {
async listMemories() {
listMemoriesCallCount++;
return records;
},
async getMemoryById(id) {
return records.find((record) => record.id === id) ?? null;
},
async setPinned(id, pinned) {
const index = records.findIndex((record) => record.id === id);
if (index < 0) return null;
records[index] = { ...records[index], pinned };
return records[index];
},
async updateMemoryUsage() { return null; }
};
// 1. Populate cache
await getRelevantMemories({ agent_id: 'agent-c', repository });
assert.equal(listMemoriesCallCount, 1, 'Repository should be queried initially');
// 2. Verify cache hit
await getRelevantMemories({ agent_id: 'agent-c', repository });
assert.equal(listMemoriesCallCount, 1, 'Repository should NOT be queried on cache hit');
// 3. Unpin memory
const updated = await unpinMemory('m1', { repository });
assert.ok(updated);
assert.equal(updated.pinned, false);
assert.equal(records[0].pinned, false);
// 4. Verify cache miss (cleared)
await getRelevantMemories({ agent_id: 'agent-c', repository });
assert.equal(listMemoriesCallCount, 2, 'Repository should be queried again after cache clear');
});
test('unpinMemory returns null for non-existent ID but still clears cache', async () => {
const records = [];
let listMemoriesCallCount = 0;
const repository = {
async listMemories() {
listMemoriesCallCount++;
return records;
},
async setPinned(_id, _pinned) {
return null;
},
async updateMemoryUsage() { return null; }
};
// 1. Populate cache (use different agent_id to avoid cache collision with previous test)
await getRelevantMemories({ agent_id: 'agent-d', repository });
assert.equal(listMemoriesCallCount, 1);
// 2. Unpin non-existent memory
const result = await unpinMemory('non-existent', { repository });
assert.equal(result, null);
// 3. Verify cache cleared
await getRelevantMemories({ agent_id: 'agent-d', repository });
assert.equal(listMemoriesCallCount, 2, 'Cache should be cleared even if ID not found');
});
-159
View File
@@ -1,159 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { applyLifecycleActions, createLifecyclePlan } from '../src/services/memory/pruner.js';
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 ?? 1_000,
last_seen: overrides.last_seen ?? 1_000,
source: overrides.source ?? 'ingest',
ttl_days: overrides.ttl_days ?? null,
merged_into: overrides.merged_into ?? null,
pinned: overrides.pinned ?? false,
};
}
test('createLifecyclePlan respects delete safety constraints and logs reasons', async () => {
const now = 80 * 24 * 60 * 60 * 1000;
const memories = [
createMemory({
id: 'pinned',
pinned: true,
importance: 0.01,
created_at: 0,
last_seen: 0,
ttl_days: 1,
tags: ['ops'],
}),
createMemory({
id: 'borderline',
importance: 0.2,
created_at: 0,
last_seen: now - (30 * 24 * 60 * 60 * 1000),
ttl_days: 1,
tags: ['ops'],
}),
createMemory({
id: 'deletable',
importance: 0.05,
created_at: 0,
last_seen: 0,
ttl_days: 1,
tags: ['ops'],
}),
];
const plan = await createLifecyclePlan(memories, {
now,
retentionMs: 30 * 24 * 60 * 60 * 1000,
getEmbedding: (memory) => (memory.id === 'borderline' ? [1, 0] : [0, 1]),
});
const byId = new Map(plan.actions.map((entry) => [entry.id, entry]));
assert.equal(byId.get('pinned')?.action, 'keep');
assert.equal(byId.get('borderline')?.action, 'archive');
assert.equal(byId.get('deletable')?.action, 'delete');
for (const log of plan.policyLogs) {
assert.equal(typeof log.reason, 'string');
assert.ok(log.reason.length > 0);
}
});
test('createLifecyclePlan condenses near-duplicates and archives merged source records', async () => {
const now = 100_000;
const memories = [
createMemory({
id: 'a',
summary: 'service restarted due to outage',
importance: 0.2,
created_at: 95_000,
last_seen: 95_000,
tags: ['incident', 'ops'],
}),
createMemory({
id: 'b',
summary: 'outage led to service restart',
importance: 0.2,
created_at: 95_100,
last_seen: 95_100,
tags: ['incident'],
}),
];
const plan = await createLifecyclePlan(memories, {
now,
retentionMs: 1_000,
duplicateWindowMs: 10_000,
similarityThreshold: 0.9,
getEmbedding: (memory) => (memory.id === 'a' ? [1, 0] : [0.99, 0.01]),
generateSummary: async () => JSON.stringify({
summary: 'Service outage and restart incident summary.',
importance: 0.4,
}),
});
assert.equal(plan.condensedGroups.length, 1);
assert.equal(plan.condensedGroups[0].merged.id, 'b');
const mergedDecision = plan.actions.find((item) => item.id === 'a');
assert.equal(mergedDecision?.action, 'archive');
assert.equal(mergedDecision?.merged_into, 'b');
});
test('applyLifecycleActions executes keep/archive/delete and merge markers', async () => {
const calls = [];
const repository = {
async markMerged(id, mergedInto) {
calls.push(['markMerged', id, mergedInto]);
},
async keepMemory(id, reason) {
calls.push(['keepMemory', id, reason]);
},
async archiveMemory(id, reason) {
calls.push(['archiveMemory', id, reason]);
},
async deleteMemory(id, reason) {
calls.push(['deleteMemory', id, reason]);
},
};
const result = await applyLifecycleActions(repository, {
actions: [
{ id: 'k1', action: 'keep', reason: 'keep reason', merged_into: null },
{ id: 'a1', action: 'archive', reason: 'archive reason', merged_into: 'k1' },
{ id: 'd1', action: 'delete', reason: 'delete reason', merged_into: null },
],
policyLogs: [],
});
// 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);
});
-205
View File
@@ -1,205 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { filterAndRankMemories } from '../src/services/memory/retriever.js';
import {
formatMemoriesForPrompt,
isMemoryRetrievalEnabled,
renderPromptWithMemoryContext,
} from '../src/services/memory/formatter.js';
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 ?? 1000,
last_seen: overrides.last_seen ?? 1000,
source: overrides.source ?? 'ingest',
ttl_days: overrides.ttl_days ?? null,
merged_into: overrides.merged_into ?? null,
pinned: overrides.pinned ?? false,
};
}
test('filterAndRankMemories applies metadata filters and composite score', async () => {
const now = 10_000;
const memories = [
createMemory({
id: 'm1',
content: 'billing incident root cause',
summary: 'billing incident',
importance: 0.9,
tags: ['incident', 'billing'],
created_at: 9000,
last_seen: 9500,
pinned: true,
}),
createMemory({
id: 'm2',
content: 'billing note',
summary: 'routine billing note',
importance: 0.2,
tags: ['billing'],
created_at: 8000,
last_seen: 8000,
pinned: false,
}),
createMemory({
id: 'm3',
content: 'other agent record',
summary: 'not for this agent',
agent_id: 'agent-2',
tags: ['incident', 'billing'],
}),
];
const vectorAdapter = {
async embedText() {
return [1, 0];
},
async queryVector() {
return [
{ id: 'm1', score: 0.95 },
{ id: 'm2', score: 0.8 },
];
},
};
const ranked = await filterAndRankMemories(memories, {
agent_id: 'agent-1',
query: 'billing incident',
tags: ['billing'],
timeframe: { from: 7000, to: 10_000 },
pinnedPreference: 'prefer',
k: 5,
now,
vectorAdapter,
});
assert.deepEqual(ranked.map((item) => item.id), ['m1', 'm2']);
const pinnedOnly = await filterAndRankMemories(memories, {
agent_id: 'agent-1',
query: 'billing',
pinnedPreference: 'only',
k: 5,
now,
vectorAdapter,
});
assert.deepEqual(pinnedOnly.map((item) => item.id), ['m1']);
});
test('formatMemoriesForPrompt returns prompt blocks and enforces token budget trimming', () => {
const memories = [
createMemory({
id: 'm1',
created_at: 1_700_000_000_000,
importance: 0.95,
summary: 'A'.repeat(200),
content: 'B'.repeat(500),
tags: ['incident', 'urgent'],
}),
createMemory({
id: 'm2',
created_at: 1_700_000_100_000,
importance: 0.7,
summary: 'second',
content: 'short context',
tags: ['ops'],
}),
];
const result = formatMemoriesForPrompt(memories, {
tokenBudget: 70,
k: 2,
});
assert.ok(result.blocks.length >= 1);
assert.ok(result.blocks.length <= 2);
assert.ok(result.usedTokens <= 70);
assert.ok(result.text.includes('[m1,'));
if (result.blocks.length === 1) {
assert.equal(result.truncated, true);
}
});
test('isMemoryRetrievalEnabled supports global toggle and agent canary list', () => {
assert.equal(isMemoryRetrievalEnabled('agent-a', { TORCH_MEMORY_RETRIEVAL_ENABLED: 'true' }), true);
assert.equal(isMemoryRetrievalEnabled('agent-a', { TORCH_MEMORY_RETRIEVAL_ENABLED: 'false' }), false);
assert.equal(
isMemoryRetrievalEnabled('agent-a', { TORCH_MEMORY_RETRIEVAL_ENABLED: 'agent-a,agent-b' }),
true,
);
assert.equal(
isMemoryRetrievalEnabled('agent-c', { TORCH_MEMORY_RETRIEVAL_ENABLED: 'agent-a,agent-b' }),
false,
);
assert.equal(
isMemoryRetrievalEnabled('agent-a', {
TORCH_MEMORY_ENABLED: 'false',
TORCH_MEMORY_RETRIEVAL_ENABLED: 'true',
}),
false,
);
});
test('renderPromptWithMemoryContext injects bounded memory block before base prompt', async () => {
const memories = [
createMemory({
id: 'm-ctx',
summary: 'Known issue in deploy pipeline',
content: 'Retry build after rotating temporary token',
created_at: 1_700_000_000_000,
importance: 0.91,
tags: ['deploy', 'incident'],
}),
];
const calls = [];
const usageUpdates = [];
const memoryService = {
async getRelevantMemories(params) {
calls.push(params);
return memories;
},
async updateMemoryUsage(id, lastSeen) {
usageUpdates.push({ id, lastSeen });
},
};
const output = await renderPromptWithMemoryContext({
basePrompt: 'Base final prompt body',
userRequest: 'Please debug deployment failure',
agentContext: { branch: 'main', tool: 'torch' },
agent_id: 'agent-a',
memoryService,
retrievalK: 1,
tokenBudget: 60,
env: { TORCH_MEMORY_RETRIEVAL_ENABLED: 'agent-a,agent-b' },
});
assert.equal(calls.length, 1);
assert.equal(calls[0].agent_id, 'agent-a');
assert.equal(calls[0].k, 1);
assert.match(calls[0].query, /Please debug deployment failure/);
assert.match(calls[0].query, /"branch":"main"/);
const headerIndex = output.indexOf('[CONTEXT — Relevant memories]');
const baseIndex = output.indexOf('Base final prompt body');
assert.ok(headerIndex >= 0);
assert.ok(baseIndex > headerIndex);
assert.ok(output.includes('[m-ctx,'));
assert.equal(usageUpdates.length, 1);
assert.equal(usageUpdates[0].id, 'm-ctx');
assert.equal(typeof usageUpdates[0].lastSeen, 'number');
});
-155
View File
@@ -1,155 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import crypto from 'node:crypto';
const originalCwd = process.cwd();
let tmpDir;
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 ?? 'test content',
summary: overrides.summary ?? 'test 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,
};
}
// Global state variables for the module under test
let runPruneCycle;
let listMemories;
// Setup temp environment and load module
test.before(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memory-prune-test-'));
// Create .scheduler-memory directory
const memDir = path.join(tmpDir, '.scheduler-memory');
await fs.mkdir(memDir, { recursive: true });
// Create initial state
// now = 200_000, retentionMs will be 100,000
// cutoff = 100,000
const memories = [
createMemory({ id: 'recent', last_seen: 150_000 }), // > 100,000 -> Keep
createMemory({ id: 'pinned', last_seen: 50_000, pinned: true }), // < 100,000 but pinned -> Keep
createMemory({ id: 'stale', last_seen: 50_000 }), // < 100,000 -> Delete
createMemory({ id: 'stale-dry', last_seen: 50_000 }), // < 100,000 -> Delete candidate
createMemory({ id: 'stale-off', last_seen: 50_000 }), // < 100,000 -> Delete candidate
];
// memory-store.json expects an array of entries [[id, memory], ...]
const entries = memories.map(m => [m.id, m]);
await fs.writeFile(path.join(memDir, 'memory-store.json'), JSON.stringify(entries), 'utf8');
// Change CWD
process.chdir(tmpDir);
// Import module (it will load the file we just wrote)
const mod = await import('../src/services/memory/index.js');
runPruneCycle = mod.runPruneCycle;
listMemories = mod.listMemories;
});
test.after(async () => {
if (originalCwd) process.chdir(originalCwd);
if (tmpDir) await fs.rm(tmpDir, { recursive: true, force: true });
});
test('runPruneCycle respects off mode', async () => {
const now = 200_000;
const retentionMs = 100_000;
const result = await runPruneCycle({
now,
retentionMs,
pruneMode: 'off',
});
assert.equal(result.mode, 'off');
// Verify nothing deleted
const memories = await listMemories();
assert.equal(memories.length, 5);
});
test('runPruneCycle respects dry-run mode', async () => {
const now = 200_000;
const retentionMs = 100_000;
const result = await runPruneCycle({
now,
retentionMs,
pruneMode: 'dry-run',
});
assert.equal(result.mode, 'dry-run');
// Expect 3 candidates: stale, stale-dry, stale-off
assert.equal(result.candidates.length, 3);
// Verify nothing deleted
const memories = await listMemories();
assert.equal(memories.length, 5);
});
test('runPruneCycle removes stale unpinned memories in active mode', async () => {
const now = 200_000;
const retentionMs = 100_000;
const telemetry = [];
const metrics = [];
const result = await runPruneCycle({
now,
retentionMs,
pruneMode: 'active',
emitTelemetry: (event, payload) => telemetry.push({ event, payload }),
emitMetric: (metric, payload) => metrics.push({ metric, payload }),
});
assert.equal(result.pruned.length, 3); // stale, stale-dry, stale-off
// Verify deletion
const memories = await listMemories();
assert.equal(memories.length, 2);
const ids = memories.map(m => m.id);
assert.ok(ids.includes('recent'));
assert.ok(ids.includes('pinned'));
assert.ok(!ids.includes('stale'));
// Verify telemetry
const prunedEvent = telemetry.find(t => t.event === 'memory:pruned');
assert.ok(prunedEvent);
assert.equal(prunedEvent.payload.pruned_count, 3);
// Verify metrics
const prunedMetric = metrics.find(m => m.metric === 'memory_pruned_total');
assert.ok(prunedMetric);
assert.equal(prunedMetric.payload.count, 3);
});
test('runPruneCycle starts scheduler if requested', async () => {
const result = await runPruneCycle({
scheduleEveryMs: 1000,
pruneMode: 'active',
});
assert.ok(result.scheduler);
assert.equal(typeof result.scheduler.stop, 'function');
result.scheduler.stop();
});
-145
View File
@@ -1,145 +0,0 @@
import { test, mock } from 'node:test';
import assert from 'node:assert/strict';
import {
createDbAdvisoryLockProvider,
startMemoryMaintenanceScheduler,
} from '../src/services/memory/scheduler.js';
test('createDbAdvisoryLockProvider acquires and releases postgres advisory locks', async () => {
const calls = [];
const db = {
async query(sql, params) {
calls.push({ sql, params });
if (sql.includes('pg_try_advisory_lock')) {
return { rows: [{ locked: true }] };
}
return { rows: [] };
},
};
const lockProvider = createDbAdvisoryLockProvider(db, { namespace: 'tests' });
const result = await lockProvider.withLock('job', async () => 42);
assert.equal(result.acquired, true);
assert.equal(result.result, 42);
assert.equal(calls.length, 2);
assert.match(calls[0].sql, /pg_try_advisory_lock/);
assert.match(calls[1].sql, /pg_advisory_unlock/);
assert.deepEqual(calls[0].params, calls[1].params);
});
test('startMemoryMaintenanceScheduler runs all jobs and emits metrics including retries', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout', 'setInterval'] });
const metrics = [];
let ingestAttempts = 0;
const scheduler = startMemoryMaintenanceScheduler({
runImmediately: true,
maxRetries: 2,
retryDelayMs: 1,
random: () => 0,
emitMetric(name, payload) {
metrics.push({ name, payload });
},
lockProvider: {
async withLock(key, task) {
if (key.includes('consolidate-observations')) {
return { acquired: false, result: null };
}
return { acquired: true, result: await task() };
},
},
handlers: {
async ingestRecentRuntimeEvents() {
ingestAttempts += 1;
if (ingestAttempts === 1) {
throw new Error('transient');
}
return { itemCount: 3 };
},
async consolidateObservations() {
return { itemCount: 11 };
},
async pruningCycle() {
return { itemCount: 2 };
},
async deepMergeArchivalMaintenance() {
return { itemCount: 1 };
},
},
});
// Advance time to allow async jobs and retries to complete.
// We tick enough to cover the retryDelayMs (1ms).
// We also need to allow the promise microtask queue to drain between ticks
// if necessary, but tick() usually handles the timer callbacks.
// Since the scheduler logic uses await, we need to ensure those promises resolve.
// Tick time forward and yield to event loop to allow promise chains to progress.
// The job involves multiple async steps (lock, handler, retry wait, lock, handler).
// We tick multiple times to ensure the retry timer fires and subsequent microtasks run.
for (let i = 0; i < 10; i++) {
mock.timers.tick(5);
await new Promise((resolve) => setImmediate(resolve));
}
scheduler.stop();
mock.timers.reset();
const byJob = metrics.reduce((acc, entry) => {
const key = entry.payload.job;
acc[key] = acc[key] || [];
acc[key].push(entry);
return acc;
}, {});
assert.equal(byJob.ingestRecentRuntimeEvents?.length, 1, 'Expected 1 ingest metric');
assert.equal(byJob.ingestRecentRuntimeEvents[0].payload.status, 'success');
assert.equal(byJob.ingestRecentRuntimeEvents[0].payload.itemCount, 3);
assert.equal(byJob.ingestRecentRuntimeEvents[0].payload.retries, 1);
assert.equal(byJob.consolidateObservations[0].payload.status, 'skipped_lock_unavailable');
assert.equal(byJob.pruningCycle[0].payload.status, 'success');
assert.equal(byJob.deepMergeArchivalMaintenance[0].payload.status, 'success');
});
test('startMemoryMaintenanceScheduler skips jobs when feature flags disable memory', async () => {
mock.timers.enable({ apis: ['Date', 'setTimeout', 'setInterval'] });
const metrics = [];
const called = [];
const scheduler = startMemoryMaintenanceScheduler({
runImmediately: true,
env: { TORCH_MEMORY_ENABLED: 'false' },
emitMetric(name, payload) {
metrics.push({ name, payload });
},
handlers: {
async ingestRecentRuntimeEvents() {
called.push('ingest');
},
async consolidateObservations() {
called.push('consolidate');
},
async pruningCycle() {
called.push('prune');
},
async deepMergeArchivalMaintenance() {
called.push('merge');
},
},
});
mock.timers.tick(10);
await new Promise(resolve => setImmediate(resolve));
scheduler.stop();
mock.timers.reset();
assert.deepEqual(called, []);
assert.ok(metrics.some((entry) => entry.payload.status === 'skipped_flag_disabled'));
});
-78
View File
@@ -1,78 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { ingestEvents } from '../src/services/memory/index.js';
import {
SCHEMA_VERSION,
normalizeMemoryItem,
validateMemoryItem,
} from '../src/services/memory/schema.js';
test('normalizeMemoryItem applies durable schema defaults and coercions', () => {
const item = normalizeMemoryItem({
agent_id: 'agent-1',
content: 'hello',
summary: 'hello',
tags: ['a', 2, ''],
importance: 2,
ttl_days: 3.8,
pinned: 'yes',
});
assert.equal(item.schema_version, SCHEMA_VERSION);
assert.equal(item.agent_id, 'agent-1');
assert.deepEqual(item.tags, ['a', '2']);
assert.equal(item.importance, 1);
assert.equal(item.ttl_days, 3);
assert.equal(item.pinned, true);
assert.equal(item.type, 'event');
assert.equal(item.source, 'ingest');
assert.ok(item.id);
});
test('validateMemoryItem reports field-level errors', () => {
const result = validateMemoryItem({
schema_version: SCHEMA_VERSION,
id: 'id-1',
agent_id: '',
session_id: 's-1',
type: 'event',
content: 'content',
summary: 'summary',
tags: ['tag'],
importance: 0.4,
embedding_id: null,
created_at: Date.now(),
last_seen: Date.now(),
source: 'ingest',
ttl_days: null,
merged_into: null,
pinned: false,
});
assert.equal(result.valid, false);
assert.ok(result.errors.some((error) => error.field === 'agent_id'));
});
test('ingestEvents rejects invalid writes and logs structured validation details', async () => {
const originalError = console.error;
const entries = [];
console.error = (...args) => {
entries.push(args);
};
try {
await assert.rejects(
() => ingestEvents([{ content: 'missing agent id' }]),
/Memory ingest rejected/
);
} finally {
console.error = originalError;
}
assert.equal(entries.length, 1);
const [tag, payload] = entries[0];
assert.equal(tag, 'memory_validation_error');
assert.equal(payload.stage, 'ingest');
assert.ok(Array.isArray(payload.fields));
assert.ok(payload.fields.some((field) => field.field === 'agent_id'));
});
-138
View File
@@ -1,138 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
memoryStats,
ingestEvents,
runPruneCycle,
} from '../src/services/memory/index.js';
test('memoryStats with empty repository returns zero counts and rates', async () => {
const repository = {
async listMemories() {
return [];
},
};
const stats = await memoryStats({ repository });
assert.deepEqual(stats.countsByType, {});
assert.equal(stats.totals.total, 0);
assert.equal(stats.totals.pinned, 0);
assert.equal(stats.totals.archived, 0);
assert.equal(stats.rates.archivedRate, 0);
assert.ok(Number.isFinite(stats.rates.deletedRate));
assert.equal(stats.indexSizeEstimateBytes, 0);
assert.ok(Number.isFinite(stats.ingestThroughputPerMinute));
});
test('memoryStats with populated repository returns correct counts and totals', async () => {
const memories = [
{ type: 'event', pinned: false, merged_into: null },
{ type: 'event', pinned: true, merged_into: null },
{ type: 'summary', pinned: false, merged_into: 'some-id' },
];
const repository = {
async listMemories() {
return memories;
},
};
const stats = await memoryStats({ repository });
assert.equal(stats.countsByType.event, 2);
assert.equal(stats.countsByType.summary, 1);
assert.equal(stats.totals.total, 3);
assert.equal(stats.totals.pinned, 1);
assert.equal(stats.totals.archived, 1);
// archivedRate = archived / max(1, total) = 1 / 3
assert.equal(stats.rates.archivedRate, 1 / 3);
// indexSizeEstimateBytes = 3 * 512 = 1536
assert.equal(stats.indexSizeEstimateBytes, 1536);
});
test('memoryStats calculates ingest throughput based on window', async (t) => {
t.mock.timers.enable({ apis: ['Date'] });
const baseTime = 1000000;
t.mock.timers.setTime(baseTime);
const repository = {
async insertMemory() { return {}; },
async hasIngestionFingerprint() { return false; },
async storeIngestionFingerprint() { },
async linkEmbedding() { },
async listMemories() { return []; },
};
// Create 3 distinct records (different agents to avoid batching into one record)
await ingestEvents([
{ content: 'e1', agent_id: 'agent-1', timestamp: baseTime },
{ content: 'e2', agent_id: 'agent-2', timestamp: baseTime },
{ content: 'e3', agent_id: 'agent-3', timestamp: baseTime },
], { repository });
// Move time forward 30 mins
const checkTime = baseTime + 30 * 60 * 1000;
t.mock.timers.setTime(checkTime);
// Check stats with 60 min window (should include baseTime)
// Window: [checkTime - 60min, checkTime] = [baseTime - 30min, baseTime + 30min]
const stats1 = await memoryStats({
repository,
now: checkTime,
windowMs: 60 * 60 * 1000
});
// We expect 3 ingested items in this window.
// throughput = 3 / (60 mins) = 0.05
assert.equal(stats1.ingestThroughputPerMinute, 3 / 60);
// Check stats with window that excludes the events (checkTime + 90 mins)
const farTime = baseTime + 90 * 60 * 1000;
// Window: [farTime - 60min, farTime] = [baseTime + 30min, baseTime + 90min]
// Events at baseTime are < baseTime + 30min, so excluded.
const stats2 = await memoryStats({
repository,
now: farTime,
windowMs: 60 * 60 * 1000
});
assert.equal(stats2.ingestThroughputPerMinute, 0);
});
test('memoryStats reflects deleted count from prune cycle', async () => {
const repository = {
async listPruneCandidates() {
// Return 2 candidates to be pruned
return [
{ id: 'p1', agent_id: 'a', type: 'event', last_seen: 0, pinned: false },
{ id: 'p2', agent_id: 'a', type: 'event', last_seen: 0, pinned: false },
];
},
async listMemories() { return []; }
};
// Run prune cycle
await runPruneCycle({ repository, retentionMs: 100, now: 1000 });
const stats = await memoryStats({ repository });
// deletedObserved should be increased by 2.
assert.ok(stats.totals.deletedObserved >= 2);
// deletedRate should be > 0.
assert.ok(stats.rates.deletedRate > 0);
});
test('memoryStats handles windowMs parameter correctly', async () => {
const repository = { async listMemories() { return []; } };
// Test with invalid windowMs, should default to 1 hour (60 mins)
const statsDefault = await memoryStats({ repository, windowMs: 'invalid' });
assert.ok(Number.isFinite(statsDefault.ingestThroughputPerMinute));
// Test with very small window
const statsSmall = await memoryStats({ repository, windowMs: 1 });
assert.ok(Number.isFinite(statsSmall.ingestThroughputPerMinute));
});
-47
View File
@@ -1,47 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { loadMemoryPromptTemplates, summarizeEvents } from '../src/services/memory/summarizer.js';
const SAMPLE_EVENTS = [
{ agent_id: 'a', content: 'Deployment completed for service alpha', timestamp: 1, tags: [], metadata: {} },
{ agent_id: 'a', content: 'Contact: [email protected]', timestamp: 2, tags: [], metadata: {} },
];
test('loadMemoryPromptTemplates loads all memory prompt templates from disk', () => {
const templates = loadMemoryPromptTemplates();
assert.ok(templates.summarize.includes('STRICT JSON only'));
assert.ok(templates.condense.includes('malformed JSON'));
assert.ok(templates.score.includes('importance'));
assert.ok(templates.prune.includes('prune'));
});
test('summarizeEvents retries with repair prompt when first response is malformed JSON', async () => {
const prompts = [];
const result = await summarizeEvents(SAMPLE_EVENTS, {
generateSummary(prompt) {
prompts.push(prompt);
if (prompts.length === 1) {
return '{"summary":"partial"';
}
return '{"summary":"Deployment completed for service alpha [redacted:email]","importance":0.6}';
},
});
assert.equal(prompts.length, 2);
assert.equal(result.importance, 0.6);
assert.equal(result.summary, 'Deployment completed for service alpha [redacted:email]');
});
test('summarizeEvents falls back to deterministic minimal summary with conservative importance when parsing fails twice', async () => {
const result = await summarizeEvents(SAMPLE_EVENTS, {
maxSummaryLength: 80,
async generateSummary() {
return 'not json';
},
});
assert.equal(result.importance, 0.25);
assert.match(result.summary, /Deployment completed for service alpha/);
assert.match(result.summary, /\[redacted:email\]/);
});
-102
View File
@@ -1,102 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import path from 'node:path';
import fs from 'node:fs';
const FIXTURE_PATH = path.join(process.cwd(), 'test', 'memory-telemetry-check.mjs');
const FIXTURE_CONTENT = `
import { ingestEvents } from '../src/services/memory/index.js';
// Mock process.stdout.write to capture output
const originalWrite = process.stdout.write;
let output = '';
process.stdout.write = (chunk, encoding, callback) => {
output += chunk.toString();
return originalWrite.call(process.stdout, chunk, encoding, callback);
};
// Also mock process.env to ensure ingest is DISABLED to trigger skip telemetry
process.env.TORCH_MEMORY_ENABLED = 'true';
process.env.TORCH_MEMORY_INGEST_ENABLED = 'off';
async function run() {
console.log('--- Start Test ---');
// Call ingestEvents which should trigger telemetry
await ingestEvents([], { agent_id: 'test-agent' });
console.log('--- End Test ---');
if (output.includes('memory_telemetry')) {
console.log('\\n[FAIL] Found memory_telemetry in stdout (Pollution detected)');
process.exit(1);
} else {
console.log('\\n[PASS] Did not find memory_telemetry in stdout (Clean)');
}
}
run().catch(console.error);
`;
test.before(() => {
fs.writeFileSync(FIXTURE_PATH, FIXTURE_CONTENT);
});
test.after(() => {
if (fs.existsSync(FIXTURE_PATH)) {
fs.unlinkSync(FIXTURE_PATH);
}
});
// Helper to run the script and capture output
function runScript(env = {}) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [FIXTURE_PATH], {
env: { ...process.env, ...env },
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
// The script exits with 0 on pass, 1 on fail
resolve({ code, stdout, stderr });
});
});
}
test('telemetry should not log to stdout or stderr by default', async () => {
const { code, stdout, stderr } = await runScript();
assert.equal(code, 0, 'Script should pass');
// The pass message contains "memory_telemetry", so we check for the FAIL condition specifically
assert.doesNotMatch(stdout, /Pollution detected/, 'stdout should not indicate pollution');
assert.match(stdout, /\[PASS\]/, 'stdout should indicate pass');
// stderr should be empty or minimal (no debug logs)
assert.doesNotMatch(stderr, /memory_telemetry/, 'stderr should not contain telemetry payload');
});
test('telemetry should log to stderr when NODE_DEBUG=torch-memory is set', async () => {
const { code, stdout, stderr } = await runScript({ NODE_DEBUG: 'torch-memory' });
assert.equal(code, 0, 'Script should pass (stdout is clean)');
assert.doesNotMatch(stdout, /Pollution detected/, 'stdout should not indicate pollution');
// stderr should contain the debug log
// util.debuglog format: "TORCH-MEMORY <pid>: <msg>"
assert.match(stderr, /TORCH-MEMORY/, 'stderr should contain debug tag');
assert.match(stderr, /memory_telemetry/, 'stderr should contain telemetry event name');
});
-282
View File
@@ -1,282 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { normalizeMemoryItem, validateMemoryItem } from '../src/services/memory/schema.js';
import { summarizeEvents } from '../src/services/memory/summarizer.js';
import { ingestMemoryWindow } from '../src/services/memory/ingestor.js';
import { filterAndRankMemories } from '../src/services/memory/retriever.js';
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import { startMemoryMaintenanceScheduler } from '../src/services/memory/scheduler.js';
import {
BASE_MEMORY_RECORD,
EXPECTED_SUMMARY,
EXPECTED_TAGS,
FIXED_NOW,
SYNTHETIC_EVENTS,
} from './fixtures/memory-fixtures.js';
/** Drain the async job queue without wall-clock dependency. */
const flushAsync = async () => {
for (let i = 0; i < 10; i++) {
await new Promise((resolve) => setImmediate(resolve));
}
};
const withMockedNow = async (now, run) => {
const originalNow = Date.now;
Date.now = () => now;
try {
await run();
} finally {
Date.now = originalNow;
}
};
test('schema validation covers required fields, coercion, and rejection paths', () => {
const normalized = normalizeMemoryItem({
agent_id: 'agent-alpha',
content: 'synthetic content',
summary: 'synthetic summary',
tags: ['incident', 7, ''],
importance: 8,
ttl_days: 4.9,
pinned: 'true',
});
assert.equal(normalized.agent_id, 'agent-alpha');
assert.deepEqual(normalized.tags, ['incident', '7']);
assert.equal(normalized.importance, 1);
assert.equal(normalized.ttl_days, 4);
assert.equal(normalized.pinned, true);
const validation = validateMemoryItem({
...BASE_MEMORY_RECORD,
agent_id: '',
importance: 2,
ttl_days: -1,
});
assert.equal(validation.valid, false);
assert.ok(validation.errors.some((error) => error.field === 'agent_id'));
assert.ok(validation.errors.some((error) => error.field === 'importance'));
assert.ok(validation.errors.some((error) => error.field === 'ttl_days'));
});
test('summarizer parser accepts valid json and recovers after malformed json response', async () => {
let calls = 0;
const summary = await summarizeEvents(SYNTHETIC_EVENTS, {
generateSummary() {
calls += 1;
if (calls === 1) return '{"summary":"broken"';
return JSON.stringify({
summary: EXPECTED_SUMMARY,
importance: 0.78,
});
},
now: FIXED_NOW,
});
assert.equal(calls, 2);
assert.equal(summary.summary, EXPECTED_SUMMARY);
assert.equal(summary.importance, 0.78);
});
test('ingest integration persists memory records and links embeddings from event batches', async () => {
const inserted = [];
const linked = [];
const repository = {
async insertMemory(memory) {
inserted.push(memory);
return memory;
},
async linkEmbedding(memoryId, embedding) {
linked.push({ memoryId, embedding });
},
};
const records = await ingestMemoryWindow(
{
events: SYNTHETIC_EVENTS,
windowStart: FIXED_NOW - 10_000,
windowEnd: FIXED_NOW,
},
{
repository,
now: FIXED_NOW,
maxChunkChars: 1_000,
embedText: () => [0.11, 0.89],
embedderAdapter: {
async embedText() {
return [0.11, 0.89];
},
async upsertVector() {},
},
generateSummary: async () => JSON.stringify({
summary: EXPECTED_SUMMARY,
importance: 0.82,
}),
}
);
assert.equal(records.length, 1);
assert.equal(inserted.length, 1);
assert.equal(linked.length, 1);
assert.equal(inserted[0].summary, EXPECTED_SUMMARY);
assert.deepEqual(inserted[0].tags, EXPECTED_TAGS);
assert.equal(linked[0].memoryId, inserted[0].id);
});
test('retrieval ranking blends semantic, importance, and recency signals', async () => {
const records = [
{
...BASE_MEMORY_RECORD,
id: 'semantic-high',
importance: 0.2,
last_seen: FIXED_NOW - 200_000,
},
{
...BASE_MEMORY_RECORD,
id: 'importance-high',
summary: 'Minor semantic match',
importance: 0.95,
last_seen: FIXED_NOW - 90_000_000,
},
{
...BASE_MEMORY_RECORD,
id: 'recent-balanced',
summary: 'Recent task update',
importance: 0.7,
last_seen: FIXED_NOW - 10_000,
},
];
const ranked = await filterAndRankMemories(records, {
agent_id: 'agent-alpha',
query: 'incident resolved',
k: 3,
now: FIXED_NOW,
vectorAdapter: {
async embedText() {
return [1, 0];
},
async queryVector() {
return [
{ id: 'semantic-high', score: 0.99 },
{ id: 'recent-balanced', score: 0.52 },
{ id: 'importance-high', score: 0.15 },
];
},
},
});
assert.deepEqual(ranked.map((item) => item.id), ['semantic-high', 'recent-balanced', 'importance-high']);
});
test('pruner policy protects pins, merges duplicates, and picks archive/delete outcomes', async () => {
const memories = [
{
...BASE_MEMORY_RECORD,
id: 'pinned-memory',
pinned: true,
importance: 0.01,
ttl_days: 1,
created_at: FIXED_NOW - (120 * 24 * 60 * 60 * 1000),
last_seen: FIXED_NOW - (120 * 24 * 60 * 60 * 1000),
},
{
...BASE_MEMORY_RECORD,
id: 'merge-a',
summary: 'service restart after outage',
importance: 0.2,
created_at: FIXED_NOW - 10_000,
last_seen: FIXED_NOW - 9_000,
},
{
...BASE_MEMORY_RECORD,
id: 'merge-b',
summary: 'outage triggered service restart',
importance: 0.2,
created_at: FIXED_NOW - 9_500,
last_seen: FIXED_NOW - 8_500,
tags: ['incident', 'ops'],
},
{
...BASE_MEMORY_RECORD,
id: 'delete-me',
importance: 0.01,
ttl_days: 1,
created_at: FIXED_NOW - (100 * 24 * 60 * 60 * 1000),
last_seen: FIXED_NOW - (100 * 24 * 60 * 60 * 1000),
pinned: false,
},
];
const plan = await createLifecyclePlan(memories, {
now: FIXED_NOW,
retentionMs: 30 * 24 * 60 * 60 * 1000,
similarityThreshold: 0.9,
duplicateWindowMs: 60_000,
getEmbedding(memory) {
if (memory.id === 'merge-a') return [1, 0];
if (memory.id === 'merge-b') return [0.99, 0.01];
return [0, 1];
},
generateSummary: async () => JSON.stringify({
summary: 'Merged outage restart summary',
importance: 0.4,
}),
});
const actionById = new Map(plan.actions.map((action) => [action.id, action.action]));
assert.equal(actionById.get('pinned-memory'), 'keep');
assert.equal(actionById.get('merge-a'), 'archive');
assert.equal(actionById.get('merge-b'), 'keep');
assert.equal(actionById.get('delete-me'), 'delete');
});
test('scheduler smoke test handles mocked clock and lock contention', async () => {
const metrics = [];
await withMockedNow(FIXED_NOW, async () => {
const scheduler = startMemoryMaintenanceScheduler({
runImmediately: true,
maxRetries: 0,
random: () => 0,
emitMetric(name, payload) {
metrics.push({ name, payload });
},
lockProvider: {
async withLock(lockKey, task) {
if (lockKey.includes('consolidate-observations')) {
return { acquired: false, result: null };
}
return { acquired: true, result: await task() };
},
},
handlers: {
async ingestRecentRuntimeEvents() {
return { itemCount: 2 };
},
async consolidateObservations() {
return { itemCount: 0 };
},
async pruningCycle() {
return { itemCount: 1 };
},
async deepMergeArchivalMaintenance() {
return { itemCount: 1 };
},
},
});
await flushAsync();
scheduler.stop();
});
const statuses = new Map(metrics.map((entry) => [entry.payload.job, entry.payload.status]));
assert.equal(statuses.get('ingestRecentRuntimeEvents'), 'success');
assert.equal(statuses.get('consolidateObservations'), 'skipped_lock_unavailable');
assert.equal(statuses.get('pruningCycle'), 'success');
assert.equal(statuses.get('deepMergeArchivalMaintenance'), 'success');
});
-63
View File
@@ -1,63 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { spawnSync, spawn } from 'node:child_process';
import fs from 'node:fs';
import { WebSocketServer } from 'ws';
describe('CLI Smoke Test', () => {
it('should print usage when no args provided', () => {
const result = spawnSync('node', ['bin/torch-lock.mjs']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr.toString(), /Usage:/);
});
it('should fail when checking without cadence', () => {
const result = spawnSync('node', ['bin/torch-lock.mjs', 'check']);
assert.strictEqual(result.status, 1);
assert.match(result.stderr.toString(), /--cadence <daily|weekly> is required/);
});
it('should include paused agents in check output', async () => {
// Start a mock relay
const wss = new WebSocketServer({ port: 0 });
const port = await new Promise(resolve => wss.on('listening', () => resolve(wss.address().port)));
wss.on('connection', (ws) => {
ws.on('message', (message) => {
const [type, subId] = JSON.parse(message);
if (type === 'REQ') {
ws.send(JSON.stringify(['EOSE', subId]));
}
});
});
const configPath = 'test-torch-config-paused.json';
fs.writeFileSync(configPath, JSON.stringify({
nostrLock: {
relays: [`ws://127.0.0.1:${port}`]
},
scheduler: { paused: { daily: ['paused-agent'] } }
}));
try {
const child = spawn('node', ['bin/torch-lock.mjs', 'check', '--cadence', 'daily', '--json', '--quiet'], {
env: { ...process.env, TORCH_CONFIG_PATH: configPath }
});
let stdout = '';
child.stdout.on('data', data => stdout += data);
const code = await new Promise(resolve => child.on('close', resolve));
assert.strictEqual(code, 0);
const output = JSON.parse(stdout);
assert.deepStrictEqual(output.paused, ['paused-agent']);
assert.ok(output.excluded.includes('paused-agent'));
} finally {
wss.close();
if (fs.existsSync(configPath)) {
fs.unlinkSync(configPath);
}
}
});
});
-65
View File
@@ -1,65 +0,0 @@
import fs from 'node:fs';
import path from 'node:path';
import { test } from 'node:test';
import assert from 'node:assert';
import { cmdInit } from '../src/ops.mjs';
test('ops.mjs: cmdInit creates .gitignore with node_modules', async (t) => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), 'test-gitignore-'));
// Clean up afterwards
t.after(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
const mockAnswers = {
installDir: 'torch',
namespace: 'test-ns',
hashtag: 'test-hash',
relays: ['wss://relay.damus.io'],
};
// Run init
// We silence console logs to keep test output clean, if possible, but it's fine.
await cmdInit(false, tmpDir, mockAnswers);
const gitIgnorePath = path.join(tmpDir, 'torch', '.gitignore');
if (fs.existsSync(gitIgnorePath)) {
const content = fs.readFileSync(gitIgnorePath, 'utf8');
assert.match(content, /node_modules/, '.gitignore should contain node_modules');
} else {
assert.fail('.gitignore was not created');
}
});
test('ops.mjs: cmdInit appends to existing .gitignore', async (t) => {
const tmpDir = fs.mkdtempSync(path.join(process.cwd(), 'test-gitignore-existing-'));
// Clean up afterwards
t.after(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
const torchDir = path.join(tmpDir, 'torch');
fs.mkdirSync(torchDir, { recursive: true });
// Create existing .gitignore without node_modules
fs.writeFileSync(path.join(torchDir, '.gitignore'), 'existing-pattern\n', 'utf8');
const mockAnswers = {
installDir: 'torch',
namespace: 'test-ns',
hashtag: 'test-hash',
relays: ['wss://relay.damus.io'],
};
// Run init (force=true because directory exists)
await cmdInit(true, tmpDir, mockAnswers);
const gitIgnorePath = path.join(torchDir, '.gitignore');
const content = fs.readFileSync(gitIgnorePath, 'utf8');
assert.match(content, /existing-pattern/, 'Should preserve existing content');
assert.match(content, /node_modules/, 'Should append node_modules');
});
-44
View File
@@ -1,44 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
function runInitWithSingleNewline(cwd) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [path.resolve('bin/torch-lock.mjs'), 'init', '--force'], {
cwd,
stdio: ['pipe', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.stdin.write('\n');
child.stdin.end();
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
test('init completes in non-interactive mode when stdin closes early', async () => {
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'torch-init-noninteractive-'));
await fs.promises.writeFile(path.join(tmpDir, 'package.json'), JSON.stringify({ name: 'tmp-host', version: '1.0.0' }), 'utf8');
const result = await runInitWithSingleNewline(tmpDir);
assert.equal(result.code, 0, `stdout:\n${result.stdout}\nstderr:\n${result.stderr}`);
assert.ok(fs.existsSync(path.join(tmpDir, 'torch')), 'torch directory should be created');
assert.ok(fs.existsSync(path.join(tmpDir, 'torch-config.json')), 'root torch-config.json should be created');
assert.ok(fs.existsSync(path.join(tmpDir, 'torch', 'src', 'prompts', 'scheduler-flow.md')), 'scheduler flow should be installed');
});
-256
View File
@@ -1,256 +0,0 @@
import { test, after } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import { exec } from 'node:child_process';
import { promisify } from 'node:util';
import { WebSocketServer } from 'ws';
import { cmdInit, cmdUpdate } from '../src/ops.mjs';
const execAsync = promisify(exec);
const MOCK_CONFIG = {
installDir: 'torch',
namespace: 'test-namespace',
hashtag: 'test-hashtag',
relays: ['wss://relay.damus.io']
};
const tempBase = fs.mkdtempSync(path.join(process.cwd(), 'test-ops-'));
after(() => {
fs.rmSync(tempBase, { recursive: true, force: true });
});
test('cmdInit creates directory structure and files', async () => {
const projectRoot = path.join(tempBase, 'project1');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
const torchDir = path.join(projectRoot, 'torch');
assert.ok(fs.existsSync(torchDir), 'torch dir created');
assert.ok(fs.existsSync(path.join(torchDir, 'prompts', 'daily')), 'daily prompts created');
assert.ok(fs.existsSync(path.join(torchDir, 'roster.json')), 'roster created');
assert.ok(fs.existsSync(path.join(torchDir, 'META_PROMPTS.md')), 'META_PROMPTS.md created');
assert.ok(fs.existsSync(path.join(torchDir, 'eslint.config.mjs')), 'eslint config copied');
// Check content transformation
const metaPrompts = fs.readFileSync(path.join(torchDir, 'META_PROMPTS.md'), 'utf8');
assert.ok(metaPrompts.includes('torch/prompts/daily/'), 'paths replaced in META_PROMPTS');
// Check Dashboard file
const dashboardPath = path.join(torchDir, 'TORCH_DASHBOARD.md');
assert.ok(fs.existsSync(dashboardPath), 'TORCH_DASHBOARD.md created');
const dashboardContent = fs.readFileSync(dashboardPath, 'utf8');
assert.ok(dashboardContent.includes('test-hashtag'), 'Dashboard content contains hashtag');
assert.ok(dashboardContent.includes('test-namespace'), 'Dashboard content contains namespace');
assert.ok(dashboardContent.includes('https://www.torchagent.ai/dashboard/'), 'Dashboard content contains link');
});
test('cmdInit fails if directory exists without force', async () => {
const projectRoot = path.join(tempBase, 'project2');
const torchDir = path.join(projectRoot, 'torch');
fs.mkdirSync(torchDir, { recursive: true });
fs.writeFileSync(path.join(torchDir, 'dummy.txt'), 'content');
await assert.rejects(
async () => await cmdInit(false, projectRoot, MOCK_CONFIG),
/already exists/,
'Should fail if torch dir exists'
);
});
test('cmdUpdate preserves modified prompt', async () => {
const projectRoot = path.join(tempBase, 'project3');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
const dailyDir = path.join(projectRoot, 'torch', 'prompts', 'daily');
const files = fs.readdirSync(dailyDir);
assert.ok(files.length > 0, 'Should have daily prompts');
const targetFile = path.join(dailyDir, files[0]);
fs.writeFileSync(targetFile, 'MODIFIED CONTENT', 'utf8');
await cmdUpdate(false, projectRoot);
const content = fs.readFileSync(targetFile, 'utf8');
assert.strictEqual(content, 'MODIFIED CONTENT', 'Prompt should be preserved');
});
test('cmdUpdate overwrites prompt with force', async () => {
const projectRoot = path.join(tempBase, 'project4');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
const dailyDir = path.join(projectRoot, 'torch', 'prompts', 'daily');
const files = fs.readdirSync(dailyDir);
const targetFile = path.join(dailyDir, files[0]);
fs.writeFileSync(targetFile, 'MODIFIED CONTENT', 'utf8');
await cmdUpdate(true, projectRoot); // Force update
const content = fs.readFileSync(targetFile, 'utf8');
assert.notStrictEqual(content, 'MODIFIED CONTENT', 'Prompt should be overwritten');
});
test('cmdUpdate creates backup', async () => {
const projectRoot = path.join(tempBase, 'project5');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
await cmdUpdate(false, projectRoot);
const torchDir = path.join(projectRoot, 'torch');
const backupRoot = path.join(torchDir, '_backups');
assert.ok(fs.existsSync(backupRoot), '_backups directory created');
const entries = fs.readdirSync(backupRoot);
assert.ok(entries.length > 0, 'Should have at least one backup');
const backupName = entries[0];
assert.match(backupName, /^backup_/, 'Backup directory name starts with backup_');
const backupContent = fs.readdirSync(path.join(backupRoot, backupName));
assert.ok(backupContent.includes('roster.json'), 'Backup contains roster.json');
});
test('cmdUpdate succeeds when running from package root install (src and dest identical)', async () => {
const fixtureRoot = path.join(tempBase, 'project_self_root');
fs.mkdirSync(fixtureRoot, { recursive: true });
const dirsToCopy = ['src', 'bin', 'dashboard', 'landing', 'assets', 'scripts'];
const filesToCopy = ['package.json', 'build.mjs', 'README.md', 'torch-config.example.json', 'TORCH.md', 'eslint.config.mjs'];
for (const dir of dirsToCopy) {
fs.cpSync(path.join(process.cwd(), dir), path.join(fixtureRoot, dir), { recursive: true });
}
for (const file of filesToCopy) {
fs.copyFileSync(path.join(process.cwd(), file), path.join(fixtureRoot, file));
}
const moduleUrl = pathToFileURL(path.join(fixtureRoot, 'src', 'ops.mjs'));
const { cmdUpdate: fixtureCmdUpdate } = await import(moduleUrl.href);
await assert.doesNotReject(async () => {
await fixtureCmdUpdate(true, fixtureRoot);
}, 'update should not fail when package root is install target');
const backupRoot = path.join(fixtureRoot, '_backups');
assert.ok(fs.existsSync(backupRoot), '_backups directory created for package-root update');
assert.ok(fs.readdirSync(backupRoot).length > 0, 'backup entry created for package-root update');
});
test('torch-lock check respects local roster', async () => {
const wss = new WebSocketServer({ port: 0 });
const port = wss.address().port;
const mockRelayUrl = `ws://127.0.0.1:${port}`;
wss.on('connection', (ws) => {
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
if (data[0] === 'REQ') {
const subId = data[1];
ws.send(JSON.stringify(['EOSE', subId]));
}
} catch (_err) {
// ignore
}
});
});
try {
const projectRoot = path.join(tempBase, 'project_roster');
fs.mkdirSync(projectRoot, { recursive: true });
// Use local mock relay
const localConfig = { ...MOCK_CONFIG, relays: [mockRelayUrl] };
// Init
await cmdInit(false, projectRoot, localConfig);
// Modify roster
const rosterPath = path.join(projectRoot, 'torch', 'roster.json');
const roster = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
roster.daily.push('custom-agent-007');
fs.writeFileSync(rosterPath, JSON.stringify(roster, null, 2));
// Run CLI
const binPath = path.resolve(process.cwd(), 'bin/torch-lock.mjs');
const { stdout } = await execAsync(`${process.execPath} ${binPath} check --cadence daily`, {
cwd: projectRoot,
encoding: 'utf8',
env: { ...process.env, NOSTR_LOCK_RELAYS: undefined, NOSTR_LOCK_RELAY_FALLBACKS: '' }
});
const result = JSON.parse(stdout);
assert.ok(result.available.includes('custom-agent-007'), 'Custom agent found in roster');
} finally {
wss.close();
}
});
test('cmdInit creates torch-config.json with random namespace', async () => {
const projectRoot = path.join(tempBase, 'project_config_init');
fs.mkdirSync(projectRoot, { recursive: true });
await cmdInit(false, projectRoot, MOCK_CONFIG);
const configPath = path.join(projectRoot, 'torch-config.json');
assert.ok(fs.existsSync(configPath), 'torch-config.json created');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.strictEqual(config.nostrLock.namespace, 'test-namespace', 'Namespace should match mock');
assert.strictEqual(config.dashboard.hashtag, 'test-hashtag', 'Hashtag should match mock');
assert.strictEqual(
config.scheduler.handoffCommandByCadence.daily,
'node torch/scripts/agent/run-selected-prompt.mjs',
'daily handoff command should be configured for host installs',
);
assert.strictEqual(
config.scheduler.handoffCommandByCadence.weekly,
'node torch/scripts/agent/run-selected-prompt.mjs',
'weekly handoff command should be configured for host installs',
);
});
test('cmdInit upserts TORCH memory integration into existing AGENTS.md and CLAUDE.md', async () => {
const projectRoot = path.join(tempBase, 'project_memory_hook_init');
fs.mkdirSync(projectRoot, { recursive: true });
const agentsPath = path.join(projectRoot, 'AGENTS.md');
const claudePath = path.join(projectRoot, 'CLAUDE.md');
fs.writeFileSync(agentsPath, '# Team Policy\n\nKeep tests strict.\n', 'utf8');
fs.writeFileSync(claudePath, '# Local Guidance\n\nFollow repo conventions.\n', 'utf8');
await cmdInit(false, projectRoot, MOCK_CONFIG);
const agentsContent = fs.readFileSync(agentsPath, 'utf8');
const claudeContent = fs.readFileSync(claudePath, 'utf8');
assert.ok(agentsContent.includes('## TORCH Memory Integration'), 'AGENTS.md should include memory heading');
assert.ok(claudeContent.includes('## TORCH Memory Integration'), 'CLAUDE.md should include memory heading');
assert.ok(agentsContent.includes('Keep tests strict.'), 'AGENTS.md existing content should be preserved');
assert.ok(claudeContent.includes('Follow repo conventions.'), 'CLAUDE.md existing content should be preserved');
});
test('cmdUpdate keeps TORCH memory integration idempotent', async () => {
const projectRoot = path.join(tempBase, 'project_memory_hook_update');
fs.mkdirSync(projectRoot, { recursive: true });
const agentsPath = path.join(projectRoot, 'AGENTS.md');
fs.writeFileSync(agentsPath, '# Agent Rules\n', 'utf8');
await cmdInit(false, projectRoot, MOCK_CONFIG);
await cmdUpdate(false, projectRoot);
const agentsContent = fs.readFileSync(agentsPath, 'utf8');
const headingMatches = agentsContent.match(/## TORCH Memory Integration/g) ?? [];
assert.strictEqual(headingMatches.length, 1, 'Memory integration block should not be duplicated');
});
-78
View File
@@ -1,78 +0,0 @@
import { test, expect } from '@playwright/test';
import fs from 'fs';
import path from 'path';
// Prepare logging directory
const logDir = 'test_logs';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
const logFile = path.join(logDir, 'playwright_console.log');
// Clear previous log
fs.writeFileSync(logFile, '');
test.use({
extraHTTPHeaders: {
// base64('admin:password') = 'YWRtaW46cGFzc3dvcmQ='
'Authorization': 'Basic YWRtaW46cGFzc3dvcmQ='
}
});
test.describe.serial('TORCH Dashboard', () => {
test.beforeEach(async ({ page }) => {
page.on('console', msg => {
const logLine = `[${msg.type()}] ${msg.text()}\n`;
fs.appendFileSync(logFile, logLine);
// Also log to stdout so it appears in the runner output
// console.log(`[Browser Console] ${msg.text()}`);
});
});
test('should load the dashboard and verify title', async ({ page }) => {
await page.goto('/');
// Check for title "TORCH Dashboard" or similar. Checking regex /TORCH/ is safer.
await expect(page).toHaveTitle(/TORCH/);
// Check for some basic element presence to ensure rendering happened
// Assuming there is a header or main content
// We can inspect dashboard/index.html to be sure
// But generic check for body visibility is a start
await expect(page.locator('body')).toBeVisible();
// Verify console log capture
await page.evaluate(() => console.log('Test log from browser'));
});
test('should fail authentication with wrong credentials', async ({ browser }) => {
const context = await browser.newContext({
extraHTTPHeaders: {
'Authorization': 'Basic YWRtaW46d3Jvbmc=' // admin:wrong
}
});
const page = await context.newPage();
const response = await page.goto('/');
expect(response.status()).toBe(401);
await context.close();
});
test('should serve public config correctly', async ({ request }) => {
const response = await request.get('/torch-config.json');
expect(response.ok()).toBeTruthy();
const config = await response.json();
expect(config.nostrLock.namespace).toBe('test-torch');
// Security check: Auth should be stripped
expect(config.dashboard.auth).toBeUndefined();
});
test('should shutdown gracefully to save coverage', async ({ request }) => {
// Attempt to shutdown. It might fail if already down (unlikely in serial).
try {
// We don't expect a response because the server dies
await request.get('/shutdown', { timeout: 1000 }).catch(() => {});
} catch (_) {
// Ignore
}
// Wait a bit for file write
await new Promise(resolve => setTimeout(resolve, 2000));
});
});
@@ -1,30 +0,0 @@
{
"nostrLock": {
"namespace": "test-torch",
"relays": ["wss://relay.damus.io"],
"relayFallbacks": [],
"ttlSeconds": 60,
"queryTimeoutMs": 5000,
"publishTimeoutMs": 5000,
"minSuccessfulRelayPublishes": 1,
"minActiveRelayPool": 1
},
"dashboard": {
"auth": "admin:password",
"defaultCadenceView": "daily",
"defaultStatusView": "active",
"relays": ["wss://relay.damus.io"],
"namespace": "test-torch",
"hashtag": "test-torch-lock"
},
"scheduler": {
"firstPromptByCadence": {
"daily": "src/prompts/daily/agent.md",
"weekly": "src/prompts/weekly/agent.md"
},
"paused": {
"daily": [],
"weekly": []
}
}
}
@@ -1,239 +0,0 @@
import { afterEach, beforeEach, describe, it } from 'node:test';
import assert from 'node:assert';
import net from 'node:net';
import { WebSocketServer } from 'ws';
import { publishLock, _resetRelayHealthState } from '../src/lock-ops.mjs';
async function getFreePort() {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, '127.0.0.1', () => {
const address = server.address();
const port = typeof address === 'object' && address ? address.port : null;
server.close((closeErr) => {
if (closeErr) {
reject(closeErr);
return;
}
resolve(port);
});
});
server.on('error', reject);
});
}
async function createRelaySimulatorFixture(modes) {
if (modes.length !== 3) {
throw new Error(`Expected exactly three relays, got ${modes.length}`);
}
const servers = [];
const relays = [];
for (const mode of modes) {
const port = await getFreePort();
const relayUrl = `ws://127.0.0.1:${port}`;
if (mode.type === 'connection-fail') {
relays.push(relayUrl);
continue;
}
const wss = new WebSocketServer({ host: '127.0.0.1', port });
wss.on('connection', (socket) => {
socket.on('message', (rawData) => {
let payload;
try {
payload = JSON.parse(rawData.toString());
} catch {
return;
}
if (!Array.isArray(payload) || payload[0] !== 'EVENT') {
return;
}
const event = payload[1] || {};
if (mode.type === 'success') {
const delayMs = mode.delayMs ?? 0;
setTimeout(() => {
if (socket.readyState === socket.OPEN) {
socket.send(JSON.stringify(['OK', event.id, true, 'accepted']));
}
}, delayMs);
}
});
});
servers.push(wss);
relays.push(relayUrl);
}
return {
relays,
async stop() {
await Promise.all(servers.map((server) => new Promise((resolve, reject) => {
server.close((err) => {
if (err) reject(err);
else resolve();
});
})));
},
};
}
function parseTelemetry(lines) {
return lines.map((line) => JSON.parse(line));
}
function makeEvent(id) {
return {
id,
kind: 1,
pubkey: 'f'.repeat(64),
created_at: Math.floor(Date.now() / 1000),
tags: [],
content: 'relay-test',
sig: 'e'.repeat(128),
};
}
describe('relay fanout quorum integration', { concurrency: false }, () => {
let fixture = null;
beforeEach(() => {
_resetRelayHealthState();
});
afterEach(async () => {
if (fixture) {
await fixture.stop();
fixture = null;
}
});
it('passes with required=1 when one relay succeeds and two fail', async () => {
fixture = await createRelaySimulatorFixture([
{ type: 'success' },
{ type: 'timeout' },
{ type: 'timeout' },
]);
const telemetryLogs = [];
const result = await publishLock(fixture.relays, makeEvent('evt-required1-pass'), {
getPublishTimeoutMsFn: () => 80,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
assert.strictEqual(result.id, 'evt-required1-pass');
const telemetry = parseTelemetry(telemetryLogs);
const quorumSummary = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(quorumSummary);
assert.strictEqual(quorumSummary.successCount, 1);
assert.strictEqual(quorumSummary.requiredSuccesses, 1);
assert.strictEqual(quorumSummary.relayAttemptedCount, 3);
});
it('fails with quorum error for required=1 when all relays fail', async () => {
fixture = await createRelaySimulatorFixture([
{ type: 'timeout' },
{ type: 'timeout' },
{ type: 'timeout' },
]);
const telemetryLogs = [];
await assert.rejects(
() => publishLock(fixture.relays, makeEvent('evt-required1-all-fail'), {
getPublishTimeoutMsFn: () => 80,
getMinSuccessfulRelayPublishesFn: () => 1,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_quorum_failure/,
);
const telemetry = parseTelemetry(telemetryLogs);
const relayOutcomes = telemetry.filter((entry) => entry.event === 'lock_publish_failure');
assert.strictEqual(relayOutcomes.length, 3);
assert.ok(relayOutcomes.every((entry) => fixture.relays.includes(entry.relayUrl)));
const quorumSummary = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(quorumSummary);
assert.strictEqual(quorumSummary.successCount, 0);
assert.strictEqual(quorumSummary.requiredSuccesses, 1);
assert.strictEqual(quorumSummary.relayAttemptedCount, 3);
});
it('fails with required=2 when only one relay succeeds', async () => {
fixture = await createRelaySimulatorFixture([
{ type: 'success' },
{ type: 'timeout' },
{ type: 'timeout' },
]);
const telemetryLogs = [];
await assert.rejects(
() => publishLock(fixture.relays, makeEvent('evt-required2-fail'), {
getPublishTimeoutMsFn: () => 80,
getMinSuccessfulRelayPublishesFn: () => 2,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
}),
/error_category=relay_publish_quorum_failure/,
);
const telemetry = parseTelemetry(telemetryLogs);
const relayOutcomes = telemetry.filter((entry) => entry.event === 'lock_publish_failure');
assert.strictEqual(relayOutcomes.length, 2);
const quorumSummary = telemetry.find((entry) => entry.event === 'lock_publish_quorum_failed');
assert.ok(quorumSummary);
assert.strictEqual(quorumSummary.successCount, 1);
assert.strictEqual(quorumSummary.requiredSuccesses, 2);
assert.strictEqual(quorumSummary.relayAttemptedCount, 3);
});
it('passes when delayed successes reach quorum before timeout', async () => {
fixture = await createRelaySimulatorFixture([
{ type: 'success', delayMs: 5 },
{ type: 'success', delayMs: 25 },
{ type: 'timeout' },
]);
const telemetryLogs = [];
const startedAt = Date.now();
const result = await publishLock(fixture.relays, makeEvent('evt-delayed-quorum'), {
getPublishTimeoutMsFn: () => 80,
getMinSuccessfulRelayPublishesFn: () => 2,
getRelayFallbacksFn: () => [],
getMinActiveRelayPoolFn: () => 1,
retryAttempts: 1,
telemetryLogger: (line) => telemetryLogs.push(line),
healthLogger: () => {},
});
const elapsedMs = Date.now() - startedAt;
assert.strictEqual(result.id, 'evt-delayed-quorum');
assert.ok(elapsedMs < 160);
const telemetry = parseTelemetry(telemetryLogs);
const quorumSummary = telemetry.find((entry) => entry.event === 'lock_publish_quorum_met');
assert.ok(quorumSummary);
assert.strictEqual(quorumSummary.successCount, 2);
assert.strictEqual(quorumSummary.requiredSuccesses, 2);
assert.strictEqual(quorumSummary.relayAttemptedCount, 3);
});
});
-143
View File
@@ -1,143 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { RelayHealthManager } from '../src/lock-ops.mjs';
describe('RelayHealthManager', () => {
it('should isolate state between instances', () => {
const manager1 = new RelayHealthManager();
const manager2 = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 3,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
};
manager1.recordOutcome('wss://relay1', true, null, 100, config);
// Manager 1 should have metrics
const metrics1 = manager1.ensureMetrics('wss://relay1', config);
assert.strictEqual(metrics1.recentOutcomes.length, 1);
// Manager 2 should have empty metrics for relay1
const metrics2 = manager2.ensureMetrics('wss://relay1', config);
assert.strictEqual(metrics2.recentOutcomes.length, 0);
});
it('should prioritize relays based on health', () => {
const manager = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 2,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
minActiveRelayPool: 1,
};
// Relay 1: Healthy
manager.recordOutcome('wss://healthy', true, null, 100, config);
// Relay 2: Failing
manager.recordOutcome('wss://failing', false, 'error', 100, config);
manager.recordOutcome('wss://failing', false, 'error', 100, config); // Quarantined
const result = manager.prioritizeRelays(['wss://healthy', 'wss://failing'], config);
assert.deepStrictEqual(result.prioritized, ['wss://healthy']);
// Check ranking
const healthyRank = result.ranked.find(r => r.relay === 'wss://healthy');
const failingRank = result.ranked.find(r => r.relay === 'wss://failing');
assert.ok(healthyRank.score > failingRank.score);
assert.strictEqual(failingRank.summary.quarantined, true);
});
it('should reset state correctly', () => {
const manager = new RelayHealthManager();
const config = {
rollingWindowSize: 5,
failureThreshold: 3,
quarantineCooldownMs: 1000,
maxQuarantineCooldownMs: 5000,
};
manager.recordOutcome('wss://relay1', true, null, 100, config);
assert.strictEqual(manager.metricsByRelay.size, 1);
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);
});
});
-171
View File
@@ -1,171 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { evaluateAlertThresholds } from '../src/relay-health.mjs';
function entry(timestamp, healthyRelays, totalRelays = 3) {
return {
timestamp,
summary: {
healthyRelays,
totalRelays,
allRelaysUnhealthy: healthyRelays === 0,
},
};
}
test('evaluateAlertThresholds', async (t) => {
const now = Date.parse('2026-02-15T12:00:00Z');
const minute = 60_000;
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];
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);
});
});
-133
View File
@@ -1,133 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { summarizeHistory } from '../src/relay-health.mjs';
function createEntry(timestamp, healthyRelays, totalRelays = 3) {
return {
timestamp,
summary: {
healthyRelays,
totalRelays,
},
};
}
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
];
const result = summarizeHistory(history, { nowMs: now, windowMinutes: 60 });
assert.equal(result.sampleCount, 3);
// Total probes: 9, Total healthy: 4. Rate: 4/9
assert.equal(result.successRate, 4 / 9);
// 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);
});
-157
View File
@@ -1,157 +0,0 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import { getRoster, _resetRosterCache, _setRosterDependencies, _restoreRosterDependencies } from '../src/roster.mjs';
// Mock dependencies
const mockFs = {
existsSync: mock.fn(),
readFileSync: mock.fn(),
};
const mockLoadTorchConfig = mock.fn();
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');
// Mock console.error to suppress warnings
const originalConsoleError = console.error;
let mockConsoleError;
beforeEach(() => {
mockConsoleError = mock.fn();
console.error = mockConsoleError;
_resetRosterCache();
mockFs.existsSync.mock.resetCalls();
mockFs.readFileSync.mock.resetCalls();
mockLoadTorchConfig.mock.resetCalls();
// Default: config returns empty nostrLock
mockLoadTorchConfig.mock.mockImplementation(async () => ({ nostrLock: {} }));
// Default: fs.existsSync returns false (no files found)
mockFs.existsSync.mock.mockImplementation(() => false);
// Default: fs.readFileSync returns empty object
mockFs.readFileSync.mock.mockImplementation(() => '{}');
// Inject mocks
_setRosterDependencies({
fs: mockFs,
loadTorchConfig: mockLoadTorchConfig
});
// Clear env vars
delete process.env.NOSTR_LOCK_DAILY_ROSTER;
delete process.env.NOSTR_LOCK_WEEKLY_ROSTER;
});
afterEach(() => {
_restoreRosterDependencies();
console.error = originalConsoleError;
mock.reset();
});
it('returns fallback roster when no configuration or files exist', async () => {
const roster = await getRoster('daily');
assert.ok(Array.isArray(roster));
assert.ok(roster.length > 0);
assert.ok(roster.includes('audit-agent')); // verification of fallback content
});
it('loads internal module roster if user/cwd rosters are missing', async () => {
// User/CWD missing
mockFs.existsSync.mock.mockImplementation(() => false);
// When reading file, return internal roster content
mockFs.readFileSync.mock.mockImplementation(() => {
return JSON.stringify({ daily: ['internal-daily'], weekly: ['internal-weekly'] });
});
const daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['internal-daily']);
});
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 daily = await getRoster('daily');
const weekly = await getRoster('weekly');
assert.deepStrictEqual(daily, ['user-agent-daily']);
assert.deepStrictEqual(weekly, ['user-agent-weekly']);
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1); // Should be cached after first call
});
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 daily = await getRoster('daily');
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 }');
// Should catch error and return fallback
const daily = await getRoster('daily');
assert.ok(daily.includes('audit-agent'));
// Should have tried to read file
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1);
});
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 daily = await getRoster('daily');
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 daily = await getRoster('daily');
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 daily = await getRoster('daily');
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 daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['agent1', 'agent2', 'agent3']);
});
});
@@ -1,723 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const SOURCE_SCRIPT = path.resolve('scripts/agent/run-scheduler-cycle.mjs');
const SNAPSHOT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '__snapshots__');
async function runNode(scriptPath, args, { cwd, env }) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath, ...args], {
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
function extractDetailLine(markdownBody) {
const line = String(markdownBody)
.split(/\r?\n/)
.find((entry) => entry.startsWith('- detail: '));
return line ? line.slice('- detail: '.length) : '';
}
function normalizePromptFileDetail(detailLine) {
return String(detailLine)
.replace(/at\s+.*?agent-a\.md:/, 'at <PROMPT_PATH>:')
.replace(/in\s+.*?agent-a\.md\./, 'in <PROMPT_PATH>.')
.replace(/'[^']*agent-a\.md'/g, "'<PROMPT_PATH>'");
}
async function assertSnapshot(snapshotName, actual) {
const expected = await fs.readFile(path.join(SNAPSHOT_DIR, snapshotName), 'utf8');
assert.equal(actual, expected);
}
async function setupFixture({
memoryPolicy,
lockShellBody = '',
roster = ['agent-a'],
lockHealthPreflight = undefined,
schedulerPolicy = {},
preflightScript = '#!/usr/bin/env node\nconsole.log(JSON.stringify({ ok: true, relays: ["wss://relay.test"] }));\nprocess.exit(0);\n',
}) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-memory-policy-'));
const scriptsDir = path.join(root, 'scripts', 'agent');
const binDir = path.join(root, 'bin');
await fs.mkdir(path.join(root, 'src', 'prompts'), { recursive: true });
await fs.mkdir(path.join(root, 'src', 'prompts', 'daily'), { recursive: true });
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(root, 'src', 'utils.mjs'));
await fs.mkdir(scriptsDir, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
try {
await fs.symlink(path.resolve('node_modules'), path.join(root, 'node_modules'));
} catch (err) {
// Ignore if symlink fails (e.g. if node_modules doesn't exist in test env, though it should)
console.error('Warning: Failed to symlink node_modules:', err.message);
}
await fs.copyFile(SOURCE_SCRIPT, path.join(scriptsDir, 'run-scheduler-cycle.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-utils.mjs'), path.join(scriptsDir, 'scheduler-utils.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-lock.mjs'), path.join(scriptsDir, 'scheduler-lock.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-config.mjs'), path.join(scriptsDir, 'scheduler-config.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-logger.mjs'), path.join(scriptsDir, 'scheduler-logger.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-state.mjs'), path.join(scriptsDir, 'scheduler-state.mjs'));
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(root, 'src', 'utils.mjs'));
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(root, 'src', 'torch-config.mjs'));
await fs.copyFile(path.resolve('src/constants.mjs'), path.join(root, 'src', 'constants.mjs'));
await fs.copyFile(path.resolve('src/relay-health.mjs'), path.join(root, 'src', 'relay-health.mjs'));
await fs.copyFile(path.resolve('src/lock-ops.mjs'), path.join(root, 'src', 'lock-ops.mjs'));
await fs.writeFile(
path.join(scriptsDir, 'verify-run-artifacts.mjs'),
'#!/usr/bin/env node\nprocess.exit(0);\n',
'utf8',
);
await fs.writeFile(path.join(scriptsDir, 'check-relay-health.mjs'), preflightScript, 'utf8');
await fs.writeFile(path.join(root, 'src', 'prompts', 'roster.json'), JSON.stringify({ daily: roster }, null, 2));
for (const agent of roster) {
await fs.writeFile(path.join(root, 'src', 'prompts', 'daily', `${agent}.md`), `# ${agent}\n`, 'utf8');
}
await fs.writeFile(
path.join(root, 'torch-config.json'),
JSON.stringify({
scheduler: {
firstPromptByCadence: { daily: 'agent-a' },
handoffCommandByCadence: { daily: 'echo HANDOFF_OK' },
validationCommandsByCadence: { daily: ['true'] },
memoryPolicyByCadence: { daily: memoryPolicy },
...schedulerPolicy,
...(lockHealthPreflight === undefined ? {} : { lockHealthPreflight }),
},
}, null, 2),
'utf8',
);
await fs.writeFile(
path.join(binDir, 'npm'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" != "run" ]]; then
exit 1
fi
case "$2" in
lock:check:daily)
echo '{"excluded":[]}'
;;
lock:health)
shift 2
if [[ "\${1:-}" == "--" ]]; then
shift
fi
node scripts/agent/check-relay-health.mjs "$@"
;;
lock:lock)
${lockShellBody || ' :'}
;;
lock:complete|lint)
;;
*)
;;
esac
`,
'utf8',
);
await fs.chmod(path.join(binDir, 'npm'), 0o755);
return {
root,
scriptPath: path.join(scriptsDir, 'run-scheduler-cycle.mjs'),
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
},
};
}
test('fails required memory policy when retrieval/storage evidence is missing', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: {
mode: 'required',
retrieveCommand: 'echo RETRIEVE_WITHOUT_MARKER',
storeCommand: 'echo STORE_WITHOUT_MARKER',
retrieveSuccessMarkers: ['MEMORY_RETRIEVED'],
storeSuccessMarkers: ['MEMORY_STORED'],
retrieveArtifacts: [],
storeArtifacts: [],
},
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 1, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /Required memory steps not verified/);
});
test('accepts required memory policy when markers or artifacts are produced', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: {
mode: 'required',
retrieveCommand: 'echo MEMORY_RETRIEVED && mkdir -p .scheduler-memory && : > .scheduler-memory/retrieve-daily.ok',
storeCommand: 'echo MEMORY_STORED && mkdir -p .scheduler-memory && : > .scheduler-memory/store-daily.ok',
retrieveSuccessMarkers: ['MEMORY_RETRIEVED'],
storeSuccessMarkers: ['MEMORY_STORED'],
retrieveArtifacts: ['.scheduler-memory/retrieve-daily.ok'],
storeArtifacts: ['.scheduler-memory/store-daily.ok'],
},
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: { ...fixture.env, AGENT_PLATFORM: 'codex' },
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const completedLog = logs.find((name) => name.endsWith('__completed.md'));
assert.ok(completedLog);
const completedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', completedLog), 'utf8');
assert.match(completedBody, /platform: 'codex'/);
await fs.access(path.join(fixture.root, '.scheduler-memory', 'retrieve-daily.ok'));
await fs.access(path.join(fixture.root, '.scheduler-memory', 'store-daily.ok'));
});
test('host-mode scheduler resolves parent config and rewrites torch-prefixed node commands', { concurrency: false }, async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-host-mode-'));
const torchDir = path.join(root, 'torch');
const scriptsDir = path.join(torchDir, 'scripts', 'agent');
const binDir = path.join(root, 'bin');
await fs.mkdir(path.join(torchDir, 'src', 'prompts', 'daily'), { recursive: true });
await fs.mkdir(path.join(torchDir, 'scripts', 'memory'), { recursive: true });
await fs.mkdir(scriptsDir, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
try {
await fs.symlink(path.resolve('node_modules'), path.join(torchDir, 'node_modules'));
} catch {
// no-op
}
await fs.copyFile(SOURCE_SCRIPT, path.join(scriptsDir, 'run-scheduler-cycle.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-utils.mjs'), path.join(scriptsDir, 'scheduler-utils.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-lock.mjs'), path.join(scriptsDir, 'scheduler-lock.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-config.mjs'), path.join(scriptsDir, 'scheduler-config.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-logger.mjs'), path.join(scriptsDir, 'scheduler-logger.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-state.mjs'), path.join(scriptsDir, 'scheduler-state.mjs'));
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(torchDir, 'src', 'utils.mjs'));
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(torchDir, 'src', 'torch-config.mjs'));
await fs.copyFile(path.resolve('src/constants.mjs'), path.join(torchDir, 'src', 'constants.mjs'));
await fs.copyFile(path.resolve('src/relay-health.mjs'), path.join(torchDir, 'src', 'relay-health.mjs'));
await fs.copyFile(path.resolve('src/lock-ops.mjs'), path.join(torchDir, 'src', 'lock-ops.mjs'));
await fs.writeFile(path.join(torchDir, 'scripts', 'agent', 'verify-run-artifacts.mjs'), '#!/usr/bin/env node\nprocess.exit(0);\n', 'utf8');
await fs.writeFile(path.join(torchDir, 'src', 'prompts', 'roster.json'), JSON.stringify({ daily: ['agent-a'] }, null, 2), 'utf8');
await fs.writeFile(path.join(torchDir, 'src', 'prompts', 'daily', 'agent-a.md'), '# agent-a\n', 'utf8');
await fs.writeFile(
path.join(torchDir, 'scripts', 'memory', 'retrieve.mjs'),
'import fs from "node:fs/promises"; await fs.mkdir(".scheduler-memory", { recursive: true }); await fs.writeFile(".scheduler-memory/retrieve-daily.ok", "ok\\n", "utf8"); console.log("MEMORY_RETRIEVED");\n',
'utf8',
);
await fs.writeFile(
path.join(torchDir, 'scripts', 'memory', 'store.mjs'),
'import fs from "node:fs/promises"; await fs.mkdir(".scheduler-memory", { recursive: true }); await fs.writeFile(".scheduler-memory/store-daily.ok", "ok\\n", "utf8"); console.log("MEMORY_STORED");\n',
'utf8',
);
await fs.writeFile(
path.join(torchDir, 'scripts', 'agent', 'run-selected-prompt.mjs'),
'#!/usr/bin/env node\nconsole.log("HANDOFF_OK");\n',
'utf8',
);
await fs.writeFile(
path.join(root, 'torch-config.json'),
JSON.stringify({
scheduler: {
firstPromptByCadence: { daily: 'agent-a' },
handoffCommandByCadence: { daily: 'node torch/scripts/agent/run-selected-prompt.mjs' },
validationCommandsByCadence: { daily: ['true'] },
memoryPolicyByCadence: {
daily: {
mode: 'required',
retrieveCommand: 'node torch/scripts/memory/retrieve.mjs',
storeCommand: 'node torch/scripts/memory/store.mjs',
retrieveSuccessMarkers: ['MEMORY_RETRIEVED'],
storeSuccessMarkers: ['MEMORY_STORED'],
retrieveArtifacts: ['.scheduler-memory/retrieve-daily.ok'],
storeArtifacts: ['.scheduler-memory/store-daily.ok'],
},
},
},
}, null, 2),
'utf8',
);
await fs.writeFile(
path.join(binDir, 'npm'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" != "run" ]]; then
exit 1
fi
case "$2" in
lock:check:daily)
echo '{"excluded":[]}'
;;
lock:lock|lock:complete|lint)
;;
*)
;;
esac
`,
'utf8',
);
await fs.chmod(path.join(binDir, 'npm'), 0o755);
const result = await runNode(path.join(scriptsDir, 'run-scheduler-cycle.mjs'), ['--cadence', 'daily'], {
cwd: torchDir,
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
},
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
assert.match(result.stdout, /Status:\s+completed/);
await fs.access(path.join(torchDir, '.scheduler-memory', 'retrieve-daily.ok'));
await fs.access(path.join(torchDir, '.scheduler-memory', 'store-daily.ok'));
});
test('records backend failure metadata when lock command exits with code 2', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
lockShellBody: ` echo 'publish failed to all relays token=abc123 SECRET_KEY=xyz987' >&2
echo 'websocket: connection refused' >&1
exit 2`,
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: { ...fixture.env, AGENT_PLATFORM: 'codex' },
});
assert.equal(result.code, 2, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /reason: Lock backend error/);
assert.match(failedBody, /failure_class: 'backend_unavailable'/);
assert.match(failedBody, /failure_category: 'lock_backend_error'/);
assert.match(failedBody, /backend_category: 'relay_publish_quorum_failure'/);
assert.match(failedBody, /lock_command: 'AGENT_PLATFORM=codex npm run lock:lock -- --agent agent-a --cadence daily'/);
assert.match(failedBody, /lock_stderr_excerpt: 'publish failed to all relays token=\[REDACTED\] SECRET_KEY=\[REDACTED\]'/);
assert.match(failedBody, /lock_stdout_excerpt: 'websocket: connection refused'/);
assert.match(failedBody, /lock_failure_reason_distribution: '\{\}'/);
assert.match(failedBody, /lock_attempt_id: '3'/);
assert.match(failedBody, /lock_correlation_id: 'daily:agent-a:/);
assert.match(failedBody, /Likely backend\/relay connectivity issue during lock acquisition\./);
assert.match(failedBody, /Retry command: npm run scheduler:daily/);
assert.match(failedBody, /Recommended auto-remediation:/);
assert.match(failedBody, /Run health check: npm run lock:health -- --cadence daily/);
assert.match(failedBody, /Review incident runbook: docs\/agent-handoffs\/learnings\/2026-02-15-relay-health-preflight-job.md/);
assert.match(failedBody, /Prompt not executed\./);
assert.match(failedBody, /platform: 'codex'/);
const detail = extractDetailLine(failedBody);
await assertSnapshot('lock-backend-error-detail.snapshot.txt', `${detail}\n`);
});
test('defers backend failures in non-strict mode and reuses idempotency key on successful retry', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
schedulerPolicy: {
strict_lock: false,
degraded_lock_retry_window: 3600000,
max_deferrals: 3,
},
lockShellBody: ` count_file="$PWD/.lock-count"
key_log="$PWD/.idempotency-keys"
count=0
if [[ -f "$count_file" ]]; then
count="$(cat "$count_file")"
fi
count=$((count + 1))
echo "$count" > "$count_file"
echo "\${SCHEDULER_LOCK_IDEMPOTENCY_KEY:-missing}" >> "$key_log"
if [[ "$count" -lt 3 ]]; then
echo 'relay query timeout' >&2
exit 2
fi
:`,
});
const first = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(first.code, 0, `stdout: ${first.stdout}\nstderr: ${first.stderr}`);
const second = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(second.code, 0, `stdout: ${second.stdout}\nstderr: ${second.stderr}`);
const third = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(third.code, 0, `stdout: ${third.stdout}\nstderr: ${third.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const deferredLog = logs.find((name) => name.endsWith('__deferred.md'));
assert.ok(deferredLog);
const deferredBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', deferredLog), 'utf8');
assert.match(deferredBody, /failure_class: 'backend_unavailable'/);
assert.match(deferredBody, /failure_category: 'lock_backend_error'/);
assert.match(deferredBody, /deferral_attempt_count: '\d+'/);
const keyLog = await fs.readFile(path.join(fixture.root, '.idempotency-keys'), 'utf8');
const keys = keyLog.trim().split(/\r?\n/).filter(Boolean);
assert.equal(keys.length, 3);
assert.equal(keys[0], 'missing');
assert.notEqual(keys[1], 'missing');
assert.equal(keys[1], keys[2]);
});
test('fails after exceeding non-strict deferral budget', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
schedulerPolicy: {
strict_lock: false,
degraded_lock_retry_window: 3600000,
max_deferrals: 1,
},
lockShellBody: ` echo 'publish failed to all relays' >&2
exit 2`,
});
const first = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(first.code, 0, `stdout: ${first.stdout}\nstderr: ${first.stderr}`);
const second = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(second.code, 2, `stdout: ${second.stdout}\nstderr: ${second.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /failure_class: 'backend_unavailable'/);
assert.match(failedBody, /failure_category: 'lock_backend_error'/);
assert.match(failedBody, /deferral_attempt_count: '2'/);
});
test('strict lock mode fails immediately without deferral', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
schedulerPolicy: {
strict_lock: true,
degraded_lock_retry_window: 3600000,
max_deferrals: 5,
},
lockShellBody: ` echo 'relay timeout' >&2
exit 2`,
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(result.code, 2, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
assert.equal(logs.some((name) => name.endsWith('__deferred.md')), false);
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
});
test('skips lock health preflight by default when not enabled', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
preflightScript: '#!/usr/bin/env node\nconsole.error("preflight should not run");\nprocess.exit(9);\n',
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
assert.ok(logs.some((name) => name.endsWith('__completed.md')));
});
test('fails scheduler with preflight metadata when lock health preflight is enabled', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
lockHealthPreflight: true,
preflightScript: `#!/usr/bin/env node
console.log(JSON.stringify({
ok: false,
relays: ['wss://relay-1.test', 'wss://relay-2.test'],
failureCategory: 'relay query timeout',
error: 'Relay query timed out'
}));
process.exit(2);
`,
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 2, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /reason: Lock backend unavailable preflight/);
assert.match(failedBody, /prompt not executed/i);
assert.match(failedBody, /failure_category: 'lock_backend_error'/);
assert.match(failedBody, /preflight_failure_category: 'relay query timeout'/);
assert.match(failedBody, /relay_list: 'wss:\/\/relay-1.test, wss:\/\/relay-2.test'/);
});
test('categorizes unreadable prompt files as prompt_parse_error', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
});
await fs.unlink(path.join(fixture.root, 'src', 'prompts', 'daily', 'agent-a.md'));
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 1, `stdout: ${result.stdout}
stderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /reason: Prompt file parse\/read failed/);
assert.match(failedBody, /failure_category: 'prompt_parse_error'/);
assert.doesNotMatch(failedBody, /Recommended auto-remediation|Run health check: npm run lock:health|Retry command: npm run scheduler:/);
const detail = normalizePromptFileDetail(extractDetailLine(failedBody));
await assertSnapshot('prompt-parse-error-detail.snapshot.txt', `${detail}\n`);
});
test('categorizes invalid prompt schema as prompt_schema_error', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
});
await fs.writeFile(path.join(fixture.root, 'src', 'prompts', 'daily', 'agent-a.md'), 'plain text without heading\n', 'utf8');
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 1, `stdout: ${result.stdout}
stderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /reason: Prompt file schema validation failed/);
assert.match(failedBody, /failure_category: 'prompt_schema_error'/);
});
test('categorizes handoff runtime failures as execution_error', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
schedulerPolicy: {
handoffCommandByCadence: { daily: 'exit 7' },
},
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 7, `stdout: ${result.stdout}
stderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /reason: Prompt\/handoff execution failed/);
assert.match(failedBody, /failure_category: 'execution_error'/);
});
test('retries lock acquisition for backend error and succeeds on a later attempt', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
lockShellBody: ` count_file="$PWD/.lock-count"
count=0
if [[ -f "$count_file" ]]; then
count="$(cat "$count_file")"
fi
count=$((count + 1))
echo "$count" > "$count_file"
if [[ "$count" -lt 2 ]]; then
echo 'relay query timeout' >&2
exit 2
fi
:`,
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '2',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
assert.match(result.stdout, /"event":"scheduler.lock.retry"/);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
assert.equal(logs.filter((name) => name.endsWith('__failed.md')).length, 0);
});
test('stops retrying after configured lock backend retries are exhausted', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
lockShellBody: ` echo 'publish failed to all relays' >&2
exit 2`,
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '1',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(result.code, 2, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily'));
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog);
const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8');
assert.match(failedBody, /lock_attempts_total: '2'/);
assert.match(failedBody, /lock_backoff_schedule_ms: '0'/);
assert.match(failedBody, /backend_category: 'relay_publish_quorum_failure'/);
});
test('does not use backend retry flow for exit code 3 lock conflicts', { concurrency: false }, async () => {
const fixture = await setupFixture({
memoryPolicy: { mode: 'optional' },
lockShellBody: ` count_file="$PWD/.lock-count"
count=0
if [[ -f "$count_file" ]]; then
count="$(cat "$count_file")"
fi
count=$((count + 1))
echo "$count" > "$count_file"
if [[ "$count" -eq 1 ]]; then
exit 3
fi
:`,
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '3',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
assert.doesNotMatch(result.stdout, /"event":"scheduler.lock.retry"/);
});
-71
View File
@@ -1,71 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
const SCRIPT_PATH = path.resolve('scripts/agent/run-selected-prompt.mjs');
function runScript({ cwd, env }) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [SCRIPT_PATH], {
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
test('runs selected scheduler prompt through configured runner command', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'run-selected-prompt-'));
const promptPath = path.join(root, 'prompt.md');
await fs.writeFile(promptPath, '# hello from prompt\n', 'utf8');
const result = await runScript({
cwd: root,
env: {
...process.env,
SCHEDULER_PROMPT_PATH: promptPath,
SCHEDULER_AGENT: 'agent-a',
SCHEDULER_CADENCE: 'daily',
SCHEDULER_AGENT_RUNNER_COMMAND: 'node -e "if(!process.env.SCHEDULER_PROMPT_MARKDOWN.includes(\'hello from prompt\')) process.exit(6)"',
},
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
});
test('propagates non-zero runner exit code', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'run-selected-prompt-fail-'));
const promptPath = path.join(root, 'prompt.md');
await fs.writeFile(promptPath, '# will fail\n', 'utf8');
const result = await runScript({
cwd: root,
env: {
...process.env,
SCHEDULER_PROMPT_PATH: promptPath,
SCHEDULER_AGENT: 'agent-b',
SCHEDULER_CADENCE: 'weekly',
SCHEDULER_AGENT_RUNNER_COMMAND: 'node -e "process.exit(7)"',
},
});
assert.equal(result.code, 7, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
});
@@ -1,218 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
const SOURCE_SCRIPT = path.resolve('scripts/agent/run-scheduler-cycle.mjs');
const REQUIRED_LOCK_FAILURE_FIELDS = [
'status',
'reason',
'backend_category',
'lock_command',
'lock_stderr_excerpt',
'lock_stdout_excerpt',
'detail',
];
const BACKEND_CATEGORY_ENUM = new Set([
'relay_query_timeout',
'relay_publish_quorum_failure',
'websocket_connection_refused_or_dns',
'malformed_relay_url_config',
'unknown_backend_error',
]);
async function runNode(scriptPath, args, { cwd, env }) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath, ...args], {
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
function parseFrontmatter(markdown) {
const lines = String(markdown).split(/\r?\n/);
if (lines[0] !== '---') return {};
const result = {};
for (let i = 1; i < lines.length; i += 1) {
const line = lines[i];
if (line === '---') break;
const sep = line.indexOf(':');
if (sep < 0) continue;
const key = line.slice(0, sep).trim();
const raw = line.slice(sep + 1).trim();
result[key] = raw.startsWith("'") && raw.endsWith("'")
? raw.slice(1, -1).replace(/''/g, "'")
: raw;
}
return result;
}
function validateLockBackendFailureFrontmatter(frontmatter) {
for (const key of REQUIRED_LOCK_FAILURE_FIELDS) {
assert.equal(typeof frontmatter[key], 'string', `expected ${key} to be a string`);
assert.ok(frontmatter[key].trim().length > 0, `expected ${key} to be non-empty`);
}
assert.equal(frontmatter.status, 'failed', 'expected status=failed for lock-backend failure artifact');
assert.ok(
BACKEND_CATEGORY_ENUM.has(frontmatter.backend_category),
`expected backend_category to be canonical enum, received: ${frontmatter.backend_category}`,
);
}
async function setupFixture({ lockShellBody }) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-lock-schema-'));
const scriptsDir = path.join(root, 'scripts', 'agent');
const binDir = path.join(root, 'bin');
await fs.mkdir(path.join(root, 'src', 'prompts', 'daily'), { recursive: true });
await fs.mkdir(scriptsDir, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await fs.copyFile(SOURCE_SCRIPT, path.join(scriptsDir, 'run-scheduler-cycle.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-utils.mjs'), path.join(scriptsDir, 'scheduler-utils.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-lock.mjs'), path.join(scriptsDir, 'scheduler-lock.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-config.mjs'), path.join(scriptsDir, 'scheduler-config.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-logger.mjs'), path.join(scriptsDir, 'scheduler-logger.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-state.mjs'), path.join(scriptsDir, 'scheduler-state.mjs'));
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(root, 'src', 'utils.mjs'));
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(root, 'src', 'torch-config.mjs'));
await fs.copyFile(path.resolve('src/constants.mjs'), path.join(root, 'src', 'constants.mjs'));
await fs.writeFile(path.join(scriptsDir, 'verify-run-artifacts.mjs'), '#!/usr/bin/env node\nprocess.exit(0);\n', 'utf8');
await fs.writeFile(path.join(root, 'src', 'prompts', 'roster.json'), JSON.stringify({ daily: ['agent-a'] }, null, 2), 'utf8');
await fs.writeFile(path.join(root, 'src', 'prompts', 'daily', 'agent-a.md'), '# agent-a\n', 'utf8');
await fs.writeFile(
path.join(root, 'torch-config.json'),
JSON.stringify({
scheduler: {
strict_lock: true,
firstPromptByCadence: { daily: 'agent-a' },
handoffCommandByCadence: { daily: 'echo HANDOFF_OK' },
validationCommandsByCadence: { daily: ['true'] },
memoryPolicyByCadence: { daily: { mode: 'optional' } },
},
}, null, 2),
'utf8',
);
await fs.writeFile(
path.join(binDir, 'npm'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" != "run" ]]; then
exit 1
fi
case "$2" in
lock:check:daily)
echo '{"excluded":[]}'
;;
lock:lock)
${lockShellBody}
;;
lock:complete|lint)
;;
*)
;;
esac
`,
'utf8',
);
await fs.chmod(path.join(binDir, 'npm'), 0o755);
return {
root,
scriptPath: path.join(scriptsDir, 'run-scheduler-cycle.mjs'),
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
};
}
async function getFailedFrontmatter(root) {
const logDir = path.join(root, 'task-logs', 'daily');
const logs = await fs.readdir(logDir);
const failedLog = logs.find((name) => name.endsWith('__failed.md'));
assert.ok(failedLog, `expected __failed.md in ${logDir}`);
const content = await fs.readFile(path.join(logDir, failedLog), 'utf8');
return parseFrontmatter(content);
}
test('scheduler lock backend failure artifact matches required frontmatter schema', { concurrency: false }, async () => {
const fixture = await setupFixture({
lockShellBody: "echo 'lock_publish_quorum_failed error_category=relay_publish_quorum_failure publish failed to all relays' >&2\n echo 'relay publish failed' >&1\n exit 2",
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 2, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const frontmatter = await getFailedFrontmatter(fixture.root);
validateLockBackendFailureFrontmatter(frontmatter);
assert.equal(frontmatter.backend_category, 'relay_publish_quorum_failure');
});
test('schema contract catches missing required lock-backend field', () => {
const frontmatter = {
status: 'failed',
reason: 'Lock backend error',
backend_category: 'relay_publish_quorum_failure',
lock_command: 'npm run lock:lock -- --agent agent-a --cadence daily',
lock_stderr_excerpt: 'relay publish failed',
lock_stdout_excerpt: 'retrying',
detail: 'Prompt not executed.',
};
delete frontmatter.lock_stdout_excerpt;
assert.throws(
() => validateLockBackendFailureFrontmatter(frontmatter),
/expected lock_stdout_excerpt to be a string/,
);
});
test('schema contract catches misnamed lock-backend key', () => {
const frontmatter = {
status: 'failed',
reason: 'Lock backend error',
backend_category: 'relay_publish_quorum_failure',
lock_command: 'npm run lock:lock -- --agent agent-a --cadence daily',
lock_stderr_excerpt: 'relay publish failed',
lock_stdout_excerpt_misnamed: 'retrying',
detail: 'Prompt not executed.',
};
assert.throws(
() => validateLockBackendFailureFrontmatter(frontmatter),
/expected lock_stdout_excerpt to be a string/,
);
});
-242
View File
@@ -1,242 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { detectPlatform } from '../src/utils.mjs';
const SOURCE_SCRIPT = path.resolve('scripts/agent/run-scheduler-cycle.mjs');
async function runNode(scriptPath, args, { cwd, env }) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [scriptPath, ...args], {
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
async function setupFixture({ lockShellBody = ':', schedulerPolicy = {} }) {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-lock-e2e-'));
const scriptsDir = path.join(root, 'scripts', 'agent');
const binDir = path.join(root, 'bin');
await fs.mkdir(path.join(root, 'src', 'prompts', 'daily'), { recursive: true });
await fs.mkdir(scriptsDir, { recursive: true });
await fs.mkdir(binDir, { recursive: true });
await fs.copyFile(SOURCE_SCRIPT, path.join(scriptsDir, 'run-scheduler-cycle.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-utils.mjs'), path.join(scriptsDir, 'scheduler-utils.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-lock.mjs'), path.join(scriptsDir, 'scheduler-lock.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-config.mjs'), path.join(scriptsDir, 'scheduler-config.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-logger.mjs'), path.join(scriptsDir, 'scheduler-logger.mjs'));
await fs.copyFile(path.resolve('scripts/agent/scheduler-state.mjs'), path.join(scriptsDir, 'scheduler-state.mjs'));
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(root, 'src', 'utils.mjs'));
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(root, 'src', 'torch-config.mjs'));
await fs.copyFile(path.resolve('src/constants.mjs'), path.join(root, 'src', 'constants.mjs'));
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(root, 'src', 'torch-config.mjs'));
await fs.writeFile(
path.join(scriptsDir, 'verify-run-artifacts.mjs'),
'#!/usr/bin/env node\nprocess.exit(0);\n',
'utf8',
);
await fs.writeFile(path.join(root, 'src', 'prompts', 'roster.json'), JSON.stringify({ daily: ['agent-a'] }, null, 2));
await fs.writeFile(path.join(root, 'src', 'prompts', 'daily', 'agent-a.md'), '# agent-a\n', 'utf8');
await fs.writeFile(
path.join(root, 'torch-config.json'),
JSON.stringify({
scheduler: {
firstPromptByCadence: { daily: 'agent-a' },
handoffCommandByCadence: { daily: 'echo HANDOFF_OK' },
validationCommandsByCadence: { daily: ['true'] },
memoryPolicyByCadence: { daily: { mode: 'optional' } },
...schedulerPolicy,
},
}, null, 2),
'utf8',
);
await fs.writeFile(
path.join(binDir, 'npm'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" != "run" ]]; then
exit 1
fi
case "$2" in
lock:check:daily)
echo '{"excluded":[]}'
;;
lock:lock)
${lockShellBody}
;;
lock:complete|lint)
;;
*)
;;
esac
`,
'utf8',
);
await fs.chmod(path.join(binDir, 'npm'), 0o755);
return {
root,
scriptPath: path.join(scriptsDir, 'run-scheduler-cycle.mjs'),
env: {
...process.env,
PATH: `${binDir}:${process.env.PATH}`,
},
};
}
function parseFrontmatter(markdown) {
const lines = String(markdown).split(/\r?\n/);
if (lines[0] !== '---') return {};
const result = {};
for (let i = 1; i < lines.length; i += 1) {
const line = lines[i];
if (line === '---') break;
const sep = line.indexOf(':');
if (sep < 0) continue;
const key = line.slice(0, sep).trim();
const raw = line.slice(sep + 1).trim();
result[key] = raw.startsWith("'") && raw.endsWith("'")
? raw.slice(1, -1).replace(/''/g, "'")
: raw;
}
return result;
}
async function readLogByStatus(root, status) {
const logDir = path.join(root, 'task-logs', 'daily');
const logs = await fs.readdir(logDir);
const file = logs.find((name) => name.endsWith(`__${status}.md`));
assert.ok(file, `expected ${status} log in ${logDir}; saw: ${logs.join(', ')}`);
const content = await fs.readFile(path.join(logDir, file), 'utf8');
return { file, content, frontmatter: parseFrontmatter(content) };
}
test('lock preflight e2e: successful lock writes completed status snapshot', { concurrency: false }, async () => {
const fixture = await setupFixture({ lockShellBody: ':' });
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const { frontmatter } = await readLogByStatus(fixture.root, 'completed');
assert.deepEqual(
{
cadence: frontmatter.cadence,
agent: frontmatter.agent,
status: frontmatter.status,
platform: frontmatter.platform,
},
{
cadence: 'daily',
agent: 'agent-a',
status: 'completed',
platform: detectPlatform() || 'unknown',
},
);
});
test('lock preflight e2e: exit code 2 quorum failure persists failed backend status and prompt-not-started marker', { concurrency: false }, async () => {
const fixture = await setupFixture({
schedulerPolicy: {
strict_lock: true,
},
lockShellBody: "echo 'lock_publish_quorum_failed error_category=relay_publish_quorum_failure publish failed to all relays' >&2\n exit 2",
});
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: {
...fixture.env,
SCHEDULER_LOCK_MAX_RETRIES: '0',
SCHEDULER_LOCK_BACKOFF_MS: '0',
SCHEDULER_LOCK_JITTER_MS: '0',
},
});
assert.equal(result.code, 2, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const { content, frontmatter } = await readLogByStatus(fixture.root, 'failed');
assert.deepEqual(
{
status: frontmatter.status,
failure_category: frontmatter.failure_category,
failure_class: frontmatter.failure_class,
backend_category: frontmatter.backend_category,
},
{
status: 'failed',
failure_category: 'lock_backend_error',
failure_class: 'backend_unavailable',
backend_category: 'relay_publish_quorum_failure',
},
);
assert.match(content, /Prompt not executed\./i);
assert.doesNotMatch(content, /prompt_parse_error|prompt_schema_error/i);
const stateRaw = JSON.parse(await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', '.scheduler-run-state.json'), 'utf8'));
assert.deepEqual(
{
has_run_date: typeof stateRaw.run_date === 'string' && stateRaw.run_date.length > 0,
lock_deferral: stateRaw.lock_deferral,
},
{
has_run_date: true,
lock_deferral: null,
},
);
});
test('lock preflight e2e: non-lock failure exits failed without prompt parse/schema classification', { concurrency: false }, async () => {
const fixture = await setupFixture({ lockShellBody: "echo 'unexpected lock command crash' >&2\n exit 9" });
const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], {
cwd: fixture.root,
env: fixture.env,
});
assert.equal(result.code, 9, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
const { content, frontmatter } = await readLogByStatus(fixture.root, 'failed');
assert.deepEqual(
{
status: frontmatter.status,
reason_line: /- reason: (.+)/.exec(content)?.[1] ?? null,
failure_category: frontmatter.failure_category ?? null,
},
{
status: 'failed',
reason_line: 'Failed to acquire lock',
failure_category: null,
},
);
assert.doesNotMatch(content, /prompt_parse_error|prompt_schema_error/i);
});
-170
View File
@@ -1,170 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { cmdCheck } from '../src/lib.mjs';
describe('Scheduler Ratchet Logic (Log Checking)', () => {
const mockRelays = ['wss://relay.test'];
const mockNamespace = 'test-ns';
const mockTtl = 3600;
const mockRoster = ['agentA', 'agentB', 'agentC'];
const mockDateStr = '2023-10-27'; // A Friday
const mockIsoWeekStr = '2023-W43';
// Base dependencies for cmdCheck
const baseDeps = {
getRelays: () => mockRelays,
getNamespace: () => mockNamespace,
getTtl: () => mockTtl,
getRoster: () => mockRoster,
loadTorchConfig: () => ({ scheduler: { paused: { daily: [], weekly: [] } } }),
getDateStr: () => mockDateStr,
getIsoWeek: () => mockIsoWeekStr,
queryLocks: async () => [], // No active locks by default
log: () => {},
error: () => {},
// Default readdir mock: returns empty array
readdir: async () => [],
logDir: 'mock-logs',
};
it('excludes agents completed today (daily)', async () => {
const deps = {
...baseDeps,
readdir: async () => [
'2023-10-27T10-00-00Z__agentA__completed.md', // Matching date
'2023-10-26T10-00-00Z__agentB__completed.md', // Old date
'2023-10-27T12-00-00Z__agentC__failed.md', // Failed today
],
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.completed, ['agentA']);
assert.ok(result.excluded.includes('agentA'));
assert.ok(!result.excluded.includes('agentB')); // Completed yesterday, not excluded today
assert.ok(!result.excluded.includes('agentC')); // Failed, not excluded
assert.deepStrictEqual(result.available, ['agentB', 'agentC']);
});
it('excludes agents completed this week (weekly)', async () => {
const deps = {
...baseDeps,
getIsoWeek: (date) => {
if (!date) return mockIsoWeekStr;
// Mock simple week logic: 2023-10-23 to 2023-10-29 is W43
if (date >= '2023-10-23' && date <= '2023-10-29') return '2023-W43';
return '2023-W42';
},
readdir: async () => [
'2023-10-23T10-00-00Z__agentA__completed.md', // Monday (in week)
'2023-10-20T10-00-00Z__agentB__completed.md', // Last week
],
};
const result = await cmdCheck('weekly', deps);
assert.deepStrictEqual(result.completed, ['agentA']);
assert.ok(result.excluded.includes('agentA'));
assert.ok(!result.excluded.includes('agentB'));
assert.deepStrictEqual(result.available, ['agentB', 'agentC']);
});
it('combines locked and completed agents in exclusion list', async () => {
const deps = {
...baseDeps,
queryLocks: async () => [{ agent: 'agentB', eventId: 'ev1' }], // agentB is locked
readdir: async () => [
'2023-10-27T10-00-00Z__agentA__completed.md', // agentA is completed
],
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.completed, ['agentA']);
assert.deepStrictEqual(result.locked, ['agentB']);
// agentA (completed) and agentB (locked) should be excluded
assert.ok(result.excluded.includes('agentA'));
assert.ok(result.excluded.includes('agentB'));
assert.deepStrictEqual(result.available, ['agentC']);
});
it('ignores logs when --ignore-logs is set', async () => {
const deps = {
...baseDeps,
readdir: async () => [
'2023-10-27T10-00-00Z__agentA__completed.md',
],
ignoreLogs: true,
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.completed, []);
assert.ok(!result.excluded.includes('agentA'));
assert.deepStrictEqual(result.available, ['agentA', 'agentB', 'agentC']);
});
it('handles missing log directory gracefully', async () => {
const deps = {
...baseDeps,
readdir: async () => {
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
},
};
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.completed, []);
assert.deepStrictEqual(result.available, ['agentA', 'agentB', 'agentC']);
});
});
describe('Scheduler cycle ordering guarantees', () => {
it('keeps optional preflight before lock-acquire and preserves handoff/artifact/validation/complete ordering', async () => {
const { readFile } = await import('node:fs/promises');
const source = await readFile(new URL('../scripts/agent/run-scheduler-cycle.mjs', import.meta.url), 'utf8');
const order = [
"const preflight = await runLockHealthPreflight({ cadence, platform });",
"const checkResult = await runCommand('npm', ['run', `lock:check:${cadence}`",
"const lockAttempt = await acquireLockWithRetry({",
"if (schedulerConfig.handoffCommand)",
"const artifactCheck = await runCommand('node', [",
"for (const validation of schedulerConfig.validationCommands)",
"const completeResult = await runCommand(",
"await writeLog({ cadence, agent: selectedAgent, status: 'completed'",
];
let previousIndex = -1;
for (const token of order) {
const index = source.indexOf(token);
assert.ok(index >= 0, `Expected token not found: ${token}`);
assert.ok(index > previousIndex, `Token out of order: ${token}`);
previousIndex = index;
}
});
it('includes lock backend failure metadata and classifier checkpoints', async () => {
const { readFile } = await import('node:fs/promises');
const source = await readFile(new URL('../scripts/agent/run-scheduler-cycle.mjs', import.meta.url), 'utf8');
const lockSource = await readFile(new URL('../scripts/agent/scheduler-lock.mjs', import.meta.url), 'utf8');
const requiredSnippets = [
"backend_category: backendCategory",
"lock_command: lockCommand",
"lock_stderr_excerpt: stderrExcerpt || '(empty)'",
"lock_stdout_excerpt: stdoutExcerpt || '(empty)'",
"reason: 'Lock backend error'",
];
for (const snippet of requiredSnippets) {
assert.ok(source.includes(snippet), `Expected scheduler snippet not found: ${snippet}`);
}
assert.ok(lockSource.includes('function classifyLockBackendError(outputText)'), 'Expected classifyLockBackendError in scheduler-lock.mjs');
});
});
-310
View File
@@ -1,310 +0,0 @@
/**
* Unit tests for the scheduler time-window guard.
*
* Scenario-first, cheat-resistant (Dark Factory standard):
* - Every test asserts observable outcomes at the boundary of
* buildRecentlyRunExclusionSet (the Set it returns).
* - Determinism controls: `now` is injected so the clock is fixed.
* - No mocking of internal logic; real temp-directory I/O is used so
* the implementation cannot "return true" and pass.
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import {
buildRecentlyRunExclusionSet,
parseDateValue,
parseTimestampFromFilename,
parseAgentFromFilename,
isStrictSchedulerLogFilename,
parseFrontmatterCreatedAt,
parseFrontmatterAgent,
CADENCE_WINDOW_MS,
} from '../scripts/agent/scheduler-utils.mjs';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Create a task log file with optional YAML frontmatter. */
async function writeLog(dir, filename, { createdAt } = {}) {
const frontmatter = createdAt
? `---\nagent: test\ncreated_at: ${createdAt}\n---\n`
: '';
await fs.writeFile(path.join(dir, filename), `${frontmatter}# log\n`, 'utf8');
}
/** ISO string with colons replaced by dashes (canonical log filename format). */
function isoToFilename(isoString) {
return isoString.replace(/:/g, '-').replace(/\.\d+Z$/, 'Z');
}
/** Build a canonical log filename for a given agent/status/time. */
function logFilename(agent, status, isoString) {
return `${isoToFilename(isoString)}__${agent}__${status}.md`;
}
// Fixed reference point: 2026-02-20T12:00:00Z
const NOW = new Date('2026-02-20T12:00:00Z').getTime();
// ---------------------------------------------------------------------------
// Unit tests for helper utilities
// ---------------------------------------------------------------------------
describe('parseDateValue', () => {
it('parses a valid ISO string', () => {
assert.equal(parseDateValue('2026-02-20T12:00:00Z'), NOW);
});
it('returns null for empty input', () => {
assert.equal(parseDateValue(''), null);
assert.equal(parseDateValue(null), null);
assert.equal(parseDateValue(undefined), null);
});
it('returns null for invalid date strings', () => {
assert.equal(parseDateValue('not-a-date'), null);
});
});
describe('isStrictSchedulerLogFilename', () => {
it('accepts valid completed log', () => {
assert.ok(isStrictSchedulerLogFilename('2026-02-20T12-00-00Z__audit-agent__completed.md'));
});
it('accepts valid failed log', () => {
assert.ok(isStrictSchedulerLogFilename('2026-02-20T12-00-00Z__audit-agent__failed.md'));
});
it('rejects deferred logs', () => {
assert.ok(!isStrictSchedulerLogFilename('2026-02-20T12-00-00Z__audit-agent__deferred.md'));
});
it('rejects filenames without timestamp', () => {
assert.ok(!isStrictSchedulerLogFilename('audit-agent__completed.md'));
});
it('rejects plain text files', () => {
assert.ok(!isStrictSchedulerLogFilename('notes.md'));
});
it('rejects .scheduler-run-state.json', () => {
assert.ok(!isStrictSchedulerLogFilename('.scheduler-run-state.json'));
});
});
describe('parseAgentFromFilename', () => {
it('extracts agent name from completed log', () => {
assert.equal(parseAgentFromFilename('2026-02-20T12-00-00Z__audit-agent__completed.md'), 'audit-agent');
});
it('extracts agent name from failed log', () => {
assert.equal(parseAgentFromFilename('2026-02-20T12-00-00Z__ci-health-agent__failed.md'), 'ci-health-agent');
});
it('returns null for invalid filename', () => {
assert.equal(parseAgentFromFilename('bad-file.md'), null);
});
});
describe('parseTimestampFromFilename', () => {
it('parses timestamp from canonical filename', () => {
const filename = '2026-02-20T12-00-00Z__audit-agent__completed.md';
assert.equal(parseTimestampFromFilename(filename), NOW);
});
it('returns null for non-canonical filename', () => {
assert.equal(parseTimestampFromFilename('audit-agent__completed.md'), null);
});
});
describe('parseFrontmatterCreatedAt / parseFrontmatterAgent', () => {
const doc = `---\nagent: known-issues-agent\ncreated_at: 2026-02-20T12:00:00Z\nstatus: completed\n---\n# body\n`;
it('reads created_at from frontmatter', () => {
assert.equal(parseFrontmatterCreatedAt(doc), '2026-02-20T12:00:00Z');
});
it('reads agent from frontmatter', () => {
assert.equal(parseFrontmatterAgent(doc), 'known-issues-agent');
});
it('returns null when no frontmatter', () => {
assert.equal(parseFrontmatterCreatedAt('# no frontmatter'), null);
assert.equal(parseFrontmatterAgent('# no frontmatter'), null);
});
});
// ---------------------------------------------------------------------------
// Scenario tests for buildRecentlyRunExclusionSet
// ---------------------------------------------------------------------------
describe('buildRecentlyRunExclusionSet — scenarios', () => {
let tmpDir;
before(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-time-window-'));
});
after(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
// SCN-TWG-01
it('SCN-TWG-01: excludes agents with completed logs within 24-hour daily window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn01-'));
// Ran 1 hour ago — within the 24h window
const recentTs = new Date(NOW - 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('audit-agent', 'completed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('audit-agent'), 'audit-agent ran 1h ago should be excluded');
assert.equal(result.size, 1);
});
// SCN-TWG-02
it('SCN-TWG-02: does NOT exclude agents whose last run is outside the 24-hour window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn02-'));
// Ran 25 hours ago — outside the window
const oldTs = new Date(NOW - 25 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('audit-agent', 'completed', oldTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(!result.has('audit-agent'), 'agent outside window should not be excluded');
assert.equal(result.size, 0);
});
// SCN-TWG-03
it('SCN-TWG-03: excludes agents with failed logs within the window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn03-'));
const recentTs = new Date(NOW - 30 * 60 * 1000).toISOString(); // 30 min ago
await writeLog(dir, logFilename('ci-health-agent', 'failed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('ci-health-agent'), 'recently failed agent should be excluded');
});
// SCN-TWG-04
it('SCN-TWG-04: cross-midnight scenario — agent ran 10 min before midnight, checked 10 min after', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn04-'));
const tenMinutesAgo = new Date(NOW - 20 * 60 * 1000).toISOString(); // 20 min ago (simulates cross-midnight)
await writeLog(dir, logFilename('load-test-agent', 'completed', tenMinutesAgo));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('load-test-agent'), 'cross-midnight agent should still be excluded');
});
// SCN-TWG-05
it('SCN-TWG-05: uses frontmatter created_at over filename timestamp when both present', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn05-'));
// Filename timestamp = 26h ago (outside window), but frontmatter says 1h ago (inside window)
const oldTs = new Date(NOW - 26 * 60 * 60 * 1000).toISOString();
const recentFrontmatter = new Date(NOW - 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('docs-agent', 'completed', oldTs), { createdAt: recentFrontmatter });
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('docs-agent'), 'frontmatter created_at should take precedence over filename timestamp');
});
// SCN-TWG-06
it('SCN-TWG-06: weekly window uses 7-day threshold', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn06-'));
// 5 days ago — within 7-day window
const inWindowTs = new Date(NOW - 5 * 24 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('changelog-agent', 'completed', inWindowTs));
// 8 days ago — outside 7-day window
const outWindowTs = new Date(NOW - 8 * 24 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('dead-code-agent', 'completed', outWindowTs));
const result = await buildRecentlyRunExclusionSet(dir, 'weekly', NOW);
assert.ok(result.has('changelog-agent'), 'agent 5d ago should be excluded for weekly');
assert.ok(!result.has('dead-code-agent'), 'agent 8d ago should not be excluded for weekly');
});
// SCN-TWG-07
it('SCN-TWG-07: empty log directory returns empty set', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn07-'));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.equal(result.size, 0);
});
// SCN-TWG-08
it('SCN-TWG-08: non-existent directory returns empty set without throwing', async () => {
const nonExistent = path.join(tmpDir, 'does-not-exist-' + Date.now());
const result = await buildRecentlyRunExclusionSet(nonExistent, 'daily', NOW);
assert.equal(result.size, 0);
});
// SCN-TWG-09
it('SCN-TWG-09: only the most recent log matters — agent excluded if any log is within window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn09-'));
// Two logs: one old, one recent
const oldTs = new Date(NOW - 25 * 60 * 60 * 1000).toISOString();
const recentTs = new Date(NOW - 2 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('perf-agent', 'completed', oldTs));
await writeLog(dir, logFilename('perf-agent', 'failed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('perf-agent'), 'agent with any log within window should be excluded');
});
// SCN-TWG-10
it('SCN-TWG-10: mixed roster — correctly partitions in-window vs out-of-window agents', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn10-'));
const recentTs = new Date(NOW - 1 * 60 * 60 * 1000).toISOString(); // 1h ago
const oldTs = new Date(NOW - 30 * 60 * 60 * 1000).toISOString(); // 30h ago
await writeLog(dir, logFilename('audit-agent', 'completed', recentTs));
await writeLog(dir, logFilename('ci-health-agent', 'completed', oldTs));
await writeLog(dir, logFilename('log-fixer-agent', 'failed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('audit-agent'));
assert.ok(!result.has('ci-health-agent'));
assert.ok(result.has('log-fixer-agent'));
assert.equal(result.size, 2);
});
// SCN-TWG-11 — anti-cheat: verify the guard cannot be trivially bypassed
it('SCN-TWG-11: non-log files in directory are ignored', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn11-'));
// Write files that are NOT canonical log filenames
await fs.writeFile(path.join(dir, '.scheduler-run-state.json'), '{}', 'utf8');
await fs.writeFile(path.join(dir, 'README.md'), '# readme', 'utf8');
await fs.writeFile(path.join(dir, '2026-02-20T12-00-00Z__audit-agent__deferred.md'), 'deferred', 'utf8');
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.equal(result.size, 0, 'non-log files and deferred logs must not trigger exclusion');
});
});
describe('CADENCE_WINDOW_MS constants', () => {
it('daily window is exactly 24 hours', () => {
assert.equal(CADENCE_WINDOW_MS.daily, 24 * 60 * 60 * 1000);
});
it('weekly window is exactly 7 days', () => {
assert.equal(CADENCE_WINDOW_MS.weekly, 7 * 24 * 60 * 60 * 1000);
});
});
-21
View File
@@ -1,21 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { _secureRandom } from '../src/lock-ops.mjs';
describe('secureRandom', () => {
it('returns values in [0, 1)', () => {
for (let i = 0; i < 1000; i++) {
const val = _secureRandom();
assert.ok(val >= 0, `Value ${val} should be >= 0`);
assert.ok(val < 1, `Value ${val} should be < 1`);
}
});
it('returns varied values', () => {
const values = new Set();
for (let i = 0; i < 100; i++) {
values.add(_secureRandom());
}
assert.ok(values.size > 90, 'Values should be reasonably unique');
});
});
-74
View File
@@ -1,74 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
const SCRIPT = path.resolve('scripts/agent/summarize-lock-reliability.mjs');
function runNode(args, cwd) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [SCRIPT, ...args], { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
});
}
test('summarizes recent lock reliability by platform/cadence/backend/relay', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'lock-reliability-'));
await fs.mkdir(path.join(root, 'task-logs', 'daily'), { recursive: true });
await fs.mkdir(path.join(root, 'task-logs', 'weekly'), { recursive: true });
await fs.writeFile(path.join(root, 'task-logs', 'daily', '2026-02-14T00-00-00Z__agent-a__failed.md'), `---
cadence: daily
agent: agent-a
status: failed
platform: codex
backend_category: publish failed to all relays
relay_list: 'wss://relay-1.test, wss://relay-2.test'
---
# Scheduler failed
- reason: Lock backend error
`, 'utf8');
await fs.writeFile(path.join(root, 'task-logs', 'weekly', '2026-02-14T00-00-01Z__agent-b__failed.md'), `---
cadence: weekly
agent: agent-b
status: failed
platform: codex
preflight_failure_category: relay query timeout
relay_list: 'wss://relay-1.test'
---
# Scheduler failed
- reason: Lock backend unavailable preflight
`, 'utf8');
await fs.writeFile(path.join(root, 'task-logs', 'daily', '2026-02-14T00-00-02Z__agent-c__completed.md'), `---
cadence: daily
agent: agent-c
status: completed
platform: claude
---
# Scheduler completed
`, 'utf8');
const result = await runNode(['--since-days', '365', '--out-dir', 'artifacts/reliability'], root);
assert.equal(result.code, 0, `stdout=${result.stdout}\nstderr=${result.stderr}`);
const jsonReport = JSON.parse(await fs.readFile(path.join(root, 'artifacts', 'reliability', 'lock-reliability-summary.json'), 'utf8'));
assert.equal(jsonReport.byPlatform.codex, 2);
assert.equal(jsonReport.byPlatform.claude, 1);
assert.equal(jsonReport.byCadence.daily, 2);
assert.equal(jsonReport.byCadence.weekly, 1);
assert.equal(jsonReport.byBackendCategory['publish failed to all relays'], 1);
assert.equal(jsonReport.byBackendCategory['preflight:relay query timeout'], 1);
assert.equal(jsonReport.byRelayEndpoint['wss://relay-1.test'], 2);
const markdown = await fs.readFile(path.join(root, 'artifacts', 'reliability', 'lock-reliability-summary.md'), 'utf8');
assert.match(markdown, /## By platform/);
assert.match(markdown, /- codex: 2/);
});
-47
View File
@@ -1,47 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
const SCRIPT = path.resolve('scripts/test-audit/run-flaky-check.mjs');
function runFlakyCheck(args) {
return new Promise((resolve) => {
const childEnv = { ...process.env };
delete childEnv.NODE_TEST_CONTEXT;
delete childEnv.NODE_UNIQUE_ID;
const child = spawn(process.execPath, [SCRIPT, ...args], {
cwd: process.cwd(),
stdio: ['ignore', 'pipe', 'pipe'],
env: childEnv,
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
});
}
test('run-flaky-check reports pass/fail counts from TAP output files', async () => {
const outputDir = await fs.mkdtemp(path.join(os.tmpdir(), 'flaky-check-report-'));
const passFixture = 'test/fixtures/flaky-check/pass.fixture.test.mjs';
const failFixture = 'test/fixtures/flaky-check/fail.fixture.test.mjs';
const result = await runFlakyCheck(['--output-dir', outputDir, passFixture, failFixture]);
assert.equal(result.code, 0, result.stderr || result.stdout);
const matrix = JSON.parse(await fs.readFile(path.join(outputDir, 'flakiness-matrix.json'), 'utf8'));
// The tool keys results by test name, not filename
assert.deepEqual(matrix['pass fixture stays green'], { pass: 5, fail: 0 });
assert.deepEqual(matrix['fail fixture stays red'], { pass: 0, fail: 5 });
const diagnostics = JSON.parse(await fs.readFile(path.join(outputDir, 'flakiness-runs.json'), 'utf8'));
assert.equal(diagnostics.length, 5);
assert.ok(diagnostics.every((run) => run.observed >= 2));
});
-106
View File
@@ -1,106 +0,0 @@
import { describe, it, beforeEach, mock } from 'node:test';
import assert from 'node:assert';
import { loadTorchConfig, _resetTorchConfigCache } from '../src/torch-config.mjs';
describe('loadTorchConfig (Unit Tests with Mocked FS)', () => {
beforeEach(() => {
_resetTorchConfigCache();
});
it('loads valid config correctly', async () => {
const mockFs = {
existsSync: mock.fn(() => true),
promises: {
readFile: mock.fn(async () => JSON.stringify({
nostrLock: { namespace: 'unit-test-ns' }
}))
}
};
const config = await loadTorchConfig(mockFs);
assert.strictEqual(config.nostrLock.namespace, 'unit-test-ns');
// loadTorchConfig no longer calls existsSync on the passed fs, only readFile (and catches ENOENT)
// Actually, getTorchConfigPath calls existsSync on REAL fs.
// So checking mockFs.existsSync is wrong if loadTorchConfig doesn't call it.
// In my change, loadTorchConfig calls await fileSystem.promises.readFile.
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
});
it('returns default config when file is missing', async () => {
const mockFs = {
promises: {
readFile: mock.fn(async () => {
const err = new Error('ENOENT');
err.code = 'ENOENT';
throw err;
})
}
};
const config = await loadTorchConfig(mockFs);
assert.strictEqual(config.nostrLock.namespace, null);
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
});
it('throws error on malformed JSON', async () => {
const mockFs = {
promises: {
readFile: mock.fn(async () => '{ invalid json }')
}
};
await assert.rejects(async () => {
await loadTorchConfig(mockFs);
}, /Failed to parse/);
});
it('caches the config and ignores subsequent fs calls', async () => {
const mockFs = {
promises: {
readFile: mock.fn(async () => JSON.stringify({ nostrLock: { namespace: 'cached' } }))
}
};
const config1 = await loadTorchConfig(mockFs);
assert.strictEqual(config1.nostrLock.namespace, 'cached');
// Second call with a different "fs" (or same) should not trigger fs operations
const mockFs2 = {
promises: {
readFile: mock.fn(async () => JSON.stringify({ nostrLock: { namespace: 'changed' } }))
}
};
const config2 = await loadTorchConfig(mockFs2);
assert.strictEqual(config2.nostrLock.namespace, 'cached');
assert.strictEqual(config1, config2);
assert.strictEqual(mockFs.promises.readFile.mock.calls.length, 1);
assert.strictEqual(mockFs2.promises.readFile.mock.calls.length, 0);
});
it('returns null for empty string lists (new consistent behavior)', async () => {
const mockFs = {
promises: {
readFile: mock.fn(async () => JSON.stringify({
nostrLock: {
relays: [],
relayFallbacks: []
},
dashboard: {
relays: []
}
}))
}
};
const config = await loadTorchConfig(mockFs);
// New behavior: all return null
assert.strictEqual(config.nostrLock.relays, null);
assert.strictEqual(config.dashboard.relays, null);
assert.strictEqual(config.nostrLock.relayFallbacks, null);
});
});
-172
View File
@@ -1,172 +0,0 @@
import { describe, it, after, beforeEach } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import {
getTorchConfigPath,
parseTorchConfig,
loadTorchConfig,
_resetTorchConfigCache,
getRelays,
getQueryTimeoutMs,
getPublishTimeoutMs,
getMinSuccessfulRelayPublishes,
getRelayFallbacks,
getMinActiveRelayPool,
} from '../src/torch-config.mjs';
import { DEFAULT_RELAYS } from '../src/constants.mjs';
describe('torch-config', () => {
describe('getTorchConfigPath', () => {
const originalEnv = process.env.TORCH_CONFIG_PATH;
const originalCwd = process.cwd();
after(() => {
if (originalEnv === undefined) delete process.env.TORCH_CONFIG_PATH;
else process.env.TORCH_CONFIG_PATH = originalEnv;
process.chdir(originalCwd);
});
it('returns default path when env var is not set', () => {
delete process.env.TORCH_CONFIG_PATH;
assert.strictEqual(getTorchConfigPath(), path.resolve(process.cwd(), 'torch-config.json'));
});
it('returns custom path when env var is set', () => {
process.env.TORCH_CONFIG_PATH = 'custom-config.json';
assert.strictEqual(getTorchConfigPath(), path.resolve(process.cwd(), 'custom-config.json'));
});
it('prefers parent torch-config.json in host-repo mode when cwd is <repo>/torch', () => {
delete process.env.TORCH_CONFIG_PATH;
const fixtureRoot = fs.mkdtempSync(path.join(process.cwd(), 'test-torch-config-host-'));
const hostPkgPath = path.join(fixtureRoot, 'package.json');
const parentConfigPath = path.join(fixtureRoot, 'torch-config.json');
const torchDir = path.join(fixtureRoot, 'torch');
const localConfigPath = path.join(torchDir, 'torch-config.json');
fs.mkdirSync(torchDir, { recursive: true });
fs.writeFileSync(hostPkgPath, JSON.stringify({ name: 'seedpass', version: '1.0.0' }), 'utf8');
fs.writeFileSync(parentConfigPath, JSON.stringify({ nostrLock: { namespace: 'parent' } }), 'utf8');
fs.writeFileSync(localConfigPath, JSON.stringify({ nostrLock: { namespace: 'local' } }), 'utf8');
process.chdir(torchDir);
assert.strictEqual(getTorchConfigPath(), path.resolve(parentConfigPath));
});
});
describe('parseTorchConfig', () => {
it('parses new lock backend knobs', () => {
const config = parseTorchConfig({
nostrLock: {
namespace: ' test ',
relays: ['wss://relay1'],
relayFallbacks: [' wss://fallback-1 ', 'wss://fallback-2'],
queryTimeoutMs: 5000,
publishTimeoutMs: 8000,
minSuccessfulRelayPublishes: 2,
minActiveRelayPool: 2,
},
});
assert.strictEqual(config.nostrLock.namespace, 'test');
assert.deepStrictEqual(config.nostrLock.relays, ['wss://relay1']);
assert.deepStrictEqual(config.nostrLock.relayFallbacks, ['wss://fallback-1', 'wss://fallback-2']);
assert.strictEqual(config.nostrLock.queryTimeoutMs, 5000);
assert.strictEqual(config.nostrLock.publishTimeoutMs, 8000);
assert.strictEqual(config.nostrLock.minSuccessfulRelayPublishes, 2);
assert.strictEqual(config.nostrLock.minActiveRelayPool, 2);
});
});
describe('loadTorchConfig', () => {
const tempConfigPath = path.resolve(process.cwd(), 'temp-torch-config.json');
const badConfigPath = path.resolve(process.cwd(), 'bad-torch-config.json');
const originalEnv = process.env.TORCH_CONFIG_PATH;
beforeEach(() => {
_resetTorchConfigCache();
delete process.env.TORCH_CONFIG_PATH;
});
after(() => {
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
if (fs.existsSync(badConfigPath)) fs.unlinkSync(badConfigPath);
if (originalEnv === undefined) delete process.env.TORCH_CONFIG_PATH;
else process.env.TORCH_CONFIG_PATH = originalEnv;
_resetTorchConfigCache();
});
it('throws fatal error for invalid relay URL', async () => {
fs.writeFileSync(tempConfigPath, JSON.stringify({ nostrLock: { relays: ['https://bad'] } }));
process.env.TORCH_CONFIG_PATH = tempConfigPath;
await assert.rejects(() => loadTorchConfig(), /Invalid relay URL in nostrLock\.relays/);
});
it('throws fatal error for invalid timeout range', async () => {
fs.writeFileSync(tempConfigPath, JSON.stringify({ nostrLock: { queryTimeoutMs: 50 } }));
process.env.TORCH_CONFIG_PATH = tempConfigPath;
await assert.rejects(() => loadTorchConfig(), /Invalid nostrLock\.queryTimeoutMs/);
});
it('throws error for malformed JSON', async () => {
fs.writeFileSync(badConfigPath, '{ invalid json }');
process.env.TORCH_CONFIG_PATH = badConfigPath;
await assert.rejects(() => loadTorchConfig(), /Failed to parse/);
});
});
describe('backend getters', () => {
const originalEnv = {
NOSTR_LOCK_RELAYS: process.env.NOSTR_LOCK_RELAYS,
NOSTR_LOCK_QUERY_TIMEOUT_MS: process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS,
NOSTR_LOCK_PUBLISH_TIMEOUT_MS: process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS,
NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES: process.env.NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES,
NOSTR_LOCK_RELAY_FALLBACKS: process.env.NOSTR_LOCK_RELAY_FALLBACKS,
NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL: process.env.NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL,
};
beforeEach(() => {
_resetTorchConfigCache();
delete process.env.NOSTR_LOCK_RELAYS;
delete process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS;
delete process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS;
delete process.env.NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES;
delete process.env.NOSTR_LOCK_RELAY_FALLBACKS;
delete process.env.NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL;
});
after(() => {
for (const [k, v] of Object.entries(originalEnv)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
_resetTorchConfigCache();
});
it('returns default relays', async () => {
assert.deepStrictEqual(await getRelays(), DEFAULT_RELAYS);
});
it('validates env knob values', async () => {
process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS = '99';
await assert.rejects(() => getQueryTimeoutMs(), /NOSTR_LOCK_QUERY_TIMEOUT_MS/);
process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS = '5000';
process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS = 'abc';
await assert.rejects(() => getPublishTimeoutMs(), /NOSTR_LOCK_PUBLISH_TIMEOUT_MS/);
process.env.NOSTR_LOCK_PUBLISH_TIMEOUT_MS = '8000';
process.env.NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES = '0';
await assert.rejects(() => getMinSuccessfulRelayPublishes(), /NOSTR_LOCK_MIN_SUCCESSFUL_PUBLISHES/);
process.env.NOSTR_LOCK_RELAY_FALLBACKS = 'https://bad';
await assert.rejects(() => getRelayFallbacks(), /NOSTR_LOCK_RELAY_FALLBACKS/);
process.env.NOSTR_LOCK_RELAY_FALLBACKS = '';
process.env.NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL = '0';
await assert.rejects(() => getMinActiveRelayPool(), /NOSTR_LOCK_MIN_ACTIVE_RELAY_POOL/);
});
});
});
-51
View File
@@ -1,51 +0,0 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { withTimeout } from '../src/lock-utils.mjs';
describe('Async Utilities', () => {
describe('withTimeout', () => {
it('resolves when promise completes before timeout', async () => {
const result = await withTimeout(
new Promise((resolve) => setTimeout(() => resolve('success'), 10)),
200,
'Timed out'
);
assert.strictEqual(result, 'success');
});
it('rejects when promise times out', async () => {
try {
await withTimeout(
new Promise((resolve) => setTimeout(() => resolve('success'), 200)),
10,
'Operation timed out'
);
assert.fail('Should have rejected');
} catch (err) {
assert.strictEqual(err.message, 'Operation timed out');
}
});
it('clears timeout on success', async () => {
const result = await withTimeout(
Promise.resolve('fast'),
1000,
'timeout'
);
assert.strictEqual(result, 'fast');
});
it('propagates original promise rejection', async () => {
try {
await withTimeout(
Promise.reject(new Error('Original error')),
100,
'Timeout'
);
assert.fail('Should have rejected');
} catch (err) {
assert.strictEqual(err.message, 'Original error');
}
});
});
});
-222
View File
@@ -1,222 +0,0 @@
import { describe, it, after } from 'node:test';
import assert from 'node:assert';
import fs from 'node:fs';
import path from 'node:path';
import { getIsoWeekStr, todayDateStr, nowUnix, ensureDir, detectPlatform, relayListLabel, mergeRelayList } 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
assert.strictEqual(getIsoWeekStr('2023-01-08'), '2023-W01'); // Sun
assert.strictEqual(getIsoWeekStr('2023-01-09'), '2023-W02'); // Mon
});
it('handles year boundaries correctly', () => {
// 2023-01-01 is Sunday. Belong to last week of 2022.
assert.strictEqual(getIsoWeekStr('2023-01-01'), '2022-W52');
// 2024-01-01 is Monday. Week 1 of 2024.
assert.strictEqual(getIsoWeekStr('2024-01-01'), '2024-W01');
// 2023-12-31 is Sunday. Week 52 of 2023.
assert.strictEqual(getIsoWeekStr('2023-12-31'), '2023-W52');
});
it('handles leap years', () => {
assert.strictEqual(getIsoWeekStr('2024-02-29'), '2024-W09');
});
it('handles empty input (defaults to today)', () => {
// Just check it returns a string in correct format
const result = getIsoWeekStr();
assert.match(result, /^\d{4}-W\d{2}$/);
});
it('handles invalid input gracefully', () => {
assert.strictEqual(getIsoWeekStr('invalid-date'), '');
});
});
});
describe('Relay Utilities', () => {
describe('mergeRelayList', () => {
it('merges two lists of relays without duplicates', () => {
const primary = ['wss://relay1.com', 'wss://relay2.com'];
const fallback = ['wss://relay2.com', 'wss://relay3.com'];
const merged = mergeRelayList(primary, fallback);
assert.deepStrictEqual(merged, ['wss://relay1.com', 'wss://relay2.com', 'wss://relay3.com']);
});
it('handles empty lists', () => {
assert.deepStrictEqual(mergeRelayList([], []), []);
assert.deepStrictEqual(mergeRelayList(['wss://relay1.com'], []), ['wss://relay1.com']);
assert.deepStrictEqual(mergeRelayList([], ['wss://relay1.com']), ['wss://relay1.com']);
});
});
});
describe('Platform Utilities', () => {
describe('detectPlatform', () => {
const originalEnv = { ...process.env };
// Helper to clear relevant env vars
const clearEnv = () => {
delete process.env.JULES_SESSION_ID;
delete process.env.JULES_API_KEY;
delete process.env.CODEX_SESSION_ID;
delete process.env.CODEX_API_KEY;
delete process.env.GOOSE_SESSION_ID;
delete process.env.GOOSE_API_KEY;
delete process.env.CLAUDE_SESSION_ID;
delete process.env.CLAUDE_API_KEY;
delete process.env.ANTHROPIC_API_KEY;
delete process.env.GEMINI_API_KEY;
delete process.env.GOOGLE_API_KEY;
delete process.env.ANTIGRAVITY_API_KEY;
delete process.env.ANTIGRAVITY_SESSION_ID;
delete process.env.QWEN_API_KEY;
delete process.env.QWEN_SESSION_ID;
};
after(() => {
process.env = originalEnv;
});
it('detects jules', () => {
clearEnv();
process.env.JULES_SESSION_ID = 'test';
assert.strictEqual(detectPlatform(), 'jules');
});
it('detects codex', () => {
clearEnv();
process.env.CODEX_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'codex');
});
it('detects goose', () => {
clearEnv();
process.env.GOOSE_SESSION_ID = 'test';
assert.strictEqual(detectPlatform(), 'goose');
});
it('detects claude via CLAUDE_API_KEY', () => {
clearEnv();
process.env.CLAUDE_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'claude');
});
it('detects claude via ANTHROPIC_API_KEY', () => {
clearEnv();
process.env.ANTHROPIC_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'claude');
});
it('detects gemini via GEMINI_API_KEY', () => {
clearEnv();
process.env.GEMINI_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'gemini');
});
it('detects gemini via GOOGLE_API_KEY', () => {
clearEnv();
process.env.GOOGLE_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'gemini');
});
it('detects antigravity via ANTIGRAVITY_API_KEY', () => {
clearEnv();
process.env.ANTIGRAVITY_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'antigravity');
});
it('detects antigravity via ANTIGRAVITY_SESSION_ID', () => {
clearEnv();
process.env.ANTIGRAVITY_SESSION_ID = 'test';
assert.strictEqual(detectPlatform(), 'antigravity');
});
it('detects qwen via QWEN_API_KEY', () => {
clearEnv();
process.env.QWEN_API_KEY = 'test';
assert.strictEqual(detectPlatform(), 'qwen');
});
it('returns null if no platform detected', () => {
clearEnv();
assert.strictEqual(detectPlatform(), null);
});
});
});
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);
});
});
});
describe('String Utilities', () => {
describe('relayListLabel', () => {
it('joins relay URLs with comma and space', () => {
const relays = ['ws://relay1.com', 'ws://relay2.com'];
assert.strictEqual(relayListLabel(relays), 'ws://relay1.com, ws://relay2.com');
});
it('handles single relay', () => {
const relays = ['ws://relay1.com'];
assert.strictEqual(relayListLabel(relays), 'ws://relay1.com');
});
it('handles empty array', () => {
const relays = [];
assert.strictEqual(relayListLabel(relays), '');
});
});
});
-126
View File
@@ -1,126 +0,0 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
const SCRIPT = path.resolve('scripts/agent/verify-run-artifacts.mjs');
async function runVerify(cwd, args = []) {
return new Promise((resolve) => {
const child = spawn(process.execPath, [SCRIPT, ...args], {
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env },
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
child.on('close', (code) => resolve({ code: code ?? 1, stdout, stderr }));
});
}
async function setupFixture() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'verify-run-artifacts-'));
await fs.mkdir(path.join(root, 'src', 'context'), { recursive: true });
await fs.mkdir(path.join(root, 'src', 'todo'), { recursive: true });
await fs.mkdir(path.join(root, 'src', 'decisions'), { recursive: true });
await fs.mkdir(path.join(root, 'src', 'test_logs'), { recursive: true });
await fs.mkdir(path.join(root, 'src', 'prompts', 'daily'), { recursive: true });
await fs.mkdir(path.join(root, 'docs', 'agent-handoffs', 'incidents'), { recursive: true });
await fs.writeFile(path.join(root, 'src', 'prompts', 'daily', 'docs-agent.md'), '# docs-agent\n', 'utf8');
return root;
}
async function writeArtifacts(root, { includeMetadata = true, failureId = 'ISSUE-123' } = {}) {
const metadata = includeMetadata
? ['agent: docs-agent', 'cadence: daily', 'session: session-1', 'run-start: 2026-02-14T00:00:00.000Z', 'prompt-file: src/prompts/daily/docs-agent.md'].join('\n')
: '';
await fs.writeFile(path.join(root, 'src', 'context', 'CONTEXT_1.md'), `${metadata}\n\nGoal: update docs\nScope: scripts\nConstraints: minimal\n`, 'utf8');
await fs.writeFile(path.join(root, 'src', 'todo', 'TODO_1.md'), `${metadata}\n\nPending tasks:\n- finish checks\n`, 'utf8');
await fs.writeFile(path.join(root, 'src', 'decisions', 'DECISIONS_1.md'), `${metadata}\n\nDecision: keep scope tight\nRationale: minimal risk\n`, 'utf8');
await fs.writeFile(path.join(root, 'src', 'test_logs', 'TEST_LOG_1.md'), `${metadata}\n\nCommand: npm test\nResult: failed\nFailure-ID: ${failureId}\n`, 'utf8');
}
test('passes when artifacts include metadata and failure IDs map to unresolved known issue entries', async () => {
const root = await setupFixture();
await writeArtifacts(root);
await fs.writeFile(
path.join(root, 'KNOWN_ISSUES.md'),
`### [Issue Tracking]\n- **Status:** Open\n- **Issue-ID:** ISSUE-123\n- **Symptom:** reproducible failure\n`,
'utf8',
);
const result = await runVerify(root, [
'--agent', 'docs-agent',
'--cadence', 'daily',
'--prompt-path', 'src/prompts/daily/docs-agent.md',
'--run-start', '2026-02-14T00:00:00.000Z',
'--check-failure-notes',
]);
assert.equal(result.code, 0, result.stderr || result.stdout);
});
test('fails when required metadata is missing from artifacts', async () => {
const root = await setupFixture();
await writeArtifacts(root, { includeMetadata: false });
await fs.writeFile(path.join(root, 'KNOWN_ISSUES.md'), '_No active issues currently documented._\n', 'utf8');
const result = await runVerify(root, [
'--agent', 'docs-agent',
'--cadence', 'daily',
'--prompt-path', 'src/prompts/daily/docs-agent.md',
]);
assert.equal(result.code, 1);
assert.match(result.stderr, /missing `agent:` metadata/i);
});
test('fails when failure identifiers are not cross-linked to known issues or incidents', async () => {
const root = await setupFixture();
await writeArtifacts(root, { includeMetadata: true, failureId: 'ISSUE-999' });
await fs.writeFile(
path.join(root, 'KNOWN_ISSUES.md'),
`### [Issue Tracking]\n- **Status:** Open\n- **Issue-ID:** ISSUE-123\n`,
'utf8',
);
const result = await runVerify(root, [
'--agent', 'docs-agent',
'--cadence', 'daily',
'--prompt-path', 'src/prompts/daily/docs-agent.md',
'--check-failure-notes',
]);
assert.equal(result.code, 1);
assert.match(result.stderr, /not cross-linked/i);
});
test('fails when artifact filenames contain Windows-invalid characters', async () => {
const root = await setupFixture();
await writeArtifacts(root, { includeMetadata: true });
await fs.writeFile(path.join(root, 'KNOWN_ISSUES.md'), '_No active issues currently documented._\n', 'utf8');
const validPath = path.join(root, 'src', 'test_logs', 'TEST_LOG_1.md');
const invalidPath = path.join(root, 'src', 'test_logs', 'TEST_LOG_2026-02-15T01:03:31Z.md');
const content = await fs.readFile(validPath, 'utf8');
await fs.writeFile(invalidPath, content, 'utf8');
await fs.unlink(validPath);
const result = await runVerify(root, [
'--agent', 'docs-agent',
'--cadence', 'daily',
'--prompt-path', 'src/prompts/daily/docs-agent.md',
]);
assert.equal(result.code, 1);
assert.match(result.stderr, /invalid filename characters/i);
assert.match(result.stderr, /TEST_LOG_2026-02-15T01:03:31Z\.md/);
});