Files
Blizzard ca38dcd0c9 feat(tools): 新增 sql_query 只读 SQL 查询工具(agent 可见)
agent 可对数据库执行只读查询。三重防护:
- 静态校验:仅单条 SELECT/WITH,词边界拒 insert/update/delete/drop/alter/create/
  truncate/grant 等写/DDL 关键字(不误伤 created_at 之类列名)。
- 只读事务(sql.TxOptions{ReadOnly:true}):Postgres 引擎级强制只读,硬兜底。
- 行数(100)+超时(10s)+单元格(200 rune)上限。
连接:SQL_QUERY_DSN 优先(生产应指向专用只读库/账号),未设回退服务已解析的平台 PG DSN
(经 NewGateway 传入 g.pgDSN,不再裸读 env)。独立小连接池(8)。

测试:单测 validateReadOnlySQL(放行 SELECT/WITH/列名含 created;拒写/DDL/多语句)。
live 自主 agent 实测:查 sundynix_task=157 行、sundynix_model=2 行。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 17:07:34 +08:00

88 lines
2.2 KiB
Go

package mcp
import (
"math"
"strings"
"testing"
)
func TestEvalArith(t *testing.T) {
cases := map[string]float64{
"1+2*3": 7,
"(1+2)*3": 9,
"2^3^2": 512, // 右结合
"-3+5": 2,
"10/4": 2.5,
"10%3": 1,
"2*(3+4)-5": 9,
"-(2+3)*2": -10,
"3.5e1+0.5": 35.5,
}
for in, want := range cases {
got, err := evalArith(in)
if err != nil {
t.Errorf("evalArith(%q) 报错: %v", in, err)
continue
}
if math.Abs(got-want) > 1e-9 {
t.Errorf("evalArith(%q)=%v want %v", in, got, want)
}
}
}
func TestEvalArithErrors(t *testing.T) {
for _, in := range []string{"1/0", "1+", "(1+2", "1+2)", "rm -rf /", "import os", ""} {
if _, err := evalArith(in); err == nil {
t.Errorf("evalArith(%q) 应报错但没有", in)
}
}
}
func TestHTMLToText(t *testing.T) {
in := `<html><head><title>x</title></head><body><script>alert(1)</script>
<h1>标题</h1><p>第一段&amp;符号</p><style>.a{}</style><div>第二段</div></body></html>`
got := htmlToText(in)
if strings.Contains(got, "alert") || strings.Contains(got, ".a{}") {
t.Fatalf("脚本/样式未剔除: %q", got)
}
if !strings.Contains(got, "标题") || !strings.Contains(got, "第一段&符号") || !strings.Contains(got, "第二段") {
t.Fatalf("正文/实体提取不正确: %q", got)
}
}
func TestDDGUnwrap(t *testing.T) {
got := ddgUnwrap("//duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.com%2Fa&rut=x")
if got != "https://example.com/a" {
t.Fatalf("ddgUnwrap 解码错误: %q", got)
}
}
func TestValidateReadOnlySQL(t *testing.T) {
okCases := []string{
"SELECT 1",
"select count(*) from sundynix_task",
"WITH x AS (SELECT 1) SELECT * FROM x",
" select * from t where created_at > now() ", // created/update 作为列名一部分不应误伤
}
for _, q := range okCases {
if _, ok := validateReadOnlySQL(q); !ok {
t.Errorf("应通过: %q", q)
}
}
badCases := []string{
"insert into t values (1)",
"update t set a=1",
"delete from t",
"drop table t",
"select 1; drop table t",
"truncate t",
"SELECT 1; SELECT 2",
"grant all on t to x",
}
for _, q := range badCases {
if _, ok := validateReadOnlySQL(q); ok {
t.Errorf("应拒绝: %q", q)
}
}
}