mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge pull request #74 from PR0M3TH3AN/unit-tests-lib-mjs-1840636568505209047
🧪 Add unit tests for src/lib.mjs
This commit is contained in:
2 files changed
+361
-135
No files matched your search
+99
-135
@@ -3,14 +3,22 @@
|
||||
// TORCH — Task Orchestration via Relay-Coordinated Handoff
|
||||
// Generic Nostr-based task locking for multi-agent development.
|
||||
|
||||
import { generateSecretKey as _generateSecretKey, getPublicKey as _getPublicKey, finalizeEvent as _finalizeEvent } from 'nostr-tools/pure';
|
||||
import { useWebSocketImplementation } from 'nostr-tools/relay';
|
||||
import WebSocket from 'ws';
|
||||
import {
|
||||
loadTorchConfig as _loadTorchConfig,
|
||||
getRelays as _getRelays,
|
||||
getNamespace as _getNamespace,
|
||||
getTtl as _getTtl,
|
||||
} from './torch-config.mjs';
|
||||
import {
|
||||
DEFAULT_DASHBOARD_PORT,
|
||||
VALID_CADENCES,
|
||||
} from './constants.mjs';
|
||||
import { cmdInit, cmdUpdate } from './ops.mjs';
|
||||
import { publishLock, parseLockEvent, queryLocks } from './lock-ops.mjs';
|
||||
import { getRoster as _getRoster } from './roster.mjs';
|
||||
import { queryLocks as _queryLocks, publishLock as _publishLock, parseLockEvent } from './lock-ops.mjs';
|
||||
import { cmdDashboard } from './dashboard.mjs';
|
||||
import { cmdCheck } from './cmd-check.mjs';
|
||||
import { cmdLock } from './cmd-lock.mjs';
|
||||
@@ -197,103 +205,32 @@ function nowUnix() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
export function parseLockEvent(event) {
|
||||
const dTag = event.tags.find((t) => t[0] === 'd')?.[1] ?? '';
|
||||
const expTag = event.tags.find((t) => t[0] === 'expiration')?.[1];
|
||||
const expiresAt = expTag ? parseInt(expTag, 10) : null;
|
||||
// Re-export for backward compatibility/library usage
|
||||
export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock };
|
||||
|
||||
let content = {};
|
||||
try {
|
||||
const parsed = JSON.parse(event.content);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
content = parsed;
|
||||
}
|
||||
} catch (err) {
|
||||
console.debug(`Note: Failed to parse event content as JSON: ${err.message}`);
|
||||
// Ignore malformed JSON content
|
||||
}
|
||||
|
||||
return {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
createdAtIso: new Date(event.created_at * 1000).toISOString(),
|
||||
expiresAt,
|
||||
expiresAtIso: expiresAt ? new Date(expiresAt * 1000).toISOString() : null,
|
||||
dTag,
|
||||
agent: content.agent ?? null,
|
||||
cadence: content.cadence ?? null,
|
||||
status: content.status ?? null,
|
||||
date: content.date ?? null,
|
||||
platform: content.platform ?? null,
|
||||
};
|
||||
}
|
||||
export async function cmdCheck(cadence, deps = {}) {
|
||||
const {
|
||||
getRelays = _getRelays,
|
||||
getNamespace = _getNamespace,
|
||||
loadTorchConfig = _loadTorchConfig,
|
||||
queryLocks = _queryLocks,
|
||||
getRoster = _getRoster,
|
||||
getDateStr = todayDateStr,
|
||||
log = console.log,
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
function filterActiveLocks(locks) {
|
||||
const now = nowUnix();
|
||||
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
|
||||
}
|
||||
|
||||
export async function queryLocks(relays, cadence, dateStr, namespace) {
|
||||
const pool = new SimplePool();
|
||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||
const queryTimeoutMs = getQueryTimeoutMs();
|
||||
|
||||
try {
|
||||
const events = await Promise.race([
|
||||
pool.querySync(relays, {
|
||||
kinds: [30078],
|
||||
'#t': [tagFilter],
|
||||
}),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Relay query timed out')), queryTimeoutMs),
|
||||
),
|
||||
]);
|
||||
|
||||
return filterActiveLocks(events.map(parseLockEvent));
|
||||
} finally {
|
||||
pool.close(relays);
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishLock(relays, event) {
|
||||
const pool = new SimplePool();
|
||||
|
||||
try {
|
||||
const publishPromises = pool.publish(relays, event);
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
const successes = results.filter((r) => r.status === 'fulfilled');
|
||||
|
||||
if (successes.length === 0) {
|
||||
const errors = results.map((r, i) => {
|
||||
if (r.status === 'rejected') {
|
||||
const reason = r.reason;
|
||||
const message = reason instanceof Error ? reason.message : String(reason ?? 'unknown');
|
||||
return `${relays[i]}: ${message}`;
|
||||
}
|
||||
return `${relays[i]}: unknown`;
|
||||
});
|
||||
throw new Error(`Failed to publish to any relay:\n ${errors.join('\n ')}`);
|
||||
}
|
||||
|
||||
console.error(` Published to ${successes.length}/${relays.length} relays`);
|
||||
return event;
|
||||
} finally {
|
||||
pool.close(relays);
|
||||
}
|
||||
}
|
||||
|
||||
export async function cmdCheck(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const dateStr = getDateStr();
|
||||
const config = loadTorchConfig();
|
||||
const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || [];
|
||||
|
||||
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
error(`Relays: ${relays.join(', ')}`);
|
||||
if (pausedAgents.length > 0) {
|
||||
console.error(`Paused agents: ${pausedAgents.join(', ')}`);
|
||||
error(`Paused agents: ${pausedAgents.join(', ')}`);
|
||||
}
|
||||
|
||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
@@ -325,51 +262,67 @@ export async function cmdCheck(cadence) {
|
||||
})),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
export async function cmdLock(agent, cadence, dryRun = false, deps = {}) {
|
||||
const {
|
||||
getRelays = _getRelays,
|
||||
getNamespace = _getNamespace,
|
||||
getTtl = _getTtl,
|
||||
queryLocks = _queryLocks,
|
||||
getRoster = _getRoster,
|
||||
publishLock = _publishLock,
|
||||
generateSecretKey = _generateSecretKey,
|
||||
getPublicKey = _getPublicKey,
|
||||
finalizeEvent = _finalizeEvent,
|
||||
raceCheckDelayMs = RACE_CHECK_DELAY_MS,
|
||||
getDateStr = todayDateStr,
|
||||
log = console.log,
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const dateStr = getDateStr();
|
||||
const ttl = getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
|
||||
error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const roster = getRoster(cadence);
|
||||
if (!roster.includes(agent)) {
|
||||
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
throw new ExitError(1, 'Agent not in roster');
|
||||
}
|
||||
|
||||
console.error('Step 1: Checking for existing locks...');
|
||||
error('Step 1: Checking for existing locks...');
|
||||
const existingLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const conflicting = existingLocks.filter((l) => l.agent === agent);
|
||||
|
||||
if (conflicting.length > 0) {
|
||||
const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
console.error(
|
||||
error(
|
||||
`LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` +
|
||||
`(created ${earliest.createdAtIso}, platform: ${earliest.platform})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=denied');
|
||||
console.log('LOCK_REASON=already_locked');
|
||||
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||
log('LOCK_STATUS=denied');
|
||||
log('LOCK_REASON=already_locked');
|
||||
log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||
throw new ExitError(3, 'Lock denied');
|
||||
}
|
||||
|
||||
console.error('Step 2: Generating ephemeral keypair...');
|
||||
error('Step 2: Generating ephemeral keypair...');
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
|
||||
error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
|
||||
|
||||
console.error('Step 3: Building lock event...');
|
||||
error('Step 3: Building lock event...');
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: 30078,
|
||||
@@ -395,17 +348,17 @@ export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
sk,
|
||||
);
|
||||
|
||||
console.error(` Event ID: ${event.id}`);
|
||||
error(` Event ID: ${event.id}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
|
||||
console.error('RACE CHECK: won (dry run — no real contention possible)');
|
||||
error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
|
||||
error('RACE CHECK: won (dry run — no real contention possible)');
|
||||
} else {
|
||||
console.error('Step 4: Publishing to relays...');
|
||||
error('Step 4: Publishing to relays...');
|
||||
await publishLock(relays, event);
|
||||
|
||||
console.error('Step 5: Race check...');
|
||||
await new Promise((resolve) => setTimeout(resolve, RACE_CHECK_DELAY_MS));
|
||||
error('Step 5: Race check...');
|
||||
await new Promise((resolve) => setTimeout(resolve, raceCheckDelayMs));
|
||||
|
||||
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const racingLocks = postLocks
|
||||
@@ -414,46 +367,57 @@ export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
|
||||
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
|
||||
const winner = racingLocks[0];
|
||||
console.error(
|
||||
error(
|
||||
`RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=race_lost');
|
||||
console.log('LOCK_REASON=earlier_claim_exists');
|
||||
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||
log('LOCK_STATUS=race_lost');
|
||||
log('LOCK_REASON=earlier_claim_exists');
|
||||
log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||
throw new ExitError(3, 'Race check lost');
|
||||
}
|
||||
|
||||
console.error('RACE CHECK: won');
|
||||
error('RACE CHECK: won');
|
||||
}
|
||||
|
||||
console.log('LOCK_STATUS=ok');
|
||||
console.log(`LOCK_EVENT_ID=${event.id}`);
|
||||
console.log(`LOCK_PUBKEY=${pk}`);
|
||||
console.log(`LOCK_AGENT=${agent}`);
|
||||
console.log(`LOCK_CADENCE=${cadence}`);
|
||||
console.log(`LOCK_DATE=${dateStr}`);
|
||||
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
||||
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
||||
log('LOCK_STATUS=ok');
|
||||
log(`LOCK_EVENT_ID=${event.id}`);
|
||||
log(`LOCK_PUBKEY=${pk}`);
|
||||
log(`LOCK_AGENT=${agent}`);
|
||||
log(`LOCK_CADENCE=${cadence}`);
|
||||
log(`LOCK_DATE=${dateStr}`);
|
||||
log(`LOCK_EXPIRES=${expiresAt}`);
|
||||
log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
||||
return { status: 'ok', eventId: event.id };
|
||||
}
|
||||
|
||||
export async function cmdList(cadence) {
|
||||
export async function cmdList(cadence, deps = {}) {
|
||||
const {
|
||||
getRelays = _getRelays,
|
||||
getNamespace = _getNamespace,
|
||||
queryLocks = _queryLocks,
|
||||
getRoster = _getRoster,
|
||||
getDateStr = todayDateStr,
|
||||
log = console.log,
|
||||
error = console.error
|
||||
} = deps;
|
||||
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const dateStr = getDateStr();
|
||||
const cadences = cadence ? [cadence] : ['daily', 'weekly'];
|
||||
|
||||
console.error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`);
|
||||
error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`);
|
||||
|
||||
for (const c of cadences) {
|
||||
const locks = await queryLocks(relays, c, dateStr, namespace);
|
||||
|
||||
console.log(`\n${'='.repeat(72)}`);
|
||||
console.log(`Active ${namespace} ${c} locks (${dateStr})`);
|
||||
console.log('='.repeat(72));
|
||||
for (const { c, locks } of results) {
|
||||
log(`\n${'='.repeat(72)}`);
|
||||
log(`Active ${namespace} ${c} locks (${dateStr})`);
|
||||
log('='.repeat(72));
|
||||
|
||||
if (locks.length === 0) {
|
||||
console.log(' (no active locks)');
|
||||
log(' (no active locks)');
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -464,7 +428,7 @@ export async function cmdList(cadence) {
|
||||
const remaining = lock.expiresAt ? lock.expiresAt - nowUnix() : null;
|
||||
const remainMin = remaining ? Math.round(remaining / 60) : '?';
|
||||
|
||||
console.log(
|
||||
log(
|
||||
` ${(lock.agent ?? 'unknown').padEnd(30)} ` +
|
||||
`age: ${String(ageMin).padStart(4)}m ` +
|
||||
`ttl: ${String(remainMin).padStart(4)}m ` +
|
||||
@@ -480,11 +444,11 @@ export async function cmdList(cadence) {
|
||||
const available = roster.filter((a) => !lockedAgents.has(a));
|
||||
|
||||
if (unknownLockedAgents.length > 0) {
|
||||
console.log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`);
|
||||
log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`\n Locked: ${lockedAgents.size}/${roster.length}`);
|
||||
console.log(` Available: ${available.join(', ') || '(none)'}`);
|
||||
log(`\n Locked: ${lockedAgents.size}/${roster.length}`);
|
||||
log(` Available: ${available.join(', ') || '(none)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
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\)/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user