mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-07 03:00:29 +00:00
192 lines
5.5 KiB
TypeScript
192 lines
5.5 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { StageHandler, StageState } from "@qltysh/fabro-api-client";
|
|
import type { RunArtifactEntry } from "@qltysh/fabro-api-client";
|
|
|
|
import type { Stage } from "../../lib/stage-sidebar";
|
|
import { groupArtifactsByFile, splitArtifactPath } from "./group";
|
|
|
|
function stage(nodeId: string, startedAt: string | null, visit = 1): Stage {
|
|
return {
|
|
id: `${nodeId}@${visit}`,
|
|
name: nodeId,
|
|
handler: StageHandler.AGENT,
|
|
nodeId,
|
|
visit,
|
|
graphVisit: null,
|
|
resumedFromStageId: null,
|
|
status: StageState.SUCCEEDED,
|
|
duration: "1s",
|
|
startedAt,
|
|
providerUsed: null,
|
|
};
|
|
}
|
|
|
|
function artifact(
|
|
nodeSlug: string,
|
|
path: string,
|
|
size: number,
|
|
retry = 1,
|
|
visit = 1,
|
|
): RunArtifactEntry {
|
|
return {
|
|
stage_id: `${nodeSlug}@${visit}`,
|
|
node_slug: nodeSlug,
|
|
retry,
|
|
relative_path: path,
|
|
size,
|
|
};
|
|
}
|
|
|
|
/** Mirrors run 01KYJ8ZR0N: one report rewritten by four stages. */
|
|
const REPORT = ".ai/reports/2026-07-27-wrk-002-instance-lifecycle.md";
|
|
|
|
const STAGES: Stage[] = [
|
|
stage("start", "2026-07-27T17:12:08Z"),
|
|
stage("plan", "2026-07-27T17:21:44Z"),
|
|
stage("implement_plan", "2026-07-27T17:44:18Z"),
|
|
stage("simplify", "2026-07-27T18:43:11Z"),
|
|
stage("consolidate_reviews", "2026-07-27T19:44:26Z"),
|
|
stage("fix_review_findings", "2026-07-27T19:49:22Z"),
|
|
];
|
|
|
|
describe("splitArtifactPath", () => {
|
|
test("splits a nested path into directory prefix and filename", () => {
|
|
expect(splitArtifactPath(".ai/reports/run.md")).toEqual({
|
|
dir: ".ai/reports/",
|
|
name: "run.md",
|
|
});
|
|
});
|
|
|
|
test("leaves a root-level path without a directory", () => {
|
|
expect(splitArtifactPath("README.md")).toEqual({ dir: "", name: "README.md" });
|
|
});
|
|
});
|
|
|
|
describe("groupArtifactsByFile", () => {
|
|
test("collapses repeated captures of one path into a single file", () => {
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("consolidate_reviews", REPORT, 14323),
|
|
artifact("fix_review_findings", REPORT, 17483),
|
|
artifact("implement_plan", REPORT, 8422),
|
|
artifact("simplify", REPORT, 13162),
|
|
],
|
|
STAGES,
|
|
);
|
|
|
|
expect(files).toHaveLength(1);
|
|
expect(files[0].path).toBe(REPORT);
|
|
expect(files[0].dir).toBe(".ai/reports/");
|
|
expect(files[0].name).toBe("2026-07-27-wrk-002-instance-lifecycle.md");
|
|
expect(files[0].versions).toHaveLength(4);
|
|
});
|
|
|
|
test("orders versions newest first using the API stage order", () => {
|
|
const stages = [
|
|
stage("implement_plan", "2026-07-27T20:00:00Z"),
|
|
stage("simplify", "2026-07-27T18:43:11Z"),
|
|
];
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("implement_plan", REPORT, 8422),
|
|
artifact("simplify", REPORT, 13162),
|
|
],
|
|
stages,
|
|
);
|
|
|
|
expect(files[0].versions.map((v) => v.stageLabel)).toEqual([
|
|
"simplify",
|
|
"implement_plan",
|
|
]);
|
|
expect(files[0].versions[0].size).toBe(13162);
|
|
});
|
|
|
|
test.each([
|
|
["equal", "2026-07-27T18:43:11Z", "2026-07-27T18:43:11Z"],
|
|
["missing", null, null],
|
|
])("preserves API order when stage timestamps are %s", (_case, firstAt, secondAt) => {
|
|
const stages = [
|
|
stage("implement_plan", firstAt),
|
|
stage("simplify", secondAt),
|
|
];
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("simplify", REPORT, 13162),
|
|
artifact("implement_plan", REPORT, 8422),
|
|
],
|
|
stages,
|
|
);
|
|
|
|
expect(files[0].versions.map((v) => v.stageLabel)).toEqual([
|
|
"simplify",
|
|
"implement_plan",
|
|
]);
|
|
});
|
|
|
|
test("reports the byte change each capture introduced, oldest capture first", () => {
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("implement_plan", REPORT, 8422),
|
|
artifact("simplify", REPORT, 13162),
|
|
artifact("consolidate_reviews", REPORT, 14323),
|
|
artifact("fix_review_findings", REPORT, 17483),
|
|
],
|
|
STAGES,
|
|
);
|
|
|
|
// versions are newest-first, so deltas read 17483-14323, 14323-13162, ...
|
|
expect(files[0].versions.map((v) => v.delta)).toEqual([3160, 1161, 4740, null]);
|
|
});
|
|
|
|
test("drops captures from graph control nodes", () => {
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("start", ".ai/reports/pre-existing.md", 12402),
|
|
artifact("plan", ".ai/plans/plan.md", 21749),
|
|
],
|
|
STAGES,
|
|
);
|
|
|
|
expect(files.map((file) => file.path)).toEqual([".ai/plans/plan.md"]);
|
|
});
|
|
|
|
test("sorts files by their most recent capture", () => {
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("plan", ".ai/plans/plan.md", 21749),
|
|
artifact("fix_review_findings", REPORT, 17483),
|
|
artifact("simplify", ".ai/reviews/bugs.xml", 5231),
|
|
],
|
|
STAGES,
|
|
);
|
|
|
|
expect(files.map((file) => file.path)).toEqual([
|
|
REPORT,
|
|
".ai/reviews/bugs.xml",
|
|
".ai/plans/plan.md",
|
|
]);
|
|
});
|
|
|
|
test("keeps retries of one stage as separate ordered versions", () => {
|
|
const files = groupArtifactsByFile(
|
|
[
|
|
artifact("simplify", REPORT, 13162, 2),
|
|
artifact("simplify", REPORT, 9000, 1),
|
|
],
|
|
STAGES,
|
|
);
|
|
|
|
expect(files[0].versions.map((v) => v.retry)).toEqual([2, 1]);
|
|
expect(files[0].versions[0].size).toBe(13162);
|
|
expect(files[0].versions.map((v) => v.delta)).toEqual([4162, null]);
|
|
});
|
|
|
|
test("returns no files when every capture came from a control node", () => {
|
|
const files = groupArtifactsByFile(
|
|
[artifact("start", ".ai/reports/pre-existing.md", 12402)],
|
|
STAGES,
|
|
);
|
|
|
|
expect(files).toEqual([]);
|
|
});
|
|
});
|