Files
httprunner/uixt/ai/utils_test.go

705 lines
18 KiB
Go

package ai
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExtractJSONFromContent(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple JSON object",
input: `{"key": "value"}`,
expected: `{"key": "value"}`,
},
{
name: "JSON in markdown code block",
input: "```json\n{\"key\": \"value\"}\n```",
expected: `{"key": "value"}`,
},
{
name: "JSON in code block without language",
input: "```\n{\"key\": \"value\"}\n```",
expected: `{"key": "value"}`,
},
{
name: "JSON with surrounding text",
input: `Here is the result: {"key": "value"} and some more text`,
expected: `{"key": "value"}`,
},
{
name: "multiple JSON objects",
input: `{"first": "object"} and {"second": "object"}`,
expected: `{"first": "object"}`,
},
{
name: "nested JSON in markdown",
input: "```json\n{\"data\": {\"nested\": \"value\"}}\n```",
expected: `{"data": {"nested": "value"}}`,
},
{
name: "JSON array",
input: `[{"item": 1}, {"item": 2}]`,
expected: `[{"item": 1}, {"item": 2}]`,
},
{
name: "JSON array in markdown",
input: "```json\n[{\"item\": 1}, {\"item\": 2}]\n```",
expected: `[{"item": 1}, {"item": 2}]`,
},
{
name: "text without JSON",
input: "This is just plain text without any JSON",
expected: "",
},
{
name: "malformed JSON",
input: `{"key": "value"`,
expected: `{"key": "value"`,
},
{
name: "JSON with unicode",
input: `{"message": "测试消息"}`,
expected: `{"message": "测试消息"}`,
},
{
name: "multiple code blocks, select first JSON",
input: "First block:\n```json\n{\"first\": true}\n```\nSecond block:\n```json\n{\"second\": true}\n```",
expected: `{"first": true}`,
},
{
name: "mixed language code blocks",
input: "```python\nprint('hello')\n```\n```json\n{\"key\": \"value\"}\n```",
expected: `{"key": "value"}`,
},
{
name: "JSON with special characters",
input: `{"special": "chars: @#$%^&*()"}`,
expected: `{"special": "chars: @#$%^&*()"}`,
},
{
name: "empty JSON object",
input: `{}`,
expected: `{}`,
},
{
name: "empty JSON array",
input: `[]`,
expected: `[]`,
},
{
name: "JSON with line breaks",
input: "{\n \"key\": \"value\",\n \"number\": 123\n}",
expected: "{\n \"key\": \"value\",\n \"number\": 123\n}",
},
{
name: "markdown with extra whitespace",
input: " ```json \n {\"key\": \"value\"} \n ``` ",
expected: `{"key": "value"}`,
},
{
name: "code block with tildes",
input: "~~~json\n{\"key\": \"value\"}\n~~~",
expected: `{"key": "value"}`,
},
{
name: "JSON after other text patterns",
input: `The response should be formatted as: {"status": "success"}`,
expected: `{"status": "success"}`,
},
{
name: "JSON in mixed content",
input: `Analysis complete. Result: {"analysis": "positive", "confidence": 0.95} - End of analysis.`,
expected: `{"analysis": "positive", "confidence": 0.95}`,
},
{
name: "complex nested JSON",
input: `{"outer": {"inner": {"deep": "value", "numbers": [1, 2, 3]}}}`,
expected: `{"outer": {"inner": {"deep": "value", "numbers": [1, 2, 3]}}}`,
},
{
name: "JSON with escaped quotes",
input: `{"message": "He said \"Hello\" to me"}`,
expected: `{"message": "He said \"Hello\" to me"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractJSONFromContent(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestSanitizeUTF8Content(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "valid UTF-8",
input: "Hello 世界",
expected: "Hello 世界",
},
{
name: "invalid UTF-8 with replacement characters",
input: "Hello \ufffd\ufffd World",
expected: "Hello World",
},
{
name: "mixed valid and invalid",
input: "测试\ufffd消息\ufffd",
expected: "测试消息",
},
{
name: "only replacement characters",
input: "\ufffd\ufffd\ufffd",
expected: "",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "ASCII only",
input: "Hello World 123",
expected: "Hello World 123",
},
{
name: "JSON with UTF-8 issues",
input: `{"message": "搜索框\ufffd\ufffd显示"}`,
expected: `{"message": "搜索框显示"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := sanitizeUTF8Content(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseJSONWithFallback(t *testing.T) {
tests := []struct {
name string
input string
expectedValid bool
expectedPass bool
expectedThought string
}{
{
name: "valid JSON",
input: `{"pass": true, "thought": "test passed"}`,
expectedValid: true,
expectedPass: true,
expectedThought: "test passed",
},
{
name: "valid JSON with false",
input: `{"pass": false, "thought": "test failed"}`,
expectedValid: true,
expectedPass: false,
expectedThought: "test failed",
},
{
name: "malformed JSON with extractable fields",
input: `malformed start {"pass": true, "thought": "extracted"} end`,
expectedValid: true,
expectedPass: true,
expectedThought: "extracted",
},
{
name: "content analysis fallback - positive",
input: `The test was successful and passed with true result`,
expectedValid: true,
expectedPass: true,
expectedThought: "Fallback analysis of malformed response (positive: 3, negative: 0)",
},
{
name: "content analysis fallback - negative",
input: `The test failed with false result and error occurred`,
expectedValid: true,
expectedPass: false,
expectedThought: "Fallback analysis of malformed response (positive: 0, negative: 3)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result AssertionResult
err := parseJSONWithFallback(tt.input, &result)
if tt.expectedValid {
assert.NoError(t, err)
assert.Equal(t, tt.expectedPass, result.Pass)
assert.Equal(t, tt.expectedThought, result.Thought)
} else {
assert.Error(t, err)
}
})
}
}
func TestExtractAssertionFieldsManually(t *testing.T) {
tests := []struct {
name string
input string
expectedPass bool
expectedThought string
shouldError bool
}{
{
name: "pass true",
input: `{"pass": true, "thought": "manual test"}`,
expectedPass: true,
expectedThought: "manual test",
shouldError: false,
},
{
name: "pass false",
input: `{"pass": false, "thought": "manual fail"}`,
expectedPass: false,
expectedThought: "manual fail",
shouldError: false,
},
{
name: "no pass field",
input: `{"thought": "no pass field"}`,
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := extractAssertionFieldsManually(tt.input)
if tt.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedPass, result.Pass)
assert.Equal(t, tt.expectedThought, result.Thought)
}
})
}
}
func TestExtractQuotedString(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple quoted string",
input: `"hello world"`,
expected: "hello world",
},
{
name: "quoted string with escaped quotes",
input: `"He said \"Hello\""`,
expected: `He said "Hello"`,
},
{
name: "quoted string with escaped backslash",
input: `"path\\to\\file"`,
expected: `path\to\file`,
},
{
name: "empty quoted string",
input: `""`,
expected: "",
},
{
name: "quoted string with unicode",
input: `"测试消息"`,
expected: "测试消息",
},
{
name: "not a quoted string",
input: "hello world",
expected: "",
},
{
name: "unclosed quoted string",
input: `"unclosed string`,
expected: "unclosed string",
},
{
name: "quoted string with extra content after",
input: `"content" and more`,
expected: "content",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractQuotedString(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestCleanJSONContent(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "remove trailing comma in object",
input: `{"key": "value",}`,
expected: `{"key": "value"}`,
},
{
name: "remove trailing comma in array",
input: `["item1", "item2",]`,
expected: `["item1", "item2"]`,
},
{
name: "clean non-printable characters",
input: "{\n\"key\": \"value\"\u0000\u0001}",
expected: "{\n\"key\": \"value\"}",
},
{
name: "preserve unicode characters",
input: `{"message": "测试消息"}`,
expected: `{"message": "测试消息"}`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := cleanJSONContent(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
func TestAnalyzeContentForAssertion(t *testing.T) {
tests := []struct {
name string
input string
expectedPass bool
}{
{
name: "positive indicators",
input: "The test was successful and passed",
expectedPass: true,
},
{
name: "negative indicators",
input: "The test failed with error",
expectedPass: false,
},
{
name: "mixed with more positive",
input: "Some errors occurred but overall test passed successfully",
expectedPass: true,
},
{
name: "no clear indicators",
input: "This is just plain text",
expectedPass: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := analyzeContentForAssertion(tt.input)
assert.Equal(t, tt.expectedPass, result.Pass)
assert.NotEmpty(t, result.Thought)
})
}
}
func TestParseStructuredResponse(t *testing.T) {
tests := []struct {
name string
input string
shouldSucceed bool
}{
{
name: "valid AssertionResult JSON",
input: `{"pass": true, "thought": "test passed"}`,
shouldSucceed: true,
},
{
name: "malformed JSON with extractable fields",
input: `malformed start {"pass": false, "thought": "extracted thought"} end`,
shouldSucceed: true,
},
{
name: "UTF-8 issues with JSON",
input: "测试结果:\ufffd\ufffd {\"pass\": true, \"thought\": \"处理完成\"}",
shouldSucceed: true,
},
{
name: "content analysis fallback",
input: "The assertion was successful and passed correctly",
shouldSucceed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result AssertionResult
err := parseStructuredResponse(tt.input, &result)
if tt.shouldSucceed {
require.NoError(t, err)
assert.NotEmpty(t, result.Thought)
} else {
assert.Error(t, err)
}
})
}
}
// Add more test cases for different struct types
func TestParseJSONWithFallback_QueryResult(t *testing.T) {
tests := []struct {
name string
input string
expectedValid bool
expectedContent string
expectedThought string
}{
{
name: "valid QueryResult JSON",
input: `{"content": "extracted info", "thought": "analysis complete"}`,
expectedValid: true,
expectedContent: "extracted info",
expectedThought: "analysis complete",
},
{
name: "malformed QueryResult with extractable fields",
input: `malformed { "content": "partial info", "thought": "partial analysis" } more text`,
expectedValid: true,
expectedContent: "partial info",
expectedThought: "partial analysis",
},
{
name: "completely malformed QueryResult",
input: `This is just plain text with no structure`,
expectedValid: true,
expectedContent: "This is just plain text with no structure",
expectedThought: "Failed to parse as JSON, returning raw content",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result QueryResult
err := parseJSONWithFallback(tt.input, &result)
if tt.expectedValid {
assert.NoError(t, err)
assert.Equal(t, tt.expectedContent, result.Content)
assert.Equal(t, tt.expectedThought, result.Thought)
} else {
assert.Error(t, err)
}
})
}
}
func TestParseJSONWithFallback_PlanningResponse(t *testing.T) {
tests := []struct {
name string
input string
expectedValid bool
expectedThought string
expectedError string
expectedActions int
}{
{
name: "valid PlanningJSONResponse",
input: `{"actions": [{"action_type": "click"}], "thought": "planning complete", "error": ""}`,
expectedValid: true,
expectedThought: "planning complete",
expectedError: "",
expectedActions: 1,
},
{
name: "malformed PlanningResponse with extractable thought",
input: `malformed { "thought": "partial planning" } more text`,
expectedValid: true,
expectedThought: "partial planning",
expectedActions: 0,
},
{
name: "completely malformed PlanningResponse",
input: `This is just plain text with no structure`,
expectedValid: true,
expectedThought: "Failed to parse structured response",
expectedError: "JSON parsing failed, returning minimal structure",
expectedActions: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result PlanningJSONResponse
err := parseJSONWithFallback(tt.input, &result)
if tt.expectedValid {
assert.NoError(t, err)
assert.Equal(t, tt.expectedThought, result.Thought)
assert.Equal(t, tt.expectedError, result.Error)
assert.Len(t, result.Actions, tt.expectedActions)
} else {
assert.Error(t, err)
}
})
}
}
func TestExtractQueryFieldsManually(t *testing.T) {
tests := []struct {
name string
input string
expectedContent string
expectedThought string
shouldError bool
}{
{
name: "both content and thought",
input: `{"content": "test content", "thought": "test thought"}`,
expectedContent: "test content",
expectedThought: "test thought",
shouldError: false,
},
{
name: "only content",
input: `{"content": "only content"}`,
expectedContent: "only content",
expectedThought: "Partial extraction from malformed response",
shouldError: false,
},
{
name: "only thought",
input: `{"thought": "only thought"}`,
expectedContent: "Extracted partial information",
expectedThought: "only thought",
shouldError: false,
},
{
name: "no extractable fields",
input: `{"other": "data"}`,
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := extractQueryFieldsManually(tt.input)
if tt.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedContent, result.Content)
assert.Equal(t, tt.expectedThought, result.Thought)
}
})
}
}
func TestExtractPlanningFieldsManually(t *testing.T) {
tests := []struct {
name string
input string
expectedThought string
expectedError string
shouldError bool
}{
{
name: "both thought and error",
input: `{"thought": "test planning", "error": "test error"}`,
expectedThought: "test planning",
expectedError: "test error",
shouldError: false,
},
{
name: "only thought",
input: `{"thought": "only planning"}`,
expectedThought: "only planning",
expectedError: "",
shouldError: false,
},
{
name: "only error",
input: `{"error": "only error"}`,
expectedThought: "Partial extraction from malformed response",
expectedError: "only error",
shouldError: false,
},
{
name: "no extractable fields",
input: `{"other": "data"}`,
shouldError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := extractPlanningFieldsManually(tt.input)
if tt.shouldError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectedThought, result.Thought)
assert.Equal(t, tt.expectedError, result.Error)
assert.NotNil(t, result.Actions) // Should always be initialized
}
})
}
}
// Test the integrated parseStructuredResponse with QueryResult
func TestParseStructuredResponse_QueryResult(t *testing.T) {
tests := []struct {
name string
input string
shouldSucceed bool
}{
{
name: "valid QueryResult JSON",
input: `{"content": "extracted data", "thought": "processing complete"}`,
shouldSucceed: true,
},
{
name: "QueryResult with UTF-8 issues",
input: "extracted data: 搜索框,里面显示着\ufffd\ufffd {\"content\": \"search box found\", \"thought\": \"visual analysis\"}",
shouldSucceed: true,
},
{
name: "malformed QueryResult",
input: `malformed start {"content": "partial info"} end`,
shouldSucceed: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var result QueryResult
err := parseStructuredResponse(tt.input, &result)
if tt.shouldSucceed {
require.NoError(t, err)
assert.NotEmpty(t, result.Content, "Content should not be empty")
assert.NotEmpty(t, result.Thought, "Thought should not be empty")
} else {
assert.Error(t, err)
}
})
}
}