dateline_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. package anthropicfp
  2. import (
  3. "bytes"
  4. "strings"
  5. "testing"
  6. )
  7. func TestNormalizeText_ASCIIHyphenIsIdentity(t *testing.T) {
  8. in := "Today's date is 2026-07-01."
  9. out, hits := NormalizeText(in)
  10. if out != in {
  11. t.Fatalf("canonical form should be returned identity, got %q", out)
  12. }
  13. if len(hits) != 0 {
  14. t.Fatalf("no hits expected on canonical input, got %d", len(hits))
  15. }
  16. }
  17. func TestNormalizeText_SlashSeparatorASCIIApostrophe(t *testing.T) {
  18. in := "Today's date is 2026/07/01."
  19. out, hits := NormalizeText(in)
  20. want := "Today's date is 2026-07-01."
  21. if out != want {
  22. t.Fatalf("want %q, got %q", want, out)
  23. }
  24. if len(hits) != 1 || hits[0].ApostropheVariant != "ascii" || hits[0].DateSeparator != "/" {
  25. t.Fatalf("unexpected hit: %+v", hits)
  26. }
  27. }
  28. func TestNormalizeText_U2019Apostrophe(t *testing.T) {
  29. in := "Today’s date is 2026-07-01."
  30. out, hits := NormalizeText(in)
  31. want := "Today's date is 2026-07-01."
  32. if out != want {
  33. t.Fatalf("want %q, got %q", want, out)
  34. }
  35. if len(hits) != 1 || hits[0].ApostropheVariant != "u2019" || hits[0].DateSeparator != "-" {
  36. t.Fatalf("unexpected hit: %+v", hits)
  37. }
  38. }
  39. func TestNormalizeText_U02BCApostropheWithSlash(t *testing.T) {
  40. in := "Todayʼs date is 2026/07/01."
  41. out, hits := NormalizeText(in)
  42. want := "Today's date is 2026-07-01."
  43. if out != want {
  44. t.Fatalf("want %q, got %q", want, out)
  45. }
  46. if len(hits) != 1 || hits[0].ApostropheVariant != "u02bc" || hits[0].DateSeparator != "/" {
  47. t.Fatalf("unexpected hit: %+v", hits)
  48. }
  49. }
  50. func TestNormalizeText_U02B9Apostrophe(t *testing.T) {
  51. in := "Todayʹs date is 2026/07/01."
  52. out, hits := NormalizeText(in)
  53. want := "Today's date is 2026-07-01."
  54. if out != want {
  55. t.Fatalf("want %q, got %q", want, out)
  56. }
  57. if len(hits) != 1 || hits[0].ApostropheVariant != "u02b9" || hits[0].DateSeparator != "/" {
  58. t.Fatalf("unexpected hit: %+v", hits)
  59. }
  60. }
  61. func TestNormalizeText_MixedSeparatorsNoMatch(t *testing.T) {
  62. // backreference \3 forces the two separators to agree
  63. in := "Today's date is 2026-07/01."
  64. out, hits := NormalizeText(in)
  65. if out != in {
  66. t.Fatalf("mixed-separator input should not be matched, got %q", out)
  67. }
  68. if len(hits) != 0 {
  69. t.Fatalf("expected no hits, got %d", len(hits))
  70. }
  71. }
  72. func TestNormalizeText_NegativeLookalike(t *testing.T) {
  73. cases := []string{
  74. "Today is a great day.",
  75. "His date is 2026-07-01.",
  76. "Yesterday's date was 2026-06-30.",
  77. "'s date is 2026-07-01.",
  78. }
  79. for _, c := range cases {
  80. out, hits := NormalizeText(c)
  81. if out != c {
  82. t.Fatalf("input %q should not be modified, got %q", c, out)
  83. }
  84. if len(hits) != 0 {
  85. t.Fatalf("input %q should produce no hits", c)
  86. }
  87. }
  88. }
  89. func TestNormalizeText_Idempotent(t *testing.T) {
  90. in := "Today’s date is 2026/07/01."
  91. out1, _ := NormalizeText(in)
  92. out2, hits2 := NormalizeText(out1)
  93. if out1 != out2 {
  94. t.Fatalf("normalization not idempotent: %q vs %q", out1, out2)
  95. }
  96. if len(hits2) != 0 {
  97. t.Fatalf("second pass should produce no hits, got %d", len(hits2))
  98. }
  99. }
  100. func TestNormalizeText_MultipleOccurrences(t *testing.T) {
  101. in := "First line.\nToday’s date is 2026/07/01.\nMore text.\nTodayʼs date is 2026-07-01.\nEnd."
  102. out, hits := NormalizeText(in)
  103. if strings.Count(out, "Today's date is") != 2 {
  104. t.Fatalf("expected two canonicalized sentences, got: %q", out)
  105. }
  106. if strings.Contains(out, "’") || strings.Contains(out, "ʼ") || strings.Contains(out, "2026/07/01") {
  107. t.Fatalf("fingerprint characters must be gone, got: %q", out)
  108. }
  109. if len(hits) != 2 {
  110. t.Fatalf("expected 2 hits, got %d", len(hits))
  111. }
  112. }
  113. func TestNormalizeDateline_SystemString(t *testing.T) {
  114. body := []byte(`{"system":"You are helpful.\nToday’s date is 2026/07/01.\nBe brief.","messages":[]}`)
  115. out, hits, changed := NormalizeDateline(body)
  116. if !changed {
  117. t.Fatalf("expected changed=true")
  118. }
  119. if len(hits) != 1 {
  120. t.Fatalf("expected 1 hit, got %d", len(hits))
  121. }
  122. if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
  123. t.Fatalf("output missing canonical dateline: %s", string(out))
  124. }
  125. if bytes.Contains(out, []byte("2026/07/01")) {
  126. t.Fatalf("output should not contain slash date: %s", string(out))
  127. }
  128. }
  129. func TestNormalizeDateline_SystemBlocksArray(t *testing.T) {
  130. body := []byte(`{"system":[{"type":"text","text":"You are helpful."},{"type":"text","text":"Todayʼs date is 2026/07/01."}],"messages":[]}`)
  131. out, hits, changed := NormalizeDateline(body)
  132. if !changed {
  133. t.Fatalf("expected changed=true")
  134. }
  135. if len(hits) != 1 || hits[0].ApostropheVariant != "u02bc" {
  136. t.Fatalf("unexpected hits: %+v", hits)
  137. }
  138. if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
  139. t.Fatalf("output missing canonical dateline: %s", string(out))
  140. }
  141. }
  142. func TestNormalizeDateline_MessagesContentStringInSystemReminder(t *testing.T) {
  143. body := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\n# currentDate\nToday’s date is 2026/07/01.\n</system-reminder>\nHello, please help."}]}`)
  144. out, hits, changed := NormalizeDateline(body)
  145. if !changed {
  146. t.Fatalf("expected changed=true")
  147. }
  148. if len(hits) != 1 {
  149. t.Fatalf("expected 1 hit, got %d", len(hits))
  150. }
  151. if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
  152. t.Fatalf("canonical dateline missing: %s", string(out))
  153. }
  154. }
  155. func TestNormalizeDateline_MessagesContentBlocksInSystemReminder(t *testing.T) {
  156. body := []byte(`{"messages":[{"role":"user","content":[{"type":"text","text":"<system-reminder>\nToday’s date is 2026/07/01.\n</system-reminder>"},{"type":"text","text":"do X"}]}]}`)
  157. out, hits, changed := NormalizeDateline(body)
  158. if !changed {
  159. t.Fatalf("expected changed=true")
  160. }
  161. if len(hits) != 1 {
  162. t.Fatalf("expected 1 hit, got %d", len(hits))
  163. }
  164. if !bytes.Contains(out, []byte("Today's date is 2026-07-01.")) {
  165. t.Fatalf("canonical dateline missing: %s", string(out))
  166. }
  167. }
  168. func TestNormalizeDateline_LeavesOutOfScopeUntouched(t *testing.T) {
  169. // User prose outside <system-reminder> that mentions today's date must
  170. // not be modified. tool_use.input / tool_result.content are never scanned.
  171. body := []byte(`{"messages":[` +
  172. `{"role":"user","content":"Today’s date is 2026/07/01. Please help."},` +
  173. `{"role":"assistant","content":[{"type":"tool_use","id":"x","name":"y","input":{"note":"Today’s date is 2026/07/01."}}]},` +
  174. `{"role":"user","content":[{"type":"tool_result","tool_use_id":"x","content":"log: Today’s date is 2026/07/01."}]}` +
  175. `]}`)
  176. out, hits, changed := NormalizeDateline(body)
  177. if changed {
  178. t.Fatalf("expected changed=false, hits=%v out=%s", hits, string(out))
  179. }
  180. if !bytes.Equal(out, body) {
  181. t.Fatalf("output should equal input byte-for-byte")
  182. }
  183. }
  184. func TestNormalizeDateline_Idempotent(t *testing.T) {
  185. body := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\nToday’s date is 2026/07/01.\n</system-reminder>"}]}`)
  186. first, _, changed1 := NormalizeDateline(body)
  187. if !changed1 {
  188. t.Fatalf("expected first pass to change body")
  189. }
  190. second, _, changed2 := NormalizeDateline(first)
  191. if changed2 {
  192. t.Fatalf("second pass should not report changes")
  193. }
  194. if !bytes.Equal(first, second) {
  195. t.Fatalf("second pass diverged: %s vs %s", string(first), string(second))
  196. }
  197. }
  198. func TestNormalizeDateline_EmptyBody(t *testing.T) {
  199. out, hits, changed := NormalizeDateline(nil)
  200. if changed || out != nil || hits != nil {
  201. t.Fatalf("empty body should be no-op")
  202. }
  203. }
  204. func TestNormalizeDateline_NoDateline(t *testing.T) {
  205. body := []byte(`{"messages":[{"role":"user","content":"hello"}],"system":"just a system prompt"}`)
  206. out, hits, changed := NormalizeDateline(body)
  207. if changed || len(hits) != 0 {
  208. t.Fatalf("expected no changes; changed=%v hits=%v", changed, hits)
  209. }
  210. if &out[0] != &body[0] {
  211. // Identity is a bonus but not strict; verify content equality at minimum
  212. if !bytes.Equal(out, body) {
  213. t.Fatalf("output must byte-match input when no changes needed")
  214. }
  215. }
  216. }
  217. func TestNormalizeDateline_MultipleSystemReminderBlocksInSameText(t *testing.T) {
  218. body := []byte(`{"messages":[{"role":"user","content":"<system-reminder>\nToday’s date is 2026/07/01.\n</system-reminder>\nsome prose\n<system-reminder>\nAlso Todayʼs date is 2026/07/01.\n</system-reminder>"}]}`)
  219. out, hits, changed := NormalizeDateline(body)
  220. if !changed {
  221. t.Fatalf("expected changed=true")
  222. }
  223. if len(hits) != 2 {
  224. t.Fatalf("expected 2 hits, got %d", len(hits))
  225. }
  226. if bytes.Contains(out, []byte("2026/07/01")) {
  227. t.Fatalf("slash separator must be gone: %s", string(out))
  228. }
  229. }