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

669 lines
25 KiB
Go

package parser
import (
"context"
"database/sql"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const shelleySchema = `
CREATE TABLE conversations (
conversation_id TEXT PRIMARY KEY,
slug TEXT,
user_initiated BOOLEAN NOT NULL DEFAULT TRUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
cwd TEXT,
archived BOOLEAN NOT NULL DEFAULT FALSE,
parent_conversation_id TEXT,
model TEXT,
current_generation INTEGER NOT NULL DEFAULT 1,
is_draft BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE messages (
message_id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
sequence_id INTEGER NOT NULL,
type TEXT NOT NULL,
llm_data TEXT,
user_data TEXT,
usage_data TEXT,
display_data TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
generation INTEGER NOT NULL DEFAULT 1,
excluded_from_context BOOLEAN NOT NULL DEFAULT FALSE
);
`
func newShelleyTestDB(t *testing.T) (string, string, *sql.DB) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, shelleyDBName)
db, err := sql.Open("sqlite3", path)
require.NoError(t, err, "open shelley test db")
t.Cleanup(func() { db.Close() })
_, err = db.Exec(shelleySchema)
require.NoError(t, err, "create shelley schema")
return dir, path, db
}
func newShelleyTestDBWithSchema(
t *testing.T, schema string,
) (string, string, *sql.DB) {
t.Helper()
dir := t.TempDir()
path := filepath.Join(dir, shelleyDBName)
db, err := sql.Open("sqlite3", path)
require.NoError(t, err, "open shelley test db")
t.Cleanup(func() { db.Close() })
_, err = db.Exec(schema)
require.NoError(t, err, "create shelley schema")
return dir, path, db
}
func seedShelleyConversation(
t *testing.T, db *sql.DB,
id, slug, cwd, model, parent string, userInitiated bool,
createdAt, updatedAt string,
) {
t.Helper()
_, err := db.Exec(
`INSERT INTO conversations
(conversation_id, slug, user_initiated, created_at,
updated_at, cwd, parent_conversation_id, model)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
id, slug, userInitiated, createdAt, updatedAt, cwd,
nullable(parent), nullable(model),
)
require.NoError(t, err, "seed shelley conversation %s", id)
}
func seedShelleyMessage(
t *testing.T, db *sql.DB,
convID string, seq int, generation int, msgType,
llmData, userData, usageData, createdAt string,
) {
t.Helper()
_, err := db.Exec(
`INSERT INTO messages
(message_id, conversation_id, sequence_id, generation,
type, llm_data, user_data, usage_data, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
convID+"-m"+itoa(seq), convID, seq, generation, msgType,
nullable(llmData), nullable(userData), nullable(usageData),
createdAt,
)
require.NoError(t, err, "seed shelley message %s/%d", convID, seq)
}
func nullable(s string) any {
if s == "" {
return nil
}
return s
}
func itoa(i int) string {
return string(rune('0' + i))
}
// seedShelleyMainConversation seeds a conversation that exercises text,
// thinking, a tool call, a tool result, per-message token usage, and a
// second-generation (post-compaction) message.
func seedShelleyMainConversation(t *testing.T, db *sql.DB) {
t.Helper()
seedShelleyConversation(
t, db, "cMAIN1", "Add Shelley parser",
"/home/user/dev/myapp", "claude-sonnet-4-6", "", true,
"2026-06-15T10:00:00Z", "2026-06-15T10:05:00Z",
)
seedShelleyMessage(t, db, "cMAIN1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"Add a Shelley parser"}]}`,
"", "", "2026-06-15T10:00:00Z")
seedShelleyMessage(t, db, "cMAIN1", 2, 1, "agent",
`{"Role":1,"Content":[`+
`{"Type":3,"Thinking":"Plan it."},`+
`{"Type":2,"Text":"On it."},`+
`{"ID":"toolu_1","Type":5,"ToolName":"bash","ToolInput":{"cmd":"ls"}}]}`,
"",
`{"input_tokens":1000,"cache_creation_input_tokens":200,`+
`"cache_read_input_tokens":50,"output_tokens":300,`+
`"cost_usd":0.012,"model":"claude-sonnet-4-6"}`,
"2026-06-15T10:00:05Z")
seedShelleyMessage(t, db, "cMAIN1", 3, 1, "tool",
`{"Role":0,"Content":[{"Type":6,"ToolUseID":"toolu_1",`+
`"ToolResult":[{"Type":2,"Text":"file1\nfile2"}],"ToolError":false}]}`,
"", "", "2026-06-15T10:00:06Z")
seedShelleyMessage(t, db, "cMAIN1", 4, 1, "agent",
`{"Role":1,"Content":[{"Type":2,"Text":"Done."}]}`,
"",
`{"input_tokens":1500,"cache_creation_input_tokens":0,`+
`"cache_read_input_tokens":0,"output_tokens":120,`+
`"cost_usd":0.004,"model":"claude-sonnet-4-6"}`,
"2026-06-15T10:00:10Z")
// Second generation: a post-compaction continuation that must still
// be included in the full history view.
seedShelleyMessage(t, db, "cMAIN1", 5, 2, "agent",
`{"Role":1,"Content":[{"Type":2,"Text":"Continued after compaction."}]}`,
"",
`{"input_tokens":800,"cache_creation_input_tokens":0,`+
`"cache_read_input_tokens":0,"output_tokens":90,`+
`"model":"claude-sonnet-4-6"}`,
"2026-06-15T10:05:00Z")
}
// parseShelleyConversationDirectForTest exercises the Shelley provider's
// single-conversation parse in isolation, mirroring the legacy direct-parse
// entrypoint the fold removed.
func parseShelleyConversationDirectForTest(
t *testing.T, dbPath, rawID, machine string, _ os.FileInfo,
) (*ParseResult, error) {
t.Helper()
return shelleyParseMember(
multiSessionSource{Container: dbPath, MemberID: rawID},
ParseRequest{Machine: machine},
)
}
func TestParseShelleyConversation(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyMainConversation(t, db)
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(
t, dbPath, "cMAIN1", "test-machine", info,
)
require.NoError(t, err, "parseConversationDirect")
require.NotNil(t, result, "expected result")
sess := result.Session
assert.Equal(t, "shelley:cMAIN1", sess.ID, "ID")
assert.Equal(t, AgentShelley, sess.Agent, "Agent")
assert.Equal(t, "test-machine", sess.Machine, "Machine")
assert.Equal(t, "myapp", sess.Project, "Project")
assert.Equal(t, "/home/user/dev/myapp", sess.Cwd, "Cwd")
assert.Equal(t, "Add Shelley parser", sess.SessionName, "SessionName")
assert.Equal(t, "Add a Shelley parser", sess.FirstMessage, "FirstMessage")
assert.Equal(t, 5, sess.MessageCount, "MessageCount")
assert.Equal(t, 1, sess.UserMessageCount, "UserMessageCount")
assert.Equal(t, dbPath+"#cMAIN1", sess.File.Path, "File.Path")
assert.Empty(t, sess.ParentSessionID, "ParentSessionID")
// Session token aggregates: peak context is a MAX, total output a SUM.
assert.Equal(t, 1500, sess.PeakContextTokens, "PeakContextTokens")
assert.Equal(t, 510, sess.TotalOutputTokens, "TotalOutputTokens")
assert.True(t, sess.HasPeakContextTokens, "HasPeakContextTokens")
assert.True(t, sess.HasTotalOutputTokens, "HasTotalOutputTokens")
msgs := result.Messages
require.Len(t, msgs, 5, "messages len")
// Ordinals come straight from sequence_id.
for i, m := range msgs {
assert.Equalf(t, i+1, m.Ordinal, "msg[%d].Ordinal", i)
}
// User message.
assert.Equal(t, RoleUser, msgs[0].Role, "msg[0].Role")
assert.Equal(t, "Add a Shelley parser", msgs[0].Content, "msg[0].Content")
// Agent message: text + thinking + tool call + tokens.
a := msgs[1]
assert.Equal(t, RoleAssistant, a.Role, "msg[1].Role")
assert.Equal(t, "On it.", a.Content, "msg[1].Content")
assert.True(t, a.HasThinking, "msg[1].HasThinking")
assert.Equal(t, "Plan it.", a.ThinkingText, "msg[1].ThinkingText")
assert.True(t, a.HasToolUse, "msg[1].HasToolUse")
require.Len(t, a.ToolCalls, 1, "msg[1].ToolCalls len")
assert.Equal(t, "bash", a.ToolCalls[0].ToolName, "tool name")
assert.Equal(t, "Bash", a.ToolCalls[0].Category, "tool category")
assert.Equal(t, "toolu_1", a.ToolCalls[0].ToolUseID, "tool use id")
assert.JSONEq(t, `{"cmd":"ls"}`, a.ToolCalls[0].InputJSON, "tool input")
assert.Equal(t, 1250, a.ContextTokens, "msg[1].ContextTokens")
assert.Equal(t, 300, a.OutputTokens, "msg[1].OutputTokens")
assert.True(t, a.HasContextTokens, "msg[1].HasContextTokens")
assert.True(t, a.HasOutputTokens, "msg[1].HasOutputTokens")
assert.Equal(t, "claude-sonnet-4-6", a.Model, "msg[1].Model")
assert.NotEmpty(t, a.TokenUsage, "msg[1].TokenUsage raw")
// Tool result message: user role, empty content, paired by ToolUseID.
tr := msgs[2]
assert.Equal(t, RoleUser, tr.Role, "msg[2].Role")
assert.Empty(t, tr.Content, "msg[2].Content")
require.Len(t, tr.ToolResults, 1, "msg[2].ToolResults len")
assert.Equal(t, "toolu_1", tr.ToolResults[0].ToolUseID, "result tool use id")
assert.Equal(t, "file1\nfile2",
DecodeContent(tr.ToolResults[0].ContentRaw), "decoded tool result")
// Final first-generation agent message.
assert.Equal(t, "Done.", msgs[3].Content, "msg[3].Content")
assert.Equal(t, 120, msgs[3].OutputTokens, "msg[3].OutputTokens")
// Second-generation message is included in the history.
assert.Equal(t, "Continued after compaction.", msgs[4].Content, "msg[4].Content")
}
func TestParseShelleySubagentRelationship(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyMainConversation(t, db)
seedShelleyConversation(
t, db, "cSUB01", "do subtask",
"/home/user/dev/myapp", "claude-sonnet-4-6", "cMAIN1", false,
"2026-06-15T10:01:00Z", "2026-06-15T10:01:30Z",
)
seedShelleyMessage(t, db, "cSUB01", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"do the subtask"}]}`,
"", "", "2026-06-15T10:01:00Z")
seedShelleyMessage(t, db, "cSUB01", 2, 1, "agent",
`{"Role":1,"Content":[{"Type":2,"Text":"subtask done"}]}`,
"", "", "2026-06-15T10:01:30Z")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(
t, dbPath, "cSUB01", "test-machine", info,
)
require.NoError(t, err, "parse subagent")
require.NotNil(t, result, "expected subagent result")
assert.Equal(t, "shelley:cSUB01", result.Session.ID, "subagent ID")
assert.Equal(t, "shelley:cMAIN1", result.Session.ParentSessionID, "parent")
assert.Equal(t, RelSubagent, result.Session.RelationshipType, "relationship")
}
func TestDiscoverAndFindShelley(t *testing.T) {
root, dbPath, db := newShelleyTestDB(t)
seedShelleyMainConversation(t, db)
// Discovery and source lookup are provider-owned after the fold; the
// physical DB still surfaces as a single source and a raw conversation
// ID resolves to its virtual path.
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, sources, 1, "discovered sources")
assert.Equal(t, dbPath, sources[0].DisplayPath, "discovered db path")
assert.Equal(t, AgentShelley, sources[0].Provider, "discovered provider")
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: "cMAIN1",
})
require.NoError(t, err)
require.True(t, ok, "find existing")
assert.Equal(t, dbPath+"#cMAIN1", found.DisplayPath, "find existing path")
_, ok, err = provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: "cNOPE0",
})
require.NoError(t, err)
assert.False(t, ok, "find missing")
_, ok, err = provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: "../escape",
})
require.NoError(t, err)
assert.False(t, ok, "reject path-like id")
assert.True(t, ShelleyConversationExists(dbPath, "cMAIN1"), "exists")
assert.False(t, ShelleyConversationExists(dbPath, "cNOPE0"), "not exists")
// Empty root yields no discovery.
emptyProvider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{t.TempDir()}})
require.True(t, ok)
emptySources, err := emptyProvider.Discover(context.Background())
require.NoError(t, err)
assert.Empty(t, emptySources, "empty dir discovery")
}
func TestParseShelleyVirtualPath(t *testing.T) {
dbPath := "/home/user/.config/shelley/shelley.db"
got, id, ok := parseShelleyVirtualPath(dbPath + "#cABC123")
require.True(t, ok, "valid virtual path")
assert.Equal(t, dbPath, got, "db path")
assert.Equal(t, "cABC123", id, "conversation id")
_, _, ok = parseShelleyVirtualPath("/x/other.db#id")
assert.False(t, ok, "wrong db name")
_, _, ok = parseShelleyVirtualPath(dbPath)
assert.False(t, ok, "no separator")
_, _, ok = parseShelleyVirtualPath(dbPath + "#")
assert.False(t, ok, "empty id")
}
func TestAgentByPrefixShelley(t *testing.T) {
def, ok := AgentByPrefix("shelley:cMAIN1")
require.True(t, ok, "shelley prefix matches")
assert.Equal(t, AgentShelley, def.Type, "shelley type")
def, ok = AgentByPrefix("host~shelley:cMAIN1")
require.True(t, ok, "host-prefixed shelley matches")
assert.Equal(t, AgentShelley, def.Type, "host-prefixed type")
// A colon-free ID must fall back to Claude, never Shelley.
def, ok = AgentByPrefix("cMAIN1")
require.True(t, ok, "colon-free id matches Claude fallback")
assert.Equal(t, AgentClaude, def.Type, "colon-free routes to Claude")
}
func TestShelleyTokenCount(t *testing.T) {
tests := []struct {
name string
in json.Number
want int
}{
{"empty", json.Number(""), 0},
{"plain", json.Number("1234"), 1234},
{"zero", json.Number("0"), 0},
{"negative", json.Number("-5"), 0},
{"float", json.Number("42.0"), 42},
{"garbage", json.Number("abc"), 0},
{"implausible", json.Number("9999999999999"), 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, shelleyTokenCount(tt.in))
})
}
}
// TestParseShelleyTimestampFormats verifies the real on-disk DATETIME
// format Shelley writes (space-separated, no T/Z) parses into non-zero
// timestamps. Shelley relies on SQLite's DEFAULT CURRENT_TIMESTAMP, so
// stored values look like "2026-06-15 10:00:00".
func TestParseShelleyTimestampFormats(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyConversation(
t, db, "cTIME1", "ts", "/home/user/dev/app",
"claude-sonnet-4-6", "", true,
"2026-06-15 10:00:00", "2026-06-15 10:00:30",
)
seedShelleyMessage(t, db, "cTIME1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"hi"}]}`,
"", "", "2026-06-15 10:00:00")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(t, dbPath, "cTIME1", "m", info)
require.NoError(t, err)
require.NotNil(t, result)
assert.False(t, result.Session.StartedAt.IsZero(), "StartedAt parsed")
assert.False(t, result.Session.EndedAt.IsZero(), "EndedAt parsed")
assert.Positive(t, result.Session.File.Mtime, "File.Mtime positive")
}
// TestParseShelleyRobustContent verifies graceful handling of unknown
// content types, redacted thinking, malformed llm_data, and token
// capture on errored assistant turns (type="error").
func TestParseShelleyRobustContent(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyConversation(
t, db, "cROB1", "robust", "/home/user/dev/app",
"claude-sonnet-4-6", "", true,
"2026-06-15T10:00:00Z", "2026-06-15T10:00:40Z",
)
seedShelleyMessage(t, db, "cROB1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"go"}]}`,
"", "", "2026-06-15T10:00:00Z")
// Unknown content type (99) is ignored; redacted thinking (2) is
// surfaced as a placeholder; the text block survives.
seedShelleyMessage(t, db, "cROB1", 2, 1, "agent",
`{"Role":1,"Content":[{"Type":4},{"Type":99,"Text":"ignored"},`+
`{"Type":2,"Text":"real text"}]}`,
"", "", "2026-06-15T10:00:05Z")
// Malformed llm_data with no user_data fallback is dropped.
seedShelleyMessage(t, db, "cROB1", 3, 1, "agent",
`{not json`, "", "", "2026-06-15T10:00:06Z")
// Errored assistant turn stored as type="error" still carries usage.
seedShelleyMessage(t, db, "cROB1", 4, 1, "error",
`{"Role":1,"Content":[{"Type":2,"Text":"request failed"}]}`,
"",
`{"input_tokens":700,"output_tokens":40,"model":"claude-sonnet-4-6"}`,
"2026-06-15T10:00:40Z")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(t, dbPath, "cROB1", "m", info)
require.NoError(t, err, "must not error on robust content")
require.NotNil(t, result)
// user + agent(real text) + error message; the malformed row dropped.
require.Len(t, result.Messages, 3, "messages len")
agentMsg := result.Messages[1]
assert.Equal(t, "real text", agentMsg.Content, "unknown type ignored, text kept")
assert.Contains(t, agentMsg.ThinkingText, "redacted", "redacted thinking placeholder")
errMsg := result.Messages[2]
assert.True(t, errMsg.IsSystem, "error message flagged system")
assert.Equal(t, "request failed", errMsg.Content, "error text preserved")
assert.Equal(t, 40, errMsg.OutputTokens, "errored-turn output tokens captured")
assert.Equal(t, 700, errMsg.ContextTokens, "errored-turn input tokens captured")
// Errored-turn tokens roll up into the session totals.
assert.Equal(t, 40, result.Session.TotalOutputTokens, "session output total")
}
func TestParseShelleyUsageOnlyRows(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyConversation(
t, db, "cUSG1", "usage only", "/home/user/dev/app",
"claude-sonnet-4-6", "", true,
"2026-06-15T10:00:00Z", "2026-06-15T10:00:40Z",
)
seedShelleyMessage(t, db, "cUSG1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"go"}]}`,
"", "", "2026-06-15T10:00:00Z")
seedShelleyMessage(t, db, "cUSG1", 2, 1, "error",
`{not json`, "",
`{"input_tokens":123,"output_tokens":33,"model":"claude-sonnet-4-6"}`,
"2026-06-15T10:00:40Z")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(t, dbPath, "cUSG1", "m", info)
require.NoError(t, err, "must not error on usage-only row")
require.NotNil(t, result)
require.Len(t, result.Messages, 2, "messages len")
usageOnly := result.Messages[1]
assert.True(t, usageOnly.IsSystem, "usage-only row is metadata")
assert.Empty(t, usageOnly.Content, "usage-only content")
assert.Equal(t, 123, usageOnly.ContextTokens, "context tokens")
assert.Equal(t, 33, usageOnly.OutputTokens, "output tokens")
assert.True(t, usageOnly.HasContextTokens, "has context tokens")
assert.True(t, usageOnly.HasOutputTokens, "has output tokens")
assert.NotEmpty(t, usageOnly.TokenUsage, "raw token usage")
assert.Equal(t, 123, result.Session.PeakContextTokens, "session peak context")
assert.Equal(t, 33, result.Session.TotalOutputTokens, "session output total")
}
// TestParseShelleyWebSearchToolResult verifies that a server-side web
// search turn is preserved: the web_search call (Type 7) becomes a tool
// call, and the web_search_tool_result (Type 8) whose nested
// web_search_result blocks (Type 9) carry Title/URL instead of Text is
// stored with readable content rather than dropped empty.
func TestParseShelleyWebSearchToolResult(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyConversation(
t, db, "cWEB1", "search the web", "/home/user/dev/app",
"claude-sonnet-4-6", "", true,
"2026-06-15T10:00:00Z", "2026-06-15T10:00:30Z",
)
seedShelleyMessage(t, db, "cWEB1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"what is iota"}]}`,
"", "", "2026-06-15T10:00:00Z")
seedShelleyMessage(t, db, "cWEB1", 2, 1, "agent",
`{"Role":1,"Content":[`+
`{"ID":"srvtoolu_1","Type":7,"ToolName":"web_search",`+
`"ToolInput":{"query":"golang iota"}},`+
`{"Type":8,"ToolUseID":"srvtoolu_1","ToolResult":[`+
`{"Type":9,"Title":"Go iota explained","URL":"https://go.dev/iota"},`+
`{"Type":9,"Title":"Effective Go",`+
`"URL":"https://go.dev/doc/effective_go"}]}]}`,
"", "", "2026-06-15T10:00:05Z")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(t, dbPath, "cWEB1", "m", info)
require.NoError(t, err, "parse web search conversation")
require.NotNil(t, result, "expected result")
require.Len(t, result.Messages, 2, "messages len")
agent := result.Messages[1]
// The server-side web_search call is captured as a tool call.
require.Len(t, agent.ToolCalls, 1, "web_search tool call captured")
assert.Equal(t, "web_search", agent.ToolCalls[0].ToolName, "tool name")
// The web_search_tool_result is paired to the server tool call and its
// nested Title/URL result blocks are preserved, not stored empty.
require.Len(t, agent.ToolResults, 1, "web_search tool result captured")
assert.Equal(t, "srvtoolu_1",
agent.ToolResults[0].ToolUseID, "result tool use id")
decoded := DecodeContent(agent.ToolResults[0].ContentRaw)
assert.Equal(t,
"Go iota explained https://go.dev/iota\n"+
"Effective Go https://go.dev/doc/effective_go",
decoded, "web search result title/url preserved")
assert.Positive(t, agent.ToolResults[0].ContentLength,
"content length nonzero")
}
// TestShelleySameSecondChangeSignal verifies the split change signal: a
// message appended in the same wall-clock second (so updated_at is
// unchanged) leaves File.Mtime fixed at the conversation's real
// timestamp but shifts the content fingerprint (stored in file_hash), so
// the sync skip still re-parses. The watcher-only ShelleySourceMtime,
// compared for inequality, must also change. The stored values and the
// meta skip query must agree, or unchanged conversations would re-parse
// forever.
func TestShelleySameSecondChangeSignal(t *testing.T) {
_, dbPath, db := newShelleyTestDB(t)
seedShelleyConversation(
t, db, "cSEC1", "same second", "/home/user/dev/app",
"claude-sonnet-4-6", "", true,
"2026-06-15T10:00:00Z", "2026-06-15T10:00:00Z",
)
seedShelleyMessage(t, db, "cSEC1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"first"}]}`,
"", "", "2026-06-15T10:00:00Z")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
conn, err := OpenShelleyDB(dbPath)
require.NoError(t, err, "open shelley db")
defer conn.Close()
first, err := parseShelleyConversationDirectForTest(t, dbPath, "cSEC1", "m", info)
require.NoError(t, err)
require.NotNil(t, first)
mtime1 := first.Session.File.Mtime
hash1 := first.Session.File.Hash
// File.Mtime is the conversation's real timestamp, so it stays a valid
// value for modified-between range queries (never a synthetic future).
base := parseTimestamp("2026-06-15T10:00:00Z").UnixNano()
assert.Equal(t, base, mtime1, "File.Mtime is the real updated_at")
assert.NotEmpty(t, hash1, "content fingerprint set")
metas1, err := ListShelleyConversationMetas(conn, dbPath)
require.NoError(t, err)
require.Len(t, metas1, 1)
assert.Equal(t, mtime1, metas1[0].FileMtime,
"stored File.Mtime must match the meta skip timestamp")
assert.Equal(t, hash1, metas1[0].Fingerprint,
"stored file_hash must match the meta skip fingerprint")
srcMtime1, err := ShelleySourceMtime(dbPath + "#cSEC1")
require.NoError(t, err)
assert.Positive(t, srcMtime1, "SourceMtime resolves the conversation")
// Append a second message in the SAME second: updated_at is unchanged,
// sequence_id advances (1 -> 2) and the payload adds content bytes.
seedShelleyMessage(t, db, "cSEC1", 2, 1, "agent",
`{"Role":1,"Content":[{"Type":2,"Text":"second"}]}`,
"", "", "2026-06-15T10:00:00Z")
second, err := parseShelleyConversationDirectForTest(t, dbPath, "cSEC1", "m", info)
require.NoError(t, err)
require.NotNil(t, second)
assert.Equal(t, mtime1, second.Session.File.Mtime,
"same-second append leaves the real timestamp unchanged")
assert.NotEqual(t, hash1, second.Session.File.Hash,
"a same-second append must change the content fingerprint")
metas2, err := ListShelleyConversationMetas(conn, dbPath)
require.NoError(t, err)
require.Len(t, metas2, 1)
assert.Equal(t, second.Session.File.Hash, metas2[0].Fingerprint,
"meta fingerprint tracks the same-second append")
srcMtime2, err := ShelleySourceMtime(dbPath + "#cSEC1")
require.NoError(t, err)
assert.NotEqual(t, srcMtime1, srcMtime2,
"watcher SourceMtime tracks the same-second append")
}
func TestShelleyNumericUserInitiatedScans(t *testing.T) {
schema := strings.Replace(
shelleySchema,
"user_initiated BOOLEAN NOT NULL DEFAULT TRUE",
"user_initiated INTEGER NOT NULL DEFAULT 1",
1,
)
_, dbPath, db := newShelleyTestDBWithSchema(t, schema)
seedShelleyConversation(
t, db, "cNUM1", "numeric user_initiated",
"/home/user/dev/app", "claude-sonnet-4-6", "cPARENT",
false, "2026-06-15T10:00:00Z",
"2026-06-15T10:00:10Z",
)
seedShelleyMessage(t, db, "cNUM1", 1, 1, "user",
`{"Role":0,"Content":[{"Type":2,"Text":"hello"}]}`,
"", "", "2026-06-15T10:00:00Z")
info, err := os.Stat(dbPath)
require.NoError(t, err, "stat db")
result, err := parseShelleyConversationDirectForTest(
t, dbPath, "cNUM1", "m", info,
)
require.NoError(t, err)
require.NotNil(t, result)
assert.Equal(t, RelSubagent, result.Session.RelationshipType,
"numeric false user_initiated should map to subagent")
conn, err := OpenShelleyDB(dbPath)
require.NoError(t, err, "open shelley db")
defer conn.Close()
metas, err := ListShelleyConversationMetas(conn, dbPath)
require.NoError(t, err)
require.Len(t, metas, 1)
assert.Equal(t, result.Session.File.Hash, metas[0].Fingerprint,
"parse and meta paths should convert user_initiated the same way")
}
func TestApplyShelleyUsageTolerant(t *testing.T) {
var msg ParsedMessage
applyShelleyUsage(&msg,
`{"input_tokens":"-5","cache_read_input_tokens":"100",`+
`"output_tokens":"42","model":""}`, "fallback-model")
assert.Equal(t, 100, msg.ContextTokens, "ContextTokens (negative input clamped)")
assert.Equal(t, 42, msg.OutputTokens, "OutputTokens")
assert.True(t, msg.HasContextTokens, "HasContextTokens")
assert.True(t, msg.HasOutputTokens, "HasOutputTokens")
assert.Equal(t, "fallback-model", msg.Model, "falls back to conversation model")
}