@@ -374,10 +374,9 @@ func TestStoreAnalyticsUsageAndTrends(t *testing.T) {
374374 require .NoError (t , err )
375375 assert .Equal (t , 2 , signals .UnscoredSessions )
376376
377- trends , err := store .GetTrendsTerms (ctx , filter , []db.TrendTermInput {{
378- Term : "alpha" ,
379- Variants : []string {"alpha" },
380- }}, "week" )
377+ trendTerms , err := db .ParseTrendTerms ([]string {"alpha" })
378+ require .NoError (t , err )
379+ trends , err := store .GetTrendsTerms (ctx , filter , trendTerms , "week" )
381380 require .NoError (t , err )
382381 assert .Equal (t , 1 , trends .Series [0 ].Total )
383382
@@ -1391,15 +1390,14 @@ func TestTrendsTermsApplySessionFiltersAndSystemPrefixExclusion(t *testing.T) {
13911390 require .NoError (t , err )
13921391 store := NewStoreFromDB (syncer .DB ())
13931392
1393+ trendTerms , err := db .ParseTrendTerms ([]string {"seam" })
1394+ require .NoError (t , err )
13941395 trends , err := store .GetTrendsTerms (ctx , db.AnalyticsFilter {
13951396 From : "2026-01-22" ,
13961397 To : "2026-01-22" ,
13971398 Timezone : "UTC" ,
13981399 Project : "alpha" ,
1399- }, []db.TrendTermInput {{
1400- Term : "seam" ,
1401- Variants : []string {"seam" },
1402- }}, "day" )
1400+ }, trendTerms , "day" )
14031401 require .NoError (t , err )
14041402 require .Len (t , trends .Series , 1 )
14051403 assert .Equal (t , 1 , trends .Series [0 ].Total )
@@ -1558,6 +1556,94 @@ func TestUsageDedupesClaudeMessageIDs(t *testing.T) {
15581556 assert .Equal (t , []string {"claude-test" }, sessionUsage .Models )
15591557}
15601558
1559+ func TestUsageDedupPrefersInRangeDuplicate (t * testing.T ) {
1560+ ctx := context .Background ()
1561+ local := newLocalDB (t )
1562+ require .NoError (t , local .UpsertModelPricing ([]db.ModelPricing {{
1563+ ModelPattern : "claude-test" ,
1564+ InputPerMTok : 3 ,
1565+ OutputPerMTok : 15 ,
1566+ }}))
1567+
1568+ before := syncMessage ("duck-usage-edge-a" , 0 , "assistant" , "before midnight" , "2026-01-12T23:30:00.000Z" )
1569+ before .ClaudeMessageID = "edge-message"
1570+ before .ClaudeRequestID = "edge-request"
1571+ after := syncMessage ("duck-usage-edge-b" , 0 , "assistant" , "after midnight" , "2026-01-13T00:30:00.000Z" )
1572+ after .ClaudeMessageID = "edge-message"
1573+ after .ClaudeRequestID = "edge-request"
1574+
1575+ _ , err := local .WriteSessionBatchAtomic ([]db.SessionBatchWrite {
1576+ {
1577+ Session : syncSession ("duck-usage-edge-a" , "alpha" , "edge a" , "2026-01-12T23:30:00.000Z" , 1 ),
1578+ Messages : []db.Message {before },
1579+ DataVersion : 1 ,
1580+ ReplaceMessages : true ,
1581+ },
1582+ {
1583+ Session : syncSession ("duck-usage-edge-b" , "alpha" , "edge b" , "2026-01-13T00:30:00.000Z" , 1 ),
1584+ Messages : []db.Message {after },
1585+ DataVersion : 1 ,
1586+ ReplaceMessages : true ,
1587+ },
1588+ })
1589+ require .NoError (t , err )
1590+
1591+ syncer := newTestSync (t , filepath .Join (t .TempDir (), "usage-edge.duckdb" ), local , SyncOptions {})
1592+ _ , err = syncer .Push (ctx , true , nil )
1593+ require .NoError (t , err )
1594+ store := NewStoreFromDB (syncer .DB ())
1595+
1596+ // The duplicate before midnight is outside the window but inside
1597+ // the padded UTC bounds and sorts first by timestamp. It must not
1598+ // win the dedup and suppress the in-range duplicate.
1599+ got , err := store .GetDailyUsage (ctx , db.UsageFilter {
1600+ From : "2026-01-13" , To : "2026-01-13" , Timezone : "UTC" ,
1601+ })
1602+ require .NoError (t , err )
1603+ assert .Equal (t , 1 , got .Totals .InputTokens )
1604+ assert .Equal (t , 2 , got .Totals .OutputTokens )
1605+ }
1606+
1607+ func TestTrendsTermsWordBoundaryAndOverlapParity (t * testing.T ) {
1608+ ctx := context .Background ()
1609+ local := newLocalDB (t )
1610+ start := "2026-01-22T09:00:00.000Z"
1611+ content := "seam seams seamless testing test attest"
1612+ _ , err := local .WriteSessionBatchAtomic ([]db.SessionBatchWrite {{
1613+ Session : syncSession ("duck-trend-parity" , "alpha" , "trend parity" , start , 1 ),
1614+ Messages : []db.Message {
1615+ syncMessage ("duck-trend-parity" , 0 , "user" , content , start ),
1616+ },
1617+ DataVersion : 1 ,
1618+ ReplaceMessages : true ,
1619+ }})
1620+ require .NoError (t , err )
1621+ syncer := newTestSync (t , filepath .Join (t .TempDir (), "trends-parity.duckdb" ), local , SyncOptions {})
1622+ _ , err = syncer .Push (ctx , true , nil )
1623+ require .NoError (t , err )
1624+ store := NewStoreFromDB (syncer .DB ())
1625+
1626+ terms , err := db .ParseTrendTerms ([]string {"seam" , "test|testing" })
1627+ require .NoError (t , err )
1628+ filter := db.AnalyticsFilter {
1629+ From : "2026-01-22" , To : "2026-01-22" , Timezone : "UTC" ,
1630+ }
1631+
1632+ got , err := store .GetTrendsTerms (ctx , filter , terms , "day" )
1633+ require .NoError (t , err )
1634+ require .Len (t , got .Series , 2 )
1635+ // Word-bounded: "seamless" does not count for "seam", and
1636+ // "testing" is not double-counted via its "test" substring.
1637+ assert .Equal (t , 2 , got .Series [0 ].Total )
1638+ assert .Equal (t , 2 , got .Series [1 ].Total )
1639+
1640+ want , err := local .GetTrendsTerms (ctx , filter , terms , "day" )
1641+ require .NoError (t , err )
1642+ require .Len (t , want .Series , 2 )
1643+ assert .Equal (t , want .Series [0 ].Total , got .Series [0 ].Total )
1644+ assert .Equal (t , want .Series [1 ].Total , got .Series [1 ].Total )
1645+ }
1646+
15611647func TestDailyUsageBreakdownsAndCacheSavings (t * testing.T ) {
15621648 ctx := context .Background ()
15631649 local := newLocalDB (t )
0 commit comments