feat: add fake phase harness
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user