diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/list.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/list.go new file mode 100644 index 00000000..28a3335b --- /dev/null +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/list.go @@ -0,0 +1,205 @@ +package main + +// The operator's list (novox/hq ADR 0515 §1, ADR 0517 §1, §2, §4; issue 1858): a pure function over the +// entries, the handlings and the day, answered by list. Days are the operator's (days.go, Europe/Brussels). +// +// - A task falls under today when it is due today or before, this week when due by the end of Sunday, and +// later when due after that or not at all. +// - A promise falls under its kind: you promised, waiting on others, on its way. +// - A reminded item is hidden until the day it shows again, and that day it is under today; the mail +// itself reminded hides all its items, and shows them, or itself when it has none, under today that day. +// - A handled item is gone from its section; handled today, it is under handled today with its undo. The +// mail itself handled takes all its items with it. +// - Good to know holds the entries with no task or promise, not read later, not a scam warning and not +// handled; read later the read-later ones; a scam warning is in its own place and nowhere else. + +import ( + "sort" + "strconv" + "time" +) + +// DefaultListLimit bounds each section when list is not asked another limit. +const DefaultListLimit = 50 + +// ListItem is one line of the list: an item of an entry, with what the page shows of its mail. +type ListItem struct { + Entry Entry + Item string // ItemMail, "task n" or "promise n" + Kind string // "task", "promise" or "mail" + Line string + Due time.Time // a task's due date or a promise's date; zero for none + Promise PromiseKind + Who string + Reminded bool // shown under today because it was reminded + Handled time.Time // for handled today +} + +// OperatorList is the list for one mode. +type OperatorList struct { + Today, ThisWeek, Later []ListItem + YouPromised, WaitingOnOthers, OnItsWay []ListItem + GoodToKnow, ReadLater, ScamWarnings []ListItem // mail items + HandledToday []ListItem +} + +// BuildList builds the list of the entries of accounts (every account when accounts is empty) for the day of +// now. +func BuildList(entries []Entry, handlings []Handling, accounts []string, now time.Time) OperatorList { + today := dayOf(now) + sunday := endOfWeek(today) + byItem := map[int64]map[string]Handling{} + for _, h := range handlings { + if byItem[h.Entry] == nil { + byItem[h.Entry] = map[string]Handling{} + } + byItem[h.Entry][h.Item] = h + } + wanted := map[string]bool{} + for _, a := range accounts { + wanted[a] = true + } + var l OperatorList + for _, e := range entries { + if len(wanted) > 0 && !wanted[e.Account] { + continue + } + hs := byItem[e.ID] + mail := hs[ItemMail] + mailItem := ListItem{Entry: e, Item: ItemMail, Kind: "mail", Line: e.Summary} + if !mail.Handled.IsZero() { + if dayOf(mail.Handled).Equal(today) { + mailItem.Handled = mail.Handled + l.HandledToday = append(l.HandledToday, mailItem) + } + continue // the mail itself handled takes all its items with it + } + if !mail.ShowsAgain.IsZero() && mail.ShowsAgain.After(today) { + continue // the mail itself reminded: hidden, all of it, until its day + } + mailToday := mail.ShowsAgain.Equal(today) + if e.Mark == MarkScamWarning { + l.ScamWarnings = append(l.ScamWarnings, mailItem) + continue + } + items := itemsOf(e) + for _, it := range items { + h := hs[it.Item] + switch { + case !h.Handled.IsZero(): + if dayOf(h.Handled).Equal(today) { + it.Handled = h.Handled + l.HandledToday = append(l.HandledToday, it) + } + case !h.ShowsAgain.IsZero() && h.ShowsAgain.After(today): + // reminded: hidden until its day + case h.ShowsAgain.Equal(today) || mailToday: + it.Reminded = true + l.Today = append(l.Today, it) + case it.Kind == "task": + switch { + case !it.Due.IsZero() && !it.Due.After(today): + l.Today = append(l.Today, it) + case !it.Due.IsZero() && !it.Due.After(sunday): + l.ThisWeek = append(l.ThisWeek, it) + default: + l.Later = append(l.Later, it) + } + default: + switch it.Promise { + case YouPromised: + l.YouPromised = append(l.YouPromised, it) + case WaitingOnOthers: + l.WaitingOnOthers = append(l.WaitingOnOthers, it) + default: + l.OnItsWay = append(l.OnItsWay, it) + } + } + } + switch { + case len(items) == 0 && mailToday: + mailItem.Reminded = true + l.Today = append(l.Today, mailItem) + case e.ReadLater: + l.ReadLater = append(l.ReadLater, mailItem) + case len(items) == 0: + l.GoodToKnow = append(l.GoodToKnow, mailItem) + } + } + for _, section := range [][]ListItem{l.Today, l.ThisWeek, l.Later, l.YouPromised, l.WaitingOnOthers, l.OnItsWay} { + sortByDue(section) + } + for _, section := range [][]ListItem{l.GoodToKnow, l.ReadLater, l.ScamWarnings} { + sort.SliceStable(section, func(i, j int) bool { return section[i].Entry.Date.After(section[j].Entry.Date) }) + } + sort.SliceStable(l.HandledToday, func(i, j int) bool { return l.HandledToday[i].Handled.After(l.HandledToday[j].Handled) }) + return l +} + +// itemsOf is an entry's tasks and promises as items, in their numbers' order. +func itemsOf(e Entry) []ListItem { + var out []ListItem + for _, t := range e.Tasks { + out = append(out, ListItem{Entry: e, Item: "task " + strconv.Itoa(t.N), Kind: "task", Line: t.Line, Due: t.Due}) + } + for _, p := range e.Promises { + out = append(out, ListItem{Entry: e, Item: "promise " + strconv.Itoa(p.N), Kind: "promise", Line: p.Line, Due: p.Date, + Promise: p.Kind, Who: p.Who}) + } + return out +} + +// sortByDue orders a section nearest date first, none last, then newest mail first. +func sortByDue(section []ListItem) { + sort.SliceStable(section, func(i, j int) bool { + a, b := section[i], section[j] + if a.Due.IsZero() != b.Due.IsZero() { + return b.Due.IsZero() + } + if !a.Due.Equal(b.Due) { + return a.Due.Before(b.Due) + } + return a.Entry.Date.After(b.Entry.Date) + }) +} + +// listItemOut is one line as list answers it: never a mail body, only the mail's headers, its summary and the +// model's own lines. +func listItemOut(it ListItem) map[string]any { + e := it.Entry + out := map[string]any{ + "id": e.ID, "item": it.Item, "kind": it.Kind, "line": it.Line, "account": e.Account, + "sender-name": e.SenderName, "sender-address": e.SenderAddress, "subject": e.Subject, + "date": e.Date.UTC().Format(time.RFC3339), "summary": e.Summary, "mark": string(e.Mark), "why": e.Why, + } + if !it.Due.IsZero() { + key := "due-date" + if it.Kind == "promise" { + key = "promise-date" + } + out[key] = it.Due.Format("2006-01-02") + } + if it.Kind == "promise" { + out["promise-kind"] = string(it.Promise) + out["who"] = it.Who + } + if it.Reminded { + out["reminded"] = true + } + if !it.Handled.IsZero() { + out["handled"] = it.Handled.UTC().Format(time.RFC3339) + out["undo"] = true // until the day ends (ADR 0517 §1) + } + return out +} + +func sectionOut(items []ListItem, limit int) []map[string]any { + if len(items) > limit { + items = items[:limit] + } + out := make([]map[string]any, len(items)) + for i, it := range items { + out[i] = listItemOut(it) + } + return out +} diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/list_test.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/list_test.go new file mode 100644 index 00000000..4131fae2 --- /dev/null +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/list_test.go @@ -0,0 +1,230 @@ +package main + +// Issue 1858 (novox/hq ADR 0517 §1, §2, §4; ADR 0515 §1, §4): the operator's list for a mode — tasks under +// today, this week and later by their due date (Europe/Brussels), promises by kind, good to know, read later, +// handled today with its undo, the scam warnings in their own place — and when the operator last looked. + +import ( + "context" + "encoding/json" + "testing" + "time" +) + +// listOf calls list and answers it as the page reads it: JSON. +type listed struct { + Mode string `json:"mode"` + TodayIs string `json:"today-is"` + LastLooked string `json:"last-looked"` + Today []map[string]any `json:"today"` + ThisWeek []map[string]any `json:"this-week"` + Later []map[string]any `json:"later"` + YouPromised []map[string]any `json:"you-promised"` + WaitingOnOthers []map[string]any `json:"waiting-on-others"` + OnItsWay []map[string]any `json:"on-its-way"` + GoodToKnow []map[string]any `json:"good-to-know"` + ReadLater []map[string]any `json:"read-later"` + HandledToday []map[string]any `json:"handled-today"` + ScamWarnings []map[string]any `json:"scam-warnings"` +} + +func listOf(t *testing.T, b *Bundle, args map[string]any) listed { + t.Helper() + out, err := callTool(t, b, ListTool, args) + if err != nil { + t.Fatal(err) + } + raw, _ := json.Marshal(out) + var l listed + if err := json.Unmarshal(raw, &l); err != nil { + t.Fatal(err) + } + return l +} + +// lineSet is the lines of a section, each as " ". +func lineSet(section []map[string]any) map[string]bool { + out := map[string]bool{} + for _, it := range section { + out[jsonID(it["id"])+" "+it["item"].(string)] = true + } + return out +} + +func jsonID(v any) string { raw, _ := json.Marshal(v); return string(raw) } + +func idItem(id int64, item string) string { return jsonID(id) + " " + item } + +func addEntry(t *testing.T, store Store, e Entry) int64 { + t.Helper() + if e.Date.IsZero() { + e.Date = time.Date(2026, 10, 20, 8, 0, 0, 0, time.UTC) + } + if e.Mark == "" { + e.Mark = MarkNeither + } + id, err := store.AddEntry(context.Background(), e) + if err != nil { + t.Fatal(err) + } + return id +} + +func day(y int, m time.Month, d int) time.Time { return time.Date(y, m, d, 0, 0, 0, 0, time.UTC) } + +// 1858/AC1: handle task 2, and list shows task 1 and no longer task 2; handle the mail itself, and none of +// its items shows. +func TestHandlingATaskOrTheMailHidesItsItems(t *testing.T) { + store := newMemStore() + id := entryWithItems(t, store) + now := brusselsAt(2026, 10, 22, 9, 0) + b := listBundle(store, &now) + if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id), "item": "task 2"}); err != nil { + t.Fatal(err) + } + l := listOf(t, b, map[string]any{}) + open := lineSet(append(append(l.Today, l.ThisWeek...), l.Later...)) + if !open[idItem(id, "task 1")] || open[idItem(id, "task 2")] { + t.Fatalf("tasks listed %v, want task 1 and not task 2", open) + } + if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id)}); err != nil { + t.Fatal(err) + } + l = listOf(t, b, map[string]any{}) + for name, section := range map[string][]map[string]any{"today": l.Today, "this week": l.ThisWeek, "later": l.Later, + "on its way": l.OnItsWay, "good to know": l.GoodToKnow, "read later": l.ReadLater} { + if len(section) != 0 { + t.Fatalf("%s holds %v after the mail itself was handled, want nothing of it", name, section) + } + } +} + +// 1858/AC2: an item handled today is under handled today, with its undo; unhandle puts it back in its +// section; after local midnight it is no longer under handled today. +func TestHandledTodayHasItsUndoUntilTheDayEnds(t *testing.T) { + store := newMemStore() + id := entryWithItems(t, store) + now := brusselsAt(2026, 10, 22, 22, 30) // 20:30 UTC: still the 22nd in Brussels + b := listBundle(store, &now) + if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id), "item": "task 1"}); err != nil { + t.Fatal(err) + } + l := listOf(t, b, map[string]any{}) + if !lineSet(l.HandledToday)[idItem(id, "task 1")] || l.HandledToday[0]["undo"] != true { + t.Fatalf("handled today = %v, want task 1 with its undo", l.HandledToday) + } + if lineSet(l.Later)[idItem(id, "task 1")] { + t.Fatal("a handled task is still in its section") + } + if _, err := callTool(t, b, UnhandleTool, map[string]any{"id": float64(id), "item": "task 1"}); err != nil { + t.Fatal(err) + } + l = listOf(t, b, map[string]any{}) + if len(l.HandledToday) != 0 || !lineSet(l.Later)[idItem(id, "task 1")] { + t.Fatalf("after unhandle: handled today %v, later %v; want task 1 back under later", l.HandledToday, l.Later) + } + if _, err := callTool(t, b, HandleTool, map[string]any{"id": float64(id), "item": "task 1"}); err != nil { + t.Fatal(err) + } + now = brusselsAt(2026, 10, 23, 0, 1) // 22:01 UTC on the 22nd: the next day in Brussels + l = listOf(t, b, map[string]any{}) + if len(l.HandledToday) != 0 || lineSet(l.Later)[idItem(id, "task 1")] { + t.Fatalf("after midnight: handled today %v, later %v; want task 1 in neither", l.HandledToday, l.Later) + } +} + +// 1858/AC3: a reminded item is hidden the same day, and under today the next. +func TestARemindedItemShowsUnderTodayTheNextDay(t *testing.T) { + store := newMemStore() + id := entryWithItems(t, store) + now := brusselsAt(2026, 10, 22, 9, 0) + b := listBundle(store, &now) + if _, err := callTool(t, b, RemindTool, map[string]any{"id": float64(id), "item": "promise 1"}); err != nil { + t.Fatal(err) + } + l := listOf(t, b, map[string]any{}) + if lineSet(l.OnItsWay)[idItem(id, "promise 1")] || lineSet(l.Today)[idItem(id, "promise 1")] { + t.Fatalf("the reminded promise shows the same day: on its way %v, today %v", l.OnItsWay, l.Today) + } + now = brusselsAt(2026, 10, 23, 7, 0) + l = listOf(t, b, map[string]any{}) + if !lineSet(l.Today)[idItem(id, "promise 1")] || lineSet(l.OnItsWay)[idItem(id, "promise 1")] { + t.Fatalf("the next day: today %v, on its way %v; want the promise under today", l.Today, l.OnItsWay) + } +} + +// 1858/AC5: due today, Saturday of this week, next month and none fall under today, this week, later and +// later; promises by kind; good to know holds the entries with no task or promise, not read later, not a +// scam warning and not handled; read later holds the read-later ones; the mode's accounts alone; and the +// answer carries when the operator last looked. +func TestTheListsSections(t *testing.T) { + ctx := context.Background() + store := newMemStore() + now := brusselsAt(2026, 10, 22, 9, 0) // a Thursday + b := listBundle(store, &now) + tasks := addEntry(t, store, Entry{Account: "work", UID: 1, Subject: "four tasks", Tasks: []Task{ + {N: 1, Line: "due today", Due: day(2026, 10, 22)}, + {N: 2, Line: "due Saturday", Due: day(2026, 10, 24)}, + {N: 3, Line: "due next month", Due: day(2026, 11, 20)}, + }}) + noDue := addEntry(t, store, Entry{Account: "work", UID: 2, Tasks: []Task{{N: 1, Line: "no due date"}}}) + overdue := addEntry(t, store, Entry{Account: "work", UID: 3, Tasks: []Task{{N: 1, Line: "overdue", Due: day(2026, 10, 20)}}}) + promises := addEntry(t, store, Entry{Account: "work", UID: 4, Promises: []Promise{ + {N: 1, Kind: YouPromised, Line: "send the slides"}, {N: 2, Kind: WaitingOnOthers, Line: "Tim's quote"}, + {N: 3, Kind: OnItsWay, Line: "the Telco router"}, + }}) + know := addEntry(t, store, Entry{Account: "work", UID: 5, Subject: "a newsletter"}) + later := addEntry(t, store, Entry{Account: "work", UID: 6, ReadLater: true}) + scam := addEntry(t, store, Entry{Account: "work", UID: 7, Mark: MarkScamWarning, Tasks: []Task{{N: 1, Line: "pay the fake invoice"}}}) + handled := addEntry(t, store, Entry{Account: "work", UID: 8, Subject: "handled long ago"}) + _ = store.Act(ctx, Handling{Entry: handled, Item: ItemMail, Handled: now.AddDate(0, 0, -3)}, ActionLine{What: ActHandled}) + personal := addEntry(t, store, Entry{Account: "personal", UID: 9, Subject: "Hostco bill", Tasks: []Task{{N: 1, Line: "pay Hostco"}}}) + looked := now.Add(-2 * time.Hour) + _ = store.Record(ctx, ActionLine{What: ActPageOpened, Mode: "work", At: looked}) + + l := listOf(t, b, map[string]any{"mode": "work", "accounts": []any{"work"}}) + want := map[string]map[string]bool{ + "today": {idItem(tasks, "task 1"): true, idItem(overdue, "task 1"): true}, + "this week": {idItem(tasks, "task 2"): true}, + "later": {idItem(tasks, "task 3"): true, idItem(noDue, "task 1"): true}, + "you promised": {idItem(promises, "promise 1"): true}, + "waiting on others": {idItem(promises, "promise 2"): true}, + "on its way": {idItem(promises, "promise 3"): true}, + "good to know": {idItem(know, "mail"): true}, + "read later": {idItem(later, "mail"): true}, + "scam warnings": {idItem(scam, "mail"): true}, + } + got := map[string]map[string]bool{ + "today": lineSet(l.Today), "this week": lineSet(l.ThisWeek), "later": lineSet(l.Later), + "you promised": lineSet(l.YouPromised), "waiting on others": lineSet(l.WaitingOnOthers), "on its way": lineSet(l.OnItsWay), + "good to know": lineSet(l.GoodToKnow), "read later": lineSet(l.ReadLater), "scam warnings": lineSet(l.ScamWarnings), + } + for name, w := range want { + if len(got[name]) != len(w) { + t.Errorf("%s = %v, want %v", name, got[name], w) + continue + } + for k := range w { + if !got[name][k] { + t.Errorf("%s = %v, want %v", name, got[name], w) + } + } + } + if l.Mode != "work" || l.TodayIs != "2026-10-22" || l.LastLooked != looked.UTC().Format(time.RFC3339) { + t.Errorf("mode %q, today-is %q, last-looked %q; want work, 2026-10-22, %s", l.Mode, l.TodayIs, l.LastLooked, looked.UTC().Format(time.RFC3339)) + } + if t.Failed() { + return + } + // Everything: every account, the personal one's task too. + all := listOf(t, b, map[string]any{"mode": "everything"}) + if !lineSet(all.Later)[idItem(personal, "task 1")] { + t.Fatalf("everything's later = %v, want the personal account's task too", all.Later) + } + // A task line carries what the page shows: its line, its due date, its mail's sender and subject. + for _, it := range l.ThisWeek { + if it["line"] != "due Saturday" || it["due-date"] != "2026-10-24" || it["kind"] != "task" || it["subject"] != "four tasks" { + t.Fatalf("this week's task = %v", it) + } + } +} diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/listverbs.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/listverbs.go index 37f945a1..64396cb5 100644 --- a/modules/mesh-mail-reader/cmd/mesh-mail-reader/listverbs.go +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/listverbs.go @@ -183,3 +183,71 @@ func (b *Bundle) remindTool(args map[string]any) (any, error) { h.ShowsAgain = dayOf(now).AddDate(0, 0, 1) return a.act(RemindTool, ActReminded, h, now) } + +// listTool is the operator's list for a mode (ADR 0517 §4): the mode's accounts (mesh-mail-web names them from +// its setting modes; every account when none is given), each section at most limit lines, and when the +// operator last looked (the newest page opened line). Read-only: the page opened is record's to write. +func (b *Bundle) listTool(args map[string]any) (any, error) { + if _, err := b.checkCaller(args); err != nil { + return nil, err + } + if err := only(args, "mode", "accounts", "limit"); err != nil { + return nil, err + } + mode, err := modeArg(args) + if err != nil { + return nil, err + } + var accounts []string + if v, has := args["accounts"]; has && v != nil { + list, ok := v.([]any) + if !ok { + return nil, fmt.Errorf("%s needs accounts to be a list of account names", ListTool) + } + for _, a := range list { + s, ok := a.(string) + if !ok || s == "" { + return nil, fmt.Errorf("%s needs accounts to be a list of account names", ListTool) + } + accounts = append(accounts, s) + } + } + limit, err := number(args, "limit", DefaultListLimit) + if err != nil { + return nil, err + } + if limit <= 0 { + limit = DefaultListLimit + } + ctx := context.Background() + store, err := b.Store(ctx) + if err != nil { + return nil, err + } + entries, err := store.Entries(ctx) + if err != nil { + return nil, err + } + handlings, err := store.Handlings(ctx) + if err != nil { + return nil, err + } + now := b.now() + l := BuildList(entries, handlings, accounts, now) + out := map[string]any{ + "mode": mode, "today-is": dayOf(now).Format("2006-01-02"), + "today": sectionOut(l.Today, limit), "this-week": sectionOut(l.ThisWeek, limit), "later": sectionOut(l.Later, limit), + "you-promised": sectionOut(l.YouPromised, limit), "waiting-on-others": sectionOut(l.WaitingOnOthers, limit), + "on-its-way": sectionOut(l.OnItsWay, limit), "good-to-know": sectionOut(l.GoodToKnow, limit), + "read-later": sectionOut(l.ReadLater, limit), "scam-warnings": sectionOut(l.ScamWarnings, limit), + "handled-today": sectionOut(l.HandledToday, limit), + } + looked, ok, err := store.LastAction(ctx, ActPageOpened) + if err != nil { + return nil, err + } + if ok { + out["last-looked"] = looked.UTC().Format(time.RFC3339) + } + return out, nil +} diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/manifest_test.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/manifest_test.go index 9565112b..023e71a1 100644 --- a/modules/mesh-mail-reader/cmd/mesh-mail-reader/manifest_test.go +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/manifest_test.go @@ -59,7 +59,7 @@ func TestTheManifestClaimsMailReadingAndInvokesReadOnly(t *testing.T) { t.Fatalf("runs-as %q, want %q", m.RunsAs, Module) } wantClaims := []seat{ - {Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"}}, + {Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"}}, } if !reflect.DeepEqual(m.Claims, wantClaims) { t.Fatalf("claims %+v, want %+v", m.Claims, wantClaims) diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools.go index 802c4bd2..cdacccde 100644 --- a/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools.go +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools.go @@ -196,6 +196,19 @@ func (b *Bundle) Tools() []stdio.Tool { "no mail read says so). Answered only to a caller the setting callers names. Read-only.", Input: schema(nil, map[string]any{}), Run: b.statusTool}, + {Name: ListTool, + Description: "The operator's list for a mode (novox/hq ADR 0517 §4, ADR 0515 §1): tasks under today, this " + + "week and later by their due date (Europe/Brussels), promises by kind (you promised, waiting on " + + "others, on its way), good to know, read later, handled today with its undo and the scam warnings, " + + "each line an item (a task, a promise or the mail itself) with its mail's headers and summary — " + + "never a mail body — and when the operator last looked. Answered only to a caller the setting " + + "callers names. Read-only.", + Input: schema(nil, map[string]any{ + "mode": text("work, personal or everything (when left out)"), + "accounts": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "the mode's accounts, by name; every account when left out"}, + "limit": integer("how many lines each section answers at most; 50 when left out"), + }), + Run: b.listTool}, {Name: HandleTool, Description: "Mark one item of a mail entry handled for the operator (novox/hq ADR 0517 §1): a task or a " + "promise by its number, or the mail itself, which handles all its items with it. It changes nothing " + diff --git a/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools_test.go b/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools_test.go index 231e01ef..ca7f9fc5 100644 --- a/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools_test.go +++ b/modules/mesh-mail-reader/cmd/mesh-mail-reader/tools_test.go @@ -21,9 +21,7 @@ func TestToolsListsTheSixVerbsAndHealth(t *testing.T) { } } want := []string{ - "mail-reading.entries", "mail-reading.groups", "mail-reading.correct", "mail-reading.lessons", - "mail-reading.undo", "mail-reading.status", "mail-reading.handle", "mail-reading.unhandle", "mail-reading.remind", - "mail_reader_health", + "mail-reading.entries", "mail-reading.groups", "mail-reading.correct", "mail-reading.lessons", "mail-reading.undo", "mail-reading.status", "mail-reading.list", "mail-reading.handle", "mail-reading.unhandle", "mail-reading.remind", "mail_reader_health", } if len(names) != len(want) { t.Fatalf("Tools() = %v, want %v", names, want) diff --git a/modules/mesh-mail-reader/module.json b/modules/mesh-mail-reader/module.json index 3930172d..11190aa1 100644 --- a/modules/mesh-mail-reader/module.json +++ b/modules/mesh-mail-reader/module.json @@ -6,7 +6,7 @@ { "name": "mail-reading", "scope": "mesh", - "serves": ["entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"] + "serves": ["entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"] } ], "consumes": [ @@ -48,6 +48,7 @@ "mail-reading.lessons", "mail-reading.undo", "mail-reading.status", + "mail-reading.list", "mail-reading.handle", "mail-reading.unhandle", "mail-reading.remind", diff --git a/modules/mesh-mail/cmd/mesh-mail/manifest_test.go b/modules/mesh-mail/cmd/mesh-mail/manifest_test.go index 17a026bb..acdc9c56 100644 --- a/modules/mesh-mail/cmd/mesh-mail/manifest_test.go +++ b/modules/mesh-mail/cmd/mesh-mail/manifest_test.go @@ -148,7 +148,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { {Name: "mail-store", Scope: "mesh", Serves: []string{"accounts", "search", "read", "send", "send-status"}}, {Name: "mail-webclient", Scope: "mesh", Serves: []string{"open", "status"}}, {Name: "mail-notifier", Scope: "node", Serves: []string{"status"}, Needs: []string{"node-notifier"}}, - {Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"}}, + {Name: "mail-reading", Scope: "mesh", Serves: []string{"entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"}}, } if !reflect.DeepEqual(m.Seats, wantSeats) { t.Fatalf("seats %+v, want %+v", m.Seats, wantSeats) diff --git a/modules/mesh-mail/module.json b/modules/mesh-mail/module.json index 008acefd..fd5e23fe 100644 --- a/modules/mesh-mail/module.json +++ b/modules/mesh-mail/module.json @@ -22,7 +22,7 @@ { "name": "mail-reading", "scope": "mesh", - "serves": ["entries", "groups", "correct", "lessons", "undo", "status", "handle", "unhandle", "remind"] + "serves": ["entries", "groups", "correct", "lessons", "undo", "status", "list", "handle", "unhandle", "remind"] } ], "claims": [