Files
money/internal/config/config_test.go
T
nikolaandClaude Opus 5 ebf7770569 Pair transfers from rules.toml
The boolean transfer flag went two commits ago because a one-sided verdict let
half a movement vanish and left the report unbalanced. This is what replaces
it: a [[transfer]] block names both legs, and only a matched pair is dropped
from the report -- both legs together, never one.

Legs pair within five days, nearest date first, and a transaction belongs to at
most one transfer, so the first definition to claim a leg keeps it, exactly as
the first matching rule keeps a tag. The pairing is derived state like the tags:
Engine.Link rewrites the whole transfers table from rules.toml, which is why
retag re-derives both halves of what that file decides, and why it runs over
the whole index rather than a filtered view -- pairing inside one would let a
movement count as a transfer in one report and not in another. An unmatched leg
is not a transfer and keeps counting, surfaced as a warning instead.

Within one currency the amount is the evidence and must be the exact opposite.
Across currencies it is not checked at all: there are no rates here, so the two
numbers are unrelated and the dates carry the pairing alone.

tolerance_pct is the one exception, per definition, for a route where the bank
takes a fee and the two statements genuinely disagree. It defaults to zero and
belongs on the one definition that charges; a global or default tolerance would
loosen every route that does not. The difference it admits is not forgiven --
the pair leaves the report entirely, so a fee hidden inside one would be
spending that appears nowhere. Pair.Fee is what left less what arrived, and
report.Excluded carries it out per currency alongside the legs. It counts only
pairs whose legs are both in view, for the same reason it counts legs and not
transfers: half a pair cannot say what the other half received.

The screens:

- 6 builds a definition against the index as you type, showing the pairs it
  would form and the legs it would catch but leave unpaired. Six fields need
  more room than the rule builder's four, so the form sheds its spacing, then
  its hints, then the borders on unfocused fields.
- 7 lists every definition with what it pairs. Two counts, because they mean
  different things: an unpaired leg is a definition doing something and not
  finishing it, no pairs at all is dead weight. Tol names the tolerance, blank
  where amounts must agree.
- 3 grows a (transfers) row under TOTAL, and a fees row beneath it, or the
  report silently disagrees with the account balances.

Two things that are not part of transfers but are the same day's work:

- ls --uniq lists each account and description once, normalised the way a glob
  sees them, which is the shape of "what still needs a rule?" -- fifty visits
  to one shop are one pattern to write, not fifty rows to read.
- The rule builder's preview now filters to what the glob matches instead of
  marking matches in a full list. The count carries the context the rows no
  longer can: 2 of 7, measured against everything still in view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 19:19:12 +02:00

676 lines
19 KiB
Go

package config
import (
"os"
"path/filepath"
"strings"
"testing"
)
// writeUserConfig points XDG_CONFIG_HOME at a temporary directory and puts a
// config file in it, so tests never touch the real one.
func writeUserConfig(t *testing.T, body string) string {
t.Helper()
dir := t.TempDir()
t.Setenv("XDG_CONFIG_HOME", dir)
if body == "" {
return dir
}
cfgDir := filepath.Join(dir, "money")
if err := os.MkdirAll(cfgDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(cfgDir, "config.toml"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
return dir
}
func TestResolveRootPrecedence(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory in this environment")
}
t.Run("flag beats everything", func(t *testing.T) {
writeUserConfig(t, "root = \"/from/config\"\n")
t.Setenv("MONEY_ROOT", "/from/env")
root, source, err := ResolveRoot("/from/flag")
if err != nil {
t.Fatal(err)
}
if root != "/from/flag" || source != RootFromFlag {
t.Errorf("got %q from %q, want /from/flag from the flag", root, source)
}
})
t.Run("env beats config", func(t *testing.T) {
writeUserConfig(t, "root = \"/from/config\"\n")
t.Setenv("MONEY_ROOT", "/from/env")
root, source, err := ResolveRoot("")
if err != nil {
t.Fatal(err)
}
if root != "/from/env" || source != RootFromEnv {
t.Errorf("got %q from %q, want /from/env from the environment", root, source)
}
})
t.Run("config beats the default", func(t *testing.T) {
writeUserConfig(t, "root = \"/from/config\"\n")
t.Setenv("MONEY_ROOT", "")
root, source, err := ResolveRoot("")
if err != nil {
t.Fatal(err)
}
if root != "/from/config" || source != RootFromConfig {
t.Errorf("got %q from %q, want /from/config from the config file", root, source)
}
})
t.Run("default when nothing is set", func(t *testing.T) {
writeUserConfig(t, "") // XDG dir exists but holds no config file
t.Setenv("MONEY_ROOT", "")
root, source, err := ResolveRoot("")
if err != nil {
t.Fatal(err)
}
want := filepath.Join(home, "money")
if root != want || source != RootFromDefault {
t.Errorf("got %q from %q, want %q from the default", root, source, want)
}
})
// A hand-written config file is likely to contain a tilde, which no shell
// has expanded for us.
t.Run("tilde in the config file is expanded", func(t *testing.T) {
writeUserConfig(t, "root = \"~/finances\"\n")
t.Setenv("MONEY_ROOT", "")
root, _, err := ResolveRoot("")
if err != nil {
t.Fatal(err)
}
want := filepath.Join(home, "finances")
if root != want {
t.Errorf("root = %q, want %q", root, want)
}
})
t.Run("relative paths become absolute", func(t *testing.T) {
writeUserConfig(t, "")
t.Setenv("MONEY_ROOT", "some/relative/dir")
root, _, err := ResolveRoot("")
if err != nil {
t.Fatal(err)
}
if !filepath.IsAbs(root) {
t.Errorf("root = %q, want an absolute path", root)
}
})
}
func TestAppendRule(t *testing.T) {
root := t.TempDir()
existing := "[[rule]]\ntag = \"salary\"\nmatch = \"*PAYROLL*\"\n"
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(existing), 0o644); err != nil {
t.Fatal(err)
}
if err := AppendRule(root, Rule{Match: "*LIDL*", Tag: "groceries", Account: "checking"}); err != nil {
t.Fatal(err)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatalf("the file no longer parses after appending: %v", err)
}
if len(loaded.Rule) != 2 {
t.Fatalf("got %d rules, want 2: %+v", len(loaded.Rule), loaded.Rule)
}
// Appending, not prepending: the existing rule keeps precedence.
if loaded.Rule[0].Tag != "salary" {
t.Errorf("first rule = %+v, want the pre-existing salary rule", loaded.Rule[0])
}
added := loaded.Rule[1]
if added.Match != "*LIDL*" || added.Tag != "groceries" || added.Account != "checking" {
t.Errorf("appended rule = %+v", added)
}
}
func TestAppendRuleCreatesFile(t *testing.T) {
root := t.TempDir()
if err := AppendRule(root, Rule{Match: "*LIDL*", Tag: "groceries"}); err != nil {
t.Fatal(err)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatal(err)
}
if len(loaded.Rule) != 1 || loaded.Rule[0].Match != "*LIDL*" {
t.Errorf("rules = %+v, want the one appended rule", loaded.Rule)
}
// An omitted account must not be written as an empty string.
if loaded.Rule[0].Account != "" {
t.Errorf("account = %q, want empty", loaded.Rule[0].Account)
}
}
func TestAppendRuleRejectsIncomplete(t *testing.T) {
root := t.TempDir()
cases := map[string]Rule{
"no pattern": {Tag: "groceries"},
"no verdict": {Match: "*LIDL*"},
}
for name, r := range cases {
if err := AppendRule(root, r); err == nil {
t.Errorf("%s: expected an error", name)
}
}
if _, err := os.Stat(filepath.Join(root, RulesFile)); !os.IsNotExist(err) {
t.Error("a rejected rule must not create the file")
}
}
// Globs and tags containing quotes must survive the round trip.
func TestAppendRuleQuotesValues(t *testing.T) {
root := t.TempDir()
if err := AppendRule(root, Rule{Match: `*"ODD" PAYEE*`, Tag: "misc"}); err != nil {
t.Fatal(err)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatalf("quoted values broke the file: %v", err)
}
if loaded.Rule[0].Match != `*"ODD" PAYEE*` {
t.Errorf("match = %q, want the quotes preserved", loaded.Rule[0].Match)
}
}
// 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.
# Weekly shop.
[[rule]]
match = "*LIDL*"
tag = "groceries"
[[rule]]
match = "*PAYROLL*"
tag = "salary"
# Moving money to myself.
[[rule]]
match = "*TO SAVINGS*"
tag = "transfer"
`
// Deleting must edit the file textually: a user's comments and layout are not
// recoverable from the parsed rules.
func TestDeleteRulesKeepsCommentsAndOrder(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, RulesFile)
if err := os.WriteFile(path, []byte(rulesWithComments), 0o644); err != nil {
t.Fatal(err)
}
n, err := DeleteRules(root, []int{1}) // the payroll rule, which has no comment
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Errorf("deleted %d rules, want 1", n)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatal(err)
}
if len(loaded.Rule) != 2 {
t.Fatalf("got %d rules, want 2: %+v", len(loaded.Rule), loaded.Rule)
}
if loaded.Rule[0].Match != "*LIDL*" || loaded.Rule[1].Match != "*TO SAVINGS*" {
t.Errorf("surviving rules = %+v, want the order preserved", loaded.Rule)
}
body, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
text := string(body)
for _, want := range []string{
"# Rules for my accounts.",
"# Order matters: the first match wins.",
"# Weekly shop.",
"# Moving money to myself.",
} {
if !strings.Contains(text, want) {
t.Errorf("comment %q was lost:\n%s", want, text)
}
}
if strings.Contains(text, "PAYROLL") {
t.Errorf("the deleted rule is still there:\n%s", text)
}
}
// A comment directly above a rule documents it and goes with it; a heading
// separated by a blank line does not.
func TestDeleteRulesTakesAttachedComment(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(rulesWithComments), 0o644); err != nil {
t.Fatal(err)
}
if _, err := DeleteRules(root, []int{0}); err != nil { // the commented LIDL rule
t.Fatal(err)
}
body, _ := os.ReadFile(filepath.Join(root, RulesFile))
text := string(body)
if strings.Contains(text, "# Weekly shop.") {
t.Errorf("the rule's own comment should have gone with it:\n%s", text)
}
if !strings.Contains(text, "# Rules for my accounts.") {
t.Errorf("the file heading must survive:\n%s", text)
}
}
func TestDeleteRulesMultiple(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(rulesWithComments), 0o644); err != nil {
t.Fatal(err)
}
n, err := DeleteRules(root, []int{0, 2})
if err != nil {
t.Fatal(err)
}
if n != 2 {
t.Errorf("deleted %d, want 2", n)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatal(err)
}
if len(loaded.Rule) != 1 || loaded.Rule[0].Match != "*PAYROLL*" {
t.Errorf("remaining = %+v, want only the payroll rule", loaded.Rule)
}
}
func TestDeleteRulesAll(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(rulesWithComments), 0o644); err != nil {
t.Fatal(err)
}
if _, err := DeleteRules(root, []int{0, 1, 2}); err != nil {
t.Fatal(err)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatalf("emptying the file must leave it loadable: %v", err)
}
if len(loaded.Rule) != 0 {
t.Errorf("got %+v, want no rules", loaded.Rule)
}
}
func TestDeleteRulesOutOfRange(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(rulesWithComments), 0o644); err != nil {
t.Fatal(err)
}
if _, err := DeleteRules(root, []int{7}); err == nil {
t.Error("expected an error for an out-of-range position")
}
loaded, _ := LoadRules(root)
if len(loaded.Rule) != 3 {
t.Error("a rejected delete must leave the file untouched")
}
}
// Deleting nothing is a no-op, not a rewrite.
func TestDeleteRulesEmptySelection(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, RulesFile)
if err := os.WriteFile(path, []byte(rulesWithComments), 0o644); err != nil {
t.Fatal(err)
}
if n, err := DeleteRules(root, nil); err != nil || n != 0 {
t.Fatalf("n=%d err=%v, want 0 and no error", n, err)
}
body, _ := os.ReadFile(path)
if string(body) != rulesWithComments {
t.Error("the file was rewritten despite deleting nothing")
}
}
func TestUserConfigPathFollowsXDG(t *testing.T) {
t.Setenv("XDG_CONFIG_HOME", "/custom/config")
path, err := UserConfigPath()
if err != nil {
t.Fatal(err)
}
if want := "/custom/config/money/config.toml"; path != want {
t.Errorf("path = %q, want %q", path, want)
}
// Unset, it falls back to ~/.config.
t.Setenv("XDG_CONFIG_HOME", "")
home, err := os.UserHomeDir()
if err != nil {
t.Skip("no home directory in this environment")
}
path, err = UserConfigPath()
if err != nil {
t.Fatal(err)
}
if want := filepath.Join(home, ".config", "money", "config.toml"); path != want {
t.Errorf("path = %q, want %q", path, want)
}
}
// A missing config file is normal, not an error.
func TestLoadUserConfigMissingFile(t *testing.T) {
writeUserConfig(t, "")
c, path, err := LoadUserConfig()
if err != nil {
t.Fatalf("a missing config file should not be an error: %v", err)
}
if c.Root != "" {
t.Errorf("root = %q, want empty", c.Root)
}
if path == "" {
t.Error("expected the path to be reported even when the file is absent")
}
}
// A malformed config file is an error, so a typo does not silently send the
// tool to the wrong directory.
func TestLoadUserConfigMalformed(t *testing.T) {
writeUserConfig(t, "root = this is not valid toml\n")
if _, _, err := LoadUserConfig(); err == nil {
t.Error("expected an error for a malformed config file")
}
}
func TestAppendTransfer(t *testing.T) {
root := t.TempDir()
existing := "[[rule]]\nmatch = \"*LIDL*\"\ntag = \"groceries\"\n"
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(existing), 0o644); err != nil {
t.Fatal(err)
}
tr := Transfer{
FromAccount: "nlb", FromDesc: "*TO REVOLUT*",
ToAccount: "revolut", ToDesc: "*FROM NLB*",
Note: `the monthly "top-up"`,
}
if err := AppendTransfer(root, tr); err != nil {
t.Fatal(err)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatalf("the file no longer parses after appending: %v", err)
}
if len(loaded.Rule) != 1 {
t.Errorf("got %d rules, want the existing one untouched", len(loaded.Rule))
}
if len(loaded.Transfer) != 1 || loaded.Transfer[0] != tr {
t.Errorf("transfers = %+v, want %+v", loaded.Transfer, tr)
}
}
// Both sides are required: a one-sided definition can never pair anything, so
// it is refused rather than written and silently ignored.
func TestTransferNeedsBothSides(t *testing.T) {
full := Transfer{FromAccount: "a", FromDesc: "*OUT*", ToAccount: "b", ToDesc: "*IN*"}
cases := map[string]func(*Transfer){
"no from_account": func(t *Transfer) { t.FromAccount = "" },
"no from_desc": func(t *Transfer) { t.FromDesc = "" },
"no to_account": func(t *Transfer) { t.ToAccount = "" },
"no to_desc": func(t *Transfer) { t.ToDesc = "" },
}
for name, break_ := range cases {
root := t.TempDir()
tr := full
break_(&tr)
if err := AppendTransfer(root, tr); err == nil {
t.Errorf("%s: expected an error", name)
}
if _, err := os.Stat(filepath.Join(root, RulesFile)); !os.IsNotExist(err) {
t.Errorf("%s: a rejected transfer must not create the file", name)
}
}
// The same check applies to a file written by hand.
root := t.TempDir()
body := "[[transfer]]\nfrom_account = \"nlb\"\nfrom_desc = \"*OUT*\"\nto_account = \"revolut\"\n"
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
if _, err := LoadRules(root); err == nil {
t.Error("expected a half-written transfer to be refused on load")
}
}
// Rules and transfers share a file, so deleting one kind must not take a
// neighbouring block of the other kind with it.
func TestDeleteLeavesTheOtherKindAlone(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, RulesFile)
original := `[[rule]]
match = "*LIDL*"
tag = "groceries"
# Moving money to the broker.
[[transfer]]
from_account = "nlb"
from_desc = "*TO TRADEREPUBLIC*"
to_account = "traderepublic"
to_desc = "*FROM NLB*"
[[rule]]
match = "*ZARA*"
tag = "clothes"
`
if err := os.WriteFile(path, []byte(original), 0o644); err != nil {
t.Fatal(err)
}
// Deleting the first rule must stop at the transfer that follows it.
if _, err := DeleteRules(root, []int{0}); err != nil {
t.Fatal(err)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatal(err)
}
if len(loaded.Rule) != 1 || loaded.Rule[0].Tag != "clothes" {
t.Errorf("rules = %+v, want only the clothes rule left", loaded.Rule)
}
if len(loaded.Transfer) != 1 {
t.Fatalf("transfers = %+v, want the transfer untouched", loaded.Transfer)
}
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), "# Moving money to the broker.") {
t.Errorf("rules.toml = %q, want the transfer's own comment kept", raw)
}
// And deleting the transfer leaves the remaining rule alone.
if _, err := DeleteTransfers(root, []int{0}); err != nil {
t.Fatal(err)
}
loaded, err = LoadRules(root)
if err != nil {
t.Fatal(err)
}
if len(loaded.Transfer) != 0 || len(loaded.Rule) != 1 {
t.Errorf("after deleting the transfer: %d rules, %d transfers; want 1 and 0",
len(loaded.Rule), len(loaded.Transfer))
}
if raw, err = os.ReadFile(path); err != nil {
t.Fatal(err)
}
if strings.Contains(string(raw), "Moving money to the broker") {
t.Errorf("rules.toml = %q, want the deleted transfer's comment gone with it", raw)
}
}
func TestDeleteTransfersOutOfRange(t *testing.T) {
root := t.TempDir()
if err := AppendTransfer(root, Transfer{
FromAccount: "a", FromDesc: "*OUT*", ToAccount: "b", ToDesc: "*IN*",
}); err != nil {
t.Fatal(err)
}
if _, err := DeleteTransfers(root, []int{3}); err == nil {
t.Error("expected an out-of-range position to be refused")
}
}
// A tolerance survives the round trip through the file, and a definition
// without one keeps meaning what it always meant: exact amounts.
func TestAppendTransferWithTolerance(t *testing.T) {
root := t.TempDir()
tr := Transfer{
FromAccount: "nlb", FromDesc: "*TO REVOLUT*",
ToAccount: "revolut", ToDesc: "*FROM NLB*",
TolerancePct: 1.5,
Note: "NLB takes a wire fee",
}
if err := AppendTransfer(root, tr); err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(filepath.Join(root, RulesFile))
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(body), "tolerance_pct = 1.5") {
t.Errorf("file = %q, want the tolerance written unquoted", body)
}
loaded, err := LoadRules(root)
if err != nil {
t.Fatal(err)
}
if len(loaded.Transfer) != 1 || loaded.Transfer[0] != tr {
t.Errorf("transfers = %+v, want %+v", loaded.Transfer, tr)
}
}
// The default is exact, so an unset tolerance must not be written out as a key
// suggesting the definition decided something.
func TestAppendTransferOmitsAZeroTolerance(t *testing.T) {
root := t.TempDir()
tr := Transfer{
FromAccount: "nlb", FromDesc: "*TO REVOLUT*",
ToAccount: "revolut", ToDesc: "*FROM NLB*",
}
if err := AppendTransfer(root, tr); err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(filepath.Join(root, RulesFile))
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), "tolerance_pct") {
t.Errorf("file = %q, want no tolerance key", body)
}
}
// A negative tolerance is a typo, and one at 100% or beyond would let any
// amount pair with any other, leaving the dates to decide alone.
func TestTransferToleranceIsBounded(t *testing.T) {
full := Transfer{FromAccount: "a", FromDesc: "*OUT*", ToAccount: "b", ToDesc: "*IN*"}
for _, pct := range []float64{-1, 100, 250} {
root := t.TempDir()
tr := full
tr.TolerancePct = pct
if err := AppendTransfer(root, tr); err == nil {
t.Errorf("tolerance_pct %g: expected an error", pct)
}
if _, err := os.Stat(filepath.Join(root, RulesFile)); !os.IsNotExist(err) {
t.Errorf("tolerance_pct %g: a rejected transfer must not create the file", pct)
}
}
// The same check applies to a file written by hand.
root := t.TempDir()
body := "[[transfer]]\nfrom_account = \"a\"\nfrom_desc = \"*OUT*\"\n" +
"to_account = \"b\"\nto_desc = \"*IN*\"\ntolerance_pct = 150\n"
if err := os.WriteFile(filepath.Join(root, RulesFile), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
if _, err := LoadRules(root); err == nil {
t.Error("expected an out-of-range tolerance to be refused on load")
}
}