Files
multica/server/internal/handler/handler_writejson_test.go
Multica Eve b6adf23f91 feat(api): emit Content-Length header on JSON responses (#5021)
The core writeJSON helpers streamed the body via json.NewEncoder(w).Encode
after WriteHeader, which forces net/http into chunked transfer encoding and
omits Content-Length. Buffer the marshaled body first, set an accurate
Content-Length, then write — so API (and health) JSON responses advertise
their exact size. writeMeasuredJSON gets the same header. Adds a test
asserting the header matches the on-wire body length.

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-07 14:43:19 +08:00

156 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"bytes"
"net/http"
"net/http/httptest"
"strconv"
"testing"
)
// TestWriteMeasuredJSONByteIdenticalToWriteJSON locks the load-bearing assumption
// behind the F2 claim-observability patch: swapping writeJSON for writeMeasuredJSON
// at the /tasks/claim response sites must not change a single byte on the wire.
//
// writeJSON encodes via json.NewEncoder(w).Encode (which appends a trailing
// newline); writeMeasuredJSON marshals via json.Marshal and appends the newline
// by hand. Both HTML-escape by default, so the emitted bytes must match for every
// input. This table-driven test fails closed if that invariant ever drifts, so the
// "no wire-behavior change" claim is provable rather than reasoned.
func TestWriteMeasuredJSONByteIdenticalToWriteJSON(t *testing.T) {
type skill struct {
Name string `json:"name"`
Description string `json:"description"`
Files map[string]string `json:"files"`
}
type claimResp struct {
ID string `json:"id"`
Name string `json:"name"`
Skills []skill `json:"skills"`
Args []string `json:"args"`
}
cases := []struct {
name string
v any
}{
{"nil", nil},
{"no_task", map[string]any{"task": nil}},
{"empty_map", map[string]any{}},
{"empty_slice", []string{}},
{"scalar_string", "plain string"},
{"scalar_bool", true},
{"numbers", map[string]any{"i": 42, "f": 3.5, "neg": -17, "big": 1234567890123}},
{"html_escapable", map[string]any{"s": `a<b> & "c" 'd' <script>`}},
{"ampersand_lt_gt", map[string]any{"raw": "1 < 2 && 3 > 2"}},
{"unicode_and_separators", map[string]any{"s": "héllo 世界 🚀 "}},
{"nested", map[string]any{"a": []any{1, "two", true, nil}, "b": map[string]any{"c": []int{1, 2, 3}}}},
{"large_claim_with_skills", map[string]any{"task": claimResp{
ID: "11111111-2222-3333-4444-555555555555",
Name: "agent <CC> & friends",
Skills: []skill{
{Name: "multica-working-on-issues", Description: "do work <safely> & well", Files: map[string]string{"SKILL.md": "# Title\n<b>x</b> & y"}},
{Name: "multica-mentioning", Description: "ping people", Files: map[string]string{"SKILL.md": "line1\nline2"}},
},
Args: []string{"--flag", "a<b", "c&d"},
}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
recEnc := httptest.NewRecorder()
writeJSON(recEnc, http.StatusOK, tc.v)
recMeasured := httptest.NewRecorder()
n, err := writeMeasuredJSON(recMeasured, http.StatusOK, tc.v)
if err != nil {
t.Fatalf("writeMeasuredJSON returned error: %v", err)
}
encBody := recEnc.Body.Bytes()
measuredBody := recMeasured.Body.Bytes()
if !bytes.Equal(encBody, measuredBody) {
t.Fatalf("wire bytes differ:\n writeJSON = %q\n writeMeasuredJSON = %q", encBody, measuredBody)
}
if n != len(measuredBody) {
t.Fatalf("reported payload bytes %d != actual body length %d", n, len(measuredBody))
}
if n != len(encBody) {
t.Fatalf("reported payload bytes %d != writeJSON body length %d", n, len(encBody))
}
if recEnc.Code != recMeasured.Code {
t.Fatalf("status code differs: writeJSON=%d writeMeasuredJSON=%d", recEnc.Code, recMeasured.Code)
}
if got, want := recMeasured.Header().Get("Content-Type"), recEnc.Header().Get("Content-Type"); got != want {
t.Fatalf("Content-Type differs: writeMeasuredJSON=%q writeJSON=%q", got, want)
}
})
}
// Sanity guard: both encoders HTML-escape by default, so a literal '<' rune must
// not survive into the body (it is emitted as the escaped form). This documents
// the escaping behaviour that makes the byte-identity comparison meaningful,
// without depending on the escaped literal appearing in source.
rec := httptest.NewRecorder()
writeJSON(rec, http.StatusOK, map[string]string{"x": "<&>"})
if bytes.ContainsRune(rec.Body.Bytes(), '<') {
t.Fatalf("expected '<' to be HTML-escaped out of the body, got %q", rec.Body.String())
}
}
// TestWriteJSONSetsContentLength verifies that the JSON response writers advertise
// an accurate Content-Length header. Encoding straight into the ResponseWriter after
// WriteHeader forces net/http into chunked transfer encoding (no Content-Length), so
// both writeJSON and writeMeasuredJSON buffer the body first and set the header
// explicitly. The value must equal the exact number of bytes written on the wire.
func TestWriteJSONSetsContentLength(t *testing.T) {
cases := []struct {
name string
v any
}{
{"empty_map", map[string]any{}},
{"simple", map[string]string{"hello": "world"}},
{"html_escapable", map[string]any{"s": `a<b> & "c" <script>`}},
{"unicode", map[string]any{"s": "héllo 世界 🚀"}},
{"nested", map[string]any{"a": []any{1, "two", true, nil}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rec := httptest.NewRecorder()
writeJSON(rec, http.StatusOK, tc.v)
got := rec.Header().Get("Content-Length")
if got == "" {
t.Fatalf("writeJSON did not set Content-Length header")
}
cl, err := strconv.Atoi(got)
if err != nil {
t.Fatalf("Content-Length %q is not an integer: %v", got, err)
}
if cl != rec.Body.Len() {
t.Fatalf("Content-Length = %d, want %d (actual body length)", cl, rec.Body.Len())
}
// writeMeasuredJSON must set the same, accurate Content-Length.
recMeasured := httptest.NewRecorder()
n, err := writeMeasuredJSON(recMeasured, http.StatusOK, tc.v)
if err != nil {
t.Fatalf("writeMeasuredJSON returned error: %v", err)
}
gotMeasured := recMeasured.Header().Get("Content-Length")
clMeasured, err := strconv.Atoi(gotMeasured)
if err != nil {
t.Fatalf("writeMeasuredJSON Content-Length %q is not an integer: %v", gotMeasured, err)
}
if clMeasured != recMeasured.Body.Len() || clMeasured != n {
t.Fatalf("writeMeasuredJSON Content-Length = %d, body = %d, reported = %d; all must match", clMeasured, recMeasured.Body.Len(), n)
}
if got != gotMeasured {
t.Fatalf("Content-Length differs: writeJSON=%q writeMeasuredJSON=%q", got, gotMeasured)
}
})
}
}