From 647724bd5bc7d13bb237716c9bf0660556076a03 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 14 Feb 2026 13:11:46 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AA=20Add=20unit=20tests=20for=20src/t?= =?UTF-8?q?orch-config.mjs=20and=20refactor=20for=20testability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/torch-config.mjs | 38 +++++--- test/torch-config.test.mjs | 184 +++++++++++++++++++++++++++++++++++++ 2 files changed, 207 insertions(+), 15 deletions(-) create mode 100644 test/torch-config.test.mjs diff --git a/src/torch-config.mjs b/src/torch-config.mjs index e3f6efa..16a84e2 100644 --- a/src/torch-config.mjs +++ b/src/torch-config.mjs @@ -27,27 +27,14 @@ export function getTorchConfigPath() { return path.resolve(process.cwd(), DEFAULT_CONFIG_PATH); } -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 }); - } - } - +export function parseTorchConfig(raw, configPath = null) { const nostrLock = raw.nostrLock || {}; const dashboard = raw.dashboard || {}; const scheduler = raw.scheduler || {}; const firstPromptByCadence = scheduler.firstPromptByCadence || {}; const paused = scheduler.paused || {}; - cachedConfig = { + return { configPath, raw, 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; } diff --git a/test/torch-config.test.mjs b/test/torch-config.test.mjs new file mode 100644 index 0000000..dbe28f4 --- /dev/null +++ b/test/torch-config.test.mjs @@ -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'); + }); + }); +});