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:
5
.changeset/fn-6947-workflow-ir-project-identity.md
Normal file
5
.changeset/fn-6947-workflow-ir-project-identity.md
Normal 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.
|
||||
@@ -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 } } });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user