Artifact 43bcfee6f6e74e88d50f0bf82b192cba934d9e4bf102e1c8d33e9945faa22b9f:
- File
internal/harness/harness_test.go
— part of check-in
[b0dc69c9a7]
at
2026-08-15 11:14:05
on branch trunk
— events: embedded NATS leg of the ingest pipeline (0001 §4)
Watcher publishes parsed artifacts to zit.evt.
.artifact; durable pull consumer folds; cursors in KV; reset event on store rewrite.(user: dmestas, size: 5702) [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 ( "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "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}, {"/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 { out = append(out, surface{d.Pattern, semdiff.JSON}) } } return out } 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() }) if _, err := ingest.NewPipeline(zitcli.New(repo), ev, ix, "store").Sync(); err != nil { t.Fatal(err) } r := registry(t, ix) 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) 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) } }