Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions e2e/edge_case_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,13 @@ func TestJSONOnAckCommand(t *testing.T) {
id := extractIncidentID(t, r.Stdout)
t.Cleanup(func() { runCLI(t, "incident", "close", id) })

// ack is served by the generated twin; --json wraps the OK line as {"message":"..."}.
// ack is served by the generated twin. /incident/ack answers with an empty
// data envelope, so --json emits the one thing the call did report: its
// request id.
r = runCLI(t, "incident", "ack", id, "--json")
requireSuccess(t, r)
requireValidJSON(t, r.Stdout)
requireContains(t, r.Stdout, "OK: POST /incident/ack")
requireRequestIDOnly(t, r.Stdout)
}

// Test 307: --json on close command
Expand Down
42 changes: 42 additions & 0 deletions e2e/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
Expand Down Expand Up @@ -217,6 +218,47 @@ func requireValidJSON(t *testing.T, output string) {
}
}

// noPayloadAckRe matches the whole single line a command prints when its
// endpoint answers with no payload. Unlike the unit tests, which pin the line
// by equality against a stub's fixed request id, e2e runs against the real API
// and cannot know the id — so it pins the shape AND requires the id to be
// non-empty, which is the part a live server must supply.
var noPayloadAckRe = regexp.MustCompile(`^OK: the API returned no payload \(request_id \S+\)\.$`)

// requireNoPayloadAck fails the test unless output is exactly the no-payload
// acknowledgement line. Equality on the whole output (not a substring) is the
// point: a caller reading this through `head` must find the line first, so
// anything printed above it is a regression.
func requireNoPayloadAck(t *testing.T, output string) {
t.Helper()
got := strings.TrimRight(output, "\n")
if !noPayloadAckRe.MatchString(got) {
t.Fatalf("output must be exactly the no-payload acknowledgement line, got:\n%s", output)
}
}

// requireRequestIDOnly fails the test unless output is the structured form of
// the no-payload acknowledgement: an object carrying a non-empty request_id and
// nothing a caller would mistake for a payload. It also guards against the old
// shape, which wrapped a rendered sentence as {"message": "OK: POST /..."} and
// gave a machine caller nothing to read.
func requireRequestIDOnly(t *testing.T, output string) {
t.Helper()
var ack struct {
RequestID string `json:"request_id"`
Message string `json:"message"`
}
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &ack); err != nil {
t.Fatalf("output is not a JSON object (%v):\n%s", err, output)
}
if ack.RequestID == "" {
t.Fatalf("structured acknowledgement carries no request_id:\n%s", output)
}
if ack.Message != "" {
t.Fatalf("structured acknowledgement still renders a prose message:\n%s", output)
}
}

// requireTableHeaders fails the test if any of the given headers are missing
// from the first line of the output.
func requireTableHeaders(t *testing.T, output string, headers ...string) {
Expand Down
2 changes: 1 addition & 1 deletion e2e/incident_extended_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ func TestIncidentAckSingleID(t *testing.T) {
// Served by the generated twin (positional id → incident_ids).
r = runCLI(t, "incident", "ack", id)
requireSuccess(t, r)
requireContains(t, r.Stdout, "OK: POST /incident/ack")
requireNoPayloadAck(t, r.Stdout)
}

// Test 204: close single ID
Expand Down
2 changes: 1 addition & 1 deletion e2e/incident_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestIncidentLifecycle(t *testing.T) {
// Step 3: Ack (served by the generated twin; positional id → incident_ids).
r = runCLI(t, "incident", "ack", id)
requireSuccess(t, r)
requireContains(t, r.Stdout, "OK: POST /incident/ack")
requireNoPayloadAck(t, r.Stdout)

// Step 4: Get - should be Processing
r = runCLI(t, "incident", "get", id)
Expand Down
6 changes: 1 addition & 5 deletions internal/cli/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,7 @@ Request fields:
if err != nil {
return err
}
if resp != nil && len(resp.Raw) > 0 {
return ctx.WriteRaw(resp.Raw)
}
ctx.WriteResult("OK: POST /alert/merge")
return nil
return printRawOrNoPayload(ctx, resp)
})
},
}
Expand Down
4 changes: 2 additions & 2 deletions internal/cli/alert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,8 +154,8 @@ func TestCommandAlertMergeCommentFileReachesWireByteForByte(t *testing.T) {
if stub.lastBody["incident_id"] != "inc-1" {
t.Fatalf("[alert-merge-comment-file] expected incident_id %q, got %#v", "inc-1", stub.lastBody["incident_id"])
}
if !strings.Contains(out, "OK: POST /alert/merge") {
t.Fatalf("[alert-merge-comment-file] unexpected output:\n%s", out)
if got := strings.TrimRight(out, "\n"); got != gfStubNoPayloadLine {
t.Fatalf("[alert-merge-comment-file] default output must be exactly %q, got:\n%s", gfStubNoPayloadLine, out)
}
}

Expand Down
5 changes: 3 additions & 2 deletions internal/cli/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,9 @@ func (ctx *RunContext) WriteResult(message string) {

// WriteRaw writes a non-JSON response body (e.g. a CSV/file download surfaced
// on Response.Raw by the *export endpoints) straight to the output writer, so
// shell redirection (`> file.csv`) captures the bytes verbatim instead of the
// canned "OK: POST ..." acknowledgment.
// shell redirection (`> file.csv`) captures the bytes verbatim rather than the
// no-payload acknowledgement those commands print when no bytes arrive (see
// printRawOrNoPayload).
func (ctx *RunContext) WriteRaw(body []byte) error {
_, err := ctx.Writer.Write(body)
return err
Expand Down
12 changes: 6 additions & 6 deletions internal/cli/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,8 @@ func TestCommandIncidentUnack(t *testing.T) {
if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want {
t.Fatalf("[incident-unack] expected ids %q, got %q", want, got)
}
if !strings.Contains(out, "OK: POST /incident/unack") {
t.Fatalf("[incident-unack] unexpected output:\n%s", out)
if got := strings.TrimRight(out, "\n"); got != gfStubNoPayloadLine {
t.Fatalf("[incident-unack] default output must be exactly %q, got:\n%s", gfStubNoPayloadLine, out)
}
}

Expand All @@ -516,8 +516,8 @@ func TestCommandIncidentWake(t *testing.T) {
if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1"; got != want {
t.Fatalf("[incident-wake] expected ids %q, got %q", want, got)
}
if !strings.Contains(out, "OK: POST /incident/wake") {
t.Fatalf("[incident-wake] unexpected output:\n%s", out)
if got := strings.TrimRight(out, "\n"); got != gfStubNoPayloadLine {
t.Fatalf("[incident-wake] default output must be exactly %q, got:\n%s", gfStubNoPayloadLine, out)
}
}

Expand Down Expand Up @@ -1312,8 +1312,8 @@ func TestCommandIncidentDisableMerge(t *testing.T) {
if got, want := strings.Join(stub.bodyStrings("incident_ids"), ","), "inc-1,inc-2"; got != want {
t.Fatalf("[incident-disable-merge] expected ids %q, got %q", want, got)
}
if !strings.Contains(out, "OK: POST /incident/disable-merge") {
t.Fatalf("[incident-disable-merge] unexpected output:\n%s", out)
if got := strings.TrimRight(out, "\n"); got != gfStubNoPayloadLine {
t.Fatalf("[incident-disable-merge] default output must be exactly %q, got:\n%s", gfStubNoPayloadLine, out)
}
}

Expand Down
Loading
Loading