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

1429 lines
47 KiB
Go

package parser
import (
"database/sql"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"testing"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// parseOpenCodeAll parses every session in an OpenCode SQLite database using
// the same per-session primitives the provider uses (ListOpenCodeSessionMeta +
// parseOpenCodeDBSession), reproducing the deleted ParseOpenCodeDB
// whole-database free function for the retained parse tests.
func parseOpenCodeAll(dbPath, machine string) ([]ParseResult, error) {
metas, err := ListOpenCodeSessionMeta(dbPath)
if err != nil {
return nil, err
}
var out []ParseResult
for _, m := range metas {
sess, msgs, err := parseOpenCodeDBSession(dbPath, m.SessionID, machine)
if err != nil {
return nil, err
}
if sess == nil {
continue
}
out = append(out, ParseResult{Session: *sess, Messages: msgs})
}
return out, nil
}
// openCodeSchema matches the real OpenCode database schema.
// Role and part type live inside the JSON data columns.
const openCodeSchema = `
CREATE TABLE project (
id TEXT PRIMARY KEY,
worktree TEXT NOT NULL,
time_created INTEGER NOT NULL DEFAULT 0,
time_updated INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE session (
id TEXT PRIMARY KEY,
project_id TEXT NOT NULL,
parent_id TEXT,
title TEXT,
time_created INTEGER NOT NULL,
time_updated INTEGER NOT NULL,
FOREIGN KEY (project_id) REFERENCES project(id)
);
CREATE TABLE message (
id TEXT PRIMARY KEY,
session_id TEXT NOT NULL,
time_created INTEGER NOT NULL,
time_updated INTEGER NOT NULL,
data TEXT NOT NULL,
FOREIGN KEY (session_id) REFERENCES session(id)
);
CREATE TABLE part (
id TEXT PRIMARY KEY,
message_id TEXT NOT NULL,
session_id TEXT NOT NULL,
time_created INTEGER NOT NULL,
time_updated INTEGER NOT NULL,
data TEXT NOT NULL,
FOREIGN KEY (message_id) REFERENCES message(id)
);
`
func assertEq[T comparable](t *testing.T, name string, got, want T) {
t.Helper()
assert.Equal(t, want, got, name)
}
type OpenCodeSeeder struct {
db *sql.DB
t *testing.T
}
func (s *OpenCodeSeeder) AddProject(id, worktree string) {
s.t.Helper()
_, err := s.db.Exec(`INSERT INTO project (id, worktree) VALUES (?, ?)`, id, worktree)
require.NoError(s.t, err, "add project")
}
func (s *OpenCodeSeeder) AddSession(id, projectID, parentID, title string, timeCreated, timeUpdated int64) {
s.t.Helper()
var pID, tStr any
if parentID != "" {
pID = parentID
}
if title != "" {
tStr = title
}
_, err := s.db.Exec(`INSERT INTO session (id, project_id, parent_id, title, time_created, time_updated) VALUES (?, ?, ?, ?, ?, ?)`,
id, projectID, pID, tStr, timeCreated, timeUpdated)
require.NoError(s.t, err, "add session")
}
func (s *OpenCodeSeeder) AddMessage(id, sessionID string, timeCreated, timeUpdated int64, data string) {
s.t.Helper()
_, err := s.db.Exec(`INSERT INTO message (id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?)`,
id, sessionID, timeCreated, timeUpdated, data)
require.NoError(s.t, err, "add message")
}
func (s *OpenCodeSeeder) AddPart(id, messageID, sessionID string, timeCreated, timeUpdated int64, data string) {
s.t.Helper()
_, err := s.db.Exec(`INSERT INTO part (id, message_id, session_id, time_created, time_updated, data) VALUES (?, ?, ?, ?, ?, ?)`,
id, messageID, sessionID, timeCreated, timeUpdated, data)
require.NoError(s.t, err, "add part")
}
func newTestDB(t *testing.T) (string, *OpenCodeSeeder, *sql.DB) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "opencode.db")
copyOpenCodeSchemaTemplate(t, dbPath)
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err, "open test db")
seeder := &OpenCodeSeeder{db: db, t: t}
return dbPath, seeder, db
}
var (
openCodeSchemaTemplateOnce sync.Once
openCodeSchemaTemplateBytes []byte
openCodeSchemaTemplateErr error
)
func copyOpenCodeSchemaTemplate(t *testing.T, dbPath string) {
t.Helper()
openCodeSchemaTemplateOnce.Do(func() {
openCodeSchemaTemplateBytes, openCodeSchemaTemplateErr =
buildOpenCodeSchemaTemplate()
})
require.NoError(t, openCodeSchemaTemplateErr)
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755),
"mkdir opencode test db dir")
require.NoError(t, os.WriteFile(dbPath, openCodeSchemaTemplateBytes, 0o644),
"copy opencode schema template")
}
func buildOpenCodeSchemaTemplate() ([]byte, error) {
dir, err := os.MkdirTemp("", "agentsview-opencode-schema-*")
if err != nil {
return nil, fmt.Errorf("create opencode schema template dir: %w", err)
}
defer os.RemoveAll(dir)
dbPath := filepath.Join(dir, "opencode.db")
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("open opencode schema template: %w", err)
}
if _, err = db.Exec(openCodeSchema); err != nil {
_ = db.Close()
return nil, fmt.Errorf("create opencode schema template: %w", err)
}
if err = db.Close(); err != nil {
return nil, fmt.Errorf("close opencode schema template: %w", err)
}
raw, err := os.ReadFile(dbPath)
if err != nil {
return nil, fmt.Errorf("read opencode schema template: %w", err)
}
return raw, nil
}
// seedHybridSQLiteDB creates an OpenCode-shaped SQLite DB at
// dbPath containing a single session row with the given ID. Used
// by tests that exercise OpenCode-format source lookup in hybrid and
// pure-SQLite roots, where a real DB file (not just a marker) is
// required.
func seedHybridSQLiteDB(t *testing.T, dbPath, sessionID string) {
t.Helper()
copyOpenCodeSchemaTemplate(t, dbPath)
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err, "open hybrid db")
t.Cleanup(func() { db.Close() })
_, err = db.Exec(
`INSERT INTO project (id, worktree)
VALUES (?, ?)`,
"prj_seed", "/tmp/seed",
)
require.NoError(t, err, "seed project")
_, err = db.Exec(
`INSERT INTO session
(id, project_id, time_created, time_updated)
VALUES (?, ?, ?, ?)`,
sessionID, "prj_seed", int64(1), int64(2),
)
require.NoError(t, err, "seed session")
}
func seedStandardSession(t *testing.T, seeder *OpenCodeSeeder) {
t.Helper()
seeder.AddProject("prj_1", "/home/user/code/myapp")
seeder.AddSession("ses_abc", "prj_1", "", "Test Session", 1700000000000, 1700000060000)
seeder.AddMessage("msg_1", "ses_abc", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_1", "msg_1", "ses_abc", 1700000000000, 1700000000000, `{"type":"text","text":"Hello, help me with Go"}`)
seeder.AddMessage("msg_2", "ses_abc", 1700000010000, 1700000010000, `{"role":"assistant"}`)
seeder.AddPart("prt_2", "msg_2", "ses_abc", 1700000010000, 1700000010000, `{"type":"text","text":"Sure, I can help with Go."}`)
}
func writeOpenCodeStorageFile(
t *testing.T, path string, data any,
) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755),
"mkdir %s", filepath.Dir(path))
raw, err := json.Marshal(data)
require.NoError(t, err, "marshal %s", path)
require.NoError(t, os.WriteFile(path, raw, 0o644), "write %s", path)
}
func TestParseOpenCodeDB_StandardSession(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seedStandardSession(t, seeder)
sessions, err := parseOpenCodeAll(dbPath, "testmachine")
require.NoError(t, err, "ParseOpenCodeDB")
assertEq(t, "sessions len", len(sessions), 1)
s := sessions[0]
assertEq(t, "ID", s.Session.ID, "opencode:ses_abc")
assertEq(t, "Agent", s.Session.Agent, AgentOpenCode)
assertEq(t, "Machine", s.Session.Machine, "testmachine")
assertEq(t, "Project", s.Session.Project, "myapp")
assertEq(t, "Cwd", s.Session.Cwd, "/home/user/code/myapp")
assertEq(t, "MessageCount", s.Session.MessageCount, 2)
assertEq(t, "FirstMessage", s.Session.FirstMessage, "Test Session")
wantPath := dbPath + "#ses_abc"
assertEq(t, "File.Path", s.Session.File.Path, wantPath)
wantMtime := int64(1700000060000) * 1_000_000
assertEq(t, "File.Mtime", s.Session.File.Mtime, wantMtime)
assertEq(t, "Messages len", len(s.Messages), 2)
assertEq(t, "msg[0].Role", s.Messages[0].Role, RoleUser)
assertEq(t, "msg[1].Role", s.Messages[1].Role, RoleAssistant)
assertEq(t, "msg[1].Content", s.Messages[1].Content, "Sure, I can help with Go.")
}
func TestOpenOpenCodeDBDoesNotForceWALMode(t *testing.T) {
dbPath, _, writer := newTestDB(t)
require.NoError(t, writer.Close())
reader, err := openOpenCodeDB(dbPath)
require.NoError(t, err)
defer reader.Close()
_, err = reader.Exec("CREATE TABLE must_stay_read_only (id INTEGER)")
require.Error(t, err, "OpenCode source databases must stay read-only")
var journalMode string
require.NoError(t, reader.QueryRow("PRAGMA journal_mode").Scan(&journalMode))
assert.Equal(t, "delete", journalMode)
assert.NoFileExists(t, dbPath+"-wal")
assert.NoFileExists(t, dbPath+"-shm")
}
func TestParseOpenCodeFile_StorageSession(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "user",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_2.json",
), map[string]any{
"id": "msg_2",
"sessionID": "ses_storage",
"role": "assistant",
"modelID": "gpt-5.2-codex",
"tokens": map[string]any{
"input": 11,
"output": 7,
"cache": map[string]any{
"read": 3,
"write": 2,
},
},
"time": map[string]any{
"created": 1700000010000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "prt_1.json",
), map[string]any{
"id": "prt_1",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "Hello from storage",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_2", "prt_2.json",
), map[string]any{
"id": "prt_2",
"sessionID": "ses_storage",
"messageID": "msg_2",
"type": "tool",
"tool": "read",
"callID": "call_storage",
"state": map[string]any{
"input": map[string]any{
"file_path": "main.go",
},
},
"time": map[string]any{
"created": 1700000010000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_2", "prt_3.json",
), map[string]any{
"id": "prt_3",
"sessionID": "ses_storage",
"messageID": "msg_2",
"type": "text",
"text": "Here is the file.",
"time": map[string]any{
"created": 1700000011000,
},
})
sess, msgs, err := parseOpenCodeStorageFile(
sessionPath, "testmachine",
)
require.NoError(t, err, "parseOpenCodeStorageFile")
require.NotNil(t, sess, "expected non-nil session")
assertEq(t, "ID", sess.ID, "opencode:ses_storage")
assertEq(t, "Agent", sess.Agent, AgentOpenCode)
assertEq(t, "Project", sess.Project, "myapp")
assertEq(t, "Cwd", sess.Cwd, "/home/user/code/myapp")
assertEq(t, "Machine", sess.Machine, "testmachine")
assertEq(t, "MessageCount", sess.MessageCount, 2)
assertEq(t, "FirstMessage", sess.FirstMessage, "Storage Session")
assertEq(t, "File.Path", sess.File.Path, sessionPath)
assertEq(t, "File.Mtime", sess.File.Mtime > 0, true)
assertEq(t, "messages len", len(msgs), 2)
assertEq(t, "msg[0].Role", msgs[0].Role, RoleUser)
assertEq(t, "msg[0].Content", msgs[0].Content, "Hello from storage")
assertEq(t, "msg[1].Role", msgs[1].Role, RoleAssistant)
assertEq(t, "msg[1].Model", msgs[1].Model, "gpt-5.2-codex")
assertEq(t, "msg[1].HasToolUse", msgs[1].HasToolUse, true)
assertEq(t, "msg[1].Content", msgs[1].Content, "Here is the file.")
assertEq(t, "msg[1].HasOutputTokens", msgs[1].HasOutputTokens, true)
assertEq(t, "msg[1].OutputTokens", msgs[1].OutputTokens, 7)
assertToolCalls(t, msgs[1].ToolCalls, []ParsedToolCall{{
ToolName: "read",
Category: "Read",
ToolUseID: "call_storage",
InputJSON: `{"file_path":"main.go"}`,
}})
}
func TestParseOpenCodeFile_StorageSessionInvalidChildFails(
t *testing.T,
) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "user",
"time": map[string]any{
"created": 1700000000000,
},
})
require.NoError(t, os.MkdirAll(filepath.Join(
root, "storage", "message", "ses_storage",
), 0o755), "mkdir invalid message dir")
require.NoError(t, os.WriteFile(filepath.Join(
root, "storage", "message", "ses_storage", "msg_bad.json",
), []byte(`{"id":"msg_bad"`), 0o644), "write invalid message")
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "prt_1.json",
), map[string]any{
"id": "prt_1",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "Hello from storage",
"time": map[string]any{
"created": 1700000000000,
},
})
require.NoError(t, os.MkdirAll(filepath.Join(
root, "storage", "part", "msg_1",
), 0o755), "mkdir invalid part dir")
require.NoError(t, os.WriteFile(filepath.Join(
root, "storage", "part", "msg_1", "prt_bad.json",
), []byte(`{"id":"prt_bad"`), 0o644), "write invalid part")
sess, msgs, err := parseOpenCodeStorageFile(
sessionPath, "testmachine",
)
require.Error(t, err, "expected parseOpenCodeStorageFile error")
assert.Nil(t, sess, "session, want nil")
assert.Nil(t, msgs, "msgs, want nil")
}
func TestParseOpenCodeFile_MissingPartDirAllowed(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "assistant",
"modelID": "gpt-5.2-codex",
"time": map[string]any{
"created": 1700000000000,
},
})
sess, msgs, err := parseOpenCodeStorageFile(
sessionPath, "testmachine",
)
require.NoError(t, err, "parseOpenCodeStorageFile")
assert.Nil(t, sess, "session, want nil")
assert.Nil(t, msgs, "msgs, want nil")
}
func TestParseOpenCodeFile_StorageMessageMissingIDFails(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"sessionID": "ses_storage",
"role": "assistant",
"time": map[string]any{
"created": 1700000000000,
},
})
sess, msgs, err := parseOpenCodeStorageFile(
sessionPath, "testmachine",
)
require.Error(t, err, "expected parseOpenCodeStorageFile error")
assert.Nil(t, sess, "session, want nil")
assert.Nil(t, msgs, "msgs, want nil")
}
func TestParseOpenCodeFile_StoragePartMissingIDFails(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "assistant",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "part_1.json",
), map[string]any{
"messageID": "msg_1",
"type": "text",
"text": "hello",
"time": map[string]any{
"created": 1700000000000,
},
})
sess, msgs, err := parseOpenCodeStorageFile(
sessionPath, "testmachine",
)
require.Error(t, err, "expected parseOpenCodeStorageFile error")
assert.Nil(t, sess, "session, want nil")
assert.Nil(t, msgs, "msgs, want nil")
}
func TestParseOpenCodeFile_StoragePartOrderingUsesStartTime(
t *testing.T,
) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "assistant",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "part_1.json",
), map[string]any{
"id": "part_1",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "second",
"time": map[string]any{
"start": 1700000002000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "part_2.json",
), map[string]any{
"id": "part_2",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "first",
"time": map[string]any{
"start": 1700000001000,
},
})
_, msgs, err := parseOpenCodeStorageFile(sessionPath, "testmachine")
require.NoError(t, err, "parseOpenCodeStorageFile")
require.Len(t, msgs, 1, "messages len")
assertEq(t, "msg[0].Content", msgs[0].Content, "first\nsecond")
}
func TestParseOpenCodeFile_StoragePartOrderingPrefersStartOverCreated(
t *testing.T,
) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "assistant",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "part_1.json",
), map[string]any{
"id": "part_1",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "second",
"time": map[string]any{
"start": 1700000002000,
"created": 1700000001000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "part_2.json",
), map[string]any{
"id": "part_2",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "first",
"time": map[string]any{
"start": 1700000001000,
"created": 1700000002000,
},
})
_, msgs, err := parseOpenCodeStorageFile(sessionPath, "testmachine")
require.NoError(t, err, "parseOpenCodeStorageFile")
require.Len(t, msgs, 1, "messages len")
assertEq(t, "msg[0].Content", msgs[0].Content, "first\nsecond")
}
func TestParseOpenCodeFile_StorageStepFinishTokens(t *testing.T) {
root := t.TempDir()
sessionPath := filepath.Join(
root, "storage", "session", "global", "ses_storage.json",
)
writeOpenCodeStorageFile(t, sessionPath, map[string]any{
"id": "ses_storage",
"directory": "/home/user/code/myapp",
"title": "Storage Session",
"time": map[string]any{
"created": 1700000000000,
"updated": 1700000060000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "message", "ses_storage", "msg_1.json",
), map[string]any{
"id": "msg_1",
"sessionID": "ses_storage",
"role": "assistant",
"modelID": "gpt-5.2-codex",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "prt_1.json",
), map[string]any{
"id": "prt_1",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "text",
"text": "reply from storage",
"time": map[string]any{
"created": 1700000000000,
},
})
writeOpenCodeStorageFile(t, filepath.Join(
root, "storage", "part", "msg_1", "prt_2.json",
), map[string]any{
"id": "prt_2",
"sessionID": "ses_storage",
"messageID": "msg_1",
"type": "step-finish",
"tokens": map[string]any{
"input": 11,
"output": 7,
"cache": map[string]any{
"read": 3,
"write": 2,
},
},
"time": map[string]any{
"created": 1700000001000,
},
})
sess, msgs, err := parseOpenCodeStorageFile(sessionPath, "testmachine")
require.NoError(t, err, "parseOpenCodeStorageFile")
require.NotNil(t, sess, "want one parsed session")
require.Len(t, msgs, 1, "messages")
assertEq(t, "msg[0].Model", msgs[0].Model, "gpt-5.2-codex")
assertEq(t, "msg[0].HasOutputTokens", msgs[0].HasOutputTokens, true)
assertEq(t, "msg[0].OutputTokens", msgs[0].OutputTokens, 7)
assertEq(t, "msg[0].HasContextTokens", msgs[0].HasContextTokens, true)
assertEq(t, "msg[0].ContextTokens", msgs[0].ContextTokens, 16)
assertEq(
t, "session HasTotalOutputTokens",
sess.HasTotalOutputTokens, true,
)
assertEq(t, "session TotalOutputTokens", sess.TotalOutputTokens, 7)
assertEq(
t, "session HasPeakContextTokens",
sess.HasPeakContextTokens, true,
)
assertEq(t, "session PeakContextTokens", sess.PeakContextTokens, 16)
}
func TestParseOpenCodeDB_TitleFallback(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
// Empty title: should use first user message.
seeder.AddSession("ses_empty", "prj_1", "", "",
1700000000000, 1700000010000)
seeder.AddMessage("msg_1", "ses_empty",
1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_1", "msg_1", "ses_empty",
1700000000000, 1700000000000,
`{"type":"text","text":"Help me debug this crash"}`)
// Placeholder title: should also use first user message.
seeder.AddSession("ses_default", "prj_1", "",
"New session - 2026-03-22T10:00:00.000Z",
1700000020000, 1700000030000)
seeder.AddMessage("msg_2", "ses_default",
1700000020000, 1700000020000, `{"role":"user"}`)
seeder.AddPart("prt_2", "msg_2", "ses_default",
1700000020000, 1700000020000,
`{"type":"text","text":"Refactor the auth module"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
assertEq(t, "sessions len", len(sessions), 2)
for _, s := range sessions {
switch s.Session.ID {
case "opencode:ses_empty":
assertEq(t, "empty title fallback",
s.Session.FirstMessage,
"Help me debug this crash")
case "opencode:ses_default":
assertEq(t, "placeholder title fallback",
s.Session.FirstMessage,
"Refactor the auth module")
}
}
}
func TestParseOpenCodeDB_ToolParts(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_tools", "prj_1", "", "", 1700000000000, 1700000030000)
seeder.AddMessage("msg_u", "ses_tools", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_tools", 1700000000000, 1700000000000, `{"type":"text","text":"read my file"}`)
seeder.AddMessage("msg_a", "ses_tools", 1700000010000, 1700000012000, `{"role":"assistant"}`)
seeder.AddPart("prt_r", "msg_a", "ses_tools", 1700000010000, 1700000010000, `{"type":"reasoning","text":"Let me think about this..."}`)
seeder.AddPart("prt_t", "msg_a", "ses_tools", 1700000011000, 1700000011000, `{"type":"tool","tool":"read","callID":"call_1","state":{"input":{"file_path":"main.go"}}}`)
seeder.AddPart("prt_txt", "msg_a", "ses_tools", 1700000012000, 1700000012000, `{"type":"text","text":"Here is the file content."}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
assertEq(t, "sessions len", len(sessions), 1)
msgs := sessions[0].Messages
assertEq(t, "messages len", len(msgs), 2)
ast := msgs[1]
assertEq(t, "HasThinking", ast.HasThinking, true)
assertEq(t, "HasToolUse", ast.HasToolUse, true)
assertToolCalls(t, ast.ToolCalls, []ParsedToolCall{{
ToolName: "read",
Category: "Read",
ToolUseID: "call_1",
InputJSON: `{"file_path":"main.go"}`,
}})
}
// TestParseOpenCodeDB_SkillTool verifies that a "skill" tool part
// populates ParsedToolCall.SkillName straight from the tool's own
// input, without going through the read-file/shell-command
// inference heuristics. Before this fix extractOpenCodeToolCall
// never set SkillName at all (#1040).
func TestParseOpenCodeDB_SkillTool(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_skill", "prj_1", "", "", 1700000000000, 1700000030000)
seeder.AddMessage("msg_u", "ses_skill", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_skill", 1700000000000, 1700000000000, `{"type":"text","text":"use the doc-writer skill"}`)
seeder.AddMessage("msg_a", "ses_skill", 1700000010000, 1700000010000, `{"role":"assistant"}`)
seeder.AddPart("prt_t", "msg_a", "ses_skill", 1700000010000, 1700000010000,
`{"type":"tool","tool":"skill","callID":"call_skill","state":{"input":{"name":"doc-writer"}}}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
msgs := sessions[0].Messages
require.Len(t, msgs, 2, "messages len")
ast := msgs[1]
require.Len(t, ast.ToolCalls, 1, "tool calls len")
assertEq(t, "SkillName", ast.ToolCalls[0].SkillName, "doc-writer")
}
// TestParseOpenCodeDB_SkillNameFromReadTool verifies that a
// "read" tool part whose input points at a real on-disk SKILL.md
// infers the skill name from the file's frontmatter, matching the
// Cursor/Codex read-file heuristic shared via inferOpenCodeSkillName.
func TestParseOpenCodeDB_SkillNameFromReadTool(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_read", "prj_1", "", "", 1700000000000, 1700000030000)
seeder.AddMessage("msg_u", "ses_read", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_read", 1700000000000, 1700000000000, `{"type":"text","text":"read the skill file"}`)
seeder.AddMessage("msg_a", "ses_read", 1700000010000, 1700000010000, `{"role":"assistant"}`)
seeder.AddPart("prt_t", "msg_a", "ses_read", 1700000010000, 1700000010000,
`{"type":"tool","tool":"read","callID":"call_read","state":{"input":{"file_path":`+
quoteJSON(t, path)+`}}}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
msgs := sessions[0].Messages
require.Len(t, msgs, 2, "messages len")
ast := msgs[1]
require.Len(t, ast.ToolCalls, 1, "tool calls len")
assertEq(t, "SkillName", ast.ToolCalls[0].SkillName, "foo")
}
// TestParseOpenCodeDB_SkillNameFromReadToolRelativePath verifies
// that a read tool with a relative SKILL.md path resolves against
// the session worktree and reads frontmatter instead of falling
// back to the parent directory name.
func TestParseOpenCodeDB_SkillNameFromReadToolRelativePath(t *testing.T) {
// Frontmatter name intentionally differs from the folder name
// ("renamed") to prove we read frontmatter, not the directory.
path := writeTestSkill(t, "renamed", "actual-skill")
worktree := filepath.Dir(filepath.Dir(filepath.Dir(path)))
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", worktree)
seeder.AddSession("ses_rel", "prj_1", "", "", 1700000000000, 1700000030000)
seeder.AddMessage("msg_u", "ses_rel", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_rel", 1700000000000, 1700000000000, `{"type":"text","text":"read the skill"}`)
seeder.AddMessage("msg_a", "ses_rel", 1700000010000, 1700000010000, `{"role":"assistant"}`)
seeder.AddPart("prt_t", "msg_a", "ses_rel", 1700000010000, 1700000010000,
`{"type":"tool","tool":"read","callID":"call_read","state":{"input":{"file_path":"skills/renamed/SKILL.md"}}}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
msgs := sessions[0].Messages
require.Len(t, msgs, 2, "messages len")
ast := msgs[1]
require.Len(t, ast.ToolCalls, 1, "tool calls len")
assertEq(t, "SkillName", ast.ToolCalls[0].SkillName, "actual-skill")
}
// TestParseOpenCodeDB_SkillNameFromShellCommandRelativePath
// verifies that a "bash" tool part running a relative-path
// SKILL.md read is resolved against the session's project
// worktree (threaded through buildOpenCodeMessage), matching the
// Codex shell-command heuristic.
func TestParseOpenCodeDB_SkillNameFromShellCommandRelativePath(t *testing.T) {
path := writeTestSkill(t, "foo", "foo")
worktree := filepath.Dir(filepath.Dir(filepath.Dir(path)))
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", worktree)
seeder.AddSession("ses_bash", "prj_1", "", "", 1700000000000, 1700000030000)
seeder.AddMessage("msg_u", "ses_bash", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_bash", 1700000000000, 1700000000000, `{"type":"text","text":"cat the skill file"}`)
seeder.AddMessage("msg_a", "ses_bash", 1700000010000, 1700000010000, `{"role":"assistant"}`)
seeder.AddPart("prt_t", "msg_a", "ses_bash", 1700000010000, 1700000010000,
`{"type":"tool","tool":"bash","callID":"call_bash","state":{"input":{"command":"cat skills/foo/SKILL.md"}}}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
msgs := sessions[0].Messages
require.Len(t, msgs, 2, "messages len")
ast := msgs[1]
require.Len(t, ast.ToolCalls, 1, "tool calls len")
assertEq(t, "SkillName", ast.ToolCalls[0].SkillName, "foo")
}
func TestParseOpenCodeDB_EmptySession(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_empty", "prj_1", "", "", 1700000000000, 1700000000000)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
assertEq(t, "sessions len", len(sessions), 0)
}
func TestParseOpenCodeDB_NonexistentDB(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "nonexistent.db")
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "expected nil error")
assert.Nil(t, sessions, "expected nil sessions")
}
func TestParseOpenCodeDB_ProjectFromWorktree(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
// Create a temp dir that looks like a git repo so
// ExtractProjectFromCwd resolves it.
repoDir := filepath.Join(t.TempDir(), "my-project")
require.NoError(t, os.MkdirAll(filepath.Join(repoDir, ".git"), 0o755))
seeder.AddProject("prj_git", repoDir)
seeder.AddSession("ses_git", "prj_git", "", "", 1700000000000, 1700000010000)
seeder.AddMessage("msg_1", "ses_git", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_1", "msg_1", "ses_git", 1700000000000, 1700000000000, `{"type":"text","text":"hello"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
assertEq(t, "sessions len", len(sessions), 1)
assertEq(t, "Project", sessions[0].Session.Project, "my_project")
}
func TestParseOpenCodeSession_SingleSession(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seedStandardSession(t, seeder)
sess, msgs, err := parseOpenCodeDBSession(dbPath, "ses_abc", "testmachine")
require.NoError(t, err, "parseOpenCodeDBSession")
require.NotNil(t, sess, "expected non-nil session")
assertEq(t, "ID", sess.ID, "opencode:ses_abc")
assertEq(t, "messages len", len(msgs), 2)
}
func TestParseOpenCodeDB_OrdinalContinuity(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_ord", "prj_1", "", "", 1700000000000, 1700000050000)
// msg 0: user (kept, ordinal 0)
seeder.AddMessage("msg_1", "ses_ord", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_1", "msg_1", "ses_ord", 1700000000000, 1700000000000, `{"type":"text","text":"first"}`)
// msg 1: system (skipped role)
seeder.AddMessage("msg_2", "ses_ord", 1700000010000, 1700000010000, `{"role":"system"}`)
seeder.AddPart("prt_2", "msg_2", "ses_ord", 1700000010000, 1700000010000, `{"type":"text","text":"system msg"}`)
// msg 2: user with empty content (skipped)
seeder.AddMessage("msg_3", "ses_ord", 1700000020000, 1700000020000, `{"role":"user"}`)
seeder.AddPart("prt_3", "msg_3", "ses_ord", 1700000020000, 1700000020000, `{"type":"text","text":""}`)
// msg 3: assistant (kept, ordinal 1)
seeder.AddMessage("msg_4", "ses_ord", 1700000030000, 1700000030000, `{"role":"assistant"}`)
seeder.AddPart("prt_4", "msg_4", "ses_ord", 1700000030000, 1700000030000, `{"type":"text","text":"response"}`)
// msg 4: user (kept, ordinal 2)
seeder.AddMessage("msg_5", "ses_ord", 1700000040000, 1700000040000, `{"role":"user"}`)
seeder.AddPart("prt_5", "msg_5", "ses_ord", 1700000040000, 1700000040000, `{"type":"text","text":"follow up"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
assertEq(t, "sessions len", len(sessions), 1)
msgs := sessions[0].Messages
assertEq(t, "messages len", len(msgs), 3)
for i, m := range msgs {
assertEq(t, "Ordinal", m.Ordinal, i)
}
assertEq(t, "msgs[0].Content", msgs[0].Content, "first")
assertEq(t, "msgs[1].Content", msgs[1].Content, "response")
assertEq(t, "msgs[2].Content", msgs[2].Content, "follow up")
}
func TestParseOpenCodeDB_ParentSession(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_parent", "prj_1", "", "", 1700000000000, 1700000010000)
seeder.AddSession("ses_child", "prj_1", "ses_parent", "", 1700000020000, 1700000030000)
// Add messages to both so they aren't skipped
seeder.AddMessage("msg_p", "ses_parent", 1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_p", "msg_p", "ses_parent", 1700000000000, 1700000000000, `{"type":"text","text":"parent msg"}`)
seeder.AddMessage("msg_c", "ses_child", 1700000020000, 1700000020000, `{"role":"user"}`)
seeder.AddPart("prt_c", "msg_c", "ses_child", 1700000020000, 1700000020000, `{"type":"text","text":"child msg"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
var child *ParseResult
for i := range sessions {
if sessions[i].Session.ID == "opencode:ses_child" {
child = &sessions[i]
}
}
require.NotNil(t, child, "child session not found")
assertEq(t, "ParentSessionID", child.Session.ParentSessionID, "opencode:ses_parent")
}
func TestListOpenCodeSessionMeta(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seedStandardSession(t, seeder)
metas, err := ListOpenCodeSessionMeta(dbPath)
require.NoError(t, err, "ListOpenCodeSessionMeta")
assertEq(t, "metas len", len(metas), 1)
m := metas[0]
assertEq(t, "SessionID", m.SessionID, "ses_abc")
wantPath := dbPath + "#ses_abc"
assertEq(t, "VirtualPath", m.VirtualPath, wantPath)
wantMtime := int64(1700000060000) * 1_000_000
assertEq(t, "FileMtime", m.FileMtime, wantMtime)
}
func TestListOpenCodeSessionMeta_NonexistentDB(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "nope.db")
metas, err := ListOpenCodeSessionMeta(dbPath)
require.NoError(t, err, "unexpected error")
assertEq(t, "metas len", len(metas), 0)
}
// TestParseOpenCodeDB_TokenUsage verifies that an assistant
// message with modelID and tokens populates ParsedMessage.Model
// and TokenUsage in the agentsview-native key shape, and that
// session totals roll up. Without this fix the usage dashboard
// reports $0 for every OpenCode session.
func TestParseOpenCodeDB_TokenUsage(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/home/user/code/myapp")
seeder.AddSession("ses_usage", "prj_1", "", "Usage Test",
1700000000000, 1700000060000)
seeder.AddMessage("msg_user", "ses_usage",
1700000000000, 1700000000000,
`{"role":"user"}`)
seeder.AddPart("prt_user", "msg_user", "ses_usage",
1700000000000, 1700000000000,
`{"type":"text","text":"hi"}`)
// Assistant message with full token blob: input, output,
// cache.read, cache.write. Mirrors the real OpenCode shape
// for OpenAI and Anthropic providers.
assistantData := `{"role":"assistant","modelID":"gpt-5.2-codex",` +
`"providerID":"openai","cost":0.0186375,` +
`"tokens":{"input":10370,"output":35,"reasoning":0,` +
`"cache":{"read":0,"write":0}}}`
seeder.AddMessage("msg_asst", "ses_usage",
1700000010000, 1700000010000, assistantData)
seeder.AddPart("prt_asst", "msg_asst", "ses_usage",
1700000010000, 1700000010000,
`{"type":"text","text":"answer"}`)
// Second assistant with cache read+write to verify the
// nested cache.{read,write} fields map onto the
// cache_{read,creation}_input_tokens keys.
cacheData := `{"role":"assistant","modelID":"claude-sonnet-4-20250514",` +
`"providerID":"anthropic","cost":0.04641675,` +
`"tokens":{"input":1,"output":102,"reasoning":0,` +
`"cache":{"read":500,"write":11969}}}`
seeder.AddMessage("msg_asst2", "ses_usage",
1700000020000, 1700000020000, cacheData)
seeder.AddPart("prt_asst2", "msg_asst2", "ses_usage",
1700000020000, 1700000020000,
`{"type":"text","text":"answer2"}`)
sessions, err := parseOpenCodeAll(dbPath, "testmachine")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
s := sessions[0]
var asst1, asst2 *ParsedMessage
for i := range s.Messages {
m := &s.Messages[i]
if m.Role != RoleAssistant {
continue
}
switch m.Model {
case "gpt-5.2-codex":
asst1 = m
case "claude-sonnet-4-20250514":
asst2 = m
}
}
require.NotNil(t, asst1, "missing gpt-5.2-codex assistant message")
require.NotNil(t, asst2, "missing claude-sonnet assistant message")
checkUsage := func(name string, m *ParsedMessage,
wantIn, wantOut, wantCacheRead, wantCacheCreate int) {
t.Helper()
require.NotEmpty(t, m.TokenUsage, "%s: TokenUsage empty", name)
var got map[string]int
require.NoError(t, json.Unmarshal(m.TokenUsage, &got),
"%s: unmarshal TokenUsage", name)
assertEq(t, name+" input_tokens",
got["input_tokens"], wantIn)
assertEq(t, name+" output_tokens",
got["output_tokens"], wantOut)
assertEq(t, name+" cache_read_input_tokens",
got["cache_read_input_tokens"], wantCacheRead)
assertEq(t, name+" cache_creation_input_tokens",
got["cache_creation_input_tokens"], wantCacheCreate)
assertEq(t, name+" OutputTokens",
m.OutputTokens, wantOut)
assertEq(t, name+" HasOutputTokens",
m.HasOutputTokens, wantOut > 0)
wantCtx := wantIn + wantCacheRead + wantCacheCreate
assertEq(t, name+" ContextTokens",
m.ContextTokens, wantCtx)
assertEq(t, name+" HasContextTokens",
m.HasContextTokens, wantCtx > 0)
}
checkUsage("gpt", asst1, 10370, 35, 0, 0)
checkUsage("claude", asst2, 1, 102, 500, 11969)
// Session-level rollups via accumulateMessageTokenUsage.
assert.True(t, s.Session.HasTotalOutputTokens, "session HasTotalOutputTokens")
assertEq(t, "TotalOutputTokens",
s.Session.TotalOutputTokens, 137) // 35 + 102
assert.True(t, s.Session.HasPeakContextTokens, "session HasPeakContextTokens")
assertEq(t, "PeakContextTokens",
s.Session.PeakContextTokens, 12470) // 1 + 500 + 11969
}
// TestParseOpenCodeDB_UnknownTokensShape verifies that a
// present but unrecognized `tokens` object (empty {} or a
// foreign schema) leaves TokenUsage empty so the usage query
// filter skips the row, rather than fabricating a zero-valued
// record that pollutes the dashboard.
func TestParseOpenCodeDB_UnknownTokensShape(t *testing.T) {
cases := []struct {
name string
tokensRaw string
}{
{"empty object", `{}`},
{"foreign keys only", `{"totalTokens":42,"promptCount":3}`},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_u", "prj_1", "", "Unknown",
1700000000000, 1700000010000)
seeder.AddMessage("msg_u", "ses_u",
1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_u",
1700000000000, 1700000000000,
`{"type":"text","text":"hi"}`)
data := `{"role":"assistant","modelID":"gpt-5.4",` +
`"providerID":"openai","tokens":` + tc.tokensRaw + `}`
seeder.AddMessage("msg_a", "ses_u",
1700000005000, 1700000005000, data)
seeder.AddPart("prt_a", "msg_a", "ses_u",
1700000005000, 1700000005000,
`{"type":"text","text":"answer"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
var asst *ParsedMessage
for i := range sessions[0].Messages {
if sessions[0].Messages[i].Role == RoleAssistant {
asst = &sessions[0].Messages[i]
break
}
}
require.NotNil(t, asst, "missing assistant message")
assertEq(t, "Model", asst.Model, "gpt-5.4")
assert.Empty(t, asst.TokenUsage,
"TokenUsage = %q, want empty", string(asst.TokenUsage))
assertEq(t, "HasOutputTokens",
asst.HasOutputTokens, false)
assertEq(t, "HasContextTokens",
asst.HasContextTokens, false)
})
}
}
// TestParseOpenCodeDB_ZeroTokens verifies that an explicit
// tokens block with every counter set to zero is preserved as
// "known zero" rather than collapsed to "unknown". The
// normalized token_usage row is still written and both
// coverage flags are set, so downstream rollups can
// distinguish an errored request from a missing usage blob.
func TestParseOpenCodeDB_ZeroTokens(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_zero", "prj_1", "", "Zero",
1700000000000, 1700000010000)
seeder.AddMessage("msg_u", "ses_zero",
1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_zero",
1700000000000, 1700000000000,
`{"type":"text","text":"hi"}`)
// Errored assistant request: OpenCode still records the
// tokens object with every field set to zero. Non-empty
// content keeps the row out of the "empty message" filter
// so the usage extraction path is actually exercised.
seeder.AddMessage("msg_a", "ses_zero",
1700000005000, 1700000005000,
`{"role":"assistant","modelID":"gpt-5.2-chat-latest",`+
`"providerID":"openai","cost":0,`+
`"tokens":{"input":0,"output":0,"reasoning":0,`+
`"cache":{"read":0,"write":0}}}`)
seeder.AddPart("prt_a", "msg_a", "ses_zero",
1700000005000, 1700000005000,
`{"type":"text","text":"sorry, request failed"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
var asst *ParsedMessage
for i := range sessions[0].Messages {
if sessions[0].Messages[i].Role == RoleAssistant {
asst = &sessions[0].Messages[i]
break
}
}
require.NotNil(t, asst, "missing assistant message")
assertEq(t, "Model", asst.Model, "gpt-5.2-chat-latest")
require.NotEmpty(t, asst.TokenUsage, "TokenUsage empty; want zero-valued JSON preserved")
var got map[string]int
require.NoError(t, json.Unmarshal(asst.TokenUsage, &got), "unmarshal TokenUsage")
assertEq(t, "input_tokens", got["input_tokens"], 0)
assertEq(t, "output_tokens", got["output_tokens"], 0)
assertEq(t, "cache_read_input_tokens",
got["cache_read_input_tokens"], 0)
assertEq(t, "cache_creation_input_tokens",
got["cache_creation_input_tokens"], 0)
assertEq(t, "HasOutputTokens", asst.HasOutputTokens, true)
assertEq(t, "HasContextTokens", asst.HasContextTokens, true)
assertEq(t, "OutputTokens", asst.OutputTokens, 0)
assertEq(t, "ContextTokens", asst.ContextTokens, 0)
}
// TestParseOpenCodeDB_NoTokenUsage verifies that assistant
// messages with no tokens block (e.g. errored requests) leave
// TokenUsage empty so they are filtered out by the usage query.
func TestParseOpenCodeDB_NoTokenUsage(t *testing.T) {
dbPath, seeder, db := newTestDB(t)
defer db.Close()
seeder.AddProject("prj_1", "/tmp/proj")
seeder.AddSession("ses_err", "prj_1", "", "Errored",
1700000000000, 1700000010000)
seeder.AddMessage("msg_u", "ses_err",
1700000000000, 1700000000000, `{"role":"user"}`)
seeder.AddPart("prt_u", "msg_u", "ses_err",
1700000000000, 1700000000000,
`{"type":"text","text":"hi"}`)
// No tokens block at all (errored request).
seeder.AddMessage("msg_a", "ses_err",
1700000005000, 1700000005000,
`{"role":"assistant","modelID":"gpt-5.4","providerID":"openai"}`)
seeder.AddPart("prt_a", "msg_a", "ses_err",
1700000005000, 1700000005000,
`{"type":"text","text":"oops"}`)
sessions, err := parseOpenCodeAll(dbPath, "m")
require.NoError(t, err, "ParseOpenCodeDB")
require.Len(t, sessions, 1, "sessions len")
var asst *ParsedMessage
for i := range sessions[0].Messages {
if sessions[0].Messages[i].Role == RoleAssistant {
asst = &sessions[0].Messages[i]
break
}
}
require.NotNil(t, asst, "missing assistant message")
assertEq(t, "Model", asst.Model, "gpt-5.4")
assert.Empty(t, asst.TokenUsage, "TokenUsage = %q, want empty", string(asst.TokenUsage))
}
func TestOpenCodeStorageFingerprintMissingDetectsContentRewrite(
t *testing.T,
) {
stored := buildOpenCodeStorageFingerprint(
[]openCodeMessageRow{{
id: "msg-1",
data: `{"role":"assistant","modelID":"gpt-5"}`,
timeCreated: 100,
}},
map[string][]openCodePartRow{
"msg-1": {{
id: "part-1",
messageID: "msg-1",
data: `{"type":"text","text":"complete"}`,
timeCreated: 101,
}},
},
)
current := buildOpenCodeStorageFingerprint(
[]openCodeMessageRow{{
id: "msg-1",
data: `{"role":"assistant","modelID":"gpt-5"}`,
timeCreated: 100,
}},
map[string][]openCodePartRow{
"msg-1": {{
id: "part-1",
messageID: "msg-1",
data: `{"type":"text","text":"truncated"}`,
timeCreated: 101,
}},
},
)
assert.True(t, OpenCodeStorageFingerprintMissing(stored, current),
"expected content rewrite to invalidate fingerprint")
}