llm browser

This commit is contained in:
developer committed 2026-10-19 15:25:27 +00:00
1 parent afed023152
commit a4dbb75195
39 files changed
+3709 -58

No files matched your search

+98
View File
@@ -15,6 +15,7 @@ import (
"time"
"git.k6n.net/mats/fullstack-template/internal/api"
"git.k6n.net/mats/fullstack-template/internal/browse"
"git.k6n.net/mats/fullstack-template/internal/items"
)
@@ -293,3 +294,100 @@ func TestCORSPreflight(t *testing.T) {
t.Errorf("Allow-Origin for unknown origin = %q, want none", got)
}
}
// fakeFetcher is the same in-memory browse.Fetcher as
// internal/browse/service_test.go, duplicated on purpose: test fakes stay
// package-local. A nil response simulates a connection failure.
type fakeFetcher struct {
pages map[string]*browse.Response
}
func (f fakeFetcher) Fetch(_ context.Context, url string) (*browse.Response, error) {
r, ok := f.pages[url]
if !ok {
return &browse.Response{Status: 404, FinalURL: url, ContentType: "text/plain", Body: []byte("not found")}, nil
}
if r == nil {
return nil, &browse.FetchError{URL: url, Err: errors.New("dial tcp: connection refused")}
}
return r, nil
}
func newBrowseTestServer(t *testing.T) (http.Handler, fakeFetcher) {
t.Helper()
pages := map[string]*browse.Response{}
add := func(url, ct, body string) {
pages[url] = &browse.Response{Status: 200, FinalURL: url, ContentType: ct, Body: []byte(body)}
}
add("https://site.test/", "text/html; charset=utf-8",
`<!doctype html><html><head><title>Site</title><meta name="description" content="d"></head>`+
`<body><h1>Hello</h1><p><a href="/docs">docs</a></p></body></html>`)
add("https://site.test/llms.txt", "text/markdown", "# Site\n\n> summary\n\n## Docs\n\n- [Docs](https://site.test/docs): the docs\n")
add("https://site.test/robots.txt", "text/plain", "User-agent: *\nAllow: /\n")
return api.New(api.Deps{
Logger: slog.New(slog.NewTextHandler(io.Discard, nil)),
Items: items.NewService(newFakeRepo()),
Browse: browse.NewService(fakeFetcher{pages: pages}),
DB: stubPinger{},
AllowedOrigins: []string{"http://localhost:5173"},
}), fakeFetcher{pages: pages}
}
func TestBrowseEndpoint(t *testing.T) {
h, _ := newBrowseTestServer(t)
rec := do(t, h, http.MethodGet, `/api/v1/browse?url=https%3A%2F%2Fsite.test%2F`, "")
if rec.Code != http.StatusOK {
t.Fatalf("browse = %d: %s", rec.Code, rec.Body.String())
}
var snap browse.Snapshot
decodeBody(t, rec, &snap)
if snap.FinalURL != "https://site.test/" || snap.Status != 200 {
t.Errorf("snapshot head = %d %s", snap.Status, snap.FinalURL)
}
if snap.Page == nil || snap.Page.Title != "Site" {
t.Errorf("page = %+v", snap.Page)
}
if !strings.Contains(snap.Markdown, "# Hello") || !strings.Contains(snap.Markdown, "[docs](https://site.test/docs)") {
t.Errorf("markdown = %q", snap.Markdown)
}
if snap.Files.LlmsTxt == nil || !strings.Contains(snap.Files.LlmsTxt.Content, "# Site") {
t.Errorf("llms.txt = %+v", snap.Files.LlmsTxt)
}
if len(snap.Checks) == 0 {
t.Error("checks empty")
}
// Bare-domain input normalizes to https on the wire.
rec = do(t, h, http.MethodGet, `/api/v1/browse?url=site.test`, "")
if rec.Code != http.StatusOK {
t.Fatalf("bare domain = %d: %s", rec.Code, rec.Body.String())
}
}
func TestBrowseEndpointErrors(t *testing.T) {
h, fetcher := newBrowseTestServer(t)
// Missing and malformed input map to validation_failed.
for _, target := range []string{"/api/v1/browse", "/api/v1/browse?url=", "/api/v1/browse?url=ftp://x.test"} {
rec := do(t, h, http.MethodGet, target, "")
if rec.Code != http.StatusBadRequest {
t.Errorf("GET %s = %d, want 400: %s", target, rec.Code, rec.Body.String())
}
}
// A dead site maps to 502 unavailable.
fetcher.pages["https://dead.test/"] = nil
rec := do(t, h, http.MethodGet, `/api/v1/browse?url=https%3A%2F%2Fdead.test%2F`, "")
if rec.Code != http.StatusBadGateway {
t.Errorf("dead site = %d, want 502: %s", rec.Code, rec.Body.String())
}
var errBody struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
decodeBody(t, rec, &errBody)
if errBody.Error.Code != "unavailable" {
t.Errorf("error code = %q, want unavailable", errBody.Error.Code)
}
}