Files
pulse/internal/api/router_public_paths_inventory_test.go
T
rcourtman eeb2975d22 Stability sweep on the agent-substrate arc
Three things landed:

1. /api/agent/capabilities was missing from publicPathsAllowlist
   in router_public_paths_inventory_test.go. Slice 47 added the
   path to publicPaths in router.go and to publicRouteAllowlist
   in route_inventory_test.go but missed this second mirror,
   which scans publicPaths via go/ast. The test was failing on
   origin; this commit closes the gap.

2. The error-envelope paragraph in api-contracts.md now
   distinguishes capability-specific stable codes (the closed
   set declared per capability in the manifest) from
   cross-cutting codes the multi-tenant / auth middleware
   emits universally (invalid_org, org_suspended, access_denied).
   The previous wording implied all stable codes lived in
   per-capability errorCodes lists, which would have forced
   duplication on every capability or misled agents about which
   codes to expect.

3. New contract pin TestContract_AgentSurfaceErrorCodesMatch-
   ManifestDeclarations enforces the symmetry both directions:
   every code emitted by an agent-surface handler must be either
   declared in the matching capability or be one of the three
   cross-cutting codes; every manifest-declared code must have a
   matching emission. Drift either way is a contract regression.
   Pin verified clean against the current handler set.

Stale forward-reference fixed: the capabilities paragraph no
longer says "future MCP-server slices read the manifest" — slice
51 already shipped that adapter.

Sweep also surfaced two failures in internal/mock/ from
unrelated platform-support drift (unraid token set added in
ac82a2852 but the mock contract test wasn't updated). Those are
not part of the agent-substrate arc and not mine to fix; flagged
in the closing summary so they don't get lost.
2026-05-09 23:04:22 +01:00

131 lines
3.9 KiB
Go

package api
import (
"go/ast"
"go/parser"
"go/token"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
)
func TestRouterPublicPathsInventory(t *testing.T) {
literalPaths, dynamicPaths := parsePublicPaths(t)
expectedLiterals := sliceToSet(t, publicPathsAllowlist, "public paths allowlist")
expectedDynamics := sliceToSet(t, publicPathsDynamicAllowlist, "public dynamic allowlist")
expectedPublic := sliceToSet(t, publicRouteAllowlist, "public route allowlist")
actualLiterals := sliceToSet(t, literalPaths, "router public paths")
actualDynamics := sliceToSet(t, dynamicPaths, "router public dynamic paths")
if missing := setDifference(actualLiterals, expectedLiterals); len(missing) > 0 {
t.Fatalf("public paths missing from allowlist: %s", strings.Join(sortedKeys(missing), ", "))
}
if stale := setDifference(expectedLiterals, actualLiterals); len(stale) > 0 {
t.Fatalf("public allowlist contains paths not in router.go: %s", strings.Join(sortedKeys(stale), ", "))
}
if missing := setDifference(actualDynamics, expectedDynamics); len(missing) > 0 {
t.Fatalf("public dynamic paths missing from allowlist: %s", strings.Join(sortedKeys(missing), ", "))
}
if stale := setDifference(expectedDynamics, actualDynamics); len(stale) > 0 {
t.Fatalf("public dynamic allowlist contains paths not in router.go: %s", strings.Join(sortedKeys(stale), ", "))
}
if missing := setDifference(expectedLiterals, expectedPublic); len(missing) > 0 {
t.Fatalf("publicPaths entries missing from public route allowlist: %s", strings.Join(sortedKeys(missing), ", "))
}
supportedPublic := sliceToSet(t, append(publicPathsAllowlist, authBypassAllowlist...), "supported public routes")
if stale := setDifference(expectedPublic, supportedPublic); len(stale) > 0 {
t.Fatalf("public route allowlist contains paths not backed by bypass logic: %s", strings.Join(sortedKeys(stale), ", "))
}
}
func parsePublicPaths(t *testing.T) ([]string, []string) {
t.Helper()
_, file, _, ok := runtime.Caller(0)
if !ok {
t.Fatalf("failed to locate test file path")
}
routerPath := filepath.Join(filepath.Dir(file), "router.go")
fset := token.NewFileSet()
fileAST, err := parser.ParseFile(fset, routerPath, nil, 0)
if err != nil {
t.Fatalf("parse router.go: %v", err)
}
var literalPaths []string
var dynamicPaths []string
var found bool
ast.Inspect(fileAST, func(node ast.Node) bool {
assign, ok := node.(*ast.AssignStmt)
if !ok || len(assign.Lhs) != 1 || len(assign.Rhs) != 1 {
return true
}
ident, ok := assign.Lhs[0].(*ast.Ident)
if !ok || ident.Name != "publicPaths" {
return true
}
comp, ok := assign.Rhs[0].(*ast.CompositeLit)
if !ok {
return true
}
found = true
for _, elt := range comp.Elts {
switch v := elt.(type) {
case *ast.BasicLit:
if v.Kind != token.STRING {
continue
}
unquoted, err := strconv.Unquote(v.Value)
if err != nil {
unquoted = strings.Trim(v.Value, "`\"'")
}
literalPaths = append(literalPaths, unquoted)
case *ast.Ident:
dynamicPaths = append(dynamicPaths, v.Name)
case *ast.SelectorExpr:
dynamicPaths = append(dynamicPaths, selectorName(v))
}
}
return false
})
if !found {
t.Fatalf("publicPaths slice not found in router.go")
}
return literalPaths, dynamicPaths
}
var publicPathsDynamicAllowlist = []string{}
var publicPathsAllowlist = []string{
"/api/health",
"/api/security/status",
"/api/security/recovery",
"/api/security/validate-bootstrap-token",
"/api/security/quick-setup",
"/api/version",
"/api/login",
"/api/public/signup",
"/api/public/magic-link/request",
"/api/public/magic-link/verify",
"/api/cloud/handoff/exchange",
"/api/webhooks/stripe",
"/install.sh",
"/install.ps1",
"/download/pulse-agent",
"/api/agent/capabilities",
"/api/agent/version",
"/api/agent/ws",
"/api/server/info",
"/api/ai/oauth/callback",
"/auth/cloud-handoff",
licensePurchaseActivationPath,
}