Files
2026-08-11 10:40:10 +01:00

181 lines
5.6 KiB
Go

package api
import (
"os"
"regexp"
"sort"
"strings"
"testing"
)
// The OpenAPI specification is written by hand, so it can drift from the code.
// This test compares it against the routes the router actually registers, which
// is the thing the specification claims to describe.
//
// The route list below is checked against router.go itself, so a route added
// there without a corresponding entry here also fails.
// specPath is the specification under test.
const specPath = "../../docs/openapi.yaml"
// routerPath is the file whose route registrations are the source of truth.
const routerPath = "router.go"
// apiRoutePattern matches the API routes registered in the router.
var apiRoutePattern = regexp.MustCompile(`mux\.Handle\("(GET|POST|PATCH|DELETE|PUT) (/api/v1[^"]*|/health/[^"]*)"`)
// specPathPattern matches top-level path keys in the specification.
var specPathPattern = regexp.MustCompile(`(?m)^ (/(?:api/v1|health)[^:]*):`)
// specMethodPattern matches an HTTP method under a path.
var specMethodPattern = regexp.MustCompile(`(?m)^ (get|post|patch|delete|put):`)
func readFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(b)
}
// routerRoutes returns "METHOD /path" for every API route the router registers.
func routerRoutes(t *testing.T) map[string]bool {
t.Helper()
out := map[string]bool{}
for _, m := range apiRoutePattern.FindAllStringSubmatch(readFile(t, routerPath), -1) {
out[m[1]+" "+m[2]] = true
}
if len(out) == 0 {
t.Fatal("no routes were found in router.go; the matcher needs updating")
}
return out
}
// specRoutes returns "METHOD /path" for every operation in the specification.
func specRoutes(t *testing.T) map[string]bool {
t.Helper()
spec := readFile(t, specPath)
// Split the document at each path key, so methods are attributed correctly.
locs := specPathPattern.FindAllStringSubmatchIndex(spec, -1)
out := map[string]bool{}
for i, loc := range locs {
path := spec[loc[2]:loc[3]]
end := len(spec)
if i+1 < len(locs) {
end = locs[i+1][0]
}
for _, m := range specMethodPattern.FindAllStringSubmatch(spec[loc[1]:end], -1) {
out[strings.ToUpper(m[1])+" "+path] = true
}
}
if len(out) == 0 {
t.Fatal("no operations were found in the specification; the matcher needs updating")
}
return out
}
func TestOpenAPISpecMatchesRegisteredRoutes(t *testing.T) {
router := routerRoutes(t)
spec := specRoutes(t)
var missing, extra []string
for route := range router {
if !spec[route] {
missing = append(missing, route)
}
}
for route := range spec {
if !router[route] {
extra = append(extra, route)
}
}
sort.Strings(missing)
sort.Strings(extra)
if len(missing) > 0 {
t.Errorf("these routes exist but are not documented in %s:\n %s",
specPath, strings.Join(missing, "\n "))
}
if len(extra) > 0 {
t.Errorf("these routes are documented in %s but are not registered:\n %s",
specPath, strings.Join(extra, "\n "))
}
}
// The specification enumerates several closed sets that also exist as Go
// constants. A value added in one place and not the other would make the
// specification wrong in a way clients would act on.
func TestOpenAPIEnumsMatchTheDomain(t *testing.T) {
spec := readFile(t, specPath)
cases := []struct {
name string
values []string
}{
{"relationship types", []string{
"similar_to", "mentions", "involves", "supports", "contradicts", "updates",
"supersedes", "provides_context_for", "possibly_explains", "occurred_before",
"occurred_after", "derived_from", "same_entity_as",
}},
{"provenance", []string{
"explicit", "deterministic", "model_inferred", "user_confirmed", "user_rejected",
}},
{"entity types", []string{
"person", "organisation", "location", "product", "event", "date", "subject", "concept",
}},
{"source types", []string{
"note", "document", "json", "email", "transcript", "markdown",
}},
{"job types", []string{
"parse_item", "chunk_item", "generate_embeddings", "extract_entities",
"extract_claims", "discover_relationships", "reprocess_item",
"delete_item_data", "reembed_workspace",
}},
{"error codes", []string{
CodeInvalidRequest, CodeValidationFailed, CodeUnauthorised, CodeForbidden,
CodeNotFound, CodeDuplicate, CodeConflict, CodePayloadTooLarge,
CodeUnsupportedMedia, CodeRateLimited, CodeCSRFFailed, CodeDependencyDown,
CodeModelInvalid, CodeFeatureDisabled, CodeInternal,
}},
{"signal types", []string{
"semantic_similarity", "shared_entity", "alias_entity", "shared_concept",
"temporal_proximity", "shared_metadata", "claim_support", "claim_contradiction",
"model_classification", "user_confirmation", "user_rejection", "content_hash_match",
}},
}
for _, c := range cases {
for _, v := range c.values {
if !strings.Contains(spec, v) {
t.Errorf("%s: %q is not documented in the specification", c.name, v)
}
}
}
}
// The specification must not describe scores as probabilities, since the whole
// design rests on their not being probabilities.
func TestOpenAPIDescribesScoresHonestly(t *testing.T) {
spec := readFile(t, specPath)
for _, required := range []string{
"NOT a calibrated probability",
"Not a probability",
"not a fact",
} {
if !strings.Contains(spec, required) {
t.Errorf("the specification does not state %q", required)
}
}
// A claim that would be untrue.
for _, forbidden := range []string{"probability that", "confidence interval", "calibrated probability that"} {
if strings.Contains(strings.ToLower(spec), strings.ToLower(forbidden)) {
t.Errorf("the specification contains the misleading phrase %q", forbidden)
}
}
}