package parser import ( "database/sql" "os" "path/filepath" "strings" "testing" "time" _ "github.com/mattn/go-sqlite3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // newHermesTestProvider builds a concrete hermesProvider for the given roots so // package tests can exercise the folded parse, discovery, and source-lookup // behavior directly through provider methods. func newHermesTestProvider(t *testing.T, roots ...string) *hermesProvider { t.Helper() provider, ok := NewProvider(AgentHermes, ProviderConfig{ Roots: roots, Machine: "local", }) require.True(t, ok) hp, ok := provider.(*hermesProvider) require.True(t, ok) return hp } // parseHermesTestSession parses a Hermes transcript at path through the // provider-owned parse method, replacing the removed package-level // ParseHermesSession entrypoint. func parseHermesTestSession( t *testing.T, path, project, machine string, ) (*ParsedSession, []ParsedMessage, error) { t.Helper() return newHermesTestProvider(t).parseSession(path, project, machine) } // parseHermesTestArchive parses a Hermes archive root through the provider-owned // archive method, replacing the removed package-level ParseHermesArchive // entrypoint. func parseHermesTestArchive( t *testing.T, root, project, machine string, ) ([]ParseResult, error) { t.Helper() return newHermesTestProvider(t).parseArchive(root, project, machine) } // discoverHermesTestSessions discovers Hermes sources under root through the // provider source set, replacing the removed package-level // DiscoverHermesSessions entrypoint. func discoverHermesTestSessions(t *testing.T, root string) []DiscoveredFile { t.Helper() return discoverHermesSessions(root) } // findHermesTestSourceFile resolves a Hermes session ID to a transcript path // through the provider source set, replacing the removed package-level // FindHermesSourceFile entrypoint. func findHermesTestSourceFile(t *testing.T, sessionsDir, sessionID string) string { t.Helper() return findHermesSourceFile(sessionsDir, sessionID) } func runHermesJSONLTest( t *testing.T, filename, content string, ) (*ParsedSession, []ParsedMessage) { t.Helper() if filename == "" { filename = "20260403_153620_5a3e2ff1.jsonl" } path := createTestFile(t, filename, content) sess, msgs, err := parseHermesTestSession( t, path, "", "local", ) require.NoError(t, err) return sess, msgs } func runHermesJSONTest( t *testing.T, filename, content string, ) (*ParsedSession, []ParsedMessage) { t.Helper() if filename == "" { filename = "session_20260403_153620_5a3e2ff1.json" } path := createTestFile(t, filename, content) sess, msgs, err := parseHermesTestSession( t, path, "", "local", ) require.NoError(t, err) return sess, msgs } func createHermesStateDB(t *testing.T, root string) { t.Helper() db, err := sql.Open("sqlite3", filepath.Join(root, "state.db")) require.NoError(t, err) // Close the setup handle when this helper returns rather than at test // cleanup. Tests delete state.db mid-run to exercise deletion handling, and // Windows refuses to remove a file still held open by this process. defer func() { _ = db.Close() }() _, err = db.Exec(` CREATE TABLE sessions ( id TEXT PRIMARY KEY, source TEXT NOT NULL, user_id TEXT, model TEXT, model_config TEXT, system_prompt TEXT, parent_session_id TEXT, started_at REAL NOT NULL, ended_at REAL, end_reason TEXT, message_count INTEGER DEFAULT 0, tool_call_count INTEGER DEFAULT 0, input_tokens INTEGER DEFAULT 0, output_tokens INTEGER DEFAULT 0, cache_read_tokens INTEGER DEFAULT 0, cache_write_tokens INTEGER DEFAULT 0, reasoning_tokens INTEGER DEFAULT 0, billing_provider TEXT, billing_base_url TEXT, billing_mode TEXT, estimated_cost_usd REAL, actual_cost_usd REAL, cost_status TEXT, cost_source TEXT, pricing_version TEXT, title TEXT, api_call_count INTEGER DEFAULT 0 ); CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT, tool_call_id TEXT, tool_calls TEXT, tool_name TEXT, timestamp REAL NOT NULL, token_count INTEGER, finish_reason TEXT, reasoning TEXT, reasoning_content TEXT, reasoning_details TEXT, codex_reasoning_items TEXT, codex_message_items TEXT ); INSERT INTO sessions ( id, source, model, parent_session_id, started_at, ended_at, message_count, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, reasoning_tokens, estimated_cost_usd, cost_status, cost_source, title, api_call_count ) VALUES ( 'child', 'discord', 'gpt-5.4', 'parent', 1778767200.0, 1778767800.0, 1, 300, 70, 20, 5, 9, 0.123, 'estimated', 'hermes', 'Child Session', 4 ); INSERT INTO messages ( session_id, role, content, timestamp ) VALUES ( 'child', 'user', 'state db only has one message', 1778767210.0 ); `) require.NoError(t, err) } func TestParseHermesArchive_StateDBMetadataUsageAndTranscriptChoice( t *testing.T, ) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) createHermesStateDB(t, root) require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "session_child.json"), []byte(`{ "platform":"discord", "session_start":"2026-05-14T10:00:00Z", "last_updated":"2026-05-14T10:20:00Z", "messages":[ {"role":"user","content":"hello from transcript","timestamp":"2026-05-14T10:01:00Z"}, {"role":"assistant","content":"reply from transcript","timestamp":"2026-05-14T10:02:00Z"} ] }`), 0o644, )) results, err := parseHermesTestArchive(t, root, "", "local") require.NoError(t, err) require.Len(t, results, 1) res := results[0] assert.Equal(t, "hermes:child", res.Session.ID) assert.Equal(t, "hermes:parent", res.Session.ParentSessionID) assert.Equal(t, RelContinuation, res.Session.RelationshipType) assert.Equal(t, "Child Session", res.Session.SessionName) assert.Equal(t, "hermes-discord", res.Session.Project) assert.Equal(t, "child", res.Session.SourceSessionID) assert.Equal(t, "hermes-state-db", res.Session.SourceVersion) require.Len(t, res.Messages, 2) assert.Equal(t, "hello from transcript", res.Messages[0].Content) require.Len(t, res.UsageEvents, 1) assert.Equal(t, "gpt-5.4", res.UsageEvents[0].Model) assert.Equal(t, 300, res.UsageEvents[0].InputTokens) assert.Equal(t, 70, res.UsageEvents[0].OutputTokens) assert.Equal(t, 20, res.UsageEvents[0].CacheReadInputTokens) assert.Equal(t, 5, res.UsageEvents[0].CacheCreationInputTokens) assert.Equal(t, 9, res.UsageEvents[0].ReasoningTokens) } func TestParseHermesArchive_FallsBackToTranscriptsWhenStateDBUnreadable( t *testing.T, ) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) require.NoError(t, os.WriteFile( filepath.Join(root, "state.db"), []byte("not sqlite"), 0o644, )) require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "session_child.json"), []byte(`{ "platform":"discord", "session_start":"2026-05-14T10:00:00Z", "last_updated":"2026-05-14T10:20:00Z", "messages":[ {"role":"user","content":"hello from transcript","timestamp":"2026-05-14T10:01:00Z"}, {"role":"assistant","content":"reply from transcript","timestamp":"2026-05-14T10:02:00Z"} ] }`), 0o644, )) results, err := parseHermesTestArchive(t, root, "override-project", "local") require.NoError(t, err) require.Len(t, results, 1) res := results[0] assert.Equal(t, "hermes:child", res.Session.ID) assert.Equal(t, "override-project", res.Session.Project) assert.Equal(t, "hello from transcript", res.Session.FirstMessage) assert.Len(t, res.Messages, 2) assert.Empty(t, res.UsageEvents) } func TestWriteHermesSessionJSONL_UsesMatchingProfileStateDB(t *testing.T) { defaultRoot := t.TempDir() defaultSessions := filepath.Join(defaultRoot, "sessions") require.NoError(t, os.MkdirAll(defaultSessions, 0o755)) createHermesStateDB(t, defaultRoot) defaultDB, err := sql.Open("sqlite3", filepath.Join(defaultRoot, "state.db")) require.NoError(t, err) defer func() { _ = defaultDB.Close() }() _, err = defaultDB.Exec(` DELETE FROM messages; DELETE FROM sessions; INSERT INTO sessions ( id, source, model, started_at, ended_at, message_count, title ) VALUES ( 'default-only', 'default', 'gpt-default', 1778767200.0, 1778767800.0, 1, 'Default Session' ); INSERT INTO messages ( session_id, role, content, timestamp ) VALUES ( 'default-only', 'user', 'wrong root message', 1778767210.0 ); `) require.NoError(t, err) profileRoot := t.TempDir() profileSessions := filepath.Join(profileRoot, "sessions") require.NoError(t, os.MkdirAll(profileSessions, 0o755)) createHermesStateDB(t, profileRoot) profileDB, err := sql.Open("sqlite3", filepath.Join(profileRoot, "state.db")) require.NoError(t, err) defer func() { _ = profileDB.Close() }() _, err = profileDB.Exec(` INSERT INTO sessions ( id, source, model, started_at, ended_at, message_count, title ) VALUES ( 'sibling', 'profile', 'gpt-sibling', 1778767200.0, 1778767800.0, 1, 'Sibling Session' ); INSERT INTO messages ( session_id, role, content, timestamp ) VALUES ( 'sibling', 'user', 'sibling message', 1778767211.0 ); `) require.NoError(t, err) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, filepath.Join(profileRoot, "state.db"), []string{defaultSessions, profileSessions}, "child", )) out := buf.String() assert.NotContains(t, out, "SQLite format 3") assert.Contains(t, out, `"role":"session_meta"`) assert.Contains(t, out, "state db only has one message") assert.NotContains(t, out, "wrong root message") assert.NotContains(t, out, "sibling message") for line := range strings.SplitSeq(strings.TrimSpace(out), "\n") { assert.JSONEq(t, line, line) } } func TestWriteHermesSessionJSONL_TranscriptSourceCopiesFile(t *testing.T) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) body := strings.Join([]string{ `{"role":"session_meta","model":"gpt-4","timestamp":"2026-04-03T15:27:00Z"}`, `{"role":"user","content":"hello from transcript","timestamp":"2026-04-03T15:28:00Z"}`, "", }, "\n") require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "child.jsonl"), []byte(body), 0o644, )) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, filepath.Join(root, "state.db"), []string{sessionsDir}, "child", )) assert.Equal(t, body, buf.String()) } func TestWriteHermesSessionJSONL_FallsBackWhenStoredStateDBMissesSession(t *testing.T) { defaultRoot := t.TempDir() defaultSessions := filepath.Join(defaultRoot, "sessions") require.NoError(t, os.MkdirAll(defaultSessions, 0o755)) createHermesStateDB(t, defaultRoot) profileRoot := t.TempDir() profileSessions := filepath.Join(profileRoot, "sessions") require.NoError(t, os.MkdirAll(profileSessions, 0o755)) createHermesStateDB(t, profileRoot) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, filepath.Join(defaultRoot, "state.db"), []string{defaultSessions, profileSessions}, "child", )) assert.Contains(t, buf.String(), "state db only has one message") } func TestWriteHermesSessionJSONL_FallsBackToTranscriptWhenStateDBMissesSessionInSameRoot(t *testing.T) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) createHermesStateDB(t, root) dbPath := filepath.Join(root, "state.db") conn, err := sql.Open("sqlite3", dbPath) require.NoError(t, err) t.Cleanup(func() { _ = conn.Close() }) _, err = conn.Exec(`DELETE FROM messages WHERE session_id = 'child'`) require.NoError(t, err) _, err = conn.Exec(`DELETE FROM sessions WHERE id = 'child'`) require.NoError(t, err) body := strings.Join([]string{ `{"role":"session_meta","model":"gpt-4","timestamp":"2026-05-14T10:00:00Z"}`, `{"role":"user","content":"fallback transcript prompt","timestamp":"2026-05-14T10:01:00Z"}`, "", }, "\n") require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "child.jsonl"), []byte(body), 0o644, )) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, dbPath, []string{sessionsDir}, "child", )) assert.Equal(t, body, buf.String()) } func TestWriteHermesSessionJSONL_FallsBackWhenStoredStateDBUnreadable(t *testing.T) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) require.NoError(t, os.WriteFile( filepath.Join(root, "state.db"), []byte("not a sqlite database"), 0o644, )) body := strings.Join([]string{ `{"role":"session_meta","model":"gpt-4","timestamp":"2026-05-14T10:00:00Z"}`, `{"role":"user","content":"fallback unreadable transcript","timestamp":"2026-05-14T10:01:00Z"}`, "", }, "\n") require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "child.jsonl"), []byte(body), 0o644, )) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, filepath.Join(root, "state.db"), []string{sessionsDir}, "child", )) assert.Equal(t, body, buf.String()) } func TestWriteHermesSessionJSONL_PrioritizesAdjacentTranscriptBeforeRoots(t *testing.T) { for _, tt := range []struct { name string prepareRoot func(t *testing.T, root string) }{ { name: "missing stored state db", }, { name: "unreadable stored state db", prepareRoot: func(t *testing.T, root string) { t.Helper() require.NoError(t, os.WriteFile( filepath.Join(root, "state.db"), []byte("not a sqlite database"), 0o644, )) }, }, } { t.Run(tt.name, func(t *testing.T) { storedRoot := t.TempDir() storedSessions := filepath.Join(storedRoot, "sessions") require.NoError(t, os.MkdirAll(storedSessions, 0o755)) if tt.prepareRoot != nil { tt.prepareRoot(t, storedRoot) } body := strings.Join([]string{ `{"role":"session_meta","model":"gpt-4","timestamp":"2026-05-14T10:00:00Z"}`, `{"role":"user","content":"adjacent stored transcript","timestamp":"2026-05-14T10:01:00Z"}`, "", }, "\n") require.NoError(t, os.WriteFile( filepath.Join(storedSessions, "child.jsonl"), []byte(body), 0o644, )) otherRoot := t.TempDir() otherSessions := filepath.Join(otherRoot, "sessions") require.NoError(t, os.MkdirAll(otherSessions, 0o755)) createHermesStateDB(t, otherRoot) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, filepath.Join(storedRoot, "state.db"), []string{otherSessions, storedSessions}, "child", )) assert.Equal(t, body, buf.String()) }) } } func TestWriteHermesSessionJSONL_PrefersTranscriptWhenQualityWins(t *testing.T) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) createHermesStateDB(t, root) body := strings.Join([]string{ `{"role":"session_meta","model":"gpt-4","timestamp":"2026-05-14T10:00:00Z"}`, `{"role":"user","content":"transcript prompt","timestamp":"2026-05-14T10:01:00Z"}`, `{"role":"assistant","content":"transcript answer","timestamp":"2026-05-14T10:02:00Z"}`, "", }, "\n") require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "child.jsonl"), []byte(body), 0o644, )) var buf strings.Builder require.NoError(t, WriteHermesSessionJSONL( &buf, filepath.Join(root, "state.db"), []string{sessionsDir}, "child", )) assert.Equal(t, body, buf.String()) } func TestParseHermesArchive_UsesStateMessagesWhenJSONLIsLowerQuality( t *testing.T, ) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) createHermesStateDB(t, root) require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "child.jsonl"), []byte(strings.Join([]string{ `{"role":"session_meta","platform":"discord","timestamp":"2026-05-14T10:00:00Z"}`, `{"role":"user","content":"x","timestamp":"2026-05-14T10:01:00Z"}`, }, "\n")), 0o644, )) results, err := parseHermesTestArchive(t, root, "", "local") require.NoError(t, err) require.Len(t, results, 1) res := results[0] require.Len(t, res.Messages, 1) assert.Equal(t, "state db only has one message", res.Messages[0].Content) assert.Equal(t, "hermes-state-db", res.Session.SourceVersion) } func TestParseHermesArchiveIncludesTranscriptsMissingFromStateDB( t *testing.T, ) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) createHermesStateDB(t, root) require.NoError(t, os.WriteFile( filepath.Join(sessionsDir, "session_extra.json"), []byte(`{ "platform":"linux", "session_start":"2026-05-14T11:00:00Z", "messages":[ {"role":"user","content":"extra transcript","timestamp":"2026-05-14T11:01:00Z"} ] }`), 0o644, )) results, err := parseHermesTestArchive(t, root, "", "local") require.NoError(t, err) require.Len(t, results, 2) ids := []string{results[0].Session.ID, results[1].Session.ID} assert.Contains(t, ids, "hermes:child") assert.Contains(t, ids, "hermes:extra") } func TestBuildHermesStateResultKeepsUsageOnlySessions(t *testing.T) { res, ok := buildHermesStateResult( hermesStateSession{ id: "usage-only", source: "cli", model: "gpt-5.4", startedAt: time.Date(2026, 5, 14, 12, 0, 0, 0, time.UTC), inputTokens: 10, }, nil, t.TempDir(), "state.db", "", "local", ) require.True(t, ok) assert.Equal(t, "hermes:usage-only", res.Session.ID) assert.Empty(t, res.Messages) require.Len(t, res.UsageEvents, 1) assert.Equal(t, 10, res.UsageEvents[0].InputTokens) } func TestBuildHermesStateResultPopulatesSessionAggregateTokens(t *testing.T) { res, ok := buildHermesStateResult( hermesStateSession{ id: "agg", source: "cli", model: "gpt-5.5", startedAt: time.Date(2026, 5, 14, 12, 0, 0, 0, time.UTC), inputTokens: 300, outputTokens: 70, cacheReadTokens: 20, }, nil, t.TempDir(), "state.db", "", "local", ) require.True(t, ok) // Session-aggregate columns (session list / detail / stats portfolio) // must reflect Hermes's own authoritative accounting, not stay at 0. assert.True(t, res.Session.HasTotalOutputTokens) assert.Equal(t, 70, res.Session.TotalOutputTokens) assert.True(t, res.Session.HasPeakContextTokens) assert.Equal(t, 320, res.Session.PeakContextTokens) } func TestHermesUsageEvents_UnknownCostStatusLeavesCostUnpriced(t *testing.T) { // estimated_cost_usd is a literal 0 with cost_status "unknown": // Hermes does not actually know the cost, so the event must leave // CostUSD nil and let agentsview price it from the catalog rather // than asserting a false known $0. events := hermesUsageEvents(hermesStateSession{ model: "gpt-5.5", inputTokens: 100, outputTokens: 50, estimatedCost: sql.NullFloat64{Float64: 0, Valid: true}, costStatus: "unknown", }, "hermes:unknown-cost") require.Len(t, events, 1) assert.Nil(t, events[0].CostUSD) } func TestHermesUsageEvents_IncludedCostStatusIsKnownZero(t *testing.T) { // cost_status "included" backed by a real cost_source means the // usage is genuinely covered/free, so a known $0 is correct even // when no estimated cost is recorded. events := hermesUsageEvents(hermesStateSession{ model: "openrouter/owl-alpha", inputTokens: 100, outputTokens: 50, costStatus: "included", costSource: "provider_models_api", }, "hermes:included-cost") require.Len(t, events, 1) require.NotNil(t, events[0].CostUSD) assert.Equal(t, 0.0, *events[0].CostUSD) } func TestHermesUsageEvents_IncludedWithoutCostSourceLeavesCostUnpriced(t *testing.T) { // Hermes marks models it does not price (e.g. gpt-5.5) cost_status // "included" with cost_source "none". That is a default placeholder, // not a confident free-usage signal, so the event must leave CostUSD // nil and let agentsview price it from the catalog instead of // reporting a false $0. for _, src := range []string{"none", ""} { events := hermesUsageEvents(hermesStateSession{ model: "gpt-5.5", inputTokens: 100, outputTokens: 50, estimatedCost: sql.NullFloat64{Float64: 0, Valid: true}, costStatus: "included", costSource: src, }, "hermes:included-no-source") require.Len(t, events, 1) assert.Nilf(t, events[0].CostUSD, "cost_source %q must not produce a confident $0", src) } } func TestHermesUsageEvents_ActualCostTakesPrecedence(t *testing.T) { events := hermesUsageEvents(hermesStateSession{ model: "gpt-5.5", inputTokens: 100, actualCost: sql.NullFloat64{Float64: 1.23, Valid: true}, estimatedCost: sql.NullFloat64{Float64: 0, Valid: true}, costStatus: "unknown", }, "hermes:actual-cost") require.Len(t, events, 1) require.NotNil(t, events[0].CostUSD) assert.Equal(t, 1.23, *events[0].CostUSD) } func TestHermesUsageEvents_PositiveEstimateUsedWhenStatusEmpty(t *testing.T) { events := hermesUsageEvents(hermesStateSession{ model: "gpt-5.5", inputTokens: 100, estimatedCost: sql.NullFloat64{Float64: 0.5, Valid: true}, costStatus: "", }, "hermes:estimate-cost") require.Len(t, events, 1) require.NotNil(t, events[0].CostUSD) assert.Equal(t, 0.5, *events[0].CostUSD) } func TestBuildHermesStateResultLeavesAggregatesUnsetWhenNoTokens(t *testing.T) { res, ok := buildHermesStateResult( hermesStateSession{ id: "no-usage", source: "cli", model: "gpt-5.5", startedAt: time.Date(2026, 5, 14, 12, 0, 0, 0, time.UTC), // No token counts: a session with messages but no recorded usage. }, []hermesStateMessage{{ role: "user", content: "hi", timestamp: time.Date(2026, 5, 14, 12, 0, 1, 0, time.UTC), }}, t.TempDir(), "state.db", "", "local", ) require.True(t, ok) assert.False(t, res.Session.HasTotalOutputTokens) assert.Zero(t, res.Session.TotalOutputTokens) assert.False(t, res.Session.HasPeakContextTokens) assert.Zero(t, res.Session.PeakContextTokens) } func TestCountHermesUsersSkipsToolResultOnlyMessages(t *testing.T) { got := countHermesUsers([]ParsedMessage{ {Role: RoleUser, Content: "real prompt"}, {Role: RoleUser, ToolResults: []ParsedToolResult{{ToolUseID: "tc1"}}}, {Role: RoleUser, Content: "system", IsSystem: true}, }) assert.Equal(t, 1, got) } func TestDiscoverHermesSessionsFindsTranscriptOnlyRoot( t *testing.T, ) { root := t.TempDir() sessionsDir := filepath.Join(root, "sessions") require.NoError(t, os.MkdirAll(sessionsDir, 0o755)) path := filepath.Join(sessionsDir, "session_child.json") require.NoError(t, os.WriteFile(path, []byte(`{"messages":[]}`), 0o644)) files := discoverHermesTestSessions(t, root) require.Len(t, files, 1) assert.Equal(t, path, files[0].Path) } func TestParseHermesSession_CompactionBoundaryIsSystem(t *testing.T) { sess, msgs := runHermesJSONTest(t, "", `{ "platform":"darwin", "session_start":"2026-05-14T10:00:00Z", "last_updated":"2026-05-14T10:02:00Z", "messages":[ {"role":"user","content":"[CONTEXT COMPACTION - REFERENCE ONLY]\nold context","timestamp":"2026-05-14T10:01:00Z"}, {"role":"user","content":"real prompt","timestamp":"2026-05-14T10:02:00Z"} ] }`) require.Len(t, msgs, 2) assert.True(t, msgs[0].IsSystem) assert.True(t, msgs[0].IsCompactBoundary) assert.Equal(t, "system", msgs[0].SourceType) assert.Equal(t, "compact_boundary", msgs[0].SourceSubtype) assert.Equal(t, 1, sess.UserMessageCount) assert.Equal(t, "real prompt", sess.FirstMessage) } // --- JSONL format tests --- func TestParseHermesSession_JSONL_Basic(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta","platform":"linux","model":"gpt-4","timestamp":"2026-04-03T15:27:00.000000"}`, `{"role":"user","content":"Fix the tests","timestamp":"2026-04-03T15:27:21.014566"}`, `{"role":"assistant","content":"I will fix them now.","timestamp":"2026-04-03T15:27:25.123456"}`, }, "\n") sess, msgs := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) assertSessionMeta(t, sess, "hermes:20260403_153620_5a3e2ff1", "hermes-linux", AgentHermes, ) assert.Equal(t, "Fix the tests", sess.FirstMessage) assertMessageCount(t, sess.MessageCount, 2) assert.Equal(t, 1, sess.UserMessageCount) assert.Equal(t, "local", sess.Machine) require.Len(t, msgs, 2) assertMessage(t, msgs[0], RoleUser, "Fix the tests") assertMessage(t, msgs[1], RoleAssistant, "I will fix them now.") assert.Equal(t, 0, msgs[0].Ordinal) assert.Equal(t, 1, msgs[1].Ordinal) } func TestParseHermesSession_JSONL_ToolCalls(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta","platform":"darwin","timestamp":"2026-04-03T15:00:00.000000"}`, `{"role":"user","content":"Read main.go","timestamp":"2026-04-03T15:00:01.000000"}`, `{"role":"assistant","content":"","reasoning":"Let me read it.","finish_reason":"tool_calls","tool_calls":[{"id":"tc1","function":{"name":"read_file","arguments":"{\"path\":\"main.go\"}"}}],"timestamp":"2026-04-03T15:00:02.000000"}`, `{"role":"tool","content":"package main\n","tool_call_id":"tc1","timestamp":"2026-04-03T15:00:03.000000"}`, }, "\n") sess, msgs := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) assertMessageCount(t, sess.MessageCount, 3) assert.Equal(t, 1, sess.UserMessageCount) // Assistant message with reasoning and tool call. assert.True(t, msgs[1].HasThinking) assert.True(t, msgs[1].HasToolUse) assert.Contains(t, msgs[1].Content, "[Thinking]") assert.Contains(t, msgs[1].Content, "Let me read it.") require.Len(t, msgs[1].ToolCalls, 1) assert.Equal(t, "read_file", msgs[1].ToolCalls[0].ToolName) assert.Equal(t, "Read", msgs[1].ToolCalls[0].Category) assert.Equal(t, "tc1", msgs[1].ToolCalls[0].ToolUseID) // Tool result message. assert.Equal(t, RoleUser, msgs[2].Role) require.Len(t, msgs[2].ToolResults, 1) assert.Equal(t, "tc1", msgs[2].ToolResults[0].ToolUseID) assert.Equal(t, "package main\n", DecodeContent(msgs[2].ToolResults[0].ContentRaw), ) } func TestParseHermesSession_JSONL_MultipleToolCalls(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta","timestamp":"2026-04-03T15:00:00.000000"}`, `{"role":"user","content":"Check both files","timestamp":"2026-04-03T15:00:01.000000"}`, `{"role":"assistant","content":"Reading both.","tool_calls":[{"id":"tc1","function":{"name":"read_file","arguments":"{}"}},{"id":"tc2","function":{"name":"search_files","arguments":"{}"}}],"timestamp":"2026-04-03T15:00:02.000000"}`, }, "\n") _, msgs := runHermesJSONLTest(t, "", content) require.Len(t, msgs[1].ToolCalls, 2) assert.Equal(t, "read_file", msgs[1].ToolCalls[0].ToolName) assert.Equal(t, "Read", msgs[1].ToolCalls[0].Category) assert.Equal(t, "search_files", msgs[1].ToolCalls[1].ToolName) assert.Equal(t, "Grep", msgs[1].ToolCalls[1].Category) } func TestParseHermesSession_JSONL_NoPlatform(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta","model":"gpt-4"}`, `{"role":"user","content":"hello","timestamp":"2026-04-03T15:00:00.000000"}`, }, "\n") sess, _ := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) assert.Equal(t, "hermes", sess.Project) } func TestParseHermesSession_JSONL_ExplicitProject(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta","platform":"darwin"}`, `{"role":"user","content":"hello","timestamp":"2026-04-03T15:00:00.000000"}`, }, "\n") path := createTestFile( t, "20260403_153620_abc.jsonl", content, ) sess, _, err := parseHermesTestSession( t, path, "my-project", "local", ) require.NoError(t, err) require.NotNil(t, sess) assert.Equal(t, "my-project", sess.Project) } func TestParseHermesSession_JSONL_EmptyMessages(t *testing.T) { content := `{"role":"session_meta","platform":"linux"}` sess, msgs := runHermesJSONLTest(t, "", content) assert.Nil(t, sess) assert.Nil(t, msgs) } func TestParseHermesSession_JSONL_EmptyUserContent(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta"}`, `{"role":"user","content":"","timestamp":"2026-04-03T15:00:00.000000"}`, `{"role":"user","content":" ","timestamp":"2026-04-03T15:00:01.000000"}`, `{"role":"user","content":"real message","timestamp":"2026-04-03T15:00:02.000000"}`, }, "\n") sess, msgs := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) assertMessageCount(t, sess.MessageCount, 1) assert.Equal(t, 1, sess.UserMessageCount) assert.Equal(t, "real message", sess.FirstMessage) require.Len(t, msgs, 1) } func TestParseHermesSession_JSONL_EmptyAssistant(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta"}`, `{"role":"user","content":"hi","timestamp":"2026-04-03T15:00:00.000000"}`, `{"role":"assistant","content":"","timestamp":"2026-04-03T15:00:01.000000"}`, }, "\n") sess, msgs := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) // Empty assistant with no tool calls is skipped. assertMessageCount(t, sess.MessageCount, 1) require.Len(t, msgs, 1) } func TestParseHermesSession_JSONL_ToolResultNoID(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta"}`, `{"role":"user","content":"hi","timestamp":"2026-04-03T15:00:00.000000"}`, `{"role":"tool","content":"result","tool_call_id":"","timestamp":"2026-04-03T15:00:01.000000"}`, }, "\n") sess, msgs := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) // Tool result without ID is skipped. assertMessageCount(t, sess.MessageCount, 1) require.Len(t, msgs, 1) } func TestParseHermesSession_JSONL_InvalidJSON(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta"}`, `not valid json`, `{"role":"user","content":"hello","timestamp":"2026-04-03T15:00:00.000000"}`, }, "\n") sess, msgs := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) // Invalid line is skipped, valid messages are parsed. assertMessageCount(t, sess.MessageCount, 1) require.Len(t, msgs, 1) } func TestParseHermesSession_JSONL_Timestamps(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta","timestamp":"2026-04-03T10:00:00.000000"}`, `{"role":"user","content":"first","timestamp":"2026-04-03T10:00:05.000000"}`, `{"role":"assistant","content":"reply","timestamp":"2026-04-03T10:05:00.000000"}`, }, "\n") sess, _ := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) wantStart := time.Date( 2026, 4, 3, 10, 0, 0, 0, time.Local, ) wantEnd := time.Date( 2026, 4, 3, 10, 5, 0, 0, time.Local, ) assertTimestamp(t, sess.StartedAt, wantStart) assertTimestamp(t, sess.EndedAt, wantEnd) } func TestParseHermesSession_JSONL_FirstMessageTruncation(t *testing.T) { longMsg := strings.Repeat("a", 400) content := strings.Join([]string{ `{"role":"session_meta"}`, `{"role":"user","content":"` + longMsg + `","timestamp":"2026-04-03T15:00:00.000000"}`, }, "\n") sess, _ := runHermesJSONLTest(t, "", content) require.NotNil(t, sess) // truncate clips at 300 + 3 ellipsis = 303. assert.Equal(t, 303, len(sess.FirstMessage)) } func TestParseHermesSession_JSONL_Errors(t *testing.T) { t.Run("missing file", func(t *testing.T) { _, _, err := parseHermesTestSession( t, "/nonexistent/file.jsonl", "", "local", ) assert.Error(t, err) }) } // --- JSON format tests --- func TestParseHermesSession_JSON_Basic(t *testing.T) { content := `{ "platform": "darwin", "session_start": "2026-04-03T15:00:00.000000", "last_updated": "2026-04-03T15:05:00.000000", "messages": [ {"role": "user", "content": "Deploy the app", "timestamp": "2026-04-03T15:00:01.000000"}, {"role": "assistant", "content": "Deploying now.", "timestamp": "2026-04-03T15:00:05.000000"} ] }` sess, msgs := runHermesJSONTest(t, "", content) require.NotNil(t, sess) assertSessionMeta(t, sess, "hermes:20260403_153620_5a3e2ff1", "hermes-darwin", AgentHermes, ) assert.Equal(t, "Deploy the app", sess.FirstMessage) assertMessageCount(t, sess.MessageCount, 2) assert.Equal(t, 1, sess.UserMessageCount) require.Len(t, msgs, 2) assertMessage(t, msgs[0], RoleUser, "Deploy the app") assertMessage(t, msgs[1], RoleAssistant, "Deploying now.") } func TestParseHermesSession_JSON_ToolCalls(t *testing.T) { content := `{ "session_start": "2026-04-03T15:00:00.000000", "messages": [ {"role": "user", "content": "Edit the file", "timestamp": "2026-04-03T15:00:01.000000"}, { "role": "assistant", "content": "Editing.", "reasoning": "I need to patch it.", "tool_calls": [ {"id": "tc1", "function": {"name": "patch", "arguments": "{\"file\":\"main.go\"}"}} ], "timestamp": "2026-04-03T15:00:02.000000" }, {"role": "tool", "content": "patched", "tool_call_id": "tc1", "timestamp": "2026-04-03T15:00:03.000000"} ] }` sess, msgs := runHermesJSONTest(t, "", content) require.NotNil(t, sess) assertMessageCount(t, sess.MessageCount, 3) assert.True(t, msgs[1].HasThinking) assert.True(t, msgs[1].HasToolUse) require.Len(t, msgs[1].ToolCalls, 1) assert.Equal(t, "patch", msgs[1].ToolCalls[0].ToolName) assert.Equal(t, "Edit", msgs[1].ToolCalls[0].Category) require.Len(t, msgs[2].ToolResults, 1) assert.Equal(t, "tc1", msgs[2].ToolResults[0].ToolUseID) } func TestParseHermesSession_JSON_ReasoningDetails(t *testing.T) { content := `{ "messages": [ {"role": "user", "content": "think hard", "timestamp": "2026-04-03T15:00:00.000000"}, {"role": "assistant", "content": "done", "reasoning_details": "deep thought", "timestamp": "2026-04-03T15:00:01.000000"} ] }` _, msgs := runHermesJSONTest(t, "", content) // reasoning_details is a fallback for reasoning. assert.True(t, msgs[1].HasThinking) assert.Contains(t, msgs[1].Content, "deep thought") } func TestParseHermesSession_JSON_EmptyMessages(t *testing.T) { content := `{"platform":"linux","messages":[]}` sess, msgs := runHermesJSONTest(t, "", content) assert.Nil(t, sess) assert.Nil(t, msgs) } func TestParseHermesSession_JSON_NoPlatform(t *testing.T) { content := `{ "messages": [ {"role": "user", "content": "hi", "timestamp": "2026-04-03T15:00:00.000000"} ] }` sess, _ := runHermesJSONTest(t, "", content) require.NotNil(t, sess) assert.Equal(t, "hermes", sess.Project) } func TestParseHermesSession_JSON_MessageTimestampsExtendBounds( t *testing.T, ) { // Per-message timestamps can extend session bounds beyond // the envelope session_start/last_updated. content := `{ "session_start": "2026-04-03T15:00:00.000000", "last_updated": "2026-04-03T15:05:00.000000", "messages": [ {"role": "user", "content": "early", "timestamp": "2026-04-03T14:50:00.000000"}, {"role": "assistant", "content": "late", "timestamp": "2026-04-03T15:10:00.000000"} ] }` sess, _ := runHermesJSONTest(t, "", content) require.NotNil(t, sess) wantStart := time.Date( 2026, 4, 3, 14, 50, 0, 0, time.Local, ) wantEnd := time.Date( 2026, 4, 3, 15, 10, 0, 0, time.Local, ) assertTimestamp(t, sess.StartedAt, wantStart) assertTimestamp(t, sess.EndedAt, wantEnd) } func TestParseHermesSession_JSON_Errors(t *testing.T) { t.Run("missing file", func(t *testing.T) { _, _, err := parseHermesTestSession( t, "/nonexistent/file.json", "", "local", ) assert.Error(t, err) }) t.Run("not an object", func(t *testing.T) { path := createTestFile( t, "session_bad.json", `"just a string"`, ) _, _, err := parseHermesTestSession(t, path, "", "local") assert.Error(t, err) assert.Contains(t, err.Error(), "invalid JSON") }) } // --- Routing test --- func TestParseHermesSession_RoutesOnExtension(t *testing.T) { jsonlContent := strings.Join([]string{ `{"role":"session_meta","platform":"linux"}`, `{"role":"user","content":"jsonl path","timestamp":"2026-04-03T15:00:00.000000"}`, }, "\n") jsonContent := `{ "platform": "darwin", "messages": [ {"role":"user","content":"json path","timestamp":"2026-04-03T15:00:00.000000"} ] }` t.Run("jsonl", func(t *testing.T) { sess, _ := runHermesJSONLTest( t, "20260403_test.jsonl", jsonlContent, ) require.NotNil(t, sess) assert.Equal(t, "hermes-linux", sess.Project) }) t.Run("json", func(t *testing.T) { sess, _ := runHermesJSONTest( t, "session_test.json", jsonContent, ) require.NotNil(t, sess) assert.Equal(t, "hermes-darwin", sess.Project) }) } // --- HermesSessionID --- func TestHermesSessionID(t *testing.T) { tests := []struct { name string want string }{ {"20260403_153620_5a3e2ff1.jsonl", "20260403_153620_5a3e2ff1"}, {"20260403_153620_5a3e2ff1.json", "20260403_153620_5a3e2ff1"}, {"session_20260403_abc.json", "20260403_abc"}, {"session_20260403_abc.jsonl", "20260403_abc"}, {"plain_name.jsonl", "plain_name"}, {"no_ext", "no_ext"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := HermesSessionID(tt.name) if got != tt.want { t.Errorf( "HermesSessionID(%q) = %q, want %q", tt.name, got, tt.want, ) } }) } } // --- parseHermesTimestamp --- func TestParseHermesTimestamp(t *testing.T) { tests := []struct { name string input string want time.Time }{ { "microseconds", "2026-04-03T15:27:21.014566", time.Date( 2026, 4, 3, 15, 27, 21, 14566000, time.Local, ), }, { "no fractional seconds", "2026-04-03T15:27:21", time.Date( 2026, 4, 3, 15, 27, 21, 0, time.Local, ), }, { "RFC3339 with timezone", "2026-04-03T15:27:21Z", time.Date( 2026, 4, 3, 15, 27, 21, 0, time.UTC, ), }, { "RFC3339 with offset", "2026-04-03T15:27:21+05:30", time.Date( 2026, 4, 3, 15, 27, 21, 0, time.FixedZone("", 5*3600+30*60), ), }, { "empty string", "", time.Time{}, }, { "garbage", "not-a-timestamp", time.Time{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseHermesTimestamp(tt.input) if tt.want.IsZero() { assertZeroTimestamp(t, got, "timestamp") } else { assertTimestamp(t, got, tt.want) } }) } } // --- stripHermesSkillPrefix --- func TestStripHermesSkillPrefix(t *testing.T) { tests := []struct { name string input string want string }{ { "no prefix", "Fix the bug in main.go", "Fix the bug in main.go", }, { "skill with user instruction", `[SYSTEM: The user has invoked the "commit" skill. Please follow the instructions below.]` + "\n\n---\nname: commit\n---\ncommit stuff\n\n" + "The user has provided the following instruction alongside the skill invocation: Please commit my changes", "Please commit my changes", }, { "skill without user instruction", `[SYSTEM: The user has invoked the "review" skill. Please follow the instructions.]` + "\n\n---\nname: review\n---\nreview stuff\n\n", "[Skill: review]", }, { "skill with runtime note stripped", `[SYSTEM: The user has invoked the "debug" skill. Follow instructions.]` + "\n\nThe user has provided the following instruction alongside the skill invocation: Fix it" + "\n\n[Runtime note: some internal detail]", "Fix it", }, { "empty user instruction falls back to skill name", `[SYSTEM: The user has invoked the "test" skill. Follow instructions.]` + "\n\nThe user has provided the following instruction alongside the skill invocation: ", "[Skill: test]", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := stripHermesSkillPrefix(tt.input) assert.Equal(t, tt.want, got) }) } } // --- DiscoverHermesSessions --- func TestDiscoverHermesSessions(t *testing.T) { tests := []struct { name string files map[string]string wantFiles []string }{ { name: "JSONL only", files: map[string]string{ "20260403_153620_aaa.jsonl": "{}", "20260404_100000_bbb.jsonl": "{}", }, wantFiles: []string{ "20260403_153620_aaa.jsonl", "20260404_100000_bbb.jsonl", }, }, { name: "JSON only", files: map[string]string{ "session_20260403_aaa.json": "{}", }, wantFiles: []string{ "session_20260403_aaa.json", }, }, { name: "JSONL takes priority over JSON", files: map[string]string{ "20260403_153620_aaa.jsonl": "{}", "session_20260403_153620_aaa.json": "{}", }, wantFiles: []string{ "20260403_153620_aaa.jsonl", }, }, { name: "non-session JSON ignored", files: map[string]string{ "config.json": "{}", "random.json": "{}", "notes.txt": "hi", "session_a.json": "{}", }, wantFiles: []string{ "session_a.json", }, }, { name: "directories ignored", files: map[string]string{ "20260403_aaa.jsonl": "{}", "subdir/nested.jsonl": "{}", }, wantFiles: []string{ "20260403_aaa.jsonl", }, }, { name: "empty dir", files: map[string]string{}, wantFiles: nil, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() setupFileSystem(t, dir, tt.files) files := discoverHermesTestSessions(t, dir) assertDiscoveredFiles( t, files, tt.wantFiles, AgentHermes, ) }) } t.Run("empty string dir", func(t *testing.T) { files := discoverHermesTestSessions(t, "") assert.Nil(t, files) }) t.Run("nonexistent dir", func(t *testing.T) { files := discoverHermesTestSessions( t, filepath.Join(t.TempDir(), "nope"), ) assert.Nil(t, files) }) } // --- FindHermesSourceFile --- func TestFindHermesSourceFile(t *testing.T) { tests := []struct { name string files map[string]string sessionID string wantFile string }{ { name: "finds JSONL", files: map[string]string{"20260403_aaa.jsonl": "{}"}, sessionID: "20260403_aaa", wantFile: "20260403_aaa.jsonl", }, { name: "finds JSON", files: map[string]string{"session_20260403_aaa.json": "{}"}, sessionID: "20260403_aaa", wantFile: "session_20260403_aaa.json", }, { name: "prefers JSONL over JSON", files: map[string]string{ "20260403_aaa.jsonl": "{}", "session_20260403_aaa.json": "{}", }, sessionID: "20260403_aaa", wantFile: "20260403_aaa.jsonl", }, { name: "not found", files: map[string]string{"20260403_aaa.jsonl": "{}"}, sessionID: "nonexistent", wantFile: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() setupFileSystem(t, dir, tt.files) got := findHermesTestSourceFile(t, dir, tt.sessionID) want := "" if tt.wantFile != "" { want = filepath.Join(dir, tt.wantFile) } if got != want { t.Errorf("got %q, want %q", got, want) } }) } t.Run("invalid session IDs", func(t *testing.T) { dir := t.TempDir() setupFileSystem(t, dir, map[string]string{ "20260403_aaa.jsonl": "{}", }) for _, id := range []string{"", "../etc/passwd", "a/b", "a b"} { got := findHermesTestSourceFile(t, dir, id) if got != "" { t.Errorf( "FindHermesSourceFile(%q) = %q, want empty", id, got, ) } } }) } // --- Taxonomy --- func TestHermesToolTaxonomy(t *testing.T) { tests := []struct { tool string category string }{ {"read_file", "Read"}, {"write_file", "Write"}, {"edit_file", "Edit"}, {"search_files", "Grep"}, {"run_command", "Bash"}, {"execute_command", "Bash"}, {"patch", "Edit"}, {"terminal", "Bash"}, {"execute_code", "Bash"}, {"vision_analyze", "Read"}, {"delegate_task", "Task"}, {"browser_navigate", "Tool"}, {"browser_click", "Tool"}, {"todo", "Tool"}, {"memory", "Tool"}, {"skill_view", "Tool"}, } for _, tt := range tests { t.Run(tt.tool, func(t *testing.T) { got := NormalizeToolCategory(tt.tool) if got != tt.category { t.Errorf( "NormalizeToolCategory(%q) = %q, want %q", tt.tool, got, tt.category, ) } }) } } // --- Registry --- func TestHermesRegistryEntry(t *testing.T) { var found *AgentDef for i := range Registry { if Registry[i].Type == AgentHermes { found = &Registry[i] break } } require.NotNil(t, found, "AgentHermes not in Registry") assert.Equal(t, "Hermes Agent", found.DisplayName) assert.Equal(t, "HERMES_SESSIONS_DIR", found.EnvVar) assert.Equal(t, "hermes_sessions_dirs", found.ConfigKey) assert.Equal(t, "hermes:", found.IDPrefix) assert.True(t, found.FileBased) assert.Contains(t, found.DefaultDirs, ".hermes/sessions") // The watch-root resolvers are provider-owned and consumed by watcher setup. assert.NotNil(t, found.WatchRootsFunc) assert.NotNil(t, found.ShallowWatchRootsFunc) } // --- File info --- func TestParseHermesSession_FileInfo(t *testing.T) { content := strings.Join([]string{ `{"role":"session_meta"}`, `{"role":"user","content":"hi","timestamp":"2026-04-03T15:00:00.000000"}`, }, "\n") path := createTestFile( t, "20260403_test.jsonl", content, ) info, err := os.Stat(path) require.NoError(t, err) sess, _, err := parseHermesTestSession(t, path, "", "local") require.NoError(t, err) require.NotNil(t, sess) assert.Equal(t, path, sess.File.Path) assert.Equal(t, info.Size(), sess.File.Size) assert.Equal(t, info.ModTime().UnixNano(), sess.File.Mtime) }