Skip to content
quarry Artifact [84a6d7ef77]
Login

Artifact [84a6d7ef77]

Artifact 84a6d7ef77d13fa22d71cc2b72f02d9bf83f920ad3663f69f9d598f48469f757:


// 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)
	}
}