a live task can be edited, a resolved one cannot (V-509)
SetTaskStatus was the only mutation on a task row, so a typo in a dictated task was permanent and a deadline could not move. EditTask rewrites the three fields capture set — text, due date and weight — and nothing else. Status stays the one-way ladder SetTaskStatus owns. Two things the task asked to settle. A text edit re-normalises the dedupe key and can collide with another live row. That is ErrTaskDuplicate, a refusal rather than a merge: two live rows carry two provenances, two capture times and possibly two external identities, and merging picks a winner for all three with nobody asked. The surface names the row that holds the text. A resolved task is refused outright (ErrTaskResolved). Its text is the record of what was finished, and rewriting it rewrites history. due nil clears the date, because clearing has to be sayable — an absent date and "remove the date" cannot be one argument.
This commit is contained in:
@@ -465,3 +465,80 @@ func TestPromotingACandidateNeedsADefinitionOfDone(t *testing.T) {
|
||||
t.Errorf("status = %q, want open", got.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditTaskRewritesTheCaptureFields(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := newTestStore(t)
|
||||
now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.UTC)
|
||||
due := now.Add(48 * time.Hour)
|
||||
|
||||
res, err := st.CaptureTask(ctx, Task{Text: "купить малако", Source: "tap:voice", CreatedTs: now, Weight: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.EditTask(ctx, res.ID, " купить молоко ", &due, 3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := st.LookupTask(ctx, res.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.Text != "купить молоко" || got.Weight != 3 || got.Due == nil || !got.Due.Equal(due) {
|
||||
t.Fatalf("task = %+v, want the dictation typo fixed with the date and weight", got)
|
||||
}
|
||||
// Clearing the date has to be sayable, or an absent date and "remove the
|
||||
// date" would be one argument.
|
||||
if err := st.EditTask(ctx, res.ID, "купить молоко", nil, 3); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err = st.LookupTask(ctx, res.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if got.Due != nil {
|
||||
t.Errorf("due = %v, want it cleared", got.Due)
|
||||
}
|
||||
// The dedupe key moved with the text: capturing the old wording is new work.
|
||||
again, err := st.CaptureTask(ctx, Task{Text: "купить малако", Source: "tap:voice", CreatedTs: now})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !again.Created {
|
||||
t.Error("the old normalised text must be free after the edit")
|
||||
}
|
||||
if err := st.EditTask(ctx, res.ID, "", nil, 0); !errors.Is(err, ErrTaskEmpty) {
|
||||
t.Errorf("empty text = %v, want ErrTaskEmpty", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditTaskRefusesACollisionAndAResolvedRow(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := newTestStore(t)
|
||||
now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.UTC)
|
||||
|
||||
first, err := st.CaptureTask(ctx, Task{Text: "купить молоко", Source: "tap:voice", CreatedTs: now})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second, err := st.CaptureTask(ctx, Task{Text: "оплатить интернет", Source: "tap:voice", CreatedTs: now})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Two live rows carry two provenances and two capture times, so a merge
|
||||
// would pick a winner for both with nobody asked.
|
||||
if err := st.EditTask(ctx, second.ID, "Купить молоко!", nil, 0); !errors.Is(err, ErrTaskDuplicate) {
|
||||
t.Errorf("collision = %v, want ErrTaskDuplicate", err)
|
||||
}
|
||||
// Editing a row to the text it already has is not a collision with itself.
|
||||
if err := st.EditTask(ctx, first.ID, "купить молоко", nil, 2); err != nil {
|
||||
t.Errorf("re-saving the same text: %v", err)
|
||||
}
|
||||
if err := st.SetTaskStatus(ctx, first.ID, TaskDone, now.Add(time.Hour), "tap:web"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A resolved task's text is the record of what was finished.
|
||||
if err := st.EditTask(ctx, first.ID, "купить кефир", nil, 0); !errors.Is(err, ErrTaskResolved) {
|
||||
t.Errorf("editing a resolved task = %v, want ErrTaskResolved", err)
|
||||
}
|
||||
if err := st.EditTask(ctx, 9999, "что-то", nil, 0); !errors.Is(err, ErrTaskNotFound) {
|
||||
t.Errorf("editing a missing row = %v, want ErrTaskNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user