Skip to content
quarry Artifact [961c4f28da]
Login

Artifact [961c4f28da]

Artifact 961c4f28da8adaf4b69f8453eb662aad5292856df57cc16e7754ca67d2494332:


// Package harness runs the disposability loop (0001 §11): ingest real
// stores, snapshot every rendered surface, rm -rf the derived data
// directory, re-ingest from the stores, snapshot again, and semantically
// diff. Any diff is hostage state and fails the build. The fleet version
// runs two stores through one process — shared NATS, per-repo indexes —
// and asserts they cannot bleed into each other.
package harness

import (
	"encoding/json"
	"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 for one repo, plus the
// fleet-level pages.
func surfaces(repos []string) []surface {
	out := []surface{
		{"/", semdiff.HTML},
		{"/api/v1/repos", semdiff.JSON},
		{"/api/v1/version", semdiff.JSON},
		{"/api/v1/openapi.json", semdiff.JSON},
	}
	for _, r := range repos {
		p := "/r/" + r
		a := "/api/v1/r/" + r
		out = append(out,
			surface{p + "/timeline", semdiff.HTML},
			surface{p + "/wiki", semdiff.HTML},
			surface{p + "/wiki/Guide", semdiff.HTML},
			surface{p + "/wiki/Guide/history", semdiff.HTML},
			surface{a + "/wiki/Guide", semdiff.JSON},
			surface{a + "/wiki/Guide/history", semdiff.JSON},
			surface{p + "/ticket", semdiff.HTML},
			surface{p + "/search?q=README", semdiff.HTML},
			surface{a + "/timeline", semdiff.JSON},
			surface{a + "/wiki", semdiff.JSON},
			surface{a + "/ticket", semdiff.JSON},
			surface{a + "/search?q=README", semdiff.JSON},
		)
	}
	return out
}

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 one store under test: a zit repository with a few
// commits carrying a distinguishing seed word.
func buildStore(t *testing.T, seed string) 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 " + seed + "\n", "add README " + seed},
		{"src/main.go", "package " + seed + "\n", "add main"},
		{"README.md", "# store " + seed + "\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)
	}
	// Non-checkin artifacts too (zit#71 verbs): the disposability rule
	// covers every rendered surface, wiki and tickets included.
	wiki := exec.Command("zit", "wiki", "commit", "Guide", "--user", seed, "--mimetype", "text/x-markdown")
	wiki.Dir = repo
	wiki.Stdin = strings.NewReader("# Guide\n\n" + seed + " wiki body\n")
	if out, err := wiki.CombinedOutput(); err != nil {
		t.Fatalf("zit wiki commit: %v\n%s", err, out)
	}
	zrun(t, repo, "ticket", "new", "--user", seed, "title="+seed+" ticket", "status=Open")
	return repo
}

// ingestAndServe is the from-nothing build: one embedded NATS and one
// index per repo, all under dataDir (so the wipe destroys stream,
// cursors, and every table), each store flowing watcher → events → fold
// under its own project code, both registry consumers on top.
func ingestAndServe(t *testing.T, repos map[string]string, dataDir string) *httptest.Server {
	t.Helper()
	return ingestAndServeAuth(t, repos, dataDir, nil)
}

func ingestAndServeAuth(t *testing.T, repos map[string]string, dataDir string, verify api.Verify) *httptest.Server {
	t.Helper()
	ev, err := events.Start(filepath.Join(dataDir, "nats"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(ev.Close)
	deps := api.Deps{Version: "0.0.0-harness", Repos: map[string]*api.Repo{}}
	for name, path := range repos {
		ix, err := index.Open(filepath.Join(dataDir, name+".db"))
		if err != nil {
			t.Fatal(err)
		}
		t.Cleanup(func() { ix.Close() })
		cli := zitcli.New(path)
		pipe := ingest.NewPipeline(cli, ev, ix, name, "harness")
		deps.Repos[name] = &api.Repo{Index: ix, Zit: cli, Sync: pipe.Sync}
		if _, err := pipe.Sync(); err != nil {
			t.Fatal(err)
		}
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	apiRouter, err := api.Router(r, "0.0.0-harness", verify)
	if err != nil {
		t.Fatal(err)
	}
	mux := http.NewServeMux()
	mux.Handle("/api/", apiRouter)
	mux.Handle("/", web.Handler(r, nil, nil, web.Verify(verify)))
	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) {
	stores := map[string]string{
		"alpha": buildStore(t, "alphaseed"),
		"beta":  buildStore(t, "betaseed"),
	}
	sf := surfaces([]string{"alpha", "beta"})

	dataA := filepath.Join(t.TempDir(), "data")
	if err := os.MkdirAll(dataA, 0o755); err != nil {
		t.Fatal(err)
	}
	first := ingestAndServe(t, stores, dataA)

	// Per-repo check-in and file surfaces are discovered live; their
	// paths are content-addressed hashes, identical across rebuilds.
	for _, repo := range []string{"alpha", "beta"} {
		var tl struct {
			Entries []struct {
				Kind string `json:"kind"`
				Hash string `json:"hash"`
			} `json:"entries"`
		}
		if err := json.Unmarshal(snapshot(t, first, "/api/v1/r/"+repo+"/timeline?y=ci"), &tl); err != nil {
			t.Fatal(err)
		}
		if len(tl.Entries) == 0 {
			t.Fatalf("%s: no check-ins", repo)
		}
		for _, e := range tl.Entries {
			if e.Kind != "ci" {
				continue
			}
			sf = append(sf,
				surface{"/r/" + repo + "/checkin/" + e.Hash, semdiff.HTML},
				surface{"/r/" + repo + "/checkin/" + e.Hash + "/diff", semdiff.HTML},
				surface{"/api/v1/r/" + repo + "/checkin/" + e.Hash, semdiff.JSON})
		}
		var tip struct {
			Files []struct {
				Name string `json:"name"`
			} `json:"files"`
		}
		if err := json.Unmarshal(snapshot(t, first, "/api/v1/r/"+repo+"/checkin/"+tl.Entries[0].Hash), &tip); err != nil {
			t.Fatal(err)
		}
		for _, f := range tip.Files {
			sf = append(sf,
				surface{"/r/" + repo + "/file/" + tl.Entries[0].Hash + "/" + f.Name, semdiff.HTML},
				surface{"/api/v1/r/" + repo + "/file/" + tl.Entries[0].Hash + "/" + f.Name, semdiff.JSON})
		}
	}

	// Isolation: repo A's content is invisible from repo B's surfaces.
	var cross struct {
		Count int `json:"count"`
	}
	if err := json.Unmarshal(snapshot(t, first, "/api/v1/r/beta/search?q=alphaseed"), &cross); err != nil {
		t.Fatal(err)
	}
	if cross.Count != 0 {
		t.Fatalf("repo isolation broken: alpha content searchable from beta (%d hits)", cross.Count)
	}
	if err := json.Unmarshal(snapshot(t, first, "/api/v1/r/alpha/search?q=alphaseed"), &cross); err != nil {
		t.Fatal(err)
	}
	if cross.Count == 0 {
		t.Fatal("alpha content not searchable from alpha")
	}

	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 — both
	// indexes, the event stream, the cursors.
	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, stores, 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)
	}
}