From 9f51596e2f1e03140615a3e090269cf4f321dadd Mon Sep 17 00:00:00 2001 From: claude Date: Tue, 4 Aug 2026 03:14:52 +0400 Subject: [PATCH] mavend: the simulator routes on the seeds the deploy loads (V-465) The seed path was relative to the working directory, which is cmd/mavend under `go test`. Every open failed, and the three scenarios replayed a whole scripted day against a classifier holding zero examples. They passed. A green simulator was proving something other than the routing the box runs, and a regression in the seed set could not have surfaced there. seedPath walks up to five levels to find models/seeds, so the daemon started from the repo root behaves exactly as before and a test started anywhere inside the tree finds the same files. All three scenarios still pass with 339 seeds loaded, so the outcome was not resting on the empty classifier. The new test asserts the count rather than logging it. A silent zero is the failure that hid here. --- cmd/mavend/simulator_test.go | 23 +++++++++++++++++++++++ cmd/mavend/voicewire.go | 33 ++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 5 deletions(-) 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)