usage_log_session_id_unit_test.go 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. //go:build unit
  2. package repository
  3. import (
  4. "database/sql"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/Wei-Shaw/sub2api/internal/service"
  9. "github.com/stretchr/testify/require"
  10. )
  11. func newSessionIDUsageLog(sessionID *string) *service.UsageLog {
  12. return &service.UsageLog{
  13. UserID: 1,
  14. APIKeyID: 2,
  15. AccountID: 3,
  16. RequestID: "req-session-id",
  17. Model: "claude-3",
  18. InputTokens: 10,
  19. OutputTokens: 5,
  20. TotalCost: 1.0,
  21. ActualCost: 1.0,
  22. SessionID: sessionID,
  23. CreatedAt: time.Now().UTC(),
  24. }
  25. }
  26. // TestPrepareUsageLogInsert_SessionIDArgWiring pins the session_id column to the
  27. // arg slice / arg-type table so the five INSERT column lists stay in sync. session_id
  28. // is immediately before native_compaction_v2; created_at is always last.
  29. func TestPrepareUsageLogInsert_SessionIDArgWiring(t *testing.T) {
  30. sessionID := "sess-persisted-123"
  31. prepared := prepareUsageLogInsert(newSessionIDUsageLog(&sessionID))
  32. require.Len(t, prepared.args, len(usageLogInsertArgTypes),
  33. "prepared args must match the arg-type table length")
  34. // created_at is last; native_compaction_v2 is penultimate; session_id precedes it.
  35. sessionArg := prepared.args[len(prepared.args)-3]
  36. ns, ok := sessionArg.(sql.NullString)
  37. require.True(t, ok, "session_id arg should be a sql.NullString, got %T", sessionArg)
  38. require.True(t, ns.Valid)
  39. require.Equal(t, sessionID, ns.String)
  40. require.Equal(t, "text", usageLogInsertArgTypes[len(usageLogInsertArgTypes)-3],
  41. "session_id arg type must be text")
  42. require.Equal(t, "boolean", usageLogInsertArgTypes[len(usageLogInsertArgTypes)-2],
  43. "native_compaction_v2 arg type must be boolean")
  44. }
  45. // TestPrepareUsageLogInsert_SessionIDNullWhenAbsent proves an absent session id is
  46. // persisted as SQL NULL rather than an empty string.
  47. func TestPrepareUsageLogInsert_SessionIDNullWhenAbsent(t *testing.T) {
  48. prepared := prepareUsageLogInsert(newSessionIDUsageLog(nil))
  49. sessionArg := prepared.args[len(prepared.args)-3]
  50. ns, ok := sessionArg.(sql.NullString)
  51. require.True(t, ok, "session_id arg should be a sql.NullString, got %T", sessionArg)
  52. require.False(t, ns.Valid, "absent session id must be NULL, not empty string")
  53. empty := ""
  54. preparedEmpty := prepareUsageLogInsert(newSessionIDUsageLog(&empty))
  55. nsEmpty := preparedEmpty.args[len(preparedEmpty.args)-3].(sql.NullString)
  56. require.False(t, nsEmpty.Valid, "empty session id must also be NULL")
  57. }
  58. func TestPrepareUsageLogInsert_RequestedReasoningEffortArgWiring(t *testing.T) {
  59. requested := "max"
  60. forwarded := "xhigh"
  61. prepared := prepareUsageLogInsert(&service.UsageLog{
  62. UserID: 1,
  63. APIKeyID: 2,
  64. AccountID: 3,
  65. RequestID: "req-requested-effort",
  66. Model: "gpt-5.4",
  67. ReasoningEffort: &forwarded,
  68. RequestedReasoningEffort: &requested,
  69. CreatedAt: time.Now().UTC(),
  70. })
  71. require.Len(t, prepared.args, len(usageLogInsertArgTypes))
  72. require.Equal(t, "text", usageLogInsertArgTypes[48], "requested_reasoning_effort must follow reasoning_effort")
  73. require.Equal(t, "text", usageLogInsertArgTypes[47], "reasoning_effort arg type must stay text")
  74. forwardedArg, ok := prepared.args[47].(sql.NullString)
  75. require.True(t, ok)
  76. require.True(t, forwardedArg.Valid)
  77. require.Equal(t, forwarded, forwardedArg.String)
  78. requestedArg, ok := prepared.args[48].(sql.NullString)
  79. require.True(t, ok)
  80. require.True(t, requestedArg.Valid)
  81. require.Equal(t, requested, requestedArg.String)
  82. }
  83. // TestUsageLogInsertQueries_IncludeSessionID guards that every generated INSERT path
  84. // and the SELECT column list reference session_id.
  85. func TestUsageLogInsertQueries_IncludeSessionID(t *testing.T) {
  86. require.Contains(t, usageLogSelectColumns, "requested_reasoning_effort",
  87. "SELECT column list must include requested_reasoning_effort")
  88. require.Contains(t, usageLogSelectColumns, "session_id",
  89. "SELECT column list must include session_id")
  90. sessionID := "sess-in-query"
  91. log := newSessionIDUsageLog(&sessionID)
  92. prepared := prepareUsageLogInsert(log)
  93. key := usageLogBatchKey(log.RequestID, log.APIKeyID)
  94. batchQuery, batchArgs := buildUsageLogBatchInsertQuery([]string{key},
  95. map[string]usageLogInsertPrepared{key: prepared})
  96. require.Contains(t, batchQuery, "session_id")
  97. require.Contains(t, batchQuery, "requested_reasoning_effort")
  98. // Two column references (INSERT column list + SELECT ... FROM input) plus the CTE def.
  99. require.GreaterOrEqual(t, strings.Count(batchQuery, "session_id"), 3)
  100. require.Len(t, batchArgs, len(prepared.args)+1,
  101. "batch args include the synthetic input_index before usage-log values")
  102. bestEffortQuery, bestEffortArgs := buildUsageLogBestEffortInsertQuery([]usageLogInsertPrepared{prepared})
  103. require.Contains(t, bestEffortQuery, "session_id")
  104. require.Len(t, bestEffortArgs, len(prepared.args))
  105. }