import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert'; import { cmdCheck, cmdLock, cmdList } from '../src/lib.mjs'; describe('src/lib.mjs', () => { // Common mocks const mockRelays = ['wss://relay.test']; const mockNamespace = 'test-ns'; const mockTtl = 3600; const mockRoster = ['agent1', 'agent2', 'agent3']; const mockDateStr = new Date().toISOString().slice(0, 10); let logs = []; let errors = []; const mockLog = (...args) => logs.push(args.join(' ')); const mockError = (...args) => errors.push(args.join(' ')); const baseDeps = { getRelays: () => mockRelays, getNamespace: () => mockNamespace, getTtl: () => mockTtl, getRoster: () => mockRoster, loadTorchConfig: () => ({ scheduler: { paused: { daily: [], weekly: [] } } }), getDateStr: () => mockDateStr, log: mockLog, error: mockError, }; beforeEach(() => { logs = []; errors = []; }); describe('cmdCheck', () => { it('returns correct structure with empty locks', async () => { const deps = { ...baseDeps, queryLocks: async () => [], }; const result = await cmdCheck('daily', deps); assert.strictEqual(result.namespace, mockNamespace); assert.strictEqual(result.cadence, 'daily'); assert.strictEqual(result.date, mockDateStr); assert.deepStrictEqual(result.locked, []); assert.deepStrictEqual(result.available, mockRoster.sort()); assert.strictEqual(result.lockCount, 0); }); it('identifies locked agents', async () => { const locks = [ { agent: 'agent1', eventId: 'ev1', createdAtIso: 'iso1', platform: 'test' } ]; const deps = { ...baseDeps, queryLocks: async () => locks, }; const result = await cmdCheck('daily', deps); assert.deepStrictEqual(result.locked, ['agent1']); assert.deepStrictEqual(result.available, ['agent2', 'agent3']); assert.strictEqual(result.lockCount, 1); }); it('excludes paused agents', async () => { const deps = { ...baseDeps, queryLocks: async () => [], loadTorchConfig: () => ({ scheduler: { paused: { daily: ['agent2'], weekly: [] } } }), }; const result = await cmdCheck('daily', deps); assert.deepStrictEqual(result.paused, ['agent2']); assert.deepStrictEqual(result.excluded, ['agent2']); assert.deepStrictEqual(result.available, ['agent1', 'agent3']); }); it('identifies unknown locked agents', async () => { const locks = [ { agent: 'unknown-agent', eventId: 'ev1' } ]; const deps = { ...baseDeps, queryLocks: async () => locks, }; const result = await cmdCheck('daily', deps); assert.deepStrictEqual(result.unknownLockedAgents, ['unknown-agent']); assert.deepStrictEqual(result.locked, ['unknown-agent']); // unknown agents are not in roster, so available should be full roster (since no roster agent is locked) assert.deepStrictEqual(result.available, mockRoster.sort()); }); }); describe('cmdLock', () => { const mockPublishLock = async () => {}; const mockGenerateSecretKey = () => new Uint8Array(32); const mockGetPublicKey = () => 'mock-pubkey'; const mockFinalizeEvent = (t) => ({ ...t, id: 'mock-id', sig: 'mock-sig' }); it('successfully locks an available agent', async () => { let published = false; const deps = { ...baseDeps, queryLocks: async () => [], // No existing locks, and race check returns empty publishLock: async () => { published = true; }, generateSecretKey: mockGenerateSecretKey, getPublicKey: mockGetPublicKey, finalizeEvent: mockFinalizeEvent, raceCheckDelayMs: 1, // Fast race check }; const result = await cmdLock('agent1', 'daily', false, deps); assert.strictEqual(result.status, 'ok'); assert.strictEqual(result.eventId, 'mock-id'); assert.strictEqual(published, true); assert.match(logs.join('\n'), /LOCK_STATUS=ok/); }); it('fails if agent is not in roster', async () => { const deps = { ...baseDeps }; await assert.rejects( async () => await cmdLock('unknown', 'daily', false, deps), (err) => err.code === 1 && err.message === 'Agent not in roster' ); }); it('fails if agent is already locked', async () => { const existingLocks = [ { agent: 'agent1', eventId: 'existing', createdAt: 100, createdAtIso: 'time' } ]; const deps = { ...baseDeps, queryLocks: async () => existingLocks, }; await assert.rejects( async () => await cmdLock('agent1', 'daily', false, deps), (err) => err.code === 3 && err.message === 'Lock denied' ); assert.match(logs.join('\n'), /LOCK_STATUS=denied/); }); it('fails if race check is lost', async () => { let callCount = 0; const deps = { ...baseDeps, queryLocks: async () => { callCount++; if (callCount === 1) return []; // First check: no locks // Second check (race): found an earlier lock return [{ agent: 'agent1', eventId: 'winner', createdAt: 100, // old timestamp createdAtIso: 'old-time' }, { agent: 'agent1', eventId: 'mock-id', createdAt: 200, // our timestamp (mocked event) // We need to ensure cmdLock sees this as later. // In cmdLock, `now` is mocked via verify? No, `now` is `nowUnix()`. // But we can't easily mock `nowUnix` inside `cmdLock` without injecting it too. // However, `cmdLock` compares `createdAt`. // The mocked event has `created_at: now`. // If we return a lock with smaller `createdAt`, we lose. }]; }, publishLock: async () => {}, generateSecretKey: mockGenerateSecretKey, getPublicKey: mockGetPublicKey, finalizeEvent: mockFinalizeEvent, raceCheckDelayMs: 1, }; // We need to make sure the "winner" has explicit createdAt that is definitely before what cmdLock generates. // cmdLock uses Math.floor(Date.now() / 1000). // We can't mock Date.now easily unless we inject it. // But we can inject `finalizeEvent` to control the created timestamp of our event? // No, `finalizeEvent` receives the template. // Let's rely on the fact that `cmdLock` uses `nowUnix()`. // The "winner" in `queryLocks` return can have `createdAt: 0`. await assert.rejects( async () => await cmdLock('agent1', 'daily', false, deps), (err) => err.code === 3 && err.message === 'Race check lost' ); assert.match(logs.join('\n'), /LOCK_STATUS=race_lost/); }); it('dry run does not publish', async () => { let published = false; const deps = { ...baseDeps, queryLocks: async () => [], publishLock: async () => { published = true; }, generateSecretKey: mockGenerateSecretKey, getPublicKey: mockGetPublicKey, finalizeEvent: mockFinalizeEvent, }; const result = await cmdLock('agent1', 'daily', true, deps); assert.strictEqual(result.status, 'ok'); assert.strictEqual(published, false); assert.match(errors.join('\n'), /\[DRY RUN\]/); }); }); describe('cmdList', () => { it('lists active locks', async () => { const locks = [ { agent: 'agent1', eventId: 'ev1', createdAt: Date.now()/1000 - 60, createdAtIso: 'iso1', platform: 'test' } ]; const deps = { ...baseDeps, queryLocks: async () => locks, }; await cmdList('daily', deps); const output = logs.join('\n'); assert.match(output, /Active test-ns daily locks/); assert.match(output, /agent1/); assert.match(output, /Locked: 1\/3/); }); it('warns about unknown agents', async () => { const locks = [ { agent: 'unknown', eventId: 'ev1', createdAt: Date.now()/1000 } ]; const deps = { ...baseDeps, queryLocks: async () => locks, }; await cmdList('daily', deps); const output = logs.join('\n'); assert.match(output, /Warning: lock events found with non-roster agent names: unknown/); }); it('handles no locks', async () => { const deps = { ...baseDeps, queryLocks: async () => [], }; await cmdList('daily', deps); const output = logs.join('\n'); assert.match(output, /\(no active locks\)/); }); }); });