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

199 lines
7.3 KiB
Go

package db
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetSessionActivity(t *testing.T) {
d := testDB(t)
sid := "test-activity"
err := d.UpsertSession(Session{
ID: sid,
Agent: "claude",
StartedAt: new("2026-03-26T10:00:00Z"),
})
require.NoError(t, err)
// Insert messages spanning ~29 minutes.
msgs := []Message{
{SessionID: sid, Ordinal: 0, Role: "user", Content: "hello", Timestamp: "2026-03-26T10:00:00Z", ContentLength: 5},
{SessionID: sid, Ordinal: 1, Role: "assistant", Content: "hi", Timestamp: "2026-03-26T10:00:30Z", ContentLength: 2},
{SessionID: sid, Ordinal: 2, Role: "user", Content: "next", Timestamp: "2026-03-26T10:01:30Z", ContentLength: 4},
{SessionID: sid, Ordinal: 3, Role: "assistant", Content: "resp", Timestamp: "2026-03-26T10:02:00Z", ContentLength: 4},
// Gap: no messages from 10:02 to 10:28.
{SessionID: sid, Ordinal: 4, Role: "user", Content: "back", Timestamp: "2026-03-26T10:28:00Z", ContentLength: 4},
{SessionID: sid, Ordinal: 5, Role: "assistant", Content: "wb", Timestamp: "2026-03-26T10:29:00Z", ContentLength: 2},
// System message — should be excluded from counts.
{SessionID: sid, Ordinal: 6, Role: "user", Content: "This session is being continued from a previous conversation.", Timestamp: "2026-03-26T10:29:30Z", ContentLength: 60, IsSystem: true},
}
require.NoError(t, d.InsertMessages(msgs))
resp, err := d.GetSessionActivity(context.Background(), sid)
require.NoError(t, err)
// 29 min span => 1min buckets (snapInterval(1740) = 60).
assert.Equal(t, int64(60), resp.IntervalSeconds, "interval")
// System message should still count toward total (7 total messages).
assert.Equal(t, 7, resp.TotalMessages, "total")
// Should have 30 buckets (min 0 to min 29).
assert.GreaterOrEqual(t, len(resp.Buckets), 28, "bucket count")
// First bucket (10:00-10:01) should have user=1, assistant=1.
first := resp.Buckets[0]
assert.Equal(t, 1, first.UserCount, "first bucket user")
assert.Equal(t, 1, first.AssistantCount, "first bucket asst")
require.NotNil(t, first.FirstOrdinal, "first bucket first_ordinal")
assert.Equal(t, 0, *first.FirstOrdinal, "first bucket first_ordinal")
// Middle empty bucket should have nil FirstOrdinal.
mid := resp.Buckets[15]
assert.Equal(t, 0, mid.UserCount, "mid bucket user")
assert.Equal(t, 0, mid.AssistantCount, "mid bucket asst")
assert.Nil(t, mid.FirstOrdinal, "mid bucket first_ordinal")
}
func TestGetSessionActivity_NoMessages(t *testing.T) {
d := testDB(t)
sid := "test-empty"
require.NoError(t, d.UpsertSession(Session{ID: sid, Agent: "claude"}))
resp, err := d.GetSessionActivity(context.Background(), sid)
require.NoError(t, err)
assert.Empty(t, resp.Buckets, "buckets")
}
func TestGetSessionActivity_NullTimestamps(t *testing.T) {
d := testDB(t)
sid := "test-null-ts"
require.NoError(t, d.UpsertSession(Session{ID: sid, Agent: "claude"}))
msgs := []Message{
{SessionID: sid, Ordinal: 0, Role: "user", Content: "hi", ContentLength: 2},
{SessionID: sid, Ordinal: 1, Role: "assistant", Content: "hello", ContentLength: 5},
}
require.NoError(t, d.InsertMessages(msgs))
resp, err := d.GetSessionActivity(context.Background(), sid)
require.NoError(t, err)
assert.Empty(t, resp.Buckets, "buckets")
assert.Equal(t, 2, resp.TotalMessages, "total")
}
func TestGetSessionActivity_SingleMessage(t *testing.T) {
d := testDB(t)
sid := "test-single"
require.NoError(t, d.UpsertSession(Session{ID: sid, Agent: "claude"}))
msgs := []Message{
{SessionID: sid, Ordinal: 0, Role: "user", Content: "hi", Timestamp: "2026-03-26T10:00:00Z", ContentLength: 2},
}
require.NoError(t, d.InsertMessages(msgs))
resp, err := d.GetSessionActivity(context.Background(), sid)
require.NoError(t, err)
require.Len(t, resp.Buckets, 1, "buckets")
assert.Equal(t, 1, resp.Buckets[0].UserCount, "user count")
}
func TestGetSessionActivity_MalformedTimestamps(t *testing.T) {
d := testDB(t)
sid := "test-malformed-ts"
require.NoError(t, d.UpsertSession(Session{ID: sid, Agent: "claude"}))
msgs := []Message{
{SessionID: sid, Ordinal: 0, Role: "user", Content: "hi", Timestamp: "2026-03-26T10:00:00Z", ContentLength: 2},
{SessionID: sid, Ordinal: 1, Role: "assistant", Content: "hello", Timestamp: "not-a-timestamp", ContentLength: 5},
{SessionID: sid, Ordinal: 2, Role: "user", Content: "bye", Timestamp: "2026-03-26T10:00:30Z", ContentLength: 3},
}
require.NoError(t, d.InsertMessages(msgs))
resp, err := d.GetSessionActivity(context.Background(), sid)
require.NoError(t, err)
// Malformed timestamp excluded from buckets; valid ones bucketed.
require.NotEmpty(t, resp.Buckets, "expected at least 1 bucket")
// Both valid user messages (ord 0 and 2) are within 30s,
// so they land in the same bucket.
assert.Equal(t, 2, resp.Buckets[0].UserCount, "first bucket user")
assert.Equal(t, 0, resp.Buckets[0].AssistantCount, "first bucket asst")
assert.Equal(t, 3, resp.TotalMessages, "total")
}
func TestGetSessionActivity_FractionalTimestamps(t *testing.T) {
d := testDB(t)
sid := "test-frac-ts"
require.NoError(t, d.UpsertSession(Session{ID: sid, Agent: "claude"}))
// Two messages within the same 60s bucket but with fractional
// timestamps that would be mis-bucketed by whole-second truncation.
// 10:00:00.900 and 10:00:59.100 are 58.2s apart — same 60s bucket.
msgs := []Message{
{SessionID: sid, Ordinal: 0, Role: "user", Content: "a", Timestamp: "2026-03-26T10:00:00.900Z", ContentLength: 1},
{SessionID: sid, Ordinal: 1, Role: "assistant", Content: "b", Timestamp: "2026-03-26T10:00:59.100Z", ContentLength: 1},
// This message is in the next bucket (60.1s after the anchor).
{SessionID: sid, Ordinal: 2, Role: "user", Content: "c", Timestamp: "2026-03-26T10:01:01.000Z", ContentLength: 1},
}
require.NoError(t, d.InsertMessages(msgs))
resp, err := d.GetSessionActivity(context.Background(), sid)
require.NoError(t, err)
require.Equal(t, int64(60), resp.IntervalSeconds, "interval")
// First bucket should have both fractional-second messages.
require.NotEmpty(t, resp.Buckets, "expected at least 1 bucket")
first := resp.Buckets[0]
assert.Equal(t, 1, first.UserCount, "first bucket user")
assert.Equal(t, 1, first.AssistantCount, "first bucket asst")
// Second bucket should have the third message.
require.GreaterOrEqual(t, len(resp.Buckets), 2, "expected at least 2 buckets")
second := resp.Buckets[1]
assert.Equal(t, 1, second.UserCount, "second bucket user")
}
func TestSnapInterval(t *testing.T) {
tests := []struct {
name string
duration int64 // seconds
want int64
}{
{"30s session", 30, 60},
{"5m session", 300, 60},
{"10m session", 600, 60},
{"20m session", 1200, 60},
{"30m session", 1800, 60},
{"1h session", 3600, 120},
{"2h session", 7200, 300},
{"4h session", 14400, 600},
{"8h session", 28800, 900},
{"12h session", 43200, 1800},
{"16h session", 57600, 1800},
{"24h session", 86400, 3600},
{"48h session", 172800, 7200},
// Extreme: 30 days. 7200s would give 361 buckets,
// so interval scales up to keep count <= 50.
// ceil(2592000 / 49) = 52898
{"30d session", 2592000, 52898},
{"0s session", 0, 60},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := SnapInterval(tt.duration)
assert.Equal(t, tt.want, got, "SnapInterval(%d)", tt.duration)
})
}
}