Skip to content
quarry Artifact [46487e67ca]
Login

Artifact [46487e67ca]

Artifact 46487e67cadb5e9d30144c8e57bcc817b09351a585971fc988695ea1381c9c98:


package zitcli

import (
	"craftdesign.group/quarry/internal/fault"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"testing"
)

// fakeZit puts a script named "zit" first on PATH for this test.
func fakeZit(t *testing.T, script string) {
	t.Helper()
	dir := t.TempDir()
	p := filepath.Join(dir, "zit")
	if err := os.WriteFile(p, []byte("#!/bin/sh\n"+script+"\n"), 0o755); err != nil {
		t.Fatal(err)
	}
	t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH"))
	if _, err := exec.LookPath("zit"); err != nil {
		t.Fatalf("fake zit not on PATH: %v", err)
	}
}

// When the child dies partway through the stream the reader sees an EOF
// — but the EOF is the shape of the silence, not the reason for it. The
// reason is on the child's stderr, and reporting the symptom alone is
// how one store failing to ingest became ten thousand identical journal
// rows naming neither the store nor the cause.
func TestFramesReportsWhyTheChildStopped(t *testing.T) {
	fakeZit(t, `printf 'artifact aaaa 2\nhi\n'; echo "store is locked by another process" >&2; exit 1`)

	c := &Client{repo: "tcl"}
	var seen int
	_, err := c.framesEach(Cursor{}, false, func(Artifact) error { seen++; return nil })
	if err == nil {
		t.Fatal("a child that exited 1 mid-stream was reported as success")
	}
	if seen != 1 {
		t.Errorf("artifacts delivered = %d, want the one that arrived before the failure", seen)
	}
	got := err.Error()
	if !strings.Contains(got, "store is locked by another process") {
		t.Errorf("the child's stderr is missing, so the journal names no cause:\n  %s", got)
	}
	// The store is named in the JOURNAL, which is what this assertion
	// always meant — its own message says "the journal names no cause".
	// It used to read err.Error(), the string a CLIENT receives, and
	// those parted company when the store path became log-only: it
	// describes the forge's filesystem rather than the request, and a
	// 404 body carrying it was readable by anyone who could provoke the
	// error. Same intent, correct reader.
	var named bool
	for _, fld := range fault.FieldsOf(err) {
		if fld.Key == "store" && strings.Contains(fld.Value, "tcl") {
			named = true
		}
	}
	if !named {
		t.Errorf("the store is not named in the journal fields:\n  %s", got)
	}
	if strings.Contains(got, "tcl") {
		t.Errorf("the store path reached the CLIENT's error:\n  %s", got)
	}
}

// The other direction: when WE abandon the stream, the child's exit is
// our own signal and the parse error is the real story. Killing it and
// then blaming its status would invent a cause.
func TestFramesKeepsItsOwnErrorWhenItAbandonsTheStream(t *testing.T) {
	fakeZit(t, `printf 'nonsense line\n'; sleep 30`)

	c := &Client{repo: "tcl"}
	_, err := c.framesEach(Cursor{}, false, func(Artifact) error { return nil })
	if err == nil {
		t.Fatal("an unparseable frame was accepted")
	}
	if got := err.Error(); !strings.Contains(got, "unexpected frame line") {
		t.Errorf("the parse error was replaced by the kill's status:\n  %s", got)
	}
}

// A clean stream still parses, so the fix did not move the happy path.
func TestFramesStillReadsAGoodStream(t *testing.T) {
	fakeZit(t, `printf 'artifact aaaa 2\nhi\ncursor S123 V456\n'`)

	c := &Client{repo: "tcl"}
	var got []Artifact
	cur, err := c.framesEach(Cursor{}, false, func(a Artifact) error {
		got = append(got, a)
		return nil
	})
	if err != nil {
		t.Fatalf("a good stream failed: %v", err)
	}
	if len(got) != 1 || string(got[0].Bytes) != "hi" {
		t.Errorf("artifacts = %+v", got)
	}
	if cur.Since != "S123" || cur.Verify != "V456" {
		t.Errorf("cursor = %+v", cur)
	}
}