- match: require candidate ordinal to equal the query ordinal in fuzzy search (deterministic typo recall 73% -> 80%, no wrong houses) - resolver: compatibleVariant guard — LLM may only fix street spelling, keeping the same words, ordinal and house unit; damerau distance for transpositions - llm: prompt moved out of code (go:embed default_prompt.txt) with llm.system_prompt / llm.system_prompt_file overrides; guided JSON parsing tolerant of thinking blocks - address: recognize ordinals appearing after the street name - tests: first coverage for the LLM adapter and config prompt loading; README Measured (sample 200): deterministic 94.7-95.8% exact / 0 wrong across 4 seeds; LLM+guard typo 90.7-89.8% / 0 wrong. LLM fallback latency ~1.2-1.9s.
129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.totmin.ru/en2zmax/addrkit/internal/domain/address"
|
|
"git.totmin.ru/en2zmax/addrkit/internal/domain/match"
|
|
"git.totmin.ru/en2zmax/addrkit/internal/domain/outages"
|
|
)
|
|
|
|
func testResolver(t *testing.T) *match.Resolver {
|
|
t.Helper()
|
|
var entries []address.Entry
|
|
for _, a := range []string{"Станционная улица 1/1", "2-я Станционная улица 1/1", "Искитимская улица 5"} {
|
|
p := address.Parse(a)
|
|
entries = append(entries, address.Entry{
|
|
ID: a, Address: a, District: "Тестовый",
|
|
Ordinal: p.Ordinal, NameKey: p.NameKey, StreetType: p.StreetType, StreetKey: p.StreetKey,
|
|
House: p.House, HouseFrac: p.HouseFrac, HouseLetter: p.HouseLetter,
|
|
})
|
|
}
|
|
return match.NewResolver(match.NewIndex("test-version", entries, match.DefaultOptions()), nil)
|
|
}
|
|
|
|
type fakeSource struct {
|
|
list []outages.Outage
|
|
err error
|
|
}
|
|
|
|
func (f fakeSource) Query(context.Context, string) ([]outages.Outage, error) {
|
|
return f.list, f.err
|
|
}
|
|
|
|
func newServer(t *testing.T, src fakeSource) *httptest.Server {
|
|
t.Helper()
|
|
srv := New(testResolver(t), src, slog.New(slog.NewTextHandler(io.Discard, nil)), time.UTC)
|
|
ts := httptest.NewServer(srv.Handler())
|
|
t.Cleanup(ts.Close)
|
|
return ts
|
|
}
|
|
|
|
func postJSON(t *testing.T, url, body string) *http.Response {
|
|
t.Helper()
|
|
resp, err := http.Post(url, "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatalf("post: %v", err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
func TestResolveEndpoint(t *testing.T) {
|
|
ts := newServer(t, fakeSource{})
|
|
resp := postJSON(t, ts.URL+"/v1/resolve", `{"address":"2-я Станционная 1/1"}`)
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("status = %d", resp.StatusCode)
|
|
}
|
|
var res match.Result
|
|
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if res.Status != address.StatusFound || res.Address != "2-я Станционная улица 1/1" {
|
|
t.Fatalf("unexpected result: %+v", res)
|
|
}
|
|
}
|
|
|
|
func TestOutagesEndpointFiltersHouse(t *testing.T) {
|
|
src := fakeSource{list: []outages.Outage{
|
|
{Address: "Станционная улица 1/1", System: "Теплоснабжение"},
|
|
{Address: "Станционная улица 1/2", System: "Теплоснабжение"},
|
|
}}
|
|
ts := newServer(t, src)
|
|
resp := postJSON(t, ts.URL+"/v1/outages", `{"address":"станционная 1/1"}`)
|
|
defer resp.Body.Close()
|
|
|
|
var out outagesResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if out.Status != address.StatusFound {
|
|
t.Fatalf("status = %q, want found", out.Status)
|
|
}
|
|
if len(out.Outages) != 1 || out.Outages[0].Address != "Станционная улица 1/1" {
|
|
t.Fatalf("house filter failed: %+v", out.Outages)
|
|
}
|
|
}
|
|
|
|
func TestResolveValidation(t *testing.T) {
|
|
ts := newServer(t, fakeSource{})
|
|
resp := postJSON(t, ts.URL+"/v1/resolve", `{}`)
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestReadyzAndVersion(t *testing.T) {
|
|
ts := newServer(t, fakeSource{})
|
|
|
|
resp, err := http.Get(ts.URL + "/v1/readyz")
|
|
if err != nil {
|
|
t.Fatalf("readyz: %v", err)
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("readyz status = %d", resp.StatusCode)
|
|
}
|
|
|
|
resp, err = http.Get(ts.URL + "/v1/version")
|
|
if err != nil {
|
|
t.Fatalf("version: %v", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
var v map[string]string
|
|
_ = json.NewDecoder(resp.Body).Decode(&v)
|
|
if v["data_version"] != "test-version" {
|
|
t.Fatalf("version = %q", v["data_version"])
|
|
}
|
|
}
|