12c18dcf65
PR 114's review is anchored on internal/phraser/query_ru_v1.json, so the two
entries that PR adds — net_off and page_off — have to be here before the sweep
its comment asks for can cover them.
One conflict, in internal/phraser/query.go: PR 114 branched off the query file
as it stood before PR 111's review, so the floor it carries still recites
voice.weather.default_location at him and still puts {tail} in every net_empty
variant. Both are what that review threw out. Resolved to this branch's floor
plus PR 114's two new keys.
--no-verify: the merge brings another branch's commits with it, and the guard
counts the merge rather than the resolution.
181 lines
5.6 KiB
Go
181 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/config"
|
|
"github.com/kami/maven/internal/ipc"
|
|
"github.com/kami/maven/internal/phraser"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
func TestWireNetScanOffUnlessEnabled(t *testing.T) {
|
|
for name, cfg := range map[string]*config.Config{
|
|
"no block": {},
|
|
"written but dark": {NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"192.168.1.0/24"},
|
|
}},
|
|
"enabled but nothing to scan": {NetScan: &config.NetScanConfig{Enabled: true}},
|
|
"enabled but public": {NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"8.8.8.0/24"}, Enabled: true,
|
|
}},
|
|
"enabled but far too wide": {NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"10.0.0.0/8"}, Enabled: true,
|
|
}},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if w := wireNetScan(cfg, nil); w != nil {
|
|
t.Fatal("the scanner must not wire for this config")
|
|
}
|
|
})
|
|
}
|
|
|
|
var w *netWiring
|
|
if _, ok := w.scanSummary(context.Background()); ok {
|
|
t.Fatal("a nil wiring must not claim a query")
|
|
}
|
|
|
|
ok := wireNetScan(&config.Config{NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"192.168.1.0/24"}, Enabled: true,
|
|
}}, nil)
|
|
if ok == nil {
|
|
t.Fatal("a valid enabled block should wire")
|
|
}
|
|
}
|
|
|
|
// A loopback /32 with nothing listening on the scanned port: the summary must
|
|
// come back honest rather than inventing a host. This also exercises the real
|
|
// dialer end to end without touching anything outside this box.
|
|
func TestScanSummaryOnAnEmptyRange(t *testing.T) {
|
|
w := wireNetScan(&config.Config{NetScan: &config.NetScanConfig{
|
|
// Port 1 on loopback: nothing listens and the connection is refused
|
|
// immediately, so the scan is fast and touches only this machine.
|
|
Subnets: []string{"127.0.0.1/32"}, Ports: []int{1}, Rate: 1000, Enabled: true,
|
|
}}, nil)
|
|
if w == nil {
|
|
t.Fatal("wireNetScan returned nil")
|
|
}
|
|
out, claimed := w.scanSummary(context.Background())
|
|
if !claimed {
|
|
t.Fatal("the summary did not claim the turn")
|
|
}
|
|
if out == "" {
|
|
t.Fatal("empty summary")
|
|
}
|
|
// Persona: feminine self-reference, informal address, no pet names.
|
|
low := strings.ToLower(out)
|
|
for _, bad := range []string{"нашёл", "не смог ", "вы ", "ваш", "милый", "дорогой"} {
|
|
if strings.Contains(low, bad) {
|
|
t.Errorf("persona violation %q in %q", bad, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestIsNetworkQuery(t *testing.T) {
|
|
yes := []string{
|
|
"какие устройства в сети?",
|
|
"кто в сети?",
|
|
"просканируй сеть",
|
|
"покажи устройства в локальной сети",
|
|
"сколько машин в сети",
|
|
}
|
|
no := []string{
|
|
"",
|
|
"интернет не работает",
|
|
"сеть какая-то медленная",
|
|
"я в сети инстаграма",
|
|
"что включено дома?",
|
|
"напомни оплатить интернет",
|
|
}
|
|
for _, u := range yes {
|
|
if !isNetworkQuery(u) {
|
|
t.Errorf("isNetworkQuery(%q) = false, want true", u)
|
|
}
|
|
}
|
|
for _, u := range no {
|
|
if isNetworkQuery(u) {
|
|
t.Errorf("isNetworkQuery(%q) = true, want false", u)
|
|
}
|
|
}
|
|
}
|
|
|
|
// notingAPI counts the notes a scan writes, and remembers the last one.
|
|
type notingAPI struct {
|
|
ipc.CoreAPI
|
|
n int
|
|
last string
|
|
}
|
|
|
|
func (a *notingAPI) WriteNote(_ context.Context, _ time.Time, text string, _ []float32, _ string) (int64, error) {
|
|
a.n++
|
|
a.last = text
|
|
return int64(a.n), nil
|
|
}
|
|
|
|
// The spoken answer must not be a list of IP addresses. It goes to piper as
|
|
// well as to /chat, and six dotted quads read out as a digit stream is not an
|
|
// answer anybody can use. The addresses belong in the written record.
|
|
func TestScanSummarySpeaksACountAndWritesTheAddresses(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer ln.Close()
|
|
_, portStr, _ := net.SplitHostPort(ln.Addr().String())
|
|
port, _ := strconv.Atoi(portStr)
|
|
|
|
api := ¬ingAPI{}
|
|
w := wireNetScan(&config.Config{NetScan: &config.NetScanConfig{
|
|
Subnets: []string{"127.0.0.1/32"}, Ports: []int{port}, Rate: 1000, Enabled: true,
|
|
}}, api)
|
|
if w == nil {
|
|
t.Fatal("wireNetScan returned nil")
|
|
}
|
|
out, claimed := w.scanSummary(context.Background())
|
|
if !claimed {
|
|
t.Fatal("the summary did not claim the turn")
|
|
}
|
|
if strings.Contains(out, "127.0.0.1") || strings.Contains(out, portStr) {
|
|
t.Errorf("the spoken reply reads addresses out loud: %q", out)
|
|
}
|
|
if !strings.Contains(out, "нашла 1 устройство") {
|
|
t.Errorf("reply = %q, want a count", out)
|
|
}
|
|
if api.n != 1 {
|
|
t.Fatalf("wrote %d notes, want 1", api.n)
|
|
}
|
|
if !strings.Contains(api.last, "127.0.0.1") {
|
|
t.Errorf("the written record has no addresses: %q", api.last)
|
|
}
|
|
|
|
// A follow-up question inside the TTL reuses the answer: two questions in
|
|
// a row must not be two sweeps of the LAN.
|
|
if _, _ = w.scanSummary(context.Background()); api.n != 1 {
|
|
t.Errorf("a repeat question rescanned and rewrote the record (%d notes)", api.n)
|
|
}
|
|
}
|
|
|
|
// An unconfigured scanner names the gap instead of declining the turn.
|
|
//
|
|
// Falling through sent "какие устройства в сети?" to the search leg, which
|
|
// answered with a paragraph about routers in general — and put a question about
|
|
// his own LAN on an upstream engine, which the personal boundary exists to
|
|
// prevent (Vikunja #479).
|
|
func TestQueryNetworkNamesTheGapWhenNotConfigured(t *testing.T) {
|
|
h := &reactiveHandler{}
|
|
reply, ok := h.queryNetwork(context.Background(), &queryTurn{
|
|
dec: router.Decision{Utterance: "какие устройства в сети?"},
|
|
})
|
|
if !ok {
|
|
t.Fatal("an unconfigured scanner let the question fall through to search")
|
|
}
|
|
if !phraser.IsQ(phraser.QueryNetOff, nil, reply) {
|
|
t.Errorf("got %q, want the gap named", reply)
|
|
}
|
|
}
|