Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

1107 lines
40 KiB
Go

//go:build pgtest
package postgres
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
)
func prepareUsageSchema(
t *testing.T, schema string,
) (string, *Store) {
t.Helper()
pgURL := testPGURL(t)
pg, err := Open(pgURL, schema, true)
require.NoError(t, err, "Open")
t.Cleanup(func() { _ = pg.Close() })
ctx := context.Background()
_, err = pg.Exec(`DROP SCHEMA IF EXISTS ` + schema + ` CASCADE`)
require.NoError(t, err, "drop schema")
require.NoError(t, EnsureSchema(ctx, pg, schema), "EnsureSchema")
store, err := NewStore(pgURL, schema, true)
require.NoError(t, err, "NewStore")
t.Cleanup(func() { _ = store.Close() })
return pgURL, store
}
func TestStoreGetDailyUsageUsesFallbackPricing(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_fallback_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES (
'usage-fallback-001', 'test-machine', 'proj', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp,
content_length, model, token_usage
) VALUES (
'usage-fallback-001', 0, 'assistant', 'hi',
'2026-03-12T10:00:00Z'::timestamptz, 2,
'claude-sonnet-4-20250514',
'{"input_tokens":1000000}'
)`)
require.NoError(t, err, "insert message")
result, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
})
require.NoError(t, err, "GetDailyUsage")
assert.Equal(t, 3.0, result.Totals.TotalCost)
assert.Len(t, result.Daily, 1)
}
func TestStoreGetDailyUsageWithBreakdowns(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_breakdown_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES
('test-model-a', 1, 2, 3, 0.5, 'seed'),
('test-model-b', 2, 4, 0, 0, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('usage-breakdown-001', 'test-machine', 'proj-a', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('usage-breakdown-002', 'test-machine', 'proj-b', 'codex',
'2026-03-12T11:00:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('usage-breakdown-001', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'test-model-a',
'{"input_tokens":1000000,"output_tokens":500000,"cache_creation_input_tokens":250000,"cache_read_input_tokens":250000}'),
('usage-breakdown-002', 0, 'assistant', 'two',
'2026-03-12T11:00:00Z'::timestamptz, 3,
'test-model-b',
'{"input_tokens":500000,"output_tokens":250000}')`)
require.NoError(t, err, "insert messages")
result, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
Breakdowns: true,
})
require.NoError(t, err, "GetDailyUsage")
require.Len(t, result.Daily, 1)
day := result.Daily[0]
assert.Equal(t, 1500000, day.InputTokens)
assert.Equal(t, 750000, day.OutputTokens)
assert.Len(t, day.ProjectBreakdowns, 2)
assert.Len(t, day.AgentBreakdowns, 2)
assert.Len(t, day.ModelBreakdowns, 2)
assert.Greater(t, day.TotalCost, 0.0)
noCounts, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
SkipSessionCounts: true,
})
require.NoError(t, err, "GetDailyUsage skip session counts")
assert.Equal(t, result.Totals.InputTokens, noCounts.Totals.InputTokens)
assert.Zero(t, noCounts.SessionCounts.Total)
assert.Nil(t, noCounts.SessionCounts.ByProject)
assert.Nil(t, noCounts.SessionCounts.ByAgent)
}
func TestStoreGetDailyUsageDedupesBySourceUUIDWhenClaudePairIncomplete(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_source_uuid_daily_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('test-model-source-daily', 1, 2, 3, 0.5, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('usage-source-daily-001', 'test-machine', 'proj-a', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('usage-source-daily-002', 'test-machine', 'proj-b', 'claude',
'2026-03-12T10:01:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage, claude_message_id, claude_request_id, source_uuid
) VALUES
('usage-source-daily-001', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'test-model-source-daily',
'{"input_tokens":1000000,"output_tokens":500000,"cache_creation_input_tokens":250000,"cache_read_input_tokens":250000}',
'msg-1', '', 'source-1'),
('usage-source-daily-002', 0, 'assistant', 'two',
'2026-03-12T10:01:00Z'::timestamptz, 3,
'test-model-source-daily',
'{"input_tokens":1000000,"output_tokens":500000,"cache_creation_input_tokens":250000,"cache_read_input_tokens":250000}',
'msg-1', '', 'source-1')`)
require.NoError(t, err, "insert messages")
result, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
})
require.NoError(t, err, "GetDailyUsage")
require.Len(t, result.Daily, 1)
assert.Equal(t, 1000000, result.Daily[0].InputTokens)
assert.Equal(t, 500000, result.Daily[0].OutputTokens)
}
func TestStoreGetSessionUsagePricedModel(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_session_usage_priced_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('gpt-5.1', 3, 15, 3.75, 0.30, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count,
total_output_tokens, peak_context_tokens,
has_total_output_tokens, has_peak_context_tokens
) VALUES (
'codex:usage-priced', 'test-machine', 'my-project', 'codex',
'2026-03-12T10:00:00Z'::timestamptz, 2, 1,
1234, 56789, true, true
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES (
'codex:usage-priced', 1, 'assistant', 'done',
'2026-03-12T10:01:00Z'::timestamptz, 4,
'gpt-5.1',
'{"input_tokens":1000,"output_tokens":500,"cache_creation_input_tokens":200,"cache_read_input_tokens":300}'
)`)
require.NoError(t, err, "insert message")
got, err := store.GetSessionUsage(ctx, "codex:usage-priced", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, got, "GetSessionUsage result")
assert.Equal(t, "codex:usage-priced", got.SessionID)
assert.Equal(t, "codex", got.Agent)
assert.Equal(t, "my-project", got.Project)
assert.Equal(t, 1234, got.TotalOutputTokens)
assert.Equal(t, 56789, got.PeakContextTokens)
assert.True(t, got.HasTokenData)
assert.True(t, got.HasCost)
assert.InDelta(t, 0.01134, got.CostUSD, 1e-9)
assert.Equal(t, []string{"gpt-5.1"}, got.Models)
assert.Empty(t, got.UnpricedModels)
require.Len(t, got.Breakdown, 1, "Breakdown")
entry := got.Breakdown[0]
assert.Equal(t, 1, entry.Ordinal)
require.NotNil(t, entry.MessageOrdinal)
assert.Equal(t, 1, *entry.MessageOrdinal)
assert.Equal(t, "message", entry.Source)
assert.Equal(t, "Prompt 2", entry.Label)
assert.Equal(t, "2026-03-12T10:01:00Z", entry.Timestamp)
assert.Equal(t, "gpt-5.1", entry.Model)
assert.Equal(t, 1000, entry.InputTokens)
assert.Equal(t, 500, entry.OutputTokens)
assert.Equal(t, 200, entry.CacheCreationInputTokens)
assert.Equal(t, 300, entry.CacheReadInputTokens)
assert.True(t, entry.HasCost)
assert.InDelta(t, 0.01134, entry.CostUSD, 1e-9)
}
func TestStoreGetSessionUsageDedupesSourceUUIDWhenClaudePairIncomplete(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_session_usage_source_uuid_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('claude-opus-4-6', 5, 25, 6.25, 0.5, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES (
'claude:usage-source', 'test-machine', 'proj', 'claude-code',
'2026-03-12T10:00:00Z'::timestamptz, 2, 1
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage, claude_message_id, claude_request_id, source_uuid
) VALUES
('claude:usage-source', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'claude-opus-4-6', '{"input_tokens":1000,"output_tokens":500}',
'msg-1', '', 'source-1'),
('claude:usage-source', 1, 'assistant', 'two',
'2026-03-12T10:01:00Z'::timestamptz, 3,
'claude-opus-4-6', '{"input_tokens":1000,"output_tokens":500}',
'msg-1', '', 'source-1')`)
require.NoError(t, err, "insert messages")
got, err := store.GetSessionUsage(ctx, "claude:usage-source", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, got, "GetSessionUsage result")
assert.True(t, got.HasCost)
assert.InDelta(t, 0.0175, got.CostUSD, 1e-9)
assert.Equal(t, []string{"claude-opus-4-6"}, got.Models)
require.Len(t, got.Breakdown, 1, "Breakdown")
require.NotNil(t, got.Breakdown[0].MessageOrdinal)
assert.Equal(t, 0, *got.Breakdown[0].MessageOrdinal)
}
func TestStoreGetSessionUsageNoTokenRowsKeepsMetadata(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_session_usage_empty_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES (
'codex:usage-empty', 'test-machine', 'quiet-project', 'codex',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1
)`)
require.NoError(t, err, "insert session")
got, err := store.GetSessionUsage(ctx, "codex:usage-empty", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, got, "GetSessionUsage result")
assert.Equal(t, "codex:usage-empty", got.SessionID)
assert.Equal(t, "codex", got.Agent)
assert.Equal(t, "quiet-project", got.Project)
assert.False(t, got.HasTokenData)
assert.False(t, got.HasCost)
assert.Zero(t, got.CostUSD)
assert.Empty(t, got.Models)
assert.Empty(t, got.UnpricedModels)
assert.Empty(t, got.Breakdown)
}
func TestStoreGetSessionUsageNotFound(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_session_usage_missing_test")
got, err := store.GetSessionUsage(context.Background(), "missing", true)
require.NoError(t, err, "GetSessionUsage")
assert.Nil(t, got, "GetSessionUsage")
}
func TestStoreGetTopSessionsByCostDedupesClaudeKeys(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_top_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('test-model-top', 1, 0, 0, 0, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('usage-top-001', 'test-machine', 'proj-a', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('usage-top-002', 'test-machine', 'proj-b', 'claude',
'2026-03-12T10:01:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage, claude_message_id, claude_request_id
) VALUES
('usage-top-001', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'test-model-top', '{"input_tokens":1000000}', 'msg-1', 'req-1'),
('usage-top-002', 0, 'assistant', 'two',
'2026-03-12T10:01:00Z'::timestamptz, 3,
'test-model-top', '{"input_tokens":1000000}', 'msg-1', 'req-1')`)
require.NoError(t, err, "insert messages")
top, err := store.GetTopSessionsByCost(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
}, 20)
require.NoError(t, err, "GetTopSessionsByCost")
require.Len(t, top, 1)
assert.Equal(t, "usage-top-001", top[0].SessionID)
}
func TestStoreGetTopSessionsByCostDedupesSourceUUIDFallback(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_top_source_uuid_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('test-model-top-source', 1, 0, 0, 0, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('usage-top-source-001', 'test-machine', 'proj-a', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('usage-top-source-002', 'test-machine', 'proj-b', 'claude',
'2026-03-12T10:01:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage, claude_message_id, claude_request_id, source_uuid
) VALUES
('usage-top-source-001', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'test-model-top-source', '{"input_tokens":1000000}', 'msg-1', '', 'source-1'),
('usage-top-source-002', 0, 'assistant', 'two',
'2026-03-12T10:01:00Z'::timestamptz, 3,
'test-model-top-source', '{"input_tokens":1000000}', 'msg-1', '', 'source-1')`)
require.NoError(t, err, "insert messages")
top, err := store.GetTopSessionsByCost(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
}, 20)
require.NoError(t, err, "GetTopSessionsByCost")
require.Len(t, top, 1)
assert.Equal(t, "usage-top-source-001", top[0].SessionID)
}
func TestStoreGetUsageSessionCountsDedupesClaudeKeys(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_counts_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('usage-counts-001', 'test-machine', 'proj-a', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('usage-counts-002', 'test-machine', 'proj-b', 'claude',
'2026-03-12T10:01:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage, claude_message_id, claude_request_id
) VALUES
('usage-counts-001', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'test-model-counts', '{"input_tokens":1}', 'msg-1', 'req-1'),
('usage-counts-002', 0, 'assistant', 'two',
'2026-03-12T10:01:00Z'::timestamptz, 3,
'test-model-counts', '{"input_tokens":1}', 'msg-1', 'req-1')`)
require.NoError(t, err, "insert messages")
counts, err := store.GetUsageSessionCounts(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
})
require.NoError(t, err, "GetUsageSessionCounts")
assert.Equal(t, 1, counts.Total)
assert.Equal(t, 1, counts.ByProject["proj-a"])
_, ok := counts.ByProject["proj-b"]
assert.False(t, ok, "proj-b should have been deduped out: %#v", counts.ByProject)
}
func TestStoreGetUsageMatchingSessionCountCountsCopilotSessionsWithoutUsageRows(
t *testing.T,
) {
_, store := prepareUsageSchema(t, "agentsview_usage_matching_sessions_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at, ended_at,
message_count, user_message_count
) VALUES
('copilot-empty', 'test-machine', 'proj-a', 'copilot',
'2026-03-12T10:00:00Z'::timestamptz,
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('claude-usage', 'test-machine', 'proj-a', 'claude',
'2026-03-12T11:00:00Z'::timestamptz,
'2026-03-12T11:00:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('copilot-empty', 0, 'assistant', 'copilot',
'2026-03-12T10:00:00Z'::timestamptz, 7,
'gpt-5.3-codex', ''),
('claude-usage', 0, 'assistant', 'claude',
'2026-03-12T11:00:00Z'::timestamptz, 6,
'claude-sonnet-4-20250514', '{"input_tokens":1}')`)
require.NoError(t, err, "insert messages")
count, err := store.GetUsageMatchingSessionCount(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
Agent: "copilot",
Model: "gpt-5.3-codex",
})
require.NoError(t, err, "GetUsageMatchingSessionCount")
assert.Equal(t, 1, count)
}
func TestStoreGetUsageMatchingSessionCountCountsCopilotSessionByMessageTimestampOutsideSessionWindow(
t *testing.T,
) {
_, store := prepareUsageSchema(t, "agentsview_usage_matching_sessions_late_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at, ended_at,
message_count, user_message_count
) VALUES
('copilot-late-message', 'test-machine', 'proj-a', 'copilot',
'2026-02-08T10:00:00Z'::timestamptz,
'2026-02-08T10:00:00Z'::timestamptz, 1, 1),
('copilot-out-of-range', 'test-machine', 'proj-a', 'copilot',
'2026-02-08T10:00:00Z'::timestamptz,
'2026-02-08T10:00:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('copilot-late-message', 0, 'assistant', 'copilot',
'2026-02-10T12:00:00Z'::timestamptz, 7,
'gpt-5.3-codex', ''),
('copilot-out-of-range', 0, 'assistant', 'copilot',
'2026-02-08T10:00:00Z'::timestamptz, 7,
'gpt-5.3-codex', '')`)
require.NoError(t, err, "insert messages")
count, err := store.GetUsageMatchingSessionCount(ctx, db.UsageFilter{
From: "2026-02-10",
To: "2026-02-10",
Timezone: "UTC",
Agent: "copilot",
})
require.NoError(t, err, "GetUsageMatchingSessionCount")
assert.Equal(t, 1, count)
}
// TestStoreGetUsageMatchingSessionCountModelFilterAppliesToBoundedRow
// guards against the model/exclude-model predicate matching session-wide
// instead of on the in-range message row: a session with an out-of-range
// message on the filtered model but an in-range message on a different
// model must not match a Model filter for the out-of-range model.
func TestStoreGetUsageMatchingSessionCountModelFilterAppliesToBoundedRow(
t *testing.T,
) {
_, store := prepareUsageSchema(t, "agentsview_usage_matching_sessions_model_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at, ended_at,
message_count, user_message_count
) VALUES
('copilot-mixed-model', 'test-machine', 'proj-a', 'copilot',
'2026-02-08T10:00:00Z'::timestamptz,
'2026-02-10T12:00:00Z'::timestamptz, 2, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('copilot-mixed-model', 0, 'assistant', 'copilot',
'2026-02-08T10:00:00Z'::timestamptz, 7,
'gpt-5.3-codex', ''),
('copilot-mixed-model', 1, 'assistant', 'claude',
'2026-02-10T12:00:00Z'::timestamptz, 6,
'claude-sonnet', '')`)
require.NoError(t, err, "insert messages")
count, err := store.GetUsageMatchingSessionCount(ctx, db.UsageFilter{
From: "2026-02-10",
To: "2026-02-10",
Timezone: "UTC",
Agent: "copilot",
Model: "gpt-5.3-codex",
})
require.NoError(t, err, "GetUsageMatchingSessionCount")
assert.Equal(t, 0, count,
"out-of-range message's model must not match the bounded window")
}
// TestStoreGetUsageMatchingSessionCountCountsAssistantMessageWithNoModel
// guards against gating matching-session eligibility on m.model != ”:
// some Copilot assistant messages parse before a model name is known, so
// an assistant message with an empty model must still count toward the
// matching-session total when no Model/ExcludeModel filter narrows it.
func TestStoreGetUsageMatchingSessionCountCountsAssistantMessageWithNoModel(
t *testing.T,
) {
_, store := prepareUsageSchema(t, "agentsview_usage_matching_sessions_no_model_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at, ended_at,
message_count, user_message_count
) VALUES
('copilot-no-model', 'test-machine', 'proj-a', 'copilot',
'2026-02-10T10:00:00Z'::timestamptz,
'2026-02-10T10:00:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('copilot-no-model', 0, 'assistant', 'copilot',
'2026-02-10T10:00:00Z'::timestamptz, 7,
'', '')`)
require.NoError(t, err, "insert messages")
count, err := store.GetUsageMatchingSessionCount(ctx, db.UsageFilter{
From: "2026-02-10",
To: "2026-02-10",
Timezone: "UTC",
Agent: "copilot",
})
require.NoError(t, err, "GetUsageMatchingSessionCount")
assert.Equal(t, 1, count,
"assistant message with no model must still count without a model filter")
}
// TestStoreGetUsageMatchingSessionCountUnboundedMatchesBoundedSemantics
// guards against the unbounded (no From/To) branch drifting from the
// bounded branch: soft-deleted sessions and sessions without
// assistant/event activity must not count, and empty-model assistant
// messages must survive an ExcludeModel filter, exactly as on the
// bounded path.
func TestStoreGetUsageMatchingSessionCountUnboundedMatchesBoundedSemantics(
t *testing.T,
) {
_, store := prepareUsageSchema(t, "agentsview_usage_matching_sessions_unbounded_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at, ended_at,
message_count, user_message_count, deleted_at
) VALUES
('copilot-live', 'test-machine', 'proj-a', 'copilot',
'2026-03-01T10:00:00Z'::timestamptz,
'2026-03-01T10:00:00Z'::timestamptz, 1, 1, NULL),
('copilot-trashed', 'test-machine', 'proj-a', 'copilot',
'2026-03-01T10:00:00Z'::timestamptz,
'2026-03-01T10:00:00Z'::timestamptz, 1, 1,
'2026-03-02T00:00:00Z'::timestamptz),
('copilot-user-only', 'test-machine', 'proj-a', 'copilot',
'2026-03-01T10:00:00Z'::timestamptz,
'2026-03-01T10:00:00Z'::timestamptz, 1, 1, NULL)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('copilot-live', 0, 'assistant', 'copilot',
'2026-03-01T10:00:00Z'::timestamptz, 7,
'', ''),
('copilot-trashed', 0, 'assistant', 'copilot',
'2026-03-01T10:00:00Z'::timestamptz, 7,
'gpt-5.3-codex', ''),
('copilot-user-only', 0, 'user', 'hello',
'2026-03-01T10:00:00Z'::timestamptz, 5,
'', '')`)
require.NoError(t, err, "insert messages")
unbounded := db.UsageFilter{Timezone: "UTC", Agent: "copilot"}
count, err := store.GetUsageMatchingSessionCount(ctx, unbounded)
require.NoError(t, err, "GetUsageMatchingSessionCount unbounded")
assert.Equal(t, 1, count,
"soft-deleted and assistant-less sessions must not count unbounded")
bounded := unbounded
bounded.From = "2026-03-01"
bounded.To = "2026-03-01"
boundedCount, err := store.GetUsageMatchingSessionCount(ctx, bounded)
require.NoError(t, err, "GetUsageMatchingSessionCount bounded")
assert.Equal(t, count, boundedCount,
"bounded and unbounded requests must match the same sessions")
excluded := unbounded
excluded.ExcludeModel = "gpt-5.3-codex"
excludedCount, err := store.GetUsageMatchingSessionCount(ctx, excluded)
require.NoError(t, err, "GetUsageMatchingSessionCount exclude-model")
assert.Equal(t, 1, excludedCount,
"empty-model assistant message must survive an ExcludeModel filter")
}
func TestStoreGetUsageSessionCountsDedupesSourceUUIDFallback(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_counts_source_uuid_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('usage-counts-source-001', 'test-machine', 'proj-a', 'claude',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1),
('usage-counts-source-002', 'test-machine', 'proj-b', 'claude',
'2026-03-12T10:01:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage, claude_message_id, claude_request_id, source_uuid
) VALUES
('usage-counts-source-001', 0, 'assistant', 'one',
'2026-03-12T10:00:00Z'::timestamptz, 3,
'test-model-counts', '{"input_tokens":1}', 'msg-1', '', 'source-1'),
('usage-counts-source-002', 0, 'assistant', 'two',
'2026-03-12T10:01:00Z'::timestamptz, 3,
'test-model-counts', '{"input_tokens":1}', 'msg-1', '', 'source-1')`)
require.NoError(t, err, "insert messages")
counts, err := store.GetUsageSessionCounts(ctx, db.UsageFilter{
From: "2026-03-12",
To: "2026-03-12",
Timezone: "UTC",
})
require.NoError(t, err, "GetUsageSessionCounts")
assert.Equal(t, 1, counts.Total)
assert.Equal(t, 1, counts.ByProject["proj-a"])
_, ok := counts.ByProject["proj-b"]
assert.False(t, ok, "proj-b should have been deduped out: %#v", counts.ByProject)
}
func TestPostgresUsageQueriesUnionUsageEvents(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_events_union_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES
('claude-sonnet-4-20250514', 1, 1, 1, 1, 'seed'),
('gpt-5.4', 1, 1, 1, 1, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES
('claude-msg', 'test-machine', 'proj-a', 'claude',
'2026-05-14T09:00:00Z'::timestamptz, 1, 1),
('hermes-event', 'test-machine', 'proj-b', 'hermes',
'2026-05-14T10:00:00Z'::timestamptz, 1, 1),
('hermes-event-2', 'test-machine', 'proj-b', 'hermes',
'2026-05-14T10:10:00Z'::timestamptz, 1, 1)`)
require.NoError(t, err, "insert sessions")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp, content_length,
model, token_usage
) VALUES
('claude-msg', 0, 'assistant', 'one',
'2026-05-14T09:05:00Z'::timestamptz, 3,
'claude-sonnet-4-20250514',
'{"input_tokens":100,"output_tokens":40}')`)
require.NoError(t, err, "insert message")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO usage_events (
session_id, source, model, input_tokens, output_tokens,
cache_read_input_tokens, occurred_at, dedup_key
) VALUES
('hermes-event', 'session', 'gpt-5.4', 300, 70, 20,
'2026-05-14T10:05:00Z'::timestamptz, 'shared-key'),
('hermes-event-2', 'session', 'gpt-5.4', 50, 5, 0,
'2026-05-14T10:10:00Z'::timestamptz, 'shared-key')`)
require.NoError(t, err, "insert usage event")
filter := db.UsageFilter{
From: "2026-05-14",
To: "2026-05-14",
Timezone: "UTC",
Breakdowns: true,
}
result, err := store.GetDailyUsage(ctx, filter)
require.NoError(t, err, "GetDailyUsage")
assert.Equal(t, 450, result.Totals.InputTokens)
assert.Equal(t, 115, result.Totals.OutputTokens)
assert.Equal(t, 20, result.Totals.CacheReadTokens)
assert.Len(t, result.Daily[0].AgentBreakdowns, 2)
top, err := store.GetTopSessionsByCost(ctx, filter, 10)
require.NoError(t, err, "GetTopSessionsByCost")
require.Len(t, top, 3)
assert.Equal(t, "hermes-event", top[0].SessionID)
assert.Equal(t, 390, top[0].TotalTokens)
counts, err := store.GetUsageSessionCounts(ctx, filter)
require.NoError(t, err, "GetUsageSessionCounts")
assert.Equal(t, 3, counts.Total)
assert.Equal(t, 2, counts.ByAgent["hermes"])
assert.Equal(t, 2, counts.ByProject["proj-b"])
}
func TestPostgresUsagePreservesSessionSummaryUsageEventTokens(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_summary_event_test")
ctx := context.Background()
rawInput := db.MaxPlausibleTokens + 250_000
rawOutput := db.MaxPlausibleTokens + 500_000
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('gpt-5.4', 1, 2, 0, 0, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count,
total_output_tokens, peak_context_tokens,
has_total_output_tokens, has_peak_context_tokens
) VALUES (
'hermes-summary', 'test-machine', 'proj', 'hermes',
'2026-05-14T10:00:00Z'::timestamptz, 1, 1,
$1, $2, true, true
)`, rawOutput, rawInput)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO usage_events (
session_id, message_ordinal, source, model, input_tokens,
output_tokens, occurred_at, dedup_key
) VALUES (
'hermes-summary', 0, 'session', 'gpt-5.4', $1, $2,
'2026-05-14T10:05:00Z'::timestamptz, 'session:hermes-summary'
)`, rawInput, rawOutput)
require.NoError(t, err, "insert usage event")
daily, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-05-14",
To: "2026-05-14",
Timezone: "UTC",
})
require.NoError(t, err, "GetDailyUsage")
require.Len(t, daily.Daily, 1, "daily entries")
assert.Equal(t, rawInput, daily.Totals.InputTokens, "daily input")
assert.Equal(t, rawOutput, daily.Totals.OutputTokens, "daily output")
usage, err := store.GetSessionUsage(ctx, "hermes-summary", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, usage, "session usage")
assert.Equal(t, rawOutput, usage.TotalOutputTokens)
assert.Equal(t, rawInput, usage.PeakContextTokens)
require.True(t, usage.HasCost, "HasCost")
wantCost := (float64(rawInput)*1.0 + float64(rawOutput)*2.0) / 1_000_000
assert.InDelta(t, wantCost, usage.CostUSD, 1e-9, "session cost")
require.Len(t, usage.Breakdown, 1, "Breakdown")
entry := usage.Breakdown[0]
assert.Equal(t, "session", entry.Source)
assert.Equal(t, "Step 1", entry.Label)
require.NotNil(t, entry.MessageOrdinal)
assert.Equal(t, 0, *entry.MessageOrdinal)
assert.Equal(t, rawInput, entry.InputTokens)
assert.Equal(t, rawOutput, entry.OutputTokens)
assert.True(t, entry.HasCost)
assert.InDelta(t, wantCost, entry.CostUSD, 1e-9, "breakdown cost")
}
func TestPostgresUsageCostsMessageReasoningTokens(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_message_reasoning_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO model_pricing (
model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok, updated_at
) VALUES ('gpt-5.4', 1, 2, 0, 0, 'seed')`)
require.NoError(t, err, "insert pricing")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES (
'pg-message-reasoning', 'test-machine', 'proj', 'codex',
'2026-05-14T10:00:00Z'::timestamptz, 1, 1
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp,
content_length, model, token_usage
) VALUES (
'pg-message-reasoning', 0, 'assistant', 'done',
'2026-05-14T10:30:00Z'::timestamptz, 4,
'gpt-5.4',
'{"input_tokens":1000,"output_tokens":0,"reasoning_tokens":3000000000}'
)`)
require.NoError(t, err, "insert message")
daily, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-05-14",
To: "2026-05-14",
Timezone: "UTC",
})
require.NoError(t, err, "GetDailyUsage")
require.Len(t, daily.Daily, 1, "daily entries")
assert.Equal(t, 1000, daily.Totals.InputTokens)
assert.Zero(t, daily.Totals.OutputTokens)
assert.InDelta(t, 4.001, daily.Totals.TotalCost, 1e-12)
usage, err := store.GetSessionUsage(ctx, "pg-message-reasoning", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, usage)
assert.True(t, usage.HasCost)
assert.InDelta(t, 4.001, usage.CostUSD, 1e-12)
}
func TestStoreGetDailyUsageSkipsCursorUsageForTerminationFilter(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_usage_cursor_termination_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count, termination_status
) VALUES (
'clean-session', 'test-machine', 'proj', 'claude',
'2026-05-14T10:00:00Z'::timestamptz, 1, 1, 'clean'
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO messages (
session_id, ordinal, role, content, timestamp,
content_length, model, token_usage
) VALUES (
'clean-session', 0, 'assistant', 'one',
'2026-05-14T10:30:00Z'::timestamptz, 3,
'claude-sonnet-4-20250514',
'{"input_tokens":100,"output_tokens":40}'
)`)
require.NoError(t, err, "insert message")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO cursor_usage_events (
occurred_at, model, kind, input_tokens, output_tokens,
cache_read_tokens, charged_cents, cursor_token_fee,
user_id, user_email, dedup_key
) VALUES (
'2026-05-14T10:05:00Z'::timestamptz,
'claude-4.6-opus-high-thinking',
'USAGE_EVENT_KIND_USAGE_BASED',
1234, 567, 8901, 15.66, 3.32,
'152683922', 'member@example.com', 'cursor:termination'
)`)
require.NoError(t, err, "insert cursor usage")
result, err := store.GetDailyUsage(ctx, db.UsageFilter{
From: "2026-05-14",
To: "2026-05-14",
Timezone: "UTC",
Termination: "clean",
})
require.NoError(t, err, "GetDailyUsage clean termination")
require.Len(t, result.Daily, 1, "daily entries")
assert.Equal(t, 100, result.Totals.InputTokens, "InputTokens")
assert.Equal(t, 40, result.Totals.OutputTokens, "OutputTokens")
assert.Equal(t, 1, result.SessionCounts.Total, "SessionCounts.Total")
}
func TestPushSyncsModelPricingToPostgres(t *testing.T) {
pgURL := testPGURL(t)
cleanPGSchema(t, pgURL)
t.Cleanup(func() { cleanPGSchema(t, pgURL) })
local := testDB(t)
require.NoError(t, local.UpsertModelPricing([]db.ModelPricing{{
ModelPattern: "test-model-sync",
InputPerMTok: 1.5,
OutputPerMTok: 2.5,
CacheCreationPerMTok: 3.5,
CacheReadPerMTok: 0.5,
}}), "UpsertModelPricing")
ps, err := New(pgURL, "agentsview", local, "test-machine", true, SyncOptions{})
require.NoError(t, err, "New")
defer ps.Close()
_, err = ps.Push(context.Background(), false, nil)
require.NoError(t, err, "Push")
store, err := NewStore(pgURL, "agentsview", true)
require.NoError(t, err, "NewStore")
defer store.Close()
rows, err := store.DB().QueryContext(context.Background(), `
SELECT model_pattern, input_per_mtok, output_per_mtok,
cache_creation_per_mtok, cache_read_per_mtok
FROM model_pricing
WHERE model_pattern = 'test-model-sync'`)
require.NoError(t, err, "query pricing")
defer rows.Close()
require.True(t, rows.Next(), "expected synced pricing row")
var (
model string
input, output, cacheCreation, cacheRead float64
)
require.NoError(t, rows.Scan(
&model, &input, &output, &cacheCreation, &cacheRead,
), "scan pricing")
assert.Equal(t, "test-model-sync", model)
assert.Equal(t, 1.5, input)
assert.Equal(t, 2.5, output)
assert.Equal(t, 3.5, cacheCreation)
assert.Equal(t, 0.5, cacheRead)
}
func TestPushFallsBackToBuiltinPricingWhenLocalTableEmpty(t *testing.T) {
pgURL := testPGURL(t)
cleanPGSchema(t, pgURL)
t.Cleanup(func() { cleanPGSchema(t, pgURL) })
local := testDB(t)
ps, err := New(pgURL, "agentsview", local, "test-machine", true, SyncOptions{})
require.NoError(t, err, "New")
defer ps.Close()
_, err = ps.Push(context.Background(), false, nil)
require.NoError(t, err, "Push")
store, err := NewStore(pgURL, "agentsview", true)
require.NoError(t, err, "NewStore")
defer store.Close()
rows, err := store.DB().QueryContext(context.Background(), `
SELECT model_pattern
FROM model_pricing
ORDER BY model_pattern`)
require.NoError(t, err, "query pricing")
defer rows.Close()
var models []string
for rows.Next() {
var model string
require.NoError(t, rows.Scan(&model), "scan model")
models = append(models, model)
}
require.NoError(t, rows.Err(), "rows err")
joined := strings.Join(models, ",")
assert.Contains(t, joined, "claude-sonnet-4-20250514",
"fallback pricing not synced")
}
func TestStoreGetSessionUsage_CopilotAICreditsComputed(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_copilot_credits_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES (
'copilot:s1', 'test-machine', 'proj', 'copilot',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO usage_events (
session_id, source, model, input_tokens, output_tokens, cost_usd, occurred_at
) VALUES (
'copilot:s1', 'api', 'gpt-4', 1000, 500, 0.10, '2026-03-12T10:00:00Z'::timestamptz
)`)
require.NoError(t, err, "insert usage event")
u, err := store.GetSessionUsage(ctx, "copilot:s1", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, u, "usage is nil")
assert.True(t, u.HasCost, "HasCost")
assert.Equal(t, 0.10, u.CostUSD, "CostUSD")
assert.Equal(t, 10.0, u.AICredits, "AICredits")
}
func TestStoreGetSessionUsage_CopilotNoAICreditsUnpriced(t *testing.T) {
_, store := prepareUsageSchema(t, "agentsview_copilot_unpriced_test")
ctx := context.Background()
_, err := store.DB().ExecContext(ctx, `
INSERT INTO sessions (
id, machine, project, agent, started_at,
message_count, user_message_count
) VALUES (
'copilot:s2', 'test-machine', 'proj', 'copilot',
'2026-03-12T10:00:00Z'::timestamptz, 1, 1
)`)
require.NoError(t, err, "insert session")
_, err = store.DB().ExecContext(ctx, `
INSERT INTO usage_events (
session_id, source, model, input_tokens, output_tokens, occurred_at
) VALUES (
'copilot:s2', 'api', 'local-model', 1000, 500, '2026-03-12T10:00:00Z'::timestamptz
)`)
require.NoError(t, err, "insert usage event")
u, err := store.GetSessionUsage(ctx, "copilot:s2", true)
require.NoError(t, err, "GetSessionUsage")
require.NotNil(t, u, "usage is nil")
assert.False(t, u.HasCost, "HasCost should be false")
assert.Equal(t, 0.0, u.AICredits, "AICredits should be 0 when unpriced")
}