Files
multica/server/internal/daemon/usage/opencode_test.go
Jiang Bohan 2ea778796a feat(daemon): add token usage log scanning for OpenCode, OpenClaw, and Hermes runtimes
Previously only Claude and Codex had log-scanning-level token usage
reporting (Flow B). This adds scanners for the remaining three runtimes:

- OpenCode: reads JSON message files from ~/.local/share/opencode/storage/message/
- OpenClaw: reads JSONL session files from ~/.openclaw/agents/*/sessions/
- Hermes: reads JSONL session files from ~/.hermes/sessions/

All three are registered in Scanner.Scan() and follow the same
(date, provider, model) aggregation pattern as existing scanners.
2026-04-13 13:42:05 +08:00

142 lines
3.6 KiB
Go

package usage
import (
"log/slog"
"os"
"path/filepath"
"testing"
)
func TestParseOpenCodeFile(t *testing.T) {
tmp := t.TempDir()
// Real OpenCode message JSON format with token usage
content := `{
"id": "msg_test001",
"sessionID": "ses_test001",
"role": "assistant",
"time": {"created": 1768332037518, "completed": 1768332039410},
"modelID": "claude-sonnet-4-5",
"providerID": "anthropic",
"tokens": {"input": 10916, "output": 5, "reasoning": 100, "cache": {"read": 448, "write": 50}}
}`
filePath := filepath.Join(tmp, "msg_test001.json")
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
s := NewScanner(slog.Default())
record := s.parseOpenCodeFile(filePath)
if record == nil {
t.Fatal("expected non-nil record")
}
if record.Provider != "opencode" {
t.Errorf("provider = %q, want %q", record.Provider, "opencode")
}
if record.Model != "claude-sonnet-4-5" {
t.Errorf("model = %q, want %q", record.Model, "claude-sonnet-4-5")
}
if record.InputTokens != 10916 {
t.Errorf("input_tokens = %d, want %d", record.InputTokens, 10916)
}
// output_tokens + reasoning
if record.OutputTokens != 105 {
t.Errorf("output_tokens = %d, want %d", record.OutputTokens, 105)
}
if record.CacheReadTokens != 448 {
t.Errorf("cache_read_tokens = %d, want %d", record.CacheReadTokens, 448)
}
if record.CacheWriteTokens != 50 {
t.Errorf("cache_write_tokens = %d, want %d", record.CacheWriteTokens, 50)
}
}
func TestParseOpenCodeFile_UserMessage(t *testing.T) {
tmp := t.TempDir()
// User messages should be ignored (no token usage to report)
content := `{
"id": "msg_user001",
"sessionID": "ses_test001",
"role": "user",
"time": {"created": 1768332037000}
}`
filePath := filepath.Join(tmp, "msg_user001.json")
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
s := NewScanner(slog.Default())
record := s.parseOpenCodeFile(filePath)
if record != nil {
t.Errorf("expected nil record for user message, got %+v", record)
}
}
func TestParseOpenCodeFile_NoCache(t *testing.T) {
tmp := t.TempDir()
// Message without cache field
content := `{
"id": "msg_test002",
"sessionID": "ses_test002",
"role": "assistant",
"time": {"created": 1768332037518},
"modelID": "gpt-4o",
"providerID": "openai",
"tokens": {"input": 500, "output": 200, "reasoning": 0}
}`
filePath := filepath.Join(tmp, "msg_test002.json")
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
s := NewScanner(slog.Default())
record := s.parseOpenCodeFile(filePath)
if record == nil {
t.Fatal("expected non-nil record")
}
if record.CacheReadTokens != 0 {
t.Errorf("cache_read_tokens = %d, want 0", record.CacheReadTokens)
}
if record.CacheWriteTokens != 0 {
t.Errorf("cache_write_tokens = %d, want 0", record.CacheWriteTokens)
}
if record.Model != "gpt-4o" {
t.Errorf("model = %q, want %q", record.Model, "gpt-4o")
}
}
func TestParseOpenCodeFile_ZeroTokens(t *testing.T) {
tmp := t.TempDir()
// Message with zero tokens should return nil
content := `{
"id": "msg_test003",
"sessionID": "ses_test003",
"role": "assistant",
"time": {"created": 1768332037518},
"modelID": "test-model",
"tokens": {"input": 0, "output": 0, "reasoning": 0}
}`
filePath := filepath.Join(tmp, "msg_test003.json")
if err := os.WriteFile(filePath, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
s := NewScanner(slog.Default())
record := s.parseOpenCodeFile(filePath)
if record != nil {
t.Errorf("expected nil record for zero tokens, got %+v", record)
}
}