Files
wehub-resource-sync f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

423 lines
13 KiB
Go

package db
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseTrendTerms(t *testing.T) {
got, err := ParseTrendTerms([]string{
" load bearing | load-bearing ",
"seam",
"seam | Seam | seams",
"slic",
})
require.NoError(t, err, "ParseTrendTerms")
assert.Equal(t, "load bearing", got[0].Term, "term label")
assert.Equal(t, []string{"load bearing", "load-bearing"}, got[0].Variants, "variants")
assert.Equal(t, []string{"seam", "seams"}, got[1].Matchers, "matchers")
assert.Equal(t, "seam", got[2].Term, "deduped term label")
assert.Equal(t, []string{"seam", "seams"}, got[2].Variants, "deduped variants")
assert.Equal(t, []string{"slic", "slics", "slice", "slices", "sliced", "slicing"},
got[3].Matchers, "stem matchers")
}
func TestParseTrendTermsValidation(t *testing.T) {
t.Run("empty input", func(t *testing.T) {
_, err := ParseTrendTerms(nil)
require.Error(t, err)
})
t.Run("empty rows dropped before limit", func(t *testing.T) {
got, err := ParseTrendTerms([]string{"", " ", "seam"})
require.NoError(t, err, "ParseTrendTerms")
require.Len(t, got, 1, "terms")
assert.Equal(t, "seam", got[0].Term)
})
t.Run("more than 12 terms", func(t *testing.T) {
values := make([]string, MaxTrendTerms+1)
for i := range values {
values[i] = "term"
}
_, err := ParseTrendTerms(values)
require.Error(t, err)
assert.Contains(t, err.Error(), "at most 12", "want max terms")
})
t.Run("more than 8 variants after dedupe", func(t *testing.T) {
_, err := ParseTrendTerms([]string{"a|b|c|d|e|f|g|h|i"})
require.Error(t, err)
assert.Contains(t, err.Error(), "at most 8", "want max variants")
})
t.Run("variant limit after dedupe", func(t *testing.T) {
got, err := ParseTrendTerms([]string{"a|A|b|c|d|e|f|g|h"})
require.NoError(t, err, "ParseTrendTerms")
assert.Len(t, got[0].Variants, MaxTrendTermVariants, "variant count")
})
}
func TestCountTrendOccurrences(t *testing.T) {
term := TrendTermInput{
Term: "seam",
Variants: []string{"seam"},
Matchers: []string{"seam", "seams"},
}
cases := []struct {
name string
text string
want int
}{
{"case insensitive", "Seam seam SEAMS", 3},
{"word boundary", "seamless seam seams", 2},
{"overlap plural", "seams", 1},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, countTrendOccurrences(tc.text, term))
})
}
}
func TestCountTrendOccurrencesSilentEStem(t *testing.T) {
terms, err := ParseTrendTerms([]string{"slic"})
require.NoError(t, err, "ParseTrendTerms")
got := countTrendOccurrences(
"slice slices sliced slicing slicer sliced-up",
terms[0],
)
assert.Equal(t, 5, got)
}
func TestCountTrendOccurrencesPhrases(t *testing.T) {
term := TrendTermInput{
Term: "load bearing",
Variants: []string{"load bearing", "load-bearing"},
Matchers: []string{"load bearing", "load-bearing"},
}
got := countTrendOccurrences("Load bearing and load-bearing", term)
assert.Equal(t, 2, got)
}
func TestTrendBucketDate(t *testing.T) {
loc := time.UTC
cases := []struct {
gran string
ts string
want string
}{
{"day", "2024-06-05T12:00:00Z", "2024-06-05"},
{"week", "2024-06-05T12:00:00Z", "2024-06-03"},
{"month", "2024-06-05T12:00:00Z", "2024-06-01"},
}
for _, tc := range cases {
parsed, _ := time.Parse(time.RFC3339, tc.ts)
assert.Equal(t, tc.want, trendBucketDate(parsed, loc, tc.gran), tc.gran)
}
}
func TestGetTrendsTermsSQLite(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-01T09:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 3
s.UserMessageCount = 2
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: "load bearing seam", Timestamp: "2024-06-01T09:00:00Z", ContentLength: 17},
Message{SessionID: "s1", Ordinal: 1, Role: "assistant", Content: "load-bearing seams seam", Timestamp: "2024-06-08T09:00:00Z", ContentLength: 23},
Message{SessionID: "s1", Ordinal: 2, Role: "user", Content: "seam system", Timestamp: "2024-06-08T09:00:00Z", ContentLength: 11, IsSystem: true},
)
terms, err := ParseTrendTerms([]string{"load bearing | load-bearing", "seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-01", To: "2024-06-09", Timezone: "UTC",
}, terms, "week")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, []string{"2024-05-27", "2024-06-03"},
trendBucketDates(got.Buckets), "bucket dates")
assert.Equal(t, []int{1, 1}, trendBucketMessageCounts(got.Buckets),
"bucket message counts")
assert.Equal(t, 2, got.MessageCount, "message count")
byTerm := trendSeriesByTerm(got.Series)
assert.Equal(t, 2, byTerm["load bearing"].Total, "load bearing total")
assert.Equal(t, 3, byTerm["seam"].Total, "seam total")
assert.Equal(t, []int{1, 1}, trendPointCounts(byTerm["load bearing"].Points),
"load bearing points")
assert.Equal(t, []int{1, 2}, trendPointCounts(byTerm["seam"].Points),
"seam points")
}
func TestGetTrendsTermsSQLiteProjectFilter(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-01T09:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 1
s.UserMessageCount = 1
})
insertSession(t, d, "s2", "proj-b", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 1
s.UserMessageCount = 1
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: "seam", Timestamp: start, ContentLength: 4},
Message{SessionID: "s2", Ordinal: 0, Role: "user", Content: "seam", Timestamp: start, ContentLength: 4},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC", Project: "proj-a",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"project-filtered total")
}
func TestGetTrendsTermsSQLiteModelFilter(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-01T09:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 1
s.UserMessageCount = 1
})
insertSession(t, d, "s2", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 1
s.UserMessageCount = 1
})
insertMessages(t, d,
Message{
SessionID: "s1", Ordinal: 0, Role: "user",
Content: "seam", Timestamp: start, ContentLength: 4,
Model: "gpt-4o",
},
Message{
SessionID: "s2", Ordinal: 0, Role: "user",
Content: "seam", Timestamp: start, ContentLength: 4,
Model: "claude-3-5-sonnet",
},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
Model: "gpt-4o",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, got.MessageCount, "message count")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"model-filtered total")
}
func TestGetTrendsTermsSQLiteModelFilterStaysOnMatchingMessages(
t *testing.T,
) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-01T09:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 3
s.UserMessageCount = 1
})
insertMessages(t, d,
Message{
SessionID: "s1", Ordinal: 0, Role: "user",
Content: "seam", Timestamp: start, ContentLength: 4,
},
Message{
SessionID: "s1", Ordinal: 1, Role: "assistant",
Content: "ready", Timestamp: start, ContentLength: 5,
Model: "gpt-4o",
},
Message{
SessionID: "s1", Ordinal: 2, Role: "assistant",
Content: "seam seam", Timestamp: start, ContentLength: 9,
Model: "claude-3-5-sonnet",
},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
Model: "gpt-4o",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 2, got.MessageCount, "message count")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"model-filtered total")
}
func TestGetTrendsTermsSQLiteUsesMessageTimestampRange(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-05-01T09:00:00Z"
created := "2024-05-01T08:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = created
s.MessageCount = 1
s.UserMessageCount = 1
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: "seam", Timestamp: "2024-06-05T09:00:00Z", ContentLength: 4},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-05", To: "2024-06-05", Timezone: "UTC",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"message timestamp total")
}
func TestGetTrendsTermsSQLiteDoesNotFilterBySessionTimestamp(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "not-a-time"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.MessageCount = 1
s.UserMessageCount = 1
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: "seam", Timestamp: "2024-06-05T09:00:00Z", ContentLength: 4},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-05", To: "2024-06-05", Timezone: "UTC",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"message timestamp total")
}
func TestGetTrendsTermsSQLiteAppliesDayAndHourToMessageTimestamp(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-04T08:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.MessageCount = 2
s.UserMessageCount = 2
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: "seam", Timestamp: "2024-06-05T09:00:00Z", ContentLength: 4},
Message{SessionID: "s1", Ordinal: 1, Role: "user", Content: "seam", Timestamp: "2024-06-05T10:00:00Z", ContentLength: 4},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
dow := 2
hour := 9
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-05",
To: "2024-06-05",
Timezone: "UTC",
DayOfWeek: &dow,
Hour: &hour,
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"hour-filtered message total")
}
func TestGetTrendsTermsSQLiteTimestampFallback(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-05T09:00:00Z"
created := "2024-06-04T08:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = created
s.MessageCount = 1
s.UserMessageCount = 1
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: "seam", Timestamp: "not-a-time", ContentLength: 4},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-05", To: "2024-06-05", Timezone: "UTC",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"fallback timestamp total")
}
func TestGetTrendsTermsSQLiteExcludesLegacySystemPrefixes(t *testing.T) {
d := testDB(t)
ctx := context.Background()
start := "2024-06-01T09:00:00Z"
insertSession(t, d, "s1", "proj-a", func(s *Session) {
s.StartedAt = &start
s.CreatedAt = start
s.MessageCount = 2
s.UserMessageCount = 2
})
insertMessages(t, d,
Message{SessionID: "s1", Ordinal: 0, Role: "user", Content: SystemMsgPrefixes[0] + " seam", Timestamp: start, ContentLength: 40},
Message{SessionID: "s1", Ordinal: 1, Role: "user", Content: "seam", Timestamp: start, ContentLength: 4},
)
terms, err := ParseTrendTerms([]string{"seam"})
require.NoError(t, err)
got, err := d.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2024-06-01", To: "2024-06-01", Timezone: "UTC",
}, terms, "day")
require.NoError(t, err, "GetTrendsTerms")
assert.Equal(t, 1, trendSeriesByTerm(got.Series)["seam"].Total,
"system-prefix-filtered total")
}
func trendBucketDates(buckets []TrendBucket) []string {
dates := make([]string, len(buckets))
for i, bucket := range buckets {
dates[i] = bucket.Date
}
return dates
}
func trendBucketMessageCounts(buckets []TrendBucket) []int {
counts := make([]int, len(buckets))
for i, bucket := range buckets {
counts[i] = bucket.MessageCount
}
return counts
}
func trendSeriesByTerm(series []TrendSeries) map[string]TrendSeries {
byTerm := make(map[string]TrendSeries, len(series))
for _, entry := range series {
byTerm[entry.Term] = entry
}
return byTerm
}
func trendPointCounts(points []TrendPoint) []int {
counts := make([]int, len(points))
for i, point := range points {
counts[i] = point.Count
}
return counts
}