Files
TORCH/test/scheduler-time-window.test.mjs
T
thePR0M3TH3ANandClaude Sonnet 4.6 a5f08ff082 feat(scheduler): add local time-window guard to prevent within-window re-runs
Adds a cadence-aware time-window check that runs after the Nostr relay lock
check to exclude agents that already ran within the current window (24h daily,
7d weekly). This closes the cross-midnight edge case where the relay's
date-scoped d-tag rolls over but the agent ran minutes ago.

Changes:
- scheduler-utils.mjs: export log-parsing helpers (parseDateValue,
  parseTimestampFromFilename, parseAgentFromFilename, isStrictSchedulerLogFilename,
  parseFrontmatterCreatedAt, parseFrontmatterAgent, parseFrontmatterValue) plus
  CADENCE_WINDOW_MS constant and buildRecentlyRunExclusionSet(). Helpers are
  exported so they can be unit-tested independently. Adds 'node:path' import.
- run-scheduler-cycle.mjs: import the moved helpers from utils (removes ~45 lines
  of duplicate definitions), call buildRecentlyRunExclusionSet after excludedSet
  is built from relay data, merge results, log newly-excluded agents. Adds cycle
  saturation detection: when all roster agents are covered by the time-window
  guard, exit 0 with a clear "Cycle complete" message instead of a failure.
- test/scheduler-time-window.test.mjs: 30 new scenario-first unit tests covering
  SCN-TWG-01..11 (in-window exclusion, out-of-window pass-through, cross-midnight,
  frontmatter precedence, weekly threshold, empty dir, missing dir, deferred
  log passthrough, mixed rosters, anti-cheat for non-log files).

All 30 new tests pass. Existing scheduler-ratchet tests (7) still pass.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
2026-02-20 11:05:52 -05:00

311 lines
12 KiB
JavaScript

/**
* Unit tests for the scheduler time-window guard.
*
* Scenario-first, cheat-resistant (Dark Factory standard):
* - Every test asserts observable outcomes at the boundary of
* buildRecentlyRunExclusionSet (the Set it returns).
* - Determinism controls: `now` is injected so the clock is fixed.
* - No mocking of internal logic; real temp-directory I/O is used so
* the implementation cannot "return true" and pass.
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import {
buildRecentlyRunExclusionSet,
parseDateValue,
parseTimestampFromFilename,
parseAgentFromFilename,
isStrictSchedulerLogFilename,
parseFrontmatterCreatedAt,
parseFrontmatterAgent,
CADENCE_WINDOW_MS,
} from '../scripts/agent/scheduler-utils.mjs';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/** Create a task log file with optional YAML frontmatter. */
async function writeLog(dir, filename, { createdAt } = {}) {
const frontmatter = createdAt
? `---\nagent: test\ncreated_at: ${createdAt}\n---\n`
: '';
await fs.writeFile(path.join(dir, filename), `${frontmatter}# log\n`, 'utf8');
}
/** ISO string with colons replaced by dashes (canonical log filename format). */
function isoToFilename(isoString) {
return isoString.replace(/:/g, '-').replace(/\.\d+Z$/, 'Z');
}
/** Build a canonical log filename for a given agent/status/time. */
function logFilename(agent, status, isoString) {
return `${isoToFilename(isoString)}__${agent}__${status}.md`;
}
// Fixed reference point: 2026-02-20T12:00:00Z
const NOW = new Date('2026-02-20T12:00:00Z').getTime();
// ---------------------------------------------------------------------------
// Unit tests for helper utilities
// ---------------------------------------------------------------------------
describe('parseDateValue', () => {
it('parses a valid ISO string', () => {
assert.equal(parseDateValue('2026-02-20T12:00:00Z'), NOW);
});
it('returns null for empty input', () => {
assert.equal(parseDateValue(''), null);
assert.equal(parseDateValue(null), null);
assert.equal(parseDateValue(undefined), null);
});
it('returns null for invalid date strings', () => {
assert.equal(parseDateValue('not-a-date'), null);
});
});
describe('isStrictSchedulerLogFilename', () => {
it('accepts valid completed log', () => {
assert.ok(isStrictSchedulerLogFilename('2026-02-20T12-00-00Z__audit-agent__completed.md'));
});
it('accepts valid failed log', () => {
assert.ok(isStrictSchedulerLogFilename('2026-02-20T12-00-00Z__audit-agent__failed.md'));
});
it('rejects deferred logs', () => {
assert.ok(!isStrictSchedulerLogFilename('2026-02-20T12-00-00Z__audit-agent__deferred.md'));
});
it('rejects filenames without timestamp', () => {
assert.ok(!isStrictSchedulerLogFilename('audit-agent__completed.md'));
});
it('rejects plain text files', () => {
assert.ok(!isStrictSchedulerLogFilename('notes.md'));
});
it('rejects .scheduler-run-state.json', () => {
assert.ok(!isStrictSchedulerLogFilename('.scheduler-run-state.json'));
});
});
describe('parseAgentFromFilename', () => {
it('extracts agent name from completed log', () => {
assert.equal(parseAgentFromFilename('2026-02-20T12-00-00Z__audit-agent__completed.md'), 'audit-agent');
});
it('extracts agent name from failed log', () => {
assert.equal(parseAgentFromFilename('2026-02-20T12-00-00Z__ci-health-agent__failed.md'), 'ci-health-agent');
});
it('returns null for invalid filename', () => {
assert.equal(parseAgentFromFilename('bad-file.md'), null);
});
});
describe('parseTimestampFromFilename', () => {
it('parses timestamp from canonical filename', () => {
const filename = '2026-02-20T12-00-00Z__audit-agent__completed.md';
assert.equal(parseTimestampFromFilename(filename), NOW);
});
it('returns null for non-canonical filename', () => {
assert.equal(parseTimestampFromFilename('audit-agent__completed.md'), null);
});
});
describe('parseFrontmatterCreatedAt / parseFrontmatterAgent', () => {
const doc = `---\nagent: known-issues-agent\ncreated_at: 2026-02-20T12:00:00Z\nstatus: completed\n---\n# body\n`;
it('reads created_at from frontmatter', () => {
assert.equal(parseFrontmatterCreatedAt(doc), '2026-02-20T12:00:00Z');
});
it('reads agent from frontmatter', () => {
assert.equal(parseFrontmatterAgent(doc), 'known-issues-agent');
});
it('returns null when no frontmatter', () => {
assert.equal(parseFrontmatterCreatedAt('# no frontmatter'), null);
assert.equal(parseFrontmatterAgent('# no frontmatter'), null);
});
});
// ---------------------------------------------------------------------------
// Scenario tests for buildRecentlyRunExclusionSet
// ---------------------------------------------------------------------------
describe('buildRecentlyRunExclusionSet — scenarios', () => {
let tmpDir;
before(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-time-window-'));
});
after(async () => {
await fs.rm(tmpDir, { recursive: true, force: true });
});
// SCN-TWG-01
it('SCN-TWG-01: excludes agents with completed logs within 24-hour daily window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn01-'));
// Ran 1 hour ago — within the 24h window
const recentTs = new Date(NOW - 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('audit-agent', 'completed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('audit-agent'), 'audit-agent ran 1h ago should be excluded');
assert.equal(result.size, 1);
});
// SCN-TWG-02
it('SCN-TWG-02: does NOT exclude agents whose last run is outside the 24-hour window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn02-'));
// Ran 25 hours ago — outside the window
const oldTs = new Date(NOW - 25 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('audit-agent', 'completed', oldTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(!result.has('audit-agent'), 'agent outside window should not be excluded');
assert.equal(result.size, 0);
});
// SCN-TWG-03
it('SCN-TWG-03: excludes agents with failed logs within the window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn03-'));
const recentTs = new Date(NOW - 30 * 60 * 1000).toISOString(); // 30 min ago
await writeLog(dir, logFilename('ci-health-agent', 'failed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('ci-health-agent'), 'recently failed agent should be excluded');
});
// SCN-TWG-04
it('SCN-TWG-04: cross-midnight scenario — agent ran 10 min before midnight, checked 10 min after', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn04-'));
const tenMinutesAgo = new Date(NOW - 20 * 60 * 1000).toISOString(); // 20 min ago (simulates cross-midnight)
await writeLog(dir, logFilename('load-test-agent', 'completed', tenMinutesAgo));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('load-test-agent'), 'cross-midnight agent should still be excluded');
});
// SCN-TWG-05
it('SCN-TWG-05: uses frontmatter created_at over filename timestamp when both present', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn05-'));
// Filename timestamp = 26h ago (outside window), but frontmatter says 1h ago (inside window)
const oldTs = new Date(NOW - 26 * 60 * 60 * 1000).toISOString();
const recentFrontmatter = new Date(NOW - 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('docs-agent', 'completed', oldTs), { createdAt: recentFrontmatter });
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('docs-agent'), 'frontmatter created_at should take precedence over filename timestamp');
});
// SCN-TWG-06
it('SCN-TWG-06: weekly window uses 7-day threshold', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn06-'));
// 5 days ago — within 7-day window
const inWindowTs = new Date(NOW - 5 * 24 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('changelog-agent', 'completed', inWindowTs));
// 8 days ago — outside 7-day window
const outWindowTs = new Date(NOW - 8 * 24 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('dead-code-agent', 'completed', outWindowTs));
const result = await buildRecentlyRunExclusionSet(dir, 'weekly', NOW);
assert.ok(result.has('changelog-agent'), 'agent 5d ago should be excluded for weekly');
assert.ok(!result.has('dead-code-agent'), 'agent 8d ago should not be excluded for weekly');
});
// SCN-TWG-07
it('SCN-TWG-07: empty log directory returns empty set', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn07-'));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.equal(result.size, 0);
});
// SCN-TWG-08
it('SCN-TWG-08: non-existent directory returns empty set without throwing', async () => {
const nonExistent = path.join(tmpDir, 'does-not-exist-' + Date.now());
const result = await buildRecentlyRunExclusionSet(nonExistent, 'daily', NOW);
assert.equal(result.size, 0);
});
// SCN-TWG-09
it('SCN-TWG-09: only the most recent log matters — agent excluded if any log is within window', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn09-'));
// Two logs: one old, one recent
const oldTs = new Date(NOW - 25 * 60 * 60 * 1000).toISOString();
const recentTs = new Date(NOW - 2 * 60 * 60 * 1000).toISOString();
await writeLog(dir, logFilename('perf-agent', 'completed', oldTs));
await writeLog(dir, logFilename('perf-agent', 'failed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('perf-agent'), 'agent with any log within window should be excluded');
});
// SCN-TWG-10
it('SCN-TWG-10: mixed roster — correctly partitions in-window vs out-of-window agents', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn10-'));
const recentTs = new Date(NOW - 1 * 60 * 60 * 1000).toISOString(); // 1h ago
const oldTs = new Date(NOW - 30 * 60 * 60 * 1000).toISOString(); // 30h ago
await writeLog(dir, logFilename('audit-agent', 'completed', recentTs));
await writeLog(dir, logFilename('ci-health-agent', 'completed', oldTs));
await writeLog(dir, logFilename('log-fixer-agent', 'failed', recentTs));
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.ok(result.has('audit-agent'));
assert.ok(!result.has('ci-health-agent'));
assert.ok(result.has('log-fixer-agent'));
assert.equal(result.size, 2);
});
// SCN-TWG-11 — anti-cheat: verify the guard cannot be trivially bypassed
it('SCN-TWG-11: non-log files in directory are ignored', async () => {
const dir = await fs.mkdtemp(path.join(tmpDir, 'scn11-'));
// Write files that are NOT canonical log filenames
await fs.writeFile(path.join(dir, '.scheduler-run-state.json'), '{}', 'utf8');
await fs.writeFile(path.join(dir, 'README.md'), '# readme', 'utf8');
await fs.writeFile(path.join(dir, '2026-02-20T12-00-00Z__audit-agent__deferred.md'), 'deferred', 'utf8');
const result = await buildRecentlyRunExclusionSet(dir, 'daily', NOW);
assert.equal(result.size, 0, 'non-log files and deferred logs must not trigger exclusion');
});
});
describe('CADENCE_WINDOW_MS constants', () => {
it('daily window is exactly 24 hours', () => {
assert.equal(CADENCE_WINDOW_MS.daily, 24 * 60 * 60 * 1000);
});
it('weekly window is exactly 7 days', () => {
assert.equal(CADENCE_WINDOW_MS.weekly, 7 * 24 * 60 * 60 * 1000);
});
});