sub2api/backend/internal/service/content_moderation_input_test.go
wucm667 199a5bcc69 fix(risk-control): Agent 工具循环中同一用户消息重复审计去重
末尾 role 检查方案:当 messages / input / contents 数组末尾一项不是用户消息
(而是 assistant、tool / function_call_output 等)时,直接跳过内容审计,
从而避免 Agent 工具循环中同一用户输入被反复审计、计费、写日志。

Fixes #2678
2026-05-22 14:54:06 +08:00

180 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"testing"
"github.com/stretchr/testify/require"
)
// 当数组末尾不是用户消息时典型场景Agent 工具循环结束于 tool/assistant
// 应直接跳过审计——不再回溯查找历史中的某条用户消息。
func TestExtractContentModerationInput_AnthropicAgentToolLoopSkipsAudit(t *testing.T) {
body := []byte(`{
"messages": [
{"role":"user","content":"调用一下天气工具"},
{"role":"assistant","content":[{"type":"tool_use","id":"tool_1","name":"weather","input":{}}]},
{"role":"user","content":[{"type":"tool_result","tool_use_id":"tool_1","content":"晴 25 度"}]}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
require.Empty(t, input.Text)
require.Empty(t, input.Images)
}
func TestExtractContentModerationInput_AnthropicFirstTurnExtractsUser(t *testing.T) {
body := []byte(`{
"messages": [
{"role":"user","content":"Q1"}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
require.Equal(t, "Q1", input.Text)
}
func TestExtractContentModerationInput_AnthropicMultiTurnExtractsLatestUser(t *testing.T) {
body := []byte(`{
"messages": [
{"role":"user","content":"Q1"},
{"role":"assistant","content":"A1"},
{"role":"user","content":"Q2"}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
require.Equal(t, "Q2", input.Text)
}
func TestExtractContentModerationInput_AnthropicStreamResendExtractsResend(t *testing.T) {
body := []byte(`{
"messages": [
{"role":"user","content":"原问题"},
{"role":"assistant","content":"部分回答……"},
{"role":"user","content":"重发"}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolAnthropicMessages, body)
require.Equal(t, "重发", input.Text)
}
func TestExtractContentModerationInput_OpenAIChatAgentToolLoopSkipsAudit(t *testing.T) {
body := []byte(`{
"messages": [
{"role":"system","content":"sys"},
{"role":"user","content":"列出我的订单"},
{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"orders","arguments":"{}"}}]},
{"role":"tool","tool_call_id":"call_1","content":"[]"}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIChat, body)
require.Empty(t, input.Text)
require.Empty(t, input.Images)
}
func TestExtractContentModerationInput_OpenAIChatMultiTurnExtractsLatestUser(t *testing.T) {
body := []byte(`{
"messages": [
{"role":"user","content":"Q1"},
{"role":"assistant","content":"A1"},
{"role":"user","content":"Q2"}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIChat, body)
require.Equal(t, "Q2", input.Text)
}
func TestExtractContentModerationInput_GeminiAgentToolLoopSkipsAudit(t *testing.T) {
body := []byte(`{
"contents": [
{"role":"user","parts":[{"text":"查询天气"}]},
{"role":"model","parts":[{"functionCall":{"name":"weather","args":{}}}]},
{"role":"user","parts":[{"functionResponse":{"name":"weather","response":{"temp":25}}}]}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolGemini, body)
require.Empty(t, input.Text)
require.Empty(t, input.Images)
}
func TestExtractContentModerationInput_GeminiFirstTurnExtractsUser(t *testing.T) {
body := []byte(`{
"contents": [
{"role":"user","parts":[{"text":"你好"}]}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolGemini, body)
require.Equal(t, "你好", input.Text)
}
func TestExtractContentModerationInput_GeminiMultiTurnExtractsLatestUser(t *testing.T) {
body := []byte(`{
"contents": [
{"role":"user","parts":[{"text":"Q1"}]},
{"role":"model","parts":[{"text":"A1"}]},
{"role":"user","parts":[{"text":"Q2"}]}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolGemini, body)
require.Equal(t, "Q2", input.Text)
}
func TestExtractContentModerationInput_ResponsesAgentToolLoopSkipsAudit(t *testing.T) {
body := []byte(`{
"input":[
{"type":"message","role":"user","content":[{"type":"input_text","text":"运行测试"}]},
{"type":"function_call","call_id":"call_1","name":"run_tests","arguments":"{}"},
{"type":"function_call_output","call_id":"call_1","output":"all passed"}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIResponses, body)
require.Empty(t, input.Text)
require.Empty(t, input.Images)
}
func TestExtractContentModerationInput_ResponsesLastUserMessageExtracted(t *testing.T) {
body := []byte(`{
"input":[
{"type":"message","role":"user","content":[{"type":"input_text","text":"first"}]},
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"answer"}]},
{"type":"message","role":"user","content":[{"type":"input_text","text":"latest"}]}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIResponses, body)
require.Equal(t, "latest", input.Text)
}
func TestExtractContentModerationInput_ResponsesLastIsAssistantSkipped(t *testing.T) {
body := []byte(`{
"input":[
{"type":"message","role":"user","content":[{"type":"input_text","text":"q1"}]},
{"type":"message","role":"assistant","content":[{"type":"output_text","text":"a1"}]}
]
}`)
input := ExtractContentModerationInput(ContentModerationProtocolOpenAIResponses, body)
require.Empty(t, input.Text)
require.Empty(t, input.Images)
}