package gateway
import (
"encoding/json"
"net/http/httptest"
"regexp"
"strconv"
"strings"
"testing"
"github.com/yetone/magpie/internal/provider"
)
// codexItem is a Responses input message, as Codex sends one.
func codexItem(role, text string) string {
kind := "input_text"
if role == "assistant" {
kind = "output_text"
}
return `{"type":"message","role":"` + role + `","content":[{"type":"` + kind + `","text":` + strconv.Quote(text) + `}]}`
}
// A developer message Codex adds once its conversation has gone on (its
// context changed: world state, settings) stays where it is: the system
// prompt, and so Anthropic's cached prefix, is the one the turns before
// had (#502: the turn after it wrote the whole conversation to the cache).
func TestCodexLaterDeveloperMessageKeepsTheSystem(t *testing.T) {
head := codexItem("developer", "sandboxed") + `,` +
codexItem("user", "cwd") + `,` + codexItem("user", "fix the bug")
turn1 := `{"model":"claude-opus-5-5","instructions":"You are Codex.","input":[` + head + `]}`
turn2 := `{"model":"claude-opus-5-5","instructions":"You are Codex.","input":[` + head + `,` +
codexItem("assistant", "done") + `,` + codexItem("developer", "a new skill") + `,` +
codexItem("user", "thanks, and the test?") + `]}`
r1, err := parseResponses([]byte(turn1))
if err != nil {
t.Fatal(err)
}
r2, err := parseResponses([]byte(turn2))
if err != nil {
t.Fatal(err)
}
if r1.System != r2.System || !strings.Contains(r1.System, "permissions instructions") {
t.Fatalf("system changed:\n%q\n%q", r1.System, r2.System)
}
var q1, q2 struct {
System json.RawMessage
Messages []json.RawMessage
}
json.Unmarshal(buildAnthropic(r1, "claude-opus-5-5"), &q1)
json.Unmarshal(buildAnthropic(r2, "claude-opus-5-5"), &q2)
if string(q1.System) != string(q2.System) {
t.Fatalf("Anthropic's system changed:\n%s\n%s", q1.System, q2.System)
}
last := r2.Messages[len(r2.Messages)-1]
if len(r2.Messages) != 3 || last.Role != "user" || !strings.Contains(text(last.Parts), "\n") ||
!strings.HasSuffix(text(last.Parts), "thanks, and the test?") {
t.Fatalf("messages: %+v", r2.Messages)
}
// the user's words are what the router's classifier is asked about
if got := userText(r2); got != "thanks, and the test?" {
t.Fatalf("user text %q", got)
}
if turn, within := turnIn(r2); turn != 2 || within {
t.Fatalf("turn %d within %v", turn, within)
}
}
// A Codex thread on a Claude subscription goes on in the Claude Code that
// had its turn before, when a memory request of Codex's (another model,
// its own instructions, the same session) came in between and Codex then
// added a developer message to the thread.
func TestCodexThreadKeepsItsClaudeRunAcrossAMemoryCall(t *testing.T) {
fakeClaude(t)
s := New()
t.Cleanup(s.subscription.abortAll)
p := provider.Provider{ID: "claude", Account: &provider.Account{Agent: "claude", User: "u"}}
said := regexp.MustCompile(`pid \d+ turn \d+`)
ask := func(body string) string {
t.Helper()
req := httptest.NewRequest("POST", "/v1/responses", strings.NewReader(body))
req.Header.Set("session_id", "019a-codex-thread")
rec := httptest.NewRecorder()
var u Usage
if code, msg := s.serveClaudeSubscription(rec, req, provider.Responses, p, "claude-sonnet-5", []byte(body), &u); code != 200 {
t.Fatalf("%d %s", code, msg)
}
got := said.FindString(rec.Body.String())
if got == "" {
t.Fatalf("no answer: %s", rec.Body)
}
return got
}
tools := `"tools":[{"type":"function","name":"shell","parameters":{"type":"object"}}]`
head := codexItem("developer", "sandboxed") + `,` + codexItem("user", "fix the bug")
first := ask(`{"model":"claude-sonnet-5","instructions":"You are Codex.",` + tools + `,"input":[` + head + `]}`)
pid, _, _ := strings.Cut(strings.TrimPrefix(first, "pid "), " ")
// Codex's memory extraction: the same session, a prompt of its own
if mem := ask(`{"model":"claude-sonnet-5","instructions":"Extract memories.","input":[` + codexItem("user", "rollout: …") + `]}`); strings.Contains(mem, "pid "+pid+" ") {
t.Fatalf("the memory call took the thread's run: %q", mem)
}
second := ask(`{"model":"claude-sonnet-5","instructions":"You are Codex.",` + tools + `,"input":[` + head + `,` +
codexItem("assistant", first) + `,` + codexItem("developer", "memories updated") + `,` +
codexItem("user", "and the test?") + `]}`)
if second != "pid "+pid+" turn 2" {
t.Fatalf("the thread's next turn: %q, first %q", second, first)
}
}