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
1190 lines
36 KiB
Go
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, ¬e)
|
|
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"))
|
|
}
|