Remove manual tagging
A tag could come from two places: rules.toml, or the TUI's t key, which wrote manual_tag with COALESCE(manual_tag, rule_tag) deciding the winner. That split paid for itself in the first invariant of the codebase, in ClearOverrides and the c key, in the * marker on the tag column, and in the one exception to a disposable index -- a tag set by hand was the only thing in index.db that the statements could not reproduce. Now rules.toml decides every tag. The index is derived entirely from the statements plus that file, so deleting it and re-importing gets back exactly what was there, and retag has nothing to be careful of. Tagging a one-off means writing a narrow rule on screen 4, which previews what the glob catches before it is saved. A manual tag in an existing index is dropped along with the column the first time this build opens it, and those rows read as whatever the rules say, or as untagged. TestManualTagSurvivesRetag guarded the invariant that has just been removed; TestRetagRewritesEveryTag replaces it with the one that took its place, and keeps Retag itself covered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+15
-86
@@ -105,87 +105,6 @@ func cursorTo(t *testing.T, m *Model, desc string) {
|
||||
t.Fatalf("no visible transaction with description %q", desc)
|
||||
}
|
||||
|
||||
func TestTagFlow(t *testing.T) {
|
||||
m, db := newTestModel(t)
|
||||
|
||||
key(t, m, "2") // transactions view
|
||||
cursorTo(t, m, "SOME RANDOM SHOP")
|
||||
|
||||
key(t, m, "t") // open the tag prompt
|
||||
if m.input != inputTag {
|
||||
t.Fatal("expected the tag prompt to open")
|
||||
}
|
||||
typeText(t, m, "hobby")
|
||||
key(t, m, "enter")
|
||||
|
||||
if m.input != inputNone {
|
||||
t.Error("expected the prompt to close after enter")
|
||||
}
|
||||
|
||||
txns, err := db.Transactions(store.Filter{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var found bool
|
||||
for _, txn := range txns {
|
||||
if txn.Description == "SOME RANDOM SHOP" {
|
||||
found = true
|
||||
if txn.ManualTag != "hobby" {
|
||||
t.Errorf("manual_tag = %q, want %q", txn.ManualTag, "hobby")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("transaction disappeared after tagging")
|
||||
}
|
||||
|
||||
// The tag column marks manual overrides with a trailing asterisk.
|
||||
if !strings.Contains(m.View(), "hobby*") {
|
||||
t.Error("expected the tagged row to render with a manual-override marker")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagPromptCancel(t *testing.T) {
|
||||
m, db := newTestModel(t)
|
||||
key(t, m, "2")
|
||||
cursorTo(t, m, "SOME RANDOM SHOP")
|
||||
|
||||
key(t, m, "t")
|
||||
typeText(t, m, "nope")
|
||||
key(t, m, "esc")
|
||||
|
||||
if m.input != inputNone {
|
||||
t.Error("expected esc to close the prompt")
|
||||
}
|
||||
txns, _ := db.Transactions(store.Filter{Untagged: true})
|
||||
if len(txns) != 1 {
|
||||
t.Errorf("esc should not have tagged anything; untagged count = %d, want 1", len(txns))
|
||||
}
|
||||
}
|
||||
|
||||
// c drops a tag set by hand, restoring whatever the rules say.
|
||||
func TestClearManualTag(t *testing.T) {
|
||||
m, db := newTestModel(t)
|
||||
key(t, m, "2")
|
||||
cursorTo(t, m, "LIDL SOFIA")
|
||||
|
||||
key(t, m, "t")
|
||||
typeText(t, m, "treats")
|
||||
key(t, m, "enter")
|
||||
if got := findTxn(t, db, "LIDL SOFIA").ManualTag; got != "treats" {
|
||||
t.Fatalf("manual tag = %q, want treats", got)
|
||||
}
|
||||
|
||||
key(t, m, "c")
|
||||
txn := findTxn(t, db, "LIDL SOFIA")
|
||||
if txn.ManualTag != "" {
|
||||
t.Errorf("expected c to clear the manual tag, got %q", txn.ManualTag)
|
||||
}
|
||||
if txn.Tag() != "groceries" {
|
||||
t.Errorf("tag after clearing = %q, want groceries from the rule", txn.Tag())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUntaggedToggle(t *testing.T) {
|
||||
m, _ := newTestModel(t)
|
||||
|
||||
@@ -197,11 +116,21 @@ func TestUntaggedToggle(t *testing.T) {
|
||||
t.Fatalf("untagged view shows %+v, want only SOME RANDOM SHOP", m.txns)
|
||||
}
|
||||
|
||||
// Tagging the last untagged row empties the view; the cursor must stay valid.
|
||||
// A rule that claims the last untagged row empties the view; the cursor
|
||||
// must stay valid. Saving a rule is the only way to tag anything now.
|
||||
//
|
||||
// Saving reloads the rules from rules.toml, so the fixture's rule has to
|
||||
// exist there too or retagging would drop the tag it granted.
|
||||
if err := config.AppendRule(m.root, config.Rule{Match: "*LIDL*", Tag: "groceries"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cursorTo(t, m, "SOME RANDOM SHOP")
|
||||
key(t, m, "t")
|
||||
typeText(t, m, "hobby")
|
||||
key(t, m, "4")
|
||||
m.ruleGlob.SetValue("*RANDOM*")
|
||||
m.setRuleFocus(2)
|
||||
m.ruleTag.SetValue("hobby")
|
||||
key(t, m, "enter")
|
||||
key(t, m, "esc") // back to the transaction list, where u applies again
|
||||
if len(m.txns) != 0 {
|
||||
t.Errorf("expected the untagged view to be empty, got %d rows", len(m.txns))
|
||||
}
|
||||
@@ -572,7 +501,7 @@ func TestRuleBuilderSavesRuleAndRetags(t *testing.T) {
|
||||
}
|
||||
tagged := 0
|
||||
for _, txn := range txns {
|
||||
if txn.Tag() == "groceries" {
|
||||
if txn.RuleTag == "groceries" {
|
||||
tagged++
|
||||
}
|
||||
}
|
||||
@@ -1019,7 +948,7 @@ func TestRuleListPruneUnused(t *testing.T) {
|
||||
}
|
||||
tagged := 0
|
||||
for _, txn := range txns {
|
||||
if txn.Tag() == "groceries" {
|
||||
if txn.RuleTag == "groceries" {
|
||||
tagged++
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user