Files
wehub-resource-sync f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
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
chore: import upstream snapshot with attribution
2026-07-13 12:30:36 +08:00

898 lines
36 KiB
Go

package parser
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func writeKimiWireJSONL(
t *testing.T, projHash, sessionUUID string,
lines []string,
) string {
t.Helper()
dir := filepath.Join(
t.TempDir(), projHash, sessionUUID,
)
require.NoError(t, os.MkdirAll(dir, 0o755))
path := filepath.Join(dir, "wire.jsonl")
content := strings.Join(lines, "\n") + "\n"
require.NoError(t,
os.WriteFile(path, []byte(content), 0o644))
return path
}
func writeKimiCodeWireJSONL(
t *testing.T, workdirDir, sessionDir, agentID string,
lines []string,
) string {
t.Helper()
dir := filepath.Join(
t.TempDir(), workdirDir, sessionDir, "agents", agentID,
)
require.NoError(t, os.MkdirAll(dir, 0o755))
path := filepath.Join(dir, "wire.jsonl")
content := strings.Join(lines, "\n") + "\n"
require.NoError(t,
os.WriteFile(path, []byte(content), 0o644))
return path
}
func parseKimiSessionForTest(
t *testing.T,
path, project, machine string,
) (*ParsedSession, []ParsedMessage, error) {
t.Helper()
return parseKimiSession(path, project, machine)
}
func TestParseKimiSession_Basic(t *testing.T) {
path := writeKimiWireJSONL(t,
"abc123", "sess-uuid-1234",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello Kimi"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi there!"}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t,
path, "myproject", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
assertSessionMeta(t, sess,
"kimi:abc123:sess-uuid-1234",
"myproject", AgentKimi,
)
assert.Equal(t, "Hello Kimi", sess.FirstMessage)
assertMessageCount(t, sess.MessageCount, 2)
assert.Equal(t, 1, sess.UserMessageCount)
wantStart := time.Unix(1704067200, 0)
assertTimestamp(t, sess.StartedAt, wantStart)
wantEnd := time.Unix(1704067202, 0)
assertTimestamp(t, sess.EndedAt, wantEnd)
require.Equal(t, 2, len(msgs))
assertMessage(t, msgs[0], RoleUser, "Hello Kimi")
assertMessage(t, msgs[1], RoleAssistant, "Hi there!")
assert.Equal(t, 0, msgs[0].Ordinal)
assert.Equal(t, 1, msgs[1].Ordinal)
}
func TestParseKimiSession_ThinkingAndToolUse(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj1", "sess1",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Read the file"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "think", "think": "Let me plan.", "encrypted": null}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "ToolCall", "payload": {"type": "function", "id": "tool_1", "function": {"name": "Glob", "arguments": "{\"pattern\": \"*.go\"}"}, "extras": null}}}`,
`{"timestamp": 1704067203.0, "message": {"type": "ToolResult", "payload": {"tool_call_id": "tool_1", "return_value": {"is_error": false, "output": "main.go\nutil.go"}}}}`,
`{"timestamp": 1704067204.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Found the files."}}}`,
`{"timestamp": 1704067205.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, "Read the file", sess.FirstMessage)
// user, assistant(thinking+tool), tool_result(user), assistant(text)
require.Equal(t, 4, len(msgs))
// First message: user
assert.Equal(t, RoleUser, msgs[0].Role)
// Second: assistant with thinking + tool call
assert.Equal(t, RoleAssistant, msgs[1].Role)
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 plan.")
assert.Contains(t, msgs[1].Content, "[Glob:")
require.Equal(t, 1, len(msgs[1].ToolCalls))
assert.Equal(t, "Glob", msgs[1].ToolCalls[0].ToolName)
assert.Equal(t, "Glob", msgs[1].ToolCalls[0].Category)
assert.Equal(t, "tool_1", msgs[1].ToolCalls[0].ToolUseID)
// Third: tool result (user role)
assert.Equal(t, RoleUser, msgs[2].Role)
require.Equal(t, 1, len(msgs[2].ToolResults))
assert.Equal(t, "tool_1", msgs[2].ToolResults[0].ToolUseID)
assert.Equal(t, "main.go\nutil.go",
DecodeContent(msgs[2].ToolResults[0].ContentRaw))
// Fourth: assistant text continuation
assert.Equal(t, RoleAssistant, msgs[3].Role)
assert.Contains(t, msgs[3].Content, "Found the files.")
}
func TestParseKimiSession_Empty(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj2", "sess2",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
assert.Nil(t, sess)
assert.Nil(t, msgs)
}
func TestParseKimiSession_ErrorToolResult(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj3", "sess3",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Do something"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ToolCall", "payload": {"type": "function", "id": "tool_err", "function": {"name": "Bash", "arguments": "{\"command\": \"exit 1\"}"}, "extras": null}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "ToolResult", "payload": {"tool_call_id": "tool_err", "return_value": {"is_error": true, "output": ""}}}}`,
`{"timestamp": 1704067203.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
// user, assistant(tool call), tool_result(error)
require.Equal(t, 3, len(msgs))
assert.Equal(t, "[error]",
DecodeContent(msgs[2].ToolResults[0].ContentRaw))
}
func TestParseKimiSession_ArrayToolResult(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj-arr", "sess-arr",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Query logs"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ToolCall", "payload": {"type": "function", "id": "tool_arr", "function": {"name": "Bash", "arguments": "{\"command\": \"echo hi\"}"}, "extras": null}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "ToolResult", "payload": {"tool_call_id": "tool_arr", "return_value": {"is_error": false, "output": [{"type": "text", "text": "line one"}, {"type": "text", "text": "line two"}]}}}}`,
`{"timestamp": 1704067203.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Done."}}}`,
`{"timestamp": 1704067204.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
// user, assistant(tool call), tool_result(array output), assistant(text)
require.Equal(t, 4, len(msgs))
require.Equal(t, 1, len(msgs[2].ToolResults))
assert.Equal(t, "line one\nline two",
DecodeContent(msgs[2].ToolResults[0].ContentRaw))
assert.Equal(t, len("line one\nline two"),
msgs[2].ToolResults[0].ContentLength)
}
func TestParseKimiSession_MultipleStatusUpdates(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj-multi", "sess-multi",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi"}}}`,
`{"timestamp": 1704067201.5, "message": {"type": "StatusUpdate", "payload": {"context_tokens": 5000, "token_usage": {"output": 100}}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "ToolCall", "payload": {"type": "function", "id": "t1", "function": {"name": "Glob", "arguments": "{}"}, "extras": null}}}`,
`{"timestamp": 1704067202.5, "message": {"type": "StatusUpdate", "payload": {"context_tokens": 8000, "token_usage": {"output": 50}}}}`,
`{"timestamp": 1704067203.0, "message": {"type": "ToolResult", "payload": {"tool_call_id": "t1", "return_value": {"is_error": false, "output": "a.go"}}}}`,
`{"timestamp": 1704067203.5, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Found it."}}}`,
`{"timestamp": 1704067204.0, "message": {"type": "StatusUpdate", "payload": {"context_tokens": 6000, "token_usage": {"output": 75}}}}`,
`{"timestamp": 1704067205.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, 225, sess.TotalOutputTokens)
assert.Equal(t, 8000, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
func TestParseKimiSession_StatusUpdate(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj4", "sess4",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi"}}}`,
`{"timestamp": 1704067201.5, "message": {"type": "StatusUpdate", "payload": {"context_tokens": 5000, "max_context_tokens": 262144, "token_usage": {"output": 42}}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, 42, sess.TotalOutputTokens)
assert.Equal(t, 5000, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
func TestParseKimiSession_SessionLevelTokensEmitUsageEvent(t *testing.T) {
// StatusUpdate carries only session-level token aggregates; the
// individual assistant message gets no per-message token_usage.
// Without a usage event the cost engine would price 0 tokens and
// show $0, so the parser emits a session-level event defaulted to
// defaultKimiModel.
path := writeKimiWireJSONL(t,
"proj-usage", "sess-usage",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi"}}}`,
`{"timestamp": 1704067201.5, "message": {"type": "StatusUpdate", "payload": {"context_tokens": 5000, "token_usage": {"output": 42}}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSession(path, "testproj", "local")
require.NoError(t, err)
require.NotNil(t, sess)
require.Equal(t, 1, len(sess.UsageEvents))
ev := sess.UsageEvents[0]
assert.Equal(t, "kimi:proj-usage:sess-usage", ev.SessionID)
assert.Equal(t, "session", ev.Source)
assert.Equal(t, defaultKimiModel, ev.Model)
assert.Equal(t, 42, ev.OutputTokens)
assert.Equal(t, "kimi:session:proj-usage:sess-usage", ev.DedupKey)
}
func TestParseKimiSession_PerMessageTokensSkipUsageEvent(t *testing.T) {
// The native wire protocol (step.begin/step.end) attaches token
// usage to each assistant message, so the per-message path already
// prices the session. No session-level usage event is needed and
// emitting one would double-count tokens.
path := writeKimiWireJSONL(t,
"proj-native", "sess-native",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"type": "turn.prompt", "timestamp": 1704067200.0, "input": [{"type": "text", "text": "Hello"}]}`,
`{"type": "context.append_loop_event", "timestamp": 1704067201.0, "event": {"type": "content.part", "part": {"type": "text", "text": "Hi"}}}`,
`{"type": "context.append_loop_event", "timestamp": 1704067202.0, "event": {"type": "step.end", "model": "moonshot/kimi-k2", "finishReason": "stop", "usage": {"output": 42, "inputOther": 100}}}`,
},
)
sess, _, err := parseKimiSession(path, "testproj", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assert.Empty(t, sess.UsageEvents)
}
func TestParseKimiSession_StepEndModelOverridesDefault(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj-native-model", "sess-native-model",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"type": "turn.prompt", "timestamp": 1704067200.0, "input": [{"type": "text", "text": "Hello"}]}`,
`{"type": "context.append_loop_event", "timestamp": 1704067201.0, "event": {"type": "content.part", "part": {"type": "text", "text": "Hi"}}}`,
`{"type": "context.append_loop_event", "timestamp": 1704067202.0, "event": {"type": "step.end", "model": "moonshot/kimi-k2", "finishReason": "stop", "usage": {"output": 42, "inputOther": 100}}}`,
},
)
_, msgs, err := parseKimiSession(path, "testproj", "local")
require.NoError(t, err)
require.Len(t, msgs, 2)
assert.Equal(t, "moonshot/kimi-k2", msgs[1].Model)
}
func TestParseKimiSession_ZeroValuedStatusUpdatePreservesCoverage(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj-zero", "sess-zero",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi"}}}`,
`{"timestamp": 1704067201.5, "message": {"type": "StatusUpdate", "payload": {"context_tokens": 0, "token_usage": {"output": 0}}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, 0, sess.TotalOutputTokens)
assert.Equal(t, 0, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
}
func TestParseKimiSession_NoProject(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj5", "sess5",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi"}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSessionForTest(t, path, "", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, "kimi", sess.Project)
}
func TestParseKimiSession_MessageTimestamps(t *testing.T) {
path := writeKimiWireJSONL(t,
"proj-ts", "sess-ts",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi there!"}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "ToolCall", "payload": {"type": "function", "id": "t1", "function": {"name": "Bash", "arguments": "{\"command\": \"ls\"}"}, "extras": null}}}`,
`{"timestamp": 1704067203.0, "message": {"type": "ToolResult", "payload": {"tool_call_id": "t1", "return_value": {"is_error": false, "output": "file.go"}}}}`,
`{"timestamp": 1704067204.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Done."}}}`,
`{"timestamp": 1704067205.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
_, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.Equal(t, 4, len(msgs))
// User message gets timestamp from TurnBegin record.
assertTimestamp(t, msgs[0].Timestamp,
time.Unix(1704067200, 0))
// Assistant (flushed by ToolResult) gets first content
// timestamp (ContentPart at :01, not ToolCall at :02).
assertTimestamp(t, msgs[1].Timestamp,
time.Unix(1704067201, 0))
// Tool result gets timestamp from ToolResult record.
assertTimestamp(t, msgs[2].Timestamp,
time.Unix(1704067203, 0))
// Final assistant gets timestamp from its ContentPart.
assertTimestamp(t, msgs[3].Timestamp,
time.Unix(1704067204, 0))
}
func TestParseKimiSession_EmptyFragmentTimestamp(t *testing.T) {
t.Run("cross-turn", func(t *testing.T) {
// Empty ContentPart followed by TurnEnd, then a real
// turn. The second turn must use its own timestamp.
path := writeKimiWireJSONL(t,
"proj-empty", "sess-empty",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": ""}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
`{"timestamp": 1704067210.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Again"}]}}}`,
`{"timestamp": 1704067211.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi!"}}}`,
`{"timestamp": 1704067212.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
_, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.Equal(t, 3, len(msgs))
assertTimestamp(t, msgs[2].Timestamp,
time.Unix(1704067211, 0))
})
t.Run("intra-turn", func(t *testing.T) {
// Empty fragment then real content in the same turn.
// Timestamp must reflect the real content, not the
// empty fragment.
path := writeKimiWireJSONL(t,
"proj-intra", "sess-intra",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": ""}}}`,
`{"timestamp": 1704067205.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Real content"}}}`,
`{"timestamp": 1704067206.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
_, msgs, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.Equal(t, 2, len(msgs))
assertTimestamp(t, msgs[1].Timestamp,
time.Unix(1704067205, 0))
})
}
func TestParseKimiSession_MissingFile(t *testing.T) {
_, _, err := parseKimiSessionForTest(t,
"/nonexistent/wire.jsonl", "proj", "local",
)
assert.Error(t, err)
}
func TestParseKimiSession_FirstMessageTruncation(t *testing.T) {
longText := strings.Repeat("a", 400)
path := writeKimiWireJSONL(t,
"proj6", "sess6",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "` + longText + `"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Ok"}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSessionForTest(t,
path, "testproj", "local",
)
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, 303, len(sess.FirstMessage))
}
func TestDiscoverKimiSessions(t *testing.T) {
dir := t.TempDir()
projDir := filepath.Join(dir, "abc123")
sessDir := filepath.Join(projDir, "uuid-1")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(sessDir, "wire.jsonl"),
[]byte(`{"type":"metadata"}`+"\n"), 0o644,
))
sessDir2 := filepath.Join(projDir, "uuid-2")
require.NoError(t, os.MkdirAll(sessDir2, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(sessDir2, "wire.jsonl"),
[]byte(`{"type":"metadata"}`+"\n"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, sources, 2)
assert.Equal(t, []string{
filepath.Join(sessDir, "wire.jsonl"),
filepath.Join(sessDir2, "wire.jsonl"),
}, sourceDisplayPaths(sources))
assert.Equal(t, []string{"abc123", "abc123"}, sourceProjects(sources))
}
func TestDiscoverKimiSessions_Empty(t *testing.T) {
provider, ok := NewProvider(AgentKimi, ProviderConfig{
Roots: []string{"", "/nonexistent"},
})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
assert.Empty(t, sources)
}
func TestFindKimiSourceFile(t *testing.T) {
dir := t.TempDir()
projDir := filepath.Join(dir, "abc123")
sessDir := filepath.Join(projDir, "uuid-1")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
wirePath := filepath.Join(sessDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
wirePath, []byte("{}"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: "abc123:uuid-1",
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, wirePath, found.DisplayPath)
for _, rawID := range []string{"abc123:nonexistent", "invalid"} {
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: rawID,
})
require.NoError(t, err)
assert.False(t, ok)
}
emptyProvider, ok := NewProvider(AgentKimi, ProviderConfig{})
require.True(t, ok)
_, ok, err = emptyProvider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: "abc123:uuid-1",
})
require.NoError(t, err)
assert.False(t, ok)
}
func TestDiscoverKimiSessions_NewLayout(t *testing.T) {
dir := t.TempDir()
workdirDir := "wd_claude-code_5534d269834e"
sessionDir := "session_2728744d-1865-4af1-b3da-97d5bf22a979"
sessDir := filepath.Join(dir, workdirDir, sessionDir, "agents", "main")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
wirePath := filepath.Join(sessDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
wirePath, []byte(`{"type":"metadata"}`+"\n"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, sources, 1)
assert.Equal(t, wirePath, sources[0].DisplayPath)
// Project is decoded from "wd_<workdir>_<hash>".
assert.Equal(t, "claude-code", sources[0].ProjectHint)
}
func TestDiscoverKimiSessions_NewLayout_NonMainAgent(t *testing.T) {
dir := t.TempDir()
workdirDir := "wd_kimi-code_6dc514e1caf6"
sessionDir := "session_c0517a58-48ee-4632-a1fd-08be3f4f9b0f"
sessDir := filepath.Join(dir, workdirDir, sessionDir, "agents", "agent-0")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
wirePath := filepath.Join(sessDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
wirePath, []byte(`{"type":"metadata"}`+"\n"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, sources, 1)
assert.Equal(t, wirePath, sources[0].DisplayPath)
}
func TestFindKimiSourceFile_NewLayout(t *testing.T) {
dir := t.TempDir()
workdirDir := "wd_pycharmprojects_a51d6966b209"
sessionDir := "session_07673173-caad-4ad9-b8b8-29cb8fdaf66b"
sessDir := filepath.Join(dir, workdirDir, sessionDir, "agents", "main")
require.NoError(t, os.MkdirAll(sessDir, 0o755))
wirePath := filepath.Join(sessDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
wirePath, []byte("{}"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
rawID := workdirDir + ":main:" + sessionDir
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: rawID,
})
require.NoError(t, err)
require.True(t, ok)
assert.Equal(t, wirePath, found.DisplayPath)
for _, rawID := range []string{
workdirDir + ":main:nonexistent",
workdirDir + ":" + sessionDir,
} {
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
RawSessionID: rawID,
})
require.NoError(t, err)
assert.False(t, ok)
}
}
func TestParseKimiSession_NewLayoutSessionID(t *testing.T) {
path := writeKimiCodeWireJSONL(t,
"wd_myproject_a1b2c3d4", "session_uuid-1234", "main",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello Kimi Code"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Hi there!"}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t, path, "myproject", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assertSessionMeta(t, sess,
"kimi:wd_myproject_a1b2c3d4:main:session_uuid-1234",
"myproject", AgentKimi,
)
assert.Equal(t, "Hello Kimi Code", sess.FirstMessage)
assertMessageCount(t, sess.MessageCount, 2)
require.Equal(t, 2, len(msgs))
assertMessage(t, msgs[0], RoleUser, "Hello Kimi Code")
assertMessage(t, msgs[1], RoleAssistant, "Hi there!")
}
func TestParseKimiSession_NativeKimiCodeEvents(t *testing.T) {
path := writeKimiCodeWireJSONL(t,
"wd_myproject_a1b2c3d4", "session_uuid-1234", "main",
[]string{
`{"type":"metadata","protocol_version":"1.4","created_at":1782012661212}`,
`{"type":"config.update","modelAlias":"kimi-code/kimi-for-coding","thinkingLevel":"high","time":1782012661213}`,
`{"type":"turn.prompt","input":[{"type":"text","text":"hello"}],"origin":{"kind":"user"},"time":1782012666987}`,
`{"type":"context.append_message","message":{"role":"user","content":[{"type":"text","text":"hello"}],"toolCalls":[],"origin":{"kind":"user"}},"time":1782012666987}`,
`{"type":"context.append_loop_event","event":{"type":"step.begin","uuid":"667ba233-c6d8-4939-b777-a5035ecc4215","turnId":"0","step":1},"time":1782012666989}`,
`{"type":"context.append_loop_event","event":{"type":"content.part","uuid":"1160b8b3-c0a6-436f-91db-ef737cd736cd","turnId":"0","step":1,"stepUuid":"667ba233-c6d8-4939-b777-a5035ecc4215","part":{"type":"think","think":"The user said hello. This is a simple greeting."}},"time":1782012668557}`,
`{"type":"context.append_loop_event","event":{"type":"content.part","uuid":"c379acc8-aaad-41bb-af8a-b478829e38f7","turnId":"0","step":1,"stepUuid":"667ba233-c6d8-4939-b777-a5035ecc4215","part":{"type":"text","text":"Hello! How can I help you today?"}},"time":1782012668558}`,
`{"type":"context.append_loop_event","event":{"type":"step.end","uuid":"667ba233-c6d8-4939-b777-a5035ecc4215","turnId":"0","step":1,"usage":{"inputOther":1598,"output":37,"inputCacheRead":14592,"inputCacheCreation":0},"finishReason":"end_turn","llmFirstTokenLatencyMs":1169,"llmStreamDurationMs":396},"time":1782012668558}`,
`{"type":"usage.record","model":"kimi-code/kimi-for-coding","usage":{"inputOther":1598,"output":37,"inputCacheRead":14592,"inputCacheCreation":0},"usageScope":"turn","time":1782012668558}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t, path, "myproject", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assertSessionMeta(t, sess,
"kimi:wd_myproject_a1b2c3d4:main:session_uuid-1234",
"myproject", AgentKimi,
)
assert.Equal(t, "hello", sess.FirstMessage)
assertMessageCount(t, sess.MessageCount, 2)
assert.Equal(t, 1, sess.UserMessageCount)
assert.Equal(t, 37, sess.TotalOutputTokens)
assert.Equal(t, 16190, sess.PeakContextTokens)
assert.True(t, sess.HasTotalOutputTokens)
assert.True(t, sess.HasPeakContextTokens)
require.Len(t, msgs, 2)
assertMessage(t, msgs[0], RoleUser, "hello")
assertTimestamp(t, msgs[0].Timestamp,
time.UnixMilli(1782012666987))
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.True(t, msgs[1].HasThinking)
assert.Contains(t, msgs[1].Content, "[Thinking]")
assert.Contains(t, msgs[1].Content, "simple greeting")
assert.Contains(t, msgs[1].Content,
"Hello! How can I help you today?")
assert.Equal(t, "kimi-code/kimi-for-coding", msgs[1].Model)
assert.Equal(t, "end_turn", msgs[1].StopReason)
assert.True(t, msgs[1].HasOutputTokens)
assert.True(t, msgs[1].HasContextTokens)
assert.Equal(t, 37, msgs[1].OutputTokens)
assert.Equal(t, 16190, msgs[1].ContextTokens)
assert.JSONEq(t,
`{"input_tokens":1598,"output_tokens":37,"cache_read_input_tokens":14592,"cache_creation_input_tokens":0}`,
string(msgs[1].TokenUsage),
)
assertTimestamp(t, msgs[1].Timestamp,
time.UnixMilli(1782012668557))
}
func TestParseKimiSession_NativeKimiCodeToolCall(t *testing.T) {
path := writeKimiCodeWireJSONL(t,
"wd_myproject_a1b2c3d4", "session_uuid-tool", "main",
[]string{
`{"type":"metadata","protocol_version":"1.4","created_at":1782012661212}`,
`{"type":"config.update","modelAlias":"kimi-code/kimi-for-coding","time":1782012661213}`,
`{"type":"turn.prompt","input":[{"type":"text","text":"List files"}],"origin":{"kind":"user"},"time":1782012666987}`,
`{"type":"context.append_loop_event","event":{"type":"step.begin","uuid":"step-1","turnId":"0","step":1},"time":1782012666989}`,
`{"type":"context.append_loop_event","event":{"type":"tool.call","uuid":"call-event","turnId":"0","step":1,"stepUuid":"step-1","toolCallId":"tool_1","name":"Bash","args":{"command":"ls","description":"List files"}},"time":1782012667000}`,
`{"type":"context.append_loop_event","event":{"type":"step.end","uuid":"step-1","turnId":"0","step":1,"usage":{"inputOther":10,"output":3,"inputCacheRead":20,"inputCacheCreation":0},"finishReason":"tool_use"},"time":1782012667001}`,
`{"type":"context.append_loop_event","event":{"type":"tool.result","parentUuid":"call-event","toolCallId":"tool_1","result":{"output":"main.go\nREADME.md","isError":false}},"time":1782012667002}`,
`{"type":"context.append_loop_event","event":{"type":"step.begin","uuid":"step-2","turnId":"0","step":2},"time":1782012667003}`,
`{"type":"context.append_loop_event","event":{"type":"content.part","uuid":"part-1","turnId":"0","step":2,"stepUuid":"step-2","part":{"type":"text","text":"Found the files."}},"time":1782012667004}`,
`{"type":"context.append_loop_event","event":{"type":"step.end","uuid":"step-2","turnId":"0","step":2,"usage":{"inputOther":30,"output":4,"inputCacheRead":40,"inputCacheCreation":5},"finishReason":"end_turn"},"time":1782012667005}`,
},
)
sess, msgs, err := parseKimiSessionForTest(t, path, "myproject", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, 7, sess.TotalOutputTokens)
assert.Equal(t, 75, sess.PeakContextTokens)
require.Len(t, msgs, 4)
assertMessage(t, msgs[0], RoleUser, "List files")
require.Equal(t, RoleAssistant, msgs[1].Role)
assert.True(t, msgs[1].HasToolUse)
assert.Contains(t, msgs[1].Content, "[Bash: List files]")
require.Len(t, msgs[1].ToolCalls, 1)
assert.Equal(t, "tool_1", msgs[1].ToolCalls[0].ToolUseID)
assert.Equal(t, "Bash", msgs[1].ToolCalls[0].ToolName)
assert.JSONEq(t, `{"command":"ls","description":"List files"}`,
msgs[1].ToolCalls[0].InputJSON)
assert.Equal(t, "tool_use", msgs[1].StopReason)
require.Equal(t, RoleUser, msgs[2].Role)
require.Len(t, msgs[2].ToolResults, 1)
assert.Equal(t, "tool_1", msgs[2].ToolResults[0].ToolUseID)
assert.Equal(t, "main.go\nREADME.md",
DecodeContent(msgs[2].ToolResults[0].ContentRaw))
assertMessage(t, msgs[3], RoleAssistant, "Found the files.")
assert.Equal(t, "end_turn", msgs[3].StopReason)
assert.Equal(t, 4, msgs[3].OutputTokens)
assert.Equal(t, 75, msgs[3].ContextTokens)
}
func TestParseKimiSession_NewLayout_AgentZero(t *testing.T) {
path := writeKimiCodeWireJSONL(t,
"wd_myproject_a1b2c3d4", "session_uuid-5678", "agent-0",
[]string{
`{"type": "metadata", "protocol_version": "1.3"}`,
`{"timestamp": 1704067200.0, "message": {"type": "TurnBegin", "payload": {"user_input": [{"type": "text", "text": "Hello subagent"}]}}}`,
`{"timestamp": 1704067201.0, "message": {"type": "ContentPart", "payload": {"type": "text", "text": "Done."}}}`,
`{"timestamp": 1704067202.0, "message": {"type": "TurnEnd", "payload": {}}}`,
},
)
sess, _, err := parseKimiSessionForTest(t, path, "myproject", "local")
require.NoError(t, err)
require.NotNil(t, sess)
assertSessionMeta(t, sess,
"kimi:wd_myproject_a1b2c3d4:agent-0:session_uuid-5678",
"myproject", AgentKimi,
)
}
func TestDiscoverKimiSessions_MixedLayouts(t *testing.T) {
dir := t.TempDir()
// Legacy layout.
legacyProjDir := filepath.Join(dir, "abc123")
legacySessDir := filepath.Join(legacyProjDir, "uuid-1")
require.NoError(t, os.MkdirAll(legacySessDir, 0o755))
legacyPath := filepath.Join(legacySessDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
legacyPath, []byte(`{"type":"metadata"}`+"\n"), 0o644,
))
// New layout.
workdirDir := "wd_foo_bar"
newSessDir := filepath.Join(dir, workdirDir, "session_xyz", "agents", "main")
require.NoError(t, os.MkdirAll(newSessDir, 0o755))
newPath := filepath.Join(newSessDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
newPath, []byte(`{"type":"metadata"}`+"\n"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, sources, 2)
paths := sourceDisplayPaths(sources)
assert.Contains(t, paths, legacyPath)
assert.Contains(t, paths, newPath)
}
func TestKimiSessionIDFromPath(t *testing.T) {
t.Run("legacy", func(t *testing.T) {
path := filepath.Join("/home", "user", ".kimi", "sessions", "abc123", "uuid-1", "wire.jsonl")
assert.Equal(t, "abc123:uuid-1", kimiSessionIDFromPath(path))
})
t.Run("kimi-code-main", func(t *testing.T) {
path := filepath.Join("/home", "user", ".kimi-code", "sessions", "wd_foo_a1b2", "session_uuid-1", "agents", "main", "wire.jsonl")
assert.Equal(t, "wd_foo_a1b2:main:session_uuid-1", kimiSessionIDFromPath(path))
})
t.Run("kimi-code-agent-zero", func(t *testing.T) {
path := filepath.Join("/home", "user", ".kimi-code", "sessions", "wd_foo_a1b2", "session_uuid-1", "agents", "agent-0", "wire.jsonl")
assert.Equal(t, "wd_foo_a1b2:agent-0:session_uuid-1", kimiSessionIDFromPath(path))
})
}
func TestDecodeKimiProjectDir(t *testing.T) {
tests := []struct {
input string
want string
}{
{"", ""},
// .kimi-code workdir names: "wd_<workdir>_<12-hex>".
{"wd_kimi-code_057f5c09ee3f", "kimi-code"},
{"wd_pi-mono_77f54d0fe81a", "pi-mono"},
// Underscores inside the workdir name are preserved.
{"wd_figma_mermaid_plugin_2_777a2abfe3e7", "figma_mermaid_plugin_2"},
// Uppercase/mixed-case hex is still recognized as the hash.
{"wd_foo_ABCDEF012345", "foo"},
// A trailing segment that is not a 12-hex hash is kept.
{"wd_foo_bar", "foo_bar"},
// Legacy opaque project hashes have no "wd_" prefix and
// are returned unchanged.
{"03cd233b1066bcc214245959059ca4c8", "03cd233b1066bcc214245959059ca4c8"},
{"abc123", "abc123"},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
assert.Equal(t, tt.want, DecodeKimiProjectDir(tt.input))
})
}
}
func TestDiscoverKimiSessions_NewLayout_RejectsInvalidComponent(t *testing.T) {
dir := t.TempDir()
// An agent name with a character outside [A-Za-z0-9_-] cannot
// round-trip through the ':'-delimited session ID, so that agent
// must be skipped at discovery while a valid sibling agent in the
// same session is still imported. A space stands in for any such
// character (':' itself is not a portable path component on
// Windows).
workdirDir := "wd_foo_1234567890ab"
sessionDir := "session_uuid-1"
badDir := filepath.Join(dir, workdirDir, sessionDir, "agents", "sub agent")
require.NoError(t, os.MkdirAll(badDir, 0o755))
require.NoError(t, os.WriteFile(
filepath.Join(badDir, "wire.jsonl"),
[]byte(`{"type":"metadata"}`+"\n"), 0o644,
))
goodDir := filepath.Join(dir, workdirDir, sessionDir, "agents", "main")
require.NoError(t, os.MkdirAll(goodDir, 0o755))
goodPath := filepath.Join(goodDir, "wire.jsonl")
require.NoError(t, os.WriteFile(
goodPath, []byte(`{"type":"metadata"}`+"\n"), 0o644,
))
provider, ok := NewProvider(AgentKimi, ProviderConfig{Roots: []string{dir}})
require.True(t, ok)
sources, err := provider.Discover(context.Background())
require.NoError(t, err)
require.Len(t, sources, 1)
assert.Equal(t, goodPath, sources[0].DisplayPath)
}