Ctrl K
ComunidadGuías…

internal/autogen/groups_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"strings"
	"testing"
)

func emitGroups(s Settings, emitted []string) string {
	return emitGroupsCoexist(s, emitted, coexistSets{})
}

// emitGroupsCoexist is emitGroups with the zero-VRAM model classes (SAM, CPU-only
// TTS.cpp, CPU-only Parakeet ASR) that get their own never-evicting groups.
func emitGroupsCoexist(s Settings, emitted []string, cs coexistSets) string {
	var b strings.Builder
	emitGroupsAndListeners(&b, s, emitted, cs)
	return b.String()
}

func TestAutogen_emitGroups_defaultSingleGroup(t *testing.T) {
	out := emitGroups(Settings{}, []string{"a", "b"})
	if !strings.Contains(out, "  exclusive:\n") {
		t.Fatalf("expected single 'exclusive' group, got:\n%s", out)
	}
	if strings.Contains(out, "listeners:") {
		t.Fatalf("default must not emit listeners, got:\n%s", out)
	}
	for _, m := range []string{`- "a"`, `- "b"`} {
		if !strings.Contains(out, m) {
			t.Fatalf("missing member %s in:\n%s", m, out)
		}
	}
}

func TestAutogen_emitGroups_nameGlobSplit(t *testing.T) {
	s := Settings{Groups: []GroupSpec{
		{Name: "game-judge", Listen: "0.0.0.0:1251", Match: []string{"*-game", "*-judge"}},
		{Name: "assistant", Listen: "0.0.0.0:1250", Match: []string{"*"}},
	}}
	emitted := []string{"qwen", "qwen-game", "qwen-judge", "gemma"}
	out := emitGroups(s, emitted)

	// game/judge variants land in game-judge; the rest in assistant (catch-all).
	gameJudge := section(out, "game-judge:", "assistant:")
	assistant := section(out, "assistant:", "")
	for _, want := range []string{`- "qwen-game"`, `- "qwen-judge"`} {
		if !strings.Contains(gameJudge, want) {
			t.Fatalf("%s not in game-judge group:\n%s", want, out)
		}
	}
	for _, want := range []string{`- "qwen"`, `- "gemma"`} {
		if !strings.Contains(assistant, want) {
			t.Fatalf("%s not in assistant group:\n%s", want, out)
		}
	}
	// listeners map each address to its group, in config order.
	if !strings.Contains(out, `"0.0.0.0:1251":`) || !strings.Contains(out, "groups: [game-judge]") {
		t.Fatalf("missing game-judge listener:\n%s", out)
	}
	if !strings.Contains(out, `"0.0.0.0:1250":`) || !strings.Contains(out, "groups: [assistant]") {
		t.Fatalf("missing assistant listener:\n%s", out)
	}
}

func TestAutogen_emitGroups_unmatchedToDefault(t *testing.T) {
	s := Settings{Groups: []GroupSpec{
		{Name: "special", Listen: "0.0.0.0:1251", Match: []string{"*-game"}},
	}}
	out := emitGroups(s, []string{"qwen", "qwen-game"})
	def := section(out, "default:", "")
	if !strings.Contains(def, `- "qwen"`) {
		t.Fatalf("unmatched 'qwen' should fall into default group:\n%s", out)
	}
	if strings.Contains(out, "groups: [default]") {
		t.Fatalf("default group must not get a listener:\n%s", out)
	}
}

// section returns the substring of out from header up to (but excluding) next,
// or to the end when next is "".
func section(out, header, next string) string {
	i := strings.Index(out, header)
	if i < 0 {
		return ""
	}
	rest := out[i:]
	if next == "" {
		return rest
	}
	if j := strings.Index(rest[len(header):], next); j >= 0 {
		return rest[:len(header)+j]
	}
	return rest
}

// A coexist group emits swap:false (members stay resident together) while keeping
// exclusive:true (the group still evicts the other groups). Non-coexist groups in
// the same config are unaffected.
func TestAutogen_emitGroups_coexistGroup(t *testing.T) {
	s := Settings{Groups: []GroupSpec{
		{Name: "evalfleet", Match: []string{"*-game"}, Coexist: true},
		{Name: "exclusive", Match: []string{"*"}},
	}}
	out := emitGroups(s, []string{"qwen-game", "gemma-game", "qwen"})

	// End marker is a newline + indented key, not the bare word: "exclusive:" also
	// matches the "exclusive: true" line INSIDE the evalfleet block and truncates it.
	fleet := section(out, "  evalfleet:\n", "\n  exclusive:")
	if !strings.Contains(fleet, "swap: false") || !strings.Contains(fleet, "exclusive: true") {
		t.Fatalf("evalfleet must be swap:false + exclusive:true, got:\n%s", out)
	}
	for _, want := range []string{`- "qwen-game"`, `- "gemma-game"`} {
		if !strings.Contains(fleet, want) {
			t.Fatalf("%s not in evalfleet group:\n%s", want, out)
		}
	}
	excl := section(out, "\n  exclusive:\n", "")
	if !strings.Contains(excl, "swap: true") {
		t.Fatalf("non-coexist group must stay swap:true, got:\n%s", out)
	}
	if strings.Contains(out, "listeners:") {
		t.Fatalf("no group has a listen address; listeners must be omitted:\n%s", out)
	}
}

// A CPU-only TTS.cpp voice must NOT sit in an exclusive swap group: reading a
// reply aloud would evict the chat model that produced it (one GPU, one pool) even
// though the voice costs no VRAM. It lands in the persistent "tts" coexistence
// group instead, and stays visible on every listener.
func TestAutogen_emitGroups_cpuTTSCoexists(t *testing.T) {
	s := Settings{Groups: []GroupSpec{
		{Name: "assistant", Listen: "0.0.0.0:1250", Match: []string{"*"}},
	}}
	out := emitGroupsCoexist(s, []string{"qwen", "kokoro-q8"}, coexistSets{TTS: []string{"kokoro-q8"}})

	assistant := section(out, "  assistant:\n", "\n  tts:")
	if strings.Contains(assistant, `- "kokoro-q8"`) {
		t.Fatalf("CPU TTS model must not be in the exclusive group:\n%s", out)
	}
	if !strings.Contains(assistant, `- "qwen"`) {
		t.Fatalf("LLM missing from the exclusive group:\n%s", out)
	}

	tts := section(out, "  tts:\n", "\nlisteners:")
	for _, want := range []string{"swap: false", "exclusive: false", "persistent: true", `- "kokoro-q8"`} {
		if !strings.Contains(tts, want) {
			t.Fatalf("tts group missing %q:\n%s", want, out)
		}
	}

	// The tts group binds no port of its own, so the listener must list it too or
	// read-aloud disappears from that catalog.
	if !strings.Contains(out, "groups: [assistant, tts]") {
		t.Fatalf("tts group must be exposed on every listener:\n%s", out)
	}
}

// A user group already named "tts" wins the name; ours is renamed rather than
// emitted as a duplicate YAML key that would silently drop theirs.
func TestAutogen_emitGroups_cpuTTSNameCollision(t *testing.T) {
	s := Settings{Groups: []GroupSpec{{Name: "tts", Match: []string{"*"}}}}
	out := emitGroupsCoexist(s, []string{"qwen", "kokoro-q8"}, coexistSets{TTS: []string{"kokoro-q8"}})
	if !strings.Contains(out, "  tts-auto:\n") {
		t.Fatalf("expected renamed tts-auto group:\n%s", out)
	}
	if strings.Count(out, "  tts:\n") != 1 {
		t.Fatalf("user's tts group must stay the only \"tts\" key:\n%s", out)
	}
}

// A CPU-only Parakeet ASR model is the same story as a TTS voice: dictating must
// not evict the chat model the transcript is headed for. Each class gets its OWN
// coexistence group so an empty class emits nothing at all.
func TestAutogen_emitGroups_cpuASRCoexists(t *testing.T) {
	s := Settings{Groups: []GroupSpec{
		{Name: "assistant", Listen: "0.0.0.0:1250", Match: []string{"*"}},
	}}
	cs := coexistSets{TTS: []string{"kokoro-q8"}, ASR: []string{"parakeet-q8"}}
	out := emitGroupsCoexist(s, []string{"qwen", "kokoro-q8", "parakeet-q8"}, cs)

	assistant := section(out, "  assistant:\n", "\n  tts:")
	if strings.Contains(assistant, `- "parakeet-q8"`) {
		t.Fatalf("CPU ASR model must not be in the exclusive group:\n%s", out)
	}

	asr := section(out, "  asr:\n", "\nlisteners:")
	for _, want := range []string{"swap: false", "exclusive: false", "persistent: true", `- "parakeet-q8"`} {
		if !strings.Contains(asr, want) {
			t.Fatalf("asr group missing %q:\n%s", want, out)
		}
	}
	if !strings.Contains(out, "groups: [assistant, tts, asr]") {
		t.Fatalf("coexistence groups must be exposed on every listener:\n%s", out)
	}
	// No SAM models here, so no empty "sam" group and no dangling listener ref.
	if strings.Contains(out, "  sam:\n") {
		t.Fatalf("empty coexistence class must emit no group:\n%s", out)
	}
}