Skip to content

Commit ddedba7

Browse files
committed
Add failing coverage for cross-provider reasoning-signature replay
A signature tagged as belonging to a different provider than the current request must never be replayed, even when the historical turn's bare model string happens to match; a second account on the same provider must still replay. The adapter has no way to express provider-tagged signatures yet, so these cases fail to even compile.
1 parent 6af12df commit ddedba7

1 file changed

Lines changed: 55 additions & 3 deletions

File tree

tests/unit/codex-responses-adapter.test.ts

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { test, expect, describe } from "bun:test";
22
import {
33
createCodexResponsesAdapter,
4+
tagSignature,
45
CODEX_ACCOUNT_ID_OPTION,
56
CODEX_SESSION_ID_OPTION,
7+
CODEX_RESPONSES_PROVIDER,
68
} from "../../src/provider/codex-responses-adapter.js";
9+
import { GROK_RESPONSES_PROVIDER } from "../../src/provider/grok-responses-adapter.js";
710
import { BEARER_CREDENTIAL_SENTINEL } from "@intx/inference";
811
import type { ConversationTurn, InferenceOptions, LastCycleSource } from "@intx/types/runtime";
912

@@ -113,7 +116,7 @@ describe("codex-responses buildRequest", () => {
113116
model: "gpt-5-codex",
114117
timestamp: 0,
115118
content: [
116-
{ type: "thinking", thinking: "internal steps...", signature: "ENC_BLOB_123" },
119+
{ type: "thinking", thinking: "internal steps...", signature: tagSignature(CODEX_RESPONSES_PROVIDER, "ENC_BLOB_123") },
117120
{ type: "text", text: "The answer is 42." },
118121
],
119122
},
@@ -126,6 +129,28 @@ describe("codex-responses buildRequest", () => {
126129
]);
127130
});
128131

132+
test("a second account on the same provider still replays the signature", () => {
133+
// codex/personal and codex/work are two ChatGPT accounts routed through the
134+
// same Codex backend (same provider, different InferenceSource.id). The
135+
// backend can decrypt a signature issued to either account, so a live
136+
// account switch must not poison reasoning continuity.
137+
const turns: ConversationTurn[] = [
138+
userTurn("solve the hard problem"),
139+
{
140+
role: "assistant",
141+
model: "gpt-5-codex",
142+
timestamp: 0,
143+
content: [
144+
{ type: "thinking", thinking: "internal steps...", signature: tagSignature(CODEX_RESPONSES_PROVIDER, "ENC_BLOB_123") },
145+
{ type: "text", text: "The answer is 42." },
146+
],
147+
},
148+
];
149+
const workAdapter = createCodexResponsesAdapter({ sourceId: "codex/work", provider: "codex-responses", model: "gpt-5-codex" });
150+
const body = JSON.parse(workAdapter.buildRequest(turns, "gpt-5-codex", baseOptions).body) as Record<string, unknown>;
151+
expect(body["input"]).toContainEqual({ type: "reasoning", summary: [], encrypted_content: "ENC_BLOB_123" });
152+
});
153+
129154
test("drops a reasoning signature issued for a different model after a provider switch", () => {
130155
// The signature was minted by grok-4.5; the request now targets a Codex
131156
// model. Replaying it would 400 with an undecryptable-content error, so
@@ -137,7 +162,31 @@ describe("codex-responses buildRequest", () => {
137162
model: "grok-4.5",
138163
timestamp: 0,
139164
content: [
140-
{ type: "thinking", thinking: "internal steps...", signature: "FOREIGN_BLOB" },
165+
{ type: "thinking", thinking: "internal steps...", signature: tagSignature(GROK_RESPONSES_PROVIDER, "FOREIGN_BLOB") },
166+
{ type: "text", text: "The answer is 42." },
167+
],
168+
},
169+
];
170+
const body = JSON.parse(adapter().buildRequest(turns, "gpt-5-codex", baseOptions).body) as Record<string, unknown>;
171+
expect(body["input"]).toEqual([
172+
{ type: "message", role: "user", content: [{ type: "input_text", text: "solve the hard problem" }] },
173+
{ type: "message", role: "assistant", content: [{ type: "output_text", text: "The answer is 42." }] },
174+
]);
175+
});
176+
177+
test("drops a reasoning signature issued by a different provider even when the model string matches", () => {
178+
// Two distinct backends (e.g. proxy aliases) can declare the identical
179+
// literal model name. Nothing but the tagged provider on the signature
180+
// itself distinguishes them, since InferenceSource.model is arbitrary
181+
// catalog text and turn.model alone cannot tell them apart.
182+
const turns: ConversationTurn[] = [
183+
userTurn("solve the hard problem"),
184+
{
185+
role: "assistant",
186+
model: "gpt-5-codex",
187+
timestamp: 0,
188+
content: [
189+
{ type: "thinking", thinking: "internal steps...", signature: tagSignature(GROK_RESPONSES_PROVIDER, "FOREIGN_BLOB") },
141190
{ type: "text", text: "The answer is 42." },
142191
],
143192
},
@@ -156,7 +205,10 @@ describe("codex-responses buildRequest", () => {
156205
role: "assistant",
157206
model: "grok-4.5",
158207
timestamp: 0,
159-
content: [{ type: "thinking", thinking: "...", signature: "FOREIGN_BLOB" }, { type: "text", text: "ok" }],
208+
content: [
209+
{ type: "thinking", thinking: "...", signature: tagSignature(GROK_RESPONSES_PROVIDER, "FOREIGN_BLOB") },
210+
{ type: "text", text: "ok" },
211+
],
160212
},
161213
userTurn("turn 2"),
162214
];

0 commit comments

Comments
 (0)