mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-04 03:41:54 +00:00
test: add unit tests for roster loading logic
- Implemented dependency injection in `src/roster.mjs` to allow mocking `fs` and `loadTorchConfig`.
- Added `test/roster.test.mjs` with comprehensive test cases:
- Fallback behavior when no files/config exist.
- Priority of `torch/roster.json` (User Roster) over `roster.json` (CWD).
- Internal module roster fallback.
- Environment variable overrides.
- Configuration overrides.
- Error handling for malformed JSON or missing fields.
- Exported `_resetRosterCache`, `_setRosterDependencies`, and `_restoreRosterDependencies` for testing purposes.
Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
cea50da512
commit
a31332457d
2 files changed
+183
-4
No files matched your search
+26
-4
@@ -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() {
|
||||
let rosterPath = ROSTER_FILE;
|
||||
|
||||
// Prefer user-managed roster if present
|
||||
if (fs.existsSync(USER_ROSTER_FILE)) {
|
||||
if (deps.fs.existsSync(USER_ROSTER_FILE)) {
|
||||
rosterPath = USER_ROSTER_FILE;
|
||||
} else if (fs.existsSync(CWD_ROSTER_FILE)) {
|
||||
} else if (deps.fs.existsSync(CWD_ROSTER_FILE)) {
|
||||
rosterPath = CWD_ROSTER_FILE;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.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)
|
||||
@@ -86,6 +103,11 @@ function loadCanonicalRoster() {
|
||||
return cachedCanonicalRoster;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function _resetRosterCache() {
|
||||
cachedCanonicalRoster = null;
|
||||
}
|
||||
|
||||
function parseEnvRoster(value) {
|
||||
if (!value) return null;
|
||||
return value
|
||||
@@ -95,7 +117,7 @@ function parseEnvRoster(value) {
|
||||
}
|
||||
|
||||
export async function getRoster(cadence) {
|
||||
const config = await loadTorchConfig();
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
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']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user