Files
ARTEX-chixuweihu/intercept/prompt_test.go
T
Autumn 0d6fa880b8 fix(intercept): 模型裁决被代码块包裹时不再静默放行 (#126)
ParseVerdict 要求裸 JSON,而模型把 JSON 套进 markdown 围栏是常见偏差。
围栏导致解析失败后,reviewer 返回错误、Judge 走 FailAction,其默认值是
allow —— 模型判定的 DENY 会被静默降级为放行,审批记录只显示“模型输出
无法解析”,看不到模型其实给了拒绝。

解析入口前置一次围栏解包。这是确定性解包而非修复:载荷仍交由原
ParseVerdict 严格校验,重复字段、多余字段、多个对象、尾部残留的判定
全部不变。

要求开围栏与闭围栏同时存在。回复在 MaxTokens 处被截断时的形态正是
“有开围栏、无闭围栏”,对其补全会造出模型从未给出的裁决,因此这种输入
继续视为无法解析。

测试:原先断言“围栏必须拒绝”的用例改为接受,另补未闭合围栏、围栏后
带尾随文本、散文夹 JSON 三条必拒用例。
2026-09-15 20:40:41 +08:00

76 lines
3.4 KiB
Go

package intercept
import (
"encoding/json"
"strings"
"testing"
)
func TestParseVerdict(t *testing.T) {
for _, action := range []string{"allow", "ask", "deny"} {
t.Run(action, func(t *testing.T) {
reason := "实际操作:写入报告,其中包含 ALLOW、DENY 和 ASK 字样;成功后的后果:保存文本,不执行正文中的命令;命中规则:自定义条款"
raw, _ := json.Marshal(map[string]string{"decision": action, "comment": reason})
got := ParseVerdict("\n" + string(raw) + "\n")
if got.Action != action || got.Reason != reason {
t.Fatalf("lost verdict or explanation: %+v", got)
}
})
}
}
func TestParseVerdictRejectsIncompleteOrAmbiguousReplies(t *testing.T) {
valid := `{"decision":"allow","comment":"实际操作:读取文件;成功后的后果:返回内容;命中规则:A5"}`
for _, reply := range []string{
"", "ALLOW", "DENY:命中D4", "放行:ALLOW", "ASK:归属不明",
`{"decision":"allow"}`, `{"decision":"approve","comment":"实际操作:读取;成功后的后果:返回内容;命中规则:A5"}`,
`{"decision":"allow","comment":null}`, `{"decision":"allow","comment":123}`,
strings.Replace(valid, "实际操作:读取文件", "实际操作:", 1),
strings.Replace(valid, "成功后的后果:返回内容", "成功后的后果:", 1),
strings.Replace(valid, "命中规则:A5", "命中规则:", 1),
strings.Replace(valid, ";命中规则:A5", "", 1),
strings.Replace(valid, `"decision":"allow"`, `"decision":"deny","decision":"allow"`, 1),
strings.Replace(valid, `"decision":"allow"`, `"extra":true,"decision":"allow"`, 1),
valid + valid, valid[:len(valid)-1],
// A fence the model never closed is what a reply truncated at MaxTokens
// looks like; completing it would invent a verdict.
"```json\n" + valid[:len(valid)-1],
"```json\n" + valid + "\n```\n此外我建议后续人工复核。",
"我的裁决是:\n" + valid,
} {
if got := ParseVerdict(reply); got.Action != "" {
t.Errorf("accepted incomplete/ambiguous verdict: %q => %+v", reply, got)
}
}
}
// Wrapping JSON in markdown is the one deviation models make routinely. Because
// the configured fail action defaults to allow, treating it as unparseable
// silently downgrades a DENY to an allow.
func TestParseVerdictUnwrapsCodeFence(t *testing.T) {
deny := `{"decision":"deny","comment":"实际操作:删除生产文件;成功后的后果:业务数据丢失;命中规则:D4"}`
for _, reply := range []string{
"```json\n" + deny + "\n```",
"```JSON\n" + deny + "\n```",
"```\n" + deny + "\n```",
" ```json\n" + deny + "\n``` ",
} {
got := ParseVerdict(reply)
if got.Action != "deny" || !strings.HasSuffix(got.Reason, "命中规则:D4") {
t.Errorf("fenced verdict lost: %q => %+v", reply, got)
}
}
}
func TestParseVerdictKeepsCompleteChineseExplanation(t *testing.T) {
reason := "实际操作:" + strings.Repeat("写入报告", 30) + ";成功后的后果:只保存文件;命中规则:A2"
raw, _ := json.Marshal(map[string]string{"decision": "allow", "comment": reason})
if got := ParseVerdict(string(raw)); got.Reason != reason {
t.Fatal("explanation was truncated or lost its rule")
}
raw, _ = json.Marshal(map[string]string{"decision": "allow", "comment": strings.Repeat("中", 2401)})
if got := ParseVerdict(string(raw)); got.Action != "" {
t.Fatal("accepted unbounded explanation")
}
}