diff --git a/cmd/mavend/simulator_test.go b/cmd/mavend/simulator_test.go index 2d1b295..bdedaf6 100644 --- a/cmd/mavend/simulator_test.go +++ b/cmd/mavend/simulator_test.go @@ -1043,3 +1043,26 @@ func TestSimulatorRefusesBackwardsSteps(t *testing.T) { t.Errorf("the clock moved to %s on a refused step, it must stay at 09:00", got) } } + +// TestSimulatorRoutesWithTheDeployedSeeds — the scenarios must replay against +// the classifier the deploy runs, not an empty one. +// +// They did not. The seed path was relative to the working directory, which is +// cmd/mavend under `go test`, so every file failed to open and the whole +// simulator scored three green scenarios with zero examples loaded (Vikunja +// #465). The count is asserted rather than logged, because a silent zero is +// exactly the failure that hid here for as long as it did. +func TestSimulatorRoutesWithTheDeployedSeeds(t *testing.T) { + cls := router.NewClassifier(router.NewHashEmbedder(1024)) + seedClassifier(cls) + total := 0 + for _, intent := range cls.Intents() { + total += len(cls.Examples(intent)) + } + if total == 0 { + t.Fatalf("no seed examples loaded from %s — the simulator would route on nothing", seedPath()) + } + if len(cls.Intents()) != 7 { + t.Fatalf("seeded %d intents, want all 7", len(cls.Intents())) + } +} diff --git a/cmd/mavend/voicewire.go b/cmd/mavend/voicewire.go index 4df771b..8316490 100644 --- a/cmd/mavend/voicewire.go +++ b/cmd/mavend/voicewire.go @@ -398,11 +398,34 @@ func buildRouter(emb router.Embedder, acts router.ActMatcher, threshold float64, }) } -// seedDir is the directory containing intent seed files. Each file is named -// .txt and contains one training example per line (blank lines and -// lines starting with # are ignored). Relative to the working directory. +// seedDir is the directory containing intent seed files, relative to the repo +// root. Each file is named .txt and holds one training example per +// line (blank lines and lines starting with # are ignored). const seedDir = "models/seeds" +// seedPath resolves seedDir against the working directory, walking up until it +// finds it. The daemon runs from the repo root and the first candidate hits. +// +// A test does not: `go test ./cmd/mavend/` runs with the working directory at +// cmd/mavend, so every open failed and the simulator scenarios replayed a whole +// scripted day against a classifier holding zero examples (Vikunja #465). They +// passed, which is the part that matters — a green simulator was not exercising +// the routing the deploy runs, and a regression in the seed set could not have +// shown up there. +// +// Bounded at five levels, so a daemon started somewhere without the seeds logs +// the same failure it always did rather than walking to the filesystem root. +func seedPath() string { + dir := seedDir + for i := 0; i < 5; i++ { + if st, err := os.Stat(dir); err == nil && st.IsDir() { + return dir + } + dir = filepath.Join("..", dir) + } + return seedDir +} + // seedClassifier floors the embedded examples so the cold-boot path // doesn't return ErrNoIntents. Loads examples from seedDir — one file per // intent (act.txt, reminder.txt, fact.txt, note.txt, query.txt). When the @@ -427,11 +450,11 @@ func seedClassifier(c *router.Classifier) { } total += n } - log.Printf("voice: loaded %d seed examples from %s", total, seedDir) + log.Printf("voice: loaded %d seed examples from %s", total, seedPath()) } func loadSeedFile(c *router.Classifier, intent router.Intent) (int, error) { - path := filepath.Join(seedDir, string(intent)+".txt") + path := filepath.Join(seedPath(), string(intent)+".txt") f, err := os.Open(path) if err != nil { return 0, fmt.Errorf("open %s: %w", path, err)