Files
openprose__prose/tools/cli/tests/evals/timeline-adaptive-cron.test.ts
dan 8564d8a7d2 feat(evals): reactor harness evaluation suite (phase 1b) (#102)
* feat(cli): add evaluation runner core

* feat(cli): add eval suite loading and cost mapping

* feat(cli): add public harness eval adapters

* feat(cli): add eval suite registry and runner helper

* fix(cli): harden eval runner safety gates

* feat(cli): harden eval evidence and isolation

* feat(cli): add eval claim eligibility gates

* Harden reactor eval claim eligibility

* Harden eval claim eligibility gates

* feat(eval): add reactor timeline case contract

* feat(eval): add reactor proof verdict library

* feat(eval): add timeline baseline adapters

* feat(eval): add docker isolation substrate

* feat(eval): add phase 1b reactor-independent tier 1

* feat(eval): add phase 1b cost and canary lanes

* feat(eval): add phase 1b statistical pilot

* feat(eval): reconcile openrouter costs and containerize competitors

* feat(evals): run isolated competitor harnesses locally

* feat(evals): add phase 1b report pilot runner

* feat(evals): add phase 1b confidence intervals

* fix(evals): harden provider host matching against URL prefix spoofing

CodeQL flagged "Incomplete URL substring sanitization" in
providerFromUrl: host.endsWith("anthropic.com") also matches an
attacker-controlled host like "evilanthropic.com". Match the exact
host or a proper subdomain (".anthropic.com") instead, applied to all
three provider domains via a hostMatches helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-28 16:17:42 -07:00

158 lines
5.0 KiB
TypeScript

import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import {
ADAPTIVE_CRON_CACHE_FILE,
createAdaptiveCronTimelineAdapter,
} from "../../src/evals/timeline-adapters/adaptive-cron.js";
import type {
JsonObject,
JsonValue,
ReactorTimelineAdapterEventContext,
ReactorTimelineEvent,
SurpriseLabel,
} from "../../src/evals/index.js";
import type { BaselineJudgeInput } from "../../src/evals/timeline-adapters/baseline-utils.js";
describe("createAdaptiveCronTimelineAdapter", () => {
test("resets the schedule on relevant changes", async () => {
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-adaptive-cron-"));
try {
const calls: BaselineJudgeInput[] = [];
const adapter = createAdaptiveCronTimelineAdapter({
baselineJudge: (input) => {
calls.push(input);
return { eventId: input.event.id, ordinal: calls.length };
},
});
const context = adapterContext(cacheRoot, adapter.name);
await adapter.onEvent(event("receipt-issued", "2026-05-17T12:00:00.000Z", "relevant-change"), context);
await adapter.onEvent(event("due-recheck", "2026-05-17T13:00:00.000Z", "silent-drift"), context);
const reset = await adapter.onEvent(
event("fresh-receipt", "2026-05-17T13:30:00.000Z", "relevant-change"),
context,
);
expect(reset.metadata).toEqual(
expect.objectContaining({
decision: "compute",
intervalMs: 60 * 60 * 1000,
nextDueAt: "2026-05-17T14:30:00.000Z",
resetInterval: true,
}),
);
expect(calls).toHaveLength(3);
expect(cacheState(cacheRoot).intervalMs).toBe(60 * 60 * 1000);
expect(cacheState(cacheRoot).nextDueAt).toBe("2026-05-17T14:30:00.000Z");
} finally {
rmSync(cacheRoot, { recursive: true, force: true });
}
});
test("skips noop events before the next due time", async () => {
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-adaptive-cron-"));
try {
const calls: BaselineJudgeInput[] = [];
const adapter = createAdaptiveCronTimelineAdapter({
baselineJudge: (input) => {
calls.push(input);
return { eventId: input.event.id };
},
});
const context = adapterContext(cacheRoot, adapter.name);
await adapter.onEvent(event("receipt-issued", "2026-05-17T12:00:00.000Z", "relevant-change"), context);
const skipped = await adapter.onEvent(event("quiet-tick", "2026-05-17T12:30:00.000Z", "noop"), context);
expect(skipped.metadata).toEqual(
expect.objectContaining({
decision: "skip",
intervalMs: 60 * 60 * 1000,
nextDueAt: "2026-05-17T13:00:00.000Z",
}),
);
expect(skipped.metrics).toEqual({ cacheHit: 0, modelCalls: 0 });
expect(calls).toHaveLength(1);
expect(cacheState(cacheRoot).nextDueAt).toBe("2026-05-17T13:00:00.000Z");
} finally {
rmSync(cacheRoot, { recursive: true, force: true });
}
});
test("rechecks and backs off at the next due time", async () => {
const cacheRoot = mkdtempSync(join(tmpdir(), "prose-adaptive-cron-"));
try {
const calls: BaselineJudgeInput[] = [];
const adapter = createAdaptiveCronTimelineAdapter({
baselineJudge: (input) => {
calls.push(input);
return input.priorVerdict === undefined
? { eventId: input.event.id }
: { eventId: input.event.id, prior: input.priorVerdict };
},
});
const context = adapterContext(cacheRoot, adapter.name);
await adapter.onEvent(event("receipt-issued", "2026-05-17T12:00:00.000Z", "relevant-change"), context);
const recheck = await adapter.onEvent(
event("due-recheck", "2026-05-17T13:00:00.000Z", "silent-drift"),
context,
);
expect(recheck.metadata).toEqual(
expect.objectContaining({
decision: "recheck",
intervalMs: 2 * 60 * 60 * 1000,
nextDueAt: "2026-05-17T15:00:00.000Z",
}),
);
expect(recheck.metrics).toEqual({ cacheHit: 0, modelCalls: 1 });
expect(calls).toHaveLength(2);
expect(calls[1]?.priorVerdict).toEqual({ eventId: "receipt-issued" });
expect(cacheState(cacheRoot).intervalMs).toBe(2 * 60 * 60 * 1000);
expect(cacheState(cacheRoot).lastVerdict).toEqual({
eventId: "due-recheck",
prior: { eventId: "receipt-issued" },
});
} finally {
rmSync(cacheRoot, { recursive: true, force: true });
}
});
});
function event(id: string, at: string, label: SurpriseLabel): ReactorTimelineEvent {
return {
id,
at,
label,
trigger: "input",
type: `fixture.${id}`,
};
}
function adapterContext(scenarioCacheDirectory: string, adapterName: string): ReactorTimelineAdapterEventContext {
return {
adapterName,
attemptId: "attempt-1",
caseId: "adaptive-cron-case",
eventIndex: 0,
runId: "run-1",
scenarioCacheDirectory,
startedAt: "2026-05-17T12:00:00.000Z",
};
}
function cacheState(cacheRoot: string): AdaptiveCronTestState {
return JSON.parse(readFileSync(join(cacheRoot, ADAPTIVE_CRON_CACHE_FILE), "utf8")).state as AdaptiveCronTestState;
}
interface AdaptiveCronTestState extends JsonObject {
intervalMs: number;
lastVerdict: JsonValue;
nextDueAt: string;
}