// 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"/); }); });