mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 07:04:45 +00:00
150 lines
6.1 KiB
TypeScript
150 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";
|
|
|
|
// 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"/);
|
|
});
|
|
});
|