Files
mesh-catalog/modules/betterbird/cmd/betterbird-tools/config_test.go
T
jochen c4b4bac757
mesh/merge-gate head c4b4bac757c5 merged into trunk 0863ec2a059d — pass: builds betterbird, sent nowhere; no bus step; 1 wait(s) for a person; every machin…
mesh/repo-check head c4b4bac757c5 merged into trunk 0863ec2a059d — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
betterbird: never report the theme as fixed by clearing prefs.js, and never manage an account's name (hq issue 345, review 96)
Betterbird keeps the active theme in its add-on database, and extensions.activeThemeID only mirrors it.
So clearing the preference changed nothing, and the drift report then hid that. The theme is now read
from extensions.json against default-theme@mozilla.org. Another theme is drift, and its remedy is the
add-on manager: config_apply never clears it and keep refuses it.

A credential's, an account's or a server's name in the defaults file is refused by code: it is never
answered or cleared, and the check names it. Apply also refuses while the profile's lock is there, and
syncs prefs.js before the rename. A saved default of a built-in preference is no longer drift.
2026-10-12 21:09:25 +02:00

521 lines
22 KiB
Go

package main
import (
"archive/zip"
"encoding/json"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
// The configuration the module ships (novox/hq issue 345): the policy's locks, the module's defaults file, and the
// tools that show the layers, flag drift, clear it and propose a node layer.
// shippedDefaultsWant is the DEFAULT layer the operator approved on 2026-10-12: the laptop's setup, with
// reminders missed while asleep shown at the next start and chat off.
var shippedDefaultsWant = map[string]string{
"mail.uidensity": "0",
"mail.uifontsize": "14",
"mail.dark-reader.enabled": "false",
"mail.pane_config.dynamic": "2",
"mail.pane_config.multiline_all": "true",
"mail.threadpane.listview": "0",
"mailnews.default_view_flags": "1",
"mail.warn_on_shift_delete": "false",
"mail.biff.show_alert": "false",
"mail.biff.play_sound": "false",
"mail.biff.show_tray_icon": "false",
"mail.biff.use_system_alert": "true",
"calendar.view.daystarthour": "9",
"calendar.view.dayendhour": "20",
"calendar.item.promptDelete": "false",
"calendar.timezone.useSystemTimezone": "true",
"calendar.alarms.show": "true",
"calendar.alarms.showmissed": "true",
"mail.chat.enabled": "false",
"mailnews.message_display.disable_remote_image": "true",
"privacy.donottrackheader.enabled": "true",
}
var shippedKeepsWant = []string{"extensions.activeThemeID", "browser.display.use_system_colors",
"layout.css.always_underline_links", "mail.spam.manualMark", "mail.compose.show_attachment_pane"}
func repoFile(t *testing.T, rel string) []byte {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", rel))
if err != nil {
t.Fatal(err)
}
return raw
}
// withDefaults places the module's defaults file as the push does.
func (f *fake) withDefaults() {
f.t.Helper()
f.write(meshDefaults, string(repoFile(f.t, "install/defaults-pref/mesh-defaults.js")))
}
// omni places a packed application holding the given files.
func (f *fake) omni(files map[string]string) {
f.t.Helper()
full := filepath.Join(f.Root, omniJa)
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
f.t.Fatal(err)
}
out, err := os.Create(full)
if err != nil {
f.t.Fatal(err)
}
w := zip.NewWriter(out)
for name, body := range files {
e, _ := w.Create(name)
e.Write([]byte(body))
}
w.Close()
out.Close()
}
// betterbirdsOwn is a small omni.ja: two files that contradict each other, as all-thunderbird.js and
// mailnews.js do in the pinned release.
func (f *fake) betterbirdsOwn() {
f.omni(map[string]string{
"greprefs.js": `pref("gfx.webrender.software", false);` + "\n",
"defaults/pref/all-thunderbird.js": `pref("offline.autoDetect", true);` + "\n" +
`pref("mail.threadpane.listview", 1);` + "\n" + `pref("mail.uidensity", 1);` + "\n" +
`pref("mail.shell.checkDefaultClient", true);` + "\n",
"defaults/pref/mailnews.js": `pref("offline.autoDetect", false);` + "\n" +
`pref("mail.biff.show_alert", true); // a comment after it` + "\n" + `pref("mail.spam.manualMark", false);` + "\n" +
`pref("mailnews.start_page.enabled", true);` + "\n",
"defaults/pref/calendar.js": `pref("calendar.alarms.showmissed", false);` + "\n",
"chrome/other.js": `pref("not.a.default", 1);` + "\n",
})
}
func stopBetterbird(t *testing.T, f *fake) {
t.Helper()
for _, pid := range []string{"4000", "4001"} {
if err := os.RemoveAll(filepath.Join(f.Root, "proc", pid)); err != nil {
t.Fatal(err)
}
}
}
func TestTheModuleShipsTheApprovedLocksAndDefaults(t *testing.T) {
m, _ := readManifest(t)
if a := artifact(t, m, "defaults"); a["from"] != "install/defaults-pref" {
t.Fatalf("the defaults file ships through the defaults artifact: %v", a)
}
if r := resource(t, m, "spelling"); r["path"] != prefDir || r["artifact"] != "defaults" {
t.Fatalf("placed in Betterbird's own defaults directory: %v", r)
}
raw := repoFile(t, "install/distribution/policies.json")
if why := policyProblem(raw); why != "" {
t.Fatal(why)
}
var p struct {
Policies map[string]json.RawMessage `json:"policies"`
}
if err := json.Unmarshal(raw, &p); err != nil {
t.Fatal(err)
}
for k, want := range map[string]string{"DisableAppUpdate": "true", "DisableTelemetry": "true", "ExtensionUpdate": "false"} {
if string(p.Policies[k]) != want {
t.Errorf("%s is %s, not %s", k, p.Policies[k], want)
}
}
prefs := policyPrefsOf(raw)
for _, n := range []string{"mail.shell.checkDefaultClient", "mail.inappnotifications.enabled", "mailnews.start_page.enabled"} {
if prefs[n] != (policyPref{Value: "false", Locked: true}) {
t.Errorf("%s is locked off: %+v", n, prefs[n])
}
}
// Betterbird's Preferences policy accepts only these prefixes (Policies.sys.mjs); privacy. and toolkit. are not.
allowed := []string{"accessibility.", "app.update.", "browser.", "calendar.", "chat.", "datareporting.policy.", "dom.",
"extensions.", "general.autoScroll", "general.smoothScroll", "geo.", "gfx.", "intl.", "layers.", "layout.", "mail.",
"mailnews.", "media.", "network.", "pdfjs.", "places.", "print.", "signon.", "spellchecker.", "ui.", "widget."}
for n := range prefs {
ok := false
for _, a := range allowed {
ok = ok || strings.HasPrefix(n, a)
}
if !ok {
t.Errorf("the policy cannot lock %s: its prefix is not one Betterbird accepts", n)
}
}
text := string(repoFile(t, "install/defaults-pref/mesh-defaults.js"))
sh := parseShipped([]byte(text))
if !reflect.DeepEqual(sh.values, shippedDefaultsWant) {
t.Errorf("the defaults are the approved ones:\n got %v\nwant %v", sh.values, shippedDefaultsWant)
}
var keeps []string
for _, n := range sh.order {
if sh.keeps[n] {
keeps = append(keeps, n)
}
}
if !reflect.DeepEqual(keeps, shippedKeepsWant) {
t.Errorf("the preferences kept at Betterbird's default: %v", keeps)
}
lines := strings.Split(text, "\n")
for i, l := range lines {
if defaultLine.MatchString(l) && (i == 0 || !strings.HasPrefix(lines[i-1], "// ")) {
t.Errorf("line %d has no comment saying why: %s", i+1, l)
}
if t2 := strings.TrimSpace(l); t2 != "" && !strings.HasPrefix(t2, "//") && !defaultLine.MatchString(l) {
t.Errorf("line %d is neither a comment nor a pref(…) line: %s", i+1, l)
}
}
for n := range sh.values {
if withheld(n) != "" || passwordSecret.MatchString(n) {
t.Errorf("%s is an account's or a credential's preference, never a default of the module", n)
}
if _, locked := prefs[n]; locked {
t.Errorf("%s is both locked and a default", n)
}
}
}
// T2: Betterbird's own defaults are values read from omni.ja, in Gecko's order, and the module's beside them.
func TestPrefsSayTheDefaultAndTheFileThatGivesIt(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
f.withDefaults()
f.betterbirdsOwn()
links(t, nil)
got, err := f.Prefs([]string{"offline.autoDetect", "mail.threadpane.listview", "mail.uidensity", "gfx.webrender.software",
"mailnews.start_page.enabled", "layout.css.always_underline_links", "not.a.default"})
if err != nil {
t.Fatal(err)
}
by := map[string]Pref{}
for _, p := range got["prefs"].([]Pref) {
by[p.Name] = p
}
if p := by["offline.autoDetect"]; p.Value != "true" || p.Default != "true" || !strings.Contains(p.DefaultFrom, "all-thunderbird.js") {
t.Errorf("of two files, the one first by name holds, as Gecko reads them: %+v", p)
}
if p := by["mail.threadpane.listview"]; p.Value != "0" || !strings.Contains(p.From, "the module's default") {
t.Errorf("the module's default is over Betterbird's: %+v", p)
}
if p := by["gfx.webrender.software"]; p.Value != "false" || !strings.Contains(p.From, "greprefs.js") {
t.Errorf("%+v", p)
}
if p := by["mailnews.start_page.enabled"]; p.Value != "false" || p.From != "locked by the module's policy" || p.Default != "true" {
t.Errorf("a locked preference says its lock and Betterbird's default under it: %+v", p)
}
if p := by["layout.css.always_underline_links"]; p.Value != "" || !strings.Contains(p.From, "built-in") {
t.Errorf("a preference in no file is said as such: %+v", p)
}
if p := by["not.a.default"]; p.Default != "" {
t.Errorf("only defaults/pref and greprefs.js are defaults: %+v", p)
}
}
// T4: of mailnews.* only what names an account, a server or the operator's tags is withheld.
func TestMailnewsIsWithheldOnlyWhereItNamesAnAccount(t *testing.T) {
for name, held := range map[string]bool{
"mailnews.start_page.enabled": false,
"mailnews.default_view_flags": false,
"mailnews.message_display.disable_remote_image": false,
"mailnews.tags.$label1.tag": true,
"mailnews.smtp.loglevel": true,
"mailnews.reply_to_self_check_all_ident": true,
"mailnews.auto_config.fetchFromISP.sendEmailAddress": true,
"mailnews.oauth.useSchemeRedirect": true,
"mail.server.server1.hostname": true,
} {
if got := withheld(name) != ""; got != held {
t.Errorf("%s: withheld %v, want %v", name, got, held)
}
}
}
// T1: every managed preference through the layers, and drift where the profile's own value beats the module's
// default without a node layer saying so.
func TestTheConfigShowsTheLayersAndTheDrift(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
f.withDefaults()
f.betterbirdsOwn()
links(t, nil)
f.declare(`user_pref("mail.uifontsize", 16);`)
f.write(profileDir+"/prefs.js", "// Mozilla User Preferences\n"+
`user_pref("extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org");`+"\n"+
`user_pref("mail.uifontsize", 16);`+"\n"+
`user_pref("mail.uidensity", 0);`+"\n"+
`user_pref("mail.biff.show_alert", true);`+"\n"+
`user_pref("mail.spam.manualMark", false);`+"\n"+
`user_pref("mailnews.start_page.enabled", true);`+"\n")
c, err := f.Config(nil)
if err != nil {
t.Fatal(err)
}
by := map[string]ConfigPref{}
for _, p := range c.Prefs {
by[p.Name] = p
}
if len(c.Prefs) != 3+len(shippedDefaultsWant)+len(shippedKeepsWant) || !strings.HasPrefix(c.Defaults, "placed") {
t.Fatalf("every lock, default and kept default: %d, %s", len(c.Prefs), c.Defaults)
}
if !reflect.DeepEqual(c.Drifting, []string{"extensions.activeThemeID", "mail.biff.show_alert"}) {
t.Fatalf("drift: %v", c.Drifting)
}
if p := by["mail.biff.show_alert"]; p.Layer != "default" || p.ModuleDefault != "false" || p.BetterbirdDefault != "true" ||
p.Profile != "true" || p.Effective != "true" || p.From != "the profile's prefs.js" || !strings.Contains(p.Drift, "beats the module's false") {
t.Errorf("%+v", p)
}
if p := by["extensions.activeThemeID"]; p.Layer != "Betterbird's default" || !strings.Contains(p.Drift, `"default-theme@mozilla.org"`) ||
!strings.Contains(p.Remedy, "add-on manager") || p.Profile != `"thunderbird-compact-dark@mozilla.org"` {
t.Errorf("another theme is drift whose remedy is the add-on manager, its id answered whole: %+v", p)
}
if p := by["mail.spam.manualMark"]; p.Drift != "" || p.BetterbirdDefault != "false" {
t.Errorf("a saved value equal to Betterbird's default is not drift: %+v", p)
}
if p := by["mail.uifontsize"]; p.Drift != "" || p.NodeLayer != "16" {
t.Errorf("a node layer that says so is not drift: %+v", p)
}
if p := by["mailnews.start_page.enabled"]; p.Layer != "locked" || p.Effective != "false" || p.Drift != "" {
t.Errorf("a lock wins over the profile: %+v", p)
}
if p := by["calendar.alarms.showmissed"]; p.Effective != "true" || p.From != "the module's default" {
t.Errorf("%+v", p)
}
// T5: the check says the drift, with both remedies.
ch, err := f.Check()
if err != nil {
t.Fatal(err)
}
found := false
for _, x := range ch.Findings {
found = found || (strings.Contains(x.What, "1 managed preference(s) drift") && strings.Contains(x.What, "mail.biff.show_alert") &&
strings.Contains(x.Do, "betterbird_config_apply") && strings.Contains(x.Do, "keep"))
}
theme := false
for _, x := range ch.Findings {
theme = theme || (strings.HasPrefix(x.What, "extensions.activeThemeID drifts") && strings.Contains(x.Do, "add-on manager") &&
!strings.Contains(x.Do, "betterbird_config_apply"))
}
if !found || !theme {
t.Fatalf("the check reports the drift: %+v", ch.Findings)
}
os.Remove(filepath.Join(f.Root, meshDefaults))
ch, _ = f.Check()
found = false
for _, x := range ch.Findings {
found = found || strings.Contains(x.What, "the module's defaults are not placed")
}
if !found {
t.Fatalf("defaults missing on a node of the mesh's release is a finding: %+v", ch.Findings)
}
}
// T3: the drifting values are cleared from prefs.js only while Betterbird is not running, and nothing else.
func TestConfigApplyClearsTheDriftOnlyWhileBetterbirdIsQuit(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
f.withDefaults()
f.betterbirdsOwn()
links(t, nil)
f.declare("// none")
prefs := "// Mozilla User Preferences\n" +
`user_pref("extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org");` + "\n" +
`user_pref("mail.identity.id1.useremail", "someone@example.org");` + "\n" +
`user_pref("mail.biff.show_alert", true);` + "\n" +
`user_pref("mail.uidensity", 0);` + "\n"
f.write(profileDir+"/prefs.js", prefs)
dry, err := f.ConfigApply(false)
if err != nil || dry.Applied || len(dry.Clears) != 1 || len(dry.NotCleared) != 1 {
t.Fatalf("a dry run lists and changes nothing: %+v %v", dry, err)
}
if f.read(profileDir+"/prefs.js") != prefs {
t.Fatal("a dry run wrote prefs.js")
}
if _, err := f.ConfigApply(true); err == nil || !strings.Contains(err.Error(), "running") {
t.Fatalf("refused while Betterbird runs: %v", err)
}
if f.read(profileDir+"/prefs.js") != prefs {
t.Fatal("prefs.js changed while Betterbird runs")
}
stopBetterbird(t, f)
f.write(profileDir+"/lock", "")
if _, err := f.ConfigApply(true); err == nil || !strings.Contains(err.Error(), "locked") {
t.Fatalf("refused while the profile's own lock is there: %v", err)
}
if err := os.Remove(filepath.Join(f.Root, profileDir, "lock")); err != nil {
t.Fatal(err)
}
a, err := f.ConfigApply(true)
if err != nil || !a.Applied {
t.Fatalf("%+v %v", a, err)
}
want := "// Mozilla User Preferences\n" +
`user_pref("extensions.activeThemeID", "thunderbird-compact-dark@mozilla.org");` + "\n" +
`user_pref("mail.identity.id1.useremail", "someone@example.org");` + "\n" +
`user_pref("mail.uidensity", 0);` + "\n"
if got := f.read(profileDir + "/prefs.js"); got != want {
t.Fatalf("only the drifting lines go:\n%s", got)
}
said := map[string]Cleared{}
for _, c := range a.Clears {
said[c.Name] = c
}
if c := said["mail.biff.show_alert"]; c.Was != "true" || c.Now != "false" {
t.Errorf("it says what it cleared and what holds now: %+v", c)
}
if _, cleared := said["extensions.activeThemeID"]; cleared || len(a.NotCleared) != 1 || !strings.Contains(a.NotCleared[0], "add-on manager") {
t.Errorf("the theme is never cleared, and said with its remedy: %+v", a)
}
raw, _ := json.Marshal(a)
if strings.Contains(string(raw), "example.org") {
t.Fatalf("no account value leaves: %s", raw)
}
if c, _ := f.Config(nil); !reflect.DeepEqual(c.Drifting, []string{"extensions.activeThemeID"}) {
t.Fatalf("once applied, only the theme drifts, until the add-on manager changes it: %v", c.Drifting)
}
os.Remove(filepath.Join(f.Root, meshDefaults))
if _, err := f.ConfigApply(true); err == nil || !strings.Contains(err.Error(), "not placed") {
t.Fatalf("without the module's defaults there is nothing to come back to: %v", err)
}
}
// T6: keeping a value on one node is a node layer proposed through the controller, never a write to user.js.
func TestKeepProposesANodeLayerThroughTheController(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
f.withDefaults()
links(t, nil)
f.declare(`user_pref("calendar.alarms.playsound", false); user_pref("mail.uifontsize", 12);`)
f.write(profileDir+"/prefs.js", `user_pref("mail.uifontsize", 16);`+"\n"+`user_pref("mail.biff.show_alert", true);`+"\n")
oldNode, oldAsk := nodeName, asker
t.Cleanup(func() { nodeName, asker = oldNode, oldAsk })
nodeName = func() string { return "shanks" }
var verb string
var args map[string]any
asker = func(v string, a any) (json.RawMessage, error) {
verb, args = v, a.(map[string]any)
return json.RawMessage(`{"ok": true, "output": "proposed: p-1"}`), nil
}
before := f.read(testHome + "/.config/betterbird/user.js")
c, err := f.Config([]string{"mail.uifontsize", "mail.biff.show_alert"})
if err != nil {
t.Fatal(err)
}
if verb != "seat:mesh-controller.settings" || args["node"] != "shanks" || args["propose"] != "true" || args["module"] != "betterbird" {
t.Fatalf("%s %v", verb, args)
}
var values map[string]string
if err := json.Unmarshal([]byte(args["values"].(string)), &values); err != nil {
t.Fatal(err)
}
want := `user_pref("calendar.alarms.playsound", false); user_pref("mail.uifontsize", 16); user_pref("mail.biff.show_alert", true);`
if values["prefs"] != want {
t.Fatalf("the layer keeps what the setting says and the kept values:\n got %s\nwant %s", values["prefs"], want)
}
if c.Proposed != "proposed: p-1" || f.read(testHome+"/.config/betterbird/user.js") != before {
t.Fatalf("proposed, and user.js untouched: %+v", c)
}
for _, bad := range [][]string{{"mailnews.start_page.enabled"}, {"some.other.pref"}, {"mail.uidensity"}} {
verb = ""
if _, err := f.Config(bad); err == nil || verb != "" {
t.Errorf("%v: a lock, an unmanaged name or a value the profile did not save is refused: %v", bad, err)
}
}
nodeName = func() string { return "" }
if _, err := f.Config([]string{"mail.uifontsize"}); err == nil || !strings.Contains(err.Error(), "MESH_NODE") {
t.Fatalf("%v", err)
}
}
// The review of pull request 253: whether Betterbird asks to be the default client is decided from the policy's
// own Preferences entry, never from what else the policy lacks.
func TestTheDefaultClientAskIsDecidedFromThePreferencesEntry(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
links(t, nil)
f.write(policyFile, `{"policies": {"ExtensionSettings": {}}}`)
if why := f.defaultClientAsk(filepath.Join(f.Root, profileDir)); why == "" {
t.Fatal("a policy lacking the updater and the lock still asks")
}
f.write(policyFile, `{"policies": {"Preferences": {"mail.shell.checkDefaultClient": {"Value": false, "Status": "locked"}}}}`)
if why := f.defaultClientAsk(filepath.Join(f.Root, profileDir)); why != "" {
t.Fatalf("the lock holds whatever else the policy lacks: %s", why)
}
}
// The review of pull request 256: the theme is the add-on database's, and a saved default is not drift.
func TestTheThemeIsTheAddOnDatabasesAndASavedDefaultIsNotDrift(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
f.withDefaults()
links(t, nil)
f.write(profileDir+"/prefs.js", `user_pref("browser.display.use_system_colors", false);`+"\n"+
`user_pref("layout.css.always_underline_links", false);`+"\n")
f.write(profileDir+"/extensions.json", `{"addons":[{"id":"thunderbird-compact-dark@mozilla.org","type":"theme","active":true,"location":"app-builtin"},`+
`{"id":"default-theme@mozilla.org","type":"theme","active":false,"location":"app-builtin"}]}`)
c, err := f.Config(nil)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(c.Drifting, []string{"extensions.activeThemeID"}) {
t.Fatalf("the active theme from the add-on database, with no preference saved, is drift; a saved default is not: %v", c.Drifting)
}
f.write(profileDir+"/prefs.js", `user_pref("extensions.activeThemeID", "default-theme@mozilla.org");`+"\n")
f.write(profileDir+"/extensions.json", `{"addons":[{"id":"default-theme@mozilla.org","type":"theme","active":true,"location":"app-builtin"}]}`)
if c, _ := f.Config(nil); len(c.Drifting) != 0 {
t.Fatalf("a profile back on the default theme does not drift: %v", c.Drifting)
}
if _, err := f.Config([]string{"extensions.activeThemeID"}); err == nil || !strings.Contains(err.Error(), "add-on manager") {
t.Fatalf("a node layer cannot choose the theme: %v", err)
}
}
// The review of pull request 256: a credential's or an account's name in the defaults file is never managed.
func TestAnAccountsNameInTheDefaultsIsNeverManaged(t *testing.T) {
f := newFake(t)
f.betterbird()
f.placedByTheMesh()
links(t, nil)
f.write(meshDefaults, string(repoFile(t, "install/defaults-pref/mesh-defaults.js"))+
`pref("mail.identity.id1.useremail", "x");`+"\n"+"// keeps Betterbird's default: mail.server.server1.hostname\n"+
`pref("mail.smtpservers", "");`+"\n")
prefs := `user_pref("mail.identity.id1.useremail", "someone@example.org");` + "\n" +
`user_pref("mail.server.server1.hostname", "imap.example.org");` + "\n" +
`user_pref("mail.smtpservers", "smtp1");` + "\n"
f.write(profileDir+"/prefs.js", prefs)
c, err := f.Config(nil)
if err != nil {
t.Fatal(err)
}
if len(c.Refused) != 3 || len(c.Drifting) != 0 {
t.Fatalf("refused, never managed: %+v", c)
}
raw, _ := json.Marshal(c)
if strings.Contains(string(raw), "example.org") {
t.Fatalf("no account value is answered: %s", raw)
}
stopBetterbird(t, f)
if _, err := f.ConfigApply(true); err != nil {
t.Fatal(err)
}
if f.read(profileDir+"/prefs.js") != prefs {
t.Fatal("prefs.js keeps every account's value")
}
ch, _ := f.Check()
found := false
for _, x := range ch.Findings {
found = found || (strings.Contains(x.What, "3 preference(s) the tools never manage") && strings.Contains(x.Do, "mesh-defaults.js"))
}
if !found {
t.Fatalf("the check says so: %+v", ch.Findings)
}
}