fix(FN-6035): address workflow projection review feedback

This commit is contained in:
gsxdsm
2026-06-09 00:01:00 -07:00
parent ad68c9d612
commit 40d05c8139
6 changed files with 113 additions and 16 deletions

View File

@@ -297,6 +297,43 @@ describe("WorkflowGraphExecutor traversal", () => {
expect(result.context["node:a:projectionError"]).toBe("store unavailable");
});
it("does not fail the node when the deprecated touched-files hook fails", async () => {
const ir: WorkflowIr = {
version: "v1",
name: "legacy-touched-files-failure",
nodes: [
{ id: "start", kind: "start" },
{ id: "a", kind: "prompt" },
{ id: "end", kind: "end" },
],
edges: [
{ from: "start", to: "a" },
{ from: "a", to: "end", condition: "success" },
],
};
const publishTaskProjection = vi.fn();
const publishTouchedFiles = vi.fn(async () => {
throw new Error("legacy sink unavailable");
});
const executor = new WorkflowGraphExecutor({
handlers: {
prompt: async () => ({
outcome: "success",
contextPatch: { modifiedFiles: ["src/projected.ts"] },
}),
},
publishTaskProjection,
publishTouchedFiles,
});
const result = await executor.run(task, settingsOn(), ir);
expect(publishTaskProjection).toHaveBeenCalledTimes(1);
expect(publishTouchedFiles).toHaveBeenCalledTimes(1);
expect(result.outcome).toBe("success");
expect(result.context["node:a:projectionError"]).toBeUndefined();
});
it("caps retries and converts exceptions to failure", async () => {
const ir: WorkflowIr = {
version: "v1",

View File

@@ -3752,19 +3752,18 @@ export class TaskExecutor {
runCustomNode: (node, nodeTask) =>
this.runGraphCustomNode(node, nodeTask, settings, resolveBindingForNode(node.id)),
publishTaskProjection: async (taskId, patch) => {
const liveTask = await this.store.getTask(taskId);
const update: Parameters<TaskStore["updateTask"]>[1] = {};
if (patch.modifiedFiles) {
const merged = [...new Set([...(liveTask?.modifiedFiles ?? []), ...patch.modifiedFiles])].sort();
if (merged.length > 0) update.modifiedFiles = merged;
}
if (patch.mergeDetails) {
update.mergeDetails = { ...(liveTask?.mergeDetails ?? {}), ...patch.mergeDetails };
}
if (patch.summary !== undefined) update.summary = patch.summary;
if (Object.keys(update).length > 0) {
await this.store.updateTask(taskId, update);
}
await this.store.updateTaskAtomic(taskId, (liveTask) => {
const update: Parameters<TaskStore["updateTask"]>[1] = {};
if (patch.modifiedFiles) {
const merged = [...new Set([...(liveTask.modifiedFiles ?? []), ...patch.modifiedFiles])].sort();
if (merged.length > 0) update.modifiedFiles = merged;
}
if (patch.mergeDetails) {
update.mergeDetails = { ...(liveTask.mergeDetails ?? {}), ...patch.mergeDetails };
}
if (patch.summary !== undefined) update.summary = patch.summary;
return update;
});
},
onEvent: (event) => executorLog.log(`[workflow-graph] ${event.type} ${event.taskId}: ${event.detail}`),
// Wire SQLite-backed per-branch persistence in production (#1407): the

View File

@@ -717,9 +717,6 @@ export class WorkflowGraphExecutor {
const source = { nodeId: node.id, nodeKind: node.kind };
try {
await this.deps.publishTaskProjection?.(taskId, patch, source);
if (patch.modifiedFiles && patch.modifiedFiles.length > 0) {
await this.deps.publishTouchedFiles?.(taskId, patch.modifiedFiles, source);
}
} catch (error) {
return {
outcome: "failure",
@@ -730,6 +727,13 @@ export class WorkflowGraphExecutor {
},
};
}
if (patch.modifiedFiles && patch.modifiedFiles.length > 0) {
try {
await this.deps.publishTouchedFiles?.(taskId, patch.modifiedFiles, source);
} catch {
// Deprecated compatibility hook; primary projection persistence owns node outcome.
}
}
return result;
}
}