Merge pull request #283 from PR0M3TH3AN/jules-test-roster-improvement-7422741331242836925

🧪 Add unit tests for roster loading logic
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-18 09:50:29 -05:00
commit 6f34b7d32f
2 files changed
+68 -38

No files matched your search

+22 -10
View File
@@ -3,6 +3,23 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { loadTorchConfig } from './torch-config.mjs';
const defaultDeps = {
fs,
loadTorchConfig,
};
const deps = { ...defaultDeps };
/** @internal */
export function _setRosterDependencies(overrides) {
Object.assign(deps, overrides);
}
/** @internal */
export function _restoreRosterDependencies() {
Object.assign(deps, defaultDeps);
}
const ROSTER_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'prompts/roster.json');
const USER_ROSTER_FILE = path.resolve(process.cwd(), 'torch/roster.json');
const CWD_ROSTER_FILE = path.resolve(process.cwd(), 'roster.json');
@@ -59,14 +76,14 @@ function loadCanonicalRoster(fsModule = fs) {
let rosterPath = ROSTER_FILE;
// Prefer user-managed roster if present
if (fsModule.existsSync(USER_ROSTER_FILE)) {
if (deps.fs.existsSync(USER_ROSTER_FILE)) {
rosterPath = USER_ROSTER_FILE;
} else if (fsModule.existsSync(CWD_ROSTER_FILE)) {
} else if (deps.fs.existsSync(CWD_ROSTER_FILE)) {
rosterPath = CWD_ROSTER_FILE;
}
try {
const parsed = JSON.parse(fsModule.readFileSync(rosterPath, 'utf8'));
const parsed = JSON.parse(deps.fs.readFileSync(rosterPath, 'utf8'));
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
const weekly = Array.isArray(parsed.weekly)
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
@@ -99,13 +116,8 @@ function parseEnvRoster(value) {
.filter(Boolean);
}
export async function getRoster(cadence, deps = {}) {
const {
loadTorchConfig: loadTorchConfigFn = loadTorchConfig,
fs: fsModule = fs,
} = deps;
const config = await loadTorchConfigFn();
export async function getRoster(cadence) {
const config = await deps.loadTorchConfig();
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
const canonical = loadCanonicalRoster(fsModule);
+46 -28
View File
@@ -1,27 +1,33 @@
import { describe, it, beforeEach, afterEach, mock } from 'node:test';
import assert from 'node:assert';
import path from 'node:path';
import { getRoster, _resetRosterCache } from '../src/roster.mjs';
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');
const mockFs = {
existsSync: mock.fn(),
readFileSync: mock.fn(),
};
const mockLoadTorchConfig = mock.fn();
// 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();
console.error = mock.fn();
// Default: config returns empty nostrLock
mockLoadTorchConfig.mock.mockImplementation(async () => ({ nostrLock: {} }));
@@ -32,22 +38,41 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
// 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(() => {
mock.reset();
_restoreRosterDependencies();
console.error = originalConsoleError;
mock.reset();
});
it('returns fallback roster when no configuration or files exist', async () => {
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const roster = await getRoster('daily', deps);
const roster = await getRoster('daily');
assert.ok(Array.isArray(roster));
assert.ok(roster.length > 0);
assert.ok(roster.includes('audit-agent'));
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 () => {
@@ -59,13 +84,12 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
throw new Error('ENOENT');
});
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const weekly = await getRoster('weekly', deps);
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);
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 () => {
@@ -77,8 +101,7 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
throw new Error('ENOENT');
});
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['cwd-agent-daily']);
});
@@ -86,9 +109,8 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => '{ invalid json }');
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
// Should catch error and return fallback
const daily = await getRoster('daily', deps);
const daily = await getRoster('daily');
assert.ok(daily.includes('audit-agent'));
// Should have tried to read file
@@ -99,8 +121,7 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ other: [] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const daily = await getRoster('daily');
assert.ok(daily.includes('audit-agent'));
});
@@ -111,8 +132,7 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['env-agent-daily']);
});
@@ -125,15 +145,13 @@ describe('Roster (Unit Tests with Dependency Injection)', () => {
mockFs.existsSync.mock.mockImplementation((filepath) => filepath === USER_ROSTER_FILE);
mockFs.readFileSync.mock.mockImplementation(() => JSON.stringify({ daily: ['file-agent'] }));
const deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
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 deps = { fs: mockFs, loadTorchConfig: mockLoadTorchConfig };
const daily = await getRoster('daily', deps);
const daily = await getRoster('daily');
assert.deepStrictEqual(daily, ['agent1', 'agent2', 'agent3']);
});
});