Files
openclaw/extensions/workboard/src/tools.test.ts
Peter Steinberger a341ae27ec feat(workboard): add orchestration primitives
Adds Workboard orchestration statuses, dependency links, idempotent child creation, dispatch, and complete/block lifecycle operations backed by the plugin SQLite keyed store.

Persists tenant, skills, workspace, schedule, runtime, retry, dispatch, and handoff metadata in card records, with claim scoping and token redaction. Surfaces the new states and metadata in the Control UI, horizontal board layout, localized strings, and Workboard docs.

Verification:
- pnpm test extensions/workboard/src/store.test.ts extensions/workboard/src/tools.test.ts extensions/workboard/src/gateway.test.ts ui/src/ui/controllers/workboard.test.ts ui/src/styles/workboard.test.ts ui/src/ui/views/workboard.test.ts -- --reporter=verbose
- pnpm ui:i18n:check
- /Users/steipete/Projects/agent-scripts/skills/autoreview/scripts/autoreview --mode branch --base origin/main, followed by focused clean local autoreview loops for final fixes
- env -u OPENCLAW_TESTBOX pnpm check:changed
- git diff --check
2026-05-30 02:40:46 +02:00

334 lines
10 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import type { OpenClawPluginApi } from "../api.js";
import { WorkboardStore, type WorkboardKeyedStore } from "./store.js";
import { createWorkboardTools } from "./tools.js";
function createMemoryStore(): WorkboardKeyedStore {
const entries = new Map<string, Awaited<ReturnType<WorkboardKeyedStore["lookup"]>>>();
return {
async register(key, value) {
entries.set(key, value);
},
async lookup(key) {
return entries.get(key);
},
async delete(key) {
return entries.delete(key);
},
async entries() {
return [...entries].flatMap(([key, value]) => (value ? [{ key, value }] : []));
},
};
}
function readPayload(result: unknown): Record<string, unknown> {
return (result as { details?: Record<string, unknown> }).details ?? {};
}
describe("workboard tools", () => {
it("lists, claims, heartbeats, and reads worker context", async () => {
const keyed = createMemoryStore();
const api = {
runtime: {
state: {
openKeyedStore: vi.fn(() => keyed),
},
},
} as unknown as OpenClawPluginApi;
const tools = createWorkboardTools({
api,
context: { agentId: "main", sessionKey: "session-1" } as never,
});
const byName = new Map(tools.map((tool) => [tool.name, tool]));
const store = keyed;
await store.register("card-1", {
version: 1,
card: {
id: "card-1",
title: "Ship coordination",
status: "todo",
priority: "normal",
labels: [],
agentId: "main",
position: 1000,
createdAt: 1,
updatedAt: 1,
},
});
await store.register("archived-1", {
version: 1,
card: {
id: "archived-1",
title: "Closed work",
status: "done",
priority: "normal",
labels: [],
position: 2000,
createdAt: 1,
updatedAt: 1,
metadata: { archivedAt: 2 },
},
});
const claimed = readPayload(
await byName.get("workboard_claim")?.execute("call-1", { id: "card-1" }),
);
expect(claimed.card).toMatchObject({
status: "running",
metadata: { claim: { ownerId: "main", token: "[redacted]" } },
});
const token = (claimed.token as string | undefined) ?? "";
const heartbeat = readPayload(
await byName
.get("workboard_heartbeat")
?.execute("call-2", { id: "card-1", token, note: "alive" }),
);
expect(heartbeat).toMatchObject({
metadata: { comments: [expect.objectContaining({ body: "alive" })] },
});
const read = readPayload(
await byName.get("workboard_read")?.execute("call-3", { id: "card-1" }),
);
expect(read.workerContext).toContain("Ship coordination");
expect(read.card).toMatchObject({ metadata: { claim: { token: "[redacted]" } } });
const released = readPayload(
await byName
.get("workboard_release")
?.execute("call-4", { id: "card-1", token, status: "review" }),
);
expect(released).toMatchObject({ status: "review" });
expect((released.metadata as { claim?: unknown } | undefined)?.claim).toBeUndefined();
const list = readPayload(await byName.get("workboard_list")?.execute("call-5", {}));
expect(list.cards).toEqual([expect.objectContaining({ id: "card-1" })]);
const archivedList = readPayload(
await byName.get("workboard_list")?.execute("call-6", { includeArchived: true }),
);
expect(archivedList.cards).toEqual(
expect.arrayContaining([expect.objectContaining({ id: "archived-1", archivedAt: 2 })]),
);
});
it("can share one store across tool instances for claim coordination", async () => {
const keyed = createMemoryStore();
const api = {
runtime: {
state: {
openKeyedStore: vi.fn(() => keyed),
},
},
} as unknown as OpenClawPluginApi;
const store = new WorkboardStore(keyed);
const mainTools = new Map(
createWorkboardTools({
api,
store,
context: { agentId: "main" } as never,
}).map((tool) => [tool.name, tool]),
);
const otherTools = new Map(
createWorkboardTools({
api,
store,
context: { agentId: "other" } as never,
}).map((tool) => [tool.name, tool]),
);
const card = await store.create({ title: "Single owner" });
await mainTools.get("workboard_claim")?.execute("call-1", { id: card.id });
await expect(
otherTools.get("workboard_claim")?.execute("call-2", { id: card.id }),
).rejects.toThrow(/already claimed/);
});
it("requires claim scope before creating or linking dependencies against claimed cards", async () => {
const keyed = createMemoryStore();
const api = {
runtime: {
state: {
openKeyedStore: vi.fn(() => keyed),
},
},
} as unknown as OpenClawPluginApi;
const store = new WorkboardStore(keyed);
const mainTools = new Map(
createWorkboardTools({
api,
store,
context: { agentId: "main" } as never,
}).map((tool) => [tool.name, tool]),
);
const otherTools = new Map(
createWorkboardTools({
api,
store,
context: { agentId: "other" } as never,
}).map((tool) => [tool.name, tool]),
);
const parent = await store.create({ title: "Claimed parent" });
const claimed = await store.claim(parent.id, { ownerId: "main", token: "parent-token" });
await expect(
otherTools.get("workboard_create")?.execute("call-1", {
title: "Blocked child",
parents: [parent.id],
}),
).rejects.toThrow(/claimed by main/);
await expect(
otherTools.get("workboard_create")?.execute("call-1b", {
title: "Blocked child",
parents: parent.id,
}),
).rejects.toThrow(/claimed by main/);
expect(await store.list()).toHaveLength(1);
await otherTools.get("workboard_create")?.execute("call-2", {
title: "Scoped child",
parents: [parent.id],
token: claimed.token,
});
const child = await store.create({ title: "Claimed child" });
await store.claim(child.id, { ownerId: "main", token: "child-token" });
await expect(
otherTools.get("workboard_link")?.execute("call-3", {
parentId: parent.id,
childId: child.id,
}),
).rejects.toThrow(/claimed by main/);
const linked = readPayload(
await otherTools.get("workboard_link")?.execute("call-4", {
parentId: parent.id,
childId: (await store.create({ title: "Idle child" })).id,
token: claimed.token,
}),
);
expect(linked.card).toMatchObject({ status: "todo" });
await expect(
mainTools.get("workboard_link")?.execute("call-5", {
parentId: parent.id,
childId: child.id,
token: "child-token",
}),
).rejects.toThrow(/active child/);
});
it("creates dependent cards and completes claimed work through tools", async () => {
const keyed = createMemoryStore();
const api = {
runtime: {
state: {
openKeyedStore: vi.fn(() => keyed),
},
},
} as unknown as OpenClawPluginApi;
const store = new WorkboardStore(keyed);
const tools = new Map(
createWorkboardTools({
api,
store,
context: { agentId: "main" } as never,
}).map((tool) => [tool.name, tool]),
);
const parentPayload = readPayload(
await tools.get("workboard_create")?.execute("call-1", {
title: "Parent",
status: "running",
}),
);
const parent = parentPayload.card as { id: string };
const childPayload = readPayload(
await tools.get("workboard_create")?.execute("call-2", {
title: "Child",
parents: [parent.id],
tenant: "qa",
skills: ["testing"],
}),
);
const child = childPayload.card as { id: string; status: string };
expect(child.status).toBe("todo");
await expect(
tools.get("workboard_complete")?.execute("call-unclaimed-complete", {
id: parent.id,
summary: "Too early.",
}),
).rejects.toThrow(/claimed/);
await expect(
tools.get("workboard_block")?.execute("call-unclaimed-block", {
id: child.id,
reason: "Too early.",
}),
).rejects.toThrow(/claimed/);
const claimed = readPayload(
await tools.get("workboard_claim")?.execute("call-3", { id: parent.id }),
);
const token = claimed.token as string;
const completed = readPayload(
await tools.get("workboard_complete")?.execute("call-4", {
id: parent.id,
token,
summary: "Done.",
createdCardIds: [child.id],
proof: { status: "passed", command: "pnpm test extensions/workboard" },
}),
);
expect(completed.card).toMatchObject({ status: "done" });
const dispatch = readPayload(await tools.get("workboard_dispatch")?.execute("call-5", {}));
expect(dispatch.promoted).toEqual([expect.objectContaining({ id: child.id, status: "ready" })]);
});
it("redacts claim tokens from dispatch tool results", async () => {
const keyed = createMemoryStore();
const api = {
runtime: {
state: {
openKeyedStore: vi.fn(() => keyed),
},
},
} as unknown as OpenClawPluginApi;
const store = new WorkboardStore(keyed);
const tools = new Map(
createWorkboardTools({
api,
store,
context: { agentId: "main" } as never,
}).map((tool) => [tool.name, tool]),
);
const card = await store.create({
title: "Scheduled",
status: "scheduled",
scheduledAt: 1,
});
await store.update(card.id, {
metadata: {
...card.metadata,
claim: {
ownerId: "main",
token: "secret-token",
claimedAt: 1,
lastHeartbeatAt: 1,
expiresAt: Date.now() + 60_000,
},
},
});
const dispatch = readPayload(await tools.get("workboard_dispatch")?.execute("call-1", {}));
const promoted = dispatch.promoted as Array<{
metadata?: { claim?: { token?: string } };
}>;
expect(promoted).toEqual([expect.objectContaining({ id: card.id })]);
expect(promoted[0]?.metadata?.claim?.token).toBe("[redacted]");
});
});