feat(FN-5057): merge fusion/fn-5057

This commit is contained in:
gsxdsm
2026-05-18 11:46:43 -07:00
parent 46d09285d5
commit 9aa7518bad
6 changed files with 248 additions and 28 deletions

View File

@@ -0,0 +1,97 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { TaskStore, resolveTaskGithubTracking, setTaskCreatedHook } from "@fusion/core";
import { createDelegateTaskTool, createTaskCreateTool } from "../agent-tools.js";
const githubTrackingHookEntry = "../../../dashboard/src/github-tracking-hook.js";
const githubTrackingEntry = "../../../dashboard/src/github-tracking.js";
const githubTrackingHookModulePromise: Promise<any> = import(/* @vite-ignore */ githubTrackingHookEntry);
const githubTrackingModulePromise: Promise<any> = import(/* @vite-ignore */ githubTrackingEntry);
function makeTmpDir(prefix: string): string {
return mkdtempSync(join(tmpdir(), prefix));
}
describe("agent tool github tracking end-to-end", () => {
let rootDir: string;
let globalDir: string;
let store: TaskStore;
beforeEach(async () => {
setTaskCreatedHook(undefined);
vi.restoreAllMocks();
rootDir = makeTmpDir("kb-engine-agent-tools-gh-track-e2e-");
globalDir = makeTmpDir("kb-engine-agent-tools-gh-track-e2e-global-");
store = new TaskStore(rootDir, globalDir, { inMemoryDb: true });
await store.init();
await store.updateSettings({
githubTrackingEnabledByDefault: true,
githubTrackingDefaultRepo: "owner/repo",
});
});
afterEach(async () => {
setTaskCreatedHook(undefined);
store.close();
await rm(rootDir, { recursive: true, force: true });
await rm(globalDir, { recursive: true, force: true });
});
it.each([
{
name: "fn_task_create",
run: async () => createTaskCreateTool(store, { sourceType: "api" }).execute(
"call-1",
{ description: "agent-created tracked task" } as never,
undefined,
undefined,
{} as never,
),
},
{
name: "fn_delegate_task",
run: async () => createDelegateTaskTool({
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
} as never, store).execute(
"call-1",
{ agent_id: "agent-1", description: "delegated tracked task" } as never,
undefined,
undefined,
{} as never,
),
},
])("invokes maybeCreateTrackingIssue for $name", async ({ run }) => {
const githubTrackingModule = await githubTrackingModulePromise;
const githubTrackingHookModule = await githubTrackingHookModulePromise;
const maybeCreateSpy = vi.spyOn(githubTrackingModule, "maybeCreateTrackingIssue").mockResolvedValue({
created: false,
reason: "tracking_disabled",
});
githubTrackingHookModule.registerGithubTrackingHook();
const result = await run();
const taskId = (result as { details?: { taskId?: string } }).details?.taskId as string;
expect(taskId).toMatch(/^FN-/);
expect(maybeCreateSpy).toHaveBeenCalledTimes(1);
const [taskArg, depsArg] = (maybeCreateSpy.mock.calls[0] ?? []) as [
{ id?: string } | undefined,
{ projectSettings?: unknown; globalSettings?: unknown } | undefined,
];
expect(taskArg?.id).toBe(taskId);
const persisted = await store.getTask(taskId);
expect(persisted).toBeTruthy();
const resolvedTracking = resolveTaskGithubTracking(
persisted!,
depsArg?.projectSettings as never,
depsArg?.globalSettings as never,
);
expect(resolvedTracking.enabled).toBe(true);
expect(resolvedTracking.repo).toEqual({ owner: "owner", repo: "repo" });
});
});

View File

@@ -30,41 +30,48 @@ describe("agent task creation github-tracking hook integration", () => {
await rm(globalDir, { recursive: true, force: true });
});
it("calls the post-create hook for fn_task_create", async () => {
it.each([
{
name: "fn_task_create",
createTool: () => createTaskCreateTool(store, { sourceType: "api" }),
params: { description: "agent-created triage task" },
expected: { description: "agent-created triage task", column: "triage", sourceType: "api" },
},
{
name: "fn_delegate_task",
createTool: () => createDelegateTaskTool({
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
} as never, store),
params: { agent_id: "agent-1", description: "delegated tracked task" },
expected: { description: "delegated tracked task", assignedAgentId: "agent-1", column: "todo", sourceType: "api" },
},
])("calls the post-create hook for $name", async ({ createTool, params, expected }) => {
const hook = vi.fn(async (_task: Task) => {});
setTaskCreatedHook(hook);
const tool = createTaskCreateTool(store);
const result = await tool.execute("call-1", { description: "agent-created triage task" } as never, undefined, undefined, {} as never);
const result = await createTool().execute("call-1", params as never, undefined, undefined, {} as never);
expect(result.details).toHaveProperty("taskId");
expect(hook).toHaveBeenCalledTimes(1);
expect(hook.mock.calls[0]?.[0]).toEqual(expect.objectContaining({
description: "agent-created triage task",
column: "triage",
}));
expect(hook.mock.calls[0]?.[0]).toEqual(expect.objectContaining(expected));
});
it("calls the post-create hook for fn_delegate_task", async () => {
const hook = vi.fn(async (_task: Task) => {});
setTaskCreatedHook(hook);
const agentStore = {
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
};
const tool = createDelegateTaskTool(agentStore as never, store);
const result = await tool.execute("call-1", {
agent_id: "agent-1",
description: "delegated tracked task",
} as never, undefined, undefined, {} as never);
expect(result.details).toEqual(expect.objectContaining({ taskId: expect.any(String), agentId: "agent-1" }));
expect(hook).toHaveBeenCalledTimes(1);
expect(hook.mock.calls[0]?.[0]).toEqual(expect.objectContaining({
description: "delegated tracked task",
assignedAgentId: "agent-1",
column: "todo",
it.each([
{
name: "fn_task_create",
run: async () => createTaskCreateTool(store, { sourceType: "api" }).execute("call-1", { description: "fails softly" } as never, undefined, undefined, {} as never),
},
{
name: "fn_delegate_task",
run: async () => createDelegateTaskTool({
getAgent: vi.fn().mockResolvedValue({ id: "agent-1", name: "Worker", role: "executor", state: "idle" }),
} as never, store).execute("call-1", { agent_id: "agent-1", description: "fails softly" } as never, undefined, undefined, {} as never),
},
])("does not throw when hook rejects for $name", async ({ run }) => {
setTaskCreatedHook(vi.fn(async () => {
throw new Error("hook failed");
}));
await expect(run()).resolves.toBeTruthy();
});
});

View File

@@ -11,6 +11,7 @@ export default defineConfig({
"@fusion/test-utils": resolve(__dirname, "../core/src/__test-utils__/workspace.ts"),
"@fusion/engine": resolve(__dirname, "./src/index.ts"),
"@fusion/plugin-sdk": resolve(__dirname, "../plugin-sdk/src/index.ts"),
"@fusion/dashboard": resolve(__dirname, "../dashboard/src/index.ts"),
},
},
test: {