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
342 lines
9.5 KiB
Go
342 lines
9.5 KiB
Go
package parser
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/klauspost/compress/zstd"
|
|
_ "github.com/mattn/go-sqlite3"
|
|
)
|
|
|
|
// parseZedAll parses every top-level thread in a Zed threads.db using the same
|
|
// per-thread primitives the provider uses (ListZedThreadMetas +
|
|
// parseZedThreadFromDB), reproducing the deleted ParseZedSessions
|
|
// whole-database free function for the retained parse tests.
|
|
func parseZedAll(dbPath, machine string) ([]ParseResult, error) {
|
|
info, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("stat %s: %w", dbPath, err)
|
|
}
|
|
conn, err := OpenZedDB(dbPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
metas, err := ListZedThreadMetas(conn, dbPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []ParseResult
|
|
for _, m := range metas {
|
|
result, err := parseZedThreadFromDB(conn, dbPath, m.RawID, machine, info)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result != nil {
|
|
out = append(out, *result)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func TestParseZedSessions_JSON(t *testing.T) {
|
|
dbPath := createZedThreadsDB(t, []zedTestThread{{
|
|
id: "10431c84-c47b-4e6c-b2df-f9f3b9ad025b",
|
|
summary: "WP Record Scaffold Generation",
|
|
createdAt: "2026-06-08T09:12:41.962819Z",
|
|
updatedAt: "2026-06-08T09:14:10.475149Z",
|
|
folderPaths: "/Users/alice/code/my-app",
|
|
dataType: "json",
|
|
data: []byte(`{
|
|
"model": {"model": "claude-opus-4", "provider": "anthropic"},
|
|
"request_token_usage": {
|
|
"req-1": {"input_tokens": 1000, "output_tokens": 200},
|
|
"req-2": {"input_tokens": 1500, "output_tokens": 300}
|
|
},
|
|
"messages": [
|
|
{"User": {"content": [{"Text": "Generate code"}]}},
|
|
{"Agent": {"content": [
|
|
{"Thinking": {"text": "Plan"}},
|
|
{"Text": "Done"},
|
|
{"ToolUse": {"id": "call_1", "name": "terminal", "input": {"command": "make test"}}}
|
|
], "tool_results": {"call_1": {"content": [{"Text": "ok"}]}}}}
|
|
]
|
|
}`),
|
|
}})
|
|
|
|
results, err := parseZedAll(dbPath, "local")
|
|
if err != nil {
|
|
t.Fatalf("ParseZedSessions: %v", err)
|
|
}
|
|
if len(results) != 1 {
|
|
t.Fatalf("len(results) = %d, want 1", len(results))
|
|
}
|
|
|
|
sess := results[0].Session
|
|
if sess.ID != "zed:10431c84-c47b-4e6c-b2df-f9f3b9ad025b" {
|
|
t.Fatalf("session ID = %q", sess.ID)
|
|
}
|
|
if sess.Project != "my_app" || sess.Cwd != "/Users/alice/code/my-app" {
|
|
t.Fatalf("project/cwd = %q/%q", sess.Project, sess.Cwd)
|
|
}
|
|
if sess.FirstMessage != "Generate code" || sess.MessageCount != 2 || sess.UserMessageCount != 1 {
|
|
t.Fatalf("session summary = %+v", sess)
|
|
}
|
|
if sess.File.Path != ZedSQLiteVirtualPath(dbPath, "10431c84-c47b-4e6c-b2df-f9f3b9ad025b") {
|
|
t.Fatalf("file path = %q", sess.File.Path)
|
|
}
|
|
if sess.TotalOutputTokens != 500 || sess.PeakContextTokens != 1500 {
|
|
t.Fatalf("tokens: total_out=%d (want 500), peak_ctx=%d (want 1500)", sess.TotalOutputTokens, sess.PeakContextTokens)
|
|
}
|
|
if !sess.HasTotalOutputTokens || !sess.HasPeakContextTokens {
|
|
t.Fatalf("token presence flags not set: has_total=%v, has_peak=%v", sess.HasTotalOutputTokens, sess.HasPeakContextTokens)
|
|
}
|
|
|
|
msgs := results[0].Messages
|
|
if msgs[0].Role != RoleUser || msgs[0].Content != "Generate code" {
|
|
t.Fatalf("user msg = %+v", msgs[0])
|
|
}
|
|
if msgs[1].Role != RoleAssistant || msgs[1].Content != "Done" || !msgs[1].HasThinking || !msgs[1].HasToolUse {
|
|
t.Fatalf("assistant msg = %+v", msgs[1])
|
|
}
|
|
if msgs[1].Model != "claude-opus-4" {
|
|
t.Fatalf("assistant model = %q, want %q", msgs[1].Model, "claude-opus-4")
|
|
}
|
|
if len(msgs[1].ToolCalls) != 1 || msgs[1].ToolCalls[0].ToolName != "terminal" {
|
|
t.Fatalf("tool calls = %+v", msgs[1].ToolCalls)
|
|
}
|
|
if len(msgs[1].ToolResults) != 1 || msgs[1].ToolResults[0].ToolUseID != "call_1" {
|
|
t.Fatalf("tool results = %+v", msgs[1].ToolResults)
|
|
}
|
|
tr := msgs[1].ToolResults[0]
|
|
if got := DecodeContent(tr.ContentRaw); got != "ok" {
|
|
t.Fatalf("tool result ContentRaw decoded = %q, want %q (raw=%s)", got, "ok", tr.ContentRaw)
|
|
}
|
|
|
|
ue := results[0].UsageEvents
|
|
if len(ue) != 1 {
|
|
t.Fatalf("UsageEvents len = %d, want 1", len(ue))
|
|
}
|
|
if ue[0].Model != "claude-opus-4" {
|
|
t.Fatalf("usage event model = %q, want %q", ue[0].Model, "claude-opus-4")
|
|
}
|
|
// input: 1000+1500=2500; output: 200+300=500
|
|
if ue[0].InputTokens != 2500 || ue[0].OutputTokens != 500 {
|
|
t.Fatalf("usage event tokens: in=%d (want 2500), out=%d (want 500)", ue[0].InputTokens, ue[0].OutputTokens)
|
|
}
|
|
if ue[0].DedupKey != "session:zed:10431c84-c47b-4e6c-b2df-f9f3b9ad025b" {
|
|
t.Fatalf("usage event dedup key = %q", ue[0].DedupKey)
|
|
}
|
|
}
|
|
|
|
func TestExtractZedDocMeta(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
doc map[string]any
|
|
wantModel string
|
|
wantTotalIn int
|
|
wantTotalOut int
|
|
wantPeakCtx int
|
|
wantHasUsage bool
|
|
}{
|
|
{
|
|
name: "empty doc",
|
|
doc: map[string]any{},
|
|
wantHasUsage: false,
|
|
},
|
|
{
|
|
name: "model only",
|
|
doc: map[string]any{
|
|
"model": map[string]any{"model": "gpt-4o", "provider": "openai"},
|
|
},
|
|
wantModel: "gpt-4o",
|
|
wantHasUsage: false,
|
|
},
|
|
{
|
|
name: "model and request usage",
|
|
doc: map[string]any{
|
|
"model": map[string]any{"model": "claude-opus-4", "provider": "anthropic"},
|
|
"request_token_usage": map[string]any{
|
|
"r1": map[string]any{"input_tokens": float64(1000), "output_tokens": float64(100)},
|
|
"r2": map[string]any{"input_tokens": float64(2000), "output_tokens": float64(200)},
|
|
},
|
|
},
|
|
wantModel: "claude-opus-4",
|
|
wantTotalIn: 3000,
|
|
wantTotalOut: 300,
|
|
wantPeakCtx: 2000,
|
|
wantHasUsage: true,
|
|
},
|
|
{
|
|
name: "empty request_token_usage",
|
|
doc: map[string]any{
|
|
"model": map[string]any{"model": "gpt-5.5", "provider": "newapi"},
|
|
"request_token_usage": map[string]any{},
|
|
},
|
|
wantModel: "gpt-5.5",
|
|
wantHasUsage: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := extractZedDocMeta(tc.doc)
|
|
if got.model != tc.wantModel {
|
|
t.Errorf("model = %q, want %q", got.model, tc.wantModel)
|
|
}
|
|
if got.totalInputTokens != tc.wantTotalIn {
|
|
t.Errorf("totalInputTokens = %d, want %d", got.totalInputTokens, tc.wantTotalIn)
|
|
}
|
|
if got.totalOutputTokens != tc.wantTotalOut {
|
|
t.Errorf("totalOutputTokens = %d, want %d", got.totalOutputTokens, tc.wantTotalOut)
|
|
}
|
|
if got.peakContextTokens != tc.wantPeakCtx {
|
|
t.Errorf("peakContextTokens = %d, want %d", got.peakContextTokens, tc.wantPeakCtx)
|
|
}
|
|
if got.hasTokenUsage != tc.wantHasUsage {
|
|
t.Errorf("hasTokenUsage = %v, want %v", got.hasTokenUsage, tc.wantHasUsage)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseZedSessions_ZstdAndFiltersChildren(t *testing.T) {
|
|
encoder, err := zstd.NewWriter(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
compressed := encoder.EncodeAll([]byte(`{
|
|
"messages": [{"User": {"content": [{"Text": "Hello"}]}}]
|
|
}`), nil)
|
|
encoder.Close()
|
|
|
|
dbPath := createZedThreadsDB(t, []zedTestThread{
|
|
{
|
|
id: "parent",
|
|
summary: "Parent",
|
|
updatedAt: "2026-06-08T09:14:10Z",
|
|
dataType: "zstd",
|
|
data: compressed,
|
|
},
|
|
{
|
|
id: "child",
|
|
summary: "Child",
|
|
parentID: "parent",
|
|
updatedAt: "2026-06-08T09:14:11Z",
|
|
dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"skip"}]}}]}`),
|
|
},
|
|
})
|
|
|
|
results, err := parseZedAll(dbPath, "local")
|
|
if err != nil {
|
|
t.Fatalf("ParseZedSessions: %v", err)
|
|
}
|
|
if len(results) != 1 || results[0].Session.ID != "zed:parent" {
|
|
t.Fatalf("results = %+v", results)
|
|
}
|
|
if results[0].Messages[0].Content != "Hello" {
|
|
t.Fatalf("message = %+v", results[0].Messages[0])
|
|
}
|
|
}
|
|
|
|
func TestParseZedSessions_SkipsUnsupportedDataType(t *testing.T) {
|
|
dbPath := createZedThreadsDB(t, []zedTestThread{{
|
|
id: "bad",
|
|
dataType: "brotli",
|
|
data: []byte("x"),
|
|
}})
|
|
results, err := parseZedAll(dbPath, "local")
|
|
if err != nil {
|
|
t.Fatalf("ParseZedSessions: %v", err)
|
|
}
|
|
if len(results) != 0 {
|
|
t.Fatalf("len(results) = %d, want 0", len(results))
|
|
}
|
|
}
|
|
|
|
func TestParseZedVirtualPath(t *testing.T) {
|
|
dbPath := filepath.Join("/tmp", "with#hash", "threads.db")
|
|
virtual := ZedSQLiteVirtualPath(dbPath, "sess-1")
|
|
gotDB, gotID, ok := parseZedVirtualPath(virtual)
|
|
if !ok || gotDB != dbPath || gotID != "sess-1" {
|
|
t.Fatalf("parseZedVirtualPath = (%q, %q, %v)", gotDB, gotID, ok)
|
|
}
|
|
if _, _, ok := parseZedVirtualPath("/tmp/not-db#sess-1"); ok {
|
|
t.Fatal("parseZedVirtualPath accepted non-threads DB")
|
|
}
|
|
}
|
|
|
|
type zedTestThread struct {
|
|
id string
|
|
summary string
|
|
updatedAt string
|
|
dataType string
|
|
data []byte
|
|
parentID string
|
|
folderPaths string
|
|
createdAt string
|
|
}
|
|
|
|
func createZedThreadsDB(t *testing.T, threads []zedTestThread) string {
|
|
t.Helper()
|
|
dbPath := filepath.Join(t.TempDir(), "threads.db")
|
|
createZedThreadsDBAt(t, dbPath, threads)
|
|
return dbPath
|
|
}
|
|
|
|
func createZedThreadsDBAt(
|
|
t *testing.T,
|
|
dbPath string,
|
|
threads []zedTestThread,
|
|
) {
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer db.Close()
|
|
_, err = db.Exec(`CREATE TABLE threads (
|
|
id TEXT PRIMARY KEY,
|
|
summary TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL,
|
|
data_type TEXT NOT NULL,
|
|
data BLOB NOT NULL,
|
|
parent_id TEXT,
|
|
folder_paths TEXT,
|
|
folder_paths_order TEXT,
|
|
created_at TEXT
|
|
)`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, thread := range threads {
|
|
_, err = db.Exec(`INSERT INTO threads (
|
|
id, summary, updated_at, data_type, data,
|
|
parent_id, folder_paths, created_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
thread.id,
|
|
thread.summary,
|
|
thread.updatedAt,
|
|
thread.dataType,
|
|
thread.data,
|
|
nullString(thread.parentID),
|
|
thread.folderPaths,
|
|
thread.createdAt,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func nullString(s string) any {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return s
|
|
}
|