openai_codex_message_item_id_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. //go:build unit
  2. package service
  3. import (
  4. "testing"
  5. "github.com/stretchr/testify/require"
  6. )
  7. // TestFilterCodexInput_StripsMessageItemID_WhenPreservingReferences
  8. // verifies that message items with a non-msg id (e.g. item_*) have their id
  9. // stripped even when PreserveReferences is true. OpenAI upstream requires
  10. // message ids to begin with "msg" and rejects item_* with 400:
  11. // "Expected an ID that begins with 'msg'." (#3981)
  12. func TestFilterCodexInput_StripsMessageItemID_WhenPreservingReferences(t *testing.T) {
  13. input := []any{
  14. map[string]any{
  15. "type": "message",
  16. "id": "item_3bc5a3fa8ccde25f1c0000d4",
  17. "role": "user",
  18. "content": []any{
  19. map[string]any{"type": "input_text", "text": "hello"},
  20. },
  21. },
  22. }
  23. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  24. PreserveReferences: true,
  25. })
  26. require.Len(t, filtered, 1)
  27. msg, ok := filtered[0].(map[string]any)
  28. require.True(t, ok)
  29. require.Equal(t, "message", msg["type"])
  30. _, hasID := msg["id"]
  31. require.False(t, hasID, "item_* id should be stripped from message")
  32. require.Equal(t, "user", msg["role"], "role must be preserved")
  33. require.NotNil(t, msg["content"], "content must be preserved")
  34. }
  35. // TestFilterCodexInput_KeepsMsgID_WhenPreservingReferences
  36. // verifies that message items with a valid msg* id are kept when
  37. // PreserveReferences is true, so context references are not lost.
  38. func TestFilterCodexInput_KeepsMsgID_WhenPreservingReferences(t *testing.T) {
  39. input := []any{
  40. map[string]any{
  41. "type": "message",
  42. "id": "msg_validID123",
  43. "role": "assistant",
  44. },
  45. }
  46. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  47. PreserveReferences: true,
  48. })
  49. require.Len(t, filtered, 1)
  50. msg, ok := filtered[0].(map[string]any)
  51. require.True(t, ok)
  52. require.Equal(t, "msg_validID123", msg["id"], "valid msg* id must be preserved")
  53. }
  54. // TestFilterCodexInput_StripsMessageIDWhenNotPreservingReferences ensures the
  55. // non-continuation path still drops every message id regardless of prefix.
  56. func TestFilterCodexInput_StripsMessageIDWhenNotPreservingReferences(t *testing.T) {
  57. for _, id := range []string{"item_abc", "msg_validID123"} {
  58. input := []any{
  59. map[string]any{
  60. "type": "message",
  61. "id": id,
  62. "role": "user",
  63. },
  64. }
  65. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  66. PreserveReferences: false,
  67. })
  68. require.Len(t, filtered, 1)
  69. msg, ok := filtered[0].(map[string]any)
  70. require.True(t, ok)
  71. _, hasID := msg["id"]
  72. require.False(t, hasID, "id %q should be stripped when not preserving references", id)
  73. }
  74. }
  75. // TestFilterCodexInput_MessageIDStripDoesNotMutateInput ensures the original
  76. // input map is not modified in place when the id is stripped.
  77. func TestFilterCodexInput_MessageIDStripDoesNotMutateInput(t *testing.T) {
  78. original := map[string]any{
  79. "type": "message",
  80. "id": "item_abc",
  81. "role": "user",
  82. }
  83. filtered := filterCodexInputWithOptions([]any{original}, codexInputFilterOptions{
  84. PreserveReferences: true,
  85. })
  86. require.Len(t, filtered, 1)
  87. require.Equal(t, "item_abc", original["id"], "original input must not be mutated")
  88. }
  89. // TestFilterCodexInput_MessageStripKeepsFunctionCallBehavior guards against a
  90. // regression of #3785: message and function_call id rules are independent.
  91. func TestFilterCodexInput_MessageStripKeepsFunctionCallBehavior(t *testing.T) {
  92. input := []any{
  93. map[string]any{
  94. "type": "message",
  95. "id": "item_msg_001",
  96. "role": "user",
  97. },
  98. map[string]any{
  99. "type": "function_call",
  100. "id": "fc_validID123",
  101. "call_id": "fc_validID123",
  102. "name": "bash",
  103. },
  104. map[string]any{
  105. "type": "function_call",
  106. "id": "item_A9v0SNfS3VaLrfX0j3y4xhyK",
  107. "call_id": "fc_abc123",
  108. "name": "bash",
  109. },
  110. map[string]any{
  111. "type": "function_call_output",
  112. "id": "o1",
  113. "call_id": "fc_abc123",
  114. "output": "done",
  115. },
  116. }
  117. filtered := filterCodexInputWithOptions(input, codexInputFilterOptions{
  118. PreserveReferences: true,
  119. })
  120. require.Len(t, filtered, 4)
  121. msg, ok := filtered[0].(map[string]any)
  122. require.True(t, ok)
  123. _, hasID := msg["id"]
  124. require.False(t, hasID, "message item_* id should be stripped")
  125. fcValid, ok := filtered[1].(map[string]any)
  126. require.True(t, ok)
  127. require.Equal(t, "fc_validID123", fcValid["id"], "valid fc* id must be preserved")
  128. fcBad, ok := filtered[2].(map[string]any)
  129. require.True(t, ok)
  130. _, hasID = fcBad["id"]
  131. require.False(t, hasID, "function_call item_* id should still be stripped")
  132. require.Equal(t, "fc_abc123", fcBad["call_id"], "call_id pairing must survive")
  133. out, ok := filtered[3].(map[string]any)
  134. require.True(t, ok)
  135. require.Equal(t, "o1", out["id"], "output item id should be preserved")
  136. require.Equal(t, "fc_abc123", out["call_id"], "call_id pairing must survive")
  137. }