Phase 6: rules editor + unsubscribe detection (AUD-72)

- FR-11 rules editor: plain-text area at /rules, saved and versioned
  (SaveRules appends a version; restore re-saves an old version), parse
  errors surfaced, one-click re-classify. The reply model now reads the
  rules document too (FR-11).
- FR-7 unsubscribe: parse and store List-Unsubscribe / List-Unsubscribe-Post
  (migration v6), detect email and web links (internal/unsubscribe), a
  domain-level ignore list, and an explicit record of what was marked. No
  code path contacts an unsubscribe link; acting on one needs Sam's approval.
- Migration v6 is additive; unsubscribe gains url/updated_at.
- Tests: detector, parser, store migration/records/candidates, rules editor,
  unsubscribe handlers, message-detail rendering.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-11 22:24:38 +11:00
parent 7c66e4ad31
commit 00bb0e05c4
24 changed files with 2121 additions and 42 deletions

View File

@@ -116,3 +116,16 @@ func TestGeneratorDraftEmptyReplyIsAnError(t *testing.T) {
t.Fatal("Draft() error = nil, want an error for an empty reply")
}
}
func TestBuildPromptIncludesRules(t *testing.T) {
req := Request{
Message: store.Message{FromAddr: "customer@example.test", Subject: "hi", BodyText: "?"},
Action: "reply",
Rules: `from contains "example.test" => category=Action`,
}
tmpl, _ := TemplateByID("answer")
_, user := BuildPrompt(req, tmpl)
if !strings.Contains(user, "USER RULES") || !strings.Contains(user, `from contains "example.test"`) {
t.Errorf("user prompt does not include the rules document:\n%s", user)
}
}