Let rules carry a note
A glob like *4412* says nothing about why it exists or who it catches, and six months later neither does memory. Rules get an optional note: free text that never takes part in matching, written as a TOML key rather than a comment so it survives a round trip and can be shown back. The rule builder grows a fourth field for it and the rules screen a last column. Four fields spaced out are taller than a short window has room for, so the form now drops its blank lines and then the hints on unfocused fields before anything would run off the bottom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -196,6 +196,63 @@ func TestAppendRuleQuotesValues(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A note is documentation carried with the rule. It is a key rather than a
|
||||
// comment precisely so it survives the round trip.
|
||||
func TestAppendRuleWithNote(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
note := `the "corner shop" — 4412 is the branch code`
|
||||
if err := AppendRule(root, Rule{Match: "*4412*", Tag: "groceries", Note: note}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := LoadRules(root)
|
||||
if err != nil {
|
||||
t.Fatalf("the note broke the file: %v", err)
|
||||
}
|
||||
if loaded.Rule[0].Note != note {
|
||||
t.Errorf("note = %q, want %q", loaded.Rule[0].Note, note)
|
||||
}
|
||||
// A note alone is not a rule; the usual validation still applies.
|
||||
if err := AppendRule(root, Rule{Note: "just a thought"}); err == nil {
|
||||
t.Error("expected a rule with only a note to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
// Deleting a rule takes its note with it and leaves everyone else's alone.
|
||||
func TestDeleteRuleTakesItsNote(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
path := filepath.Join(root, RulesFile)
|
||||
original := `[[rule]]
|
||||
match = "*LIDL*"
|
||||
tag = "groceries"
|
||||
note = "the weekly shop"
|
||||
|
||||
[[rule]]
|
||||
match = "*ZARA*"
|
||||
tag = "clothes"
|
||||
note = "keep: for the January review"
|
||||
`
|
||||
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := DeleteRules(root, []int{0}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "weekly shop") {
|
||||
t.Errorf("rules.toml = %q, want the deleted rule's note gone with it", raw)
|
||||
}
|
||||
loaded, err := LoadRules(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(loaded.Rule) != 1 || loaded.Rule[0].Note != "keep: for the January review" {
|
||||
t.Errorf("rules = %+v, want the surviving rule to keep its note", loaded.Rule)
|
||||
}
|
||||
}
|
||||
|
||||
const rulesWithComments = `# Rules for my accounts.
|
||||
# Order matters: the first match wins.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user