co-mono/packages/pi-teams/src/utils/tasks.test.ts
2026-03-05 17:36:25 -08:00

151 lines
6.1 KiB
TypeScript

// Project: pi-teams
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as paths from "./paths";
import { createTask, evaluatePlan, listTasks, readTask, submitPlan, updateTask } from "./tasks";
import * as teams from "./teams";
// Mock the paths to use a temporary directory
const testDir = path.join(os.tmpdir(), "pi-teams-test-" + Date.now());
describe("Tasks Utilities", () => {
beforeEach(() => {
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
fs.mkdirSync(testDir, { recursive: true });
// Override paths to use testDir
vi.spyOn(paths, "taskDir").mockReturnValue(testDir);
vi.spyOn(paths, "configPath").mockReturnValue(path.join(testDir, "config.json"));
// Create a dummy team config
fs.writeFileSync(path.join(testDir, "config.json"), JSON.stringify({ name: "test-team" }));
});
afterEach(() => {
vi.restoreAllMocks();
if (fs.existsSync(testDir)) fs.rmSync(testDir, { recursive: true });
});
it("should create a task successfully", async () => {
const task = await createTask("test-team", "Test Subject", "Test Description");
expect(task.id).toBe("1");
expect(task.subject).toBe("Test Subject");
expect(fs.existsSync(path.join(testDir, "1.json"))).toBe(true);
});
it("should update a task successfully", async () => {
await createTask("test-team", "Test Subject", "Test Description");
const updated = await updateTask("test-team", "1", { status: "in_progress" });
expect(updated.status).toBe("in_progress");
const taskData = JSON.parse(fs.readFileSync(path.join(testDir, "1.json"), "utf-8"));
expect(taskData.status).toBe("in_progress");
});
it("should submit a plan successfully", async () => {
const task = await createTask("test-team", "Test Subject", "Test Description");
const plan = "Step 1: Do something\nStep 2: Profit";
const updated = await submitPlan("test-team", task.id, plan);
expect(updated.status).toBe("planning");
expect(updated.plan).toBe(plan);
const taskData = JSON.parse(fs.readFileSync(path.join(testDir, `${task.id}.json`), "utf-8"));
expect(taskData.status).toBe("planning");
expect(taskData.plan).toBe(plan);
});
it("should fail to submit an empty plan", async () => {
const task = await createTask("test-team", "Empty Test", "Should fail");
await expect(submitPlan("test-team", task.id, "")).rejects.toThrow("Plan must not be empty");
await expect(submitPlan("test-team", task.id, " ")).rejects.toThrow("Plan must not be empty");
});
it("should list tasks", async () => {
await createTask("test-team", "Task 1", "Desc 1");
await createTask("test-team", "Task 2", "Desc 2");
const tasksList = await listTasks("test-team");
expect(tasksList.length).toBe(2);
expect(tasksList[0].id).toBe("1");
expect(tasksList[1].id).toBe("2");
});
it("should have consistent lock paths (Fixed BUG 2)", async () => {
// This test verifies that both updateTask and readTask now use the same lock path
// Both should now lock `${taskId}.json.lock`
await createTask("test-team", "Bug Test", "Testing lock consistency");
const taskId = "1";
const taskFile = path.join(testDir, `${taskId}.json`);
const commonLockFile = `${taskFile}.lock`;
// 1. Holding the common lock
fs.writeFileSync(commonLockFile, "9999");
// 2. Try updateTask, it should fail
// Using small retries to speed up the test and avoid fake timer issues with native setTimeout
await expect(updateTask("test-team", taskId, { status: "in_progress" }, 2)).rejects.toThrow(
"Could not acquire lock",
);
// 3. Try readTask, it should fail too
await expect(readTask("test-team", taskId, 2)).rejects.toThrow("Could not acquire lock");
fs.unlinkSync(commonLockFile);
});
it("should approve a plan successfully", async () => {
const task = await createTask("test-team", "Plan Test", "Should be approved");
await submitPlan("test-team", task.id, "Wait for it...");
const approved = await evaluatePlan("test-team", task.id, "approve");
expect(approved.status).toBe("in_progress");
expect(approved.planFeedback).toBe("");
});
it("should reject a plan with feedback", async () => {
const task = await createTask("test-team", "Plan Test", "Should be rejected");
await submitPlan("test-team", task.id, "Wait for it...");
const feedback = "Not good enough!";
const rejected = await evaluatePlan("test-team", task.id, "reject", feedback);
expect(rejected.status).toBe("planning");
expect(rejected.planFeedback).toBe(feedback);
});
it("should fail to evaluate a task not in 'planning' status", async () => {
const task = await createTask("test-team", "Status Test", "Invalid status for eval");
// status is "pending"
await expect(evaluatePlan("test-team", task.id, "approve")).rejects.toThrow("must be in 'planning' status");
});
it("should fail to evaluate a task without a plan", async () => {
const task = await createTask("test-team", "Plan Missing Test", "No plan submitted");
await updateTask("test-team", task.id, { status: "planning" }); // bypass submitPlan to have no plan
await expect(evaluatePlan("test-team", task.id, "approve")).rejects.toThrow("no plan has been submitted");
});
it("should fail to reject a plan without feedback", async () => {
const task = await createTask("test-team", "Feedback Test", "Should require feedback");
await submitPlan("test-team", task.id, "My plan");
await expect(evaluatePlan("test-team", task.id, "reject")).rejects.toThrow(
"Feedback is required when rejecting a plan",
);
await expect(evaluatePlan("test-team", task.id, "reject", " ")).rejects.toThrow(
"Feedback is required when rejecting a plan",
);
});
it("should sanitize task IDs in all file operations", async () => {
const dirtyId = "../evil-id";
// sanitizeName should throw on this dirtyId
await expect(readTask("test-team", dirtyId)).rejects.toThrow(/Invalid name: "..\/evil-id"/);
await expect(updateTask("test-team", dirtyId, { status: "in_progress" })).rejects.toThrow(
/Invalid name: "..\/evil-id"/,
);
await expect(evaluatePlan("test-team", dirtyId, "approve")).rejects.toThrow(/Invalid name: "..\/evil-id"/);
});
});