Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions engine/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,47 @@ Deno.test("D27 settle: consolidate PLUCKS the vendor meter before fromResponse;
assertEquals(output.sawCost, false);
});

Deno.test("consolidate output: an explicit null IS the payload (issue #104); only absence keeps the raw", async () => {
// the hook contract says absent = unchanged; null is valid Json and a
// consolidator may return it deliberately (everything it touched was
// billing). The settle path used ?? so null clobbered back to raw and
// re-exposed exactly the fields the hook meant to strip.
const connectors = demoConnector();
connectors[0].endpoints[0].def = defineEndpoint({
meta: {
displayName: "Search",
summary: "Searches.",
categories: ["demo-search"],
},
request: { method: "POST", path: "/search" },
input: { schema: { body: z.object({ q: z.string().min(1) }) } },
usage: {
model: {
kind: "PER_UNIT",
unit: "RESULT",
consumes: { credit: "default", amount: 0.5 },
},
estimate: () => ({ counts: {} }), // metered docs must estimate (D24)
evidence: ({ data, utils }) => ({
counts: {
"RESULT": utils.json.len(data.output, "$.results"),
},
}),
consolidate: () => ({ credits: {}, output: null }),
},
});
const bundle = await compileBundle(connectors, COMPILE_OPTS);
const engine = new Engine({
transport: jsonTransport(200, {
results: [{ id: "a" }],
vendorTotal: 0,
}),
});
const loaded = await engine.load(sealUnit(bundle, "demo#search"));
const result = await loaded.run({ body: { q: "x" } });
assertEquals(result.output, null);
});

Deno.test("no consolidate fn: raw payload passes through; the derived fold settles", async () => {
// demoConnector links no vendor-meter fn — evidence counts, the
// engine folds, and the raw body rides out untouched
Expand Down
7 changes: 6 additions & 1 deletion engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,12 @@ export class LoadedEndpoint implements RunnableEndpoint {
});
}
}
output = consolidated.output ?? raw;
// absent (undefined) = unchanged; an explicit null is a
// legit Json value the consolidator chose — only absence
// falls back to the raw payload (D27)
output = consolidated.output === undefined
? raw
: consolidated.output;
} else {
output = raw;
}
Expand Down