package api import ( "encoding/json" "net/http" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // v0.46.0 (API half, controller channel) — the full pull round-trip on /report: // 1. baseline ACK omits controller_log_requested // 2. operator requests the controller bundle → the ACK advertises it // 3. the next report carries controller_log_tail → stored + request cleared // 4. the FOLLOWING ACK omits the flag again (consume-once) func TestReportACK_ControllerLogRoundTrip(t *testing.T) { h, st, _ := newTestHandler(t) reportBody := `{"customer_id":"cust-a"}` rr := do(h, http.MethodPost, "/report", globalKey, reportBody) if rr.Code != http.StatusOK { t.Fatalf("report POST = %d (%s)", rr.Code, rr.Body.String()) } var ack map[string]json.RawMessage json.Unmarshal(rr.Body.Bytes(), &ack) if _, present := ack["controller_log_requested"]; present { t.Fatalf("baseline ACK must omit controller_log_requested: %s", rr.Body.String()) } if err := st.RequestLogBundle("cust-a", store.LogBundleComponentController); err != nil { t.Fatal(err) } rr = do(h, http.MethodPost, "/report", globalKey, reportBody) ack = map[string]json.RawMessage{} json.Unmarshal(rr.Body.Bytes(), &ack) if string(ack["controller_log_requested"]) != "true" { t.Fatalf("ACK controller_log_requested = %s, want true", ack["controller_log_requested"]) } tailReport := `{"customer_id":"cust-a","controller_log_tail":{"collected_at":"2026-07-11T10:00:00Z","lines":["[DEBUG] phase agent_add -> verifying","[INFO] operator log pull served (controller ring)"]}}` rr = do(h, http.MethodPost, "/report", globalKey, tailReport) if rr.Code != http.StatusOK { t.Fatalf("tail report POST = %d", rr.Code) } bundles, err := st.GetLogBundles("cust-a") if err != nil || len(bundles) != 1 || bundles[0].Component != store.LogBundleComponentController { t.Fatalf("stored bundles = %+v err=%v, want 1 controller bundle", bundles, err) } _, lines, _ := st.GetLogBundleContent(bundles[0].ID, "cust-a") if len(lines) != 2 || lines[0] != "[DEBUG] phase agent_add -> verifying" { t.Fatalf("bundle lines not stored verbatim: %#v", lines) } rr = do(h, http.MethodPost, "/report", globalKey, reportBody) ack = map[string]json.RawMessage{} json.Unmarshal(rr.Body.Bytes(), &ack) if _, present := ack["controller_log_requested"]; present { t.Fatalf("request survived fulfillment — the controller would ship every cycle: %s", rr.Body.String()) } } // v0.46.0 (API half, agent channel) — the heartbeat round-trip on /host-report, // PLUS S6: a pre-0.83 agent that never ships log_tail leaves the request visibly // PENDING across heartbeats (documented, harmless — the envelope just keeps // advertising). Companion red-proof: clear the request at advertise time instead of // arrival → the stays-pending assertion fails. func TestHostReportEnvelope_AgentLogRoundTripAndOldAgentPending(t *testing.T) { h, st, _ := newTestHandler(t) st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"}) // Baseline: no flag. rr := do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1")) var env map[string]json.RawMessage json.Unmarshal(rr.Body.Bytes(), &env) if _, present := env["log_tail_requested"]; present { t.Fatalf("baseline envelope must omit log_tail_requested: %s", rr.Body.String()) } // Request → advertised. if err := st.RequestLogBundle("h1", store.LogBundleComponentAgent); err != nil { t.Fatal(err) } rr = do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1")) env = map[string]json.RawMessage{} json.Unmarshal(rr.Body.Bytes(), &env) if string(env["log_tail_requested"]) != "true" { t.Fatalf("envelope log_tail_requested = %s, want true", env["log_tail_requested"]) } // S6: an old agent ignores the flag — after N plain heartbeats the request is STILL pending. for i := 0; i < 3; i++ { do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1")) } if pending, _ := st.PendingLogBundleRequest("h1", store.LogBundleComponentAgent); !pending { t.Fatal("pre-0.83 request vanished without fulfillment — it must stay visibly pending") } // A 0.83 agent ships the tail → stored + cleared. tailBody := `{"host_id":"h1","host":{},"guests":[],"storage_targets":[],"backups":[],"cloudflared":{},"audit_tail":[],` + `"log_tail":{"collected_at":"2026-07-11T10:00:00Z","lines":["2026-07-11T10:00:00Z [INFO] operator log pull served"]}}` rr = do(h, http.MethodPost, "/host-report", "HKEY", tailBody) if rr.Code != http.StatusOK { t.Fatalf("tail heartbeat = %d (%s)", rr.Code, rr.Body.String()) } bundles, err := st.GetLogBundles("h1") if err != nil || len(bundles) != 1 || bundles[0].Component != store.LogBundleComponentAgent { t.Fatalf("agent bundle = %+v err=%v", bundles, err) } if pending, _ := st.PendingLogBundleRequest("h1", store.LogBundleComponentAgent); pending { t.Error("request survived fulfillment (consume-once broken)") } // The following envelope no longer advertises. rr = do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1")) env = map[string]json.RawMessage{} json.Unmarshal(rr.Body.Bytes(), &env) if _, present := env["log_tail_requested"]; present { t.Fatalf("envelope still advertises after fulfillment: %s", rr.Body.String()) } }