mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-04 11:51:53 +00:00
feat: add unit tests for src/lib.mjs via dependency injection
Refactor src/lib.mjs to support dependency injection for cmdCheck, cmdLock, and cmdList. This allows unit testing without heavy mocking frameworks or side effects. Add test/lib.test.mjs covering: - cmdCheck: happy path, paused/excluded agents, unknown agents. - cmdLock: success, roster check, existing lock check, race check (lost), dry run. - cmdList: listing locks, empty state. Coverage for src/lib.mjs is now significantly improved. Verification: Ran `npm test` and verified all tests pass. Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
695adb6dc9
commit
9eda853e7e
2 files changed
+359
-59
No files matched your search
+97
-59
@@ -3,14 +3,14 @@
|
||||
// TORCH — Task Orchestration via Relay-Coordinated Handoff
|
||||
// Generic Nostr-based task locking for multi-agent development.
|
||||
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
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,
|
||||
getRelays,
|
||||
getNamespace,
|
||||
getTtl,
|
||||
loadTorchConfig as _loadTorchConfig,
|
||||
getRelays as _getRelays,
|
||||
getNamespace as _getNamespace,
|
||||
getTtl as _getTtl,
|
||||
} from './torch-config.mjs';
|
||||
import {
|
||||
DEFAULT_DASHBOARD_PORT,
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
VALID_CADENCES,
|
||||
} from './constants.mjs';
|
||||
import { cmdInit, cmdUpdate } from './ops.mjs';
|
||||
import { getRoster } from './roster.mjs';
|
||||
import { queryLocks, publishLock, parseLockEvent } 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';
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
@@ -41,19 +41,31 @@ function nowUnix() {
|
||||
}
|
||||
|
||||
// Re-export for backward compatibility/library usage
|
||||
export { parseLockEvent, queryLocks, publishLock, cmdDashboard };
|
||||
export { parseLockEvent, cmdDashboard, _queryLocks as queryLocks, _publishLock as publishLock };
|
||||
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
@@ -83,51 +95,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,
|
||||
@@ -153,17 +181,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
|
||||
@@ -172,36 +200,46 @@ 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(', ')}`);
|
||||
|
||||
const results = await Promise.all(
|
||||
cadences.map(async (c) => {
|
||||
@@ -211,12 +249,12 @@ export async function cmdList(cadence) {
|
||||
);
|
||||
|
||||
for (const { c, locks } of results) {
|
||||
console.log(`\n${'='.repeat(72)}`);
|
||||
console.log(`Active ${namespace} ${c} locks (${dateStr})`);
|
||||
console.log('='.repeat(72));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -227,7 +265,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 ` +
|
||||
@@ -242,11 +280,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