Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 52 additions & 40 deletions packages/cli/__tests__/run-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@ describe("run-state", () => {
describe("createSprintRunState", () => {
it("creates state with correct structure", () => {
const state = createSprintRunState("Sprint 1", "feat/sprint-1", [
{ number: 10, order: 1 },
{ number: 11, order: 2 },
{ number: 12, order: 3 },
{ taskId: "10", order: 1 },
{ taskId: "11", order: 2 },
{ taskId: "12", order: 3 },
]);

expect(state.type).toBe("sprint");
expect(state.sprint).toBe("Sprint 1");
expect(state.branch).toBe("feat/sprint-1");
expect(state.tasks.length).toBe(3);
expect(state.tasks[0].issue).toBe(10);
expect(state.tasks[0].taskId).toBe("10");
expect(state.tasks[0].order).toBe(1);
expect(state.tasks[0].status).toBe("pending");
expect(state.runId).toMatch(/^run-/);
Expand All @@ -48,33 +48,37 @@ describe("run-state", () => {

describe("createParallelRunState", () => {
it("creates state with correct structure", () => {
const state = createParallelRunState([5, 6, 7]);
const state = createParallelRunState(["5", "6", "7"]);

expect(state.type).toBe("parallel");
expect(state.tasks.length).toBe(3);
expect(state.tasks[0].issue).toBe(5);
expect(state.tasks[0].taskId).toBe("5");
expect(state.tasks[0].order).toBe(1);
expect(state.tasks[2].order).toBe(3);
});
});

describe("save/load/clear", () => {
it("round-trips state through save and load", () => {
const state = createSprintRunState("S1", "b", [{ number: 1, order: 1 }]);
const state = createSprintRunState("S1", "b", [
{ taskId: "1", order: 1 },
]);
saveRunState(TEST_DIR, state);

const loaded = loadRunState(TEST_DIR, "S1");
expect(loaded).not.toBeNull();
expect(loaded?.runId).toBe(state.runId);
expect(loaded?.tasks[0].issue).toBe(1);
expect(loaded?.tasks[0].taskId).toBe("1");
});

it("returns null when no state file exists", () => {
expect(loadRunState(TEST_DIR, "nonexistent")).toBeNull();
});

it("clears state file", () => {
const state = createSprintRunState("S1", "b", [{ number: 1, order: 1 }]);
const state = createSprintRunState("S1", "b", [
{ taskId: "1", order: 1 },
]);
saveRunState(TEST_DIR, state);
expect(loadRunState(TEST_DIR, "S1")).not.toBeNull();

Expand All @@ -91,50 +95,56 @@ describe("run-state", () => {
describe("task mutations", () => {
it("markTaskInProgress", () => {
const state = createSprintRunState("S", "b", [
{ number: 1, order: 1 },
{ number: 2, order: 2 },
{ taskId: "1", order: 1 },
{ taskId: "2", order: 2 },
]);
markTaskInProgress(state, 1);
markTaskInProgress(state, "1");
expect(state.tasks[0].status).toBe("in_progress");
expect(state.tasks[1].status).toBe("pending");
});

it("markTaskDone", () => {
const state = createSprintRunState("S", "b", [{ number: 1, order: 1 }]);
markTaskInProgress(state, 1);
markTaskDone(state, 1, 42);
const state = createSprintRunState("S", "b", [
{ taskId: "1", order: 1 },
]);
markTaskInProgress(state, "1");
markTaskDone(state, "1", 42);
expect(state.tasks[0].status).toBe("done");
expect(state.tasks[0].pr).toBe(42);
expect(state.tasks[0].completedAt).toBeTruthy();
});

it("markTaskFailed", () => {
const state = createSprintRunState("S", "b", [{ number: 1, order: 1 }]);
markTaskInProgress(state, 1);
markTaskFailed(state, 1, "Build failed");
const state = createSprintRunState("S", "b", [
{ taskId: "1", order: 1 },
]);
markTaskInProgress(state, "1");
markTaskFailed(state, "1", "Build failed");
expect(state.tasks[0].status).toBe("failed");
expect(state.tasks[0].error).toBe("Build failed");
expect(state.tasks[0].failedAt).toBeTruthy();
});

it("handles non-existent issue gracefully", () => {
const state = createSprintRunState("S", "b", [{ number: 1, order: 1 }]);
markTaskInProgress(state, 999); // Should not throw
it("handles non-existent task gracefully", () => {
const state = createSprintRunState("S", "b", [
{ taskId: "1", order: 1 },
]);
markTaskInProgress(state, "999"); // Should not throw
expect(state.tasks[0].status).toBe("pending");
});
});

describe("getRunStats", () => {
it("returns correct counts", () => {
const state = createSprintRunState("S", "b", [
{ number: 1, order: 1 },
{ number: 2, order: 2 },
{ number: 3, order: 3 },
{ number: 4, order: 4 },
{ taskId: "1", order: 1 },
{ taskId: "2", order: 2 },
{ taskId: "3", order: 3 },
{ taskId: "4", order: 4 },
]);
markTaskDone(state, 1);
markTaskInProgress(state, 2);
markTaskFailed(state, 3, "error");
markTaskDone(state, "1");
markTaskInProgress(state, "2");
markTaskFailed(state, "3", "error");

const stats = getRunStats(state);
expect(stats.total).toBe(4);
Expand All @@ -148,33 +158,35 @@ describe("run-state", () => {
describe("getNextTask", () => {
it("returns first pending task", () => {
const state = createSprintRunState("S", "b", [
{ number: 1, order: 1 },
{ number: 2, order: 2 },
{ taskId: "1", order: 1 },
{ taskId: "2", order: 2 },
]);
markTaskDone(state, 1);
markTaskDone(state, "1");

const next = getNextTask(state);
expect(next?.issue).toBe(2);
expect(next?.taskId).toBe("2");
expect(next?.status).toBe("pending");
});

it("prioritizes failed tasks for retry", () => {
const state = createSprintRunState("S", "b", [
{ number: 1, order: 1 },
{ number: 2, order: 2 },
{ number: 3, order: 3 },
{ taskId: "1", order: 1 },
{ taskId: "2", order: 2 },
{ taskId: "3", order: 3 },
]);
markTaskDone(state, 1);
markTaskFailed(state, 2, "error");
markTaskDone(state, "1");
markTaskFailed(state, "2", "error");

const next = getNextTask(state);
expect(next?.issue).toBe(2);
expect(next?.taskId).toBe("2");
expect(next?.status).toBe("failed");
});

it("returns null when all tasks are done", () => {
const state = createSprintRunState("S", "b", [{ number: 1, order: 1 }]);
markTaskDone(state, 1);
const state = createSprintRunState("S", "b", [
{ taskId: "1", order: 1 },
]);
markTaskDone(state, "1");

expect(getNextTask(state)).toBeNull();
});
Expand Down
36 changes: 18 additions & 18 deletions packages/cli/__tests__/shutdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
loadRunState,
saveRunState,
} from "../src/core/run-state.js";
import type { RunState } from "../src/types.js";
import type { RunState } from "../src/core/run-state.js";

const testDir = join(tmpdir(), `locus-shutdown-test-${Date.now()}`);
const INTERRUPT_ERROR = "Interrupted by user";
Expand Down Expand Up @@ -40,13 +40,13 @@ describe("shutdown state preservation", () => {
startedAt: new Date().toISOString(),
tasks: [
{
issue: 1,
taskId: "1",
order: 1,
status: "done",
completedAt: new Date().toISOString(),
},
{ issue: 2, order: 2, status: "in_progress" },
{ issue: 3, order: 3, status: "pending" },
{ taskId: "2", order: 2, status: "in_progress" },
{ taskId: "3", order: 3, status: "pending" },
],
};

Expand All @@ -71,8 +71,8 @@ describe("shutdown state preservation", () => {
type: "parallel",
startedAt: new Date().toISOString(),
tasks: [
{ issue: 10, order: 1, status: "in_progress" },
{ issue: 11, order: 2, status: "pending" },
{ taskId: "10", order: 1, status: "in_progress" },
{ taskId: "11", order: 2, status: "pending" },
],
};

Expand All @@ -81,7 +81,7 @@ describe("shutdown state preservation", () => {
// File should be valid JSON
const raw = readFileSync(
join(testDir, ".locus", "run-state", "_parallel.json"),
"utf-8"
"utf-8",
);
const parsed = JSON.parse(raw);
expect(parsed.runId).toBe("run-test-002");
Expand All @@ -96,9 +96,9 @@ describe("shutdown state preservation", () => {
type: "parallel",
startedAt: new Date().toISOString(),
tasks: [
{ issue: 20, order: 1, status: "in_progress" },
{ issue: 21, order: 2, status: "in_progress" },
{ issue: 22, order: 3, status: "in_progress" },
{ taskId: "20", order: 1, status: "in_progress" },
{ taskId: "21", order: 2, status: "in_progress" },
{ taskId: "22", order: 3, status: "in_progress" },
],
};

Expand All @@ -122,21 +122,21 @@ describe("shutdown state preservation", () => {
startedAt: new Date().toISOString(),
tasks: [
{
issue: 30,
taskId: "30",
order: 1,
status: "done",
completedAt: "2026-01-01T00:00:00Z",
pr: 100,
},
{
issue: 31,
taskId: "31",
order: 2,
status: "failed",
failedAt: "2026-01-01T01:00:00Z",
error: "API limit",
},
{ issue: 32, order: 3, status: "in_progress" },
{ issue: 33, order: 4, status: "pending" },
{ taskId: "32", order: 3, status: "in_progress" },
{ taskId: "33", order: 4, status: "pending" },
],
};

Expand Down Expand Up @@ -182,13 +182,13 @@ describe("shutdown state preservation", () => {
startedAt: new Date().toISOString(),
tasks: [
{
issue: 40,
taskId: "40",
order: 1,
status: "done",
completedAt: "2026-01-01T00:00:00Z",
},
{ issue: 41, order: 2, status: "in_progress" },
{ issue: 42, order: 3, status: "pending" },
{ taskId: "41", order: 2, status: "in_progress" },
{ taskId: "42", order: 3, status: "pending" },
],
};

Expand All @@ -199,7 +199,7 @@ describe("shutdown state preservation", () => {
// Simulate resume — failed interrupted task should be retried first.
const loaded = loadRunState(testDir, "resume-test");
const next = loaded ? getNextTask(loaded) : null;
expect(next?.issue).toBe(41); // The one that was in_progress
expect(next?.taskId).toBe("41"); // The one that was in_progress
expect(next?.status).toBe("failed");
expect(next?.error).toBe(INTERRUPT_ERROR);
});
Expand Down
Loading