feat(FN-4899): complete Step 4 — surface duplicate lineage in CLI outputs
Fusion-Task-Id: FN-4899 Fusion-Task-Lineage: 2ae24667-6635-4268-a620-d6012266d8f2
This commit is contained in:
committed by
gsxdsm
parent
67d6a292c1
commit
990c1896dd
@@ -854,22 +854,10 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
describe("fn_task_show", () => {
|
||||
it("shows task details", async () => {
|
||||
const createTool = api.tools.get("fn_task_create")!;
|
||||
await createTool.execute(
|
||||
"c1",
|
||||
{ description: "Implement caching layer" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
await createTool.execute("c1", { description: "Implement caching layer" }, undefined, undefined, makeCtx(tmpDir));
|
||||
|
||||
const showTool = api.tools.get("fn_task_show")!;
|
||||
const result = await showTool.execute(
|
||||
"call-1",
|
||||
{ id: "FN-001" },
|
||||
undefined,
|
||||
undefined,
|
||||
makeCtx(tmpDir),
|
||||
);
|
||||
const result = await showTool.execute("call-1", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
|
||||
|
||||
expect(result.content[0].text).toContain("FN-001");
|
||||
expect(result.content[0].text).toContain("Implement caching layer");
|
||||
@@ -891,10 +879,7 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
sourceMetadata: { agentName: "Scout" },
|
||||
},
|
||||
});
|
||||
await store.createTask({
|
||||
description: "UI created",
|
||||
source: { sourceType: "dashboard_ui" },
|
||||
});
|
||||
await store.createTask({ description: "UI created", source: { sourceType: "dashboard_ui" } });
|
||||
|
||||
const showTool = api.tools.get("fn_task_show")!;
|
||||
const agentResult = await showTool.execute("call-2", { id: "FN-001" }, undefined, undefined, makeCtx(tmpDir));
|
||||
@@ -902,8 +887,23 @@ describe.skipIf(!SHOULD_RUN_LEGACY_EXTENSION_INTEGRATION)("fn pi extension (lega
|
||||
|
||||
expect(agentResult.content[0].text).toContain("Created via: Agent (Scout)");
|
||||
expect(dashboardResult.content[0].text).toContain("Created via: Dashboard");
|
||||
expect(agentResult.details.task.sourceMetadata?.agentName).toBe("Scout");
|
||||
expect(agentResult.details.task.sourceAgentId).toBe("agent-999");
|
||||
});
|
||||
|
||||
it("shows duplicate lineage with archived annotation", async () => {
|
||||
const store = new TaskStore(tmpDir);
|
||||
await store.init();
|
||||
|
||||
const archivedSource = await store.createTask({ description: "Archived source" });
|
||||
await store.moveTask(archivedSource.id, "done");
|
||||
await store.archiveTask(archivedSource.id);
|
||||
await store.createTask({
|
||||
description: "Dup task",
|
||||
source: { sourceType: "chat_session", sourceMetadata: { duplicateOfTaskIds: [archivedSource.id, "FN-404"] } },
|
||||
});
|
||||
|
||||
const showTool = api.tools.get("fn_task_show")!;
|
||||
const result = await showTool.execute("call-4", { id: "FN-002" }, undefined, undefined, makeCtx(tmpDir));
|
||||
expect(result.content[0].text).toContain(`Duplicate of: ${archivedSource.id} (archived), FN-404`);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -42,6 +42,17 @@ vi.mock("@fusion/core", () => {
|
||||
TaskStore: vi.fn(),
|
||||
COLUMNS,
|
||||
COLUMN_LABELS,
|
||||
getTaskDuplicateLineage: vi.fn((task: { sourceType?: string; sourceParentTaskId?: string; sourceMetadata?: any }) => {
|
||||
const ids: string[] = [];
|
||||
if (task.sourceType === "task_duplicate" && task.sourceParentTaskId) ids.push(task.sourceParentTaskId);
|
||||
const metadata = task.sourceMetadata?.duplicateOfTaskIds;
|
||||
if (Array.isArray(metadata)) {
|
||||
for (const id of metadata) {
|
||||
if (typeof id === "string" && !ids.includes(id)) ids.push(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}),
|
||||
CentralCore: vi.fn().mockImplementation(function() {
|
||||
return {
|
||||
init: vi.fn().mockResolvedValue(undefined),
|
||||
@@ -219,6 +230,28 @@ describe("runTaskShow", () => {
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).not.toContain("Source:");
|
||||
});
|
||||
|
||||
it("prints duplicate lineage from metadata and task_duplicate parent", async () => {
|
||||
mockTaskStoreGetTask(makeTask({
|
||||
sourceType: "task_duplicate",
|
||||
sourceParentTaskId: "FN-2905",
|
||||
sourceMetadata: { duplicateOfTaskIds: ["FN-9"] },
|
||||
}));
|
||||
|
||||
await runTaskShow("FN-001");
|
||||
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain("Duplicate of: FN-2905, FN-9");
|
||||
});
|
||||
|
||||
it("prints duplicate lineage from metadata list", async () => {
|
||||
mockTaskStoreGetTask(makeTask({ sourceMetadata: { duplicateOfTaskIds: ["FN-1", "FN-2"] } }));
|
||||
|
||||
await runTaskShow("FN-001");
|
||||
|
||||
const output = logSpy.mock.calls.map((call) => String(call[0])).join("\n");
|
||||
expect(output).toContain("Duplicate of: FN-1, FN-2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("task node overrides", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, CentralCore, type Settings, type Column, type StepStatus, type AgentLogType, type AgentLogEntry } from "@fusion/core";
|
||||
import { TaskStore, COLUMNS, COLUMN_LABELS, CentralCore, getTaskDuplicateLineage, type Settings, type Column, type StepStatus, type AgentLogType, type AgentLogEntry } from "@fusion/core";
|
||||
import { aiMergeTask, listBranchRecoveryCandidates, type BranchRecoveryCandidate } from "@fusion/engine";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import type { PlanningQuestion, PlanningSummary } from "@fusion/core";
|
||||
@@ -59,6 +59,22 @@ function getResearchSourceContext(sourceMetadata: unknown): string | undefined {
|
||||
return typeof runId === "string" && runId.length > 0 ? runId : undefined;
|
||||
}
|
||||
|
||||
async function formatTaskDuplicateLineage(task: Awaited<ReturnType<TaskStore["getTask"]>>, store: TaskStore): Promise<string | null> {
|
||||
const lineage = getTaskDuplicateLineage(task);
|
||||
if (lineage.length === 0) return null;
|
||||
|
||||
const labels = await Promise.all(lineage.map(async (id) => {
|
||||
try {
|
||||
const linked = await store.getTask(id);
|
||||
return linked.column === "archived" ? `${id} (archived)` : id;
|
||||
} catch {
|
||||
return id;
|
||||
}
|
||||
}));
|
||||
|
||||
return labels.join(", ");
|
||||
}
|
||||
|
||||
function formatTaskSource(task: {
|
||||
sourceType?: string;
|
||||
sourceAgentId?: string;
|
||||
@@ -677,6 +693,10 @@ export async function runTaskShow(id: string, projectName?: string) {
|
||||
if (sourceSummary) {
|
||||
console.log(` Source: ${sourceSummary}`);
|
||||
}
|
||||
const duplicateLineage = await formatTaskDuplicateLineage(task, store);
|
||||
if (duplicateLineage) {
|
||||
console.log(` Duplicate of: ${duplicateLineage}`);
|
||||
}
|
||||
console.log();
|
||||
|
||||
// Steps
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
resolveResearchSettings,
|
||||
canAgentTakeImplementationTaskForExplicitRouting,
|
||||
formatRoleMismatchReason,
|
||||
getTaskDuplicateLineage,
|
||||
resolveAgentProvisioningPolicy,
|
||||
TASK_PRIORITIES,
|
||||
resolveSecretAccessPolicy,
|
||||
@@ -269,6 +270,22 @@ function getTaskSourceLabel(task: Pick<Task, "sourceType" | "sourceMetadata" | "
|
||||
}
|
||||
}
|
||||
|
||||
async function formatDuplicateLineageLine(task: Task, store: TaskStore): Promise<string | null> {
|
||||
const lineage = getTaskDuplicateLineage(task);
|
||||
if (lineage.length === 0) return null;
|
||||
|
||||
const labels = await Promise.all(lineage.map(async (id) => {
|
||||
try {
|
||||
const linked = await store.getTask(id);
|
||||
return linked.column === "archived" ? `${id} (archived)` : id;
|
||||
} catch {
|
||||
return id;
|
||||
}
|
||||
}));
|
||||
|
||||
return `Duplicate of: ${labels.join(", ")}`;
|
||||
}
|
||||
|
||||
function formatTaskLine(t: Task): string {
|
||||
const label =
|
||||
t.title || t.description.slice(0, 60) + (t.description.length > 60 ? "…" : "");
|
||||
@@ -755,6 +772,10 @@ export default function kbExtension(pi: ExtensionAPI) {
|
||||
if (sourceLabel) {
|
||||
lines.push(`Created via: ${sourceLabel}`);
|
||||
}
|
||||
const duplicateLineage = await formatDuplicateLineageLine(task, store);
|
||||
if (duplicateLineage) {
|
||||
lines.push(duplicateLineage);
|
||||
}
|
||||
if (task.paused) lines.push("Status: PAUSED");
|
||||
lines.push("");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user