mirror of
https://github.com/wassname/pi-lgtm.git
synced 2026-06-27 18:43:44 +08:00
8ea225d119
- Strip: TaskExecute, TaskOutput, TaskStop, process-tracker, subagent RPC, settings menu - Add done_criterion (required, falsifiable) to TaskCreate - Block status=completed in TaskUpdate -- must use /lgtm - Add lgtm_ask tool: evidence + 2 failure modes + evidence_vs_failures + remaining_uncertainty - Add /lgtm command: human-only sign-off with stored evidence review - Persist all lgtm_ask fields in task.metadata for async review - Widget shows 👀 for pending_approval tasks - Update README, package.json author Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
347 lines
11 KiB
TypeScript
347 lines
11 KiB
TypeScript
import { beforeEach, describe, expect, it } from "vitest";
|
|
import type { AutoClearMode } from "../src/auto-clear.js";
|
|
import { AutoClearManager } from "../src/auto-clear.js";
|
|
import { TaskStore } from "../src/task-store.js";
|
|
|
|
describe("auto-clear: on_task_complete mode", () => {
|
|
let store: TaskStore;
|
|
let manager: AutoClearManager;
|
|
|
|
beforeEach(() => {
|
|
store = new TaskStore();
|
|
manager = new AutoClearManager(() => store, () => "on_task_complete");
|
|
});
|
|
|
|
it("does not clear completed task before REMINDER_INTERVAL turns", () => {
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
// Turns 2, 3, 4 — not enough
|
|
for (let turn = 2; turn <= 4; turn++) {
|
|
manager.onTurnStart(turn);
|
|
}
|
|
expect(store.get("1")).toBeDefined();
|
|
expect(store.get("1")!.status).toBe("completed");
|
|
});
|
|
|
|
it("clears completed task after REMINDER_INTERVAL turns", () => {
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
// Turn 5 = turn 1 + 4 (REMINDER_INTERVAL)
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeUndefined();
|
|
expect(store.list()).toHaveLength(0);
|
|
});
|
|
|
|
it("clears each task independently based on its own completion turn", () => {
|
|
store.create("Task A", "Desc", "done");
|
|
store.create("Task B", "Desc", "done");
|
|
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
store.update("2", { pending_approval: true });
|
|
store.complete("2");
|
|
manager.trackCompletion("2", 3);
|
|
|
|
// Turn 5: Task A expires (1+4), Task B still lingers (3+4=7)
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeUndefined();
|
|
expect(store.get("2")).toBeDefined();
|
|
|
|
// Turn 7: Task B expires
|
|
manager.onTurnStart(7);
|
|
expect(store.get("2")).toBeUndefined();
|
|
});
|
|
|
|
it("does not clear pending or in_progress tasks", () => {
|
|
store.create("Pending", "Desc", "done");
|
|
store.create("In Progress", "Desc", "done");
|
|
store.create("Completed", "Desc", "done");
|
|
store.update("2", { status: "in_progress" });
|
|
store.update("3", { pending_approval: true });
|
|
store.complete("3");
|
|
manager.trackCompletion("3", 1);
|
|
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeDefined(); // pending — untouched
|
|
expect(store.get("2")).toBeDefined(); // in_progress — untouched
|
|
expect(store.get("3")).toBeUndefined(); // completed — cleared
|
|
});
|
|
|
|
it("cleans up dependency edges when auto-clearing", () => {
|
|
store.create("Blocker", "Desc", "done");
|
|
store.create("Blocked", "Desc", "done");
|
|
store.update("1", { addBlocks: ["2"] });
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeUndefined();
|
|
expect(store.get("2")!.blockedBy).toEqual([]);
|
|
});
|
|
|
|
it("returns true when tasks are cleared", () => {
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
expect(manager.onTurnStart(4)).toBe(false);
|
|
expect(manager.onTurnStart(5)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("auto-clear: on_list_complete mode", () => {
|
|
let store: TaskStore;
|
|
let manager: AutoClearManager;
|
|
|
|
beforeEach(() => {
|
|
store = new TaskStore();
|
|
manager = new AutoClearManager(() => store, () => "on_list_complete");
|
|
});
|
|
|
|
it("does not clear when some tasks are still pending", () => {
|
|
store.create("Done", "Desc", "done");
|
|
store.create("Pending", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
for (let turn = 2; turn <= 10; turn++) {
|
|
manager.onTurnStart(turn);
|
|
}
|
|
expect(store.get("1")).toBeDefined();
|
|
expect(store.list()).toHaveLength(2);
|
|
});
|
|
|
|
it("does not clear immediately when all tasks complete", () => {
|
|
store.create("A", "Desc", "done");
|
|
store.create("B", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
store.update("2", { pending_approval: true });
|
|
store.complete("2");
|
|
manager.trackCompletion("2", 1);
|
|
|
|
// Turns 2-4: not enough
|
|
for (let turn = 2; turn <= 4; turn++) {
|
|
manager.onTurnStart(turn);
|
|
}
|
|
expect(store.list()).toHaveLength(2);
|
|
});
|
|
|
|
it("clears all completed tasks after REMINDER_INTERVAL turns when all are completed", () => {
|
|
store.create("A", "Desc", "done");
|
|
store.create("B", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
store.update("2", { pending_approval: true });
|
|
store.complete("2");
|
|
manager.trackCompletion("2", 1);
|
|
|
|
manager.onTurnStart(5);
|
|
expect(store.list()).toHaveLength(0);
|
|
});
|
|
|
|
it("resets countdown when a new task is created before REMINDER_INTERVAL", () => {
|
|
store.create("A", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
// Turn 3: new task created — reset countdown
|
|
manager.onTurnStart(3);
|
|
manager.resetBatchCountdown();
|
|
store.create("B", "Desc", "done");
|
|
|
|
// Turn 5 would have cleared, but countdown was reset at turn 3
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeDefined(); // still around — list isn't all completed
|
|
});
|
|
|
|
it("resets countdown when a task goes back to in_progress", () => {
|
|
store.create("A", "Desc", "done");
|
|
store.create("B", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
store.update("2", { pending_approval: true });
|
|
store.complete("2");
|
|
manager.trackCompletion("2", 1);
|
|
|
|
// Turn 3: task 2 goes back to in_progress
|
|
manager.onTurnStart(3);
|
|
store.update("2", { status: "in_progress" });
|
|
manager.resetBatchCountdown();
|
|
|
|
// Turn 5: would have cleared, but countdown was reset
|
|
manager.onTurnStart(5);
|
|
expect(store.list()).toHaveLength(2); // both still here
|
|
});
|
|
|
|
it("returns true when tasks are cleared", () => {
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
expect(manager.onTurnStart(4)).toBe(false);
|
|
expect(manager.onTurnStart(5)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("auto-clear: never mode", () => {
|
|
let store: TaskStore;
|
|
let manager: AutoClearManager;
|
|
|
|
beforeEach(() => {
|
|
store = new TaskStore();
|
|
manager = new AutoClearManager(() => store, () => "never");
|
|
});
|
|
|
|
it("never clears completed tasks regardless of turns", () => {
|
|
store.create("A", "Desc", "done");
|
|
store.create("B", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
store.update("2", { pending_approval: true });
|
|
store.complete("2");
|
|
manager.trackCompletion("1", 1);
|
|
manager.trackCompletion("2", 1);
|
|
|
|
for (let turn = 2; turn <= 20; turn++) {
|
|
manager.onTurnStart(turn);
|
|
}
|
|
expect(store.list()).toHaveLength(2);
|
|
});
|
|
|
|
it("trackCompletion is a no-op", () => {
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
manager.onTurnStart(100);
|
|
expect(store.get("1")).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("auto-clear: dynamic mode switching", () => {
|
|
it("respects mode changes via getMode callback", () => {
|
|
const store = new TaskStore();
|
|
let mode: AutoClearMode = "never";
|
|
const manager = new AutoClearManager(() => store, () => mode);
|
|
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
|
|
// Track in never mode — no-op
|
|
manager.trackCompletion("1", 1);
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeDefined();
|
|
|
|
// Switch to on_task_complete and re-track
|
|
mode = "on_task_complete";
|
|
manager.trackCompletion("1", 5);
|
|
manager.onTurnStart(9);
|
|
expect(store.get("1")).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("auto-clear: store getter (session switch)", () => {
|
|
it("operates on the current store after swap", () => {
|
|
let store = new TaskStore();
|
|
const manager = new AutoClearManager(() => store, () => "on_task_complete");
|
|
|
|
store.create("Old task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
// Simulate session switch — swap store
|
|
store = new TaskStore();
|
|
store.create("New task", "Desc", "done");
|
|
manager.reset();
|
|
|
|
// Old task tracking was reset, new store has no completed tasks
|
|
manager.onTurnStart(5);
|
|
expect(store.list()).toHaveLength(1);
|
|
expect(store.get("1")!.subject).toBe("New task");
|
|
});
|
|
|
|
it("clears from new store, not old store", () => {
|
|
let store = new TaskStore();
|
|
const manager = new AutoClearManager(() => store, () => "on_task_complete");
|
|
|
|
// Swap to new store with a completed task
|
|
store = new TaskStore();
|
|
store.create("Task in new store", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeUndefined(); // cleared from new store
|
|
});
|
|
});
|
|
|
|
describe("auto-clear: reset (new session)", () => {
|
|
it("reset clears per-task tracking so old completions don't fire", () => {
|
|
const store = new TaskStore();
|
|
const manager = new AutoClearManager(() => store, () => "on_task_complete");
|
|
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
// Simulate /new — reset before the delay expires
|
|
manager.reset();
|
|
|
|
// Old completion should NOT trigger after reset
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeDefined();
|
|
});
|
|
|
|
it("reset clears batch countdown so old all-completed state doesn't fire", () => {
|
|
const store = new TaskStore();
|
|
const manager = new AutoClearManager(() => store, () => "on_list_complete");
|
|
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
|
|
// Simulate /new — reset before the delay expires
|
|
manager.reset();
|
|
|
|
// Old batch countdown should NOT trigger after reset
|
|
manager.onTurnStart(5);
|
|
expect(store.get("1")).toBeDefined();
|
|
});
|
|
|
|
it("tracking works normally after reset", () => {
|
|
const store = new TaskStore();
|
|
const manager = new AutoClearManager(() => store, () => "on_task_complete");
|
|
|
|
store.create("Task", "Desc", "done");
|
|
store.update("1", { pending_approval: true });
|
|
store.complete("1");
|
|
manager.trackCompletion("1", 1);
|
|
manager.reset();
|
|
|
|
// Re-track after reset with new turn baseline
|
|
manager.trackCompletion("1", 10);
|
|
manager.onTurnStart(14);
|
|
expect(store.get("1")).toBeUndefined();
|
|
});
|
|
});
|