//go:build integration package main // Design 59 test 16's reader half (novox/hq ADR 0459 §12, ADR 0461 §6; issue 1524, split from 1431): // a refused judgement makes mesh-mail-reader unhealthy, with its cause and since when, and sends no tell; // once the judge answers again, the mail heard meanwhile is read in the order it was published, and the // reader answers healthy again. Reuses 1431's own harness (runtime_player_integration_test.go). Compiled // by `go vet -tags integration ./...`; never run on this machine — the lead's try runs it for real. import ( "context" "encoding/json" "errors" "fmt" "regexp" "strings" "sync" "testing" "time" ) // sinceRE matches the "(since HH:MM YYYY-MM-DD)" health.go's own Answer embeds in why (review round 1 // finding 6): proves since when is actually named, not merely that the cause's own word ("licence") is. var sinceRE = regexp.MustCompile(`since \d{2}:\d{2} \d{4}-\d{2}-\d{2}`) // healthAnswer reads mail_reader_health through the player, as a plain map (the tool carries no caller // check, ADR 0459 §12). func healthAnswer(t *testing.T, r *startedReader) map[string]any { t.Helper() text, isError, err := r.player.ToolsCall("mail_reader_health", map[string]any{}) if err != nil { t.Fatal(err) } if isError { t.Fatalf("mail_reader_health answered an error: %s", text) } var out map[string]any if err := json.Unmarshal([]byte(text), &out); err != nil { t.Fatalf("mail_reader_health's answer could not be read: %s", text) } return out } // 1524/AC1, AC2 (review round 1 finding 6): a mattering mail judged and told is answered FIRST, its own // tell dropped (so it is genuinely untold, the one thing that would make a later batch try the mail // signal again) — then a refused judgement ("holds no licence", as claude-code's own judge.go answers when // the licence binding is removed) makes the reader unhealthy with that cause and since when, and sends no // tell for that still-untold mail; once the judge answers again, both mails (the second heard while // unhealthy) are read in the order they were published, and the reader answers healthy again. The earlier // version of this test asserted "no tell while unhealthy" with no mattering mail ever judged at all, so // SendSignal had nothing to try telling regardless of health — proving nothing. func TestAcceptanceRefusedJudgementIsUnhealthyThenCatchesUpInPublishedOrder(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.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: the operator channel's own cap (1522/AC2) } return map[string]any{"sent_on": time.Now().UTC().Format(time.RFC3339)}, nil } var mu sync.Mutex refusing := false var judgedOrder recorded[int64] r.stand.Judge = func(args map[string]any) (any, error) { mu.Lock() stillRefusing := refusing mu.Unlock() if stillRefusing { // claude-code's own judge.go answers exactly this when the licence binding is removed // (judge.go:148-151); it is an error from the ask itself, never a pending/ticket shape. return nil, errors.New("holds no licence") } question, _ := args["question"].(string) ids := extractMailIDs(question) for _, id := range ids { judgedOrder.add(id) } mails := make([]map[string]any, len(ids)) for i, id := range ids { mails[i] = map[string]any{"id": id, "mark": "matters", "summary": "s", "why": "pay", "due-date": "2026-11-01"} } raw, _ := json.Marshal(map[string]any{"mails": mails}) return map[string]any{"answer": json.RawMessage(raw)}, nil } conn := pgConnect(t, r.dbURL) ctx := context.Background() // A mattering mail, judged successfully, its own tell dropped — so it stays genuinely untold. publishAdded(t, r.nc, "bob", "INBOX", 1, []map[string]any{{"name": "Alice", "address": "alice@example.org"}}, "first", time.Now()) waitFor(t, 30*time.Second, "the first (dropped) tell attempt", func() bool { return tellCalls.get() >= 1 }) waitFor(t, 10*time.Second, "the first 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 its own tell was dropped") } // From here, every judgement is refused (1524/AC1) — set before the second mail is even heard, so its // own batch, whenever it runs, is refused. mu.Lock() refusing = true mu.Unlock() publishAdded(t, r.nc, "bob", "INBOX", 2, []map[string]any{{"name": "Bob", "address": "bob2@example.org"}}, "second", time.Now().Add(time.Second)) // 1524/AC1: unhealthy with cause and since when, no tell for the mail that is already untold. waitFor(t, 30*time.Second, "mail_reader_health to answer unhealthy", func() bool { h := healthAnswer(t, r) healthy, _ := h["healthy"].(bool) return !healthy }) h := healthAnswer(t, r) why := fmt.Sprint(h["why"]) if !strings.Contains(why, "licence") { t.Fatalf("health = %v, want why naming the licence", h) } if !sinceRE.MatchString(why) { t.Fatalf("health's why = %q, want it to name since when (HH:MM YYYY-MM-DD, health.go's own Answer)", why) } // Exactly 1 (the dropped tell of the healthy phase, above) — never a relative "unchanged since some // point after unhealthy was already seen", which a tell sent in the SAME loop pass that made the // reader unhealthy would already be baked into (review round 2, issue 1524): waitFor's own poll above // only reads health, so a wrongly-sent tell from that very batch would predate this read and hide // behind it. time.Sleep(2 * time.Second) // give the next failed batch attempt, if any, time to run and fail again if n := tellCalls.get(); n != 1 { t.Fatalf("tellCalls = %d, want exactly 1 (the dropped tell from the healthy phase; none while unhealthy)", n) } // The judge answers again (1524/AC2): catch-up in the order the two mails were published. mu.Lock() refusing = false mu.Unlock() waitFor(t, BatchGap+60*time.Second, "both entries written", func() bool { var n int if err := conn.QueryRow(ctx, `select count(*) from mail_entry`).Scan(&n); err != nil { return false } return n == 2 }) order := judgedOrder.snapshot() if len(order) < 2 { t.Fatalf("judged ids = %v, want at least 2 (both mails, in one batch or more)", order) } for i := 1; i < len(order); i++ { if order[i] <= order[i-1] { t.Fatalf("judged out of arrival order: %v", order) } } waitFor(t, 10*time.Second, "healthy again", func() bool { healthy, _ := healthAnswer(t, r)["healthy"].(bool) return healthy }) assertNeverSendsMovesFlagsOrDeletes(t, r) }