Artifact 84a6d7ef77d13fa22d71cc2b72f02d9bf83f920ad3663f69f9d598f48469f757:
- File
internal/harness/harness_test.go
— part of check-in
[c650d85c33]
at
2026-08-15 14:30:45
on branch trunk
— search: FTS5 over check-ins, wiki, tickets
External-content fts via triggers on search_doc (immutable-artifact upserts stay idempotent); token-quoted queries; /search page + op; required-input ops get explicit harness surfaces. (user: dmestas, size: 7622) [annotate] [blame] [check-ins using]
// Package harness runs the disposability loop (0001 §11): ingest a real // store, snapshot every rendered surface, rm -rf the derived data // directory, re-ingest from the store, snapshot again, and semantically // diff. Any diff is hostage state and fails the build. package harness import ( "encoding/json" "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" "craftdesign.group/quarry/internal/api" "craftdesign.group/quarry/internal/events" "craftdesign.group/quarry/internal/index" "craftdesign.group/quarry/internal/ingest" "craftdesign.group/quarry/internal/ops" "craftdesign.group/quarry/internal/semdiff" "craftdesign.group/quarry/internal/web" "craftdesign.group/quarry/internal/zitcli" ) type surface struct { path string kind semdiff.Kind } // surfaces enumerates what gets snapshotted: every HTML page, the // OpenAPI document, and — generated from the registry (0003 §4) — every // GET op without path wildcards, so new ops gain coverage automatically. func surfaces(t *testing.T) []surface { t.Helper() out := []surface{ {"/", semdiff.HTML}, {"/timeline", semdiff.HTML}, {"/wiki", semdiff.HTML}, {"/ticket", semdiff.HTML}, {"/search", semdiff.HTML}, {"/search?q=README", semdiff.HTML}, {"/api/v1/search?q=README", semdiff.JSON}, {"/api/v1/openapi.json", semdiff.JSON}, } r := registry(t, nil) for _, d := range r.All() { if d.Method == http.MethodGet && len(ops.Wildcards(d.Pattern)) == 0 && !hasRequired(d.In) { out = append(out, surface{d.Pattern, semdiff.JSON}) } } return out } // hasRequired reports whether an op's In needs input — such ops can't be // snapshotted bare and get explicit parameterized surfaces instead. func hasRequired(t reflect.Type) bool { for i := 0; i < t.NumField(); i++ { if strings.Contains(t.Field(i).Tag.Get("validate"), "required") { return true } } return false } func registry(t *testing.T, ix *index.Index) *ops.Registry { t.Helper() r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: "0.0.0-harness", Index: ix}); err != nil { t.Fatal(err) } return r } func zrun(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("zit", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("zit %v: %v\n%s", args, err, out) } } // buildStore creates the store under test: a zit repository with a few // commits. (Wiki/ticket surfaces join the snapshot set with the fossil- // converted fixture corpus once their pages exist.) func buildStore(t *testing.T) string { t.Helper() if _, err := exec.LookPath("zit"); err != nil { t.Skip("zit not on PATH") } repo := t.TempDir() zrun(t, repo, "init") for _, f := range []struct{ name, body, msg string }{ {"README.md", "# store under test\n", "add README"}, {"src/main.go", "package main\n", "add main"}, {"README.md", "# store under test\nmore\n", "extend README"}, } { if err := os.MkdirAll(filepath.Dir(filepath.Join(repo, f.name)), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(repo, f.name), []byte(f.body), 0o644); err != nil { t.Fatal(err) } zrun(t, repo, "add", f.name) zrun(t, repo, "commit", "-m", f.msg) } return repo } // ingestAndServe is the from-nothing build: embedded NATS and index both // live under dataDir (so the wipe destroys stream, cursors, and tables // alike), the store flows watcher → events → fold, and both registry // consumers stand up on the result. func ingestAndServe(t *testing.T, repo, dataDir string) *httptest.Server { t.Helper() ev, err := events.Start(filepath.Join(dataDir, "nats")) if err != nil { t.Fatal(err) } t.Cleanup(ev.Close) ix, err := index.Open(filepath.Join(dataDir, "index.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { ix.Close() }) cli := zitcli.New(repo) if _, err := ingest.NewPipeline(cli, ev, ix, "store").Sync(); err != nil { t.Fatal(err) } r := ops.NewRegistry() if err := api.RegisterAll(r, api.Deps{Version: "0.0.0-harness", Index: ix, Zit: cli}); err != nil { t.Fatal(err) } apiRouter, err := api.Router(r, "0.0.0-harness") if err != nil { t.Fatal(err) } mux := http.NewServeMux() mux.Handle("/api/", apiRouter) mux.Handle("/", web.Handler(r)) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } func snapshot(t *testing.T, srv *httptest.Server, path string) []byte { t.Helper() resp, err := srv.Client().Get(srv.URL + path) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("%s: status %d", path, resp.StatusCode) } body, err := io.ReadAll(resp.Body) if err != nil { t.Fatal(err) } return body } func TestDisposability(t *testing.T) { repo := buildStore(t) sf := surfaces(t) if len(sf) < 4 { t.Fatalf("suspiciously few surfaces: %v", sf) } dataA := filepath.Join(t.TempDir(), "data") if err := os.MkdirAll(dataA, 0o755); err != nil { t.Fatal(err) } first := ingestAndServe(t, repo, dataA) // Check-in surfaces are discovered from the live timeline; their // paths are content-addressed hashes, identical across rebuilds. var tl struct { Entries []struct { Hash string `json:"hash"` } `json:"entries"` } if err := json.Unmarshal(snapshot(t, first, "/api/v1/timeline"), &tl); err != nil { t.Fatal(err) } if len(tl.Entries) == 0 { t.Fatal("no check-ins to snapshot") } for _, e := range tl.Entries { sf = append(sf, surface{"/checkin/" + e.Hash, semdiff.HTML}, surface{"/checkin/" + e.Hash + "/diff", semdiff.HTML}, surface{"/api/v1/checkin/" + e.Hash, semdiff.JSON}) } // File surfaces: every file of the tip check-in, HTML and API — // these exercise the on-demand zit cat path on both sides of the // wipe. var tip struct { Files []struct { Name string `json:"name"` } `json:"files"` } if err := json.Unmarshal(snapshot(t, first, "/api/v1/checkin/"+tl.Entries[0].Hash), &tip); err != nil { t.Fatal(err) } if len(tip.Files) == 0 { t.Fatal("tip check-in lists no files") } for _, f := range tip.Files { sf = append(sf, surface{"/file/" + tl.Entries[0].Hash + "/" + f.Name, semdiff.HTML}, surface{"/api/v1/file/" + tl.Entries[0].Hash + "/" + f.Name, semdiff.JSON}) } before := map[string][]byte{} for _, s := range sf { before[s.path] = snapshot(t, first, s.path) } first.Close() // The wipe (0001 §11 step 2): every derived byte goes. if err := os.RemoveAll(dataA); err != nil { t.Fatal(err) } dataB := filepath.Join(t.TempDir(), "data") if err := os.MkdirAll(dataB, 0o755); err != nil { t.Fatal(err) } second := ingestAndServe(t, repo, dataB) for _, s := range sf { after := snapshot(t, second, s.path) d, err := semdiff.Diff(s.kind, before[s.path], after) if err != nil { t.Fatalf("%s: %v", s.path, err) } if d != "" { t.Errorf("%s: semantic diff after wipe-and-reingest — hostage state:\n%s", s.path, d) } } } // TestSemanticNotByteEquality pins why the harness diffs semantically: // a rendering-order change must pass, missing data must fail (0001 §11). func TestSemanticNotByteEquality(t *testing.T) { d, err := semdiff.Diff(semdiff.JSON, []byte(`{"b":1,"a":"2026-08-14T10:00:00+02:00"}`), []byte(`{"a":"2026-08-14 08:00:00Z","b":1}`)) if err != nil || d != "" { t.Fatalf("harmless drift flagged: %q %v", d, err) } d, err = semdiff.Diff(semdiff.JSON, []byte(`{"a":1,"b":2}`), []byte(`{"a":1}`)) if err != nil || d == "" { t.Fatal("dropped data not flagged") } if !strings.Contains(d, "b") { t.Fatalf("diff should name the loss: %s", d) } }