chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
|
||||
vi.mock("../src/logger.js", () => ({
|
||||
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
||||
}));
|
||||
|
||||
import { registerFrontierFunction } from "../src/functions/frontier.js";
|
||||
import { registerActionsFunction } from "../src/functions/actions.js";
|
||||
import type { Action, ActionEdge, Checkpoint, Lease } from "../src/types.js";
|
||||
import type { FrontierItem } from "../src/functions/frontier.js";
|
||||
|
||||
function mockKV() {
|
||||
const store = new Map<string, Map<string, unknown>>();
|
||||
return {
|
||||
get: async <T>(scope: string, key: string): Promise<T | null> => {
|
||||
return (store.get(scope)?.get(key) as T) ?? null;
|
||||
},
|
||||
set: async <T>(scope: string, key: string, data: T): Promise<T> => {
|
||||
if (!store.has(scope)) store.set(scope, new Map());
|
||||
store.get(scope)!.set(key, data);
|
||||
return data;
|
||||
},
|
||||
delete: async (scope: string, key: string): Promise<void> => {
|
||||
store.get(scope)?.delete(key);
|
||||
},
|
||||
list: async <T>(scope: string): Promise<T[]> => {
|
||||
const entries = store.get(scope);
|
||||
return entries ? (Array.from(entries.values()) as T[]) : [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function mockSdk() {
|
||||
const functions = new Map<string, Function>();
|
||||
return {
|
||||
registerFunction: (idOrOpts: string | { id: string }, handler: Function) => {
|
||||
const id = typeof idOrOpts === "string" ? idOrOpts : idOrOpts.id;
|
||||
functions.set(id, handler);
|
||||
},
|
||||
registerTrigger: () => {},
|
||||
trigger: async (idOrInput: string | { function_id: string; payload: unknown }, data?: unknown) => {
|
||||
const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id;
|
||||
const payload = typeof idOrInput === "string" ? data : idOrInput.payload;
|
||||
const fn = functions.get(id);
|
||||
if (!fn) throw new Error(`No function: ${id}`);
|
||||
return fn(payload);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeAction(overrides: Partial<Action>): Action {
|
||||
const now = new Date().toISOString();
|
||||
return {
|
||||
id: overrides.id || `act_${Math.random().toString(36).slice(2, 10)}`,
|
||||
title: overrides.title || "Test action",
|
||||
description: overrides.description || "",
|
||||
status: overrides.status || "pending",
|
||||
priority: overrides.priority || 5,
|
||||
createdAt: overrides.createdAt || now,
|
||||
updatedAt: overrides.updatedAt || now,
|
||||
createdBy: overrides.createdBy || "agent-1",
|
||||
assignedTo: overrides.assignedTo,
|
||||
project: overrides.project,
|
||||
tags: overrides.tags || [],
|
||||
sourceObservationIds: overrides.sourceObservationIds || [],
|
||||
sourceMemoryIds: overrides.sourceMemoryIds || [],
|
||||
result: overrides.result,
|
||||
parentId: overrides.parentId,
|
||||
metadata: overrides.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
describe("Frontier Functions", () => {
|
||||
let sdk: ReturnType<typeof mockSdk>;
|
||||
let kv: ReturnType<typeof mockKV>;
|
||||
|
||||
beforeEach(() => {
|
||||
sdk = mockSdk();
|
||||
kv = mockKV();
|
||||
registerActionsFunction(sdk as never, kv as never);
|
||||
registerFrontierFunction(sdk as never, kv as never);
|
||||
});
|
||||
|
||||
describe("mem::frontier", () => {
|
||||
it("returns empty frontier when no actions exist", async () => {
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
totalActions: number;
|
||||
totalUnblocked: number;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.frontier).toEqual([]);
|
||||
expect(result.totalActions).toBe(0);
|
||||
expect(result.totalUnblocked).toBe(0);
|
||||
});
|
||||
|
||||
it("returns pending actions sorted by score", async () => {
|
||||
const lowPriority = makeAction({
|
||||
id: "act_low",
|
||||
title: "Low priority",
|
||||
priority: 2,
|
||||
});
|
||||
const highPriority = makeAction({
|
||||
id: "act_high",
|
||||
title: "High priority",
|
||||
priority: 9,
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", lowPriority.id, lowPriority);
|
||||
await kv.set("mem:actions", highPriority.id, highPriority);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.frontier.length).toBe(2);
|
||||
expect(result.frontier[0].action.id).toBe("act_high");
|
||||
expect(result.frontier[1].action.id).toBe("act_low");
|
||||
expect(result.frontier[0].score).toBeGreaterThan(
|
||||
result.frontier[1].score,
|
||||
);
|
||||
});
|
||||
|
||||
it("excludes done and cancelled actions", async () => {
|
||||
const pending = makeAction({
|
||||
id: "act_pending",
|
||||
title: "Pending",
|
||||
status: "pending",
|
||||
});
|
||||
const done = makeAction({
|
||||
id: "act_done",
|
||||
title: "Done",
|
||||
status: "done",
|
||||
});
|
||||
const cancelled = makeAction({
|
||||
id: "act_cancelled",
|
||||
title: "Cancelled",
|
||||
status: "cancelled",
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", pending.id, pending);
|
||||
await kv.set("mem:actions", done.id, done);
|
||||
await kv.set("mem:actions", cancelled.id, cancelled);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
totalActions: number;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.frontier.length).toBe(1);
|
||||
expect(result.frontier[0].action.id).toBe("act_pending");
|
||||
expect(result.totalActions).toBe(3);
|
||||
});
|
||||
|
||||
it("excludes blocked actions with unsatisfied requires edge", async () => {
|
||||
const dependency = makeAction({
|
||||
id: "act_dep",
|
||||
title: "Dependency",
|
||||
status: "pending",
|
||||
});
|
||||
const blocked = makeAction({
|
||||
id: "act_blocked",
|
||||
title: "Blocked",
|
||||
status: "blocked",
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", dependency.id, dependency);
|
||||
await kv.set("mem:actions", blocked.id, blocked);
|
||||
|
||||
const edge: ActionEdge = {
|
||||
id: "ae_1",
|
||||
type: "requires",
|
||||
sourceActionId: blocked.id,
|
||||
targetActionId: dependency.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await kv.set("mem:action-edges", edge.id, edge);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
const ids = result.frontier.map((f) => f.action.id);
|
||||
expect(ids).toContain("act_dep");
|
||||
expect(ids).not.toContain("act_blocked");
|
||||
});
|
||||
|
||||
it("respects project filter", async () => {
|
||||
const alphaAction = makeAction({
|
||||
id: "act_alpha",
|
||||
title: "Alpha task",
|
||||
project: "alpha",
|
||||
});
|
||||
const betaAction = makeAction({
|
||||
id: "act_beta",
|
||||
title: "Beta task",
|
||||
project: "beta",
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", alphaAction.id, alphaAction);
|
||||
await kv.set("mem:actions", betaAction.id, betaAction);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {
|
||||
project: "alpha",
|
||||
})) as { success: boolean; frontier: FrontierItem[] };
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.frontier.length).toBe(1);
|
||||
expect(result.frontier[0].action.project).toBe("alpha");
|
||||
});
|
||||
|
||||
it("higher priority scores higher", async () => {
|
||||
const low = makeAction({
|
||||
id: "act_low",
|
||||
title: "Low",
|
||||
priority: 1,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
const high = makeAction({
|
||||
id: "act_high",
|
||||
title: "High",
|
||||
priority: 10,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", low.id, low);
|
||||
await kv.set("mem:actions", high.id, high);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
};
|
||||
|
||||
expect(result.frontier[0].action.id).toBe("act_high");
|
||||
expect(result.frontier[0].score).toBeGreaterThan(
|
||||
result.frontier[1].score,
|
||||
);
|
||||
});
|
||||
|
||||
it("excludes actions gated by pending checkpoint", async () => {
|
||||
const gatedAction = makeAction({
|
||||
id: "act_gated",
|
||||
title: "Gated action",
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
const checkpoint: Checkpoint = {
|
||||
id: "ckpt_1",
|
||||
name: "CI check",
|
||||
description: "Waiting for CI",
|
||||
status: "pending",
|
||||
type: "ci",
|
||||
createdAt: new Date().toISOString(),
|
||||
linkedActionIds: ["act_gated"],
|
||||
};
|
||||
|
||||
await kv.set("mem:actions", gatedAction.id, gatedAction);
|
||||
await kv.set("mem:checkpoints", checkpoint.id, checkpoint);
|
||||
|
||||
const gateEdge: ActionEdge = {
|
||||
id: "ae_gate",
|
||||
type: "gated_by",
|
||||
sourceActionId: gatedAction.id,
|
||||
targetActionId: checkpoint.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await kv.set("mem:action-edges", gateEdge.id, gateEdge);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
};
|
||||
|
||||
expect(result.frontier.length).toBe(0);
|
||||
});
|
||||
|
||||
it("excludes actions conflicting with active actions", async () => {
|
||||
const activeAction = makeAction({
|
||||
id: "act_active",
|
||||
title: "Active task",
|
||||
status: "active",
|
||||
});
|
||||
const conflictAction = makeAction({
|
||||
id: "act_conflict",
|
||||
title: "Conflicting task",
|
||||
status: "pending",
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", activeAction.id, activeAction);
|
||||
await kv.set("mem:actions", conflictAction.id, conflictAction);
|
||||
|
||||
const conflictEdge: ActionEdge = {
|
||||
id: "ae_conflict",
|
||||
type: "conflicts_with",
|
||||
sourceActionId: conflictAction.id,
|
||||
targetActionId: activeAction.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await kv.set("mem:action-edges", conflictEdge.id, conflictEdge);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
};
|
||||
|
||||
const ids = result.frontier.map((f) => f.action.id);
|
||||
expect(ids).toContain("act_active");
|
||||
expect(ids).not.toContain("act_conflict");
|
||||
});
|
||||
|
||||
it("active actions get score bonus", async () => {
|
||||
const pendingAction = makeAction({
|
||||
id: "act_pending",
|
||||
title: "Pending",
|
||||
status: "pending",
|
||||
priority: 5,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
const activeAction = makeAction({
|
||||
id: "act_active",
|
||||
title: "Active",
|
||||
status: "active",
|
||||
priority: 5,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", pendingAction.id, pendingAction);
|
||||
await kv.set("mem:actions", activeAction.id, activeAction);
|
||||
|
||||
const result = (await sdk.trigger("mem::frontier", {})) as {
|
||||
success: boolean;
|
||||
frontier: FrontierItem[];
|
||||
};
|
||||
|
||||
const activeItem = result.frontier.find(
|
||||
(f) => f.action.id === "act_active",
|
||||
)!;
|
||||
const pendingItem = result.frontier.find(
|
||||
(f) => f.action.id === "act_pending",
|
||||
)!;
|
||||
|
||||
expect(activeItem.score).toBeGreaterThan(pendingItem.score);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mem::next", () => {
|
||||
it("returns top suggestion when actions exist", async () => {
|
||||
const action = makeAction({
|
||||
id: "act_1",
|
||||
title: "Top task",
|
||||
priority: 8,
|
||||
tags: ["urgent"],
|
||||
});
|
||||
await kv.set("mem:actions", action.id, action);
|
||||
|
||||
const result = (await sdk.trigger("mem::next", {})) as {
|
||||
success: boolean;
|
||||
suggestion: {
|
||||
actionId: string;
|
||||
title: string;
|
||||
description: string;
|
||||
priority: number;
|
||||
score: number;
|
||||
tags: string[];
|
||||
} | null;
|
||||
message: string;
|
||||
totalActions: number;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.suggestion).not.toBeNull();
|
||||
expect(result.suggestion!.actionId).toBe("act_1");
|
||||
expect(result.suggestion!.title).toBe("Top task");
|
||||
expect(result.suggestion!.priority).toBe(8);
|
||||
expect(result.suggestion!.tags).toEqual(["urgent"]);
|
||||
expect(result.message).toContain("Top task");
|
||||
expect(result.totalActions).toBe(1);
|
||||
});
|
||||
|
||||
it("returns null suggestion when no actions exist", async () => {
|
||||
const result = (await sdk.trigger("mem::next", {})) as {
|
||||
success: boolean;
|
||||
suggestion: null;
|
||||
message: string;
|
||||
totalActions: number;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.suggestion).toBeNull();
|
||||
expect(result.message).toContain("No actionable work");
|
||||
expect(result.totalActions).toBe(0);
|
||||
});
|
||||
|
||||
it("returns null when all actions are done", async () => {
|
||||
const doneAction = makeAction({
|
||||
id: "act_done",
|
||||
title: "Completed",
|
||||
status: "done",
|
||||
});
|
||||
await kv.set("mem:actions", doneAction.id, doneAction);
|
||||
|
||||
const result = (await sdk.trigger("mem::next", {})) as {
|
||||
success: boolean;
|
||||
suggestion: null;
|
||||
message: string;
|
||||
totalActions: number;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.suggestion).toBeNull();
|
||||
expect(result.totalActions).toBe(1);
|
||||
});
|
||||
|
||||
it("propagates failure when frontier fails", async () => {
|
||||
const originalFunctions = new Map<string, Function>();
|
||||
|
||||
const failSdk = {
|
||||
registerFunction: (idOrOpts: string | { id: string }, handler: Function) => {
|
||||
const id = typeof idOrOpts === "string" ? idOrOpts : idOrOpts.id;
|
||||
originalFunctions.set(id, handler);
|
||||
},
|
||||
registerTrigger: () => {},
|
||||
trigger: async (
|
||||
idOrInput: string | { function_id: string; payload: unknown },
|
||||
data?: unknown,
|
||||
) => {
|
||||
const id = typeof idOrInput === "string" ? idOrInput : idOrInput.function_id;
|
||||
const payload = typeof idOrInput === "string" ? data : idOrInput.payload;
|
||||
if (id === "mem::frontier") {
|
||||
return { success: false, error: "internal failure" };
|
||||
}
|
||||
const fn = originalFunctions.get(id);
|
||||
if (!fn) throw new Error(`No function: ${id}`);
|
||||
return fn(payload);
|
||||
},
|
||||
};
|
||||
|
||||
const failKv = mockKV();
|
||||
registerFrontierFunction(failSdk as never, failKv as never);
|
||||
|
||||
const nextFn = originalFunctions.get("mem::next")!;
|
||||
const result = (await nextFn({})) as {
|
||||
success: boolean;
|
||||
suggestion: null;
|
||||
message: string;
|
||||
totalActions: number;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.suggestion).toBeNull();
|
||||
expect(result.message).toContain("Failed to compute frontier");
|
||||
expect(result.totalActions).toBe(0);
|
||||
});
|
||||
|
||||
it("respects project filter", async () => {
|
||||
const alphaAction = makeAction({
|
||||
id: "act_alpha",
|
||||
title: "Alpha task",
|
||||
project: "alpha",
|
||||
priority: 5,
|
||||
});
|
||||
const betaAction = makeAction({
|
||||
id: "act_beta",
|
||||
title: "Beta task",
|
||||
project: "beta",
|
||||
priority: 10,
|
||||
});
|
||||
|
||||
await kv.set("mem:actions", alphaAction.id, alphaAction);
|
||||
await kv.set("mem:actions", betaAction.id, betaAction);
|
||||
|
||||
const result = (await sdk.trigger("mem::next", {
|
||||
project: "alpha",
|
||||
})) as {
|
||||
success: boolean;
|
||||
suggestion: { actionId: string; title: string } | null;
|
||||
};
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.suggestion).not.toBeNull();
|
||||
expect(result.suggestion!.actionId).toBe("act_alpha");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user