test(explore): #1500 regression fixtures for budget allocation (CG-6)
Two permanent fixtures pinning the failure mode from issue #1500 — explore spending its byte envelope on files that merely name-collide with the query. BOTH FAIL TODAY, by design: they document the bug and become the pass gate for CG-10 (scoring) + CG-12 (proportional allocation). __tests__/fixtures/payroll-go/ — a synthetic Go service mirroring the reporter's shape: generated FKIT CRUD beside a hand-written payroll use-case, entered from an HTTP route. Half the generated tree carries ORDINARY names detectable only by their `// Code generated ... DO NOT EDIT.` header (the #1500 case, and end-to-end cover for CG-5); `payrollpb/*.pb.go` covers the path-detectable channel. BuildPayslip, Upsert and Store each exist twice, generated and hand-written. cycle.go sits above the whole-file window so it clips; the generated files sit below it so they ship whole. Asking "how does payroll cycle create and calculate payslips?" — naming none of the answering symbols — the generated CRUD delivers 57.4% of the envelope against the hand-written layer's 25.6%, all of the latter domain types. cycle.go is allocated the single largest slice (30.6%) and delivers ZERO: the hard ceiling drops its whole section. runPayrollCycleAll, the hand-written BuildPayslip and the real Upsert never reach the agent. The second fixture is this repo, "how does explore allocate its output budget across files", where scripts/agent-eval/*.mjs take 71.8% against tools.ts's 18.5% despite scoring 4.6x lower. It reads the live index, so its assertions are relative rather than fixed percentages. - scripts/agent-eval/probe-allocation.mjs — per-file budget-share probe, driving the CG-4 diagnostic through a JSONL sidecar so it measures the shipping allocator. Fixture entries are hermetic (copy + re-index per run, verified byte-identical across runs); exits 1 while any assertion fails. - scripts/agent-eval/allocation-fixtures.json — both fixtures declared, with the 2026-08-03 baselines. - __tests__/explore-allocation-1500.test.ts — fixture-shape assertions green today; the allocation assertions held as `it.fails` so the suite stays green while the bug is open and goes RED the moment it is fixed. Also documented and deliberately left unfixed: runPayrollCycleAll's `s.store.Upsert` edge resolves to the GENERATED Store.Upsert, not the hand-written one — same-name method resolution across two packages picks the wrong receiver. It is upstream of the allocation bug, so it belongs with CG-10's scoring work. Refs #1500 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
16e17495f4
commit
bd86ad2061
@@ -0,0 +1,96 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/example/payroll-svc/internal/usecase/payroll"
|
||||
)
|
||||
|
||||
// PayrollHandler is the HTTP entry point into the payroll use-case layer.
|
||||
type PayrollHandler struct {
|
||||
svc *payroll.Service
|
||||
}
|
||||
|
||||
func NewPayrollHandler(svc *payroll.Service) *PayrollHandler {
|
||||
return &PayrollHandler{svc: svc}
|
||||
}
|
||||
|
||||
type runCycleRequest struct {
|
||||
DryRun bool `json:"dryRun"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type runCycleResponse struct {
|
||||
CycleID string `json:"cycleId"`
|
||||
Payslips int `json:"payslips"`
|
||||
GrossCents int64 `json:"grossCents"`
|
||||
NetCents int64 `json:"netCents"`
|
||||
}
|
||||
|
||||
// RunCycle kicks off a payroll cycle: it hands the cycle id to the use-case
|
||||
// layer, which builds and persists a payslip per active employee.
|
||||
func (h *PayrollHandler) RunCycle(w http.ResponseWriter, r *http.Request) {
|
||||
cycleID := r.PathValue("cycleID")
|
||||
if cycleID == "" {
|
||||
httpError(w, http.StatusBadRequest, "cycleID is required")
|
||||
return
|
||||
}
|
||||
|
||||
var req runCycleRequest
|
||||
if r.ContentLength > 0 {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpError(w, http.StatusBadRequest, "malformed body")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
result, err := h.svc.RunCycle(r.Context(), cycleID, payroll.RunOptions{
|
||||
DryRun: req.DryRun,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, payroll.ErrCycleClosed) {
|
||||
httpError(w, http.StatusConflict, "cycle already closed")
|
||||
return
|
||||
}
|
||||
httpError(w, http.StatusInternalServerError, "run failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, runCycleResponse{
|
||||
CycleID: result.CycleID,
|
||||
Payslips: len(result.Payslips),
|
||||
GrossCents: result.TotalGrossCents,
|
||||
NetCents: result.TotalNetCents,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *PayrollHandler) GetCycle(w http.ResponseWriter, r *http.Request) {
|
||||
cycle, err := h.svc.Cycle(r.Context(), r.PathValue("cycleID"))
|
||||
if err != nil {
|
||||
httpError(w, http.StatusNotFound, "no such cycle")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, cycle)
|
||||
}
|
||||
|
||||
func (h *PayrollHandler) ListPayslips(w http.ResponseWriter, r *http.Request) {
|
||||
slips, err := h.svc.PayslipsForCycle(r.Context(), r.PathValue("cycleID"))
|
||||
if err != nil {
|
||||
httpError(w, http.StatusNotFound, "no such cycle")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, slips)
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
_ = json.NewEncoder(w).Encode(body)
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, status int, msg string) {
|
||||
writeJSON(w, status, map[string]string{"error": msg})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package httpapi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// NewRouter wires the HTTP surface. The payroll cycle endpoint is the only
|
||||
// entry point into the hand-written use-case layer.
|
||||
func NewRouter(h *PayrollHandler) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("POST /v1/payroll/cycles/{cycleID}/run", h.RunCycle)
|
||||
mux.HandleFunc("GET /v1/payroll/cycles/{cycleID}", h.GetCycle)
|
||||
mux.HandleFunc("GET /v1/payroll/cycles/{cycleID}/payslips", h.ListPayslips)
|
||||
mux.HandleFunc("GET /healthz", health)
|
||||
return mux
|
||||
}
|
||||
|
||||
func health(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
Reference in New Issue
Block a user