From 99bd4ba2a381cbf207a494456a67510e1eb0dcbe Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:53:55 +0000 Subject: [PATCH] test: add unit tests for cmdCheck Refactor `cmdCheck` in `src/cmd-check.mjs` to support dependency injection for better testability. Add `test/cmd-check.test.mjs` with comprehensive unit tests for `cmdCheck`, covering happy paths and edge cases. Verify tests pass with `node --test test/cmd-check.test.mjs`. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- src/cmd-check.mjs | 23 +++-- test/cmd-check.test.mjs | 187 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 test/cmd-check.test.mjs diff --git a/src/cmd-check.mjs b/src/cmd-check.mjs index ce98ea4..72964d6 100644 --- a/src/cmd-check.mjs +++ b/src/cmd-check.mjs @@ -7,11 +7,20 @@ import { getRoster } from './roster.mjs'; import { queryLocks } from './lock-ops.mjs'; import { todayDateStr } from './utils.mjs'; -export async function cmdCheck(cadence) { - const relays = getRelays(); - const namespace = getNamespace(); - const dateStr = todayDateStr(); - const config = loadTorchConfig(); +export async function cmdCheck(cadence, deps = {}) { + const { + getRelaysFn = getRelays, + getNamespaceFn = getNamespace, + loadTorchConfigFn = loadTorchConfig, + getRosterFn = getRoster, + queryLocksFn = queryLocks, + todayDateStrFn = todayDateStr, + } = deps; + + const relays = getRelaysFn(); + const namespace = getNamespaceFn(); + const dateStr = todayDateStrFn(); + const config = loadTorchConfigFn(); const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || []; console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`); @@ -20,9 +29,9 @@ export async function cmdCheck(cadence) { console.error(`Paused agents: ${pausedAgents.join(', ')}`); } - const locks = await queryLocks(relays, cadence, dateStr, namespace); + const locks = await queryLocksFn(relays, cadence, dateStr, namespace); const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))]; - const roster = getRoster(cadence); + const roster = getRosterFn(cadence); const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])]; const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent)); diff --git a/test/cmd-check.test.mjs b/test/cmd-check.test.mjs new file mode 100644 index 0000000..287dd7b --- /dev/null +++ b/test/cmd-check.test.mjs @@ -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, []); + }); +});