package definition_test import ( "strings" "testing" "github.com/krow/krow-backend/go-api/internal/definition" ) func TestFindSubagentCycle(t *testing.T) { for _, tc := range []struct { name string graph map[string][]string want string // "" means acyclic; otherwise a substring the report must contain }{ {"empty", map[string][]string{}, ""}, {"no edges", map[string][]string{"a": nil, "b": nil}, ""}, {"a chain is not a cycle", map[string][]string{ "a": {"b"}, "b": {"c"}, "c": nil, }, ""}, {"a diamond is not a cycle", map[string][]string{ "a": {"b", "c"}, "b": {"d"}, "c": {"d"}, "d": nil, }, ""}, {"self reference", map[string][]string{"a": {"a"}}, "a -> a"}, {"two-agent loop", map[string][]string{ "a": {"b"}, "b": {"a"}, }, "a -> b -> a"}, {"longer loop", map[string][]string{ "a": {"b"}, "b": {"c"}, "c": {"a"}, }, "a -> b -> c -> a"}, {"cycle not involving the first agent walked", map[string][]string{ "a": {"b"}, "b": {"c"}, "c": {"b"}, }, "b -> c -> b"}, {"an edge to an unknown agent is not a cycle", map[string][]string{ "a": {"nowhere"}, }, ""}, } { t.Run(tc.name, func(t *testing.T) { got := definition.FindSubagentCycle(tc.graph) switch { case tc.want == "" && got != "": t.Errorf("reported a cycle %q in an acyclic graph", got) case tc.want != "" && got == "": t.Errorf("missed the cycle; want something containing %q", tc.want) case tc.want != "" && !strings.Contains(got, tc.want): t.Errorf("cycle = %q, want it to contain %q", got, tc.want) } }) } } // The report must be stable: the same graph reported differently on different // runs is an error message nobody trusts, and map iteration order in Go is // deliberately random. func TestFindSubagentCycleIsDeterministic(t *testing.T) { graph := map[string][]string{ "e": {"f"}, "f": {"e"}, "a": {"b"}, "b": {"c"}, "c": {"a"}, "z": nil, "y": {"z"}, } first := definition.FindSubagentCycle(graph) if first == "" { t.Fatal("no cycle found in a graph with two") } for i := 0; i < 50; i++ { if got := definition.FindSubagentCycle(graph); got != first { t.Fatalf("run %d reported %q, first run reported %q — the report "+ "depends on map iteration order", i, got, first) } } } // A deep chain must not overflow the stack. An author supplies this graph. func TestFindSubagentCycleHandlesADeepChain(t *testing.T) { graph := map[string][]string{} const n = 5000 for i := 0; i < n; i++ { graph[itoa(i)] = []string{itoa(i + 1)} } graph[itoa(n)] = nil if got := definition.FindSubagentCycle(graph); got != "" { t.Errorf("reported a cycle %q in a %d-long chain", got, n) } // And the same chain closed into a loop is found. graph[itoa(n)] = []string{itoa(0)} if definition.FindSubagentCycle(graph) == "" { t.Error("missed a cycle closing a long chain") } } func itoa(i int) string { if i == 0 { return "0" } var b []byte for i > 0 { b = append([]byte{byte('0' + i%10)}, b...) i /= 10 } return string(b) }