From f4dde07ef1689d8be8220a8fcdb8db6636cb5e4c Mon Sep 17 00:00:00 2001 From: rgb-vgx Date: Sun, 4 Oct 2026 12:11:34 +0700 Subject: [PATCH] fix(group): force leading slash on Go route paths for contract-id parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit normalizeHttpPath (the shared contract-id normalizer) does not add a leading slash, while ingestion's normalizeExtractedRoutePath always does — so a literal path "x" or a slashless Group("api") prefix split the contract id across the two strategies (`http::GET::x` vs `http::GET::/x`). After joining and collapsing "//", ensure the result starts with "/" so both strategies feed the same bytes into the id. Gin also panics when registering a route path without a leading slash. Tests: go-gin-route-groups 35, group suite 1298 (all green). Co-Authored-By: Claude Code --- .../core/group/extractors/http-patterns/go.ts | 8 ++++++- .../unit/group/go-gin-route-groups.test.ts | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/group/extractors/http-patterns/go.ts b/gitnexus/src/core/group/extractors/http-patterns/go.ts index 338aebbb4..ed09897a9 100644 --- a/gitnexus/src/core/group/extractors/http-patterns/go.ts +++ b/gitnexus/src/core/group/extractors/http-patterns/go.ts @@ -108,7 +108,13 @@ function joinRoutePath(prefix: string, relative: string): string { // just the join — because ingestion's normalizeExtractedRoutePath collapses // all "//" while the downstream contract-id normalizer does not: a path // that keeps "//" would split into two contract ids across the strategies. - return joined.replace(/\/+/g, '/'); + const collapsed = joined.replace(/\/+/g, '/'); + // Force a leading "/" for the same reason: ingestion's + // normalizeExtractedRoutePath always adds one, while normalizeHttpPath (the + // shared contract-id normalizer) does not — a literal "x" or a slashless + // Group("api") prefix would emit `...::x` here and `...::/x` there. Gin + // also refuses a registration path that does not start with "/". + return collapsed.startsWith('/') ? collapsed : `/${collapsed}`; } /** `parent.Group("/p", mw...)` → its receiver and literal prefix; null otherwise. */ diff --git a/gitnexus/test/unit/group/go-gin-route-groups.test.ts b/gitnexus/test/unit/group/go-gin-route-groups.test.ts index 578e7c25f..ead28a609 100644 --- a/gitnexus/test/unit/group/go-gin-route-groups.test.ts +++ b/gitnexus/test/unit/group/go-gin-route-groups.test.ts @@ -441,6 +441,28 @@ func routes(r *gin.Engine) { ]); }); + it('forces a leading slash on slashless literals and group prefixes', () => { + // Ingestion's normalizeExtractedRoutePath always adds a leading "/" but + // normalizeHttpPath (the shared contract-id normalizer) does not — a path + // like "x" would become `http::GET::x` here and `http::GET::/x` there, + // splitting the id across the two strategies. Gin likewise panics when a + // route is registered without one. + expect( + providers(`package main +func routes(r *gin.Engine) { + r.GET("x", h.X) + g := r.Group("api") + g.GET("y", h.Y) + r.GET("", h.Root) +} +`), + ).toEqual([ + { method: 'GET', path: '/x', name: 'X' }, + { method: 'GET', path: '/api/y', name: 'Y' }, + { method: 'GET', path: '/', name: 'Root' }, + ]); + }); + it("binds echo's handler (first argument) when the file imports echo only", () => { // echo is `GET(path, handler, middleware...)` — the handler is second, // not last, so a middleware selector must not become the route's name.