Repository: nats-io/natscli

Issue:
Add facility to stop at end of stream/subject with `nats sub --stream`
### Proposed change

Presently the only way to view available messages on a stream/subject and quit is with `nats stream view` but this isn't appropriate for scripting or other shell use-cases (piping to file etc) as it is interactive with pagination.

`nats sub --stream` (and related) should have the option to stop/exit when there are no more messages available. This should be easily achievable by taking a count of messages on the stream/subject, processing them and then exiting.

I propose a switch `--stop`, `--exit` or `--quit` to consume existing messages only and exit.

A currently usable workaround is to use `--wait=100ms` which exits when there are no more messages appearing after the timeout, but this feels clunky as the timeout would vary on uncertain real-world factors.

### Use case

Any situation where one needs to see messages on a stream/subject non-interactively during scripting, development and debugging. Other use-cases eg. taking periodic snapshots of a stream/subject, backups outside the nats ecosystem, monitoring etc.

### Contribution

Would be happy to yes, assuming this hasn't already been worked on by others (@ripienaar). It was fairly trivial to implement this using consumers in go in a nats application currently in development.

Discussion:

Solution patch:
diff --git a/cli/sub_command.go b/cli/sub_command.go
index e4c0d45..38b07b1 100644
--- a/cli/sub_command.go
+++ b/cli/sub_command.go
@@ -52,6 +52,7 @@ type subCmd struct {
 	sseq                  uint64
 	deliverAll            bool
 	deliverNew            bool
+	stopAtPendingZero     bool
 	reportSubjects        bool
 	reportSubjectsCount   int
 	reportSub             bool
@@ -152,6 +153,7 @@ func configureSubCommand(app commandHost) {
 	act.Flag("last", "Delivers the most recent and all future messages (requires JetStream)").UnNegatableBoolVar(&c.deliverLast)
 	act.Flag("since", "Delivers messages received since a duration like 1d3h5m2s(requires JetStream)").PlaceHolder("DURATION").StringVar(&c.deliverSince)
 	act.Flag("last-per-subject", "Deliver the most recent messages for each subject in the Stream (requires JetStream)").UnNegatableBoolVar(&c.deliverLastPerSubject)
+	act.Flag("terminate-at-end", "Stops consuming messages from JetStream once all messages were received").Short('T').UnNegatableBoolVar(&c.stopAtPendingZero)
 	act.Flag("stream", "Subscribe to a specific stream (required JetStream)").PlaceHolder("STREAM").StringVar(&c.stream)
 	act.Flag("ignore-subject", "Subjects for which corresponding messages will be ignored and therefore not shown in the output").Short('I').PlaceHolder("SUBJECT").StringsVar(&c.ignoreSubjects)
 	act.Flag("wait", "Unsubscribe after this amount of time without any traffic").DurationVar(&c.wait)
@@ -318,6 +320,9 @@ func (c *subCmd) validateInputs(ctx context.Context, nc *nats.Conn, mgr *jsm.Man
 		return fmt.Errorf("streams subscribe support only 1 subject")
 	}
 
+	if c.stopAtPendingZero && !c.jetStream {
+		return fmt.Errorf("--terminate-at-end is only supported for JetStream subscriptions")
+	}
 	if c.inbox && c.jetStream {
 		return fmt.Errorf("generating inboxes is not compatible with JetStream subscriptions")
 	}
@@ -437,8 +442,8 @@ func (c *subCmd) createMsgHandler(subState subscriptionState, subs *[]*nats.Subs
 	}
 }
 
-// createJestreamMsgHandler sets up a message handler specifically for jetstream.Msg message types
-func (c *subCmd) createJestreamMsgHandler(subState subscriptionState, nc *nats.Conn, consumers *[]jetstream.ConsumeContext, t *time.Timer) jetstream.MessageHandler {
+// createJetStreamMsgHandler sets up a message handler specifically for jetstream.Msg message types
+func (c *subCmd) createJetStreamMsgHandler(subState subscriptionState, nc *nats.Conn, consumers *[]jetstream.ConsumeContext, t *time.Timer) jetstream.MessageHandler {
 	return func(m jetstream.Msg) {
 		subState.msgMu.Lock()
 		defer subState.msgMu.Unlock()
@@ -468,7 +473,8 @@ func (c *subCmd) createJestreamMsgHandler(subState subscriptionState, nc *nats.C
 			c.printJetStreamMsg(m, nil, subState.counter)
 		}
 
-		if c.limit > 0 && subState.counter == c.limit {
+		meta, _ := m.Metadata()
+		if (c.limit > 0 && subState.counter == c.limit) || (c.stopAtPendingZero && meta != nil && meta.NumPending == 0) {
 			for _, cCtx := range *consumers {
 				cCtx.Stop()
 			}
@@ -477,6 +483,7 @@ func (c *subCmd) createJestreamMsgHandler(subState subscriptionState, nc *nats.C
 			if !c.match || len(subState.matchMap) == 0 {
 				subState.cancelFn()
 			}
+
 			return
 		}
 
@@ -687,8 +694,8 @@ func (c *subCmd) jetStreamSubscribe(ctx context.Context, subState subscriptionSt
 	return nil
 }
 
-// reportJetstreamSubscribe sets up a jetstream specific subscription for subject reporting
-func (c *subCmd) reportJetstreamSubscribe(ctx context.Context, subState subscriptionState, handler jetstream.MessageHandler, consumerContexts *[]jetstream.ConsumeContext, js jetstream.JetStream) error {
+// reportJetStreamSubscribe sets up a jetstream specific subscription for subject reporting
+func (c *subCmd) reportJetStreamSubscribe(ctx context.Context, subState subscriptionState, handler jetstream.MessageHandler, consumerContexts *[]jetstream.ConsumeContext, js jetstream.JetStream) error {
 	err := c.jetStreamSubscribe(ctx, subState, handler, consumerContexts, js)
 	if err != nil {
 		return err
@@ -968,7 +975,7 @@ func (c *subCmd) subscribe(p *fisk.ParseContext) error {
 	}
 
 	msgHandler := c.createMsgHandler(subState, &subs, t)
-	jetstreamMsgHandler := c.createJestreamMsgHandler(subState, nc, &consumerContexts, t)
+	jetstreamMsgHandler := c.createJetStreamMsgHandler(subState, nc, &consumerContexts, t)
 
 	matchHandler := c.createMatchHandler(subState)
 
@@ -1009,7 +1016,7 @@ func (c *subCmd) subscribe(p *fisk.ParseContext) error {
 		err = c.graphSubscribe(ctx, subState, nc, msgHandler, &subs)
 	case c.reportSubjects:
 		if c.jetStream {
-			err = c.reportJetstreamSubscribe(ctx, subState, jetstreamMsgHandler, &consumerContexts, js)
+			err = c.reportJetStreamSubscribe(ctx, subState, jetstreamMsgHandler, &consumerContexts, js)
 		} else {
 			err = c.reportSubscribe(ctx, subState, nc, msgHandler, &subs)
 		}
@@ -1146,7 +1153,7 @@ func (c *subCmd) printJetStreamMsg(msg jetstream.Msg, reply jetstream.Msg, ctr u
 		fmt.Printf("<<< Reply Subject: %v\n", dataMsg.Reply)
 	}
 
-	summary := fmt.Sprintf("stream: %s seq %d / subject: %s / time: %v", meta.Stream, meta.Sequence.Stream, dataMsg.Subject, meta.Timestamp)
+	summary := fmt.Sprintf("stream: %s seq: %s / pending: %s / subject: %s / time: %s", meta.Stream, f(meta.Sequence.Stream), f(meta.NumPending), dataMsg.Subject, f(meta.Timestamp))
 
 	// Output format 1: dumping, to stdout or files
 	if c.dump != "" {
@@ -1179,8 +1186,7 @@ func (c *subCmd) printJetStreamMsg(msg jetstream.Msg, reply jetstream.Msg, ctr u
 	}
 
 	if replyMsg != nil {
-		rsum := fmt.Sprintf("stream: %s seq %d / subject: %s / time: %v", meta.Stream, meta.Sequence.Stream, reply.Subject(), meta.Timestamp)
-		fmt.Printf("[#%d] Matched reply JetStream message: %s\n", ctr, rsum)
+		fmt.Printf("[#%d] Matched reply JetStream message: %s\n", ctr, summary)
 		c.prettyPrintMsg(replyMsg, c.headersOnly, c.translate)
 	}
 }
diff --git a/go.mod b/go.mod
index 007d934..8a198b5 100644
--- a/go.mod
+++ b/go.mod
@@ -62,13 +62,13 @@ require (
 	github.com/nsf/termbox-go v1.1.1 // indirect
 	github.com/prometheus/client_model v0.6.2 // indirect
 	github.com/prometheus/common v0.67.1 // indirect
-	github.com/prometheus/procfs v0.17.0 // indirect
+	github.com/prometheus/procfs v0.18.0 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
 	github.com/spf13/cast v1.10.0 // indirect
 	github.com/synadia-io/orbit.go/natsext v0.1.1 // indirect
 	go.yaml.in/yaml/v2 v2.4.3 // indirect
-	golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
-	golang.org/x/net v0.45.0 // indirect
+	golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
+	golang.org/x/net v0.46.0 // indirect
 	golang.org/x/sys v0.37.0 // indirect
 	golang.org/x/text v0.30.0 // indirect
 	golang.org/x/time v0.14.0 // indirect
diff --git a/go.sum b/go.sum
index 37d18ab..18cd523 100644
--- a/go.sum
+++ b/go.sum
@@ -136,8 +136,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
 github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
 github.com/prometheus/common v0.67.1 h1:OTSON1P4DNxzTg4hmKCc37o4ZAZDv0cfXLkOt0oEowI=
 github.com/prometheus/common v0.67.1/go.mod h1:RpmT9v35q2Y+lsieQsdOh5sXZ6ajUGC8NjZAmr8vb0Q=
-github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
-github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
+github.com/prometheus/procfs v0.18.0 h1:2QTA9cKdznfYJz7EDaa7IiJobHuV7E1WzeBwcrhk0ao=
+github.com/prometheus/procfs v0.18.0/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
@@ -180,8 +180,8 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL
 golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
-golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
-golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
+golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
+golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -192,8 +192,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
-golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -226,8 +226,8 @@ golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
-golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=



Test patch:
diff --git a/nats/tests/auth_command_test.go b/nats/tests/auth_command_test.go
index bb587e2..ccd6f62 100644
--- a/nats/tests/auth_command_test.go
+++ b/nats/tests/auth_command_test.go
@@ -22,10 +22,6 @@ import (
 	"testing"
 )
 
-const (
-	TEST_DIR = "/tmp/natscli/auth_command_test"
-)
-
 var (
 	JSON = `
 {
@@ -47,13 +43,17 @@ test.a:
 `
 )
 
+var tempDir string
+
 func setup(operator, account string, t *testing.T) {
+	t.Helper()
 	teardown(t)
-	err := os.Setenv("XDG_CONFIG_HOME", TEST_DIR)
+	tempDir = t.TempDir()
+	err := os.Setenv("XDG_CONFIG_HOME", tempDir)
 	if err != nil {
 		t.Error(err)
 	}
-	err = os.Setenv("XDG_DATA_HOME", TEST_DIR)
+	err = os.Setenv("XDG_DATA_HOME", tempDir)
 	if err != nil {
 		t.Error(err)
 	}
@@ -63,11 +63,8 @@ func setup(operator, account string, t *testing.T) {
 }
 
 func teardown(t *testing.T) {
-	err := os.RemoveAll(TEST_DIR)
-	if err != nil {
-		t.Error(err)
-	}
-	err = os.Unsetenv("NSC_HOME")
+	t.Helper()
+	err := os.Unsetenv("NSC_HOME")
 	if err != nil {
 		t.Error(err)
 	}
@@ -123,7 +120,7 @@ func TestMapping(t *testing.T) {
 					},
 				}
 
-				fp := filepath.Join(TEST_DIR, fmt.Sprintf("test.%s", tt.fileExt))
+				fp := filepath.Join(tempDir, fmt.Sprintf("test.%s", tt.fileExt))
 				file, err := os.OpenFile(fp, os.O_CREATE|os.O_WRONLY, 0644)
 				if err != nil {
 					t.Fatalf("Error opening file: %s", err)
diff --git a/nats/tests/sub_command_test.go b/nats/tests/sub_command_test.go
index fd19538..028d0c8 100644
--- a/nats/tests/sub_command_test.go
+++ b/nats/tests/sub_command_test.go
@@ -46,14 +46,17 @@ func TestNatsSubscribe(t *testing.T) {
 
 	t.Run("--dump=file", func(t *testing.T) {
 		withNatsServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn) error {
-			dumpDir := "/tmp/test1"
-			t.Cleanup(func() { _ = os.RemoveAll(dumpDir) })
+			dumpDir := t.TempDir()
 
 			done := make(chan struct{})
-			go func() {
-				runNatsCli(t, fmt.Sprintf("--server='%s' sub TEST --count=1 --dump=%s", srv.ClientURL(), dumpDir))
+			go func(t *testing.T) {
+				t.Helper()
+				out := runNatsCli(t, fmt.Sprintf("--server='%s' sub TEST --count=1 --dump='%s'", srv.ClientURL(), dumpDir))
+				if strings.Contains(string(out), "Could not save message") {
+					t.Errorf("unexpected error: %s", string(out))
+				}
 				close(done)
-			}()
+			}(t)
 
 			time.Sleep(500 * time.Millisecond)
 
@@ -174,12 +177,11 @@ func TestNatsSubscribe(t *testing.T) {
 
 	t.Run("--dump and --translate", func(t *testing.T) {
 		withNatsServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn) error {
-			dumpDir := "/tmp/test_dump_and_translate"
-			t.Cleanup(func() { _ = os.RemoveAll(dumpDir) })
+			dumpDir := t.TempDir()
 
 			done := make(chan struct{})
 			go func() {
-				runNatsCli(t, fmt.Sprintf("--server='%s' sub TEST --count=1 --dump=%s --translate=\"wc -c\"", srv.ClientURL(), dumpDir))
+				runNatsCli(t, fmt.Sprintf("--server='%s' sub TEST --count=1 --dump='%s' --translate=\"wc -c\"", srv.ClientURL(), dumpDir))
 				close(done)
 			}()
 
@@ -208,7 +210,7 @@ func TestNatsSubscribe(t *testing.T) {
 			}
 
 			if dumpFile == "" {
-				t.Fatal("no .json file found in dump directory")
+				t.Fatalf("no .json file found in dump directory %q with entries %v", dumpDir, entries)
 			}
 
 			resp, err := os.ReadFile(dumpFile)
@@ -555,6 +557,28 @@ func TestJetStreamSubscribe(t *testing.T) {
 		}
 	)
 
+	t.Run("--terminate-at-end", func(t *testing.T) {
+		withJSServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn, mgr *jsm.Manager) error {
+			createDefaultTestStream(t, mgr, 1)
+
+			err := nc.PublishMsg(defaultTestMsg)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			start := time.Now()
+			output := string(runNatsCli(t, fmt.Sprintf("--server='%s' sub --stream TEST_STREAM --last --terminate-at-end --wait 2s", srv.ClientURL())))
+			if !expectMatchLine(t, output, `\[#\d\] Received JetStream message: stream: TEST_STREAM seq: (\d+) / pending: 0 / subject: TEST_STREAM.1 / time: \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d`) {
+				t.Fatalf("missing expected summary line:\n%s", output)
+			}
+
+			if time.Since(start) > time.Second {
+				t.Fatalf("expected to terminate first message, but took %s", time.Since(start))
+			}
+			return nil
+		})
+	})
+
 	t.Run("--dump=file", func(t *testing.T) {
 		withJSServer(t, func(t *testing.T, srv *server.Server, nc *nats.Conn, mgr *jsm.Manager) error {
 			createDefaultTestStream(t, mgr, 1)
@@ -564,10 +588,10 @@ func TestJetStreamSubscribe(t *testing.T) {
 				t.Fatalf("unable to publish message: %s", err)
 			}
 
-			runNatsCli(t, fmt.Sprintf("--server='%s' sub --stream TEST_STREAM --last --count=1 --dump=/tmp/test1", srv.ClientURL()))
-			defer os.RemoveAll("/tmp/test1/")
+			dumpDir := t.TempDir()
+			runNatsCli(t, fmt.Sprintf("--server='%s' sub --stream TEST_STREAM --last --count=1 --dump='%s'", srv.ClientURL(), dumpDir))
 
-			resp, err := os.ReadFile("/tmp/test1/1.json")
+			resp, err := os.ReadFile(filepath.Join(dumpDir, "1.json"))
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -661,10 +685,9 @@ func TestJetStreamSubscribe(t *testing.T) {
 				t.Fatalf("unable to publish message: %s", err)
 			}
 
-			dumpDir := "/tmp/test_dump_and_translate"
+			dumpDir := t.TempDir()
 
-			runNatsCli(t, fmt.Sprintf("--server='%s' sub --stream TEST_STREAM --last --count=1 --dump=%s --translate=\"wc -c\"", srv.ClientURL(), dumpDir))
-			defer os.RemoveAll(dumpDir)
+			runNatsCli(t, fmt.Sprintf("--server='%s' sub --stream TEST_STREAM --last --count=1 --dump='%s' --translate=\"wc -c\"", srv.ClientURL(), dumpDir))
 
 			entries, err := os.ReadDir(dumpDir)
 			if err != nil {
@@ -682,7 +705,7 @@ func TestJetStreamSubscribe(t *testing.T) {
 			}
 
 			if dumpFile == "" {
-				t.Fatal("no .json file found in dump directory")
+				t.Fatalf("no .json file found in dump directory %q with entries %v", dumpDir, entries)
 			}
 
 			resp, err := os.ReadFile(dumpFile)
@@ -733,7 +756,7 @@ func TestJetStreamSubscribe(t *testing.T) {
 
 			output := string(runNatsCli(t, fmt.Sprintf("--server='%s' sub --stream TEST_STREAM --last --count=1", srv.ClientURL())))
 
-			if !expectMatchLine(t, output, `\[#\d\] Received JetStream message: stream: TEST_STREAM seq (\d+) / subject: TEST_STREAM.1 / time: \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d`) {
+			if !expectMatchLine(t, output, `\[#\d\] Received JetStream message: stream: TEST_STREAM seq: (\d+) / pending: (\d+) / subject: TEST_STREAM.1 / time: \d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d`) {
 				t.Fatalf("missing expected summary line:\n%s", output)
 			}