feat: add fake phase harness

This commit is contained in:
chungyeong
2026-05-10 16:48:52 +09:00
parent be0ddb6e4e
commit 64efeabd33
22 changed files with 5766 additions and 76 deletions

View File

@@ -8,8 +8,8 @@
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json",
"typecheck": "tsc -b --noEmit",
"test": "vitest run"
"typecheck": "tsc -p ../../tsconfig.typecheck.json --noEmit",
"test": "cd ../.. && vitest run --project packages/core"
},
"dependencies": {
"ajv": "8.17.1",

View File

@@ -1,9 +1,93 @@
import { describe, expect, it } from "vitest";
import { RunEventPayloadSchemas, RunEventTypeValues } from "./run-event.js";
import { RunEvent, RunEventPayloadSchemas, RunEventTypeValues } from "./run-event.js";
describe("run events", () => {
it("keeps a payload schema for every closed run event type", () => {
expect(Object.keys(RunEventPayloadSchemas).sort()).toEqual([...RunEventTypeValues].sort());
});
it("rejects malformed payloads for structured event families", () => {
expect(
RunEventPayloadSchemas["prompt.sent"].safeParse({
roleId: "implementer",
}).success,
).toBe(false);
expect(
RunEventPayloadSchemas["artifact.validated"].safeParse({
artifactId: "not-a-uuid",
hash: "not-a-sha",
path: "/tmp/spec.json",
schemaId: "dev/spec@1",
}).success,
).toBe(false);
expect(
RunEventPayloadSchemas["run.paused"].safeParse({
cause: "human_required:artifact_repair_failed",
}).success,
).toBe(false);
expect(RunEventPayloadSchemas["run.resumed"].safeParse({}).success).toBe(false);
expect(
RunEventPayloadSchemas["approval.resolved"].safeParse({
action: "pause",
approvalRequestId: "00000000-0000-4000-8000-000000000000",
}).success,
).toBe(false);
expect(
RunEventPayloadSchemas["approval.resolved"].safeParse({
action: "approve",
approvalRequestId: "00000000-0000-4000-8000-000000000000",
}).success,
).toBe(true);
expect(
RunEventPayloadSchemas["session.ready"].safeParse({
roleId: "implementer",
sessionId: "00000000-0000-4000-8000-000000000000",
}).success,
).toBe(false);
expect(
RunEventPayloadSchemas["session.failed"].safeParse({
sessionId: "00000000-0000-4000-8000-000000000000",
}).success,
).toBe(false);
expect(
RunEventPayloadSchemas["phase.started"].safeParse({
attempt: 0,
phaseKey: "implement",
}).success,
).toBe(false);
expect(
RunEventPayloadSchemas["artifact.expected"].safeParse({
attempt: 0,
path: "/tmp/spec.json",
schemaId: "dev/spec@1",
}).success,
).toBe(false);
expect(RunEventPayloadSchemas["phase.skipped"].safeParse({}).success).toBe(false);
expect(
RunEventPayloadSchemas["review.batch_recorded"].safeParse({
attempt: 0,
reviewerRole: "reviewer",
}).success,
).toBe(false);
});
it("binds exported RunEvent validation to each event type payload schema", () => {
expect(
RunEvent.safeParse({
type: "session.ready",
payload: {},
}).success,
).toBe(false);
expect(
RunEvent.safeParse({
type: "session.ready",
payload: {
recoveryAttempts: 0,
roleId: "implementer",
sessionId: "00000000-0000-4000-8000-000000000000",
},
}).success,
).toBe(true);
});
});

View File

@@ -1,5 +1,7 @@
import { z } from "zod";
import { ApprovalDecisionAction } from "./enums.js";
export const RunEventTypeValues = [
"run.created",
"run.started",
@@ -40,15 +42,146 @@ export const RunEventTypeValues = [
export const RunEventType = z.enum(RunEventTypeValues);
export type RunEventType = z.infer<typeof RunEventType>;
const payloadSchema = z.record(z.unknown());
const uuid = z.string().uuid();
const sha256 = z.string().regex(/^[a-f0-9]{64}$/);
const nonEmptyString = z.string().min(1);
const phaseAttempt = z.number().int().positive();
export const RunEventPayloadSchemas = Object.freeze(
Object.fromEntries(RunEventTypeValues.map((type) => [type, payloadSchema])),
) as Readonly<Record<RunEventType, typeof payloadSchema>>;
export const RunEvent = z.object({
type: RunEventType,
payload: payloadSchema,
const looseObject = z.object({}).passthrough();
const phasePayload = z
.object({
phaseKey: nonEmptyString,
attempt: phaseAttempt,
})
.passthrough();
const promptPayload = z
.object({
roleId: nonEmptyString,
dedupKey: sha256,
})
.passthrough();
const artifactWaitPayload = z
.object({
path: nonEmptyString,
schemaId: nonEmptyString,
attempt: phaseAttempt,
})
.passthrough();
const artifactValidationPayload = z
.object({
artifactId: uuid,
hash: sha256,
path: nonEmptyString,
schemaId: nonEmptyString,
})
.passthrough();
const sessionBasePayload = z
.object({
sessionId: uuid,
roleId: nonEmptyString,
})
.passthrough();
const sessionPromptPayload = sessionBasePayload.extend({
dedupKey: sha256,
});
const sessionRecoveryPayload = sessionBasePayload.extend({
recoveryAttempts: z.number().int().nonnegative(),
});
const approvalRequestedPayload = z
.object({
approvalRequestId: uuid,
approvalIdempotencyKey: nonEmptyString,
gateKey: nonEmptyString,
})
.passthrough();
const approvalResolvedPayload = z
.object({
action: ApprovalDecisionAction,
approvalRequestId: uuid,
})
.passthrough();
const commandPayload = z
.object({
commandId: uuid,
})
.passthrough();
const findingVerifierResolvedPayload = z
.object({
findingId: uuid,
})
.passthrough();
const backtestIterationPayload = z
.object({
iterationId: uuid,
})
.passthrough();
const reviewBatchRecordedPayload = z
.object({
reviewerRole: nonEmptyString,
attempt: phaseAttempt,
})
.passthrough();
const runPausedPayload = z
.object({
cause: nonEmptyString,
pausedFromState: nonEmptyString,
})
.passthrough();
export const RunEventPayloadSchemas = Object.freeze({
"run.created": looseObject,
"run.started": looseObject,
"run.paused": runPausedPayload,
"run.resumed": runPausedPayload.pick({ cause: true }).passthrough(),
"run.completed": looseObject,
"run.failed": looseObject,
"run.aborted": looseObject,
"phase.started": phasePayload,
"phase.completed": phasePayload,
"phase.failed": phasePayload.extend({ reason: nonEmptyString.optional() }),
"phase.skipped": phasePayload,
"prompt.sent": promptPayload,
"prompt.repaired": promptPayload,
"artifact.expected": artifactWaitPayload,
"artifact.validated": artifactValidationPayload,
"artifact.invalid": artifactValidationPayload.extend({ errors: z.array(z.unknown()) }),
"artifact.timeout": artifactWaitPayload,
"approval.requested": approvalRequestedPayload,
"approval.resolved": approvalResolvedPayload,
"session.created": sessionBasePayload.extend({ backend: nonEmptyString }),
"session.ready": sessionRecoveryPayload,
"session.busy": sessionPromptPayload,
"session.idle": sessionPromptPayload,
"session.crashed": sessionRecoveryPayload,
"session.recovered": sessionRecoveryPayload,
"session.failed": sessionBasePayload,
"command.started": commandPayload,
"command.completed": commandPayload,
"command.failed": commandPayload,
"review.batch_recorded": reviewBatchRecordedPayload,
"finding.verifier_resolved": findingVerifierResolvedPayload,
"backtest.iteration_started": backtestIterationPayload,
"backtest.iteration_completed": backtestIterationPayload,
"backtest.objective_evaluated": backtestIterationPayload,
} satisfies Record<RunEventType, z.ZodTypeAny>) as Readonly<Record<RunEventType, z.ZodTypeAny>>;
export const RunEvent = z
.object({
type: RunEventType,
payload: z.unknown(),
})
.superRefine((event, ctx) => {
const payload = RunEventPayloadSchemas[event.type].safeParse(event.payload);
if (payload.success) {
return;
}
for (const issue of payload.error.issues) {
ctx.addIssue({
...issue,
path: ["payload", ...issue.path],
});
}
});
export type RunEvent = z.infer<typeof RunEvent>;