Artifact 9326b15eab710632e889722a14e619a0c9c56a4467fe8d8639f70a98e70c448b:
- File
hub/nats_transport_test.go
— part of check-in
[6f9e3e40d0]
at
2026-05-12 20:27:38
on branch trunk
— feat(hub): cross-leaf fossil sync over NATS (#156) (#157)
Add Config.ProjectCode + Config.SeedFromUpstream so multiple hubs sharing a project-code propagate commits over a single .sync subject. Adds an auto-publish notification on a sibling .commit subject + pull-on-notify subscriber, closing the cross-process Fossil sharing gap from #156.
- hub/nats_transport.go: libfossil.Transport over nats request/reply - hub/clone.go: HTTP-based SeedFromUpstream helper (NATS-clone deferred) - hub/repo.go: Repo.publish hook fired after successful Commit - hub/hub.go: startCommitSubscriber + Config plumbing + NoEcho - hub/cross_leaf_test.go: gold-path propagation, subject alignment, drift guard - libfossil pin -> v0.6.1 (CreateOpts.ProjectCode)
Design: docs/superpowers/specs/2026-05-12-cross-leaf-fossil-sync-design.md Protocol: docs/architecture/sync-protocol.md ("NATS-Wrapped Sync")
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> (user: [email protected], size: 2883) [annotate] [blame] [check-ins using]
package hub import ( "context" "testing" "time" _ "github.com/danmestas/libfossil/db/driver/modernc" "github.com/nats-io/nats.go" ) // TestNATSTransport_RoundTrip exercises the request/reply path end-to-end // against an embedded NATS server: a fake responder echoes the payload // with a known prefix; the transport hands the response back unchanged. func TestNATSTransport_RoundTrip(t *testing.T) { h := newTestHub(t) nc, err := nats.Connect(h.NATSURL(), nats.Timeout(2*time.Second)) if err != nil { t.Fatalf("nats.Connect: %v", err) } defer nc.Close() const subject = "fossil.test.sync" sub, err := nc.Subscribe(subject, func(msg *nats.Msg) { _ = msg.Respond(append([]byte("REPLY:"), msg.Data...)) }) if err != nil { t.Fatalf("Subscribe: %v", err) } defer sub.Unsubscribe() if err := nc.FlushTimeout(time.Second); err != nil { t.Fatalf("Flush: %v", err) } tr := newNATSTransport(nc, subject, 5*time.Second) got, err := tr.RoundTrip(context.Background(), []byte("hello")) if err != nil { t.Fatalf("RoundTrip: %v", err) } if string(got) != "REPLY:hello" { t.Errorf("RoundTrip response = %q, want %q", string(got), "REPLY:hello") } } // TestNATSTransport_EmptyReplyIsError asserts the contract that an // empty reply body (the hub's HandleSync failure signal) surfaces as // an error rather than being silently treated as a valid no-op. func TestNATSTransport_EmptyReplyIsError(t *testing.T) { h := newTestHub(t) nc, err := nats.Connect(h.NATSURL(), nats.Timeout(2*time.Second)) if err != nil { t.Fatalf("nats.Connect: %v", err) } defer nc.Close() const subject = "fossil.test.empty" sub, err := nc.Subscribe(subject, func(msg *nats.Msg) { _ = msg.Respond([]byte{}) }) if err != nil { t.Fatalf("Subscribe: %v", err) } defer sub.Unsubscribe() if err := nc.FlushTimeout(time.Second); err != nil { t.Fatalf("Flush: %v", err) } tr := newNATSTransport(nc, subject, 2*time.Second) if _, err := tr.RoundTrip(context.Background(), []byte("ping")); err == nil { t.Fatal("RoundTrip on empty reply: expected error, got nil") } } // TestNATSTransport_TimeoutHonored asserts that a non-responsive subject // causes RoundTrip to error within the transport's configured timeout // rather than blocking indefinitely. func TestNATSTransport_TimeoutHonored(t *testing.T) { h := newTestHub(t) nc, err := nats.Connect(h.NATSURL(), nats.Timeout(2*time.Second)) if err != nil { t.Fatalf("nats.Connect: %v", err) } defer nc.Close() tr := newNATSTransport(nc, "fossil.test.silent", 200*time.Millisecond) start := time.Now() _, err = tr.RoundTrip(context.Background(), []byte("ping")) elapsed := time.Since(start) if err == nil { t.Fatal("RoundTrip against silent subject: expected error, got nil") } if elapsed > time.Second { t.Errorf("RoundTrip took %v with 200ms timeout — timeout not honored", elapsed) } }