Files
Maven/internal/router/taskstatus_test.go
T
claude c259ed6c73 a spoken status change reaches the board at stage 0 (V-512)
Step 4 of the board build (docs/plans/15-board-surface.md). Naming a task
instead of its position reached nothing: "закрой задачу купить молоко" routed
act, found no allowlisted fn, and the gate asked "Что сделать?". The position
path already worked through resolveCandidate, but only in the two turns after
she read the list out.

TaskStatusGrammar is the same shape TaskCaptureGrammar uses — matches broadly,
decides in Build, no eighth intent — and fills the fn slot with task_status,
which is neither a Hexis capability nor a Praxis one. Three conditions, all
required: the board noun, so no ordinary sentence claims a turn; exactly one
status class, since "готово, убери" names two and asking beats picking; and a
status word matched as an imperative exactly or a stative by lemma. So a bare
"готово" and a bare "закрой" are not this rule's, and the second belongs to
Praxis, which claims it already.

Two lexicon sets rather than one with a value. The store records which of the
two transitions happened and /tasks shows it: work he chose to stop is not work
he did.

Measured on the fixture, two new cases (ru-act-020, ru-act-021). Classifier +
ONNX 62/89 (69.7%) → 64/91 (70.3%); cascade+llm 67/89 (75.3%) → 69/91 (75.8%,
80.2% intent-only) at p50 1.225s. Both new cases claimed at stage 0, no case
regressed, clarify counts unchanged at 3 false / 1 missed.

The task's own warning stands: every such grammar runs its parser ahead of the
resident model on every turn, so this is the last one that is free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoL7EBdYC5Mhz3DJd49GJy
2026-08-05 19:53:20 +04:00

69 lines
2.7 KiB
Go

package router
import "testing"
func TestParseTaskStatus(t *testing.T) {
cases := []struct {
utterance string
ok bool
status string
text string
}{
// The shapes that reached nothing before this rule.
{"закрой задачу купить молоко", true, "done", "купить молоко"},
{"задачу купить молоко сделал", true, "done", "купить молоко"},
{"убери из задач купить молоко", true, "dropped", "купить молоко"},
{"убери из моих задач купить молоко", true, "dropped", "купить молоко"},
{"отмени задачу оплатить интернет", true, "dropped", "оплатить интернет"},
{"task buy milk done", true, "done", "buy milk"},
// The referent may be missing. The turn is still his, and the daemon has
// the list to ask about.
{"закрой задачу", true, "done", ""},
{"убери задачу", true, "dropped", ""},
// No board noun: ordinary speech, and every one of these means something
// else. "закрой" alone belongs to Praxis.
{"готово", false, "", ""},
{"закрой", false, "", ""},
{"убери со стола", false, "", ""},
{"я всё сделал", false, "", ""},
{"закрой шторы в комнате", false, "", ""},
// The board noun with no status word is a list query, not a move.
{"какие у меня задачи", false, "", ""},
{"добавь в задачи купить молоко", false, "", ""},
// Two transitions in one sentence. Asking beats picking.
{"задачу купить молоко готово убери", false, "", ""},
{"", false, "", ""},
}
for _, c := range cases {
got, ok := ParseTaskStatus(c.utterance)
if ok != c.ok {
t.Errorf("ParseTaskStatus(%q) ok = %v, want %v", c.utterance, ok, c.ok)
continue
}
if !ok {
continue
}
if got.Status != c.status || got.Text != c.text {
t.Errorf("ParseTaskStatus(%q) = %+v, want status %q text %q", c.utterance, got, c.status, c.text)
}
}
}
func TestTaskStatusGrammarFillsTheFnSlot(t *testing.T) {
g := TaskStatusGrammar()
m := g.Pattern.FindStringSubmatch("закрой задачу купить молоко")
if m == nil {
t.Fatal("pattern did not match")
}
dec, ok := g.Build(m)
if !ok {
t.Fatal("Build declined")
}
if dec.Intent != IntentAct || !dec.Slots.HasFn || dec.Slots.Fn != TaskStatusFn {
t.Fatalf("decision = %+v, want act with fn %q", dec, TaskStatusFn)
}
if dec.Slots.Value != "done" || dec.Slots.Text != "купить молоко" {
t.Fatalf("slots = %+v, want value done text \"купить молоко\"", dec.Slots)
}
}