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

772 lines
19 KiB
Go

package parser
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func writeChatGPTFixture(
t *testing.T, dir, filename, content string,
) {
t.Helper()
err := os.WriteFile(
filepath.Join(dir, filename),
[]byte(content),
0o644,
)
require.NoError(t, err)
}
// Basic 3-message conversation: user, assistant, user.
func TestParseChatGPTExport(t *testing.T) {
dir := t.TempDir()
writeChatGPTFixture(t, dir, "conversations-001.json", `[
{
"conversation_id": "abc-123",
"title": "Hello Chat",
"create_time": 1700000000.0,
"update_time": 1700000060.0,
"current_node": "node-3",
"mapping": {
"root": {
"id": "root",
"parent": null,
"children": ["node-1"],
"message": null
},
"node-1": {
"id": "node-1",
"parent": "root",
"children": ["node-2"],
"message": {
"id": "msg-1",
"create_time": 1700000010.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["What is Go?"]
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-2": {
"id": "node-2",
"parent": "node-1",
"children": ["node-3"],
"message": {
"id": "msg-2",
"create_time": 1700000020.0,
"author": {"role": "assistant"},
"content": {
"content_type": "text",
"parts": ["Go is a programming language."]
},
"status": "finished_successfully",
"metadata": {"model_slug": "gpt-4"}
}
},
"node-3": {
"id": "node-3",
"parent": "node-2",
"children": [],
"message": {
"id": "msg-3",
"create_time": 1700000050.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["Thanks!"]
},
"status": "finished_successfully",
"metadata": {}
}
}
}
}
]`)
var results []ParseResult
err := parseChatGPTExport(dir, nil, func(r ParseResult) error {
results = append(results, r)
return nil
})
require.NoError(t, err)
require.Len(t, results, 1)
s := results[0].Session
assert.Equal(t, "chatgpt:abc-123", s.ID)
assert.Equal(t, "chatgpt.com", s.Project)
assert.Equal(t, "local", s.Machine)
assert.Equal(t, AgentChatGPT, s.Agent)
assert.Equal(t, "Hello Chat", s.SessionName)
assert.Equal(t, "What is Go?", s.FirstMessage)
assert.Equal(t, 3, s.MessageCount)
assert.Equal(t, 2, s.UserMessageCount)
msgs := results[0].Messages
require.Len(t, msgs, 3)
assert.Equal(t, 0, msgs[0].Ordinal)
assert.Equal(t, RoleUser, msgs[0].Role)
assert.Equal(t, "What is Go?", msgs[0].Content)
assert.Equal(t, 1, msgs[1].Ordinal)
assert.Equal(t, RoleAssistant, msgs[1].Role)
assert.Equal(t, "Go is a programming language.", msgs[1].Content)
assert.Equal(t, "gpt-4", msgs[1].Model)
assert.Equal(t, 2, msgs[2].Ordinal)
assert.Equal(t, RoleUser, msgs[2].Role)
assert.Equal(t, "Thanks!", msgs[2].Content)
}
// Tool nodes (code_interpreter + execution_output) should
// be attached to the preceding assistant message.
func TestParseChatGPTExport_ToolCalls(t *testing.T) {
dir := t.TempDir()
writeChatGPTFixture(t, dir, "conversations-001.json", `[
{
"conversation_id": "tool-conv",
"title": "Code Run",
"create_time": 1700000000.0,
"update_time": 1700000060.0,
"current_node": "node-4",
"mapping": {
"root": {
"id": "root",
"parent": null,
"children": ["node-1"],
"message": null
},
"node-1": {
"id": "node-1",
"parent": "root",
"children": ["node-2"],
"message": {
"id": "msg-1",
"create_time": 1700000010.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["Run print(42)"]
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-2": {
"id": "node-2",
"parent": "node-1",
"children": ["node-3"],
"message": {
"id": "msg-2",
"create_time": 1700000020.0,
"author": {"role": "assistant"},
"content": {
"content_type": "text",
"parts": ["Let me run that for you."]
},
"status": "finished_successfully",
"metadata": {"model_slug": "gpt-4"}
}
},
"node-3": {
"id": "node-3",
"parent": "node-2",
"children": ["node-4"],
"message": {
"id": "msg-code",
"create_time": 1700000030.0,
"author": {"role": "tool", "name": "python"},
"content": {
"content_type": "code",
"text": "print(42)",
"language": "python"
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-4": {
"id": "node-4",
"parent": "node-3",
"children": [],
"message": {
"id": "msg-output",
"create_time": 1700000035.0,
"author": {"role": "tool", "name": "python"},
"content": {
"content_type": "execution_output",
"text": "42"
},
"status": "finished_successfully",
"metadata": {}
}
}
}
}
]`)
var results []ParseResult
err := parseChatGPTExport(dir, nil, func(r ParseResult) error {
results = append(results, r)
return nil
})
require.NoError(t, err)
require.Len(t, results, 1)
msgs := results[0].Messages
// Tool nodes are NOT separate messages.
require.Len(t, msgs, 2, "tool nodes should be attached, not separate")
asst := msgs[1]
assert.Equal(t, RoleAssistant, asst.Role)
assert.True(t, asst.HasToolUse)
require.Len(t, asst.ToolCalls, 1)
assert.Equal(t, "code_interpreter", asst.ToolCalls[0].ToolName)
assert.Equal(t, "Bash", asst.ToolCalls[0].Category)
// execution_output is paired as a result event.
require.Len(t, asst.ToolCalls[0].ResultEvents, 1)
assert.Contains(t, asst.ToolCalls[0].ResultEvents[0].Content, "42")
}
// Thoughts content type should produce [Thinking] blocks.
func TestParseChatGPTExport_Thinking(t *testing.T) {
dir := t.TempDir()
writeChatGPTFixture(t, dir, "conversations-001.json", `[
{
"conversation_id": "think-conv",
"title": "Thinking Test",
"create_time": 1700000000.0,
"update_time": 1700000060.0,
"current_node": "node-2",
"mapping": {
"root": {
"id": "root",
"parent": null,
"children": ["node-1"],
"message": null
},
"node-1": {
"id": "node-1",
"parent": "root",
"children": ["node-2"],
"message": {
"id": "msg-1",
"create_time": 1700000010.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["Explain recursion"]
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-2": {
"id": "node-2",
"parent": "node-1",
"children": [],
"message": {
"id": "msg-2",
"create_time": 1700000020.0,
"author": {"role": "assistant"},
"content": {
"content_type": "thoughts",
"thoughts": [
{"content": "Let me think about recursion."},
{"content": "It is a function calling itself."}
]
},
"status": "finished_successfully",
"metadata": {"model_slug": "o1-preview"}
}
}
}
}
]`)
var results []ParseResult
err := parseChatGPTExport(dir, nil, 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)
asst := msgs[1]
assert.True(t, asst.HasThinking)
assert.Contains(t, asst.Content, "[Thinking]")
assert.Contains(t, asst.Content, "Let me think about recursion.")
assert.Contains(t, asst.Content, "It is a function calling itself.")
assert.Contains(t, asst.Content, "[/Thinking]")
assert.Equal(t, "o1-preview", asst.Model)
}
// System nodes should have IsSystem = true and count in
// MessageCount.
func TestParseChatGPTExport_SystemMessage(t *testing.T) {
dir := t.TempDir()
writeChatGPTFixture(t, dir, "conversations-001.json", `[
{
"conversation_id": "sys-conv",
"title": "System Test",
"create_time": 1700000000.0,
"update_time": 1700000060.0,
"current_node": "node-2",
"mapping": {
"root": {
"id": "root",
"parent": null,
"children": ["node-sys"],
"message": null
},
"node-sys": {
"id": "node-sys",
"parent": "root",
"children": ["node-1"],
"message": {
"id": "msg-sys",
"create_time": 1700000005.0,
"author": {"role": "system"},
"content": {
"content_type": "text",
"parts": ["You are a helpful assistant."]
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-1": {
"id": "node-1",
"parent": "node-sys",
"children": ["node-2"],
"message": {
"id": "msg-1",
"create_time": 1700000010.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["Hi"]
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-2": {
"id": "node-2",
"parent": "node-1",
"children": [],
"message": {
"id": "msg-2",
"create_time": 1700000020.0,
"author": {"role": "assistant"},
"content": {
"content_type": "text",
"parts": ["Hello!"]
},
"status": "finished_successfully",
"metadata": {"model_slug": "gpt-4"}
}
}
}
}
]`)
var results []ParseResult
err := parseChatGPTExport(dir, nil, func(r ParseResult) error {
results = append(results, r)
return nil
})
require.NoError(t, err)
require.Len(t, results, 1)
s := results[0].Session
assert.Equal(t, 3, s.MessageCount)
assert.Equal(t, 1, s.UserMessageCount)
msgs := results[0].Messages
require.Len(t, msgs, 3)
assert.True(t, msgs[0].IsSystem)
assert.Equal(t, "You are a helpful assistant.", msgs[0].Content)
assert.False(t, msgs[1].IsSystem)
assert.Equal(t, RoleUser, msgs[1].Role)
}
// Empty directory should produce no results and no error.
func TestParseChatGPTExport_EmptyDir(t *testing.T) {
dir := t.TempDir()
err := parseChatGPTExport(dir, nil, func(r ParseResult) error {
return nil
})
require.Error(t, err)
assert.Contains(t, err.Error(), "no conversation files")
}
// Multiple conversation files should all be processed.
func TestParseChatGPTExport_MultipleShards(t *testing.T) {
dir := t.TempDir()
shard := func(id, title string) string {
return `[{
"conversation_id": "` + id + `",
"title": "` + title + `",
"create_time": 1700000000.0,
"update_time": 1700000060.0,
"current_node": "node-1",
"mapping": {
"root": {
"id": "root",
"parent": null,
"children": ["node-1"],
"message": null
},
"node-1": {
"id": "node-1",
"parent": "root",
"children": [],
"message": {
"id": "msg-1",
"create_time": 1700000010.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["Hello"]
},
"status": "finished_successfully",
"metadata": {}
}
}
}
}]`
}
writeChatGPTFixture(t, dir, "conversations-001.json",
shard("conv-a", "First"))
writeChatGPTFixture(t, dir, "conversations-002.json",
shard("conv-b", "Second"))
var results []ParseResult
err := parseChatGPTExport(dir, nil, func(r ParseResult) error {
results = append(results, r)
return nil
})
require.NoError(t, err)
require.Len(t, results, 2)
assert.Equal(t, "chatgpt:conv-a", results[0].Session.ID)
assert.Equal(t, "chatgpt:conv-b", results[1].Session.ID)
}
// Verify DAG linearization walks from current_node to root and
// reverses.
func TestLinearizeDAG(t *testing.T) {
parent := "root"
mapping := map[string]chatGPTNode{
"root": {
ID: "root",
Parent: nil,
Children: []string{"a"},
},
"a": {
ID: "a",
Parent: &parent,
Children: []string{"b"},
},
"b": {
ID: "b",
Parent: new("a"),
},
}
nodes := linearizeDAG(mapping, "b")
require.Len(t, nodes, 3)
assert.Equal(t, "root", nodes[0].ID)
assert.Equal(t, "a", nodes[1].ID)
assert.Equal(t, "b", nodes[2].ID)
}
func TestLinearizeDAG_EmptyCurrentNode(t *testing.T) {
nodes := linearizeDAG(map[string]chatGPTNode{}, "")
assert.Nil(t, nodes)
}
func TestLinearizeDAG_MissingNode(t *testing.T) {
nodes := linearizeDAG(map[string]chatGPTNode{}, "missing")
assert.Empty(t, nodes)
}
// Verify assembleContent handles all content types.
func TestAssembleContent(t *testing.T) {
tests := []struct {
name string
c chatGPTContent
want string
}{
{
name: "text with string parts",
c: chatGPTContent{
ContentType: "text",
Parts: rawParts("Hello", "World"),
},
want: "Hello\nWorld",
},
{
name: "code block",
c: chatGPTContent{
ContentType: "code",
Text: "print(42)",
Language: "python",
},
want: "```python\nprint(42)\n```",
},
{
name: "execution_output",
c: chatGPTContent{
ContentType: "execution_output",
Text: "42",
},
want: "```\n42\n```",
},
{
name: "thoughts",
c: chatGPTContent{
ContentType: "thoughts",
Thoughts: []chatGPTThought{
{Content: "thinking hard"},
},
},
want: "[Thinking]\nthinking hard\n[/Thinking]",
},
{
name: "tether_quote",
c: chatGPTContent{
ContentType: "tether_quote",
Text: "some quote",
Title: "Source",
URL: "https://example.com",
},
want: "> some quote\n> -- [Source](https://example.com)",
},
{
name: "reasoning_recap is skipped",
c: chatGPTContent{
ContentType: "reasoning_recap",
Text: "recap text",
},
want: "",
},
{
name: "tether_browsing_display is skipped",
c: chatGPTContent{
ContentType: "tether_browsing_display",
},
want: "",
},
{
name: "system_error",
c: chatGPTContent{
ContentType: "system_error",
Text: "something went wrong",
},
want: "something went wrong",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := assembleContent(tt.c, nil)
assert.Equal(t, tt.want, got)
})
}
}
// Verify image asset resolution with a mock resolver.
func TestAssembleContent_ImageAsset(t *testing.T) {
c := chatGPTContent{
ContentType: "multimodal_text",
Parts: []json.RawMessage{
json.RawMessage(`"Here is an image:"`),
json.RawMessage(`{
"content_type": "image_asset_pointer",
"asset_pointer": "file-service://file-abc123"
}`),
},
}
resolver := &mockAssetResolver{
files: map[string]string{
"file-service://file-abc123": "/tmp/img.png",
},
copyRef: "asset://abc123.png",
}
got := assembleContent(c, resolver)
assert.Contains(t, got, "Here is an image:")
assert.Contains(t, got, "![image](asset://abc123.png)")
}
// Nil resolver should produce [image unavailable].
func TestAssembleContent_ImageAsset_NilResolver(t *testing.T) {
c := chatGPTContent{
ContentType: "multimodal_text",
Parts: []json.RawMessage{
json.RawMessage(`{
"content_type": "image_asset_pointer",
"asset_pointer": "file-service://file-abc123"
}`),
},
}
got := assembleContent(c, nil)
assert.Equal(t, "[image unavailable]", got)
}
func TestUnixFloatToTime(t *testing.T) {
t.Run("nil", func(t *testing.T) {
assert.True(t, unixFloatToTime(nil).IsZero())
})
t.Run("zero", func(t *testing.T) {
z := 0.0
assert.True(t, unixFloatToTime(&z).IsZero())
})
t.Run("valid", func(t *testing.T) {
v := 1700000000.5
got := unixFloatToTime(&v)
assert.Equal(t, int64(1700000000), got.Unix())
assert.Equal(t, 500000000, got.Nanosecond())
})
}
// Conversation with web_search tool nodes.
func TestParseChatGPTExport_WebSearch(t *testing.T) {
dir := t.TempDir()
writeChatGPTFixture(t, dir, "conversations-001.json", `[
{
"conversation_id": "web-conv",
"title": "Web Search",
"create_time": 1700000000.0,
"update_time": 1700000060.0,
"current_node": "node-3",
"mapping": {
"root": {
"id": "root",
"parent": null,
"children": ["node-1"],
"message": null
},
"node-1": {
"id": "node-1",
"parent": "root",
"children": ["node-2"],
"message": {
"id": "msg-1",
"create_time": 1700000010.0,
"author": {"role": "user"},
"content": {
"content_type": "text",
"parts": ["Search for Go"]
},
"status": "finished_successfully",
"metadata": {}
}
},
"node-2": {
"id": "node-2",
"parent": "node-1",
"children": ["node-3"],
"message": {
"id": "msg-2",
"create_time": 1700000020.0,
"author": {"role": "assistant"},
"content": {
"content_type": "text",
"parts": ["Searching..."]
},
"status": "finished_successfully",
"metadata": {"model_slug": "gpt-4"}
}
},
"node-3": {
"id": "node-3",
"parent": "node-2",
"children": [],
"message": {
"id": "msg-browse",
"create_time": 1700000030.0,
"author": {"role": "tool"},
"content": {
"content_type": "tether_quote",
"text": "Go is great",
"title": "Go Blog",
"url": "https://go.dev/blog"
},
"status": "finished_successfully",
"metadata": {}
}
}
}
}
]`)
var results []ParseResult
err := parseChatGPTExport(dir, nil, 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)
asst := msgs[1]
assert.True(t, asst.HasToolUse)
require.Len(t, asst.ToolCalls, 1)
assert.Equal(t, "web_search", asst.ToolCalls[0].ToolName)
assert.Equal(t, "Tool", asst.ToolCalls[0].Category)
}
// --- helpers ---
// rawParts creates json.RawMessage slices from strings for
// chatGPTContent.Parts test fixtures.
func rawParts(ss ...string) []json.RawMessage {
var parts []json.RawMessage
for _, s := range ss {
b, _ := json.Marshal(s)
parts = append(parts, b)
}
return parts
}
type mockAssetResolver struct {
files map[string]string
copyRef string
}
func (m *mockAssetResolver) Resolve(
pointer string,
) (string, bool) {
p, ok := m.files[pointer]
return p, ok
}
func (m *mockAssetResolver) Copy(string) (string, error) {
return m.copyRef, nil
}