fix(evals): bound status-card setup by original case clock

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-02 20:58:49 -06:00
1 parent fa644b9b20
commit 0ecd43035a
2 files changed
+97 -5

No files matched your search

@@ -0,0 +1,80 @@
import { afterEach, expect, it, vi } from "vitest";
import type { ChatCatalogContext } from "../chat-catalog-contract.js";
vi.mock("./internal-definition.js", () => ({ fixtureWorker: async () => ({ id: "worker" }) }));
// This fixture isolates the prerequisite-generation wait; the required-input
// recipe and later feature actions retain their separate owning controls.
vi.mock("./status-card-work-failure.js", async original => ({ ...await original<typeof import("./status-card-work-failure.js")>(), prepareCardChecklist: async () => undefined }));
vi.mock("../chat-query-attachments.js", async original => ({ ...await original<typeof import("../chat-query-attachments.js")>(), uploadFactualQueryAttachment: async () => ({ id: "fixture-attachment" }) }));
import { integratorStatusCardsCases } from "./integrator-status-cards.js";
afterEach(() => { vi.useRealTimers(); vi.restoreAllMocks(); vi.unstubAllGlobals(); });
const start = 10_000;
const keys = ["status-cards.explain-card", "status-cards.other-authors-card", "status-cards.refresh-card"] as const;
function fixture(key: string, deadline: number | undefined) {
vi.useFakeTimers(); vi.setSystemTime(start);
let readyAt = start + 190_000, cardState = "active", summary = "Payment approval blocks Launch.", generated = true, pending = false, late = false, runStatus = "succeeded", noWork = false, lateNative = false;
const card = () => ({ id: "card", state: Date.now() >= readyAt ? cardState : "generating", summaryBody: Date.now() >= readyAt ? summary : null,
lastGeneratedAt: generated ? "2026-11-03T01:00:00Z" : null, generatingIssueId: pending ? "generation" : null, fingerprint: {} });
const get = vi.fn(async (path: string) => {
if (path === "/api/instance/settings/experimental") return { enableStatusCards: false };
if (path === "/api/status-cards/card") { if (late) vi.setSystemTime(deadline! + 1); return card(); }
if (path.endsWith("/updates") || path.endsWith("/summary-revisions")) return [];
throw Error("Unexpected fixture read");
});
const post = vi.fn(async (path: string) => {
if (path.endsWith("/projects")) return { id: "project" };
if (path.endsWith("/issues")) return { id: "source", companyId: "company", title: "Payment approval blocks launch", status: "blocked" };
if (path.endsWith("/keys")) return { id: "owned-key", token: "synthetic-fixture-key" };
if (path.endsWith("/budgets/policies")) return {};
throw Error("Unexpected fixture write");
});
const remove = vi.fn(async () => ({})), patch = vi.fn(async () => ({ id: "source", companyId: "company", status: "done" }));
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, json: async () => ({ id: "card" }) })));
const ctx = { scenario: { id: key }, observationDeadlineAt: deadline,
input: { nonce: "synthetic", privateSecrets: [], fixtures: { company: { id: "company", issuePrefix: "ACORN" }, agent: { id: "ceo" } },
api: { baseURL: "https://fixture.test", get, post, postSensitive: post, patch, delete: remove }, evidence: async () => {} },
allRuns: async () => { if (lateNative) vi.setSystemTime(deadline! + 1); return noWork ? [] : [{ id: "prerequisite", status: runStatus }]; },
} as unknown as ChatCatalogContext;
const definition = integratorStatusCardsCases.find(row => row.scenarioId === key)!;
const setup = () => definition.setup(ctx);
const settle = async (ms: number) => {
const result = setup().then(value => ({ value, error: undefined }), error => ({ value: undefined, error: error as Error }));
await vi.dynamicImportSettled(); await vi.advanceTimersByTimeAsync(ms); return result;
};
return { ctx, definition, setup, settle, get, remove, post, readyAt: (value: number) => { readyAt = value; },
state: (value: string) => { cardState = value; }, summary: (value: string) => { summary = value; },
generated: (value: boolean) => { generated = value; }, pending: () => { pending = true; }, late: () => { late = true; }, runStatus: (value: string) => { runStatus = value; }, noWork: () => { noWork = true; }, lateNative: () => { lateNative = true; } };
}
it.each(keys)("actual %s setup admits genuinely completed prerequisite after190s inside its original clock", async key => {
const cap = key.endsWith("refresh-card") ? 1_200_000 : key.endsWith("explain-card") ? 720_000 : 540_000;
const f = fixture(key, start + cap - 30_000), result = await f.settle(191_000);
expect(result.error).toBeUndefined(); expect(result.value?.card).toMatchObject({ id: "card", state: "active" });
expect(f.definition.attemptTimeoutMs).toBe(cap); expect(f.ctx.observationDeadlineAt).toBe(start + cap - 30_000);
expect(f.remove).toHaveBeenCalledWith("/api/agents/" + (key.endsWith("other-authors-card") ? "worker" : "ceo") + "/keys/owned-key", { allowNotFound: true });
});
it.each([undefined, NaN, Infinity, start, start - 1])("rejects absent/invalid/expired original deadline %s", async deadline => {
const f = fixture(keys[1], deadline); const result = await f.settle(181_000);
expect(result.error).toBeInstanceOf(Error); expect(result.value).toBeUndefined();
});
it.each(["error", "missing summary", "missing generation", "still generating", "late await"])("rejects %s rather than accepting a partial prerequisite", async fault => {
const f = fixture(keys[1], start + 1000); f.readyAt(start);
if (fault === "error") f.state("error");
if (fault === "missing summary") f.summary("");
if (fault === "missing generation") f.generated(false);
if (fault === "still generating") f.pending();
if (fault === "late await") f.late();
const result = await f.settle(3000); expect(result.error).toBeInstanceOf(Error); expect(result.value).toBeUndefined();
});
it.each(["failed", "cancelled", "timed_out"])("a completed card cannot hide actual native %s", async status => {
const f = fixture(keys[1], start + 1000); f.readyAt(start); f.runStatus(status);
const result = await f.settle(47_000); expect(result.error).toBeInstanceOf(Error); expect(result.value).toBeUndefined();
});
it("does not change the archive-card authored180s prerequisite phase", async () => {
const f = fixture("status-cards.archive-card", start + 510_000); const result = await f.settle(181_000);
expect(result.error?.message).toContain("actual prerequisite card generation"); expect(result.value).toBeUndefined();
});
it.each(["no actual run", "late native await"])("cannot treat %s as completed setup work", async fault => {
const f = fixture(keys[1], start + 1000); f.readyAt(start);
if (fault === "no actual run") f.noWork(); else f.lateNative();
const result = await f.settle(3000); expect(result.error).toBeInstanceOf(Error); expect(result.value).toBeUndefined();
});
@@ -5,7 +5,7 @@ import type { ChatCatalogCase, ChatCatalogContext } from "../chat-catalog-contra
import { pollUntil } from "../api.js";
import { checks, observation, project, task, sameTask, hasLink, type Entity, type InternalObservation } from "./internal-support.js";
import { prepareCardChecklist, updateCardChecklistSource, observeCardChecklistSuccess, observeCardWork, failCardChecklist, genuineCardWorkFailure, genuineCardChecklistSuccess, cardAccountingReady, cleanupCardChecklist, type CardRecipe, type CardWorkEvidence } from "./status-card-work-failure.js";
import { readAgentResource, cleanupIdentityProbes } from "./identity-support.js";
import { readAgentResource, cleanupIdentityProbes, remainingIdentityObservationBudget } from "./identity-support.js";
import { fixtureWorker } from "./internal-definition.js";
import { groundedStatement, integratorDefinition, inspectResult, resultHref, observeInternal } from "./integrator-support.js";
interface CardsObservation extends InternalObservation { cards: StatusCard[]; updates: Record<string, StatusCardUpdate[]>; summaries: Record<string, StatusCardSummaryRevision[]>; cardsEnabled: boolean; cardWork?: CardWorkEvidence }
@@ -120,9 +120,21 @@ async function verifyCard(ctx: ChatCatalogContext, _state: Record<string, unknow
const o = cardsObservation(value); const card = o && (o.state.card ? o.cards.find(row => row.id === (o.state.card as StatusCard).id) : o.cards[0]); if (!card) throw new Error("Missing saved card");
await inspectResult(ctx, `/${ctx.input.fixtures.company.issuePrefix}/status/${card.id}`, card.title ?? card.interestPrompt, cardLink(o!, card.id));
}
// These three prerequisite waits consumed a generic180s allowance before the
// query, although the unchanged whole-case clock is installed before setup.
const originalPrerequisiteClock = new Set(["status-cards.explain-card", "status-cards.other-authors-card", "status-cards.refresh-card"]);
function prerequisiteDeadline(ctx: ChatCatalogContext, legacyDeadline: number): number {
if (!originalPrerequisiteClock.has(ctx.scenario.id)) return legacyDeadline;
remainingIdentityObservationBudget(ctx);
return ctx.observationDeadlineAt!;
}
function prerequisiteIsTimely(ctx: ChatCatalogContext): boolean {
return !originalPrerequisiteClock.has(ctx.scenario.id) || Number.isFinite(ctx.observationDeadlineAt) && Date.now() < ctx.observationDeadlineAt!;
}
/** Author identity comes from a real revocable agent key. The compile and first
* summary are produced by an actual admitted summarizer run, never fixture PUTs. */
async function seedCard(ctx: ChatCatalogContext, authorId: string, summarizerId: string, projectId: string, projectName = "Launch", interestPrompt?: string) {
const deadlineAt = originalPrerequisiteClock.has(ctx.scenario.id) ? prerequisiteDeadline(ctx, 0) : undefined;
if (!ctx.input.privateSecrets) throw new Error("Card fixture author key needs private redaction");
const key = await ctx.input.api.postSensitive<{ id: string; token: string }>(`/api/agents/${authorId}/keys`, { name: `card-prerequisite-${ctx.input.nonce}` });
ctx.input.privateSecrets.push(key.token);
@@ -131,8 +143,8 @@ async function seedCard(ctx: ChatCatalogContext, authorId: string, summarizerId:
headers: { Authorization: `Bearer ${key.token}`, "content-type": "application/json" }, body: JSON.stringify({ title: `${projectName} blockers`, interestPrompt: interestPrompt ?? `Summarize the biggest blockers for ${projectName} project ${projectId}, citing actual source tasks.`, agentId: summarizerId, refreshPolicy: { mode: "manual" } }) });
if (!response.ok) { await response.body?.cancel(); throw new Error(`Card fixture public creation failed HTTP ${response.status}`); }
const created = await response.json() as StatusCard;
return await pollUntil({ label: "actual prerequisite card generation", deadlineAt: Date.now() + 180_000, intervalMs: 1000,
load: () => ctx.input.api.get<StatusCard>(`/api/status-cards/${created.id}`), accept: card => card.state === "active" && !!card.summaryBody && !!card.lastGeneratedAt && !card.generatingIssueId,
return await pollUntil({ label: "actual prerequisite card generation", deadlineAt: deadlineAt ?? Date.now() + 180_000, intervalMs: 1000,
load: () => ctx.input.api.get<StatusCard>(`/api/status-cards/${created.id}`), accept: card => prerequisiteIsTimely(ctx) && card.state === "active" && !!card.summaryBody && !!card.lastGeneratedAt && !card.generatingIssueId,
reject: card => card.state === "error" ? "Prerequisite card generation failed; no synthetic summary substituted" : undefined });
} finally { await ctx.input.api.delete(`/api/agents/${authorId}/keys/${key.id}`, { allowNotFound: true }); }
}
@@ -165,13 +177,13 @@ function define(id: string, seed: "none" | "ceo" | "morgan", extra: Partial<Chat
await ctx.input.api.post(`${root(ctx)}/budgets/policies`, { ...scope, amount: 500, hardStopEnabled: true, isActive: true, unpricedUsagePolicy: "allow" });
const cardRecipe = id === "status-cards.refresh-card" ? await prepareCardChecklist(ctx, worker.id, p.id, t) : undefined;
const card = await seedCard(ctx, seed === "ceo" ? ctx.input.fixtures.agent.id : worker.id, worker.id, p.id, "Launch", cardRecipe?.interestPrompt);
const initialComplete = await pollUntil({ label: "prerequisite summarizer native completion", deadlineAt: Date.now() + 45_000,
const initialComplete = await pollUntil({ label: "prerequisite summarizer native completion", deadlineAt: Math.min(Date.now() + 45_000, prerequisiteDeadline(ctx, Date.now() + 45_000)),
load: async () => {
const runs = await ctx.allRuns();
const update = cardRecipe ? (await ctx.input.api.get<StatusCardUpdate[]>(`/api/status-cards/${card.id}/updates`)).find(row => row.kind !== "compile" && row.status === "ok") : undefined;
const checklist = cardRecipe && update ? await observeCardChecklistSuccess(ctx, cardRecipe, update) : undefined;
return { runs, update, checklist };
}, accept: value => value.runs.length === 1 && value.runs.every(row => row.status === "succeeded") && (!cardRecipe || !!value.update && genuineCardChecklistSuccess(card.id, cardRecipe, value.update, value.checklist)) });
}, accept: value => prerequisiteIsTimely(ctx) && value.runs.length === 1 && value.runs.every(row => row.status === "succeeded") && (!cardRecipe || !!value.update && genuineCardChecklistSuccess(card.id, cardRecipe, value.update, value.checklist)) });
const initialChecklist = initialComplete.checklist;
if (initialChecklist) await ctx.input.evidence("chat-card-initial-required-checklist.json", initialChecklist);
let otherCard: StatusCard | undefined; const sourceTasks = [t];