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

798 lines
25 KiB
Go

package main
import (
"bytes"
"context"
"database/sql"
"encoding/json"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.kenn.io/agentsview/internal/db"
"go.kenn.io/agentsview/internal/dbtest"
"go.kenn.io/agentsview/internal/export"
"go.kenn.io/agentsview/internal/pricing"
)
type exportSessionsDocument struct {
Type string `json:"type"`
SchemaVersion int `json:"schema_version"`
DatabaseID string `json:"database_id"`
Cursor exportSessionsCursor `json:"cursor"`
Pricing map[string]any `json:"pricing"`
Projects map[string]any `json:"projects"`
Sessions []db.SessionSummaryRow `json:"sessions"`
Rows []db.SessionSummaryRow `json:"rows"`
Error string `json:"error"`
Message string `json:"message"`
}
type exportSessionsCursor struct {
Next string `json:"next"`
}
func TestExportSessionsJSONEmitsOneDocument(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "json",
)
require.NoError(t, err, "export sessions")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
assert.Equal(t, 1, doc.SchemaVersion)
assert.NotEmpty(t, doc.DatabaseID)
assert.NotNil(t, doc.Pricing)
assert.NotNil(t, doc.Projects)
assert.Len(t, doc.Sessions, 2)
assert.Empty(t, doc.Rows, "CLI output must use sessions, not rows")
assert.Empty(t, strings.TrimSpace(decoderRemainder(t, stdout)),
"stdout must contain exactly one JSON document")
}
func TestExportSessionsJSONAliasEmitsOneDocument(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--json",
)
require.NoError(t, err, "export sessions --json")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
assert.Equal(t, 1, doc.SchemaVersion)
assert.Len(t, doc.Sessions, 2)
assert.Empty(t, strings.TrimSpace(decoderRemainder(t, stdout)),
"--json must emit exactly one JSON document")
}
func TestExportSessionsJSONAliasRejectsConflictingFormat(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--json", "--format", "ndjson",
)
require.Error(t, err)
assert.Empty(t, stdout)
assert.Empty(t, stderr)
assert.Contains(t, err.Error(), "--json cannot be combined with --format ndjson")
}
func TestExportSessionsJSONNoUsageKeepsClosedCostSource(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "json",
)
require.NoError(t, err, "export sessions")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
assert.Equal(t, "computed", doc.Pricing["cost_source"])
assert.NotEqual(t, "", doc.Pricing["cost_source"])
}
func TestExportSessionsNDJSONEmitsMetaThenRows(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "ndjson",
)
require.NoError(t, err, "export sessions")
assert.Empty(t, stderr)
lines := nonEmptyLines(stdout)
require.Len(t, lines, 3)
meta := decodeExportSessionsDocument(t, lines[0])
assert.Equal(t, "meta", meta.Type)
assert.Equal(t, 1, meta.SchemaVersion)
assert.NotEmpty(t, meta.DatabaseID)
assert.NotNil(t, meta.Pricing)
assert.NotNil(t, meta.Projects)
assert.Empty(t, meta.Sessions)
for _, line := range lines[1:] {
var row db.SessionSummaryRow
require.NoError(t, json.Unmarshal([]byte(line), &row))
assert.NotEmpty(t, row.ID)
}
}
func TestExportSessionsAllJSONEmitsOneDocument(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--all", "--format", "json",
)
require.NoError(t, err, "export all sessions")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
assert.Len(t, doc.Sessions, 2)
assert.Empty(t, doc.Cursor.Next)
assert.Empty(t, strings.TrimSpace(decoderRemainder(t, stdout)),
"--all must not concatenate JSON documents")
}
func TestExportSessionsAllJSONMergesPricingAcrossPages(t *testing.T) {
setupExportGoldenDataDir(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(),
"export", "sessions",
"--all",
"--format", "json",
"--limit", "1",
)
require.NoError(t, err, "export all sessions")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
require.Len(t, doc.Sessions, 4)
models, ok := doc.Pricing["models"].(map[string]any)
require.True(t, ok, "pricing.models must be an object")
assert.Contains(t, models, goldenComputedModel)
assert.Contains(t, models, goldenReportedModel)
assert.Empty(t, doc.Cursor.Next)
}
func TestBuildExportSessionsOutputMarksCrossPageProjectConflictAmbiguous(t *testing.T) {
const projectKey = "pl1:sha256:project"
page := func(identityKey string) db.SessionExportResult {
return db.SessionExportResult{Projects: map[string]export.ProjectMapEntry{
projectKey: {
ProjectKey: projectKey,
Resolution: export.ProjectResolutionResolved,
Identity: &export.ProjectIdentity{
Key: identityKey, Kind: export.ProjectKindGitRemote,
},
},
}}
}
got := buildExportSessionsOutput("database", []db.SessionExportResult{
page("p1:sha256:first"), page("p1:sha256:second"),
})
require.Contains(t, got.Projects, projectKey)
assert.Equal(t, export.ProjectResolutionAmbiguous,
got.Projects[projectKey].Resolution)
assert.Nil(t, got.Projects[projectKey].Identity)
}
func TestMergeExportSessionsPricingTreatsNoModelPagesAsNeutral(t *testing.T) {
noModels := &export.PricingBlock{
CostSource: export.CostSourceComputed,
Models: map[string]export.EffectiveModelRate{},
}
reported := &export.PricingBlock{
CostSource: export.CostSourceReported,
Models: map[string]export.EffectiveModelRate{
"reported-model": {
CostSource: export.CostSourceReported,
},
},
}
got := mergeExportSessionsPricing(noModels, reported)
assert.Equal(t, export.CostSourceReported, got.CostSource)
got = mergeExportSessionsPricing(reported, noModels)
assert.Equal(t, export.CostSourceReported, got.CostSource)
got = mergeExportSessionsPricing(noModels, noModels)
assert.Equal(t, export.CostSourceComputed, got.CostSource)
}
func TestExportSessionsAllNDJSONCursorNextEmpty(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--all", "--format", "ndjson",
)
require.NoError(t, err, "export all sessions")
assert.Empty(t, stderr)
lines := nonEmptyLines(stdout)
require.Len(t, lines, 3)
meta := decodeExportSessionsDocument(t, lines[0])
assert.Empty(t, meta.Cursor.Next)
}
func TestExportSessionsInvalidCursorWritesStructuredResetError(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--cursor", "not-a-cursor",
)
require.Error(t, err, "invalid cursor")
assert.Equal(t, 4, exitCodeFromError(err))
assert.Empty(t, stdout)
var got exportSessionsDocument
require.NoError(t, json.Unmarshal([]byte(stderr), &got))
assert.Equal(t, "cursor_reset", got.Error)
assert.Equal(t,
"session export cursor is no longer valid; restart the export",
got.Message,
)
assert.NotEmpty(t, got.DatabaseID)
}
func TestExportSessionsWrongDatabaseCursorWritesStructuredResetError(t *testing.T) {
dataDir := testDataDir(t)
seedExportSessionsArchiveAt(t, filepath.Join(dataDir, "sessions.db"))
cursor := firstExportSessionsCursor(t)
otherDir := t.TempDir()
other := seedExportSessionsArchiveAt(t, filepath.Join(otherDir, "sessions.db"))
require.NoError(t, other.SetDatabaseIDForTest(
context.Background(), "other-export-sessions-test-db"))
t.Setenv("AGENTSVIEW_DATA_DIR", otherDir)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--cursor", cursor,
)
require.Error(t, err, "wrong database cursor")
assert.Equal(t, 4, exitCodeFromError(err))
assert.Empty(t, stdout)
var got exportSessionsDocument
require.NoError(t, json.Unmarshal([]byte(stderr), &got))
assert.Equal(t, "cursor_reset", got.Error)
assert.Equal(t,
"session export cursor is no longer valid; restart the export",
got.Message,
)
assert.NotEmpty(t, got.DatabaseID)
}
func TestExportSessionsCursorResetMainStderrIsOnlyStructuredJSON(t *testing.T) {
dataDir := testDataDir(t)
seedExportSessionsArchiveAt(t, filepath.Join(dataDir, "sessions.db"))
cmd := exec.Command(
os.Args[0],
"-test.run=^TestExportSessionsCursorResetMainHelperProcess$",
"--",
"export", "sessions", "--cursor", "not-a-cursor",
)
cmd.Env = append(os.Environ(),
"AGENTSVIEW_EXPORT_SESSIONS_MAIN_HELPER=1",
"AGENTSVIEW_DATA_DIR="+dataDir,
)
stdout, err := cmd.Output()
require.Error(t, err, "cursor reset should exit non-zero")
assert.Empty(t, stdout)
var exitErr *exec.ExitError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, sessionExportCursorResetExitCode, exitErr.ExitCode())
stderr := string(exitErr.Stderr)
assert.NotContains(t, stderr, "fatal:")
var got exportSessionsDocument
require.NoError(t, json.Unmarshal([]byte(stderr), &got))
assert.Equal(t, "cursor_reset", got.Error)
assert.Equal(t,
"session export cursor is no longer valid; restart the export",
got.Message,
)
assert.NotEmpty(t, got.DatabaseID)
}
func TestExportSessionsMainStillPrintsFatalForNonCursorErrors(t *testing.T) {
dataDir := testDataDir(t)
seedExportSessionsArchiveAt(t, filepath.Join(dataDir, "sessions.db"))
cmd := exec.Command(
os.Args[0],
"-test.run=^TestExportSessionsCursorResetMainHelperProcess$",
"--",
"export", "sessions", "--format", "xml",
)
cmd.Env = append(os.Environ(),
"AGENTSVIEW_EXPORT_SESSIONS_MAIN_HELPER=1",
"AGENTSVIEW_DATA_DIR="+dataDir,
)
stdout, err := cmd.Output()
require.Error(t, err, "invalid format should exit non-zero")
assert.Empty(t, stdout)
var exitErr *exec.ExitError
require.ErrorAs(t, err, &exitErr)
assert.Equal(t, 1, exitErr.ExitCode())
assert.Contains(t, string(exitErr.Stderr), "fatal:")
assert.Contains(t, string(exitErr.Stderr), "invalid argument \"xml\"")
}
func TestExportSessionsCursorResetMainHelperProcess(t *testing.T) {
if os.Getenv("AGENTSVIEW_EXPORT_SESSIONS_MAIN_HELPER") != "1" {
return
}
for i, arg := range os.Args {
if arg == "--" {
os.Args = append([]string{"agentsview"}, os.Args[i+1:]...)
main()
return
}
}
t.Fatal("missing helper args")
}
func TestExportSessionsExitCode4ReservedForCursorReset(t *testing.T) {
seedExportSessionsArchive(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "xml",
)
require.Error(t, err, "invalid format")
assert.NotEqual(t, 4, exitCodeFromError(err))
assert.Empty(t, stdout)
assert.Empty(t, stderr)
}
func TestExportSessionsRunsWhileWriteOwnerLockHeld(t *testing.T) {
dataDir := testDataDir(t)
seedExportSessionsArchiveAt(t, filepath.Join(dataDir, "sessions.db"))
holdWriteOwnerLockForTest(t, dataDir)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "json",
)
require.NoError(t, err, "read-only export should not need writer lock")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
assert.Len(t, doc.Sessions, 2)
assert.Equal(t, "export-sessions-test-db", doc.DatabaseID)
}
func TestExportSessionsRequiresExistingDatabaseID(t *testing.T) {
dataDir := testDataDir(t)
dbPath := filepath.Join(dataDir, "sessions.db")
database := dbtest.OpenTestDBAt(t, dbPath)
insertExportSessionsTestSession(t, database, db.Session{
ID: "missing-db-id",
Project: "alpha",
Machine: "local",
Agent: "codex",
StartedAt: dbtest.Ptr("2026-06-01T10:00:00Z"),
EndedAt: dbtest.Ptr("2026-06-01T10:10:00Z"),
MessageCount: 2,
UserMessageCount: 2,
})
require.NoError(t, database.Close())
removeArchiveDatabaseIDForTest(t, dbPath)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions",
)
require.Error(t, err, "export sessions should not initialize metadata")
assert.Empty(t, stdout)
assert.Empty(t, stderr)
assert.Contains(t, err.Error(), "database id")
readonly, openErr := db.OpenReadOnly(dbPath)
require.NoError(t, openErr)
t.Cleanup(func() { require.NoError(t, readonly.Close()) })
_, idErr := readonly.GetDatabaseID(context.Background())
require.ErrorIs(t, idErr, db.ErrDatabaseIDMissing)
}
func TestExportSessionsUpgradeRequiresBackgroundEvidenceBackfill(t *testing.T) {
dataDir := testDataDir(t)
dbPath := filepath.Join(dataDir, "sessions.db")
database := dbtest.OpenTestDBAt(t, dbPath)
insertExportSessionsTestSession(t, database, db.Session{
ID: "legacy", Project: "agentsview", Machine: "local",
Agent: "codex", Cwd: "/work/agentsview", MessageCount: 2,
UserMessageCount: 1,
})
require.NoError(t, database.Close())
raw, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
_, err = raw.Exec(`DROP TABLE session_project_identity_snapshots`)
require.NoError(t, err)
require.NoError(t, raw.Close())
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "status",
)
require.NoError(t, err)
assert.Empty(t, stderr)
assert.Equal(t, "project identity evidence: pending (0/1)\n", stdout)
stdout, stderr, err = executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "json",
)
require.Error(t, err)
assert.Empty(t, stderr)
assert.Empty(t, stdout)
assert.Contains(t, err.Error(), "project identity evidence backfill is pending")
assert.Contains(t, err.Error(), "agentsview export status")
raw, err = sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer raw.Close()
var exists int
require.NoError(t, raw.QueryRow(`
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'table' AND name = 'session_project_identity_snapshots'
`).Scan(&exists))
assert.Equal(t, 1, exists)
var state string
require.NoError(t, raw.QueryRow(`
SELECT state FROM background_migrations
WHERE name = 'session_project_identity_snapshots_v1'
`).Scan(&state))
assert.Equal(t, "pending", state)
}
func TestExportStatusReportsBackfillFailure(t *testing.T) {
dataDir := testDataDir(t)
dbPath := filepath.Join(dataDir, "sessions.db")
database := dbtest.OpenTestDBAt(t, dbPath)
raw, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
_, err = raw.Exec(`
INSERT INTO background_migrations (
name, state, total_items, completed_items, last_error
) VALUES (
'session_project_identity_snapshots_v1', 'failed', 3, 1,
'git metadata unavailable'
)
`)
require.NoError(t, err)
require.NoError(t, raw.Close())
require.NoError(t, database.Close())
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "status",
)
require.NoError(t, err)
assert.Empty(t, stderr)
assert.Equal(t,
"project identity evidence: failed (1/3)\n"+
"last error: git metadata unavailable\n",
stdout)
}
func TestExportSessionsDoesNotUpgradeUnrelatedReadOnlyOpenFailure(t *testing.T) {
dataDir := testDataDir(t)
dbPath := filepath.Join(dataDir, "sessions.db")
require.NoError(t, os.WriteFile(dbPath, nil, 0o600))
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--format", "json",
)
require.Error(t, err)
assert.Empty(t, stdout)
assert.Empty(t, stderr)
info, statErr := os.Stat(dbPath)
require.NoError(t, statErr)
assert.Zero(t, info.Size(),
"a non-schema read failure must not initialize or rebuild the archive")
}
func removeArchiveDatabaseIDForTest(t *testing.T, dbPath string) {
t.Helper()
raw, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer func() { require.NoError(t, raw.Close()) }()
_, err = raw.Exec(`DELETE FROM archive_metadata WHERE key = 'database_id'`)
require.NoError(t, err)
}
func TestExportSessionsCursorConflictingFilterIsUsageError(t *testing.T) {
seedExportSessionsArchive(t)
cursor := firstExportSessionsCursor(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions",
"--cursor", cursor,
"--project", "alpha",
)
require.Error(t, err, "cursor with filter should fail as usage error")
assert.NotEqual(t, 4, exitCodeFromError(err))
assert.Empty(t, stdout)
assert.Empty(t, stderr)
assert.Contains(t, err.Error(), "--cursor cannot be combined with --project")
}
func TestExportSessionsCursorAllowsFormatAndLimit(t *testing.T) {
seedExportSessionsArchive(t)
cursor := firstExportSessionsCursor(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions",
"--cursor", cursor,
"--format", "json",
"--limit", "1",
)
require.NoError(t, err, "cursor with format and limit")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
require.Len(t, doc.Sessions, 1)
assert.Equal(t, "alpha-old", doc.Sessions[0].ID)
}
func TestExportSessionsCursorResumesFilteredExport(t *testing.T) {
database := seedExportSessionsArchive(t)
insertExportSessionsTestSession(t, database, db.Session{
ID: "beta-middle",
Project: "beta",
Machine: "local",
Agent: "codex",
StartedAt: dbtest.Ptr("2026-06-01T09:30:00Z"),
EndedAt: dbtest.Ptr("2026-06-01T09:40:00Z"),
MessageCount: 2,
UserMessageCount: 2,
})
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions",
"--project", "alpha",
"--limit", "1",
)
require.NoError(t, err, "first filtered export page")
require.Empty(t, stderr)
first := decodeExportSessionsDocument(t, stdout)
require.Len(t, first.Sessions, 1)
assert.Equal(t, "alpha-new", first.Sessions[0].ID)
require.NotEmpty(t, first.Cursor.Next)
stdout, stderr, err = executeExportSessionsCommand(
newRootCommand(), "export", "sessions",
"--cursor", first.Cursor.Next,
)
require.NoError(t, err, "filtered cursor resume")
require.Empty(t, stderr)
second := decodeExportSessionsDocument(t, stdout)
require.Len(t, second.Sessions, 1)
assert.Equal(t, "alpha-old", second.Sessions[0].ID)
assert.Empty(t, second.Cursor.Next)
}
func TestExportSessionsJSONGolden(t *testing.T) {
setupExportGoldenDataDir(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(),
"export", "sessions",
"--format", "json",
"--limit", "2",
)
require.NoError(t, err, "export sessions json golden")
require.Empty(t, stderr)
assert.NotContains(t, stdout, "/fixtures/")
assert.NotContains(t, stdout, `"cwd"`)
assert.NotContains(t, stdout, `"machine":"golden-host"`)
assert.NotContains(t, stdout, `"root_path":"/`)
assertGoldenBytes(t, "session_export_v1.json", []byte(stdout))
}
func TestExportSessionsNDJSONGolden(t *testing.T) {
setupExportGoldenDataDir(t)
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(),
"export", "sessions",
"--format", "ndjson",
"--limit", "2",
)
require.NoError(t, err, "export sessions ndjson golden")
require.Empty(t, stderr)
assertGoldenBytes(t, "session_export_v1.ndjson", []byte(stdout))
}
func firstExportSessionsCursor(t *testing.T) string {
t.Helper()
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions", "--limit", "1",
)
require.NoError(t, err, "first export page")
require.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
require.NotEmpty(t, doc.Cursor.Next)
return doc.Cursor.Next
}
func TestExportSessionsFallbackPricingOnUnseededArchive(t *testing.T) {
dataDir := testDataDir(t)
database := dbtest.OpenTestDBAt(t, filepath.Join(dataDir, "sessions.db"))
require.NoError(t, database.SetDatabaseIDForTest(
context.Background(), "fallback-pricing-test-db"))
model := exactFallbackPricedModel(t)
insertExportSessionsTestSession(t, database, db.Session{
ID: "fallback-priced",
Project: "alpha",
Machine: "local",
Agent: "claude",
StartedAt: dbtest.Ptr("2026-06-01T10:00:00Z"),
EndedAt: dbtest.Ptr("2026-06-01T10:10:00Z"),
MessageCount: 3,
UserMessageCount: 2,
})
require.NoError(t, database.InsertMessages([]db.Message{
{
SessionID: "fallback-priced", Ordinal: 0, Role: "user",
Content: "question", ContentLength: len("question"),
Timestamp: "2026-06-01T10:00:00Z",
},
{
SessionID: "fallback-priced", Ordinal: 1, Role: "assistant",
Content: "answer", ContentLength: len("answer"),
Timestamp: "2026-06-01T10:05:00Z", Model: model,
TokenUsage: json.RawMessage(
`{"input_tokens":1000,"output_tokens":500}`),
},
{
SessionID: "fallback-priced", Ordinal: 2, Role: "user",
Content: "follow up", ContentLength: len("follow up"),
Timestamp: "2026-06-01T10:06:00Z",
},
}), "insert messages")
require.NoError(t, database.Close(), "close seeded archive")
stdout, stderr, err := executeExportSessionsCommand(
newRootCommand(), "export", "sessions")
require.NoError(t, err, "export sessions on unseeded archive")
assert.Empty(t, stderr)
doc := decodeExportSessionsDocument(t, stdout)
require.Len(t, doc.Sessions, 1, "exported sessions")
usage := doc.Sessions[0].ModelUsage
require.NotNil(t, usage, "model usage")
assert.True(t, usage.HasCost,
"fallback-priced model %s should have cost", model)
assert.Greater(t, usage.CostUSD, 0.0, "fallback-priced cost")
fallback, ok := doc.Pricing["fallback"].(map[string]any)
require.True(t, ok, "pricing fallback block")
assert.Equal(t, true, fallback["used"], "fallback used")
assert.Contains(t, doc.Pricing["source"], "embedded",
"pricing source provenance")
}
// exactFallbackPricedModel returns an embedded fallback model pattern with
// non-wildcard name and nonzero input/output rates, so lookups resolve
// deterministically regardless of snapshot contents.
func exactFallbackPricedModel(t *testing.T) string {
t.Helper()
for _, p := range pricing.FallbackPricing() {
if strings.ContainsAny(p.ModelPattern, "*/_") {
continue
}
if p.InputPerMTok > 0 && p.OutputPerMTok > 0 {
return p.ModelPattern
}
}
t.Fatal("no exact fallback-priced model in embedded snapshot")
return ""
}
func executeExportSessionsCommand(
root *cobra.Command, args ...string,
) (string, string, error) {
stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer)
root.SetOut(stdout)
root.SetErr(stderr)
root.SetArgs(args)
_, err := root.ExecuteC()
return stdout.String(), stderr.String(), err
}
func seedExportSessionsArchive(t *testing.T) *db.DB {
t.Helper()
dataDir := testDataDir(t)
return seedExportSessionsArchiveAt(t, filepath.Join(dataDir, "sessions.db"))
}
func seedExportSessionsArchiveAt(t *testing.T, path string) *db.DB {
t.Helper()
database := dbtest.OpenTestDBAt(t, path)
require.NoError(t, database.SetDatabaseIDForTest(
context.Background(), "export-sessions-test-db"))
insertExportSessionsTestSession(t, database, db.Session{
ID: "alpha-new",
Project: "alpha",
Machine: "local",
Agent: "codex",
StartedAt: dbtest.Ptr("2026-06-01T10:00:00Z"),
EndedAt: dbtest.Ptr("2026-06-01T10:10:00Z"),
MessageCount: 2,
UserMessageCount: 2,
})
insertExportSessionsTestSession(t, database, db.Session{
ID: "alpha-old",
Project: "alpha",
Machine: "local",
Agent: "codex",
StartedAt: dbtest.Ptr("2026-06-01T09:00:00Z"),
EndedAt: dbtest.Ptr("2026-06-01T09:10:00Z"),
MessageCount: 2,
UserMessageCount: 2,
})
return database
}
func insertExportSessionsTestSession(
t *testing.T, database *db.DB, session db.Session,
) {
t.Helper()
require.NoError(t, database.UpsertSession(session),
"upsert session %s", session.ID)
require.NoError(t, database.UpsertProjectIdentityObservation(
context.Background(), export.ProjectIdentityObservation{
SessionID: session.ID, Project: session.Project,
Machine: session.Machine, RootPath: session.Cwd,
GitBranch: session.GitBranch, ObservedAt: time.Now().UTC(),
}), "upsert session project identity %s", session.ID)
}
func decodeExportSessionsDocument(
t *testing.T, input string,
) exportSessionsDocument {
t.Helper()
var doc exportSessionsDocument
require.NoError(t, json.Unmarshal([]byte(input), &doc))
return doc
}
func decoderRemainder(t *testing.T, input string) string {
t.Helper()
dec := json.NewDecoder(strings.NewReader(input))
var doc any
require.NoError(t, dec.Decode(&doc))
rest, err := io.ReadAll(dec.Buffered())
require.NoError(t, err)
return string(rest)
}
func nonEmptyLines(s string) []string {
raw := strings.Split(s, "\n")
lines := make([]string, 0, len(raw))
for _, line := range raw {
if strings.TrimSpace(line) != "" {
lines = append(lines, line)
}
}
return lines
}