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

1516 lines
44 KiB
Go

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)
}