mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
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]>
This commit is contained in:
1 parent
8a9080e7c3
commit
a5f08ff082
3 files changed
+467
-49
No files matched your search
@@ -15,6 +15,13 @@ import {
|
||||
excerptText,
|
||||
getRunDateKey,
|
||||
toYamlScalar,
|
||||
buildRecentlyRunExclusionSet,
|
||||
parseDateValue,
|
||||
parseFrontmatterCreatedAt,
|
||||
parseFrontmatterAgent,
|
||||
parseTimestampFromFilename,
|
||||
isStrictSchedulerLogFilename,
|
||||
parseAgentFromFilename,
|
||||
} from './scheduler-utils.mjs';
|
||||
|
||||
import {
|
||||
@@ -125,55 +132,6 @@ async function verifyMemoryStep({ name, markers, artifacts, outputText, sinceMs
|
||||
};
|
||||
}
|
||||
|
||||
function parseFrontmatterAgent(markdown) {
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
if (lines[0]?.trim() !== '---') return null;
|
||||
for (let i = 1; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === '---') break;
|
||||
const match = line.match(/^agent\s*:\s*(.+)$/i);
|
||||
if (match?.[1]) {
|
||||
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseFrontmatterCreatedAt(markdown) {
|
||||
const lines = markdown.split(/\r?\n/);
|
||||
if (lines[0]?.trim() !== '---') return null;
|
||||
for (let i = 1; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === '---') break;
|
||||
const match = line.match(/^created_at\s*:\s*(.+)$/i);
|
||||
if (match?.[1]) {
|
||||
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseDateValue(value) {
|
||||
if (!value || typeof value !== 'string') return null;
|
||||
const ms = Date.parse(value);
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
|
||||
function parseTimestampFromFilename(filename) {
|
||||
const match = String(filename).match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)__[^_]+__(completed|failed)\.md$/);
|
||||
if (!match?.[1]) return null;
|
||||
const iso = match[1].replace(/T(\d{2})-(\d{2})-(\d{2})Z$/, 'T$1:$2:$3Z');
|
||||
return parseDateValue(iso);
|
||||
}
|
||||
|
||||
function isStrictSchedulerLogFilename(filename) {
|
||||
return /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)__[^_]+__(completed|failed)\.md$/.test(String(filename));
|
||||
}
|
||||
|
||||
function parseAgentFromFilename(filename) {
|
||||
const match = String(filename).match(/^.+__([^_]+?)__(completed|failed)\.md$/);
|
||||
return match?.[1] || null;
|
||||
}
|
||||
|
||||
async function getLatestFile(logDir) {
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
@@ -486,6 +444,19 @@ async function main() {
|
||||
])];
|
||||
const excludedSet = new Set(excluded);
|
||||
|
||||
// Time-window guard: exclude agents that already ran within the cadence window
|
||||
// (24 h for daily, 7 d for weekly). This catches the cross-midnight edge case
|
||||
// where the relay's date-scoped lock has rolled over but the run was recent.
|
||||
const windowLabel = cadence === 'weekly' ? '7-day' : '24-hour';
|
||||
const recentlyRunSet = await buildRecentlyRunExclusionSet(logDir, cadence);
|
||||
const newlyTimeExcluded = [...recentlyRunSet].filter((agent) => !excludedSet.has(agent));
|
||||
for (const agent of recentlyRunSet) {
|
||||
excludedSet.add(agent);
|
||||
}
|
||||
if (newlyTimeExcluded.length > 0) {
|
||||
console.log(`[scheduler] Time-window guard (${windowLabel}): ${newlyTimeExcluded.join(', ')} excluded — ran within window`);
|
||||
}
|
||||
|
||||
const latestFile = await getLatestFile(logDir);
|
||||
let previousAgent = null;
|
||||
|
||||
@@ -506,6 +477,22 @@ async function main() {
|
||||
});
|
||||
|
||||
if (!selectedAgent) {
|
||||
// Cycle saturation: every roster agent has a recent local log within the
|
||||
// window. This is the expected steady state once a full rotation completes.
|
||||
// Exit 0 so the triggering process (cron, CI) does not treat it as an error.
|
||||
const allCycleSaturated = roster.every((agent) => recentlyRunSet.has(agent));
|
||||
if (allCycleSaturated) {
|
||||
const reason = `Cycle complete: all ${roster.length} agents ran within the ${windowLabel} window`;
|
||||
console.log(`[scheduler] ${reason}. Nothing to do.`);
|
||||
await exitWithSummary(0, {
|
||||
status: 'skipped',
|
||||
agent: 'scheduler',
|
||||
promptPath: null,
|
||||
reason,
|
||||
platform,
|
||||
});
|
||||
}
|
||||
|
||||
await writeLog({
|
||||
cadence,
|
||||
agent: 'scheduler',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const ALLOWED_ENV_KEYS = new Set([
|
||||
@@ -198,3 +199,123 @@ export function toYamlScalar(value) {
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Time-window guard helpers
|
||||
// Exported so they can be unit-tested independently of run-scheduler-cycle.mjs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Window sizes for the per-cadence duplicate-run guard. */
|
||||
export const CADENCE_WINDOW_MS = {
|
||||
daily: 24 * 60 * 60 * 1000, // 24 hours
|
||||
weekly: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
};
|
||||
|
||||
/** Parse a date string to milliseconds, or null if invalid. */
|
||||
export function parseDateValue(value) {
|
||||
if (!value || typeof value !== 'string') return null;
|
||||
const ms = Date.parse(value);
|
||||
return Number.isFinite(ms) ? ms : null;
|
||||
}
|
||||
|
||||
/** Returns true when a filename matches the canonical scheduler log pattern. */
|
||||
export function isStrictSchedulerLogFilename(filename) {
|
||||
return /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)__[^_]+__(completed|failed)\.md$/.test(String(filename));
|
||||
}
|
||||
|
||||
/** Extract agent name from a canonical log filename, or null. */
|
||||
export function parseAgentFromFilename(filename) {
|
||||
const match = String(filename).match(/^.+__([^_]+?)__(completed|failed)\.md$/);
|
||||
return match?.[1] || null;
|
||||
}
|
||||
|
||||
/** Extract the ISO timestamp embedded in a canonical log filename as ms, or null. */
|
||||
export function parseTimestampFromFilename(filename) {
|
||||
const match = String(filename).match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)__[^_]+__(completed|failed)\.md$/);
|
||||
if (!match?.[1]) return null;
|
||||
const iso = match[1].replace(/T(\d{2})-(\d{2})-(\d{2})Z$/, 'T$1:$2:$3Z');
|
||||
return parseDateValue(iso);
|
||||
}
|
||||
|
||||
/** Read a single YAML frontmatter key from a markdown string, or null. */
|
||||
export function parseFrontmatterValue(markdown, key) {
|
||||
const lines = String(markdown).split(/\r?\n/);
|
||||
if (lines[0]?.trim() !== '---') return null;
|
||||
const keyPattern = new RegExp(`^${key}\\s*:\\s*(.+)$`, 'i');
|
||||
for (let i = 1; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
if (line.trim() === '---') break;
|
||||
const match = line.match(keyPattern);
|
||||
if (match?.[1]) {
|
||||
return match[1].trim().replace(/^['"]|['"]$/g, '');
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Read `created_at` from YAML frontmatter. */
|
||||
export function parseFrontmatterCreatedAt(markdown) {
|
||||
return parseFrontmatterValue(markdown, 'created_at');
|
||||
}
|
||||
|
||||
/** Read `agent` from YAML frontmatter. */
|
||||
export function parseFrontmatterAgent(markdown) {
|
||||
return parseFrontmatterValue(markdown, 'agent');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans task-log files for a cadence and returns a Set of agent names that
|
||||
* completed or failed within the cadence's time window (24 h daily, 7 d weekly).
|
||||
*
|
||||
* This is the local-filesystem companion to the Nostr relay lock check. It
|
||||
* catches the cross-midnight edge case where the relay's date-scoped lock has
|
||||
* rolled over but the agent ran less than a full window ago.
|
||||
*
|
||||
* Only `completed` and `failed` logs are considered. `deferred` logs are
|
||||
* intentionally skipped so the scheduler retries lock-backend failures.
|
||||
*
|
||||
* Non-fatal: if the log directory cannot be read, an empty Set is returned.
|
||||
*
|
||||
* @param {string} logDir - Absolute or relative path to task-logs/<cadence>/.
|
||||
* @param {'daily'|'weekly'} cadence
|
||||
* @param {number} [now] - Current epoch ms (injectable for tests).
|
||||
* @returns {Promise<Set<string>>} Agent names that ran within the window.
|
||||
*/
|
||||
export async function buildRecentlyRunExclusionSet(logDir, cadence, now = Date.now()) {
|
||||
const windowMs = CADENCE_WINDOW_MS[cadence] ?? CADENCE_WINDOW_MS.daily;
|
||||
const cutoff = now - windowMs;
|
||||
const recentlyRun = new Set();
|
||||
|
||||
let entries;
|
||||
try {
|
||||
await fs.mkdir(logDir, { recursive: true });
|
||||
entries = await fs.readdir(logDir, { withFileTypes: true });
|
||||
} catch {
|
||||
return recentlyRun;
|
||||
}
|
||||
|
||||
const logFiles = entries
|
||||
.filter((e) => e.isFile() && isStrictSchedulerLogFilename(e.name))
|
||||
.map((e) => e.name);
|
||||
|
||||
await Promise.all(logFiles.map(async (filename) => {
|
||||
const agentName = parseAgentFromFilename(filename);
|
||||
if (!agentName) return;
|
||||
|
||||
// Prefer frontmatter created_at (more precise) over the filename timestamp.
|
||||
let effectiveMs = parseTimestampFromFilename(filename);
|
||||
try {
|
||||
const content = await fs.readFile(path.join(logDir, filename), 'utf8');
|
||||
const frontmatterMs = parseDateValue(parseFrontmatterCreatedAt(content));
|
||||
if (frontmatterMs !== null) effectiveMs = frontmatterMs;
|
||||
} catch {
|
||||
// Read failure: fall back to filename timestamp.
|
||||
}
|
||||
|
||||
if (effectiveMs !== null && effectiveMs >= cutoff) {
|
||||
recentlyRun.add(agentName);
|
||||
}
|
||||
}));
|
||||
|
||||
return recentlyRun;
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user