package ai import ( "encoding/json" "os" "strings" "testing" "pdf-form-api/models" ) func TestNewClient(t *testing.T) { c := NewClient("https://api.example.com", "test-key", "test-model") if c.BaseURL != "https://api.example.com" { t.Errorf("expected base URL, got %s", c.BaseURL) } if c.APIKey != "test-key" { t.Errorf("expected API key, got %s", c.APIKey) } if c.Model != "test-model" { t.Errorf("expected model, got %s", c.Model) } } func TestNewClientTrimsBaseURL(t *testing.T) { c := NewClient("https://api.example.com/", "key", "model") if c.BaseURL != "https://api.example.com" { t.Errorf("expected trimmed URL, got %s", c.BaseURL) } } func TestParseResponse(t *testing.T) { tests := []struct { name string input string wantN int wantErr bool }{ { name: "valid response", input: `[{"name": "Field1", "question": "What is field 1?"}, {"name": "Field2", "question": "What is field 2?"}]`, wantN: 2, wantErr: false, }, { name: "with markdown wrapper", input: "```json\n[{'name': 'Field1', 'question': 'Q1?'}]\n```", wantN: 0, wantErr: true, }, { name: "with extra text", input: "Here is the result: [{\"name\": \"Field1\", \"question\": \"Q1?\"}]. Done!", wantN: 1, wantErr: false, }, { name: "empty array", input: "[]", wantN: 0, wantErr: false, }, { name: "no JSON array", input: "just some text", wantN: 0, wantErr: true, }, { name: "empty question gets name", input: `[{"name": "Field1", "question": ""}]`, wantN: 1, wantErr: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { results, err := parseResponse(tc.input) if tc.wantErr && err == nil { t.Error("expected error, got nil") } if !tc.wantErr && err != nil { t.Errorf("unexpected error: %v", err) } if len(results) != tc.wantN { t.Errorf("expected %d results, got %d", tc.wantN, len(results)) } }) } } func TestParseResponseEmptyQuestion(t *testing.T) { input := `[{"name": "Field1", "question": ""}]` results, err := parseResponse(input) if err != nil { t.Fatalf("parseResponse: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } if results[0].Question != "Field1" { t.Errorf("expected question to be field name, got %q", results[0].Question) } } func TestTruncate(t *testing.T) { short := "hello" if result := truncate(short, 10); result != short { t.Errorf("truncate(%q, 10) = %q", short, result) } long := strings.Repeat("a", 100) trunc := truncate(long, 10) if len(trunc) != 13 { t.Errorf("truncate long: expected 13 chars, got %d: %q", len(trunc), trunc) } if !strings.HasSuffix(trunc, "...") { t.Error("truncated string should end with ...") } } func TestGenerateQuestionsNonExistent(t *testing.T) { c := NewClient("https://api.example.com", "fake-key", "fake-model") _, err := c.GenerateQuestions("/nonexistent.pdf", nil) if err == nil { t.Error("expected error for non-existent file") } } // TestLiveAPI tests against the actual DeepSeek API if the key is available func TestLiveAPI(t *testing.T) { apiKey := os.Getenv("DEEPSEEK_API_KEY") if apiKey == "" { t.Skip("DEEPSEEK_API_KEY not set") } c := NewClient("https://api.deepseek.com", apiKey, "deepseek-chat") // Test with a simple text prompt results, err := c.askAPI("Generate a question for a form field named 'Given Name' of type text.\nReturn a JSON array: [{\"name\": \"Given Name\", \"question\": \"What is your given name?\"}]") if err != nil { t.Fatalf("askAPI: %v", err) } if len(results) == 0 { t.Error("expected at least one result") } for _, r := range results { if r.Name == "" { t.Error("result should have a name") } if r.Question == "" { t.Error("result should have a question") } t.Logf("Field: %s -> Question: %s", r.Name, r.Question) } } func TestChatMessageMarshalText(t *testing.T) { msg := chatMessage{Role: "user", Content: "hello"} data, err := json.Marshal(msg) if err != nil { t.Fatalf("Marshal text: %v", err) } var parsed map[string]interface{} if err := json.Unmarshal(data, &parsed); err != nil { t.Fatalf("Unmarshal: %v", err) } if parsed["content"] != "hello" { t.Errorf("expected content string, got %v", parsed["content"]) } } func TestChatMessageMarshalVision(t *testing.T) { msg := chatMessage{ Role: "user", ContentParts: []contentPart{ {Type: "text", Text: "look at this"}, {Type: "image_url", ImageURL: &imageURL{URL: "data:image/png;base64,abc"}}, }, } data, err := json.Marshal(msg) if err != nil { t.Fatalf("Marshal vision: %v", err) } var parsed map[string]interface{} if err := json.Unmarshal(data, &parsed); err != nil { t.Fatalf("Unmarshal: %v", err) } content, ok := parsed["content"].([]interface{}) if !ok { t.Fatalf("expected content as array, got %T", parsed["content"]) } if len(content) != 2 { t.Errorf("expected 2 content parts, got %d", len(content)) } if !strings.Contains(string(data), "image_url") { t.Error("expected image_url in marshaled JSON") } if !strings.Contains(string(data), "data:image/png;base64,abc") { t.Error("expected base64 data URL in marshaled JSON") } } func TestParseVisionResponse(t *testing.T) { input := `{ "description": "A simple test form", "fields": [ {"label": "F1", "question": "What is field 1?", "value_group": "", "wizard_page": 1}, {"label": "F2", "question": "What is field 2?", "value_group": "Name", "wizard_page": 1} ] }` result, err := parseVisionResponse(input) if err != nil { t.Fatalf("parseVisionResponse: %v", err) } if result.Description != "A simple test form" { t.Errorf("expected description, got %q", result.Description) } if len(result.Fields) != 2 { t.Fatalf("expected 2 fields, got %d", len(result.Fields)) } if result.Fields[0].Label != "F1" { t.Errorf("expected F1, got %q", result.Fields[0].Label) } if result.Fields[0].Question != "What is field 1?" { t.Errorf("expected F1 question, got %q", result.Fields[0].Question) } if result.Fields[1].Label != "F2" { t.Errorf("expected F2, got %q", result.Fields[1].Label) } if result.Fields[1].ValueGroup != "Name" { t.Errorf("expected value_group 'Name', got %q", result.Fields[1].ValueGroup) } if result.Fields[1].WizardPage != 1 { t.Errorf("expected wizard_page 1, got %d", result.Fields[1].WizardPage) } } func TestParseVisionResponseWithMarkdown(t *testing.T) { input := "Here are the results:\n```json\n{\"description\": \"A form\", \"fields\": [{\"label\": \"F1\", \"question\": \"Test Q?\", \"value_group\": \"\", \"wizard_page\": 1}]}\n```" result, err := parseVisionResponse(input) if err != nil { t.Fatalf("parseVisionResponse: %v", err) } if len(result.Fields) != 1 { t.Fatalf("expected 1 field, got %d", len(result.Fields)) } if result.Fields[0].Question != "Test Q?" { t.Errorf("expected F1 question, got %q", result.Fields[0].Question) } } func TestParseVisionResponseEmptyQuestion(t *testing.T) { input := `{"description": "A form", "fields": [{"label": "F1", "question": "", "value_group": "", "wizard_page": 1}]}` result, err := parseVisionResponse(input) if err != nil { t.Fatalf("parseVisionResponse: %v", err) } if len(result.Fields) != 1 { t.Errorf("expected 1 field, got %d", len(result.Fields)) } if result.Fields[0].Question != "F1" { t.Errorf("expected empty question to be label, got %q", result.Fields[0].Question) } } func TestParseVisionResponseNoJSON(t *testing.T) { input := "just some text" _, err := parseVisionResponse(input) if err == nil { t.Error("expected error for non-JSON input") } } func TestParseVisionResponseWithValueGroups(t *testing.T) { input := `{ "description": "A legal form", "fields": [ {"label": "F1", "question": "Client first name?", "value_group": "Client Name", "wizard_page": 1}, {"label": "F2", "question": "Client last name?", "value_group": "Client Name", "wizard_page": 1}, {"label": "F3", "question": "Attorney name?", "value_group": "Attorney Name", "wizard_page": 2}, {"label": "F4", "question": "Client name again?", "value_group": "Client Name", "wizard_page": 2} ] }` result, err := parseVisionResponse(input) if err != nil { t.Fatalf("parseVisionResponse: %v", err) } if result.Description != "A legal form" { t.Errorf("expected description, got %q", result.Description) } if len(result.Fields) != 4 { t.Fatalf("expected 4 fields, got %d", len(result.Fields)) } // Check value groups vgCount := make(map[string]int) for _, f := range result.Fields { if f.ValueGroup != "" { vgCount[f.ValueGroup]++ } } if vgCount["Client Name"] != 3 { t.Errorf("expected 3 fields in Client Name group, got %d", vgCount["Client Name"]) } if vgCount["Attorney Name"] != 1 { t.Errorf("expected 1 field in Attorney Name group, got %d", vgCount["Attorney Name"]) } // Check wizard pages wpCount := make(map[int]int) for _, f := range result.Fields { if f.WizardPage > 0 { wpCount[f.WizardPage]++ } } if wpCount[1] != 2 { t.Errorf("expected 2 fields on wizard page 1, got %d", wpCount[1]) } if wpCount[2] != 2 { t.Errorf("expected 2 fields on wizard page 2, got %d", wpCount[2]) } } func TestParseVisionResponseEmptyGroups(t *testing.T) { input := `{"description": "A form", "fields": [{"label": "F1", "question": "Q?", "value_group": "", "wizard_page": 1}]}` result, err := parseVisionResponse(input) if err != nil { t.Fatalf("parseVisionResponse: %v", err) } if result.Fields[0].ValueGroup != "" { t.Errorf("expected empty value_group, got %q", result.Fields[0].ValueGroup) } } func TestParseVisionResponseMissingOptional(t *testing.T) { input := `{"description": "A form", "fields": [{"label": "F1", "question": "Q?"}]}` result, err := parseVisionResponse(input) if err != nil { t.Fatalf("parseVisionResponse: %v", err) } if result.Fields[0].ValueGroup != "" { t.Errorf("expected empty value_group, got %q", result.Fields[0].ValueGroup) } if result.Fields[0].WizardPage != 0 { t.Errorf("expected 0 wizard_page, got %d", result.Fields[0].WizardPage) } } func TestGenerateQuestionsWithVisionNonExistent(t *testing.T) { c := NewClient("https://api.example.com", "fake-key", "fake-model") _, err := c.GenerateQuestionsWithVision("/nonexistent.pdf", nil) if err == nil { t.Error("expected error for non-existent file") } } func TestGroupFieldsByPage(t *testing.T) { fields := []models.FormField{ {Name: "F1", Page: 1}, {Name: "F2", Page: 1}, {Name: "F3", Page: 2}, {Name: "F4", Page: 0}, } m := groupFieldsByPage(fields) if len(m[1]) != 3 { t.Errorf("expected 3 fields on page 1, got %d", len(m[1])) } if len(m[2]) != 1 { t.Errorf("expected 1 field on page 2, got %d", len(m[2])) } } func TestBuildVisionFieldMeta(t *testing.T) { fields := []models.FormField{ {ID: 5, Name: "Test", Type: models.FieldText, Choices: []string{"A", "B"}, Page: 1}, } labelMap := map[int64]string{5: "a3f1b2c4"} meta := buildVisionFieldMeta(fields, labelMap) if len(meta) != 1 { t.Fatalf("expected 1 meta, got %d", len(meta)) } if meta[0].Label != "a3f1b2c4" { t.Errorf("expected label a3f1b2c4, got %s", meta[0].Label) } if !strings.Contains(strings.Join(meta[0].Choices, ","), "A") { t.Error("expected choices to contain A") } } func TestGenerateLabels(t *testing.T) { fields := []models.FormField{ {ID: 1, Name: "A"}, {ID: 2, Name: "B"}, {ID: 3, Name: "C"}, } labels := generateLabels(fields) if len(labels) != 3 { t.Fatalf("expected 3 labels, got %d", len(labels)) } // All labels should be non-empty and 8 hex chars for id, label := range labels { if label == "" { t.Errorf("empty label for field %d", id) } if len(label) != 8 { t.Errorf("expected 8-char label for field %d, got %d chars: %s", id, len(label), label) } } // Labels should be unique seen := make(map[string]bool) for _, label := range labels { if seen[label] { t.Errorf("duplicate label: %s", label) } seen[label] = true } } func TestRandHex4(t *testing.T) { // Generate several labels and verify they're valid hex for i := 0; i < 100; i++ { label := randHex4() if len(label) != 8 { t.Errorf("randHex4 returned %d chars: %s", len(label), label) } for _, c := range label { if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { t.Errorf("randHex4 returned non-hex char: %c in %s", c, label) } } } } func TestBuildReverseLabelMap(t *testing.T) { forward := map[int64]string{1: "a3f1", 2: "b7e2", 3: "c4d5"} reverse := buildReverseLabelMap(forward) if len(reverse) != 3 { t.Fatalf("expected 3 entries, got %d", len(reverse)) } if reverse["a3f1"] != 1 { t.Errorf("expected a3f1 -> 1, got %d", reverse["a3f1"]) } if reverse["b7e2"] != 2 { t.Errorf("expected b7e2 -> 2, got %d", reverse["b7e2"]) } }