issue 1857: let correct say not a task, remove a promise, or set read later (1857.4)
ADR 0516 §4: each is kept as a reading lesson, as a change of mark or mail group is, so later batches with a mail of that sender follow it; an item number the entry does not hold is refused.
This commit is contained in:
@@ -22,6 +22,19 @@ type Correction struct {
|
||||
WhySet bool
|
||||
DueDate time.Time
|
||||
DueDateSet bool
|
||||
// ADR 0516 §4, issue 1857: a task that is not one, a promise removed (each by its number, 0 for none), and
|
||||
// the read-later mark set or cleared.
|
||||
NotATask int
|
||||
RemovePromise int
|
||||
ReadLater bool
|
||||
ReadLaterSet bool
|
||||
}
|
||||
|
||||
func onOff(b bool) string {
|
||||
if b {
|
||||
return "on"
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
|
||||
// ApplyCorrection applies c to entry, answering the updated entry and a change description naming each
|
||||
@@ -46,6 +59,32 @@ func ApplyCorrection(entry Entry, c Correction, oldGroupLabel string) (Entry, st
|
||||
changes = append(changes, fmt.Sprintf("due date: %s -> %s", dateOrNone(entry.DueDate), dateOrNone(c.DueDate)))
|
||||
entry.DueDate = c.DueDate
|
||||
}
|
||||
if c.NotATask != 0 {
|
||||
var kept []Task
|
||||
for _, t := range entry.Tasks {
|
||||
if t.N == c.NotATask {
|
||||
changes = append(changes, "not a task: "+t.Line)
|
||||
continue
|
||||
}
|
||||
kept = append(kept, t)
|
||||
}
|
||||
entry.Tasks = kept
|
||||
}
|
||||
if c.RemovePromise != 0 {
|
||||
var kept []Promise
|
||||
for _, p := range entry.Promises {
|
||||
if p.N == c.RemovePromise {
|
||||
changes = append(changes, fmt.Sprintf("not a promise (%s): %s", p.Kind, p.Line))
|
||||
continue
|
||||
}
|
||||
kept = append(kept, p)
|
||||
}
|
||||
entry.Promises = kept
|
||||
}
|
||||
if c.ReadLaterSet && c.ReadLater != entry.ReadLater {
|
||||
changes = append(changes, fmt.Sprintf("read later: %s -> %s", onOff(entry.ReadLater), onOff(c.ReadLater)))
|
||||
entry.ReadLater = c.ReadLater
|
||||
}
|
||||
return entry, strings.Join(changes, "; ")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
// Issue 1857 (novox/hq ADR 0516 §4): correct also takes not-a-task, the removal of a promise, and read later
|
||||
// on or off, each kept as a reading lesson the next batch with a mail of that sender is given.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func entryWithItems(t *testing.T, store Store) int64 {
|
||||
t.Helper()
|
||||
id, err := store.AddEntry(context.Background(), Entry{
|
||||
Account: "work", Folder: "INBOX", UID: 5, SenderName: "Lena Janssens", SenderAddress: "lena@harbor.example",
|
||||
Subject: "Hostco renewal", Mark: MarkMatters, Date: time.Date(2026, 10, 21, 8, 0, 0, 0, time.UTC),
|
||||
Tasks: []Task{{N: 1, Line: "Approve the Hostco renewal"}, {N: 2, Line: "Forward the invoice to finance"}},
|
||||
Promises: []Promise{{N: 1, Kind: OnItsWay, Line: "The new contract is on its way from Hostco"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// 1857/AC5: each new kind of correction changes the entry, is kept as a reading lesson, and is offered to the
|
||||
// next batch with a mail of that sender.
|
||||
func TestCorrectTakesNotATaskARemovedPromiseAndReadLater(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args map[string]any
|
||||
check func(Entry) bool
|
||||
change string
|
||||
}{
|
||||
{"not a task", map[string]any{"not-a-task": float64(2)},
|
||||
func(e Entry) bool { return len(e.Tasks) == 1 && e.Tasks[0].N == 1 }, "not a task: Forward the invoice to finance"},
|
||||
{"remove a promise", map[string]any{"remove-promise": float64(1)},
|
||||
func(e Entry) bool { return len(e.Promises) == 0 && len(e.Tasks) == 2 }, "not a promise (on its way): The new contract is on its way from Hostco"},
|
||||
{"read later on", map[string]any{"read-later": true},
|
||||
func(e Entry) bool { return e.ReadLater }, "read later: off -> on"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
store := newMemStore()
|
||||
id := entryWithItems(t, store)
|
||||
args := map[string]any{"id": float64(id)}
|
||||
for k, v := range tc.args {
|
||||
args[k] = v
|
||||
}
|
||||
if _, err := testBundle(store, "mesh-mail-web").correctTool(args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, _, _ := store.Entry(ctx, id)
|
||||
if !tc.check(e) {
|
||||
t.Fatalf("entry after the correction: %+v", e)
|
||||
}
|
||||
lessons, _ := store.Lessons(ctx)
|
||||
if len(lessons) != 1 || lessons[0].Change != tc.change || lessons[0].SenderAddress != "lena@harbor.example" {
|
||||
t.Fatalf("lessons = %+v, want one from lena@harbor.example saying %q", lessons, tc.change)
|
||||
}
|
||||
next := BuildQuestion([]QuestionMail{{ID: 1, Account: "work", Folder: "INBOX",
|
||||
From: []Person{{Name: "Lena Janssens", Address: "lena@harbor.example"}}, Subject: "next"}}, nil, lessons)
|
||||
if !strings.Contains(next.Text, tc.change) {
|
||||
t.Fatalf("the next batch with a mail of that sender is not given the lesson %q:\n%s", tc.change, next.Text)
|
||||
}
|
||||
})
|
||||
}
|
||||
// Read later off, on an entry that has it.
|
||||
store := newMemStore()
|
||||
id := entryWithItems(t, store)
|
||||
e, _, _ := store.Entry(ctx, id)
|
||||
e.ReadLater = true
|
||||
_ = store.UpdateEntry(ctx, e)
|
||||
if _, err := testBundle(store, "mesh-mail-web").correctTool(map[string]any{"id": float64(id), "read-later": false}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if e, _, _ := store.Entry(ctx, id); e.ReadLater {
|
||||
t.Fatal("read later off left the entry read later")
|
||||
}
|
||||
if lessons, _ := store.Lessons(ctx); len(lessons) != 1 || lessons[0].Change != "read later: on -> off" {
|
||||
t.Fatalf("lessons = %+v", lessons)
|
||||
}
|
||||
}
|
||||
|
||||
// 1857/AC5: a task or promise the entry does not hold is refused, and nothing is kept.
|
||||
func TestCorrectRefusesAnItemTheEntryDoesNotHold(t *testing.T) {
|
||||
store := newMemStore()
|
||||
id := entryWithItems(t, store)
|
||||
b := testBundle(store, "mesh-mail-web")
|
||||
for _, args := range []map[string]any{
|
||||
{"id": float64(id), "not-a-task": float64(3)},
|
||||
{"id": float64(id), "remove-promise": float64(2)},
|
||||
} {
|
||||
if _, err := b.correctTool(args); err == nil {
|
||||
t.Fatalf("correct %v: no error", args)
|
||||
}
|
||||
}
|
||||
if lessons, _ := store.Lessons(context.Background()); len(lessons) != 0 {
|
||||
t.Fatalf("lessons = %+v, want none", lessons)
|
||||
}
|
||||
}
|
||||
@@ -141,16 +141,20 @@ func (b *Bundle) Tools() []stdio.Tool {
|
||||
}),
|
||||
Run: b.groupsTool},
|
||||
{Name: CorrectTool,
|
||||
Description: "Correct one mail entry's mark, mail group, why or due date (novox/hq ADR 0459 §8): " +
|
||||
Description: "Correct one mail entry's mark, mail group, why or due date, say one of its tasks is not a " +
|
||||
"task, remove one of its promises, or set or clear read later (novox/hq ADR 0459 §8, ADR 0516 §4): " +
|
||||
"kept at once as a reading lesson (the mail's own headers, the change made, when), no operator " +
|
||||
"Approve needed, and outranking that mail's own words in any later batch. Refused for an unknown " +
|
||||
"mail id or mark. Answered only to a caller the setting callers names.",
|
||||
Input: schema([]string{"id"}, map[string]any{
|
||||
"id": integer("the mail entry's id, as entries answered it"),
|
||||
"mark": text("the corrected mark: matters, scam warning or neither"),
|
||||
"group": text("the corrected mail group, by its label (made if none yet carries it)"),
|
||||
"why": text("the corrected reason, one line"),
|
||||
"due-date": text("the corrected due date (YYYY-MM-DD), or empty to clear it"),
|
||||
"id": integer("the mail entry's id, as entries answered it"),
|
||||
"mark": text("the corrected mark: matters, scam warning or neither"),
|
||||
"group": text("the corrected mail group, by its label (made if none yet carries it)"),
|
||||
"why": text("the corrected reason, one line"),
|
||||
"due-date": text("the corrected due date (YYYY-MM-DD), or empty to clear it"),
|
||||
"not-a-task": integer("the number of a task of this entry that is not a task"),
|
||||
"remove-promise": integer("the number of a promise of this entry to remove"),
|
||||
"read-later": boolean("whether the mail is read later"),
|
||||
}),
|
||||
Run: b.correctTool},
|
||||
{Name: LessonsTool,
|
||||
@@ -297,7 +301,7 @@ func (b *Bundle) correctTool(args map[string]any) (any, error) {
|
||||
if _, err := b.checkCaller(args); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := only(args, "id", "mark", "group", "why", "due-date"); err != nil {
|
||||
if err := only(args, "id", "mark", "group", "why", "due-date", "not-a-task", "remove-promise", "read-later"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id, err := number(args, "id", 0)
|
||||
@@ -360,10 +364,23 @@ func (b *Bundle) correctTool(args map[string]any) (any, error) {
|
||||
}
|
||||
c.DueDateSet = true
|
||||
}
|
||||
if c.NotATask, err = itemNumber(args, "not-a-task", "task", func(n int) bool { return hasTask(entry, n) }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if c.RemovePromise, err = itemNumber(args, "remove-promise", "promise", func(n int) bool { return hasPromise(entry, n) }); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v, has := args["read-later"]; has {
|
||||
on, ok := v.(bool)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s needs read-later to be true or false", CorrectTool)
|
||||
}
|
||||
c.ReadLater, c.ReadLaterSet = on, true
|
||||
}
|
||||
|
||||
updated, change := ApplyCorrection(entry, c, oldGroupLabel)
|
||||
if change == "" {
|
||||
return nil, fmt.Errorf("%s needs a mark, mail group, why or due-date that actually changes the entry", CorrectTool)
|
||||
return nil, fmt.Errorf("%s needs a mark, mail group, why, due-date, not-a-task, remove-promise or read-later that actually changes the entry", CorrectTool)
|
||||
}
|
||||
if err := store.UpdateEntry(ctx, updated); err != nil {
|
||||
return nil, err
|
||||
@@ -384,6 +401,40 @@ func (b *Bundle) correctTool(args map[string]any) (any, error) {
|
||||
return entryOut(updated, newLabels), nil
|
||||
}
|
||||
|
||||
// itemNumber reads a task's or a promise's number (1857/AC5), 0 when left out; a number the entry does not
|
||||
// hold is refused before anything is written.
|
||||
func itemNumber(args map[string]any, key, noun string, holds func(int) bool) (int, error) {
|
||||
if _, has := args[key]; !has {
|
||||
return 0, nil
|
||||
}
|
||||
n, err := number(args, key, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !holds(n) {
|
||||
return 0, fmt.Errorf("%s: this mail entry holds no %s %d", CorrectTool, noun, n)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func hasTask(e Entry, n int) bool {
|
||||
for _, t := range e.Tasks {
|
||||
if t.N == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasPromise(e Entry, n int) bool {
|
||||
for _, p := range e.Promises {
|
||||
if p.N == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func groupLabels(ctx context.Context, store Store) (map[int64]string, error) {
|
||||
groups, err := store.Groups(ctx)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user