fix: harden API, DB schema, and CLI safety

- Add 'reminder' to glyph CHECK constraint (was accepted by parser but
  rejected by DB)
- Default serve bind to 127.0.0.1, add --host flag for LAN access
- Validate card_data as JSON in Store.Create/Update/Promote
- Return pagination envelope {data,total,limit,offset} from list endpoint
- Append absorb breadcrumb to source entity before soft-delete
- Add Levenshtein fuzzy match to catch command typos before routing to add
- Replace DDL string-matching migrations with versioned schema_version table
- Update web UI and API tests for envelope response format
This commit is contained in:
2026-05-19 18:30:17 -04:00
parent babf1d6620
commit e09919b679
9 changed files with 243 additions and 89 deletions
+21 -14
View File
@@ -25,6 +25,20 @@ func testServer(t *testing.T) (*httptest.Server, *db.Store) {
return srv, store
}
type listEnvelope struct {
Data []EntityResponse `json:"data"`
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
func decodeList(t *testing.T, resp *http.Response) []EntityResponse {
t.Helper()
var env listEnvelope
json.NewDecoder(resp.Body).Decode(&env)
return env.Data
}
func postJSON(t *testing.T, srv *httptest.Server, path string, body any) *http.Response {
t.Helper()
b, err := json.Marshal(body)
@@ -157,8 +171,7 @@ func TestListEntities_Default(t *testing.T) {
}
defer resp.Body.Close()
var entities []EntityResponse
json.NewDecoder(resp.Body).Decode(&entities)
entities := decodeList(t, resp)
if len(entities) != 2 {
t.Fatalf("expected 2, got %d", len(entities))
}
@@ -175,8 +188,7 @@ func TestListEntities_FilterTag(t *testing.T) {
}
defer resp.Body.Close()
var entities []EntityResponse
json.NewDecoder(resp.Body).Decode(&entities)
entities := decodeList(t, resp)
if len(entities) != 1 {
t.Fatalf("expected 1, got %d", len(entities))
}
@@ -198,8 +210,7 @@ func TestListEntities_CardsOnly(t *testing.T) {
}
defer resp.Body.Close()
var entities []EntityResponse
json.NewDecoder(resp.Body).Decode(&entities)
entities := decodeList(t, resp)
if len(entities) != 1 {
t.Fatalf("expected 1 card, got %d", len(entities))
}
@@ -215,16 +226,14 @@ func TestListEntities_Pagination(t *testing.T) {
if err != nil {
t.Fatal(err)
}
var page1 []EntityResponse
json.NewDecoder(resp.Body).Decode(&page1)
page1 := decodeList(t, resp)
resp.Body.Close()
resp, err = http.Get(srv.URL + "/api/entities?limit=2&offset=2")
if err != nil {
t.Fatal(err)
}
var page2 []EntityResponse
json.NewDecoder(resp.Body).Decode(&page2)
page2 := decodeList(t, resp)
resp.Body.Close()
if len(page1) != 2 || len(page2) != 2 {
@@ -517,8 +526,7 @@ func TestAbsorbEntity_Success(t *testing.T) {
if err != nil {
t.Fatal(err)
}
var entities []EntityResponse
json.NewDecoder(listResp.Body).Decode(&entities)
entities := decodeList(t, listResp)
listResp.Body.Close()
for _, ent := range entities {
if ent.ID == source.ID {
@@ -686,8 +694,7 @@ func TestListEntities_TitleInResponse(t *testing.T) {
}
defer resp.Body.Close()
var entities []EntityResponse
json.NewDecoder(resp.Body).Decode(&entities)
entities := decodeList(t, resp)
if len(entities) != 1 {
t.Fatalf("expected 1, got %d", len(entities))
}