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

423 lines
10 KiB
Go

package parser
import (
"bytes"
"database/sql"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"testing"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const devinSyntheticSessionsSchema = `
CREATE TABLE sessions (
id TEXT PRIMARY KEY,
title TEXT,
working_directory TEXT,
model TEXT,
created_at INTEGER,
last_activity_at INTEGER,
workspace_json TEXT,
metadata_json TEXT,
hidden INTEGER NOT NULL DEFAULT 0
);
`
const devinSyntheticMessageNodesSchema = `
CREATE TABLE message_nodes (
row_id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
node_id INTEGER NOT NULL,
parent_node_id INTEGER,
chat_message TEXT NOT NULL,
created_at INTEGER NOT NULL,
metadata TEXT
);
`
type devinSessionRow struct {
ID string
Title string
WorkingDirectory string
Model string
CreatedAtMillis *int64
LastActivityMillis *int64
WorkspaceJSON string
MetadataJSON string
Hidden bool
}
type devinTestFixture struct {
Root string
CLIDir string
TranscriptsDir string
DBPath string
}
type devinSyntheticMessageNodeRow struct {
SessionID string
NodeID int64
ParentNodeID *int64
ChatMessage string
CreatedAtMillis int64
MetadataJSON string
}
func newDevinTestFixture(t *testing.T, rows ...devinSessionRow) *devinTestFixture {
t.Helper()
root := t.TempDir()
cliDir := filepath.Join(root, "cli")
transcriptsDir := filepath.Join(cliDir, "transcripts")
require.NoError(t, os.MkdirAll(transcriptsDir, 0o755))
fixture := &devinTestFixture{
Root: root,
CLIDir: cliDir,
TranscriptsDir: transcriptsDir,
DBPath: filepath.Join(cliDir, devinDBFilename),
}
initDevinTestDB(t, fixture.DBPath)
execDevinTestSQL(t, fixture.DBPath, devinSyntheticSessionsSchema)
execDevinTestSQL(t, fixture.DBPath, devinSyntheticMessageNodesSchema)
for _, row := range rows {
insertDevinSessionRow(t, fixture.DBPath, row)
}
return fixture
}
func initDevinTestDB(t *testing.T, dbPath string) {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
require.NoError(t, db.Close())
}
func insertDevinSessionRow(t *testing.T, dbPath string, row devinSessionRow) {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
_, err = db.Exec(`
INSERT INTO sessions (
id,
title,
working_directory,
model,
created_at,
last_activity_at,
workspace_json,
metadata_json,
hidden
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
row.ID,
row.Title,
row.WorkingDirectory,
row.Model,
devinNullableMillis(row.CreatedAtMillis),
devinNullableMillis(row.LastActivityMillis),
devinNullableString(row.WorkspaceJSON),
devinNullableString(row.MetadataJSON),
row.Hidden,
)
require.NoError(t, err)
}
func devinNullableMillis(ms *int64) any {
if ms == nil {
return nil
}
return *ms
}
func devinNullableString(value string) any {
if value == "" {
return nil
}
return value
}
func (f *devinTestFixture) writeTranscript(t *testing.T, sessionID, transcript string) string {
t.Helper()
transcriptPath := filepath.Join(f.TranscriptsDir, sessionID+".json")
require.NoError(t, os.WriteFile(transcriptPath, []byte(transcript), 0o644))
return transcriptPath
}
func (f *devinTestFixture) sessionVirtualPath(sessionID string) string {
return VirtualSourcePath(f.DBPath, sessionID)
}
func (f *devinTestFixture) insertMessageNodes(t *testing.T, rows ...devinSyntheticMessageNodeRow) {
t.Helper()
for _, row := range rows {
insertDevinMessageNodeRow(t, f.DBPath, row)
}
}
func newDevinSessionFixture(t *testing.T, row devinSessionRow, transcript string) (string, string) {
t.Helper()
fixture := newDevinTestFixture(t, row)
return fixture.DBPath, fixture.writeTranscript(t, row.ID, transcript)
}
func execDevinTestSQL(t *testing.T, dbPath, query string) {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
_, err = db.Exec(query)
require.NoError(t, err)
}
func insertDevinMessageNodeRow(t *testing.T, dbPath string, row devinSyntheticMessageNodeRow) {
t.Helper()
db, err := sql.Open("sqlite3", dbPath)
require.NoError(t, err)
defer db.Close()
_, err = db.Exec(`
INSERT INTO message_nodes (
session_id,
node_id,
parent_node_id,
chat_message,
created_at,
metadata
) VALUES (?, ?, ?, ?, ?, ?)
`,
row.SessionID,
row.NodeID,
devinNullableInt64(row.ParentNodeID),
row.ChatMessage,
row.CreatedAtMillis,
devinNullableString(row.MetadataJSON),
)
require.NoError(t, err)
}
func devinNullableInt64(value *int64) any {
if value == nil {
return nil
}
return *value
}
func devinMetaIDs(metas []DevinSessionMeta) []string {
ids := make([]string, 0, len(metas))
for _, meta := range metas {
ids = append(ids, meta.RawSessionID)
}
return ids
}
// parseChatGPTExport parses a ChatGPT export through the ChatGPT import-only
// provider. It is the test harness replacement for the former
// ParseChatGPTExport free function.
func parseChatGPTExport(
dir string,
assets AssetResolver,
onConversation func(ParseResult) error,
) error {
p, ok := NewProvider(AgentChatGPT, ProviderConfig{})
if !ok {
return fmt.Errorf("chatgpt provider unavailable")
}
exporter, ok := p.(ChatGPTExportParser)
if !ok {
return fmt.Errorf("chatgpt provider does not support exports")
}
return exporter.ParseChatGPTExport(dir, assets, onConversation)
}
// parseClaudeAIExport parses a Claude.ai export through the Claude.ai
// import-only provider. It is the test harness replacement for the former
// ParseClaudeAIExport free function.
func parseClaudeAIExport(
r io.Reader,
onConversation func(ParseResult) error,
) error {
p, ok := NewProvider(AgentClaudeAI, ProviderConfig{})
if !ok {
return fmt.Errorf("claude.ai provider unavailable")
}
exporter, ok := p.(ClaudeAIExportParser)
if !ok {
return fmt.Errorf("claude.ai provider does not support exports")
}
return exporter.ParseClaudeAIExport(r, onConversation)
}
// Timestamp constants for test data.
const (
tsZero = "2024-01-01T00:00:00Z"
tsZeroS1 = "2024-01-01T00:00:01Z"
tsZeroS2 = "2024-01-01T00:00:02Z"
tsEarly = "2024-01-01T10:00:00Z"
tsEarlyS1 = "2024-01-01T10:00:01Z"
tsEarlyS5 = "2024-01-01T10:00:05Z"
tsLate = "2024-01-01T10:01:00Z"
tsLateS5 = "2024-01-01T10:01:05Z"
)
// Parsed time.Time values used as expected results in
// timestamp parsing tests.
var testJan15_1030UTC = time.Date(
2024, 1, 15, 10, 30, 0, 0, time.UTC,
)
// --- Data Generators ---
func generateLargeString(size int) string {
return strings.Repeat("x", size)
}
// --- Assertions ---
func assertSessionMeta(t *testing.T, s *ParsedSession, wantID, wantProject string, wantAgent AgentType) {
t.Helper()
require.NotNil(t, s, "session is nil")
assert.Equal(t, wantID, s.ID, "session ID")
assert.Equal(t, wantProject, s.Project, "project")
assert.Equal(t, wantAgent, s.Agent, "agent")
}
func assertMessage(t *testing.T, m ParsedMessage, wantRole RoleType, wantContentSnippet string) {
t.Helper()
assert.Equal(t, wantRole, m.Role, "role")
if wantContentSnippet != "" {
assert.Contains(t, m.Content, wantContentSnippet)
}
}
func assertMessageCount(t *testing.T, count, want int) {
t.Helper()
require.Equal(t, want, count, "message count")
}
func assertTimestamp(t *testing.T, got time.Time, want time.Time) {
t.Helper()
assert.True(t, got.Equal(want), "timestamp = %v, want %v", got, want)
}
func assertZeroTimestamp(
t *testing.T, ts time.Time, label string,
) {
t.Helper()
assert.True(t, ts.IsZero(), "%s = %v, want zero", label, ts)
}
// captureLog redirects log output to a buffer for the
// duration of the test and restores it on cleanup.
func captureLog(t *testing.T) *bytes.Buffer {
t.Helper()
var buf bytes.Buffer
old := log.Writer()
log.SetOutput(&buf)
t.Cleanup(func() { log.SetOutput(old) })
return &buf
}
func assertLogContains(
t *testing.T, buf *bytes.Buffer, substrs ...string,
) {
t.Helper()
got := buf.String()
for _, s := range substrs {
assert.Contains(t, got, s, "log missing substring %q", s)
}
}
func assertLogNotContains(
t *testing.T, buf *bytes.Buffer, substrs ...string,
) {
t.Helper()
got := buf.String()
for _, s := range substrs {
assert.NotContains(t, got, s, "log should not contain substring %q", s)
}
}
func assertLogEmpty(t *testing.T, buf *bytes.Buffer) {
t.Helper()
assert.Zero(t, buf.Len(), "expected no log output, got: %q", buf.String())
}
func assertToolCallField(t *testing.T, i int, field, got, want string) {
t.Helper()
assert.Equal(t, want, got, fmt.Sprintf("tool_calls[%d].%s", i, field))
}
func assertToolCall(t *testing.T, i int, got, want ParsedToolCall) {
t.Helper()
assertToolCallField(t, i, "ToolName", got.ToolName, want.ToolName)
assertToolCallField(t, i, "Category", got.Category, want.Category)
if want.ToolUseID != "" {
assertToolCallField(t, i, "ToolUseID", got.ToolUseID, want.ToolUseID)
}
if want.InputJSON != "" {
assertToolCallField(t, i, "InputJSON", got.InputJSON, want.InputJSON)
}
if want.SkillName != "" {
assertToolCallField(t, i, "SkillName", got.SkillName, want.SkillName)
}
assertToolCallField(t, i, "SubagentSessionID", got.SubagentSessionID, want.SubagentSessionID)
}
func assertToolCalls(
t *testing.T, got, want []ParsedToolCall,
) {
t.Helper()
if !assert.Equal(t, len(want), len(got), "tool calls count") {
return
}
for i := range want {
assertToolCall(t, i, got[i], want[i])
}
}
func parseClaudeTestFile(
t *testing.T, name, content, project string,
) (ParsedSession, []ParsedMessage) {
t.Helper()
path := createTestFile(t, name, content)
results, err := parseClaudeSession(
path, project, "local",
)
require.NoError(t, err, "parseClaudeSession")
require.NotEmpty(t, results, "parseClaudeSession returned no results")
return results[0].Session, results[0].Messages
}
// parseClaudeSession parses a standalone Claude transcript through the Claude
// provider's upload entry point, honoring the explicit project. It is the
// test harness replacement for the former ParseClaudeSession free function,
// exercising the same provider-owned parse body that production uploads use.
func parseClaudeSession(
path, project, machine string,
) ([]ParseResult, error) {
provider, ok := NewProvider(AgentClaude, ProviderConfig{Machine: machine})
if !ok {
return nil, fmt.Errorf("claude provider unavailable")
}
uploader, ok := provider.(ClaudeUploadParser)
if !ok {
return nil, fmt.Errorf("claude provider does not support upload parsing")
}
return uploader.ParseUploadedTranscript(path, project, machine)
}