clanker-agent/packages/companion-teams/src/utils/tasks.test.ts
Harivansh Rathi 536241053c refactor: finish companion rename migration
Complete the remaining pi-to-companion rename across companion-os, web, vm-orchestrator, docker, and archived fixtures.

Verification:
- semantic rg sweeps for Pi/piConfig/getPi/.pi runtime references
- npm run check in apps/companion-os (fails in this worktree: biome not found)

Co-authored-by: Codex <noreply@openai.com>
2026-03-10 07:39:32 -05:00

215 lines
6.7 KiB
TypeScript

// Project: companion-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(), `companion-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"/,
);
});
});