Skip to content
quarry Artifact [705a1a7451]
Login

Artifact [705a1a7451]

Artifact 705a1a7451e4c8ba252bfeb08616cfb1726ddc4bdb91943741df801ea38059c0:


// Package harness runs the disposability loop (0001 §11): render every
// snapshot surface, wipe derived state, rebuild from scratch, render
// again, and semantically diff. Today "wipe" rebuilds the registry and
// handlers (no persistent index exists yet); when phase-1 ingest lands it
// becomes rm -rf of the data directory plus re-ingest of the fixture
// corpus — this file's loop and comparison stay as they are.
package harness

import (
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/api"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/semdiff"
	"craftdesign.group/quarry/internal/web"
)

type surface struct {
	path string
	kind semdiff.Kind
}

// buildServer stands the whole system up from nothing — the "ingest" of
// the phase-0 loop.
func buildServer(t *testing.T) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, "0.0.0-harness"); 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
}

// surfaces enumerates what gets snapshotted: every HTML page, the OpenAPI
// document, and — generated from the registry (0003 §4) — every GET op
// that needs no input. New parameterless ops gain coverage automatically;
// parameterized ops join when the fixture corpus drives them.
func surfaces(t *testing.T) []surface {
	t.Helper()
	out := []surface{
		{"/", semdiff.HTML},
		{"/api/v1/openapi.json", semdiff.JSON},
	}
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, "0.0.0-harness"); err != nil {
		t.Fatal(err)
	}
	for _, d := range r.All() {
		if d.Method == http.MethodGet && d.In.NumField() == 0 {
			out = append(out, surface{d.Pattern, semdiff.JSON})
		}
	}
	return out
}

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) {
	sf := surfaces(t)
	if len(sf) < 3 {
		t.Fatalf("suspiciously few surfaces: %v", sf)
	}

	before := map[string][]byte{}
	first := buildServer(t)
	for _, s := range sf {
		before[s.path] = snapshot(t, first, s.path)
	}
	first.Close() // the wipe: nothing survives but the stores (none yet)

	second := buildServer(t)
	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 rebuild — 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)
	}
}