147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import type { FlowRunView } from "@peezy.tech/codex-flows/flow-runtime/client";
|
|
import {
|
|
maintenanceAttemptForWorkspaceDispatch,
|
|
maintenanceAttemptWithWorkspaceRuns,
|
|
patchUpstreamReleaseEvent,
|
|
} from "../src/flow";
|
|
import type {
|
|
CandidateRefRecord,
|
|
FlowDispatchRecord,
|
|
MaintenanceAttemptRecord,
|
|
} from "../src/types";
|
|
|
|
describe("maintenance attempt sync", () => {
|
|
test("aggregates multi-run fanout statuses and candidate refs", () => {
|
|
const attempt = baseAttempt();
|
|
const next = maintenanceAttemptWithWorkspaceRuns(attempt, [
|
|
flowRun("run-completed", "completed", [candidate("refs/heads/a", "aaa")]),
|
|
flowRun("run-changed", "changed", [candidate("refs/heads/b", "bbb")]),
|
|
], "2026-05-16T00:10:00.000Z");
|
|
|
|
expect(next.status).toBe("changed");
|
|
expect(next.workspaceRunIds).toEqual(["run-completed", "run-changed"]);
|
|
expect(next.workspaceRunStatuses).toEqual({
|
|
"run-completed": "completed",
|
|
"run-changed": "changed",
|
|
});
|
|
expect(next.candidateRefs).toMatchObject([
|
|
{ ref: "refs/heads/a", sha: "aaa" },
|
|
{ ref: "refs/heads/b", sha: "bbb" },
|
|
]);
|
|
expect(next.completedAt).toBe("2026-05-16T00:10:00.000Z");
|
|
});
|
|
|
|
test("uses failure precedence across partial fanout", () => {
|
|
expect(statusFor(["completed", "skipped"])).toBe("completed");
|
|
expect(statusFor(["skipped", "skipped"])).toBe("skipped");
|
|
expect(statusFor(["completed", "changed"])).toBe("changed");
|
|
expect(statusFor(["changed", "failed"])).toBe("failed");
|
|
expect(statusFor(["failed", "blocked"])).toBe("blocked");
|
|
expect(statusFor(["blocked", "needs_intervention"])).toBe("needs_intervention");
|
|
});
|
|
|
|
test("preserves successful candidates when another run fails", () => {
|
|
const next = maintenanceAttemptWithWorkspaceRuns(baseAttempt(), [
|
|
flowRun("run-ok", "completed", [candidate("refs/heads/candidate", "abc")]),
|
|
flowRun("run-failed", "failed", [], "release verification failed"),
|
|
]);
|
|
|
|
expect(next.status).toBe("failed");
|
|
expect(next.error).toBe("release verification failed");
|
|
expect(next.candidateRefs).toMatchObject([
|
|
{ ref: "refs/heads/candidate", sha: "abc" },
|
|
]);
|
|
});
|
|
|
|
test("records replay attempts with workspace run results", () => {
|
|
const event = patchUpstreamReleaseEvent({
|
|
repo: "openai/codex",
|
|
tag: "rust-v0.130.0",
|
|
receivedAt: "2026-05-16T00:00:00.000Z",
|
|
});
|
|
const record: FlowDispatchRecord = {
|
|
eventId: event.id,
|
|
eventType: event.type,
|
|
operation: "replay",
|
|
target: "workspace-backend",
|
|
transport: "workspace-ws",
|
|
workspaceBackendUrl: "ws://127.0.0.1:3586",
|
|
status: "dispatched",
|
|
runIds: ["run-a", "run-b"],
|
|
matched: 2,
|
|
createdAt: "2026-05-16T00:05:00.000Z",
|
|
};
|
|
|
|
const attempt = maintenanceAttemptForWorkspaceDispatch(event, record, [
|
|
flowRun("run-a", "completed"),
|
|
flowRun("run-b", "changed", [candidate("refs/heads/codex-candidate", "def")]),
|
|
]);
|
|
|
|
expect(attempt.id).toBe(`${event.id}:replay:${record.createdAt}`);
|
|
expect(attempt.operation).toBe("replay");
|
|
expect(attempt.status).toBe("changed");
|
|
expect(attempt.upstreamRepo).toBe("openai/codex");
|
|
expect(attempt.upstreamTag).toBe("rust-v0.130.0");
|
|
expect(attempt.workspaceRunIds).toEqual(["run-a", "run-b"]);
|
|
expect(attempt.candidateRefs).toMatchObject([
|
|
{ ref: "refs/heads/codex-candidate", sha: "def" },
|
|
]);
|
|
});
|
|
});
|
|
|
|
function statusFor(statuses: string[]): string {
|
|
return maintenanceAttemptWithWorkspaceRuns(
|
|
baseAttempt(),
|
|
statuses.map((status, index) => flowRun(`run-${index}`, status)),
|
|
).status;
|
|
}
|
|
|
|
function baseAttempt(): MaintenanceAttemptRecord {
|
|
return {
|
|
id: "attempt-1",
|
|
eventId: "event-1",
|
|
eventType: "upstream.release",
|
|
operation: "dispatch",
|
|
status: "started",
|
|
upstreamRepo: "openai/codex",
|
|
upstreamTag: "rust-v0.130.0",
|
|
workspaceRunIds: [],
|
|
candidateRefs: [],
|
|
createdAt: "2026-05-16T00:00:00.000Z",
|
|
updatedAt: "2026-05-16T00:00:00.000Z",
|
|
};
|
|
}
|
|
|
|
function flowRun(
|
|
id: string,
|
|
status: string,
|
|
candidateRefs: CandidateRefRecord[] = [],
|
|
message = `${id} ${status}`,
|
|
): FlowRunView {
|
|
return {
|
|
id,
|
|
eventId: "event-1",
|
|
flowName: "test-flow",
|
|
stepName: id,
|
|
status,
|
|
effectiveStatus: status,
|
|
completedAt: "2026-05-16T00:10:00.000Z",
|
|
resultPayload: {
|
|
status,
|
|
message,
|
|
artifacts: { candidateRefs },
|
|
},
|
|
} as FlowRunView;
|
|
}
|
|
|
|
function candidate(ref: string, sha: string): CandidateRefRecord {
|
|
return {
|
|
kind: "branch",
|
|
repo: "peezy-tech/codex",
|
|
remote: "local",
|
|
ref,
|
|
sha,
|
|
pushed: false,
|
|
};
|
|
}
|