Files
Maven/internal/router/task_test.go
T
kami 7b2b96b957 Capture tasks, with one intake seam mail can call later (#130)
A task is not a fact and not a note. A fact is a claim about the world that a
correction supersedes; a note is something to recall by meaning. A task is work
with a lifecycle, and the read that matters is "everything outstanding right
now" — which over an append-only log would mean replaying history on every
question. So: a tasks table, migration #14, statuses candidate/open/done/dropped
that each move forward exactly once.

Dedupe is on normalised text among LIVE rows only, via a partial unique index.
That is the property the mail side needs: an extractor may call CaptureTask for
every message it reads, as often as it likes, without growing the list — while a
weekly errand is still capturable again once the last one is done.

Three ways in, one seam. ipc.CaptureTaskReq is it: the voice path
(router.ParseTaskCapture on an explicit marker — "добавь в задачи …", never
"надо бы поспать"), the /tasks form, and the email reader from #246 when it
exists. Mail-derived items set Source "email:<account>", Status "candidate" and
Evidence to whatever makes the row reviewable; a candidate is inert until he
confirms it on /tasks, and Maven names it as unconfirmed when she recites the
list rather than putting words in his mouth.

No new intent — the router enum is a contract with the relabelling prompt, so
capture rides the note intent and the list rides a query source, both matched
deterministically like the calendar and plan matchers already are.

Nothing here speaks. No tick rule reads tasks; the list is answered when asked
about, which is why /tasks POST is not step-up gated the way /tools and
/routines are — a task write moves no boundary.

Vikunja #130
2026-08-01 02:33:47 +04:00

61 lines
1.9 KiB
Go

package router
import "testing"
func TestParseTaskCapture(t *testing.T) {
cases := []struct {
in string
text string
ok bool
}{
{"добавь в задачи купить молоко", "купить молоко", true},
{"Добавь в список дел: позвонить в банк", "позвонить в банк", true},
{"запиши задачу починить кран.", "починить кран", true},
{"новая задача — оплатить интернет", "оплатить интернет", true},
{"add a task buy milk", "buy milk", true},
// A marker with nothing after it files nothing.
{"добавь в задачи", "", false},
{"новая задача", "", false},
// Not a capture: he is talking, not filing.
{"надо бы поспать", "", false},
{"я не добавил молоко в список", "", false},
{"какие у меня задачи?", "", false},
{"", "", false},
}
for _, c := range cases {
text, ok := ParseTaskCapture(c.in)
if ok != c.ok || text != c.text {
t.Errorf("ParseTaskCapture(%q) = (%q, %v), want (%q, %v)", c.in, text, ok, c.text, c.ok)
}
}
}
func TestIsTaskListQuery(t *testing.T) {
yes := []string{
"какие у меня задачи?",
"что мне нужно сделать?",
"покажи список дел",
"сколько у меня задач?",
"задачи",
"мои задачи",
"what should I do",
}
for _, s := range yes {
if !IsTaskListQuery(s) {
t.Errorf("IsTaskListQuery(%q) = false, want true", s)
}
}
no := []string{
"как дела?",
"какая погода?",
"напомни мне позвонить маме в шесть",
"я сделал зарядку",
"",
}
for _, s := range no {
if IsTaskListQuery(s) {
t.Errorf("IsTaskListQuery(%q) = true, want false", s)
}
}
}