an act with a target the system cannot have does not run (V-634)

V-633 gave tools spoken aliases, so a Russian act reaches a tool. It resolves
the verb only: the rest of the sentence became argv. "перезагрузи роутер" ran
as systemctl restart роутер, which is a real tool, a real word and a target
that cannot exist on this box. She then reported systemctl's own confusion as
if she had tried something sensible, and on a destructive row she spent a
confirm turn on it first.

The executor now refuses, ahead of the confirm gate, and names the word it
could not place. The check is the script and not a word list: a unit, a
container, a host and a path are ASCII here, so a Cyrillic argv element means
the alias match swallowed the verb and handed on the next word.

Process rows only. An MCP argument is not a target — a task title is Russian
and always was — and a house row drops the spoken args already.

It does not try to guess the right target. Identity is Nexus's, and a target
Nexus resolves reaches Hexis through handleHexisAct before this executor is
asked.
This commit is contained in:
2026-08-06 20:05:34 +04:00
parent c8f74c39d6
commit d94ed2e630
5 changed files with 139 additions and 1 deletions
+53
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
@@ -320,3 +321,55 @@ func TestExecEmptyCmdRefuses(t *testing.T) {
t.Fatal("a row with no cmd ran a program named by the utterance")
}
}
// V-634. The alias match resolves the verb and hands on the next word of the
// sentence, so "перезагрузи роутер" became `systemctl restart роутер`: a real
// tool, a real word, and a target that cannot exist on this box.
func TestExecRefusesATargetTheSystemCannotHave(t *testing.T) {
api := fakeAPI{tools: map[string]ipc.Tool{
"restart": {Name: "restart", Cmd: []string{"systemctl", "restart"}, Status: "enabled"},
"drop": {Name: "drop", Cmd: []string{"dropdb"}, Destructive: true, Status: "enabled"},
}}
ran := false
e := NewExecutor(api, 0)
e.run = func(context.Context, []string) (string, error) { ran = true; return "ok", nil }
_, err := e.Exec(context.Background(), "restart", []string{"роутер"}, false)
if !errors.Is(err, ErrUnknownTarget) {
t.Fatalf("err = %v, want ErrUnknownTarget", err)
}
if ran {
t.Fatal("the program was called with a target that cannot exist")
}
// The word is in the error, because a reply naming no word sends him to the log.
if !strings.Contains(err.Error(), "роутер") {
t.Errorf("err %v does not name the word she could not place", err)
}
// Ahead of the confirm gate: asking about an act that cannot run spends a
// turn on nothing.
if _, err := e.Exec(context.Background(), "drop", []string{"база"}, false); !errors.Is(err, ErrUnknownTarget) {
t.Errorf("destructive row: err = %v, want ErrUnknownTarget before ErrNeedsConfirm", err)
}
// An ASCII target still runs, unchanged.
if _, err := e.Exec(context.Background(), "restart", []string{"nginx"}, false); err != nil {
t.Errorf("restart nginx: %v", err)
}
}
// An MCP argument is not a target. A task title is Russian and always was.
func TestExecMCPRowKeepsRussianArgs(t *testing.T) {
api := fakeAPI{tools: map[string]ipc.Tool{
"vikunja_create": {
Name: "vikunja_create", Status: "enabled",
Cmd: []string{"mcp", "vikunja", "create_task"},
},
}}
m := &fakeMCP{out: "создала"}
e := NewExecutor(api, time.Second).WithMCP(m)
if _, err := e.Exec(context.Background(), "vikunja_create", []string{"купить хлеб"}, false); err != nil {
t.Fatalf("exec: %v", err)
}
if len(m.args) != 1 || m.args[0] != "купить хлеб" {
t.Fatalf("args = %v, want the Russian title forwarded", m.args)
}
}