165 lines
4.8 KiB
TypeScript
165 lines
4.8 KiB
TypeScript
import { Effect } from "effect";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
SmokeContractError,
|
|
advanceSmokeLoop,
|
|
decodeSmokeChatText,
|
|
decodeSmokeModelCatalog,
|
|
decodeSmokePlan,
|
|
decodeSmokeReview,
|
|
decodeSmokeWorkerReport,
|
|
initialSmokeLoopState,
|
|
redactSmokeText,
|
|
selectSmokeModelRoles,
|
|
validateSmokeCapabilities,
|
|
} from "./smoke";
|
|
|
|
const validRoleSelection = () => ({
|
|
plannerReviewerModel: "glm-5.2",
|
|
plannerReviewerProvider: "cheaptricks",
|
|
workerModel: "minimax-m3",
|
|
workerProvider: "cheaptricks",
|
|
workerToolCallMode: "serial" as const,
|
|
});
|
|
|
|
const run = <A, E>(effect: Effect.Effect<A, E>): A => Effect.runSync(effect);
|
|
|
|
describe("smoke primitives", () => {
|
|
it("selects the planner/reviewer and serial M3 worker roles", () => {
|
|
const roles = run(selectSmokeModelRoles(validRoleSelection()));
|
|
|
|
expect(roles.worker).toMatchObject({
|
|
model: "minimax-m3",
|
|
role: "worker",
|
|
toolCallMode: "serial",
|
|
});
|
|
expect(roles.plannerReviewer).toMatchObject({
|
|
model: "glm-5.2",
|
|
role: "planner-reviewer",
|
|
});
|
|
});
|
|
|
|
it("rejects a non-M3 worker or non-GLM planner", () => {
|
|
const worker = validRoleSelection();
|
|
worker.workerModel = "glm-5.2";
|
|
const workerError = run(Effect.flip(selectSmokeModelRoles(worker)));
|
|
expect(workerError).toBeInstanceOf(SmokeContractError);
|
|
expect(workerError.reason).toBe("InvalidModelRole");
|
|
|
|
const planner = validRoleSelection();
|
|
planner.plannerReviewerModel = "minimax-m3";
|
|
const plannerError = run(Effect.flip(selectSmokeModelRoles(planner)));
|
|
expect(plannerError.reason).toBe("InvalidModelRole");
|
|
});
|
|
|
|
it("advances only through the serial smoke loop", () => {
|
|
let state = initialSmokeLoopState();
|
|
state = run(advanceSmokeLoop(state, "preflight-passed"));
|
|
state = run(advanceSmokeLoop(state, "plan-validated"));
|
|
state = run(advanceSmokeLoop(state, "worker-succeeded"));
|
|
state = run(advanceSmokeLoop(state, "review-approved"));
|
|
|
|
expect(state.phase).toBe("completed");
|
|
expect(state.history).toEqual([
|
|
"preflight",
|
|
"planning",
|
|
"worker-running",
|
|
"reviewing",
|
|
"completed",
|
|
]);
|
|
});
|
|
|
|
it("turns contract failures into a terminal state", () => {
|
|
const state = run(
|
|
advanceSmokeLoop(
|
|
initialSmokeLoopState(),
|
|
"contract-failed",
|
|
"daemon is offline"
|
|
)
|
|
);
|
|
|
|
expect(state.phase).toBe("failed");
|
|
expect(state.lastError).toBe("daemon is offline");
|
|
});
|
|
|
|
it("rejects out-of-order loop transitions", () => {
|
|
const error = run(
|
|
Effect.flip(advanceSmokeLoop(initialSmokeLoopState(), "review-approved"))
|
|
);
|
|
|
|
expect(error).toBeInstanceOf(SmokeContractError);
|
|
expect(error.reason).toBe("InvalidTransition");
|
|
});
|
|
|
|
it("validates model reports at the primitive boundary", () => {
|
|
const plan = run(
|
|
decodeSmokePlan({
|
|
acceptanceCriteria: ["The button renders"],
|
|
nonGoals: [],
|
|
steps: ["Add the button", "Run the web check"],
|
|
summary: "Add a small button to the disposable project",
|
|
})
|
|
);
|
|
const worker = run(
|
|
decodeSmokeWorkerReport({
|
|
status: "completed",
|
|
summary: "Changed the project and ran the targeted check.",
|
|
})
|
|
);
|
|
const review = run(
|
|
decodeSmokeReview({
|
|
approved: true,
|
|
findings: [],
|
|
summary: "The change matches the acceptance criteria.",
|
|
})
|
|
);
|
|
|
|
expect(plan.steps).toHaveLength(2);
|
|
expect(worker.status).toBe("completed");
|
|
expect(review.approved).toBe(true);
|
|
});
|
|
|
|
it("rejects empty or malformed model output", () => {
|
|
const empty = run(
|
|
Effect.flip(
|
|
decodeSmokeChatText({ choices: [{ message: { content: " " } }] })
|
|
)
|
|
);
|
|
expect(empty.reason).toBe("InvalidModelResponse");
|
|
|
|
const malformed = run(Effect.flip(decodeSmokeReview({ approved: true })));
|
|
expect(malformed.reason).toBe("InvalidReport");
|
|
});
|
|
|
|
it("validates the canonical CPA model catalog", () => {
|
|
const catalog = run(
|
|
decodeSmokeModelCatalog({
|
|
data: [{ id: "minimax-m3" }, { id: "glm-5.2" }],
|
|
})
|
|
);
|
|
|
|
expect(catalog.data.map(({ id }) => id)).toEqual(["minimax-m3", "glm-5.2"]);
|
|
});
|
|
|
|
it("fails a capability report when any required check fails", () => {
|
|
const error = run(
|
|
Effect.flip(
|
|
validateSmokeCapabilities([
|
|
{ id: "web", message: "ready", passed: true },
|
|
{ id: "daemon", message: "daemon is offline", passed: false },
|
|
])
|
|
)
|
|
);
|
|
|
|
expect(error.reason).toBe("InvalidReport");
|
|
expect(error.message).toContain("daemon is offline");
|
|
});
|
|
|
|
it("redacts secrets without changing unrelated text", () => {
|
|
expect(
|
|
redactSmokeText("status token=secret-key endpoint=ok", ["secret-key"])
|
|
).toBe("status token=[REDACTED] endpoint=ok");
|
|
});
|
|
});
|