- Prepaid credit system: org wallet, debit on usage, chat gate, admin credit console - Engine UI: /manager, /tasks, /tasks/[id], /workforce, /dashboard, /traces pages - Direct task creation (HTTP POST /tasks, MCP tasks_create) at pending_manager_review - Pause/resume/cancel/answer actions in engine API and UI - Employee cap (7 per org default), review pipeline guard (active reviewer required) - Scheduler: wide window prevents org starvation; archived employees excluded from cap - Tenant isolation: project-id cross-org 404, engine never uses another tenant's sandbox - Order route fix: manager built with pipeline states + employee profiles - Contract extension: ModelGrantPort.get_org_policy with DefaultOrgPolicy - Engine event types in @agy/contracts (task.step, task.gate, run.log, run.tool, team.changed) - api-engine.ts typed client; api-credit.ts client - Alembic migration c4d5e6f7a8b9: prepaid credit + engine UI tables - 859 tests passed, 1 skipped; ruff/mypy/tsc/eslint/vitest all green Refs: PLAN-v2.md §9 (T1–T43), todo-v2-final.md
112 lines
3.8 KiB
TypeScript
112 lines
3.8 KiB
TypeScript
/**
|
|
* The engine's labels and board columns: every stage lands in exactly one column, a legacy
|
|
* task lands by its status, and grouping keeps the order the tasks came in.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
BOARD_COLUMNS,
|
|
columnFor,
|
|
groupByColumn,
|
|
ROLE_LABELS,
|
|
STAGE_LABELS,
|
|
stageTone,
|
|
TIER_LABELS,
|
|
} from "@/lib/engine-labels";
|
|
|
|
const STAGES = [
|
|
"pending_manager_review",
|
|
"pending_analysis",
|
|
"pending_implementation",
|
|
"pending_review",
|
|
"pending_test",
|
|
"pending_final_review",
|
|
"merge_request",
|
|
"completed",
|
|
"pending_user_decision",
|
|
"error_review",
|
|
"paused",
|
|
"cancelled",
|
|
"failed",
|
|
];
|
|
|
|
describe("the board", () => {
|
|
it("places every one of the thirteen stages in exactly one column", () => {
|
|
for (const stage of STAGES) {
|
|
const homes = BOARD_COLUMNS.filter((column) => column.stages.includes(stage as never));
|
|
expect(homes, stage).toHaveLength(1);
|
|
expect(STAGE_LABELS[stage], stage).toBeTruthy();
|
|
}
|
|
const placed = BOARD_COLUMNS.flatMap((column) => column.stages as readonly string[]);
|
|
expect([...placed].sort()).toEqual([...STAGES].sort());
|
|
});
|
|
|
|
it("uses the stage column whenever the engine owns the task", () => {
|
|
expect(columnFor("pending_test", "running")).toBe("test");
|
|
expect(columnFor("pending_final_review", "running")).toBe("review");
|
|
expect(columnFor("paused", "waiting_approval")).toBe("decision");
|
|
expect(columnFor("completed", "done")).toBe("done");
|
|
});
|
|
|
|
it("places a legacy task by its coarse status", () => {
|
|
expect(columnFor(null, "new")).toBe("manager_review");
|
|
expect(columnFor(null, "triaged")).toBe("implementation");
|
|
expect(columnFor(null, "running")).toBe("implementation");
|
|
expect(columnFor(null, "waiting_approval")).toBe("decision");
|
|
for (const status of ["done", "failed", "cancelled"]) {
|
|
expect(columnFor(null, status)).toBe("done");
|
|
}
|
|
});
|
|
|
|
it("sends an unknown stage or status to the decision column", () => {
|
|
expect(columnFor("some_new_stage", "running")).toBe("decision");
|
|
expect(columnFor(null, "some_new_status")).toBe("decision");
|
|
});
|
|
|
|
it("groups tasks by column, keeps every column, and keeps the order inside each", () => {
|
|
const tasks = [
|
|
{ id: "a", stage: "pending_test", status: "running" },
|
|
{ id: "b", stage: null, status: "new" },
|
|
{ id: "c", stage: "pending_test", status: "running" },
|
|
{ id: "d", stage: "pending_final_review", status: "running" },
|
|
];
|
|
|
|
const groups = groupByColumn(tasks);
|
|
|
|
expect(groups.test.map((task) => task.id)).toEqual(["a", "c"]);
|
|
expect(groups.review.map((task) => task.id)).toEqual(["d"]);
|
|
expect(groups.manager_review.map((task) => task.id)).toEqual(["b"]);
|
|
expect(groups.merge).toEqual([]);
|
|
expect(Object.keys(groups)).toHaveLength(BOARD_COLUMNS.length);
|
|
});
|
|
});
|
|
|
|
describe("labels and tones", () => {
|
|
it("tones a stage by how it ended or whether a person must act", () => {
|
|
expect(stageTone("completed")).toBe("success");
|
|
expect(stageTone("failed")).toBe("danger");
|
|
expect(stageTone("cancelled")).toBe("danger");
|
|
expect(stageTone("paused")).toBe("warning");
|
|
expect(stageTone("pending_user_decision")).toBe("warning");
|
|
expect(stageTone("error_review")).toBe("warning");
|
|
expect(stageTone("pending_test")).toBe("info");
|
|
});
|
|
|
|
it("labels the four tiers in Persian", () => {
|
|
expect(TIER_LABELS).toEqual({
|
|
fast: "سریع",
|
|
balanced: "متعادل",
|
|
strong: "قوی",
|
|
unknown: "نامشخص",
|
|
});
|
|
});
|
|
|
|
it("labels the five hirable roles in Persian", () => {
|
|
expect(Object.keys(ROLE_LABELS).sort()).toEqual(
|
|
["developer", "manager", "planner", "reviewer", "tester"].sort(),
|
|
);
|
|
expect(ROLE_LABELS.manager).toBe("مدیر");
|
|
expect(ROLE_LABELS.reviewer).toBe("بازبین");
|
|
expect(ROLE_LABELS.tester).toBe("آزمونگر");
|
|
});
|
|
});
|