diff --git a/internal/server/task_run_json_test.go b/internal/server/task_run_json_test.go new file mode 100644 index 00000000..db368eca --- /dev/null +++ b/internal/server/task_run_json_test.go @@ -0,0 +1,109 @@ +package server + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "chat-agent-system/internal/bizdev" + "chat-agent-system/internal/config" + "chat-agent-system/internal/taskrun" +) + +func TestTaskRunResponseMarshalUsesSnakeCase(t *testing.T) { + raw, err := json.Marshal(taskRunResponse{ + RunRecord: taskrun.RunRecord{ + RunID: "run_snake", + Status: "failed", + ErrorMessage: "dial tcp connection refused", + TaskKey: "user_requirements", + WorkspaceID: "ws_1", + }, + Resumable: false, + }) + if err != nil { + t.Fatal(err) + } + var body map[string]any + if err := json.Unmarshal(raw, &body); err != nil { + t.Fatal(err) + } + for _, key := range []string{"run_id", "status", "error_message", "workspace_id", "task_key", "resumable"} { + if _, ok := body[key]; !ok { + t.Fatalf("missing %q in %s", key, raw) + } + } + for _, pascal := range []string{"Status", "ErrorMessage", "RunID", "WorkspaceID"} { + if _, ok := body[pascal]; ok { + t.Fatalf("pascal key %q leaked into %s", pascal, raw) + } + } + if body["status"] != "failed" || body["error_message"] != "dial tcp connection refused" { + t.Fatalf("payload=%s", raw) + } +} + +func TestGetTaskRunJSONUsesSnakeCase(t *testing.T) { + srv := newTestServer(t) + ts := httptest.NewServer(srv.Routes()) + t.Cleanup(ts.Close) + + runID := "run_snake_http" + rec := &taskrun.RunRecord{ + RunID: runID, + SessionID: "sess-snake", + Status: "failed", + ErrorMessage: "dial tcp connection refused", + TaskKey: "intake", + UserID: 1, + } + if _, err := srv.taskRuns.CreateAndStart( + t.Context(), + runID, + rec, + bizdev.TaskRunRequest{TaskKey: "intake", ProjectName: "snake-json"}, + bizdev.StageRunRequest{}, + "", + ); err != nil { + t.Fatalf("CreateAndStart: %v", err) + } + + token := testBearerToken(t, ts.URL, &config.Config{ + AdminUsername: "admin", + AdminPassword: "admin-test-pass", + }) + req, err := http.NewRequest(http.MethodGet, ts.URL+"/api/v1/tasks/runs/"+runID, nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Authorization", "Bearer "+token) + resp, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + payload, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + t.Fatalf("GET status=%d body=%s", resp.StatusCode, payload) + } + var body map[string]any + if err := json.Unmarshal(payload, &body); err != nil { + t.Fatalf("decode: %v body=%s", err, payload) + } + status, _ := body["status"].(string) + if status == "" { + t.Fatalf("status key missing or empty (pascal leak?): %s", payload) + } + if _, ok := body["Status"]; ok || strings.Contains(string(payload), `"ErrorMessage"`) { + t.Fatalf("pascal keys in GET body: %s", payload) + } + if _, ok := body["error_message"]; !ok { + t.Fatalf("missing error_message: %s", payload) + } + if _, ok := body["run_id"]; !ok { + t.Fatalf("missing run_id: %s", payload) + } +}