From 27dd089ff7541036e23c7a37c0132912bb46fbc1 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 15 Jun 2026 09:11:18 +0000 Subject: [PATCH 1/2] feat(ci): core-side platformPaths drift gate (#820)\n\nAdds a core-side test asserting that every route registered in\nworkspace-server/internal/router/router.go is covered by a regex in\nmolecule-controlplane/internal/cloudflareapi/tunnel.go's platformPaths list.\n\n- New test: TestCoreRoutesCoveredByPlatformPaths in\n workspace-server/internal/router/router_cf_drift_test.go.\n- New workflow: .gitea/workflows/cf-tunnel-platform-paths-drift.yml clones\n molecule-controlplane and runs the gate on PR/push to main/staging.\n- Fails in CI if the controlplane sibling checkout is absent; skips locally.\n\nCo-Authored-By: Claude --- .../cf-tunnel-platform-paths-drift.yml | 46 +++++ .../internal/router/router_cf_drift_test.go | 193 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 .gitea/workflows/cf-tunnel-platform-paths-drift.yml create mode 100644 workspace-server/internal/router/router_cf_drift_test.go diff --git a/.gitea/workflows/cf-tunnel-platform-paths-drift.yml b/.gitea/workflows/cf-tunnel-platform-paths-drift.yml new file mode 100644 index 000000000..c94f2c942 --- /dev/null +++ b/.gitea/workflows/cf-tunnel-platform-paths-drift.yml @@ -0,0 +1,46 @@ +# cf-tunnel-platform-paths-drift +# +# Prod-safety gate (molecule-controlplane#820): every top-level route in +# molecule-core/workspace-server/internal/router/router.go must be covered by a +# regex in molecule-controlplane/internal/cloudflareapi/tunnel.go's +# platformPaths list. If a core PR adds a tenant route without a matching +# platformPaths regex, production tenants will misroute it to canvas:3000. +# +# This workflow checks out molecule-controlplane (public repo) and runs the +# core-side drift test, catching the drift at the source rather than relying on +# the reactive controlplane gate. + +name: cf-tunnel-platform-paths-drift + +on: + push: + branches: [main, staging] + pull_request: + branches: [main, staging] + +env: + GITHUB_SERVER_URL: https://git.moleculesai.app + +jobs: + core-platform-paths-drift: + name: Core routes covered by platformPaths + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: checkout molecule-controlplane + # Manual git clone because actions/checkout@v6 fails on cross-repo + # public clones when the auto-injected GITEA_TOKEN is scoped to this + # repo only. molecule-controlplane is public — no token needed. + run: | + git clone --depth 1 https://git.moleculesai.app/molecule-ai/molecule-controlplane.git _molecule-controlplane + + - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version: '1.25' + cache: false + + - name: drift gate + env: + MOLECULE_CP_DIR: ${{ github.workspace }}/_molecule-controlplane + run: go test -count=1 -v -run TestCoreRoutesCoveredByPlatformPaths ./workspace-server/internal/router/ diff --git a/workspace-server/internal/router/router_cf_drift_test.go b/workspace-server/internal/router/router_cf_drift_test.go new file mode 100644 index 000000000..f30c6c756 --- /dev/null +++ b/workspace-server/internal/router/router_cf_drift_test.go @@ -0,0 +1,193 @@ +package router + +import ( + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "testing" +) + +// TestCoreRoutesCoveredByPlatformPaths is the molecule-core side of the +// cross-repo drift gate for cf-tunnel platformPaths (molecule-controlplane#820). +// It ensures that every top-level route registered in this router is matched by +// at least one regex in the controlplane's platformPaths list. Drift is caught +// in core CI when a new tenant route is added, instead of shipping a silent +// tunnel-misroute to canvas:3000. +// +// The test reads the controlplane tunnel.go source from a sibling checkout +// (env MOLECULE_CP_DIR) and parses the platformPaths slice. In CI the checkout +// is required; locally it skips if the file is absent. +func TestCoreRoutesCoveredByPlatformPaths(t *testing.T) { + cpTunnelPath := findControlPlaneTunnel(t) + if cpTunnelPath == "" { + if os.Getenv("CI") != "" { + t.Fatal("molecule-controlplane tunnel.go not found; set MOLECULE_CP_DIR to enable the platformPaths drift gate in CI") + } + t.Skip("molecule-controlplane tunnel.go not found; set MOLECULE_CP_DIR to enable cross-repo drift gate") + } + t.Logf("loading controlplane platformPaths from %s", cpTunnelPath) + + cpSrc, err := os.ReadFile(cpTunnelPath) + if err != nil { + t.Fatalf("read %s: %v", cpTunnelPath, err) + } + + platformPaths := extractPlatformPaths(string(cpSrc)) + if len(platformPaths) == 0 { + t.Fatalf("no platformPaths extracted from %s — extractor is broken", cpTunnelPath) + } + t.Logf("extracted %d platformPaths regex(es)", len(platformPaths)) + + platformRegexes := make([]*regexp.Regexp, 0, len(platformPaths)) + for _, p := range platformPaths { + re, err := regexp.Compile(p) + if err != nil { + t.Fatalf("platformPaths regex %q failed to compile: %v", p, err) + } + platformRegexes = append(platformRegexes, re) + } + + routerPath := "router.go" + if _, err := os.Stat(routerPath); err != nil { + t.Fatalf("cannot read own router.go at %s: %v", routerPath, err) + } + routerSrc, err := os.ReadFile(routerPath) + if err != nil { + t.Fatalf("read %s: %v", routerPath, err) + } + + routes := extractGinRoutes(string(routerSrc)) + if len(routes) == 0 { + t.Fatalf("no routes extracted from %s — extractor is broken", routerPath) + } + t.Logf("extracted %d route(s) from molecule-core router", len(routes)) + + var unmatched []string + for _, route := range routes { + if !routeMatchesAny(route, platformRegexes) { + unmatched = append(unmatched, route) + } + } + + if len(unmatched) > 0 { + sort.Strings(unmatched) + t.Errorf("\n=== platformPaths drift detected ===\n"+ + "%d molecule-core route(s) are NOT matched by any controlplane platformPaths regex.\n"+ + "They will silently misroute to canvas:3000 in production tenants.\n\n"+ + "Unmatched routes:\n - %s\n\n"+ + "Fix: add a matching regex to platformPaths in molecule-controlplane/internal/cloudflareapi/tunnel.go,\n"+ + "then add the same regex to the wantPlatformPaths lockstep test in tunnel_test.go.\n", + len(unmatched), + strings.Join(unmatched, "\n - ")) + } +} + +// findControlPlaneTunnel resolves the path to molecule-controlplane's +// internal/cloudflareapi/tunnel.go using the search order documented on the +// test. Returns "" if no candidate exists. +func findControlPlaneTunnel(t *testing.T) string { + t.Helper() + rel := filepath.Join("internal", "cloudflareapi", "tunnel.go") + candidates := []string{} + if env := os.Getenv("MOLECULE_CP_DIR"); env != "" { + candidates = append(candidates, filepath.Join(env, rel)) + } + if home, err := os.UserHomeDir(); err == nil { + candidates = append(candidates, filepath.Join(home, "molecule-controlplane", rel)) + } + candidates = append(candidates, filepath.Join("..", "..", "molecule-controlplane", rel)) + candidates = append(candidates, filepath.Join("..", "molecule-controlplane", rel)) + for _, c := range candidates { + if _, err := os.Stat(c); err == nil { + return c + } + } + return "" +} + +// extractPlatformPaths parses tunnel.go source and returns the platformPaths +// regex slice as it would be pushed to cloudflared. +func extractPlatformPaths(src string) []string { + startMarker := "platformPaths := []string{" + start := strings.Index(src, startMarker) + if start < 0 { + return nil + } + start += len(startMarker) + end := strings.Index(src[start:], "}") + if end < 0 { + return nil + } + block := src[start : start+end] + + // Each entry is a backtick-quoted Go raw string. + re := regexp.MustCompile("`([^`]*)`") + var out []string + for _, m := range re.FindAllStringSubmatch(block, -1) { + out = append(out, m[1]) + } + return out +} + +// extractGinRoutes parses router.go source and returns every concrete +// top-level path registered on the gin router, with group prefixes resolved. +// +// Adapted from molecule-controlplane/internal/cloudflareapi/cross_repo_drift_test.go +// which performs the inverse check. The extractor is regex-based and sufficient +// for molecule-core's structure where Groups don't nest and prefixes are static +// literals. +func extractGinRoutes(src string) []string { + groupRe := regexp.MustCompile(`(\w+)\s*:=\s*r\.Group\(\s*"([^"]*)"`) + groupMap := make(map[string]string) // varname → prefix + for _, m := range groupRe.FindAllStringSubmatch(src, -1) { + groupMap[m[1]] = m[2] + } + + methodRe := regexp.MustCompile( + `(\w+)\.(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|Handle)\(\s*` + + `(?:"[^"]*"\s*,\s*)?` + // Handle("METHOD", "/path") form + `"([^"]*)"`) + + seen := make(map[string]struct{}) + for _, m := range methodRe.FindAllStringSubmatch(src, -1) { + recv := m[1] + path := m[3] + + full := "" + switch { + case recv == "r": + full = path + default: + if prefix, ok := groupMap[recv]; ok { + full = prefix + path + } else { + continue + } + } + if full == "" { + continue + } + if i := strings.IndexAny(full, "?#"); i >= 0 { + full = full[:i] + } + seen[full] = struct{}{} + } + + out := make([]string, 0, len(seen)) + for p := range seen { + out = append(out, p) + } + sort.Strings(out) + return out +} + +func routeMatchesAny(route string, patterns []*regexp.Regexp) bool { + for _, re := range patterns { + if re.MatchString(route) { + return true + } + } + return false +} -- 2.52.0 From cc95f0f4089013c0075f940f472ba4e009f15b85 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Mon, 15 Jun 2026 09:28:20 +0000 Subject: [PATCH 2/2] fix(ci): only hard-fail platformPaths drift gate when sibling dir is explicitly broken (#820)\n\nCR2 RC on #2924: the previous t.Fatal on CI=true broke the standard\nCI / Platform (Go) job, which does not check out molecule-controlplane.\n\n- Skip when MOLECULE_CP_DIR is unset (standard Go job).\n- Fatal only when MOLECULE_CP_DIR is explicitly set but tunnel.go is\n unreadable, signalling a broken dedicated-drift checkout.\n- Update comment to document the contract.\n\nCo-Authored-By: Claude --- .../internal/router/router_cf_drift_test.go | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/workspace-server/internal/router/router_cf_drift_test.go b/workspace-server/internal/router/router_cf_drift_test.go index f30c6c756..4f43ae9fb 100644 --- a/workspace-server/internal/router/router_cf_drift_test.go +++ b/workspace-server/internal/router/router_cf_drift_test.go @@ -13,19 +13,21 @@ import ( // cross-repo drift gate for cf-tunnel platformPaths (molecule-controlplane#820). // It ensures that every top-level route registered in this router is matched by // at least one regex in the controlplane's platformPaths list. Drift is caught -// in core CI when a new tenant route is added, instead of shipping a silent -// tunnel-misroute to canvas:3000. +// in the dedicated workflow (which clones molecule-controlplane); in the +// standard CI Go job the sibling checkout is absent, so the test skips. // -// The test reads the controlplane tunnel.go source from a sibling checkout -// (env MOLECULE_CP_DIR) and parses the platformPaths slice. In CI the checkout -// is required; locally it skips if the file is absent. +// The hard-fail only fires when the operator explicitly configured +// MOLECULE_CP_DIR but the file is unreadable — that signals a broken checkout +// and must not pass silently. func TestCoreRoutesCoveredByPlatformPaths(t *testing.T) { cpTunnelPath := findControlPlaneTunnel(t) if cpTunnelPath == "" { - if os.Getenv("CI") != "" { - t.Fatal("molecule-controlplane tunnel.go not found; set MOLECULE_CP_DIR to enable the platformPaths drift gate in CI") + if os.Getenv("MOLECULE_CP_DIR") != "" { + t.Fatal("MOLECULE_CP_DIR is set but molecule-controlplane tunnel.go is not readable; " + + "the drift gate checkout is broken") } - t.Skip("molecule-controlplane tunnel.go not found; set MOLECULE_CP_DIR to enable cross-repo drift gate") + t.Skip("molecule-controlplane tunnel.go not found; " + + "set MOLECULE_CP_DIR to enable cross-repo drift gate") } t.Logf("loading controlplane platformPaths from %s", cpTunnelPath) -- 2.52.0