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

1190 lines
36 KiB
Go

package db
import (
"context"
"errors"
"math"
"os"
"path/filepath"
"slices"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type storeContractBackend struct {
name string
open func(t *testing.T) Store
seed func(t *testing.T, store Store) storeContractFixture
supportsLocalWrites bool
}
type storeContractFixture struct {
alphaID string
betaID string
gammaID string
oldID string
automatedID string
childID string
deletedID string
}
func storeContractBackends() []storeContractBackend {
// The db package can only register SQLite without importing remote
// providers back into their dependency root. Read-only backends run
// equivalent store-contract suites in their own packages.
return []storeContractBackend{
{
name: "sqlite",
open: func(t *testing.T) Store {
t.Helper()
return openStoreContractSQLiteFixtureDB(t)
},
seed: func(t *testing.T, store Store) storeContractFixture {
t.Helper()
return storeContractSQLiteFixtureForTest(t)
},
supportsLocalWrites: true,
},
}
}
var (
storeContractSQLiteTemplateOnce sync.Once
storeContractSQLiteTemplateDir string
storeContractSQLiteTemplatePath string
storeContractSQLiteTemplateFixture storeContractFixture
storeContractSQLiteTemplateErr error
)
func openStoreContractSQLiteFixtureDB(t *testing.T) Store {
t.Helper()
src, _ := storeContractSQLiteTemplate(t)
dst := filepath.Join(t.TempDir(), "test.db")
for _, suffix := range []string{"", "-wal", "-shm"} {
require.NoError(t,
copyTemplateDBFile(src+suffix, dst+suffix, suffix == ""),
"copy store-contract fixture %q", suffix)
}
d, err := OpenPreparedTestDB(dst)
require.NoError(t, err, "open store-contract fixture")
t.Cleanup(func() { require.NoError(t, d.Close()) })
return d
}
func storeContractSQLiteFixtureForTest(t *testing.T) storeContractFixture {
t.Helper()
_, fixture := storeContractSQLiteTemplate(t)
return fixture
}
func storeContractSQLiteTemplate(t *testing.T) (string, storeContractFixture) {
t.Helper()
storeContractSQLiteTemplateOnce.Do(func() {
storeContractSQLiteTemplateDir, storeContractSQLiteTemplateErr =
os.MkdirTemp("", "agentsview-store-contract-*")
if storeContractSQLiteTemplateErr != nil {
return
}
storeContractSQLiteTemplatePath = filepath.Join(
storeContractSQLiteTemplateDir, "test.db")
storeContractSQLiteTemplateErr = copyTestDBTemplate(
storeContractSQLiteTemplatePath)
if storeContractSQLiteTemplateErr != nil {
return
}
var d *DB
d, storeContractSQLiteTemplateErr = OpenPreparedTestDB(
storeContractSQLiteTemplatePath)
if storeContractSQLiteTemplateErr != nil {
return
}
storeContractSQLiteTemplateFixture = seedStoreContractSQLite(t, d)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
storeContractSQLiteTemplateErr = d.CheckpointWALTruncate(ctx)
if closeErr := d.Close(); storeContractSQLiteTemplateErr == nil {
storeContractSQLiteTemplateErr = closeErr
}
})
require.NoError(t, storeContractSQLiteTemplateErr,
"build store-contract fixture")
return storeContractSQLiteTemplatePath, storeContractSQLiteTemplateFixture
}
func TestStoreContract(t *testing.T) {
tests := []struct {
name string
run func(t *testing.T, store Store, fixture storeContractFixture, backend storeContractBackend)
}{
{"sessions_cursor_filters_and_dates", contractSessionsCursorFiltersAndDates},
{"messages_ordering_and_tool_results", contractMessagesOrderingAndToolResults},
{"search_modes_and_secret_findings", contractSearchModesAndSecretFindings},
{"stars_and_pins", contractStarsAndPins},
{"analytics_trends_and_usage", contractAnalyticsTrendsAndUsage},
{"local_only_methods", contractLocalOnlyMethods},
}
for _, backend := range storeContractBackends() {
t.Run(backend.name, func(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
store := backend.open(t)
fixture := backend.seed(t, store)
tt.run(t, store, fixture, backend)
})
}
})
}
}
func contractSessionsCursorFiltersAndDates(
t *testing.T,
store Store,
fixture storeContractFixture,
_ storeContractBackend,
) {
t.Helper()
ctx := context.Background()
page, err := store.ListSessions(ctx, SessionFilter{Limit: 2})
require.NoError(t, err)
require.Equal(t, 5, page.Total)
require.Len(t, page.Sessions, 2)
require.Equal(t, []string{fixture.gammaID, fixture.automatedID}, sessionIDs(page.Sessions))
require.NotEmpty(t, page.NextCursor)
cur, err := store.DecodeCursor(page.NextCursor)
require.NoError(t, err)
require.Equal(t, fixture.automatedID, cur.ID)
require.Equal(t, 5, cur.Total)
next, err := store.ListSessions(ctx, SessionFilter{
Limit: 2,
Cursor: page.NextCursor,
})
require.NoError(t, err)
require.Equal(t, 5, next.Total)
require.Equal(t, []string{fixture.betaID, fixture.alphaID}, sessionIDs(next.Sessions))
alphaOnly, err := store.ListSessions(ctx, SessionFilter{
Project: "alpha",
DateFrom: "2026-01-10",
DateTo: "2026-01-12",
MinMessages: 3,
})
require.NoError(t, err)
require.Equal(t, []string{fixture.gammaID, fixture.alphaID}, sessionIDs(alphaOnly.Sessions))
noAutomation, err := store.ListSessions(ctx, SessionFilter{
ExcludeAutomated: true,
Limit: 10,
})
require.NoError(t, err)
require.NotContains(t, sessionIDs(noAutomation.Sessions), fixture.automatedID)
withChildren, err := store.ListSessions(ctx, SessionFilter{
Project: "alpha",
IncludeChildren: true,
Limit: 10,
})
require.NoError(t, err)
require.Contains(t, sessionIDs(withChildren.Sessions), fixture.childID)
children, err := store.GetChildSessions(ctx, fixture.alphaID)
require.NoError(t, err)
require.Equal(t, []string{fixture.childID}, sessionIDs(children))
trashed, err := store.GetSession(ctx, fixture.deletedID)
require.NoError(t, err)
require.Nil(t, trashed)
full, err := store.GetSessionFull(ctx, fixture.deletedID)
require.NoError(t, err)
require.NotNil(t, full)
require.NotNil(t, full.DeletedAt)
// GetSessionFull returns the visible name on every backend:
// the user rename when set, else the agent-provided session name.
named, err := store.GetSessionFull(ctx, fixture.oldID)
require.NoError(t, err)
require.NotNil(t, named)
require.NotNil(t, named.DisplayName)
require.Equal(t, "Old Contract Name", *named.DisplayName)
index, err := store.GetSidebarSessionIndex(ctx, SessionFilter{
Project: "alpha",
})
require.NoError(t, err)
require.Contains(t, sidebarSessionIDs(index.Sessions), fixture.childID)
stats, err := store.GetStats(ctx, false, false)
require.NoError(t, err)
require.Equal(t, 5, stats.SessionCount)
require.Equal(t, 13, stats.MessageCount)
require.Equal(t, 3, stats.ProjectCount)
projects, err := store.GetProjects(ctx, false, false)
require.NoError(t, err)
require.Equal(t, []string{"alpha", "beta", "review"}, projectNames(projects))
agents, err := store.GetAgents(ctx, false, false)
require.NoError(t, err)
require.Equal(t, []string{"claude", "codex", "roborev"}, agentNames(agents))
machines, err := store.GetMachines(ctx, false, false)
require.NoError(t, err)
require.Equal(t, []string{"linux", "mac"}, machines)
}
func contractMessagesOrderingAndToolResults(
t *testing.T,
store Store,
fixture storeContractFixture,
_ storeContractBackend,
) {
t.Helper()
ctx := context.Background()
asc, err := store.GetMessages(ctx, fixture.alphaID, 1, 3, true)
require.NoError(t, err)
require.Equal(t, []int{1, 2, 3}, messageOrdinals(asc))
require.Len(t, asc[0].ToolCalls, 1)
require.Equal(t, "search", asc[0].ToolCalls[0].ToolName)
require.Len(t, asc[0].ToolCalls[0].ResultEvents, 1)
require.Equal(t, "tool stream found contract parity event", asc[0].ToolCalls[0].ResultEvents[0].Content)
desc, err := store.GetMessages(ctx, fixture.alphaID, 3, 2, false)
require.NoError(t, err)
require.Equal(t, []int{3, 2}, messageOrdinals(desc))
all, err := store.GetAllMessages(ctx, fixture.alphaID)
require.NoError(t, err)
require.Equal(t, []int{0, 1, 2, 3, 4}, messageOrdinals(all))
activity, err := store.GetSessionActivity(ctx, fixture.alphaID)
require.NoError(t, err)
require.NotNil(t, activity)
require.Equal(t, 5, activity.TotalMessages)
require.NotEmpty(t, activity.Buckets)
timing, err := store.GetSessionTiming(ctx, fixture.alphaID)
require.NoError(t, err)
require.NotNil(t, timing)
}
func contractSearchModesAndSecretFindings(
t *testing.T,
store Store,
fixture storeContractFixture,
_ storeContractBackend,
) {
t.Helper()
ctx := context.Background()
if store.HasFTS() {
search, err := store.Search(ctx, SearchFilter{
Query: "duckdb",
Limit: 5,
})
require.NoError(t, err)
require.NotEmpty(t, search.Results)
require.Equal(t, fixture.alphaID, search.Results[0].SessionID)
nameSearch, err := store.Search(ctx, SearchFilter{
Query: "Old Contract Name",
Limit: 5,
})
require.NoError(t, err)
require.Equal(t, []string{fixture.oldID}, searchResultIDs(nameSearch.Results))
}
substring, err := store.SearchContent(ctx, ContentSearchFilter{
Pattern: "contract-input",
Mode: "substring",
Sources: []string{"tool_input"},
IncludeOneShot: true,
Limit: 5,
})
require.NoError(t, err)
require.Equal(t, []string{"tool_input"}, contentLocations(substring.Matches))
regex, err := store.SearchContent(ctx, ContentSearchFilter{
Pattern: `trend\s+trend`,
Mode: "regex",
Sources: []string{"messages"},
IncludeOneShot: true,
Limit: 5,
})
require.NoError(t, err)
require.Equal(t, []string{fixture.gammaID}, contentSessionIDs(regex.Matches))
if store.HasFTS() {
fts, err := store.SearchContent(ctx, ContentSearchFilter{
Pattern: "analytics",
Mode: "fts",
Sources: []string{"messages"},
IncludeOneShot: true,
Limit: 5,
})
require.NoError(t, err)
require.Contains(t, contentSessionIDs(fts.Matches), fixture.alphaID)
}
findings, err := store.ListSecretFindings(ctx, SecretFindingFilter{
Project: "alpha",
RulesVersions: []string{"contract-rules-v1"},
Limit: 10,
})
require.NoError(t, err)
require.Len(t, findings.Findings, 2)
require.Equal(t, fixture.alphaID, findings.Findings[0].SessionID)
var messageSecretSource string
for _, finding := range findings.Findings {
source, ok, err := store.SecretFindingSource(ctx, finding.SecretFinding)
require.NoError(t, err)
require.True(t, ok)
if finding.LocationKind == "message" {
messageSecretSource = source
}
}
require.Contains(t, messageSecretSource, "contract secret")
secretSessions, err := store.ListSessions(ctx, SessionFilter{
HasSecret: true,
SecretsRulesVersions: []string{"contract-rules-v1"},
Limit: 10,
})
require.NoError(t, err)
require.Equal(t, []string{fixture.alphaID}, sessionIDs(secretSessions.Sessions))
}
func contractStarsAndPins(
t *testing.T,
store Store,
fixture storeContractFixture,
backend storeContractBackend,
) {
t.Helper()
if !backend.supportsLocalWrites {
_, err := store.StarSession(fixture.alphaID)
require.ErrorIs(t, err, ErrReadOnly)
_, err = store.PinMessage(fixture.alphaID, 1, nil)
require.ErrorIs(t, err, ErrReadOnly)
return
}
ctx := context.Background()
ok, err := store.StarSession(fixture.alphaID)
require.NoError(t, err)
require.True(t, ok)
require.NoError(t, store.BulkStarSessions([]string{fixture.gammaID, "missing-session"}))
stars, err := store.ListStarredSessionIDs(ctx)
require.NoError(t, err)
require.ElementsMatch(t, []string{fixture.alphaID, fixture.gammaID}, stars)
require.NoError(t, store.UnstarSession(fixture.gammaID))
stars, err = store.ListStarredSessionIDs(ctx)
require.NoError(t, err)
require.Equal(t, []string{fixture.alphaID}, stars)
msgs, err := store.GetAllMessages(ctx, fixture.alphaID)
require.NoError(t, err)
require.Greater(t, len(msgs), 1)
note := "contract pin"
pinID, err := store.PinMessage(fixture.alphaID, msgs[1].ID, &note)
require.NoError(t, err)
require.Positive(t, pinID)
sessionPins, err := store.ListPinnedMessages(ctx, fixture.alphaID, "")
require.NoError(t, err)
require.Len(t, sessionPins, 1)
require.Equal(t, 1, sessionPins[0].Ordinal)
require.Equal(t, note, *sessionPins[0].Note)
allPins, err := store.ListPinnedMessages(ctx, "", "alpha")
require.NoError(t, err)
require.Len(t, allPins, 1)
require.Equal(t, fixture.alphaID, allPins[0].SessionID)
require.NotNil(t, allPins[0].Content)
require.NoError(t, store.UnpinMessage(fixture.alphaID, msgs[1].ID))
sessionPins, err = store.ListPinnedMessages(ctx, fixture.alphaID, "")
require.NoError(t, err)
require.Empty(t, sessionPins)
}
func contractAnalyticsTrendsAndUsage(
t *testing.T,
store Store,
fixture storeContractFixture,
_ storeContractBackend,
) {
t.Helper()
ctx := context.Background()
summary, err := store.GetAnalyticsSummary(ctx, AnalyticsFilter{
From: "2026-01-09",
To: "2026-01-12",
Timezone: "UTC",
})
require.NoError(t, err)
// The subagent child (contract-alpha-child) is now counted in the
// summary aggregate: +1 claude session, +1 message, +70 output
// tokens. The fork/automated/deleted rows stay excluded as before.
require.Equal(t, 6, summary.TotalSessions)
require.Equal(t, 14, summary.TotalMessages)
require.Equal(t, 510, summary.TotalOutputTokens)
require.Equal(t, 5, summary.TokenReportingSessions)
require.Equal(t, 3, summary.ActiveProjects)
require.Equal(t, 4, summary.ActiveDays)
require.Equal(t, 3, summary.Agents["claude"].Sessions)
noAutomation, err := store.GetAnalyticsSummary(ctx, AnalyticsFilter{
From: "2026-01-09",
To: "2026-01-12",
ExcludeAutomated: true,
})
require.NoError(t, err)
require.Equal(t, 5, noAutomation.TotalSessions)
// With one-shot exclusion on (the summary endpoint default), the
// one-shot subagent child must still be counted (workflow subagents
// are inherently one-shot) while one-shot root sessions drop. This
// exercises OneShotExclusionSQL on every backend, including PG.
//
// Kept rows: alpha (3 user msgs, 320 tok), gamma (2 user, 60 tok),
// automated (roborev, kept via is_automated, 0 tok), and the subagent
// child (1 user, 70 tok, kept via the subagent exemption). Dropped:
// beta and old (one-shot roots). Exact totals are asserted so a
// regression that re-excludes the subagent — which would give 3
// sessions / 380 tokens — fails loudly rather than passing a looser
// bound that the surviving root sessions already satisfy.
oneShotExcluded, err := store.GetAnalyticsSummary(ctx, AnalyticsFilter{
From: "2026-01-09",
To: "2026-01-12",
Timezone: "UTC",
ExcludeOneShot: true,
})
require.NoError(t, err)
require.Equal(t, 4, oneShotExcluded.TotalSessions,
"one-shot roots drop; subagent child stays")
require.Equal(t, 450, oneShotExcluded.TotalOutputTokens,
"includes the subagent child's 70 tokens (380 would mean dropped)")
require.Equal(t, 3, oneShotExcluded.TokenReportingSessions)
// Distribution surfaces stay root-only: the subagent child must NOT
// be counted in session-shape, so its short duration cannot skew the
// distributions. It is one fewer session than the summary aggregate.
shape, err := store.GetAnalyticsSessionShape(ctx, AnalyticsFilter{
From: "2026-01-09",
To: "2026-01-12",
Timezone: "UTC",
})
require.NoError(t, err)
require.Equal(t, 5, shape.Count)
activity, err := store.GetAnalyticsActivity(ctx, AnalyticsFilter{
From: "2026-01-09",
To: "2026-01-12",
Timezone: "UTC",
}, "day")
require.NoError(t, err)
require.Len(t, activity.Series, 4)
tools, err := store.GetAnalyticsTools(ctx, AnalyticsFilter{
From: "2026-01-10",
To: "2026-01-10",
})
require.NoError(t, err)
require.Equal(t, 1, tools.TotalCalls)
require.NotEmpty(t, tools.ByCategory)
require.Equal(t, "search", tools.ByCategory[0].Category)
skills, err := store.GetAnalyticsSkills(ctx, AnalyticsFilter{
From: "2026-01-10",
To: "2026-01-10",
}, "week")
require.NoError(t, err)
require.Equal(t, 1, skills.TotalSkillCalls)
require.Equal(t, 1, skills.DistinctSkills)
require.NotEmpty(t, skills.BySkill)
require.Equal(t, "contract-search", skills.BySkill[0].SkillName)
trends, err := store.GetTrendsTerms(ctx, AnalyticsFilter{
From: "2026-01-09",
To: "2026-01-12",
Timezone: "UTC",
}, []TrendTermInput{
{Term: "trend", Variants: []string{"trend"}, Matchers: []string{"trend"}},
}, "day")
require.NoError(t, err)
require.Equal(t, "day", trends.Granularity)
require.Equal(t, 2, trends.Series[0].Total)
// Trends is a distribution surface (root-only), so the subagent
// child's message is not counted here.
require.Equal(t, 13, trends.MessageCount)
daily, err := store.GetDailyUsage(ctx, UsageFilter{
From: "2026-01-10",
To: "2026-01-12",
Timezone: "UTC",
Breakdowns: true,
})
require.NoError(t, err)
require.GreaterOrEqual(t, len(daily.Daily), 2)
require.Equal(t, 315, daily.Totals.InputTokens)
require.Equal(t, 130, daily.Totals.OutputTokens)
require.InDelta(t, 0.002435, daily.Totals.TotalCost, 0.00001)
top, err := store.GetTopSessionsByCost(ctx, UsageFilter{
From: "2026-01-10",
To: "2026-01-12",
}, 3)
require.NoError(t, err)
require.NotEmpty(t, top)
require.Equal(t, fixture.alphaID, top[0].SessionID)
counts, err := store.GetUsageSessionCounts(ctx, UsageFilter{
From: "2026-01-10",
To: "2026-01-12",
})
require.NoError(t, err)
require.Equal(t, 3, counts.Total)
require.Equal(t, 2, counts.ByProject["alpha"])
usage, err := store.GetSessionUsage(ctx, fixture.alphaID, true)
require.NoError(t, err)
require.NotNil(t, usage)
require.True(t, usage.HasTokenData)
require.True(t, usage.HasCost)
require.Equal(t, []string{"claude-sonnet-contract"}, usage.Models)
require.InDelta(t, 0.002355, usage.CostUSD, 0.00001)
}
func contractLocalOnlyMethods(
t *testing.T,
store Store,
fixture storeContractFixture,
backend storeContractBackend,
) {
t.Helper()
ctx := context.Background()
if !backend.supportsLocalWrites {
require.True(t, store.ReadOnly())
ignoredName := "ignored"
requireReadOnly(t, store.RenameSession(fixture.alphaID, &ignoredName))
requireReadOnly(t, store.SoftDeleteSession(fixture.alphaID))
_, err := store.RestoreSession(fixture.deletedID)
requireReadOnly(t, err)
_, err = store.DeleteSessionIfTrashed(fixture.deletedID)
requireReadOnly(t, err)
_, err = store.EmptyTrash()
requireReadOnly(t, err)
_, err = store.InsertInsight(Insight{})
requireReadOnly(t, err)
requireReadOnly(t, store.DeleteInsight(1))
_, err = store.RecordRecallQueryEvent(ctx, RecallQueryEvent{
Surface: RecallQuerySurfaceQuery,
})
requireReadOnly(t, err)
return
}
require.False(t, store.ReadOnly())
renamed := "Renamed contract session"
require.NoError(t, store.RenameSession(fixture.betaID, &renamed))
beta, err := store.GetSession(ctx, fixture.betaID)
require.NoError(t, err)
require.NotNil(t, beta)
require.Equal(t, renamed, *beta.DisplayName)
require.NoError(t, store.SoftDeleteSession(fixture.betaID))
beta, err = store.GetSession(ctx, fixture.betaID)
require.NoError(t, err)
require.Nil(t, beta)
trashed, err := store.ListTrashedSessions(ctx)
require.NoError(t, err)
require.Contains(t, sessionIDs(trashed), fixture.betaID)
restored, err := store.RestoreSession(fixture.betaID)
require.NoError(t, err)
require.EqualValues(t, 1, restored)
deleted, err := store.DeleteSessionIfTrashed(fixture.deletedID)
require.NoError(t, err)
require.EqualValues(t, 1, deleted)
emptyCount, err := store.EmptyTrash()
require.NoError(t, err)
require.Zero(t, emptyCount)
insightID, err := store.InsertInsight(Insight{
Type: "contract",
DateFrom: "2026-01-12",
DateTo: "2026-01-12",
Agent: "claude",
Content: "Backend contract test insight",
})
require.NoError(t, err)
require.Positive(t, insightID)
insight, err := store.GetInsight(ctx, insightID)
require.NoError(t, err)
require.NotNil(t, insight)
require.Equal(t, "Backend contract test insight", insight.Content)
list, err := store.ListInsights(ctx, InsightFilter{})
require.NoError(t, err)
require.Len(t, list, 1)
require.NoError(t, store.DeleteInsight(insightID))
queryID, err := store.RecordRecallQueryEvent(ctx, RecallQueryEvent{
Query: "store contract recall query",
Surface: RecallQuerySurfaceQuery,
})
require.NoError(t, err)
require.NotEmpty(t, queryID)
}
func TestStoreContractGetUsageMatchingSessionCountCountsCopilotSessionsWithoutUsageRows(
t *testing.T,
) {
store, ok := openStoreContractSQLiteFixtureDB(t).(*DB)
require.True(t, ok, "sqlite fixture should expose *DB")
ctx := context.Background()
insertSession(t, store, "contract-copilot-empty", "alpha", func(s *Session) {
ts := "2026-01-12T16:00:00Z"
s.Agent = "copilot"
s.StartedAt = &ts
s.EndedAt = &ts
})
insertMessages(t, store, Message{
SessionID: "contract-copilot-empty",
Ordinal: 0,
Role: "assistant",
Timestamp: "2026-01-12T16:00:00Z",
Model: "gpt-5.3-codex",
TokenUsage: nil,
})
count, err := store.GetUsageMatchingSessionCount(ctx, UsageFilter{
From: "2026-01-12",
To: "2026-01-12",
Timezone: "UTC",
Agent: "copilot",
})
require.NoError(t, err)
require.Equal(t, 1, count)
}
func TestStoreContractGetUsageMatchingSessionCountCountsCopilotSessionByMessageTimestampOutsideSessionWindow(
t *testing.T,
) {
store, ok := openStoreContractSQLiteFixtureDB(t).(*DB)
require.True(t, ok, "sqlite fixture should expose *DB")
ctx := context.Background()
insertSession(t, store, "contract-copilot-late-message", "alpha", func(s *Session) {
ts := "2026-02-08T10:00:00Z"
s.Agent = "copilot"
s.StartedAt = &ts
s.EndedAt = &ts
})
insertMessages(t, store, Message{
SessionID: "contract-copilot-late-message",
Ordinal: 0,
Role: "assistant",
Timestamp: "2026-02-10T12:00:00Z",
Model: "gpt-5.3-codex",
TokenUsage: nil,
})
insertSession(t, store, "contract-copilot-out-of-range", "alpha", func(s *Session) {
ts := "2026-02-08T10:00:00Z"
s.Agent = "copilot"
s.StartedAt = &ts
s.EndedAt = &ts
})
insertMessages(t, store, Message{
SessionID: "contract-copilot-out-of-range",
Ordinal: 0,
Role: "assistant",
Timestamp: "2026-02-08T10:00:00Z",
Model: "gpt-5.3-codex",
TokenUsage: nil,
})
count, err := store.GetUsageMatchingSessionCount(ctx, UsageFilter{
From: "2026-02-10",
To: "2026-02-10",
Timezone: "UTC",
Agent: "copilot",
})
require.NoError(t, err)
require.Equal(t, 1, count)
}
func seedStoreContractSQLite(
t *testing.T,
store Store,
) storeContractFixture {
t.Helper()
pricingStore, ok := store.(interface {
UpsertModelPricing([]ModelPricing) error
})
require.True(t, ok, "sqlite contract seed requires pricing writer")
require.NoError(t, pricingStore.UpsertModelPricing([]ModelPricing{
{
ModelPattern: "claude-sonnet-contract",
InputPerMTok: 3,
OutputPerMTok: 15,
CacheCreationPerMTok: 3.75,
CacheReadPerMTok: 0.3,
},
{
ModelPattern: "codex-mini-contract",
InputPerMTok: 1,
OutputPerMTok: 5,
CacheCreationPerMTok: 1,
CacheReadPerMTok: 0.1,
},
}))
fixture := storeContractFixture{
alphaID: "contract-alpha",
betaID: "contract-beta",
gammaID: "contract-gamma",
oldID: "contract-old",
automatedID: "contract-automated",
childID: "contract-alpha-child",
deletedID: "contract-deleted",
}
alphaSecret := "The contract secret is sk-contract-alpha-123."
alphaSecretStart := len("The contract secret is ")
toolInput := `{"query":"contract-input duckdb parity secret"}`
toolSecretStart := len(`{"query":"`)
callIndex := 0
alphaHealthScore := 88
alphaHealthGrade := "A"
gammaHealthScore := 55
gammaHealthGrade := "C"
writes := []SessionBatchWrite{
contractSessionWrite(contractSessionSeed{
id: fixture.alphaID,
project: "alpha",
machine: "mac",
agent: "claude",
firstMessage: "Alpha parity investigation starts with duckdb parity keyword.",
sessionName: "Alpha DuckDB parity",
startedAt: "2026-01-10T12:00:00Z",
endedAt: "2026-01-10T12:06:00Z",
userMessages: 3,
outputTokens: 320,
peakTokens: 900,
messages: []Message{
contractMessage(fixture.alphaID, 0, "user", "Alpha parity investigation starts with duckdb parity keyword.", "2026-01-10T12:00:00Z"),
contractMessage(fixture.alphaID, 1, "assistant", "Running contract search for backend parity.", "2026-01-10T12:01:00Z",
withModel("claude-sonnet-contract"),
withTokenUsage(`{"input_tokens":200,"output_tokens":60,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}`),
withToolCall(ToolCall{
ToolName: "search",
Category: "search",
SkillName: "contract-search",
ToolUseID: "tool-alpha-1",
InputJSON: toolInput,
ResultContent: "legacy result mentions parity",
ResultEvents: []ToolResultEvent{
{
Source: "tool",
Status: "complete",
Content: "tool stream found contract parity event",
Timestamp: "2026-01-10T12:01:30Z",
},
},
})),
contractMessage(fixture.alphaID, 2, "user", "Please add backend contract tests for cursor pagination and stars.", "2026-01-10T12:02:00Z"),
contractMessage(fixture.alphaID, 3, "assistant", "Implemented analytics summary with usage rollup.", "2026-01-10T12:04:00Z",
withModel("claude-sonnet-contract"),
withTokenUsage(`{"input_tokens":100,"output_tokens":30,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}`)),
contractMessage(fixture.alphaID, 4, "user", alphaSecret, "2026-01-10T12:05:00Z"),
},
usageEvents: []UsageEvent{
{
Source: "hermes",
Model: "claude-sonnet-contract",
InputTokens: 10,
OutputTokens: 5,
OccurredAt: "2026-01-10T12:05:30Z",
DedupKey: "alpha-hermes",
},
},
signals: SessionSignalUpdate{
ToolFailureSignalCount: 1,
ToolRetryCount: 1,
Outcome: "success",
OutcomeConfidence: "high",
EndedWithRole: "user",
HealthScore: &alphaHealthScore,
HealthGrade: &alphaHealthGrade,
HasToolCalls: true,
HasContextData: true,
SecretLeakCount: 2,
SecretsRulesVersion: "contract-rules-v1",
},
findings: []SecretFinding{
{
SessionID: fixture.alphaID,
RuleName: "contract_secret",
Confidence: "definite",
LocationKind: "message",
MessageOrdinal: 4,
MatchStart: alphaSecretStart,
MatchEnd: alphaSecretStart + len("sk-contract-alpha-123"),
MatchIndex: 0,
RedactedMatch: "sk-contract-alpha-...",
RulesVersion: "contract-rules-v1",
},
{
SessionID: fixture.alphaID,
RuleName: "contract_secret",
Confidence: "candidate",
LocationKind: "tool_input",
MessageOrdinal: 1,
CallIndex: &callIndex,
MatchStart: toolSecretStart,
MatchEnd: toolSecretStart + len("contract-input"),
MatchIndex: 1,
RedactedMatch: "contract-input...",
RulesVersion: "contract-rules-v1",
},
},
}),
contractSessionWrite(contractSessionSeed{
id: fixture.betaID,
project: "beta",
machine: "linux",
agent: "codex",
firstMessage: "Beta recency search target.",
startedAt: "2026-01-11T09:00:00Z",
endedAt: "2026-01-11T09:05:00Z",
userMessages: 1,
outputTokens: 40,
peakTokens: 120,
messages: []Message{
contractMessage(fixture.betaID, 0, "user", "Beta recency search target.", "2026-01-11T09:00:00Z"),
contractMessage(fixture.betaID, 1, "assistant", "One-shot beta answer.", "2026-01-11T09:01:00Z",
withModel("codex-mini-contract"),
withTokenUsage(`{"input_tokens":5,"output_tokens":5}`)),
},
}),
contractSessionWrite(contractSessionSeed{
id: fixture.gammaID,
project: "alpha",
machine: "mac",
agent: "codex",
firstMessage: "Gamma trend request.",
startedAt: "2026-01-12T10:00:00Z",
endedAt: "2026-01-12T10:10:00Z",
userMessages: 2,
outputTokens: 60,
peakTokens: 300,
messages: []Message{
contractMessage(fixture.gammaID, 0, "user", "Gamma regex target mentions trend trend.", "2026-01-12T10:00:00Z"),
contractMessage(fixture.gammaID, 1, "assistant", "Gamma answer uses codex usage.", "2026-01-12T10:02:00Z",
withModel("codex-mini-contract"),
withTokenUsage(`{"input_tokens":0,"output_tokens":0}`)),
contractMessage(fixture.gammaID, 2, "user", "Another gamma request.", "2026-01-12T10:03:00Z"),
},
usageEvents: []UsageEvent{
{
Source: "hermes",
Model: "codex-mini-contract",
InputTokens: 0,
OutputTokens: 30,
CostUSD: floatPtr(0.00005),
CostStatus: "reported",
CostSource: "fixture",
OccurredAt: "2026-01-12T10:04:00Z",
DedupKey: "gamma-hermes",
},
},
signals: SessionSignalUpdate{
Outcome: "failed",
OutcomeConfidence: "medium",
EndedWithRole: "user",
HealthScore: &gammaHealthScore,
HealthGrade: &gammaHealthGrade,
},
}),
contractSessionWrite(contractSessionSeed{
id: fixture.automatedID,
project: "review",
machine: "mac",
agent: "roborev",
firstMessage: "You are a code reviewer. Review the code changes introduced by commit abc123.",
startedAt: "2026-01-11T13:00:00Z",
endedAt: "2026-01-11T13:01:00Z",
userMessages: 1,
messages: []Message{
contractMessage(fixture.automatedID, 0, "user", "You are a code reviewer. Review the code changes introduced by commit abc123.", "2026-01-11T13:00:00Z"),
},
}),
contractSessionWrite(contractSessionSeed{
id: fixture.oldID,
project: "alpha",
machine: "linux",
agent: "claude",
firstMessage: "Old contract first message.",
sessionName: "Old Contract Name",
startedAt: "2026-01-09T08:00:00Z",
endedAt: "2026-01-09T08:03:00Z",
userMessages: 1,
outputTokens: 20,
peakTokens: 100,
messages: []Message{
contractMessage(fixture.oldID, 0, "user", "Old contract first message.", "2026-01-09T08:00:00Z"),
contractMessage(fixture.oldID, 1, "assistant", "Old contract answer.", "2026-01-09T08:01:00Z",
withModel("claude-sonnet-contract"),
withTokenUsage(`{"input_tokens":0,"output_tokens":0}`)),
},
}),
contractSessionWrite(contractSessionSeed{
id: fixture.childID,
project: "alpha",
machine: "mac",
agent: "claude",
firstMessage: "Child session spawned from alpha.",
startedAt: "2026-01-10T12:02:30Z",
endedAt: "2026-01-10T12:03:00Z",
userMessages: 1,
outputTokens: 70,
parentID: fixture.alphaID,
relationship: "subagent",
messages: []Message{
contractMessage(fixture.childID, 0, "assistant", "Child subagent result.", "2026-01-10T12:02:30Z"),
},
}),
contractSessionWrite(contractSessionSeed{
id: fixture.deletedID,
project: "alpha",
machine: "mac",
agent: "claude",
firstMessage: "Deleted session should stay hidden.",
startedAt: "2026-01-12T15:00:00Z",
endedAt: "2026-01-12T15:01:00Z",
userMessages: 1,
messages: []Message{
contractMessage(fixture.deletedID, 0, "user", "Deleted session should stay hidden.", "2026-01-12T15:00:00Z"),
},
}),
}
result, err := store.WriteSessionBatchAtomic(writes)
require.NoError(t, err)
require.Equal(t, len(writes), result.WrittenSessions)
require.NoError(t, store.SoftDeleteSession(fixture.deletedID))
return fixture
}
type contractSessionSeed struct {
id string
project string
machine string
agent string
firstMessage string
sessionName string
startedAt string
endedAt string
userMessages int
outputTokens int
peakTokens int
parentID string
relationship string
messages []Message
usageEvents []UsageEvent
signals SessionSignalUpdate
findings []SecretFinding
}
func contractSessionWrite(seed contractSessionSeed) SessionBatchWrite {
relationship := seed.relationship
if relationship == "" {
relationship = "root"
}
session := Session{
ID: seed.id,
Project: seed.project,
Machine: seed.machine,
Agent: seed.agent,
FirstMessage: &seed.firstMessage,
StartedAt: &seed.startedAt,
EndedAt: &seed.endedAt,
MessageCount: len(seed.messages),
UserMessageCount: seed.userMessages,
RelationshipType: relationship,
TotalOutputTokens: seed.outputTokens,
PeakContextTokens: seed.peakTokens,
HasTotalOutputTokens: seed.outputTokens > 0,
HasPeakContextTokens: seed.peakTokens > 0,
}
if seed.sessionName != "" {
session.SessionName = &seed.sessionName
}
if seed.parentID != "" {
session.ParentSessionID = &seed.parentID
}
return SessionBatchWrite{
Session: session,
Messages: seed.messages,
UsageEvents: seed.usageEvents,
Signals: seed.signals,
Findings: seed.findings,
DataVersion: 1,
ReplaceMessages: true,
}
}
type messageOption func(*Message)
func contractMessage(
sessionID string,
ordinal int,
role string,
content string,
timestamp string,
opts ...messageOption,
) Message {
msg := Message{
SessionID: sessionID,
Ordinal: ordinal,
Role: role,
Content: content,
Timestamp: timestamp,
ContentLength: len(content),
}
for _, opt := range opts {
opt(&msg)
}
return msg
}
func withModel(model string) messageOption {
return func(msg *Message) {
msg.Model = model
}
}
func withTokenUsage(raw string) messageOption {
return func(msg *Message) {
msg.TokenUsage = []byte(raw)
}
}
func withToolCall(call ToolCall) messageOption {
return func(msg *Message) {
msg.HasToolUse = true
msg.ToolCalls = append(msg.ToolCalls, call)
}
}
func sessionIDs(sessions []Session) []string {
ids := make([]string, len(sessions))
for i, session := range sessions {
ids[i] = session.ID
}
return ids
}
func sidebarSessionIDs(sessions []SidebarSessionIndexRow) []string {
ids := make([]string, len(sessions))
for i, session := range sessions {
ids[i] = session.ID
}
return ids
}
func messageOrdinals(messages []Message) []int {
ordinals := make([]int, len(messages))
for i, msg := range messages {
ordinals[i] = msg.Ordinal
}
return ordinals
}
func searchResultIDs(results []SearchResult) []string {
ids := make([]string, len(results))
for i, result := range results {
ids[i] = result.SessionID
}
return ids
}
func contentSessionIDs(matches []ContentMatch) []string {
ids := make([]string, len(matches))
for i, match := range matches {
ids[i] = match.SessionID
}
return ids
}
func contentLocations(matches []ContentMatch) []string {
locations := make([]string, len(matches))
for i, match := range matches {
locations[i] = match.Location
}
return locations
}
func projectNames(projects []ProjectInfo) []string {
names := make([]string, len(projects))
for i, project := range projects {
names[i] = project.Name
}
return names
}
func agentNames(agents []AgentInfo) []string {
names := make([]string, len(agents))
for i, agent := range agents {
names[i] = agent.Name
}
return names
}
func requireReadOnly(t *testing.T, err error) {
t.Helper()
require.Error(t, err)
require.True(t, errors.Is(err, ErrReadOnly), "expected ErrReadOnly, got %v", err)
}
func floatPtr(v float64) *float64 {
if math.IsNaN(v) {
return nil
}
return &v
}
func TestStoreContractBackendsAreRegisteredExplicitly(t *testing.T) {
backends := storeContractBackends()
require.NotEmpty(t, backends)
names := make([]string, 0, len(backends))
for _, backend := range backends {
names = append(names, backend.name)
assert.NotNil(t, backend.open)
assert.NotNil(t, backend.seed)
}
require.True(t, slices.Contains(names, "sqlite"))
}