an act with nothing on the other end says so (V-556)

askClarify parked "Что сделать?" whatever was on the other end. With an empty
allowlist that question has no answer: she asks, fails, asks again and gives up,
three turns spent on a request she could have declined in the first one.

Empty allowlist now names the gap and parks nothing. A non-empty one still asks,
and names what she can run, capped at six, so the question is answerable.
This commit is contained in:
2026-08-05 23:34:25 +04:00
parent e87088afb8
commit 1b76fa8205
3 changed files with 117 additions and 3 deletions
+71 -3
View File
@@ -232,11 +232,15 @@ func TestClarifyRestatedAnswerWins(t *testing.T) {
}
// TestClarifiedActOffAllowlistIsStillRefused — clarification fills in an
// argument, it never grants authority.
// argument, it never grants authority. One tool is enabled so there is a
// question to park at all (Vikunja #556); the answer names something else.
func TestClarifiedActOffAllowlistIsStillRefused(t *testing.T) {
ctx := context.Background()
h, st, _ := newClarifyHandler(t)
marker := filepath.Join(t.TempDir(), "not-allowed-ran")
if err := st.EnableTool(ctx, "uptime", []string{"true"}, false, "test", h.now()); err != nil {
t.Fatal(err)
}
if _, asked := h.askClarify(ctx, clarifyDec(router.IntentAct, router.Slots{Text: "сделай это"}, "сделай это")); !asked {
t.Fatal("an act with no fn should be asked about")
@@ -251,7 +255,7 @@ func TestClarifiedActOffAllowlistIsStillRefused(t *testing.T) {
if _, err := os.Stat(marker); !os.IsNotExist(err) {
t.Fatalf("a clarified act off the allowlist ran anyway: %v", err)
}
if tools, err := st.ListTools(ctx, "enabled"); err != nil || len(tools) != 0 {
if tools, err := st.ListTools(ctx, "enabled"); err != nil || len(tools) != 1 {
t.Fatalf("clarify must not enable a tool: tools=%+v err=%v", tools, err)
}
}
@@ -571,7 +575,11 @@ func TestARestartExpiresTheParkedQuestion(t *testing.T) {
// line. None of them was ever an answer.
func TestClarifyStepsAsideForItsOwnRequest(t *testing.T) {
ctx := context.Background()
h, _, _ := newClarifyHandler(t)
h, st, _ := newClarifyHandler(t)
// One enabled tool, so there is a question to park (Vikunja #556).
if err := st.EnableTool(ctx, "uptime", []string{"true"}, false, "test", h.now()); err != nil {
t.Fatal(err)
}
if _, asked := h.askClarify(ctx, clarifyDec(router.IntentAct, router.Slots{Text: "выключи свет в спальне"}, "выключи свет в спальне")); !asked {
t.Fatal("an act with no fn should be asked about")
@@ -620,3 +628,63 @@ func TestClarifyQuestionShapedAnswerThatFillsTheGapStillLands(t *testing.T) {
t.Fatalf("reminder was not created: reminders=%v err=%v", reminders, err)
}
}
// TestActWithNothingOnTheOtherEndNamesTheGap — Vikunja #556. With no tool
// enabled, "Что сделать?" has no answer he could give, so she says so and parks
// nothing rather than spending three turns on a request she cannot fulfil.
func TestActWithNothingOnTheOtherEndNamesTheGap(t *testing.T) {
ctx := context.Background()
h, _, _ := newClarifyHandler(t)
reply, spoken := h.askClarify(ctx, clarifyDec(router.IntentAct, router.Slots{Text: "выключи свет"}, "выключи свет"))
if !spoken || reply == "" {
t.Fatal("an act with nothing on the other end must still say something")
}
if strings.Contains(reply, "?") {
t.Errorf("reply = %q, want a statement, not a question", reply)
}
if h.clarifyStore.Get(voiceDialogueID, h.now()) != nil {
t.Error("nothing to ask about, so nothing may be parked")
}
}
// TestActClarifyNamesWhatSheCanDo — the other half. With tools enabled the
// question stands, and it names them so it is answerable.
func TestActClarifyNamesWhatSheCanDo(t *testing.T) {
ctx := context.Background()
h, st, _ := newClarifyHandler(t)
for _, name := range []string{"uptime", "disk"} {
if err := st.EnableTool(ctx, name, []string{"true"}, false, "test", h.now()); err != nil {
t.Fatal(err)
}
}
reply, asked := h.askClarify(ctx, clarifyDec(router.IntentAct, router.Slots{Text: "сделай это"}, "сделай это"))
if !asked {
t.Fatal("with tools enabled she should still ask")
}
for _, want := range []string{"uptime", "disk"} {
if !strings.Contains(reply, want) {
t.Errorf("reply = %q, want it to name %q", reply, want)
}
}
if h.clarifyStore.Get(voiceDialogueID, h.now()) == nil {
t.Error("the request must be parked so the answer can land")
}
}
// TestFnClarifyCapsTheListItRecites — a spoken sentence cannot carry twelve
// names, which is how many this deployment enables.
func TestFnClarifyCapsTheListItRecites(t *testing.T) {
allow := []string{"a", "b", "c", "d", "e", "f", "g", "h"}
reply, ask := fnClarify(allow, 1)
if !ask {
t.Fatal("a non-empty allowlist is still a question")
}
if strings.Contains(reply, "g") || strings.Contains(reply, "h") {
t.Errorf("reply = %q, want the list capped at %d", reply, namedActsCap)
}
if !strings.Contains(reply, "…") {
t.Errorf("reply = %q, want it to admit the list was cut", reply)
}
}