internal/autogen/hash_test.go
Rama main · réplica de lectura del repositorio original.
package autogen
import (
"fmt"
"os"
"path/filepath"
"sync"
"testing"
"gopkg.in/yaml.v3"
)
const generateFilePath = `E:\Apps\LLM\quartermaster\quartermaster-generate.yaml`
// TestEnsureConfig_HashGate exercises the full startup path: first call
// generates the config, second call (inputs unchanged) skips regeneration.
func TestEnsureConfig_HashGate(t *testing.T) {
if _, err := os.Stat(realModelsRoot); err != nil {
t.Skipf("models root %s absent", realModelsRoot)
}
if _, err := os.Stat(generateFilePath); err != nil {
t.Skipf("generate file %s absent", generateFilePath)
}
dir := t.TempDir()
out := filepath.Join(dir, "config.yaml")
regen, err := EnsureConfig(generateFilePath, out, "", nil)
if err != nil {
t.Fatalf("first EnsureConfig: %v", err)
}
if !regen {
t.Fatal("first call should regenerate")
}
if _, err := os.Stat(out); err != nil {
t.Fatalf("config not written: %v", err)
}
if _, err := os.Stat(out + hashCacheSuffix); err != nil {
t.Fatalf("hash cache not written: %v", err)
}
regen, err = EnsureConfig(generateFilePath, out, "", nil)
if err != nil {
t.Fatalf("second EnsureConfig: %v", err)
}
if regen {
t.Error("second call should skip regeneration (inputs unchanged)")
}
}
// TestEnsureConfig_Concurrent keeps the regen serialized: several goroutines
// (the download-completion hook, the watch-models poll, a UI save) can reach
// EnsureConfig in the same instant, and two of them interleaving their writes
// tore config.yaml. Every goroutine must come back clean and the config on
// disk must be a whole YAML document — not a splice of two writes.
func TestEnsureConfig_Concurrent(t *testing.T) {
dir := t.TempDir()
modelsDir := filepath.Join(dir, "models")
if err := os.MkdirAll(modelsDir, 0o755); err != nil {
t.Fatal(err)
}
generateFilePath := filepath.Join(dir, "quartermaster-generate.yaml")
if err := os.WriteFile(generateFilePath, []byte(fmt.Sprintf("settings:\n modelsRoot: %s\n", filepath.ToSlash(modelsDir))), 0o644); err != nil {
t.Fatal(err)
}
out := filepath.Join(dir, "config.yaml")
const n = 8
var wg sync.WaitGroup
errs := make([]error, n)
for i := 0; i < n; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if _, err := EnsureConfig(generateFilePath, out, "", nil); err != nil {
errs[i] = err
}
}(i)
}
wg.Wait()
for i, err := range errs {
if err != nil {
t.Fatalf("concurrent EnsureConfig %d: %v", i, err)
}
}
bytes, err := os.ReadFile(out)
if err != nil {
t.Fatalf("config not written: %v", err)
}
var doc map[string]any
if err := yaml.Unmarshal(bytes, &doc); err != nil {
t.Fatalf("config.yaml is not a whole YAML document (torn write?): %v\n%s", err, string(bytes))
}
if _, err := os.Stat(out + hashCacheSuffix); err != nil {
t.Fatalf("hash cache not written: %v", err)
}
}
func TestInputsHash_Stable(t *testing.T) {
if _, err := os.Stat(realModelsRoot); err != nil {
t.Skipf("models root %s absent", realModelsRoot)
}
a, err := InputsHash(realModelsRoot, []byte("control"))
if err != nil {
t.Fatal(err)
}
b, err := InputsHash(realModelsRoot, []byte("control"))
if err != nil {
t.Fatal(err)
}
if a != b {
t.Errorf("hash not stable: %s vs %s", a, b)
}
c, err := InputsHash(realModelsRoot, []byte("different"))
if err != nil {
t.Fatal(err)
}
if a == c {
t.Error("hash should change when control bytes change")
}
}