openai_mode1_integrity_test.go 6.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. package service
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. func TestMode1CodexSemanticEquivalence(t *testing.T) {
  8. account := mode1TestAccount(1)
  9. cases := []struct{ name, before, after string }{
  10. {"string input", `{"input":"hello"}`, `{"input":[{"type":"message","role":"user","content":"hello"}]}`},
  11. {"text parts", `{"input":[{"role":"user","content":"hello"}]}`, `{"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"hello"}]}]}`},
  12. {"reasoning replay metadata", `{"input":[{"type":"reasoning","id":"rs_old","encrypted_content":"opaque"}]}`, `{"input":[{"type":"reasoning","summary":[],"encrypted_content":"opaque"}]}`},
  13. {"system promotion", `{"instructions":"original","input":[{"role":"system","content":"policy"},{"role":"user","content":"hello"}]}`, `{"instructions":"policy\n\noriginal","input":[{"role":"user","content":"hello"}]}`},
  14. {"tool schema", `{"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}]}`, `{"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`},
  15. {"tool role result", `{"input":[{"role":"tool","tool_call_id":"call_123","content":"answer"}]}`, `{"input":[{"type":"function_call_output","call_id":"fc_123","output":"answer"}]}`},
  16. {"call pair", `{"input":[{"type":"function_call","call_id":"call_123","name":"lookup","arguments":"{}"},{"type":"function_call_output","call_id":"call_123","output":"answer"}]}`, `{"input":[{"type":"function_call","call_id":"fc_123","name":"lookup","arguments":"{}"},{"type":"function_call_output","call_id":"fc_123","output":"answer"}]}`},
  17. {"previous turn reference", `{"input":[{"type":"item_reference","id":"call_123"}]}`, `{"input":[{"type":"item_reference","id":"fc_123"}]}`},
  18. {"model alias", `{"model":"gpt-5.4-high"}`, `{"model":"gpt-5.4"}`},
  19. {"minimal compatibility", `{"reasoning":{"effort":"minimal","summary":"auto"}}`, `{"reasoning":{"effort":"none","summary":"auto"}}`},
  20. {"legacy pro mode", `{"model":"gpt-5.4","reasoning":{"mode":"pro"}}`, `{"model":"gpt-5.4","reasoning":{"effort":"max"}}`},
  21. {"unsupported OAuth budget", `{"max_output_tokens":1234}`, `{}`},
  22. }
  23. for _, tc := range cases {
  24. t.Run(tc.name, func(t *testing.T) {
  25. require.NoError(t, validateMode1RequestIntegrityForAccount(account, []byte(tc.before), []byte(tc.after)))
  26. })
  27. }
  28. }
  29. func TestMode1SemanticGuardStillRejectsActualLoss(t *testing.T) {
  30. cases := []struct{ name, before, after, field string }{
  31. {"changed text", `{"input":"hello"}`, `{"input":[{"role":"user","content":"goodbye"}]}`, "input"},
  32. {"dropped turn", `{"input":[{"role":"user","content":"one"},{"role":"assistant","content":"two"}]}`, `{"input":[{"role":"user","content":"one"}]}`, "input"},
  33. {"lost system", `{"input":[{"role":"system","content":"policy"},{"role":"user","content":"hello"}]}`, `{"input":[{"role":"user","content":"hello"}]}`, "instructions"},
  34. {"lost encrypted reasoning", `{"input":[{"type":"reasoning","id":"rs_old","encrypted_content":"opaque"}]}`, `{"input":[{"type":"reasoning","summary":[]}]}`, "input"},
  35. {"lost reference", `{"input":[{"type":"item_reference","id":"msg_old"}]}`, `{"input":[]}`, "input"},
  36. {"changed reference", `{"input":[{"type":"item_reference","id":"call_123"}]}`, `{"input":[{"type":"item_reference","id":"fc_456"}]}`, "input"},
  37. {"wrong call id", `{"input":[{"type":"function_call_output","call_id":"call_123","output":"answer"}]}`, `{"input":[{"type":"function_call_output","call_id":"fc_456","output":"answer"}]}`, "input"},
  38. {"changed result", `{"input":[{"type":"function_call_output","call_id":"call_123","output":"answer"}]}`, `{"input":[{"type":"function_call_output","call_id":"fc_123","output":"different"}]}`, "input"},
  39. {"lost tools", `{"tools":[{"type":"function","name":"lookup"}]}`, `{"tools":[]}`, "tools"},
  40. {"conflicting legacy tools", `{"tools":[{"type":"function","name":"important"}],"functions":[{"name":"legacy"}]}`, `{"tools":[{"type":"function","name":"legacy"}]}`, "tools"},
  41. {"tool without association", `{"input":[{"role":"tool","content":"answer"}]}`, `{"input":[{"role":"user","content":"answer"}]}`, "input"},
  42. {"lost malformed tool", `{"tools":[{"type":"function"}]}`, `{"tools":[]}`, "tools"},
  43. {"lowered reasoning", `{"reasoning":{"effort":"high"}}`, `{"reasoning":{"effort":"low"}}`, "reasoning"},
  44. {"lost Astra mode", `{"model":"gpt-6-astra","reasoning":{"mode":"pro"}}`, `{"model":"gpt-6-astra","reasoning":{"effort":"max"}}`, "reasoning"},
  45. {"changed model", `{"model":"gpt-5.4"}`, `{"model":"gpt-5.4-mini"}`, "model"},
  46. {"changed budget", `{"max_output_tokens":2000}`, `{"max_output_tokens":10}`, "max_output_tokens"},
  47. {"lost continuity", `{"previous_response_id":"resp_123"}`, `{}`, "previous_response_id"},
  48. }
  49. for _, tc := range cases {
  50. t.Run(tc.name, func(t *testing.T) {
  51. require.ErrorContains(t, validateMode1RequestIntegrityForAccount(mode1TestAccount(1), []byte(tc.before), []byte(tc.after)), tc.field)
  52. })
  53. }
  54. apiKey := &Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}
  55. require.ErrorContains(t, validateMode1RequestIntegrityForAccount(apiKey, []byte(`{"max_output_tokens":2000}`), []byte(`{}`)), "max_output_tokens")
  56. }
  57. func TestMode1GuardAcceptsActualCodexTransform(t *testing.T) {
  58. for _, raw := range []string{
  59. `{"model":"gpt-5.4","input":"Return OK","max_output_tokens":100}`,
  60. `{"model":"gpt-5.4","instructions":"Be accurate","input":[{"role":"system","content":"Use the context"},{"role":"user","content":"one"},{"role":"assistant","content":"two"},{"type":"reasoning","id":"rs_old","encrypted_content":"opaque"},{"role":"user","content":"continue"}],"reasoning":{"effort":"high"}}`,
  61. `{"model":"gpt-5.4","input":[{"type":"function_call","call_id":"call_123","name":"lookup","arguments":"{}"},{"type":"function_call_output","call_id":"call_123","output":"answer"}],"tools":[{"type":"function","function":{"name":"lookup","parameters":{"type":"object"}}}],"tool_choice":{"type":"function","function":{"name":"lookup"}}}`,
  62. } {
  63. var body map[string]any
  64. require.NoError(t, json.Unmarshal([]byte(raw), &body))
  65. result := applyCodexOAuthTransformWithOptions(body, codexOAuthTransformOptions{OmitPromotedSystemMessagesFromInput: true})
  66. require.NoError(t, result.Error)
  67. forwarded, err := json.Marshal(body)
  68. require.NoError(t, err)
  69. require.NoError(t, validateMode1RequestIntegrityForAccount(mode1TestAccount(1), []byte(raw), forwarded), string(forwarded))
  70. }
  71. }
  72. func TestMode1GuardRejectsInvalidJSON(t *testing.T) {
  73. for _, raw := range []string{"null", "[]", "{} {}", "{", "{} trailing"} {
  74. require.Error(t, validateMode1RequestIntegrity([]byte(raw), []byte(`{}`)), raw)
  75. require.Error(t, validateMode1RequestIntegrity([]byte(`{}`), []byte(raw)), raw)
  76. }
  77. }