- remove the events/outages module, ArcGIS adapter, portal config and event statuses - add a configurable city dictionary/parser and make City part of StreetKey - seed: read city/city_name, namespace IDs by city, keep structures distinct - fix reliability: schema v2 with fail-closed load, deterministic ordering, input limits, config validation, atomic DB publish, LLM response caps - add read-only shadow mode comparing the old ES path with addrkit - breaking: registry.db must be re-seeded; config has no portal section
151 lines
4.4 KiB
Go
151 lines
4.4 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.totmin.ru/en2zmax/addrkit/internal/domain/match"
|
|
)
|
|
|
|
func writeFile(t *testing.T, dir, name, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("write %s: %v", name, err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func baseConfig(promptFields string) string {
|
|
return `{
|
|
"addr": ":6171",
|
|
"data_path": "registry.db",
|
|
"llm": { "enabled": true, "url": "http://llm"` + promptFields + ` }
|
|
}`
|
|
}
|
|
|
|
func TestLoadInlineSystemPrompt(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := writeFile(t, dir, "config.json", baseConfig(`, "system_prompt": "INLINE PROMPT"`))
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.LLM.SystemPrompt != "INLINE PROMPT" {
|
|
t.Errorf("SystemPrompt = %q", cfg.LLM.SystemPrompt)
|
|
}
|
|
}
|
|
|
|
func TestLoadSystemPromptFromFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
promptFile := writeFile(t, dir, "prompt.txt", "PROMPT FROM FILE\n")
|
|
path := writeFile(t, dir, "config.json", baseConfig(`, "system_prompt_file": "`+promptFile+`"`))
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if cfg.LLM.SystemPrompt != "PROMPT FROM FILE\n" {
|
|
t.Errorf("SystemPrompt = %q", cfg.LLM.SystemPrompt)
|
|
}
|
|
}
|
|
|
|
func TestLoadMissingSystemPromptFileFailsClosed(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := writeFile(t, dir, "config.json", baseConfig(`, "system_prompt_file": "`+filepath.Join(dir, "nope.txt")+`"`))
|
|
|
|
if _, err := Load(path); err == nil {
|
|
t.Fatal("expected error for missing system_prompt_file")
|
|
}
|
|
}
|
|
|
|
func TestDefaultsMatchMatcher(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := writeFile(t, dir, "config.json", `{"addr": ":6171", "data_path": "x.db"}`)
|
|
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
want := match.DefaultOptions()
|
|
if cfg.Resolve.FuzzyThreshold != want.FuzzyThreshold ||
|
|
cfg.Resolve.AmbiguityMargin != want.AmbiguityMargin ||
|
|
cfg.Resolve.MaxCandidates != want.MaxCandidates {
|
|
t.Fatalf("config defaults %+v do not match matcher defaults %+v", cfg.Resolve, want)
|
|
}
|
|
}
|
|
|
|
func TestRejectsBadResolveRanges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cases := map[string]string{
|
|
"threshold": `{"addr":":6171","data_path":"x.db","resolve":{"fuzzy_threshold":0}}`,
|
|
"margin": `{"addr":":6171","data_path":"x.db","resolve":{"ambiguity_margin":2}}`,
|
|
"candidates": `{"addr":":6171","data_path":"x.db","resolve":{"max_candidates":0}}`,
|
|
}
|
|
for name, body := range cases {
|
|
path := writeFile(t, dir, name+".json", body)
|
|
if _, err := Load(path); err == nil {
|
|
t.Errorf("%s: expected validation error", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCitiesAndDefaultCity(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := writeFile(t, dir, "config.json", `{
|
|
"addr": ":6171", "data_path": "x.db",
|
|
"cities": [
|
|
{"name": "Новосибирск", "aliases": ["нск", "новосибирская область"]},
|
|
{"name": "Бердск"}
|
|
],
|
|
"default_city": "Новосибирск"
|
|
}`)
|
|
cfg, err := Load(path)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(cfg.Cities) != 2 || cfg.DefaultCity != "Новосибирск" {
|
|
t.Fatalf("cities = %+v, default = %q", cfg.Cities, cfg.DefaultCity)
|
|
}
|
|
}
|
|
|
|
func TestRejectsUnknownDefaultCity(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := writeFile(t, dir, "config.json", `{
|
|
"addr": ":6171", "data_path": "x.db",
|
|
"cities": [{"name": "Бердск"}],
|
|
"default_city": "Новосибирск"
|
|
}`)
|
|
if _, err := Load(path); err == nil {
|
|
t.Fatal("expected error for default_city absent from cities")
|
|
}
|
|
}
|
|
|
|
func TestLoadCitiesFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := writeFile(t, dir, "cities.json", `[
|
|
{"name": "Новосибирск", "aliases": ["нск"]},
|
|
{"name": "Бердск"}
|
|
]`)
|
|
cities, err := LoadCitiesFile(path)
|
|
if err != nil {
|
|
t.Fatalf("LoadCitiesFile: %v", err)
|
|
}
|
|
if len(cities) != 2 || cities[0].Name != "Новосибирск" {
|
|
t.Fatalf("cities = %+v", cities)
|
|
}
|
|
}
|
|
|
|
func TestRejectsUnknownConfigFields(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// "portal" was removed with the events module; silently ignoring it would
|
|
// hide a stale deployment config.
|
|
path := writeFile(t, dir, "config.json",
|
|
`{"addr":":6171","data_path":"x.db","portal":{"url":"http://example"}}`)
|
|
if _, err := Load(path); err == nil {
|
|
t.Fatal("expected error for unknown config field")
|
|
}
|
|
}
|