feat(FN-4942): complete Step 2 — route tracking creation via hook
Fusion-Task-Id: FN-4942 Fusion-Task-Lineage: 20865705-e5ec-4261-932c-4d47c2d8dc12
This commit is contained in:
committed by
gsxdsm
parent
fc98f46196
commit
8ef7fe2b18
@@ -182,6 +182,25 @@ describe("registerGithubTrackingHook", () => {
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("passes registered hook githubToken fallback when settings token is missing", async () => {
|
||||
registerGithubTrackingHook({ githubToken: "hook-token" });
|
||||
|
||||
await store.updateSettings({
|
||||
githubTrackingDefaultRepo: "o/r",
|
||||
githubAuthMode: "token",
|
||||
});
|
||||
|
||||
await store.createTask({
|
||||
description: "hook token fallback",
|
||||
title: "Hook token fallback",
|
||||
githubTracking: { enabled: true },
|
||||
});
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(mockCreateIssue).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers settings token over request token", async () => {
|
||||
await store.updateSettings({ githubAuthToken: "settings-token" });
|
||||
const task = await store.createTask({
|
||||
@@ -401,6 +420,31 @@ describe("registerGithubTrackingHook", () => {
|
||||
expect(mockCreateIssue).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("creates exactly one issue per planning-style createTask invocation", async () => {
|
||||
registerGithubTrackingHook();
|
||||
|
||||
await store.updateSettings({
|
||||
githubTrackingEnabledByDefault: true,
|
||||
githubTrackingDefaultRepo: "owner/repo",
|
||||
githubAuthMode: "token",
|
||||
githubAuthToken: "tok",
|
||||
});
|
||||
|
||||
await store.createTask({
|
||||
title: "Planning single task",
|
||||
description: "planning summary output",
|
||||
source: { sourceType: "api" },
|
||||
});
|
||||
|
||||
await store.createTask({
|
||||
title: "Planning subtask A",
|
||||
description: "planning subtask output",
|
||||
source: { sourceType: "api", sourceMetadata: { planningSessionId: "sess-1" } },
|
||||
});
|
||||
|
||||
expect(mockCreateIssue).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("creates issue during createTask await when summarization is disabled", async () => {
|
||||
registerGithubTrackingHook();
|
||||
|
||||
|
||||
@@ -39,13 +39,13 @@ export async function createTrackingIssueForTask(
|
||||
* Idempotent: calling this twice replaces the previous hook (no chaining).
|
||||
*/
|
||||
export function registerGithubTrackingHook(
|
||||
options?: { logger?: Pick<Console, "warn" | "info"> },
|
||||
options?: { githubToken?: string; logger?: Pick<Console, "warn" | "info"> },
|
||||
): void {
|
||||
const logger = options?.logger ?? console;
|
||||
|
||||
setTaskCreatedHook(async (task: Task, store: TaskStore) => {
|
||||
try {
|
||||
await createTrackingIssueForTask(store, task, { logger });
|
||||
await createTrackingIssueForTask(store, task, { githubToken: options?.githubToken, logger });
|
||||
} catch (error) {
|
||||
// Best-effort: never propagate out of the hook.
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -11,7 +11,6 @@ import { writeSSEEvent, type SessionBufferedEvent } from "../sse-buffer.js";
|
||||
import type { AiSessionStore } from "../ai-session-store.js";
|
||||
import type { ApiRoutesContext } from "./types.js";
|
||||
import { derivePerTaskBranch, resolveBranchAssignmentContext, resolveBranchSelection } from "./branch-selection.js";
|
||||
import { createTrackingIssueForTask } from "../github-tracking-hook.js";
|
||||
|
||||
interface PlanningSubtaskRouteDeps {
|
||||
store: TaskStore;
|
||||
@@ -25,15 +24,6 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
const { router, getProjectContext, planningLogger, rethrowAsApiError } = ctx;
|
||||
const { aiSessionStore, checkSessionLock, parseLastEventId, replayBufferedSSE } = deps;
|
||||
|
||||
const dispatchTrackingIssueCreation = (scopedStore: TaskStore, task: Awaited<ReturnType<TaskStore["createTask"]>>): void => {
|
||||
void createTrackingIssueForTask(scopedStore, task, { logger: planningLogger }).catch((error: unknown) => {
|
||||
planningLogger.warn("Background planning tracking-issue creation failed", {
|
||||
taskId: task.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// ── Planning Mode Routes ──────────────────────────────────────────────────
|
||||
// UTILITY PATH: Planning and subtask session routes are on a separate control-plane lane.
|
||||
// They must NOT be gated on task-lane saturation (maxConcurrent, semaphore, queue depth).
|
||||
@@ -1087,7 +1077,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
source: { sourceType: "api" },
|
||||
branch: resolvedBranch,
|
||||
baseBranch: resolvedBaseBranch,
|
||||
}, { invokeTaskCreatedHook: false });
|
||||
});
|
||||
|
||||
// Update task with suggested size if provided
|
||||
if (summary.suggestedSize) {
|
||||
@@ -1105,7 +1095,6 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
}
|
||||
|
||||
res.status(201).json(task);
|
||||
dispatchTrackingIssueCreation(scopedStore, task);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
@@ -1281,7 +1270,7 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
branch: taskBranch,
|
||||
baseBranch: resolvedBaseBranch,
|
||||
branchContext: planningBranchContext,
|
||||
}, { invokeTaskCreatedHook: false });
|
||||
});
|
||||
|
||||
tempIdToTaskId.set(item.id, task.id);
|
||||
createdTasks.push(task);
|
||||
@@ -1309,9 +1298,6 @@ export function registerPlanningSubtaskRoutes(ctx: ApiRoutesContext, deps: Plann
|
||||
cleanupSession(planningSessionId);
|
||||
|
||||
res.status(201).json({ tasks: createdTasks });
|
||||
for (const task of createdTasks) {
|
||||
dispatchTrackingIssueCreation(scopedStore, task);
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
|
||||
@@ -458,7 +458,6 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
createInput,
|
||||
{ onSummarize, settings: { autoSummarizeTitles: settings.autoSummarizeTitles } },
|
||||
);
|
||||
await createTrackingIssueForTask(scopedStore, task, { githubToken: options?.githubToken });
|
||||
|
||||
if (acknowledgedDuplicateIds.length > 0) {
|
||||
try {
|
||||
@@ -732,7 +731,6 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const newTask = await scopedStore.duplicateTask(req.params.id);
|
||||
await createTrackingIssueForTask(scopedStore, newTask, { githubToken: options?.githubToken });
|
||||
res.status(201).json(newTask);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
@@ -759,7 +757,6 @@ export function registerTaskWorkflowRoutes(ctx: ApiRoutesContext, deps: TaskWork
|
||||
}
|
||||
|
||||
const refinedTask = await scopedStore.refineTask(req.params.id, trimmedFeedback);
|
||||
await createTrackingIssueForTask(scopedStore, refinedTask, { githubToken: options?.githubToken });
|
||||
await scopedStore.logEntry(req.params.id, "Refinement requested", trimmedFeedback);
|
||||
res.status(201).json(refinedTask);
|
||||
} catch (err: unknown) {
|
||||
|
||||
@@ -537,7 +537,7 @@ export function createServer(store: TaskStore, options?: ServerOptions): ReturnT
|
||||
// (HTTP routes, CLI, pi extension, mission triage, etc.) triggers
|
||||
// GitHub tracking issue creation when enabled.
|
||||
try {
|
||||
registerGithubTrackingHook();
|
||||
registerGithubTrackingHook({ githubToken: options?.githubToken });
|
||||
} catch (error) {
|
||||
// Some unit tests mock @fusion/core with narrow export surfaces. Keep
|
||||
// server bootstrap resilient when hook registration is unavailable.
|
||||
|
||||
Reference in New Issue
Block a user