mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
🧪 Add unit tests for src/torch-config.mjs and refactor for testability
- Extract parseTorchConfig for better isolation in tests. - Add _resetTorchConfigCache for deterministic unit testing. - Implement comprehensive tests in test/torch-config.test.mjs covering: - Path resolution with env var support. - Normalization of cadence and status. - Roster parsing and filtering. - Error handling for malformed config files. - Caching behavior. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
c731f2b09d
commit
647724bd5b
2 files changed
+207
-15
No files matched your search
+23
-15
@@ -27,27 +27,14 @@ export function getTorchConfigPath() {
|
|||||||
return path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
return path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadTorchConfig() {
|
export function parseTorchConfig(raw, configPath = null) {
|
||||||
if (cachedConfig) return cachedConfig;
|
|
||||||
|
|
||||||
const configPath = getTorchConfigPath();
|
|
||||||
let raw = {};
|
|
||||||
|
|
||||||
if (fs.existsSync(configPath)) {
|
|
||||||
try {
|
|
||||||
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nostrLock = raw.nostrLock || {};
|
const nostrLock = raw.nostrLock || {};
|
||||||
const dashboard = raw.dashboard || {};
|
const dashboard = raw.dashboard || {};
|
||||||
const scheduler = raw.scheduler || {};
|
const scheduler = raw.scheduler || {};
|
||||||
const firstPromptByCadence = scheduler.firstPromptByCadence || {};
|
const firstPromptByCadence = scheduler.firstPromptByCadence || {};
|
||||||
const paused = scheduler.paused || {};
|
const paused = scheduler.paused || {};
|
||||||
|
|
||||||
cachedConfig = {
|
return {
|
||||||
configPath,
|
configPath,
|
||||||
raw,
|
raw,
|
||||||
nostrLock: {
|
nostrLock: {
|
||||||
@@ -84,6 +71,27 @@ export function loadTorchConfig() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
export function _resetTorchConfigCache() {
|
||||||
|
cachedConfig = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadTorchConfig() {
|
||||||
|
if (cachedConfig) return cachedConfig;
|
||||||
|
|
||||||
|
const configPath = getTorchConfigPath();
|
||||||
|
let raw = {};
|
||||||
|
|
||||||
|
if (fs.existsSync(configPath)) {
|
||||||
|
try {
|
||||||
|
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||||
|
} catch (err) {
|
||||||
|
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedConfig = parseTorchConfig(raw, configPath);
|
||||||
return cachedConfig;
|
return cachedConfig;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import { test, 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 } from '../src/torch-config.mjs';
|
||||||
|
|
||||||
|
describe('torch-config', () => {
|
||||||
|
describe('getTorchConfigPath', () => {
|
||||||
|
const originalEnv = process.env.TORCH_CONFIG_PATH;
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
if (originalEnv === undefined) {
|
||||||
|
delete process.env.TORCH_CONFIG_PATH;
|
||||||
|
} else {
|
||||||
|
process.env.TORCH_CONFIG_PATH = originalEnv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default path when env var is not set', () => {
|
||||||
|
delete process.env.TORCH_CONFIG_PATH;
|
||||||
|
const expected = path.resolve(process.cwd(), 'torch-config.json');
|
||||||
|
assert.strictEqual(getTorchConfigPath(), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns custom path when env var is set', () => {
|
||||||
|
process.env.TORCH_CONFIG_PATH = 'custom-config.json';
|
||||||
|
const expected = path.resolve(process.cwd(), 'custom-config.json');
|
||||||
|
assert.strictEqual(getTorchConfigPath(), expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trims env var value', () => {
|
||||||
|
process.env.TORCH_CONFIG_PATH = ' trimmed-config.json ';
|
||||||
|
const expected = path.resolve(process.cwd(), 'trimmed-config.json');
|
||||||
|
assert.strictEqual(getTorchConfigPath(), expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseTorchConfig', () => {
|
||||||
|
it('returns default values for empty raw config', () => {
|
||||||
|
const config = parseTorchConfig({}, 'some-path.json');
|
||||||
|
assert.strictEqual(config.configPath, 'some-path.json');
|
||||||
|
assert.deepStrictEqual(config.raw, {});
|
||||||
|
assert.strictEqual(config.nostrLock.namespace, null);
|
||||||
|
assert.strictEqual(config.dashboard.defaultCadenceView, 'daily');
|
||||||
|
assert.strictEqual(config.dashboard.defaultStatusView, 'active');
|
||||||
|
assert.deepStrictEqual(config.scheduler.paused.daily, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly parses and normalizes nostrLock config', () => {
|
||||||
|
const raw = {
|
||||||
|
nostrLock: {
|
||||||
|
namespace: ' test ',
|
||||||
|
relays: [' wss://relay1 ', '', 'wss://relay2'],
|
||||||
|
ttlSeconds: 3600.5,
|
||||||
|
queryTimeoutMs: 5000,
|
||||||
|
dailyRoster: ['agent1', 'agent2'],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const config = parseTorchConfig(raw);
|
||||||
|
assert.strictEqual(config.nostrLock.namespace, 'test');
|
||||||
|
assert.deepStrictEqual(config.nostrLock.relays, ['wss://relay1', 'wss://relay2']);
|
||||||
|
assert.strictEqual(config.nostrLock.ttlSeconds, 3600);
|
||||||
|
assert.strictEqual(config.nostrLock.queryTimeoutMs, 5000);
|
||||||
|
assert.deepStrictEqual(config.nostrLock.dailyRoster, ['agent1', 'agent2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes cadence and status in dashboard config', () => {
|
||||||
|
const raw = {
|
||||||
|
dashboard: {
|
||||||
|
defaultCadenceView: ' WEEKLY ',
|
||||||
|
defaultStatusView: ' ALL '
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const config = parseTorchConfig(raw);
|
||||||
|
assert.strictEqual(config.dashboard.defaultCadenceView, 'weekly');
|
||||||
|
assert.strictEqual(config.dashboard.defaultStatusView, 'all');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to defaults for invalid cadence/status', () => {
|
||||||
|
const raw = {
|
||||||
|
dashboard: {
|
||||||
|
defaultCadenceView: 'invalid',
|
||||||
|
defaultStatusView: 'invalid'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const config = parseTorchConfig(raw);
|
||||||
|
assert.strictEqual(config.dashboard.defaultCadenceView, 'daily');
|
||||||
|
assert.strictEqual(config.dashboard.defaultStatusView, 'active');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('correctly parses scheduler config', () => {
|
||||||
|
const raw = {
|
||||||
|
scheduler: {
|
||||||
|
firstPromptByCadence: {
|
||||||
|
daily: ' daily-prompt.md ',
|
||||||
|
weekly: ' weekly-prompt.md '
|
||||||
|
},
|
||||||
|
paused: {
|
||||||
|
daily: [' paused1 '],
|
||||||
|
weekly: [' paused2 ']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const config = parseTorchConfig(raw);
|
||||||
|
assert.strictEqual(config.scheduler.firstPromptByCadence.daily, 'daily-prompt.md');
|
||||||
|
assert.strictEqual(config.scheduler.firstPromptByCadence.weekly, 'weekly-prompt.md');
|
||||||
|
assert.deepStrictEqual(config.scheduler.paused.daily, ['paused1']);
|
||||||
|
assert.deepStrictEqual(config.scheduler.paused.weekly, ['paused2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles non-array roster values by returning null or empty array', () => {
|
||||||
|
const raw = {
|
||||||
|
nostrLock: { dailyRoster: 'not-an-array' },
|
||||||
|
scheduler: { paused: { daily: 'not-an-array' } }
|
||||||
|
};
|
||||||
|
const config = parseTorchConfig(raw);
|
||||||
|
assert.strictEqual(config.nostrLock.dailyRoster, null);
|
||||||
|
assert.deepStrictEqual(config.scheduler.paused.daily, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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('loads config from disk', () => {
|
||||||
|
const raw = { nostrLock: { namespace: 'from-disk' } };
|
||||||
|
fs.writeFileSync(tempConfigPath, JSON.stringify(raw));
|
||||||
|
process.env.TORCH_CONFIG_PATH = tempConfigPath;
|
||||||
|
|
||||||
|
const config = loadTorchConfig();
|
||||||
|
assert.strictEqual(config.nostrLock.namespace, 'from-disk');
|
||||||
|
assert.strictEqual(config.configPath, tempConfigPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns defaults if file does not exist', () => {
|
||||||
|
process.env.TORCH_CONFIG_PATH = 'non-existent.json';
|
||||||
|
const config = loadTorchConfig();
|
||||||
|
assert.strictEqual(config.nostrLock.namespace, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws error for malformed JSON', () => {
|
||||||
|
fs.writeFileSync(badConfigPath, '{ invalid json }');
|
||||||
|
process.env.TORCH_CONFIG_PATH = badConfigPath;
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
loadTorchConfig();
|
||||||
|
}, /Failed to parse/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('caches the config', () => {
|
||||||
|
const raw1 = { nostrLock: { namespace: 'first' } };
|
||||||
|
fs.writeFileSync(tempConfigPath, JSON.stringify(raw1));
|
||||||
|
process.env.TORCH_CONFIG_PATH = tempConfigPath;
|
||||||
|
|
||||||
|
const config1 = loadTorchConfig();
|
||||||
|
assert.strictEqual(config1.nostrLock.namespace, 'first');
|
||||||
|
|
||||||
|
const raw2 = { nostrLock: { namespace: 'second' } };
|
||||||
|
fs.writeFileSync(tempConfigPath, JSON.stringify(raw2));
|
||||||
|
|
||||||
|
const config2 = loadTorchConfig();
|
||||||
|
assert.strictEqual(config2.nostrLock.namespace, 'first', 'Should return cached value');
|
||||||
|
assert.strictEqual(config1, config2, 'Should be the same object');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user