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 := "Actual action: write a report containing the words ALLOW, DENY and ASK; Consequence if successful: saves the text, does not execute commands in the body; Matched rule: custom clause" 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":"Actual action: read file; Consequence if successful: returns content; Matched rule: A5"}` for _, reply := range []string{ "", "ALLOW", "DENY: hit D4", "allow: ALLOW", "ASK: ownership unclear", `{"decision":"allow"}`, `{"decision":"approve","comment":"Actual action: read; Consequence if successful: returns content; Matched rule: A5"}`, `{"decision":"allow","comment":null}`, `{"decision":"allow","comment":123}`, strings.Replace(valid, "Actual action: read file", "Actual action: ", 1), strings.Replace(valid, "Consequence if successful: returns content", "Consequence if successful: ", 1), strings.Replace(valid, "Matched rule: A5", "Matched rule: ", 1), strings.Replace(valid, "; Matched rule: 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```\nAlso, I recommend a follow-up manual review.", "My verdict is:\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":"Actual action: delete production files; Consequence if successful: business data is lost; Matched rule: 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, "Matched rule: D4") { t.Errorf("fenced verdict lost: %q => %+v", reply, got) } } } func TestParseVerdictKeepsCompleteExplanation(t *testing.T) { reason := "Actual action: " + strings.Repeat("write report ", 30) + "; Consequence if successful: only saves the file; Matched rule: 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("x", 2401)}) if got := ParseVerdict(string(raw)); got.Action != "" { t.Fatal("accepted unbounded explanation") } }