+package main
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestRootRouteToolsPersistAndRemoveRoutes(t *testing.T) {
+ dir := t.TempDir()
+ server := newModelFake(t)
+ test := newDaemonMessageTest(t, dir, server)
+ root := test.loops[0]
+ source := "whatsapp:" + whatsappTestFamily.String()
+ arguments := `{"source":"` + source + `","agent":"Garden"}`
+ require.Equal(t, "Route set.", root.tools.run(t.Context(), "route", arguments))
+ require.Equal(t, source+" -> Garden", root.tools.run(t.Context(), "routes", `{}`))
+
+ info, err := os.Stat(filepath.Join(root.agent.dir, "routes.json"))
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
+ require.Contains(t, test.logs.String(), "route set kind=whatsapp name=Garden")
+ require.NotContains(t, test.logs.String(), whatsappTestFamily.String())
+ test.Close(t)
+
+ restored := newDaemonMessageTest(t, dir, server)
+ require.Equal(t, source+" -> Garden", restored.loops[0].tools.run(t.Context(), "routes", `{}`))
+ require.Equal(t, "Route removed.", restored.loops[0].tools.run(t.Context(), "unroute", `{"source":"`+source+`"}`))
+ require.Equal(t, "No routes.", restored.loops[0].tools.run(t.Context(), "routes", `{}`))
+ require.Contains(t, restored.logs.String(), "route removed kind=whatsapp")
+ restored.Close(t)
+
+ empty := newDaemonMessageTest(t, dir, server)
+ require.Equal(t, "No routes.", empty.loops[0].tools.run(t.Context(), "routes", `{}`))
+ require.Zero(t, server.Calls())
+}
+
+func TestRoutesRefuseUnknownSourcesAndNonchildren(t *testing.T) {
+ test := newDaemonMessageTest(t, t.TempDir(), newModelFake(t))
+ testCases := []struct {
+ name string
+ source string
+ agent string
+ cause string
+ }{
+ {
+ name: "unknown kind",
+ source: "mail:inbox",
+ agent: "Garden",
+ cause: "unknown source kind",
+ },
+ {
+ name: "missing kind",
+ source: whatsappTestFamily.String(),
+ agent: "Garden",
+ cause: "unknown source kind",
+ },
+ {
+ name: "empty chat",
+ source: "whatsapp:",
+ agent: "Garden",
+ cause: "invalid WhatsApp source",
+ },
+ {
+ name: "unknown chat kind",
+ source: "whatsapp:example@unknown",
+ agent: "Garden",
+ cause: "invalid WhatsApp source",
+ },
+ {
+ name: "root is not a child",
+ source: "whatsapp:" + whatsappTestFamily.String(),
+ agent: "OneChat",
+ cause: "must be a child",
+ },
+ {
+ name: "unknown child",
+ source: "whatsapp:" + whatsappTestFamily.String(),
+ agent: "Other",
+ cause: "must be a child",
+ },
+ {
+ name: "empty child",
+ source: "whatsapp:" + whatsappTestFamily.String(),
+ agent: "",
+ cause: "must be a child",
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ arguments := `{"source":"` + tc.source + `","agent":"` + tc.agent + `"}`
+ require.Contains(t, test.loops[0].tools.run(t.Context(), "route", arguments), tc.cause)
+ })
+ }
+
+ require.Contains(t, test.loops[0].tools.run(t.Context(), "unroute", `{"source":"mail:inbox"}`), "unknown source kind")
+ require.Equal(t, "No routes.", test.messages.routes.List())
+}
+
+func TestRouteToolsBelongOnlyToRoot(t *testing.T) {
+ test := newDaemonMessageTest(t, t.TempDir(), newModelFake(t))
+ for _, name := range []string{"route", "unroute", "routes"} {
+ require.Contains(t, promptToolNames(test.agents[0].agent.Tools()), name)
+ require.NotContains(t, promptToolNames(test.agents[1].agent.Tools()), name)
+ require.NotContains(t, promptToolNames(promptSubagentTools()), name)
+ require.Contains(t, test.loops[1].tools.run(t.Context(), name, `{}`), "unknown tool")
+ }
+
+ require.NotContains(t, promptChild("Garden", "OneChat", "Keep plans.", "/CHILD/instructions.md", ""), "Propose routing a source")
+}
+
+func TestWhatsAppStatusListsRoutes(t *testing.T) {
+ whatsapp, _, _ := whatsappStartTest(t, t.TempDir(), newTestClock())
+ test := newDaemonMessageTest(t, t.TempDir(), newModelFake(t))
+ whatsapp.routes = test.messages.routes
+ require.NoError(t, test.messages.routes.Set("whatsapp:"+whatsappTestFamily.String(), "Garden"))
+
+ var answer string
+
+ whatsapp.Command("status", func(text string) {
+ answer = text
+ })
+ require.Contains(t, answer, "WhatsApp is not linked.")
+ require.Contains(t, answer, "WhatsApp routes:\nwhatsapp:"+whatsappTestFamily.String()+" -> Garden")
+}
+
+func TestInvalidRouteFileStopsStartup(t *testing.T) {
+ testCases := []struct {
+ name string
+ text string
+ }{
+ {
+ name: "invalid JSON",
+ text: "{",
+ },
+ {
+ name: "unknown source",
+ text: `{"mail:inbox":"Garden"}`,
+ },
+ {
+ name: "empty destination",
+ text: `{"whatsapp:example@lid":""}`,
+ },
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ path := filepath.Join(t.TempDir(), "routes.json")
+ require.NoError(t, os.WriteFile(path, []byte(tc.text), 0o600))
+
+ _, err := openRoutes(path, discardLog())
+ require.Error(t, err)
+ })
+ }
+}