//go:build integration package main // mesh-mail-reader started whole against the real bus and store (novox/hq ADR 0459, 0461, 0472; issue // 1431, design 59 tests 10, 11, 13 — this issue's own acceptance criteria note replaces its stale report, // which named the grant split ADR 0472 made won't-fix). Compiled by `go vet -tags integration ./...`; never // run on this machine — the lead's try runs it for real. import ( "context" "encoding/json" "fmt" "regexp" "strconv" "strings" "testing" "time" ) // mailIDPattern finds a mail's own id in a batch's question (question.go's mailHeaderBlock: "mail %d: // account ..."), so a stand-in judge can answer about exactly the mails it was actually asked, without // knowing their ids ahead of time (postgres assigns them). var mailIDPattern = regexp.MustCompile(`mail (\d+):`) func extractMailIDs(question string) []int64 { matches := mailIDPattern.FindAllStringSubmatch(question, -1) out := make([]int64, 0, len(matches)) for _, m := range matches { id, _ := strconv.ParseInt(m[1], 10, 64) out = append(out, id) } return out } // judgeEveryMailAs is a stand-in Judge that marks every mail of the batch's own question the same way — // enough for a test that cares about one outcome, not a per-mail distinction. func judgeEveryMailAs(mark, group, why, dueDate string) func(args map[string]any) (any, error) { return func(args map[string]any) (any, error) { question, _ := args["question"].(string) ids := extractMailIDs(question) mails := make([]map[string]any, len(ids)) for i, id := range ids { m := map[string]any{"id": id, "mark": mark, "summary": "s"} if group != "" { m["group"] = group } if why != "" { m["why"] = why } if dueDate != "" { m["due-date"] = dueDate } mails[i] = m } raw, _ := json.Marshal(map[string]any{"mails": mails}) return map[string]any{"answer": json.RawMessage(raw)}, nil } } func oneAccountAnswer(account string) func(args map[string]any) (any, error) { return func(map[string]any) (any, error) { return map[string]any{"accounts": []map[string]any{{"account": account, "kind": "imap", "address": account + "@example.org"}}}, nil } } // assertNeverSendsMovesFlagsOrDeletes is 1431/AC2: across the run, the reader never asks a verb that // changes the mailbox. Every ask this harness's stand-ins do not know already fails the test loudly // (readerPlayer.handleAsk); this is the belt the AC's own words ask for, on top of that suspenders. func assertNeverSendsMovesFlagsOrDeletes(t *testing.T, r *startedReader) { t.Helper() for _, k := range r.stand.Keys() { lower := strings.ToLower(k) for _, forbidden := range []string{"send", "move", "flag", "delete"} { if strings.Contains(lower, forbidden) { t.Fatalf("asked %q, which names %q — the reader must never change a mailbox", k, forbidden) } } } } // 1431/AC1 (design 59 test 10): a published added event gives a mail entry in a mail group, and a search // of every text column of the reader's own database for a known line of that mail's body finds nothing. func TestAcceptancePublishedMailBecomesAnEntryNeverKeepingItsBody(t *testing.T) { r := startReader(t) r.stand.MailAccounts = oneAccountAnswer("bob") knownLine := "the-known-body-line-xyz123-never-kept" r.stand.MailRead = func(map[string]any) (any, error) { return map[string]any{"text": "hello, " + knownLine + ", goodbye"}, nil } r.stand.Judge = judgeEveryMailAs("neither", "newsletters", "", "") r.stand.MessengerCheck = func(map[string]any) (any, error) { return map[string]any{"allowed": true}, nil } r.stand.Tell = func(map[string]any) (any, error) { return map[string]any{"sent_on": nil, "dropped": []string{"telegram"}}, nil } publishAdded(t, r.nc, "bob", "INBOX", 1, []map[string]any{{"name": "Alice", "address": "alice@example.org"}}, "hi", time.Now()) conn := pgConnect(t, r.dbURL) ctx := context.Background() waitFor(t, 30*time.Second, "the mail entry to appear", func() bool { var n int if err := conn.QueryRow(ctx, `select count(*) from mail_entry`).Scan(&n); err != nil { return false } return n == 1 }) var groupID *int64 if err := conn.QueryRow(ctx, `select mail_group from mail_entry limit 1`).Scan(&groupID); err != nil { t.Fatal(err) } if groupID == nil { t.Fatal("the entry carries no mail group") } // A search of every text-ish column of the whole database for the known body line finds nothing. rows, err := conn.Query(ctx, `select table_name, column_name from information_schema.columns where table_schema = 'public' and data_type in ('text', 'character varying')`) if err != nil { t.Fatal(err) } defer rows.Close() checked := 0 for rows.Next() { var table, column string if err := rows.Scan(&table, &column); err != nil { t.Fatal(err) } var found int q := fmt.Sprintf(`select count(*) from %q where %q like $1`, table, column) if err := conn.QueryRow(ctx, q, "%"+knownLine+"%").Scan(&found); err != nil { t.Fatal(err) } checked++ if found > 0 { t.Fatalf("the known body line was found in %s.%s", table, column) } } if checked == 0 { t.Fatal("no text column was checked at all") } assertNeverSendsMovesFlagsOrDeletes(t, r) } // 1431/AC3 (design 59 test 13, first half): a batch with one mail the stand-in judge marks as mattering // gives exactly one tell naming it, its words passing the content rule (messenger_check). func TestAcceptanceOneMatteringMailSendsExactlyOneTellNamingIt(t *testing.T) { r := startReader(t) r.stand.MailAccounts = oneAccountAnswer("bob") r.stand.MailRead = func(map[string]any) (any, error) { return map[string]any{"text": "pay by friday"}, nil } r.stand.Judge = judgeEveryMailAs("matters", "", "due friday", time.Now().UTC().AddDate(0, 0, 7).Format("2006-01-02")) var checked recorded[string] r.stand.MessengerCheck = func(args map[string]any) (any, error) { text, _ := args["text"].(string) checked.add(text) // The content rule's own shape (ADR 0461 §4): refuse words carrying a URL, an address or a host name. allowed := !strings.Contains(text, "://") && !strings.Contains(text, "example.com") return map[string]any{"allowed": allowed}, nil } var tells recorded[map[string]any] r.stand.Tell = func(args map[string]any) (any, error) { tells.add(args) return map[string]any{"sent_on": []string{"telegram"}, "dropped": []string{}}, nil } publishAdded(t, r.nc, "bob", "INBOX", 1, []map[string]any{{"name": "Alice", "address": "alice@example.org"}}, "invoice", time.Now()) waitFor(t, 30*time.Second, "exactly one tell", func() bool { return tells.len() >= 1 }) time.Sleep(2 * time.Second) // give a stray second tell a chance to prove this wrong, if it would happen sent := tells.snapshot() if len(sent) != 1 { t.Fatalf("tells = %d, want exactly 1", len(sent)) } if sent[0]["headline"] != "1 mail matters" { t.Fatalf("headline = %v, want '1 mail matters'", sent[0]["headline"]) } if sent[0]["subject"] != "mail" { t.Fatalf("subject = %v, want mail", sent[0]["subject"]) } lines, _ := sent[0]["lines"].([]any) named := false for _, l := range lines { if s, ok := l.(string); ok && strings.Contains(s, "Alice") { named = true } } if !named { t.Fatalf("lines %v do not name Alice", lines) } for _, text := range checked.snapshot() { if strings.Contains(text, "://") || strings.Contains(text, "example.com") { t.Fatalf("messenger_check was asked a word carrying a URL or host name: %q", text) } } assertNeverSendsMovesFlagsOrDeletes(t, r) } // 1431/AC3 (design 59 test 13, second half): a sender whose name carries a host name gives the safe form. func TestAcceptanceSenderNameWithAHostNameGivesTheSafeForm(t *testing.T) { r := startReader(t) r.stand.MailAccounts = oneAccountAnswer("bob") r.stand.MailRead = func(map[string]any) (any, error) { return map[string]any{"text": "hi"}, nil } r.stand.Judge = judgeEveryMailAs("matters", "", "", "") r.stand.MessengerCheck = func(args map[string]any) (any, error) { text, _ := args["text"].(string) return map[string]any{"allowed": !strings.Contains(text, "evil.example.com")}, nil } var tells recorded[map[string]any] r.stand.Tell = func(args map[string]any) (any, error) { tells.add(args) return map[string]any{"sent_on": []string{"telegram"}, "dropped": []string{}}, nil } publishAdded(t, r.nc, "bob", "INBOX", 1, []map[string]any{{"name": "mail.evil.example.com", "address": "x@example.org"}}, "hi", time.Now()) waitFor(t, 30*time.Second, "one tell", func() bool { return tells.len() >= 1 }) sent := tells.snapshot() lines, _ := sent[0]["lines"].([]any) if len(lines) != 1 || lines[0] != "open the mail page" { t.Fatalf("lines = %v, want the safe form (just 'open the mail page')", lines) } assertNeverSendsMovesFlagsOrDeletes(t, r) } // 1431/AC4: with the stand-in tell answering dropped (sent_on empty, the cap of 10 an hour), the mail stays // untold, and the next mail signal — after a second batch — counts it. func TestAcceptanceDroppedTellLeavesMailUntoldAndCountedNextTime(t *testing.T) { r := startReader(t) r.stand.MailAccounts = oneAccountAnswer("bob") r.stand.MailRead = func(map[string]any) (any, error) { return map[string]any{"text": "hi"}, nil } r.stand.Judge = judgeEveryMailAs("matters", "", "", "") r.stand.MessengerCheck = func(map[string]any) (any, error) { return map[string]any{"allowed": true}, nil } var tellCalls count r.stand.Tell = func(map[string]any) (any, error) { if tellCalls.inc() == 1 { return map[string]any{"sent_on": nil, "dropped": []string{"telegram"}}, nil // dropped: the operator channel's own cap } return map[string]any{"sent_on": []string{"telegram"}, "dropped": []string{}}, nil } publishAdded(t, r.nc, "bob", "INBOX", 1, []map[string]any{{"name": "Alice", "address": "alice@example.org"}}, "hi", time.Now()) waitFor(t, 30*time.Second, "the first (dropped) tell attempt", func() bool { return tellCalls.get() >= 1 }) conn := pgConnect(t, r.dbURL) ctx := context.Background() waitFor(t, 10*time.Second, "the entry to be written", func() bool { var n int _ = conn.QueryRow(ctx, `select count(*) from mail_entry`).Scan(&n) return n == 1 }) var told bool if err := conn.QueryRow(ctx, `select told from mail_entry limit 1`).Scan(&told); err != nil { t.Fatal(err) } if told { t.Fatal("the mail was marked told even though the tell was dropped") } // A second mail, and its own batch (at least BatchGap after the first started), triggers the next mail // signal — which counts the still-untold first mail too (1429/AC4, 1522/AC2). publishAdded(t, r.nc, "bob", "INBOX", 2, []map[string]any{{"name": "Bob", "address": "bob2@example.org"}}, "second", time.Now()) waitFor(t, BatchGap+60*time.Second, "a second tell attempt", func() bool { return tellCalls.get() >= 2 }) waitFor(t, 10*time.Second, "the first entry to become told", func() bool { var firstTold bool if err := conn.QueryRow(ctx, `select told from mail_entry order by id limit 1`).Scan(&firstTold); err != nil { return false } return firstTold }) assertNeverSendsMovesFlagsOrDeletes(t, r) } // 1926/AC4: a second batch over an unchanged set of mails that matter (its own mail does not matter) sends no // second signal — the first, answered as the real operator-channel answers it (sent_on a list), marked the set // told. func TestAcceptanceRepeatedBatchesOverAnUnchangedSetSendOneSignal(t *testing.T) { r := startReader(t) r.stand.MailAccounts = oneAccountAnswer("bob") r.stand.MailRead = func(map[string]any) (any, error) { return map[string]any{"text": "hi"}, nil } var judged count matters := judgeEveryMailAs("matters", "", "", "") neither := judgeEveryMailAs("neither", "", "", "") r.stand.Judge = func(args map[string]any) (any, error) { if judged.inc() == 1 { return matters(args) } return neither(args) } r.stand.MessengerCheck = func(map[string]any) (any, error) { return map[string]any{"allowed": true}, nil } var tells count r.stand.Tell = func(map[string]any) (any, error) { tells.inc() return map[string]any{"sent_on": []string{"telegram"}, "dropped": []string{}}, nil } publishAdded(t, r.nc, "bob", "INBOX", 1, []map[string]any{{"name": "Alice", "address": "alice@example.org"}}, "hi", time.Now()) waitFor(t, 30*time.Second, "the first tell", func() bool { return tells.get() >= 1 }) publishAdded(t, r.nc, "bob", "INBOX", 2, []map[string]any{{"name": "Bob", "address": "bob2@example.org"}}, "second", time.Now()) publishAdded(t, r.nc, "bob", "INBOX", 3, []map[string]any{{"name": "Carol", "address": "carol@example.org"}}, "third", time.Now()) waitFor(t, BatchGap+60*time.Second, "later batches judged", func() bool { return judged.get() >= 2 }) time.Sleep(2 * BatchGap) // a stray repeat of the signal would come after a batch if got := tells.get(); got != 1 { t.Fatalf("1926/AC4: tells = %d, want exactly 1 for an unchanged set", got) } assertNeverSendsMovesFlagsOrDeletes(t, r) }