135 lines
3.9 KiB
Go
135 lines
3.9 KiB
Go
package toolemulation
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestLooksLikeMissedToolUseDetectsLocalToolAvoidance(t *testing.T) {
|
|
cases := []string{
|
|
"我需要使用终端工具来查看内存。",
|
|
"由于当前环境限制,请手动运行 top。",
|
|
"当前环境限制,我无法直接执行系统命令查看你的内存占用。",
|
|
"你可以在终端中运行 top -l 1 | grep PhysMem。",
|
|
"I need to read the file first.",
|
|
"Let me use the web search tool.",
|
|
"You can run the following command in your terminal.",
|
|
"现在我需要切换到计划模式。",
|
|
}
|
|
for _, tc := range cases {
|
|
if !LooksLikeMissedToolUse(tc) {
|
|
t.Fatalf("LooksLikeMissedToolUse(%q) = false", tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeRefusalDetectsLocalAccessRefusals(t *testing.T) {
|
|
cases := []string{
|
|
"当前环境限制,我无法直接执行系统命令查看你的内存占用。",
|
|
"我无法访问你的电脑或本机文件。",
|
|
"I cannot execute commands in your local machine.",
|
|
"I can't access your computer directly.",
|
|
}
|
|
for _, tc := range cases {
|
|
if !LooksLikeRefusal(tc) {
|
|
t.Fatalf("LooksLikeRefusal(%q) = false", tc)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInferToolCallsFromTextConvertsMemoryRefusalToBash(t *testing.T) {
|
|
calls := InferToolCallsFromText("当前无法执行系统命令。你可以运行 vm_stat 查看内存占用。", []ToolDef{{
|
|
Name: "Bash",
|
|
InputSchema: map[string]any{
|
|
"properties": map[string]any{
|
|
"command": map[string]any{"type": "string"},
|
|
},
|
|
"required": []any{"command"},
|
|
},
|
|
}})
|
|
if len(calls) != 1 {
|
|
t.Fatalf("call count = %d", len(calls))
|
|
}
|
|
if calls[0].Name != "Bash" {
|
|
t.Fatalf("tool name = %q", calls[0].Name)
|
|
}
|
|
command, _ := calls[0].Arguments["command"].(string)
|
|
if !strings.Contains(command, "vm_stat") || !strings.Contains(command, "memory_pressure") {
|
|
t.Fatalf("unexpected command = %q", command)
|
|
}
|
|
}
|
|
|
|
func TestLooksLikeMissedToolUseIgnoresFinalAnswers(t *testing.T) {
|
|
text := "这个文件负责 HTTP API 路由和 OpenAI 兼容响应。"
|
|
if LooksLikeMissedToolUse(text) {
|
|
t.Fatalf("LooksLikeMissedToolUse(%q) = true", text)
|
|
}
|
|
}
|
|
|
|
func TestInjectToolingIncludesAutoToolGuidance(t *testing.T) {
|
|
prompt := InjectTooling("", []ToolDef{{
|
|
Name: "read_file",
|
|
Description: "Read a text file.",
|
|
InputSchema: map[string]any{
|
|
"properties": map[string]any{
|
|
"path": map[string]any{"type": "string"},
|
|
},
|
|
"required": []any{"path"},
|
|
},
|
|
}}, ToolChoice{Mode: "auto"}, nil)
|
|
if prompt == "" {
|
|
t.Fatal("empty prompt")
|
|
}
|
|
for _, want := range []string{
|
|
"tool_choice=auto means you must decide",
|
|
"inspect a local file path",
|
|
"Core tool examples",
|
|
"NEVER ask the user to run a command",
|
|
} {
|
|
if !strings.Contains(prompt, want) {
|
|
t.Fatalf("prompt missing %q:\n%s", want, prompt)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseActionBlocksMapsCommonToolAliases(t *testing.T) {
|
|
text := "```json action\n{\"tool\":\"Bash\",\"parameters\":{\"command\":\"pwd\",\"extra\":true}}\n```"
|
|
calls, clean, err := ParseActionBlocks(text, []ToolDef{{
|
|
Name: "terminal",
|
|
InputSchema: map[string]any{
|
|
"properties": map[string]any{
|
|
"command": map[string]any{"type": "string"},
|
|
},
|
|
},
|
|
}}, Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if clean != "" {
|
|
t.Fatalf("clean = %q", clean)
|
|
}
|
|
if len(calls) != 1 {
|
|
t.Fatalf("call count = %d", len(calls))
|
|
}
|
|
if calls[0].Name != "terminal" {
|
|
t.Fatalf("tool name = %q", calls[0].Name)
|
|
}
|
|
if _, ok := calls[0].Arguments["command"]; !ok {
|
|
t.Fatalf("missing command arg: %+v", calls[0].Arguments)
|
|
}
|
|
if _, ok := calls[0].Arguments["extra"]; ok {
|
|
t.Fatalf("unexpected extra arg: %+v", calls[0].Arguments)
|
|
}
|
|
}
|
|
|
|
func TestParseActionBlocksMapsReadAlias(t *testing.T) {
|
|
text := "```json action\n{\"name\":\"Read\",\"arguments\":{\"path\":\"/tmp/a.txt\"}}\n```"
|
|
calls, _, err := ParseActionBlocks(text, []ToolDef{{Name: "read_file"}}, Config{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(calls) != 1 || calls[0].Name != "read_file" {
|
|
t.Fatalf("calls = %+v", calls)
|
|
}
|
|
}
|