Merge branch 'main' into test/ingest-events-coverage-4058176338689655233

This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-17 14:25:04 -05:00
commit 4a59ccd501
6 files changed
+581 -410

No files matched your search

+187
View File
@@ -0,0 +1,187 @@
import { describe, it, mock, afterEach, after } from 'node:test';
import assert from 'node:assert';
import { cmdCheck } from '../src/cmd-check.mjs';
// Mocks
const mockLoadTorchConfig = mock.fn(() => ({
scheduler: {
paused: {
daily: [],
weekly: [],
},
},
}));
const mockGetRelays = mock.fn(() => ['wss://mock-relay']);
const mockGetNamespace = mock.fn(() => 'mock-namespace');
const mockGetRoster = mock.fn(() => []);
const mockQueryLocks = mock.fn(async () => []);
const mockTodayDateStr = mock.fn(() => '2023-10-27');
const deps = {
loadTorchConfigFn: mockLoadTorchConfig,
getRelaysFn: mockGetRelays,
getNamespaceFn: mockGetNamespace,
getRosterFn: mockGetRoster,
queryLocksFn: mockQueryLocks,
todayDateStrFn: mockTodayDateStr,
};
describe('cmdCheck', () => {
// Capture console output
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
let consoleLogOutput = [];
let consoleErrorOutput = [];
const mockConsoleLog = mock.fn((msg) => {
consoleLogOutput.push(msg);
});
const mockConsoleError = mock.fn((msg) => {
consoleErrorOutput.push(msg);
});
// Setup console mocks
console.log = mockConsoleLog;
console.error = mockConsoleError;
afterEach(() => {
// Reset mocks
mockLoadTorchConfig.mock.resetCalls();
mockGetRelays.mock.resetCalls();
mockGetNamespace.mock.resetCalls();
mockGetRoster.mock.resetCalls();
mockQueryLocks.mock.resetCalls();
mockTodayDateStr.mock.resetCalls();
// Clear captured output
consoleLogOutput = [];
consoleErrorOutput = [];
});
// Restore console after all tests
after(() => {
console.log = originalConsoleLog;
console.error = originalConsoleError;
});
it('correctly reports available agents when no locks exist', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2']);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.strictEqual(result.cadence, 'daily');
assert.strictEqual(result.namespace, 'mock-namespace');
assert.strictEqual(result.date, '2023-10-27');
assert.deepStrictEqual(result.available, ['agent1', 'agent2']);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.paused, []);
assert.deepStrictEqual(result.excluded, []);
assert.strictEqual(result.lockCount, 0);
// Verify console output was JSON
assert.strictEqual(consoleLogOutput.length, 1);
const logOutput = JSON.parse(consoleLogOutput[0]);
assert.deepStrictEqual(logOutput.available, ['agent1', 'agent2']);
});
it('correctly identifies locked agents', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2', 'agent3']);
mockQueryLocks.mock.mockImplementation(async () => [
{ agent: 'agent1', eventId: 'evt1', createdAtIso: '2023-10-27T00:00:00Z', expiresAtIso: '2023-10-27T01:00:00Z', platform: 'linux' }
]);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, ['agent2', 'agent3']);
assert.deepStrictEqual(result.locked, ['agent1']);
assert.deepStrictEqual(result.excluded, ['agent1']);
assert.strictEqual(result.lockCount, 1);
assert.strictEqual(result.locks[0].agent, 'agent1');
});
it('correctly identifies paused agents', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2', 'agent3']);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: ['agent2'] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, ['agent1', 'agent3']);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.paused, ['agent2']);
assert.deepStrictEqual(result.excluded, ['agent2']);
});
it('handles agents that are both locked and paused', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1', 'agent2']);
mockQueryLocks.mock.mockImplementation(async () => [
{ agent: 'agent1', eventId: 'evt1' }
]);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: ['agent1'] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, ['agent2']);
assert.deepStrictEqual(result.locked, ['agent1']);
assert.deepStrictEqual(result.paused, ['agent1']);
// excluded should contain unique agents
assert.deepStrictEqual(result.excluded, ['agent1']);
});
it('identifies unknown locked agents (not in roster)', async () => {
mockGetRoster.mock.mockImplementation(() => ['agent1']);
mockQueryLocks.mock.mockImplementation(async () => [
{ agent: 'agent1', eventId: 'evt1' },
{ agent: 'unknown-agent', eventId: 'evt2' }
]);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, []);
assert.deepStrictEqual(result.locked, ['agent1', 'unknown-agent']);
assert.deepStrictEqual(result.unknownLockedAgents, ['unknown-agent']);
assert.deepStrictEqual(result.excluded, ['agent1', 'unknown-agent']);
});
it('handles weekly cadence', async () => {
mockGetRoster.mock.mockImplementation(() => ['weekly1', 'weekly2']);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { weekly: ['weekly2'] } }
}));
const result = await cmdCheck('weekly', deps);
assert.strictEqual(result.cadence, 'weekly');
assert.deepStrictEqual(result.available, ['weekly1']);
assert.deepStrictEqual(result.paused, ['weekly2']);
});
it('handles empty roster', async () => {
mockGetRoster.mock.mockImplementation(() => []);
mockQueryLocks.mock.mockImplementation(async () => []);
mockLoadTorchConfig.mock.mockImplementation(() => ({
scheduler: { paused: { daily: [] } }
}));
const result = await cmdCheck('daily', deps);
assert.deepStrictEqual(result.available, []);
assert.deepStrictEqual(result.locked, []);
assert.deepStrictEqual(result.paused, []);
});
});
+84
View File
@@ -0,0 +1,84 @@
import { describe, it, mock } from 'node:test';
import assert from 'node:assert/strict';
import { getCompletedAgents } from '../src/lock-utils.mjs';
describe('lock-utils', () => {
describe('getCompletedAgents', () => {
// Helper to create deps
const createDeps = (overrides = {}) => ({
readdir: async () => [],
getDateStr: () => '2023-01-01',
getIsoWeek: () => '2023-W01',
...overrides,
});
it('returns completed agents for daily cadence', async () => {
const deps = createDeps({
readdir: async () => [
'2023-01-01T10-00-00Z__agent1__completed.md',
'2023-01-01T11-00-00Z__agent2__failed.md',
'2023-01-02T10-00-00Z__agent3__completed.md', // different day
],
getDateStr: () => '2023-01-01',
});
const result = await getCompletedAgents('daily', 'logs', deps);
assert.deepEqual([...result], ['agent1']);
});
it('returns completed agents for weekly cadence', async () => {
const deps = createDeps({
readdir: async () => [
'2023-01-01T10-00-00Z__agent1__completed.md', // Week 52 of 2022 (Sunday) - assuming mock logic
'2023-01-02T10-00-00Z__agent2__completed.md', // Week 01 of 2023 (Monday)
],
getIsoWeek: (date) => {
if (!date) return '2023-W01'; // current week
if (date === '2023-01-01') return '2022-W52';
if (date === '2023-01-02') return '2023-W01';
return 'UNKNOWN';
}
});
// cadence is weekly
const result = await getCompletedAgents('weekly', 'logs', deps);
assert.deepEqual([...result], ['agent2']);
});
it('handles ENOENT gracefully', async () => {
const error = new Error('No such file or directory');
error.code = 'ENOENT';
const deps = createDeps({
readdir: async () => { throw error; }
});
// Spy on console.error to ensure it is NOT called
const consoleError = mock.method(console, 'error');
const result = await getCompletedAgents('daily', 'logs', deps);
assert.equal(consoleError.mock.callCount(), 0);
assert.deepEqual([...result], []);
consoleError.mock.restore();
});
it('logs error for non-ENOENT errors', async () => {
const error = new Error('Permission denied');
error.code = 'EACCES';
const deps = createDeps({
readdir: async () => { throw error; }
});
// Spy on console.error to verify it IS called
const consoleError = mock.method(console, 'error');
const result = await getCompletedAgents('daily', 'logs', deps);
assert.equal(consoleError.mock.callCount(), 1);
const call = consoleError.mock.calls[0];
assert.match(call.arguments[0], /Warning: Failed to read log dir/);
assert.match(call.arguments[0], /Permission denied/);
assert.deepEqual([...result], []);
consoleError.mock.restore();
});
});
});
+156
View File
@@ -0,0 +1,156 @@
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
const 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();
});
+138
View File
@@ -0,0 +1,138 @@
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));
});