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

400 lines
9.8 KiB
Go

package parser
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const testExportJSON = `[
{
"uuid": "conv-001",
"name": "Test Chat",
"summary": "A test conversation",
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:30:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "msg-001",
"text": "Hello, how are you?",
"content": [{"type": "text", "text": "Hello, how are you?"}],
"sender": "human",
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z",
"attachments": [],
"files": []
},
{
"uuid": "msg-002",
"text": "I'm doing well, thanks!",
"content": [{"type": "text", "text": "I'm doing well, thanks!"}],
"sender": "assistant",
"created_at": "2026-01-15T10:00:30.000000Z",
"updated_at": "2026-01-15T10:00:30.000000Z",
"attachments": [],
"files": []
}
]
},
{
"uuid": "conv-002",
"name": "",
"summary": "",
"created_at": "2026-01-16T12:00:00.000000Z",
"updated_at": "2026-01-16T12:05:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": []
},
{
"uuid": "conv-003",
"name": "Second Chat",
"summary": "",
"created_at": "2026-01-17T08:00:00.000000Z",
"updated_at": "2026-01-17T08:10:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "msg-003",
"text": "What is Go?",
"content": [{"type": "text", "text": "What is Go?"}],
"sender": "human",
"created_at": "2026-01-17T08:00:00.000000Z",
"updated_at": "2026-01-17T08:00:00.000000Z",
"attachments": [],
"files": []
}
]
}
]`
func TestParseClaudeAIExport(t *testing.T) {
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader(testExportJSON),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
// conv-002 has no messages, should be skipped.
require.Len(t, results, 2)
// First conversation.
s := results[0].Session
assert.Equal(t, "claude-ai:conv-001", s.ID)
assert.Equal(t, "claude.ai", s.Project)
assert.Equal(t, "local", s.Machine)
assert.Equal(t, AgentClaudeAI, s.Agent)
assert.Equal(t, "Hello, how are you?", s.FirstMessage)
assert.Equal(t, "Test Chat", s.SessionName)
assert.Equal(t, 2, s.MessageCount)
assert.Equal(t, 1, s.UserMessageCount)
assert.Equal(t,
"2026-01-15T10:00:00.000000Z",
s.StartedAt.Format("2006-01-02T15:04:05.000000Z"),
)
msgs := results[0].Messages
require.Len(t, msgs, 2)
assert.Equal(t, 0, msgs[0].Ordinal)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "Hello, how are you?", msgs[0].Content)
assert.Equal(t, 1, msgs[1].Ordinal)
assert.Equal(t, RoleAssistant, msgs[1].Role)
// Third conversation (second result).
s2 := results[1].Session
assert.Equal(t, "claude-ai:conv-003", s2.ID)
assert.Equal(t, 1, s2.MessageCount)
assert.Equal(t, 1, s2.UserMessageCount)
}
func TestParseClaudeAIExport_ContentBlocks(t *testing.T) {
input := `[{
"uuid": "conv-blocks",
"name": "Block Test",
"created_at": "2026-01-20T10:00:00.000000Z",
"updated_at": "2026-01-20T10:05:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "m1",
"text": "This block is not supported on your current device yet.",
"content": [
{"type": "text", "text": "First part."},
{"type": "tool_use", "text": ""},
{"type": "tool_result", "text": ""},
{"type": "text", "text": "Second part."}
],
"sender": "assistant",
"created_at": "2026-01-20T10:00:00.000000Z"
},
{
"uuid": "m2",
"text": "",
"content": [
{"type": "thinking", "thinking": "deep thought"},
{"type": "text", "text": "The answer."}
],
"sender": "assistant",
"created_at": "2026-01-20T10:01:00.000000Z"
}
]
}]`
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader(input),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
require.Len(t, results, 1)
msgs := results[0].Messages
// Message with tool_use/tool_result blocks should use
// text blocks, not the truncated top-level text.
assert.Equal(t, "First part.\n\nSecond part.", msgs[0].Content)
assert.False(t, msgs[0].HasThinking)
// Message with thinking block.
assert.Contains(t, msgs[1].Content, "[Thinking]")
assert.Contains(t, msgs[1].Content, "deep thought")
assert.Contains(t, msgs[1].Content, "The answer.")
assert.True(t, msgs[1].HasThinking)
}
func TestParseClaudeAIExport_AttachmentContent(t *testing.T) {
input := `[{
"uuid": "conv-attachments",
"name": "Attachment Test",
"created_at": "2026-01-21T10:00:00.000000Z",
"updated_at": "2026-01-21T10:05:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "m1",
"text": "Here is the base idea.",
"content": [
{"type": "text", "text": "Here is the base idea."}
],
"sender": "assistant",
"created_at": "2026-01-21T10:00:00.000000Z",
"attachments": [
{
"file_name": "notes.md",
"extracted_content": "line one\nline two"
}
]
}
]
}]`
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader(input),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
require.Len(t, results, 1)
msgs := results[0].Messages
require.Len(t, msgs, 1)
assert.Equal(
t,
"Here is the base idea.\n\n[Attachment: notes.md]\nline one\nline two",
msgs[0].Content,
)
}
func TestParseClaudeAIExport_AttachmentFallbackPaths(t *testing.T) {
input := `[{
"uuid": "conv-attachment-fallbacks",
"name": "Attachment Fallback Test",
"created_at": "2026-01-21T11:00:00.000000Z",
"updated_at": "2026-01-21T11:05:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "m1",
"text": "Top-level text survives.",
"content": [],
"sender": "assistant",
"created_at": "2026-01-21T11:00:00.000000Z",
"attachments": [
{
"extracted_content": "attachment with no filename"
}
]
},
{
"uuid": "m2",
"text": "Fallback text survives too.",
"content": [
{"type": "tool_use", "text": ""}
],
"sender": "assistant",
"created_at": "2026-01-21T11:01:00.000000Z",
"attachments": [
{
"extracted_content": "attachment after unsupported block"
}
]
}
]
}]`
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader(input),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
require.Len(t, results, 1)
msgs := results[0].Messages
require.Len(t, msgs, 2)
assert.Equal(
t,
"Top-level text survives.\n\nattachment with no filename",
msgs[0].Content,
)
assert.Equal(
t,
"Fallback text survives too.\n\nattachment after unsupported block",
msgs[1].Content,
)
}
func TestParseClaudeAIExport_IgnoredAttachments(t *testing.T) {
input := `[{
"uuid": "conv-attachments-ignored",
"name": "Attachment Ignore Test",
"created_at": "2026-01-22T10:00:00.000000Z",
"updated_at": "2026-01-22T10:05:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "m1",
"text": "User prompt",
"content": [
{"type": "text", "text": "User prompt"}
],
"sender": "human",
"created_at": "2026-01-22T10:00:00.000000Z"
},
{
"uuid": "m2",
"text": "Response kept the same.",
"content": [
{"type": "text", "text": "Response kept the same."}
],
"sender": "assistant",
"created_at": "2026-01-22T10:01:00.000000Z",
"attachments": [
{"file_name": "empty.txt", "extracted_content": ""},
{"file_name": "noop.txt", "extracted_content": " "},
{"file_name": "missing.txt"},
{
"file_name": "metadata.json",
"mime_type": "application/json",
"metadata": {"unexpected": "value"}
}
]
}
]
}]`
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader(input),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
require.Len(t, results, 1)
msgs := results[0].Messages
require.Len(t, msgs, 2)
assert.Equal(t, "User prompt", msgs[0].Content)
assert.Equal(t, "Response kept the same.", msgs[1].Content)
assert.NotContains(t, msgs[1].Content, "metadata.json")
assert.NotContains(t, msgs[1].Content, "unexpected")
}
func TestParseClaudeAIExport_TextFallbackPreservesWhitespace(t *testing.T) {
input := `[{
"uuid": "conv-fallback",
"name": "Fallback Test",
"created_at": "2026-01-23T10:00:00.000000Z",
"updated_at": "2026-01-23T10:05:00.000000Z",
"account": {"uuid": "acct-1"},
"chat_messages": [
{
"uuid": "m1",
"text": " keep surrounding whitespace ",
"content": [],
"sender": "assistant",
"created_at": "2026-01-23T10:00:00.000000Z",
"attachments": []
}
]
}]`
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader(input),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
require.Len(t, results, 1)
require.Len(t, results[0].Messages, 1)
assert.Equal(
t,
" keep surrounding whitespace ",
results[0].Messages[0].Content,
)
}
func TestParseClaudeAIExport_EmptyArray(t *testing.T) {
var results []ParseResult
err := parseClaudeAIExport(
strings.NewReader("[]"),
func(r ParseResult) error {
results = append(results, r)
return nil
},
)
require.NoError(t, err)
assert.Empty(t, results)
}
func TestParseClaudeAIExport_InvalidJSON(t *testing.T) {
err := parseClaudeAIExport(
strings.NewReader("{not json"),
func(r ParseResult) error { return nil },
)
require.Error(t, err)
}