Q47: task parents cannot form a cycle

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 08:19:41 -04:00
1 parent b164677ea7
commit d5d68ad358
5 files changed
+86 -13

No files matched your search

@@ -1,12 +1,12 @@
import { describe, expect, test } from "vitest";
import { runBlocker } from "../../src/renderer/src/pages/work/task-helpers";
import { descendantIds, runBlocker } from "../../src/renderer/src/pages/work/task-helpers";
import type { ProviderRecord, TaskRecord, TaskRunRecord } from "../../src/shared/domain";
const provider = { enabled: true, authState: "configured" } as ProviderRecord;
const task = (status: TaskRecord["status"]) => ({ id: "tsk_1", status }) as TaskRecord;
const run = (state: string, taskId = "tsk_1") => ({ taskId, state }) as TaskRunRecord;
describe("run blocker", () => {
describe("work task helpers", () => {
test("a manual run needs a provider, an open task and no session in flight", () => {
expect(runBlocker(task("in_progress"), provider, [run("completed"), run("running", "tsk_other")])).toBeNull();
expect(runBlocker(task("in_progress"), null, [])).toBe("Connect a provider to run");
@@ -14,4 +14,10 @@ describe("run blocker", () => {
expect(runBlocker(task("backlog"), provider, [run("queued")])).toBe("A session is already queued or running");
expect(runBlocker(task("review"), provider, [run("running")])).toBe("A session is already queued or running");
});
test("descendants are found through every level, so the parent picker can leave them out", () => {
const tree = [["b", "a"], ["c", "b"], ["d", null], ["e", "c"]].map(([id, parentTaskId]) => ({ id, parentTaskId }) as TaskRecord);
expect([...descendantIds("a", tree)].sort()).toEqual(["b", "c", "e"]);
expect([...descendantIds("d", tree)]).toEqual([]);
});
});
+36
View File
@@ -0,0 +1,36 @@
import { randomUUID } from "node:crypto";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../src/shared/domain";
describe("task hierarchy", () => {
test("a task cannot become its own parent or the child of its own descendant", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-tree-${randomUUID()}.db`));
const organization = new OrganizationService(connection.db);
let orgId = "";
const ids: Record<string, string> = {};
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
for (const title of ["A", "B", "C"]) {
ids[title] = organization.createTask({
orgId, title, description: "d", status: TASK_STATUS.BACKLOG, assigneeAgentId: null,
risk: RISK_LEVEL.LOW, projectId: null, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
});
}
});
const reparent = (title: string, parent: string | null) => organization.updateTask({
orgId, taskId: ids[title], title, description: "d", assigneeAgentId: null, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: parent,
});
reparent("B", ids.A);
reparent("C", ids.B);
expect(() => reparent("A", ids.A)).toThrow('"A" cannot be its own parent.');
expect(() => reparent("A", ids.C)).toThrow('"A" cannot sit under "C": "C" is already beneath "A".');
expect(connection.db.prepare("select parent_task_id from tasks where id = ?").get(ids.A)).toEqual({ parent_task_id: null });
reparent("C", ids.A);
reparent("C", null);
connection.close();
});
});