From 849b40d2200489c202eb8fbe1467bcd555c8244c Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Mon, 22 Jun 2026 23:56:34 -0700 Subject: [PATCH] FN-6947: tolerate workflow project identity failures Keep workflow IR resolution usable when project identity lookup is unavailable. - Treat workflow settings project identity errors as an absent project scope. - Preserve project-scoped cache and prompt override behavior when identity resolves. - Add resolver regression coverage and a patch changeset for the fallback behavior. Files changed: .changeset/fn-6947-workflow-ir-project-identity.md | 5 ++ .../src/__tests__/workflow-ir-resolver.test.ts | 62 +++++++++++++++++++++- packages/core/src/workflow-ir-resolver.ts | 12 ++++- 3 files changed, 76 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6947 Fusion-Task-Lineage: f63b985f-6c43-4163-8391-8ca5efb83079 --- .../fn-6947-workflow-ir-project-identity.md | 5 ++ .../__tests__/workflow-ir-resolver.test.ts | 62 ++++++++++++++++++- packages/core/src/workflow-ir-resolver.ts | 12 +++- 3 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-6947-workflow-ir-project-identity.md diff --git a/.changeset/fn-6947-workflow-ir-project-identity.md b/.changeset/fn-6947-workflow-ir-project-identity.md new file mode 100644 index 0000000000..ef1b12002b --- /dev/null +++ b/.changeset/fn-6947-workflow-ir-project-identity.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Keep workflow IR and effective-settings resolution usable when project identity lookup fails, falling back to declaration defaults instead of propagating the identity error. diff --git a/packages/core/src/__tests__/workflow-ir-resolver.test.ts b/packages/core/src/__tests__/workflow-ir-resolver.test.ts index 5d8ed61c55..bbcc28ded0 100644 --- a/packages/core/src/__tests__/workflow-ir-resolver.test.ts +++ b/packages/core/src/__tests__/workflow-ir-resolver.test.ts @@ -23,13 +23,28 @@ function makeStore(opts: { selection?: { workflowId: string; stepIds: string[] }; selectionThrows?: boolean; defs?: Record; -}) { + projectId?: string; + projectIdThrows?: boolean; + promptOverrides?: Record; +} = {}) { const getWorkflowDefinition = vi.fn(async (id: string) => opts.defs?.[id]); const getTaskWorkflowSelection = vi.fn((_taskId: string) => { if (opts.selectionThrows) throw new Error("boom"); return opts.selection; }); - return { getWorkflowDefinition, getTaskWorkflowSelection }; + const getWorkflowSettingsProjectId = vi.fn(() => { + if (opts.projectIdThrows) throw new Error("identity boom"); + return opts.projectId ?? "proj-1"; + }); + const getWorkflowPromptOverrides = vi.fn( + (_workflowId: string, _projectId: string) => opts.promptOverrides ?? {}, + ); + return { + getWorkflowDefinition, + getTaskWorkflowSelection, + getWorkflowSettingsProjectId, + getWorkflowPromptOverrides, + }; } describe("resolveWorkflowIrForTask", () => { @@ -118,6 +133,49 @@ describe("resolveWorkflowIrById", () => { expect(ir).toBe(BUILTIN_CODING_WORKFLOW_IR); expect(store.getWorkflowDefinition).not.toHaveBeenCalled(); }); + + it("degrades built-in IR resolution when project identity lookup throws", async () => { + const store = makeStore({ + projectIdThrows: true, + promptOverrides: { planning: "unreachable project override" }, + }); + + const ir = await resolveWorkflowIrById(store, "builtin:coding"); + + expect(ir).toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(store.getWorkflowSettingsProjectId).toHaveBeenCalledTimes(1); + expect(store.getWorkflowPromptOverrides).not.toHaveBeenCalled(); + expect(store.getWorkflowDefinition).not.toHaveBeenCalled(); + }); + + it("uses a workflow-only cache key when project identity lookup throws", async () => { + const store = makeStore({ projectIdThrows: true, defs: { "wf-custom": { ir: CUSTOM_IR } } }); + const cache = new Map([["wf-custom", CUSTOM_IR]]); + + const ir = await resolveWorkflowIrById(store, "wf-custom", cache); + + expect(ir).toBe(CUSTOM_IR); + expect(store.getWorkflowSettingsProjectId).toHaveBeenCalledTimes(1); + expect(store.getWorkflowDefinition).not.toHaveBeenCalled(); + }); + + it("keeps project-scoped prompt overrides and cache keys when project identity resolves", async () => { + const store = makeStore({ + projectId: "proj-override", + promptOverrides: { planning: "Project-specific plan" }, + }); + const cache = new Map(); + + const first = await resolveWorkflowIrById(store, "builtin:coding", cache); + const second = await resolveWorkflowIrById(store, "builtin:coding", cache); + + expect(first).toBe(second); + expect(first).not.toBe(BUILTIN_CODING_WORKFLOW_IR); + expect(first.nodes.find((node) => node.id === "planning")?.config?.prompt).toBe("Project-specific plan"); + expect(cache.get("builtin:coding\u0000proj-override")).toBe(first); + expect(store.getWorkflowPromptOverrides).toHaveBeenCalledTimes(1); + }); + it("parses a raw-string IR from the definition", async () => { const raw = JSON.stringify(CUSTOM_IR); const store = makeStore({ defs: { "wf-raw": { ir: raw } } }); diff --git a/packages/core/src/workflow-ir-resolver.ts b/packages/core/src/workflow-ir-resolver.ts index 2d6bc93925..fab311b988 100644 --- a/packages/core/src/workflow-ir-resolver.ts +++ b/packages/core/src/workflow-ir-resolver.ts @@ -87,7 +87,17 @@ export async function resolveWorkflowIrById( workflowId: string, irCache?: Map, ): Promise { - const projectId = store.getWorkflowSettingsProjectId?.(); + let projectId: string | undefined; + try { + projectId = store.getWorkflowSettingsProjectId?.(); + } catch { + /* + * FNXC:CustomWorkflows 2026-06-22-23:27: + * Workflow IR resolution is an engine-entry fallback path, so project identity failures must behave like no scoped project is available. + * Keep built-in/default IRs usable and skip project-scoped prompt overrides instead of propagating identity lookup errors. + */ + projectId = undefined; + } const cacheKey = projectId ? `${workflowId}\u0000${projectId}` : workflowId; const cached = irCache?.get(cacheKey); if (cached) return cached;