test(FN-4770): complete Steps 3-4 — add tracking async coverage
Fusion-Task-Id: FN-4770 Fusion-Task-Lineage: 8c8d1786-f81b-4544-9275-595b90ea5689
This commit is contained in:
committed by
gsxdsm
parent
0653d94f1c
commit
db7b196462
@@ -10,6 +10,7 @@ import { CentralDatabase } from "../central-db.js";
|
||||
import { TaskStore, TaskHasDependentsError } from "../store.js";
|
||||
import type { runCommandAsync } from "../run-command.js";
|
||||
import { buildResearchDocumentKey, type Task } from "../types.js";
|
||||
import { setTaskCreatedHook } from "../task-creation-hooks.js";
|
||||
|
||||
describe("TaskStore", () => {
|
||||
const harness = createTaskStoreTestHarness();
|
||||
@@ -34,6 +35,37 @@ describe("TaskStore", () => {
|
||||
const createSourceIssueFixture = () => harness.createSourceIssueFixture();
|
||||
const insertLogEntryWithTimestamp = (...args: any[]) => (harness as any).insertLogEntryWithTimestamp(...args);
|
||||
|
||||
describe("createTask task-created hook options", () => {
|
||||
afterEach(() => {
|
||||
setTaskCreatedHook(undefined);
|
||||
});
|
||||
|
||||
it("skips task-created hook when invokeTaskCreatedHook is false", async () => {
|
||||
const hookSpy = vi.fn();
|
||||
setTaskCreatedHook(hookSpy);
|
||||
|
||||
await store.createTask(
|
||||
{ description: "Task without post-create hook" },
|
||||
{ invokeTaskCreatedHook: false },
|
||||
);
|
||||
|
||||
expect(hookSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("invokes task-created hook by default", async () => {
|
||||
const hookSpy = vi.fn();
|
||||
setTaskCreatedHook(hookSpy);
|
||||
|
||||
const created = await store.createTask({ description: "Task with default post-create hook" });
|
||||
|
||||
expect(hookSpy).toHaveBeenCalledTimes(1);
|
||||
expect(hookSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: created.id }),
|
||||
expect.any(TaskStore),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("duplicateTask", () => {
|
||||
it("duplicates from triage column", async () => {
|
||||
const task = await store.createTask({ description: "Test task" });
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import express from "express";
|
||||
import type { TaskStore } from "@fusion/core";
|
||||
import { registerPlanningSubtaskRoutes } from "../routes/register-planning-subtask-routes.js";
|
||||
import { request as performRequest } from "../test-request.js";
|
||||
import { GitHubClient } from "../github.js";
|
||||
|
||||
type PlanningSession = {
|
||||
summary: {
|
||||
title: string;
|
||||
description: string;
|
||||
suggestedSize: "S" | "M" | "L";
|
||||
priority: "normal";
|
||||
suggestedDependencies: string[];
|
||||
keyDeliverables: string[];
|
||||
};
|
||||
initialPlan: string;
|
||||
history: Array<{ role: string; content: string }>;
|
||||
};
|
||||
|
||||
const sessions = new Map<string, PlanningSession>();
|
||||
|
||||
vi.mock("../planning.js", () => ({
|
||||
getSession: (id: string) => sessions.get(id),
|
||||
getSummary: (id: string) => sessions.get(id)?.summary,
|
||||
cleanupSession: vi.fn(),
|
||||
formatInterviewQA: vi.fn(() => ""),
|
||||
mergePlanningSubtaskDrafts: vi.fn((_sessionId: string, subtasks: unknown[]) => subtasks),
|
||||
}));
|
||||
|
||||
function deferred<T>() {
|
||||
let resolve!: (value: T | PromiseLike<T>) => void;
|
||||
let reject!: (reason?: unknown) => void;
|
||||
const promise = new Promise<T>((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
describe("planning routes github tracking background dispatch", () => {
|
||||
let app: express.Express;
|
||||
let createIssueSpy: ReturnType<typeof vi.spyOn>;
|
||||
let planningWarn: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
sessions.clear();
|
||||
planningWarn = vi.fn();
|
||||
|
||||
let idCounter = 1;
|
||||
const createdTasks = new Map<string, Record<string, unknown>>();
|
||||
const store = {
|
||||
createTask: vi.fn(async (input: { title?: string; description: string }) => {
|
||||
const task = {
|
||||
id: `FN-${idCounter++}`,
|
||||
title: input.title,
|
||||
description: input.description,
|
||||
column: "triage",
|
||||
};
|
||||
createdTasks.set(task.id, task);
|
||||
return task;
|
||||
}),
|
||||
updateTask: vi.fn(async (id: string, patch: Record<string, unknown>) => {
|
||||
const next = { ...(createdTasks.get(id) ?? { id }), ...patch };
|
||||
createdTasks.set(id, next);
|
||||
return next;
|
||||
}),
|
||||
updateGithubTracking: vi.fn(async (id: string, patch: Record<string, unknown>) => {
|
||||
const next = { ...(createdTasks.get(id) ?? { id }), githubTracking: patch };
|
||||
createdTasks.set(id, next);
|
||||
return next;
|
||||
}),
|
||||
logEntry: vi.fn(async () => undefined),
|
||||
getTask: vi.fn(async (id: string) => createdTasks.get(id)),
|
||||
getSettings: vi.fn(async () => ({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
githubTrackingDefaultRepo: "o/r",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "test-token",
|
||||
})),
|
||||
getGlobalSettingsStore: vi.fn(() => ({ getSettings: vi.fn(async () => ({})) })),
|
||||
getRootDir: vi.fn(() => "/tmp/project"),
|
||||
updateIssueInfo: vi.fn(async () => undefined),
|
||||
linkGithubIssue: vi.fn(async () => undefined),
|
||||
recordActivity: vi.fn(async () => undefined),
|
||||
} as unknown as TaskStore;
|
||||
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
|
||||
registerPlanningSubtaskRoutes(
|
||||
{
|
||||
router: app,
|
||||
getProjectContext: async () => ({ store, projectId: "proj-1" }),
|
||||
planningLogger: { info: vi.fn(), warn: planningWarn },
|
||||
rethrowAsApiError: (err: unknown) => {
|
||||
throw err instanceof Error ? err : new Error(String(err));
|
||||
},
|
||||
} as never,
|
||||
{
|
||||
store,
|
||||
checkSessionLock: () => ({ allowed: true }),
|
||||
parseLastEventId: () => undefined,
|
||||
replayBufferedSSE: () => true,
|
||||
},
|
||||
);
|
||||
|
||||
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
||||
res.status(500).json({ error: err instanceof Error ? err.message : String(err) });
|
||||
});
|
||||
|
||||
createIssueSpy = vi.spyOn(GitHubClient.prototype, "createIssue");
|
||||
});
|
||||
|
||||
it("POST /planning/create-task returns before createIssue resolves", async () => {
|
||||
const issueDeferred = deferred<{ number: number; htmlUrl: string; createdAt: string }>();
|
||||
createIssueSpy.mockReturnValue(issueDeferred.promise as never);
|
||||
|
||||
sessions.set("plan-1", {
|
||||
summary: {
|
||||
title: "Planned task",
|
||||
description: "Planned task description",
|
||||
suggestedSize: "M",
|
||||
priority: "normal",
|
||||
suggestedDependencies: [],
|
||||
keyDeliverables: [],
|
||||
},
|
||||
initialPlan: "initial",
|
||||
history: [],
|
||||
});
|
||||
|
||||
const responsePromise = performRequest(
|
||||
app,
|
||||
"POST",
|
||||
"/planning/create-task",
|
||||
JSON.stringify({ sessionId: "plan-1" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
const response = await responsePromise;
|
||||
expect(response.status).toBe(201);
|
||||
await vi.waitFor(() => {
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
issueDeferred.resolve({
|
||||
number: 1,
|
||||
htmlUrl: "https://github.com/o/r/issues/1",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("POST /planning/create-task still returns 201 when createIssue rejects", async () => {
|
||||
createIssueSpy.mockRejectedValue(new Error("github down"));
|
||||
|
||||
sessions.set("plan-2", {
|
||||
summary: {
|
||||
title: "Planned task 2",
|
||||
description: "Planned task description 2",
|
||||
suggestedSize: "M",
|
||||
priority: "normal",
|
||||
suggestedDependencies: [],
|
||||
keyDeliverables: [],
|
||||
},
|
||||
initialPlan: "initial",
|
||||
history: [],
|
||||
});
|
||||
|
||||
const response = await performRequest(
|
||||
app,
|
||||
"POST",
|
||||
"/planning/create-task",
|
||||
JSON.stringify({ sessionId: "plan-2" }),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
await vi.waitFor(() => {
|
||||
expect(planningWarn).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("POST /planning/create-tasks dispatches one createIssue per task without blocking", async () => {
|
||||
const issueDeferred = deferred<{ number: number; htmlUrl: string; createdAt: string }>();
|
||||
createIssueSpy.mockReturnValue(issueDeferred.promise as never);
|
||||
|
||||
sessions.set("plan-3", {
|
||||
summary: {
|
||||
title: "Plan",
|
||||
description: "Plan",
|
||||
suggestedSize: "M",
|
||||
priority: "normal",
|
||||
suggestedDependencies: [],
|
||||
keyDeliverables: [],
|
||||
},
|
||||
initialPlan: "initial",
|
||||
history: [],
|
||||
});
|
||||
|
||||
const response = await performRequest(
|
||||
app,
|
||||
"POST",
|
||||
"/planning/create-tasks",
|
||||
JSON.stringify({
|
||||
planningSessionId: "plan-3",
|
||||
subtasks: [
|
||||
{ id: "tmp-1", title: "Subtask 1", description: "D1" },
|
||||
{ id: "tmp-2", title: "Subtask 2", description: "D2" },
|
||||
],
|
||||
}),
|
||||
{ "content-type": "application/json" },
|
||||
);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
await vi.waitFor(() => {
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
issueDeferred.resolve({
|
||||
number: 2,
|
||||
htmlUrl: "https://github.com/o/r/issues/2",
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(createIssueSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user