diff --git a/.changeset/tough-cups-smoke.md b/.changeset/tough-cups-smoke.md new file mode 100644 index 00000000..3349eb9f --- /dev/null +++ b/.changeset/tough-cups-smoke.md @@ -0,0 +1,6 @@ +--- +"@stackables/bridge": patch +"@stackables/bridge-parser": patch +--- + +Fix chained `||` literal fallback parsing so authored left-to-right short-circuiting is preserved after safe pulls (`?.`), and add regression coverage for mixed `||` + `??` chains. diff --git a/packages/bridge-parser/src/parser/parser.ts b/packages/bridge-parser/src/parser/parser.ts index 837999dd..e6b2d59b 100644 --- a/packages/bridge-parser/src/parser/parser.ts +++ b/packages/bridge-parser/src/parser/parser.ts @@ -5237,13 +5237,18 @@ function buildBridgeBody( let falsyFallback: string | undefined; let falsyControl: ControlFlowInstruction | undefined; + let hasTruthyLiteralFallback = false; for (const alt of subs(wireNode, "nullAlt")) { + if (hasTruthyLiteralFallback) break; const altResult = extractCoalesceAlt(alt, lineNum); if ("literal" in altResult) { falsyFallback = altResult.literal; + hasTruthyLiteralFallback = Boolean(JSON.parse(altResult.literal)); } else if ("control" in altResult) { + falsyFallback = undefined; falsyControl = altResult.control; } else { + falsyFallback = undefined; sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false }); } } diff --git a/packages/bridge/test/coalesce-cost.test.ts b/packages/bridge/test/coalesce-cost.test.ts index c1fa86cc..6dfb8741 100644 --- a/packages/bridge/test/coalesce-cost.test.ts +++ b/packages/bridge/test/coalesce-cost.test.ts @@ -649,6 +649,50 @@ bridge Query.lookup { assert.equal(data.label, "fallback"); }); + test("?. with chained || literals short-circuits at first truthy literal", async () => { + const doc = parseBridge(`version 1.5 +const lorem = { + "ipsum":"dolor sit amet", + "consetetur":8.9 +} + +bridge Query.lookup { + with const + with output as o + + o.label <- const.lorem.ipsums?.kala || "A" || "B" +}`); + const gateway = createGateway(typeDefs, doc, { tools: {} }); + const executor = buildHTTPExecutor({ fetch: gateway.fetch as any }); + + const result: any = await executor({ + document: parse(`{ lookup(q: "x") { label } }`), + }); + assert.equal(result.data.lookup.label, "A"); + }); + + test("mixed || and ?? remains left-to-right with first truthy || winner", async () => { + const doc = parseBridge(`version 1.5 +const lorem = { + "ipsum": "dolor sit amet", + "consetetur": 8.9 +} + +bridge Query.lookup { + with const + with output as o + + o.label <- const.lorem.kala || const.lorem.ipsums?.mees || "B" ?? "C" +}`); + const gateway = createGateway(typeDefs, doc, { tools: {} }); + const executor = buildHTTPExecutor({ fetch: gateway.fetch as any }); + + const result: any = await executor({ + document: parse(`{ lookup(q: "x") { label } }`), + }); + assert.equal(result.data.lookup.label, "B"); + }); + test("?. passes through value when tool succeeds", async () => { const { data } = await run( `version 1.5