mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-17 11:20:37 +09:00
133 lines
3.7 KiB
TypeScript
133 lines
3.7 KiB
TypeScript
|
|
import { describe, expect, it } from "vitest";
|
||
|
|
import { classifyRunLiveness } from "../services/run-liveness.ts";
|
||
|
|
|
||
|
|
const baseInput = {
|
||
|
|
runStatus: "succeeded",
|
||
|
|
issue: {
|
||
|
|
status: "in_progress",
|
||
|
|
title: "Implement feature",
|
||
|
|
description: "Add the requested behavior.",
|
||
|
|
},
|
||
|
|
resultJson: null,
|
||
|
|
stdoutExcerpt: null,
|
||
|
|
stderrExcerpt: null,
|
||
|
|
error: null,
|
||
|
|
errorCode: null,
|
||
|
|
continuationAttempt: 0,
|
||
|
|
evidence: null,
|
||
|
|
};
|
||
|
|
|
||
|
|
describe("run liveness classifier", () => {
|
||
|
|
it("classifies text-only future work as plan_only", () => {
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
resultJson: {
|
||
|
|
summary: "I will inspect the repo next and then implement the fix.",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("plan_only");
|
||
|
|
expect(classification.nextAction).toContain("inspect the repo");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("classifies empty successful output as empty_response", () => {
|
||
|
|
const classification = classifyRunLiveness(baseInput);
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("empty_response");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("treats issue comments, documents, products, and actions as progress", () => {
|
||
|
|
const latestEvidenceAt = new Date("2026-04-18T12:00:00Z");
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
resultJson: {
|
||
|
|
summary: "Updated implementation.",
|
||
|
|
},
|
||
|
|
evidence: {
|
||
|
|
issueCommentsCreated: 1,
|
||
|
|
documentRevisionsCreated: 1,
|
||
|
|
workProductsCreated: 1,
|
||
|
|
toolOrActionEventsCreated: 1,
|
||
|
|
latestEvidenceAt,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("advanced");
|
||
|
|
expect(classification.lastUsefulActionAt).toBe(latestEvidenceAt);
|
||
|
|
});
|
||
|
|
|
||
|
|
it("does not treat workspace operations alone as concrete progress", () => {
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
resultJson: {
|
||
|
|
summary: "I will inspect the repo next.",
|
||
|
|
},
|
||
|
|
evidence: {
|
||
|
|
workspaceOperationsCreated: 1,
|
||
|
|
latestEvidenceAt: new Date("2026-04-18T12:00:00Z"),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("plan_only");
|
||
|
|
expect(classification.lastUsefulActionAt).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it("exempts planning/document tasks from plan-only retry classification", () => {
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
issue: {
|
||
|
|
status: "in_progress",
|
||
|
|
title: "Draft implementation plan",
|
||
|
|
description: "Create a plan for the work.",
|
||
|
|
},
|
||
|
|
resultJson: {
|
||
|
|
summary: "Plan:\n- Inspect files\n- Implement after approval",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("advanced");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("exempts runs that update the plan document from plan-only classification", () => {
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
resultJson: {
|
||
|
|
summary: "Next steps:\n- inspect files\n- implement the service",
|
||
|
|
},
|
||
|
|
evidence: {
|
||
|
|
documentRevisionsCreated: 1,
|
||
|
|
planDocumentRevisionsCreated: 1,
|
||
|
|
latestEvidenceAt: new Date("2026-04-18T12:00:00Z"),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("advanced");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("classifies done issues as completed", () => {
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
issue: {
|
||
|
|
...baseInput.issue,
|
||
|
|
status: "done",
|
||
|
|
},
|
||
|
|
resultJson: {
|
||
|
|
summary: "Finished the implementation.",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("completed");
|
||
|
|
});
|
||
|
|
|
||
|
|
it("classifies declared blockers as blocked", () => {
|
||
|
|
const classification = classifyRunLiveness({
|
||
|
|
...baseInput,
|
||
|
|
resultJson: {
|
||
|
|
summary: "I cannot proceed because I need access credentials.",
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(classification.livenessState).toBe("blocked");
|
||
|
|
});
|
||
|
|
});
|