Both stand-in judges answered the fixed due date 2026-11-01; since 2026-11-02 00:00 UTC that date is past, signalEntries drops the mail, no tell is sent, and both tests time out on every check that rebuilds the module (met in the enrolment group's check of issue 774, 2026-11-02 02:50). Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
170 lines
7.0 KiB
Go
170 lines
7.0 KiB
Go
//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": []string{"telegram"}}, nil // dropped: the operator channel's own cap (1522/AC2)
|
|
}
|
|
return map[string]any{"sent_on": []string{"telegram"}, "dropped": []string{}}, 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": time.Now().UTC().AddDate(0, 0, 7).Format("2006-01-02")}
|
|
}
|
|
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)
|
|
}
|