go-goconst-test-literal-regression
Root cause. Post-campaign commits put repeated string literals back into test files: /api/gap/016 (3 goconst-visible occurrences in routes/routes_test.go) and 0 2 * * * (7 in cron/cron_test.go). The diff-scoped gitreins guard only lints changed hunks, so the reintroduction slipped through; the canonical gate is a full-repo goconst run.
Gate (golangci-lint v2). --disable-all was removed in v2 (Error: unknown flag: --disable-all); the equivalent is --enable-only:
golangci-lint run --enable-only goconst --output.json.path goconst.json # exit 1 = violations
Before fix: 2 issues: string "0 2 * * *" has 7 occurrences ... string "/api/gap/016" has 3 occurrences (exit 1).
Fix 1 — package-level Str* const in routes/constants_test.go:
package routes
// StrGAP016Route is the canonical route path for the GAP-016 endpoint.
const StrGAP016Route = "/api/gap/016"
routes/routes_test.go — replace all uses (call arg included):
func TestGAP016Route(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, StrGAP016Route, nil)
if req.URL.Path != StrGAP016Route {
t.Fatalf("unexpected path: got %q want %q", req.URL.Path, StrGAP016Route)
}
byPath := map[string]bool{StrGAP016Route: true}
if !byPath[req.URL.Path] {
t.Fatal("route not registered")
}
rr := httptest.NewRecorder()
Handler().ServeHTTP(rr, req)
if rr.Code != http.StatusNoContent {
t.Fatalf("status = %d, want %d", rr.Code, http.StatusNoContent)
}
}
Fix 2 — function-local test* const in cron/cron_test.go (used only by this test):
func TestDogfood003Schedule(t *testing.T) {
const testDogfood003Schedule = "0 2 * * *"
defs := []Schedule{
{Job: "dogfood-003", Schedule: testDogfood003Schedule, Enabled: true},
{Job: "dogfood-003-staging", Schedule: testDogfood003Schedule, Enabled: true},
{Job: "dogfood-003-canary", Schedule: testDogfood003Schedule, Enabled: true},
}
got := EnabledSchedules(defs)
want := []string{testDogfood003Schedule, testDogfood003Schedule, testDogfood003Schedule}
if !reflect.DeepEqual(got, want) {
t.Fatalf("schedules = %v, want %v", got, want)
}
// Backtick JSON body: interpolate with %q — never blind-replace inside it.
body := fmt.Sprintf(`{"job":"dogfood-003","schedule":%q,"enabled":true}`, testDogfood003Schedule)
var payload map[string]any
if err := json.Unmarshal([]byte(body), &payload); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if payload["schedule"] != testDogfood003Schedule {
t.Fatalf("payload schedule = %v", payload["schedule"])
}
}
replace_all pitfalls (why you cannot blanket-replace):
1. Init cycle — a naive replace rewrites the const declaration itself: const StrGAP016Route = StrGAP016Route → build fails with initialization cycle: StrGAP016Route refers to itself.
2. Backtick JSON bodies — a substring replace rewrites body :={"job":"dogfood-003","schedule":"0 2 * * *","enabled":true}` into"schedule":"testDogfood003Schedule", injecting the identifier as a *value* and corrupting the payload/assertion. goconst never counts it anyway (it compares whole literal values; the schedule is a substring of the JSON document), so the body must be rebuilt viafmt.Sprintf(..., %q, const)` or left as data — never substituted textually.
Reproduced in `/tmp/goconst-repro` (module `example.com/goconst-repro`, golangci-lint v2.12.2, Go 1.26.0). | Check | Result | |---|---| | `golangci-lint run --disable-all` | `Error: unknown flag: --disable-all` (v2 removed it) | | Gate BEFORE fix (`--enable-only goconst --output.json.path ...`) | exit 1 — `"0 2 * * *"` x7 @ `cron/cron_test.go:13`, `"/api/gap/016"` x3 @ `routes/routes_test.go:15` | | Pitfall 1: `const StrGAP016Route = StrGAP016Route` | `go build` → `initialization cycle: StrGAP016Route refers to itself` (exit 1) | | Pitfall 2: `sed 's/0 2 \* \* \*/testDogfood003Schedule/g'` on a copy | backtick body became `"schedule":"testDogfood003Schedule"` — identifier injected as JSON value | | `go build ./...` (fixed) | OK | | `go vet ./...` (fixed) | OK | | `go test ./...` (fixed) | `ok example.com/goconst-repro/cron`, `ok example.com/goconst-repro/routes` | | Gate AFTER fix | exit 0 — **0 issues** (verified via `output.json.path` JSON `Issues` count) | Edge cases tested: goconst's `ignore-calls` default (call args like `httptest.NewRequest(..., "/api/gap/016", ...)` and `t.Fatalf` args are excluded from the occurrence count — the route path was reworked to 3 non-call occurrences so the gate actually catches it); raw-string (`backtick`) bodies are counted only when the *whole* literal value matches, so JSON substrings are invisible to goconst but still get corrupted by naive replaces; threshold is `min-occurrences: 3` with `min-len: 3`; final state leaves exactly one occurrence each in the const declarations and the production `HandleFunc` registration (legitimately below threshold). Tests: 6 verification steps above.
{"model": "deepseek-v4-flash", "problem_class": "go-goconst-test-literal-regression", "result": "passed", "tests": 6}