mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
* fix(plugin): return 403 when API is disabled (MUL-7437) Co-authored-by: multica-agent <[email protected]> * fix(api): classify disabled capabilities as non-retryable (MUL-7437) Co-authored-by: multica-agent <[email protected]> * fix(api): address disabled capability review feedback (MUL-7437) Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Sol-Boy <[email protected]> Co-authored-by: multica-agent <[email protected]>
176 lines
6.6 KiB
Go
176 lines
6.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func TestWriteFeatureDisabledIsNonRetryable(t *testing.T) {
|
|
rec := httptest.NewRecorder()
|
|
writeFeatureDisabled(rec, "feature_disabled", "feature is disabled")
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
|
|
}
|
|
if retryAfter := rec.Header().Get("Retry-After"); retryAfter != "" {
|
|
t.Fatalf("Retry-After = %q, want empty", retryAfter)
|
|
}
|
|
var body map[string]string
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
|
t.Fatalf("decode response: %v", err)
|
|
}
|
|
if body["code"] != "feature_disabled" {
|
|
t.Fatalf("code = %q, want feature_disabled", body["code"])
|
|
}
|
|
}
|
|
|
|
// 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-platform", Description: "do work <safely> & well", Files: map[string]string{"SKILL.md": "# Title\n<b>x</b> & y"}},
|
|
{Name: "team-conventions", 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)
|
|
}
|
|
})
|
|
}
|
|
}
|