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
This commit is contained in:
gsxdsm
2026-06-22 23:56:34 -07:00
parent a6685b7f03
commit 849b40d220
3 changed files with 76 additions and 3 deletions

View File

@@ -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.

View File

@@ -23,13 +23,28 @@ function makeStore(opts: {
selection?: { workflowId: string; stepIds: string[] };
selectionThrows?: boolean;
defs?: Record<string, { ir: string | WorkflowIr } | undefined>;
}) {
projectId?: string;
projectIdThrows?: boolean;
promptOverrides?: Record<string, string>;
} = {}) {
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<string, WorkflowIr>([["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<string, WorkflowIr>();
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 } } });

View File

@@ -87,7 +87,17 @@ export async function resolveWorkflowIrById(
workflowId: string,
irCache?: Map<string, WorkflowIr>,
): Promise<WorkflowIr> {
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;