package main import ( "context" "os" "path/filepath" "strings" "testing" "time" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/tool" ) // TestClassifyConfirmRejectsSubstrings — V-567. The old matcher tested bare // stems with strings.Contains, so every word below answered a question she had // asked about something else: "погода", "дальше", "надо" and "давление" carry // "да"; "покажи", "около" and "окно" carry "ок". A parked destructive act fired // on a question about the weather. func TestClassifyConfirmRejectsSubstrings(t *testing.T) { for _, text := range []string{ "погода", "какая погода", "что дальше", "надо ещё", "покажи заметки", "около окна", "давление", "давай посмотрим погоду", "не забудь купить хлеб", "окно открыто", "стоит ли брать зонт", "", } { if got := classifyConfirm(text); got != confirmUnknown { t.Errorf("classifyConfirm(%q) = %v, want confirmUnknown", text, got) } } } // TestClassifyConfirmAcceptsAnswers keeps every genuine answer the substring // matcher accepted, and pins the pair the fix could most easily get wrong: // "надо" is not an answer and "не надо" is the opposite of one. func TestClassifyConfirmAcceptsAnswers(t *testing.T) { yes := []string{"да", "Да!", "ага", "давай", "да, давай", "конечно", "подтверждаю", "ну да", "yes", "yeah", "ok", "okay", "confirm"} no := []string{"нет", "Нет.", "не надо", "не нужно", "не сейчас", "отмена", "отмени", "стоп", "нет, отмени", "no", "nope", "cancel", "stop", "don't"} for _, text := range yes { if got := classifyConfirm(text); got != confirmYes { t.Errorf("classifyConfirm(%q) = %v, want confirmYes", text, got) } } for _, text := range no { if got := classifyConfirm(text); got != confirmNo { t.Errorf("classifyConfirm(%q) = %v, want confirmNo", text, got) } } } // TestUnrelatedTurnLeavesConfirmParked — the whole point of V-567. An utterance // that is not an answer must not execute the parked act, must not consume the // turn, and must not disarm the confirm either: the answer he has not given yet // is still answerable until it expires. func TestUnrelatedTurnLeavesConfirmParked(t *testing.T) { ctx := context.Background() st := newTestStore(t) api := ipc.NewStoreAPI(st) now := time.Date(2026, 8, 6, 9, 0, 0, 0, time.UTC) h := &reactiveHandler{ api: api, dataStore: st, now: func() time.Time { return now }, tools: tool.NewExecutor(api, time.Second), } marker := filepath.Join(t.TempDir(), "destructive-tool-ran") if err := st.EnableTool(ctx, "delete_backups", []string{"touch", marker}, true, "test", h.now()); err != nil { t.Fatal(err) } h.park("delete_backups", nil, "delete_backups") if reply, handled := h.resolveConfirm(ctx, "какая погода"); handled { t.Fatalf("the weather question was consumed as a confirm: %q", reply) } if _, err := os.Stat(marker); !os.IsNotExist(err) { t.Fatalf("the parked destructive command ran on an unrelated turn: %v", err) } if h.pending == nil { t.Fatal("the confirm was disarmed by a turn that did not answer it") } // It is still answerable, and answering it still runs the act. reply, handled := h.resolveConfirm(ctx, "да") if !handled || !strings.Contains(reply, "готово") { t.Fatalf("the still-parked confirm did not resolve: handled=%v reply=%q", handled, reply) } if _, err := os.Stat(marker); err != nil { t.Fatalf("confirmed destructive command did not run: %v", err) } if h.pending != nil { t.Fatal("the confirm stayed parked after being answered") } }