Files
OmniRoute/tests/unit/idempotency-fusion-collision.test.ts
Paco Cartones 30026b2e96 fix(idempotency): fingerprint Responses request semantics (#11532)
Validated in a combined 10-PR batch worktree off release/v3.8.51 tip.
- Focused test: tests/unit/idempotency-fusion-collision.test.ts — 8/8 pass
- typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity gates — all OK
- Full-repo lint: 503 pre-existing problems confirmed identical on the pure release/v3.8.51 tip — unrelated to this diff

⚠️ base-red inherited: #11449

Thanks for closing the idempotency-collision gap between unrelated Responses requests.
2026-08-25 13:12:18 -03:00

148 lines
5.1 KiB
TypeScript

/**
* Regression: fusion judge must not replay a panel member's cached response.
*
* The idempotency layer keys on the client's `Idempotency-Key` / `x-request-id`
* header with a 5s replay window. Fusion's internal panel + judge sub-requests
* re-enter chatCore SHARING the client's headers, so they all derived the SAME
* key: a panel answer saved under the key, and ~1ms later the judge's check hit
* it — the client received a panel member's answer (labeled with the judge's
* meta headers) instead of the judge synthesis. Observed live on
* "nexa/conversation-fusion" (body = Gemini panel answer verbatim,
* X-OmniRoute-Idempotent: true, judge "latency" ~0ms).
*
* Fix: namespace the composed key by target provider/model AND a digest of the
* request messages. Panel members differ by model; the judge differs by model
* AND by messages (it appends the judge directive turn), so sub-requests can
* never collide — while a genuine client retry (same key, same model, same
* body) still replays.
*/
import test from "node:test";
import assert from "node:assert/strict";
import { composeIdempotencyKey } from "../../open-sse/handlers/chatCore/idempotency.ts";
const MSGS = [{ role: "user", content: "Client asks about our PST coverage" }];
const JUDGE_MSGS = [...MSGS, { role: "user", content: "You are the judge. Synthesize: ..." }];
test("no raw header -> null (idempotency disabled for the request)", () => {
assert.equal(
composeIdempotencyKey({
rawKey: null,
provider: "cc",
model: "claude-opus-4-8",
messages: MSGS,
}),
null
);
});
test("panel members (same raw key, same body, different models) get DIFFERENT keys", () => {
const base = { rawKey: "req-1", messages: MSGS };
const opus = composeIdempotencyKey({ ...base, provider: "cc", model: "claude-opus-4-6" });
const gemini = composeIdempotencyKey({
...base,
provider: "antigravity",
model: "gemini-3.1-pro-high",
});
const gpt = composeIdempotencyKey({ ...base, provider: "cx", model: "gpt-5.5-high" });
assert.ok(opus && gemini && gpt);
assert.notEqual(opus, gemini);
assert.notEqual(gemini, gpt);
assert.notEqual(opus, gpt);
});
test("judge (same raw key, different model AND extra judge turn) never collides with a panel member", () => {
const panel = composeIdempotencyKey({
rawKey: "req-1",
provider: "antigravity",
model: "gemini-3.1-pro-high",
messages: MSGS,
});
const judge = composeIdempotencyKey({
rawKey: "req-1",
provider: "cc",
model: "claude-opus-4-8",
messages: JUDGE_MSGS,
});
assert.notEqual(judge, panel);
});
test("judge that reuses a panel member's model still differs (messages digest separates them)", () => {
const panel = composeIdempotencyKey({
rawKey: "req-1",
provider: "cc",
model: "claude-opus-4-8",
messages: MSGS,
});
const judge = composeIdempotencyKey({
rawKey: "req-1",
provider: "cc",
model: "claude-opus-4-8",
messages: JUDGE_MSGS,
});
assert.notEqual(judge, panel);
});
test("genuine client retry (same key + model + body) -> SAME key (replay semantics preserved)", () => {
const a = composeIdempotencyKey({
rawKey: "retry-9",
provider: "cc",
model: "claude-opus-4-8",
messages: MSGS,
});
const b = composeIdempotencyKey({
rawKey: "retry-9",
provider: "cc",
model: "claude-opus-4-8",
messages: MSGS,
});
assert.equal(a, b);
});
test("Responses requests with different input get different keys", () => {
const base = { rawKey: "responses-1", provider: "openai", model: "gpt-5", messages: undefined };
const first = composeIdempotencyKey({ ...base, body: { input: "first prompt" } });
const second = composeIdempotencyKey({ ...base, body: { input: "second prompt" } });
assert.notEqual(first, second);
});
test("semantic body serialization is stable and ignores credentials and request noise", () => {
const base = { rawKey: "responses-2", provider: "openai", model: "gpt-5", messages: undefined };
const first = composeIdempotencyKey({
...base,
body: {
input: [{ role: "user", content: "hello" }],
tools: [{ type: "function", name: "lookup", parameters: { type: "object" } }],
metadata: { trace: "one" },
api_key: "secret-one",
},
});
const second = composeIdempotencyKey({
...base,
body: {
tools: [{ parameters: { type: "object" }, name: "lookup", type: "function" }],
input: [{ content: "hello", role: "user" }],
metadata: { trace: "two" },
api_key: "secret-two",
},
});
assert.equal(first, second);
});
test("Chat and Responses generation limits participate in the fingerprint", () => {
const base = {
rawKey: "responses-3",
provider: "openai",
model: "gpt-5",
messages: undefined,
body: { input: "hello", temperature: 0 },
};
assert.notEqual(
composeIdempotencyKey(base),
composeIdempotencyKey({ ...base, body: { ...base.body, temperature: 1 } })
);
assert.notEqual(
composeIdempotencyKey({ ...base, body: { input: "hello", max_output_tokens: 100 } }),
composeIdempotencyKey({ ...base, body: { input: "hello", max_output_tokens: 200 } })
);
});