Artifact 7c347ddddd56a36ccde1778d97b8a3d3b8b059cfe7d379bdbc9a1ba36016f1ae:
- File
internal/ingest/derive_test.go
— part of check-in
[06942dcc5d]
at
2026-08-24 16:57:41
on branch trunk
— Close the push gap: derived events all come from the fold
The index holds the state an op would have read, so a pushed artifact gets the same event a forge op would have raised. Ticket transitions and landing verdicts compare folded state before and after; forum, wiki and technote need only the artifact or one lookup.
Op sites stop raising derived events, so 2.1's split is now enforced rather than documented. (user: dmestas, size: 9605) [annotate] [blame] [check-ins using]
package ingest import ( "encoding/json" "testing" "time" "github.com/nats-io/nats.go" "craftdesign.group/quarry/internal/artifact" "craftdesign.group/quarry/internal/evt" "craftdesign.group/quarry/internal/index" ) func kinds(rs []raised) []evt.Kind { out := make([]evt.Kind, 0, len(rs)) for _, r := range rs { out = append(out, r.Kind) } return out } func TestDeriveReadsTheTagOperator(t *testing.T) { for _, tc := range []struct { name string art *artifact.Artifact prior index.Prior want []evt.Kind }{ // A check-in's own sym- tag raises nothing extra: branch.moved // for that check-in comes from the computed branch after the // fold, and raising it here too would double it. {"a check-in on a branch", &artifact.Artifact{ Type: artifact.Manifest, Tags: []artifact.Tag{{Name: "*sym-trunk"}}, }, index.Prior{}, []evt.Kind{evt.CheckinCreated}}, {"a check-in with a plain tag", &artifact.Artifact{ Type: artifact.Manifest, Tags: []artifact.Tag{{Name: "*sym-trunk"}, {Name: "+release"}}, }, index.Prior{}, []evt.Kind{evt.CheckinCreated, evt.TagCreated}}, {"a control artifact naming a branch", &artifact.Artifact{ Type: artifact.Control, Tags: []artifact.Tag{{Name: "*sym-feature", Target: "abc"}}, }, index.Prior{}, []evt.Kind{evt.BranchCreated}}, {"a control artifact cancelling a tag", &artifact.Artifact{ Type: artifact.Control, Tags: []artifact.Tag{{Name: "-release", Target: "abc"}}, }, index.Prior{}, []evt.Kind{evt.TagRemoved}}, // Cancelling a branch tag is not closing the branch, and nothing // in the vocabulary names it — so it raises nothing rather than // something approximate. {"a cancelled branch tag", &artifact.Artifact{ Type: artifact.Control, Tags: []artifact.Tag{{Name: "-sym-old", Target: "abc"}}, }, index.Prior{}, nil}, // Everything with an op behind it is raised at that op, which // knows the previous state the artifact does not carry. {"a new wiki page", &artifact.Artifact{ Type: artifact.Wiki, WikiTitle: "Home", Body: []byte("hi"), }, index.Prior{}, []evt.Kind{evt.WikiCreated}}, {"an edit to a known page", &artifact.Artifact{ Type: artifact.Wiki, WikiTitle: "Home", Body: []byte("hi"), }, index.Prior{WikiKnown: map[string]bool{"Home": true}}, []evt.Kind{evt.WikiEdited}}, {"an empty body deletes the page", &artifact.Artifact{ Type: artifact.Wiki, WikiTitle: "Home", }, index.Prior{WikiKnown: map[string]bool{"Home": true}}, []evt.Kind{evt.WikiDeleted}}, // A thread opener has no G card; that is the whole distinction, // and it needs no index state. {"a forum thread", &artifact.Artifact{ Type: artifact.Forum, ThreadTitle: "hello", }, index.Prior{}, []evt.Kind{evt.ForumPosted}}, {"a forum reply", &artifact.Artifact{ Type: artifact.Forum, ThreadRoot: "root", InReplyTo: "root", }, index.Prior{}, []evt.Kind{evt.ForumReplied}}, {"a landing opener", &artifact.Artifact{ Type: artifact.Forum, ThreadTitle: "land it", Body: []byte("please land\n\nzit-landing: 1\n"), }, index.Prior{}, []evt.Kind{evt.LandingOpened}}, // A reply cannot tell from its own body which kind of thread it // is in — only the opener carries the marker. {"a comment on a landing", &artifact.Artifact{ Type: artifact.Forum, ThreadRoot: "root", Body: []byte("looks good"), }, index.Prior{LandingThreads: map[string]bool{"root": true}}, []evt.Kind{evt.LandingCommented}}, // A verdict claims a state; the fold computes it. Named after. {"a verdict reply", &artifact.Artifact{ Type: artifact.Forum, ThreadRoot: "root", Body: []byte("zit-landing-state: accepted\n"), }, index.Prior{LandingThreads: map[string]bool{"root": true}}, nil}, {"a ticket change", &artifact.Artifact{ Type: artifact.Ticket, TicketID: "t1", }, index.Prior{}, nil}, {"a malformed tag name", &artifact.Artifact{ Type: artifact.Control, Tags: []artifact.Tag{{Name: "+"}}, }, index.Prior{}, nil}, } { got := kinds(derive("h1", tc.art, tc.prior)) if len(got) != len(tc.want) { t.Errorf("%s: got %v, want %v", tc.name, got, tc.want) continue } for i := range got { if got[i] != tc.want[i] { t.Errorf("%s: got %v, want %v", tc.name, got, tc.want) break } } } } func TestDeriveCarriesTheBranchAndTarget(t *testing.T) { rs := derive("h1", &artifact.Artifact{ Type: artifact.Manifest, User: "dan", Comment: "fix it\n\nmore", Tags: []artifact.Tag{{Name: "*sym-feature"}}, }, index.Prior{}) if got := rs[0].Payload["branch"]; got != "feature" { t.Errorf("checkin branch = %v, want feature", got) } if got := rs[0].Payload["summary"]; got != "fix it" { t.Errorf("summary = %v, want the first line only", got) } rs = derive("ctl", &artifact.Artifact{ Type: artifact.Control, Tags: []artifact.Tag{{Name: "+release", Target: "target-hash"}}, }, index.Prior{}) if got := rs[0].Payload["target"]; got != "target-hash" { t.Errorf("target = %v, want the artifact the tag applies to", got) } } // The pipeline end to end: a real store, a real fold, and the events a // dagnats subject trigger would actually see. // // The bootstrap round is drained FIRST and deliberately unasserted. A // first sync rebuilds from the store's census rather than the bus, and a // rebuild is not news — `checkin.created` for a history that already // existed would be the forge telling every subscriber that five years of // commits just landed. func TestFoldRaisesDerivedEventsOnce(t *testing.T) { pipe, _, repo := testPipeline(t) // Twice: the first round rebuilds from the census (no cursor), and // the second settles the once-per-process tip check. Only after that // does a round take the bus, which is the path under test. for i := 0; i < 2; i++ { if _, err := pipe.Sync(); err != nil { t.Fatal(err) } } sub, err := pipe.ev.JS().SubscribeSync("quarry.evt.test.>", nats.DeliverNew()) if err != nil { t.Fatal(err) } defer sub.Unsubscribe() commitFile(t, repo, "c.txt", "gamma\n", "add c") commitFile(t, repo, "d.txt", "delta\n", "add d") if _, err := pipe.Sync(); err != nil { t.Fatal(err) } drain := func() map[evt.Kind]int { seen := map[evt.Kind]int{} for { m, err := sub.NextMsg(400 * time.Millisecond) if err != nil { return seen } var e struct { Kind evt.Kind Repo string } if err := json.Unmarshal(m.Data, &e); err != nil { t.Fatalf("body: %v", err) } if e.Repo != "test" { t.Errorf("event carried repo %q", e.Repo) } seen[e.Kind]++ } } first := drain() if first[evt.CheckinCreated] != 2 { t.Fatalf("two new check-ins produced %d checkin.created (%v)", first[evt.CheckinCreated], first) } if first[evt.BranchMoved] == 0 { t.Errorf("no branch.moved for check-ins on trunk (%v)", first) } // Folding again must raise NOTHING. Folds are idempotent; raising is // not, and at-least-once redelivery would otherwise tell every // subscriber the history had just happened again. if _, err := pipe.Sync(); err != nil { t.Fatal(err) } if again := drain(); len(again) != 0 { t.Errorf("a refold re-raised %v", again) } // A genuinely new check-in still raises. commitFile(t, repo, "e.txt", "epsilon\n", "add e") if _, err := pipe.Sync(); err != nil { t.Fatal(err) } if third := drain(); third[evt.CheckinCreated] != 1 { t.Errorf("a new check-in raised %v", third) } } // The gap RFC 0005 named, closed and proved: a ticket changed with no // forge op behind it still raises ticket.closed rather than a shrug. // // The pipeline's own zit client IS the push case — these artifacts are // made by the CLI, exactly as a push from a clone delivers them, with no // api op anywhere. The status before the change lives only in the index, // which is why reading it there is what makes this work. func TestPushedTicketChangeNamesItsTransition(t *testing.T) { pipe, _, _ := testPipeline(t) for i := 0; i < 2; i++ { if _, err := pipe.Sync(); err != nil { t.Fatal(err) } } sub, err := pipe.ev.JS().SubscribeSync("quarry.evt.test.ticket.*", nats.DeliverNew()) if err != nil { t.Fatal(err) } defer sub.Unsubscribe() next := func() evt.Kind { t.Helper() m, err := sub.NextMsg(3 * time.Second) if err != nil { t.Fatalf("expected a ticket event: %v", err) } var e struct{ Kind evt.Kind } if err := json.Unmarshal(m.Data, &e); err != nil { t.Fatal(err) } return e.Kind } id, _, err := pipe.cli.TicketNew("dan", map[string]string{ "title": "it breaks", "status": "Open", }) if err != nil { t.Skipf("zit ticket unavailable: %v", err) } if _, err := pipe.Sync(); err != nil { t.Fatal(err) } if got := next(); got != evt.TicketOpened { t.Fatalf("a new ticket raised %s, want ticket.opened", got) } for _, tc := range []struct { status string want evt.Kind }{ {"Closed", evt.TicketClosed}, {"Open", evt.TicketReopened}, } { if _, err := pipe.cli.TicketSet(id, "dan", map[string]string{"status": tc.status}); err != nil { t.Fatal(err) } if _, err := pipe.Sync(); err != nil { t.Fatal(err) } if got := next(); got != tc.want { t.Errorf("status -> %s raised %s, want %s", tc.status, got, tc.want) } } // A change that touches something else is neither: the transition is // the closed-ness, not the edit. if _, err := pipe.cli.TicketSet(id, "dan", map[string]string{"title": "it still breaks"}); err != nil { t.Fatal(err) } if _, err := pipe.Sync(); err != nil { t.Fatal(err) } if got := next(); got != evt.TicketChanged { t.Errorf("a title edit raised %s, want ticket.changed", got) } }