Skip to content

Commit 8db90b0

Browse files
atulmguptaCopilot
andcommitted
refactor(R2d.133): carve internal/api/aifeedtri subpackage
Move the AI feedback queue triage handler and tests into a dedicated aifeedtri handler package. Update router wiring to use the new package-level constructor names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent edf40c1 commit 8db90b0

4 files changed

Lines changed: 73 additions & 46 deletions

File tree

internal/api/aifeedtri/doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Package aifeedtri provides the AI feedback queue triage HTTP handler.
2+
// Layer: handler
3+
package aifeedtri

internal/api/ai_feedback_triage_handler.go renamed to internal/api/aifeedtri/handler.go

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
package api
1+
package aifeedtri
22

33
// Phase-50 / 0046 — S5 Feedback queue triage.
44
//
5-
// ai_feedback_triage_handler.go implements the LLM-backed handler
5+
// handler.go implements the LLM-backed handler
66
// at POST /api/v1/ai/feedback/triage/draft. The flow mirrors
77
// ai_log_trace_summarization_handler.go (body-driven, scope-bound,
88
// no persistence — one-shot read-then-propose):
@@ -68,6 +68,7 @@ package api
6868
// persistence path.
6969

7070
import (
71+
"bytes"
7172
"context"
7273
"encoding/json"
7374
"errors"
@@ -85,6 +86,7 @@ import (
8586
"github.com/ev-dev-labs/teslasync/internal/ai/stream"
8687
"github.com/ev-dev-labs/teslasync/internal/ai/tools"
8788
"github.com/ev-dev-labs/teslasync/internal/ai/tools/feedback"
89+
"github.com/ev-dev-labs/teslasync/internal/api/httpx"
8890
tsauth "github.com/ev-dev-labs/teslasync/internal/auth"
8991
dbuser "github.com/ev-dev-labs/teslasync/internal/database/user"
9092
)
@@ -109,6 +111,10 @@ const aiFeedbackTriageMaxBodyBytes = 16 * 1024
109111
// excerpt is always a strict subset of the persisted column.
110112
const aiFeedbackTriageBodyExcerptMaxChars = 4096
111113

114+
func denyAllConfirm(_ context.Context, _ dispatch.ConfirmRequest) (dispatch.ConfirmDecision, error) {
115+
return dispatch.ConfirmDenied, nil
116+
}
117+
112118
// aiFeedbackTriageRequest is the typed body shape. feedback_id is
113119
// the only required field.
114120
type aiFeedbackTriageRequest struct {
@@ -117,13 +123,13 @@ type aiFeedbackTriageRequest struct {
117123
FeedbackID int64 `json:"feedback_id"`
118124
}
119125

120-
// AIFeedbackQueueTriageHandler is the HTTP handler for
126+
// Handler is the HTTP handler for
121127
// POST /api/v1/ai/feedback/triage/draft.
122128
//
123129
// Stateless beyond its constructor inputs; safe for concurrent use
124130
// across requests. Construction is in router.go so the dispatcher's
125131
// tool registry + provider registry are wired once at boot.
126-
type AIFeedbackQueueTriageHandler struct {
132+
type Handler struct {
127133
registry *provider.Registry
128134
tools *tools.Registry
129135
strategy strategy.Strategy
@@ -132,7 +138,7 @@ type AIFeedbackQueueTriageHandler struct {
132138
maxIters int
133139
}
134140

135-
// NewAIFeedbackQueueTriageHandler constructs the handler. All
141+
// NewHandler constructs the handler. All
136142
// non-pointer arguments are required; the constructor panics on a
137143
// nil so the wiring bug surfaces at boot, not at first request.
138144
//
@@ -155,24 +161,24 @@ type AIFeedbackQueueTriageHandler struct {
155161
// headerName: forward-auth header name; used to extract subject
156162
//
157163
// for audit.
158-
func NewAIFeedbackQueueTriageHandler(
164+
func NewHandler(
159165
registry *provider.Registry,
160166
toolReg *tools.Registry,
161167
strat strategy.Strategy,
162168
source feedback.FeedbackTriageSource,
163169
headerName string,
164-
) *AIFeedbackQueueTriageHandler {
170+
) *Handler {
165171
switch {
166172
case registry == nil:
167-
panic("api: NewAIFeedbackQueueTriageHandler: nil provider.Registry")
173+
panic("aifeedtri: NewHandler: nil provider.Registry")
168174
case toolReg == nil:
169-
panic("api: NewAIFeedbackQueueTriageHandler: nil tools.Registry")
175+
panic("aifeedtri: NewHandler: nil tools.Registry")
170176
case strat == nil:
171-
panic("api: NewAIFeedbackQueueTriageHandler: nil strategy.Strategy")
177+
panic("aifeedtri: NewHandler: nil strategy.Strategy")
172178
case source == nil:
173-
panic("api: NewAIFeedbackQueueTriageHandler: nil feedback.FeedbackTriageSource")
179+
panic("aifeedtri: NewHandler: nil feedback.FeedbackTriageSource")
174180
}
175-
return &AIFeedbackQueueTriageHandler{
181+
return &Handler{
176182
registry: registry,
177183
tools: toolReg,
178184
strategy: strat,
@@ -189,27 +195,27 @@ func NewAIFeedbackQueueTriageHandler(
189195
func parseFeedbackTriageRequest(w http.ResponseWriter, r *http.Request) (aiFeedbackTriageRequest, bool) {
190196
var req aiFeedbackTriageRequest
191197
if r.Body == nil {
192-
writeError(w, http.StatusBadRequest, "missing body")
198+
httpx.WriteError(w, http.StatusBadRequest, "missing body")
193199
return req, false
194200
}
195201
defer r.Body.Close()
196202
bodyBytes, readErr := io.ReadAll(io.LimitReader(r.Body, aiFeedbackTriageMaxBodyBytes))
197203
if readErr != nil {
198-
writeError(w, http.StatusBadRequest, fmt.Sprintf("failed to read body: %v", readErr))
204+
httpx.WriteError(w, http.StatusBadRequest, fmt.Sprintf("failed to read body: %v", readErr))
199205
return req, false
200206
}
201-
if len(bytesTrim(bodyBytes)) == 0 {
202-
writeError(w, http.StatusBadRequest, "empty body")
207+
if len(bytes.TrimSpace(bodyBytes)) == 0 {
208+
httpx.WriteError(w, http.StatusBadRequest, "empty body")
203209
return req, false
204210
}
205211
dec := json.NewDecoder(strings.NewReader(string(bodyBytes)))
206212
dec.DisallowUnknownFields()
207213
if err := dec.Decode(&req); err != nil {
208-
writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid JSON body: %v", err))
214+
httpx.WriteError(w, http.StatusBadRequest, fmt.Sprintf("invalid JSON body: %v", err))
209215
return req, false
210216
}
211217
if req.FeedbackID <= 0 {
212-
writeError(w, http.StatusBadRequest, "feedback_id must be > 0")
218+
httpx.WriteError(w, http.StatusBadRequest, "feedback_id must be > 0")
213219
return req, false
214220
}
215221
return req, true
@@ -220,7 +226,7 @@ func parseFeedbackTriageRequest(w http.ResponseWriter, r *http.Request) (aiFeedb
220226
// dispatcher's deferred WriteDone. Every error path either writes
221227
// a structured frame onto the SSE stream (when the writer has been
222228
// opened) or a plain JSON 4xx/5xx (before it has).
223-
func (h *AIFeedbackQueueTriageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
229+
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
224230
// 1) Parse + validate the request body.
225231
req, ok := parseFeedbackTriageRequest(w, r)
226232
if !ok {
@@ -233,7 +239,7 @@ func (h *AIFeedbackQueueTriageHandler) ServeHTTP(w http.ResponseWriter, r *http.
233239
// stream — emit JSON 502 so the frontend falls back gracefully.
234240
if _, err := h.registry.For(r.Context(), feedbackqueuetriage.FeatureID); err != nil {
235241
log.Error().Err(err).Msg("ai feedback-queue-triage: provider.For failed")
236-
writeError(w, http.StatusBadGateway, "ai provider unavailable")
242+
httpx.WriteError(w, http.StatusBadGateway, "ai provider unavailable")
237243
return
238244
}
239245

@@ -251,7 +257,7 @@ func (h *AIFeedbackQueueTriageHandler) ServeHTTP(w http.ResponseWriter, r *http.
251257
sseW, ctx, err := stream.New(ctx, w, stream.WithFeatureID(feedbackqueuetriage.FeatureID))
252258
if err != nil {
253259
log.Error().Err(err).Msg("ai feedback-queue-triage: stream.New failed (non-flushable writer)")
254-
writeError(w, http.StatusInternalServerError, "streaming not supported")
260+
httpx.WriteError(w, http.StatusInternalServerError, "streaming not supported")
255261
return
256262
}
257263

@@ -314,9 +320,9 @@ func synthesizeFeedbackTriageUserMessage(feedbackID int64) string {
314320
)
315321
}
316322

317-
// Compile-time assertion: AIFeedbackQueueTriageHandler satisfies
323+
// Compile-time assertion: Handler satisfies
318324
// http.Handler.
319-
var _ http.Handler = (*AIFeedbackQueueTriageHandler)(nil)
325+
var _ http.Handler = (*Handler)(nil)
320326

321327
// ---------------------------------------------------------------------
322328
// Production wiring for the tool interface declared by
@@ -325,7 +331,7 @@ var _ http.Handler = (*AIFeedbackQueueTriageHandler)(nil)
325331
// the log-trace-summarization slice's AILogTraceWindowSource pattern.
326332
// ---------------------------------------------------------------------
327333

328-
// AIFeedbackTriageSource is the production
334+
// FeedbackTriageSource is the production
329335
// feedback.FeedbackTriageSource. It wraps the canonical
330336
// *dbuser.UserFeedbackRepo.Get and PII-minimizes the row into a
331337
// FeedbackTriageEntry: only id / created_at / category / title /
@@ -334,18 +340,18 @@ var _ http.Handler = (*AIFeedbackQueueTriageHandler)(nil)
334340
// submitter_subject, submitter_ip, recent_errors, and console_tail
335341
// are NOT forwarded — defence in depth on top of
336342
// PolicyAlertBuilder's deny-by-default redaction.
337-
type AIFeedbackTriageSource struct {
343+
type FeedbackTriageSource struct {
338344
repo *dbuser.UserFeedbackRepo
339345
}
340346

341-
// NewAIFeedbackTriageSource constructs the production source
347+
// NewFeedbackTriageSource constructs the production source
342348
// adapter. Panics on a nil repo so the wiring bug surfaces at boot,
343349
// not at first request.
344-
func NewAIFeedbackTriageSource(repo *dbuser.UserFeedbackRepo) *AIFeedbackTriageSource {
350+
func NewFeedbackTriageSource(repo *dbuser.UserFeedbackRepo) *FeedbackTriageSource {
345351
if repo == nil {
346-
panic("api: NewAIFeedbackTriageSource: nil *dbuser.UserFeedbackRepo")
352+
panic("aifeedtri: NewFeedbackTriageSource: nil *dbuser.UserFeedbackRepo")
347353
}
348-
return &AIFeedbackTriageSource{repo: repo}
354+
return &FeedbackTriageSource{repo: repo}
349355
}
350356

351357
// LoadFeedback implements feedback.FeedbackTriageSource. Returns
@@ -357,7 +363,7 @@ func NewAIFeedbackTriageSource(repo *dbuser.UserFeedbackRepo) *AIFeedbackTriageS
357363
// The body is truncated to aiFeedbackTriageBodyExcerptMaxChars to
358364
// bound the prompt-token budget; the canonical body is preserved
359365
// in the database column unchanged.
360-
func (a *AIFeedbackTriageSource) LoadFeedback(ctx context.Context, feedbackID int64) (*feedback.FeedbackTriageEntry, error) {
366+
func (a *FeedbackTriageSource) LoadFeedback(ctx context.Context, feedbackID int64) (*feedback.FeedbackTriageEntry, error) {
361367
row, err := a.repo.Get(ctx, feedbackID)
362368
if err != nil {
363369
if errors.Is(err, dbuser.ErrFeedbackNotFound) {
@@ -382,6 +388,6 @@ func (a *AIFeedbackTriageSource) LoadFeedback(ctx context.Context, feedbackID in
382388
}, nil
383389
}
384390

385-
// Compile-time assertion: AIFeedbackTriageSource satisfies
391+
// Compile-time assertion: FeedbackTriageSource satisfies
386392
// feedback.FeedbackTriageSource.
387-
var _ feedback.FeedbackTriageSource = (*AIFeedbackTriageSource)(nil)
393+
var _ feedback.FeedbackTriageSource = (*FeedbackTriageSource)(nil)

internal/api/ai_feedback_triage_handler_test.go renamed to internal/api/aifeedtri/handler_test.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
// feedback-queue-triage`); duplicating that here would require a
1515
// live database fixture.
1616

17-
package api
17+
package aifeedtri
1818

1919
import (
2020
"bytes"
21+
"context"
2122
"net/http"
2223
"net/http/httptest"
2324
"strings"
@@ -28,6 +29,22 @@ import (
2829
"github.com/ev-dev-labs/teslasync/internal/ai/guard"
2930
)
3031

32+
type stubGuardSettings struct {
33+
mode string
34+
on map[string]bool
35+
}
36+
37+
func (s *stubGuardSettings) AIMode(_ context.Context) (string, error) {
38+
if s.mode == "" {
39+
return "off", nil
40+
}
41+
return s.mode, nil
42+
}
43+
44+
func (s *stubGuardSettings) AIFeatureEnabled(_ context.Context, id string) (bool, error) {
45+
return s.on[id], nil
46+
}
47+
3148
// TestFeedbackTriageAIOffManualLabelsWork is the load-bearing
3249
// off-mode contract proof for slice 0046. It mounts the AI
3350
// feedback-queue-triage route through the guard with ai_mode='off'
@@ -140,47 +157,47 @@ func TestFeedbackTriageAIOffManualLabelsWork(t *testing.T) {
140157
}
141158
}
142159

143-
// TestAIFeedbackQueueTriageHandler_PanicsOnNilWiring asserts the
160+
// TestHandler_PanicsOnNilWiring asserts the
144161
// handler constructor refuses zero-valued dependencies. A wiring
145162
// bug at boot must surface as a panic, not as a nil-deref on
146163
// first request.
147-
func TestAIFeedbackQueueTriageHandler_PanicsOnNilWiring(t *testing.T) {
164+
func TestHandler_PanicsOnNilWiring(t *testing.T) {
148165
t.Parallel()
149166
cases := []struct {
150167
name string
151168
fn func()
152169
}{
153-
{"all nil", func() { NewAIFeedbackQueueTriageHandler(nil, nil, nil, nil, "") }},
170+
{"all nil", func() { NewHandler(nil, nil, nil, nil, "") }},
154171
}
155172
for _, tc := range cases {
156173
t.Run(tc.name, func(t *testing.T) {
157174
defer func() {
158175
if r := recover(); r == nil {
159-
t.Fatalf("NewAIFeedbackQueueTriageHandler(%s) did not panic", tc.name)
176+
t.Fatalf("NewHandler(%s) did not panic", tc.name)
160177
}
161178
}()
162179
tc.fn()
163180
})
164181
}
165182
}
166183

167-
// TestNewAIFeedbackTriageSource_PanicsOnNilRepo asserts the
184+
// TestNewFeedbackTriageSource_PanicsOnNilRepo asserts the
168185
// production source adapter constructor refuses a nil repo.
169-
func TestNewAIFeedbackTriageSource_PanicsOnNilRepo(t *testing.T) {
186+
func TestNewFeedbackTriageSource_PanicsOnNilRepo(t *testing.T) {
170187
t.Parallel()
171188
defer func() {
172189
if r := recover(); r == nil {
173-
t.Fatal("NewAIFeedbackTriageSource(nil) did not panic")
190+
t.Fatal("NewFeedbackTriageSource(nil) did not panic")
174191
}
175192
}()
176-
_ = NewAIFeedbackTriageSource(nil)
193+
_ = NewFeedbackTriageSource(nil)
177194
}
178195

179-
// TestAIFeedbackQueueTriageHandler_RejectsBadBody asserts the
196+
// TestHandler_RejectsBadBody asserts the
180197
// handler validates the body BEFORE opening the SSE stream — a
181198
// missing, unparseable, or out-of-range field must surface as a
182199
// JSON 400, not a half-opened stream that confuses the frontend.
183-
func TestAIFeedbackQueueTriageHandler_RejectsBadBody(t *testing.T) {
200+
func TestHandler_RejectsBadBody(t *testing.T) {
184201
t.Parallel()
185202

186203
cases := []struct {

internal/api/router.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
aidigest "github.com/ev-dev-labs/teslasync/internal/api/aidigest"
3232
aidrivecoach "github.com/ev-dev-labs/teslasync/internal/api/aidrivecoach"
3333
aidrivesearch "github.com/ev-dev-labs/teslasync/internal/api/aidrivesearch"
34+
aifeedtri "github.com/ev-dev-labs/teslasync/internal/api/aifeedtri"
3435
airaghelp "github.com/ev-dev-labs/teslasync/internal/api/airaghelp"
3536
airouteeff "github.com/ev-dev-labs/teslasync/internal/api/airouteeff"
3637
aisearch "github.com/ev-dev-labs/teslasync/internal/api/aisearch"
@@ -2163,9 +2164,9 @@ func NewRouter(db *database.DB, teslaClient *tesla.Client, mqttClient *mqtt.Clie
21632164
// other slice tools above: must be registered before the
21642165
// handler constructor below so the strategy's allowedTools
21652166
// resolve at boot. The Source is the production
2166-
// AIFeedbackTriageSource adapter that wraps userFeedbackRepo
2167+
// FeedbackTriageSource adapter that wraps userFeedbackRepo
21672168
// and PII-minimizes the row into a FeedbackTriageEntry.
2168-
aiFeedbackTriageSource := NewAIFeedbackTriageSource(userFeedbackRepo)
2169+
aiFeedbackTriageSource := aifeedtri.NewFeedbackTriageSource(userFeedbackRepo)
21692170
feedback.RegisterFeedbackQueueTriageTools(aiToolRegistry, feedback.FeedbackQueueTriageSources{
21702171
Source: aiFeedbackTriageSource,
21712172
Retriever: aiFeedbackTriageRetriever,
@@ -2174,7 +2175,7 @@ func NewRouter(db *database.DB, teslaClient *tesla.Client, mqttClient *mqtt.Clie
21742175
// beyond constructor inputs. Must be constructed AFTER the
21752176
// tool registration above so the dispatcher can resolve the
21762177
// strategy's allowedTools at boot.
2177-
aiFeedbackQueueTriageHandler := NewAIFeedbackQueueTriageHandler(
2178+
aiFeedbackQueueTriageHandler := aifeedtri.NewHandler(
21782179
aiRegistry,
21792180
aiToolRegistry,
21802181
feedbackqueuetriage.New(),

0 commit comments

Comments
 (0)