来源:round-64 lane D 只读探索(#2241),锚点在 42ba064c 逐行核实,主机侧已抽验复现(P1-1 实测 ClampPageSize 在 handler 包命中 = 0、手写 clamp 文件数 = 10、constants.go 两条 MUST 原文已核对)。
问题:同一个不变量/默认值有多份互相矛盾的定义,且专门的 helper 被整层绕过
切片 1:分页天花板三份定义打架
hub-server/internal/config/constants.go:20-25 MaxListPageSize = 200,注释自称「是 api/openapi.yaml 为共享 PageSize 参数声明的值,所以是那些端点必须实际执行的界」
hub-server/internal/config/constants.go:27-30 MaxPageLimit = 500,注释自称「所有 Hub handler 的 list 端点 MUST 夹到这个天花板」
api/openapi.yaml:7592-7599 共享 PageSize 参数 maximum: 200
hub-server/internal/config/paging.go:11-33 ClampPageSize 正是为消灭这类散落而写,paging_test.go:48 还把 MaxListPageSize == 200 钉成契约测试
实测:grep -rln "ClampPageSize(" hub-server/internal/handler | wc -l = 0(14 个调用点全在 repository/service);grep -rln "if pageSize > config.Max" hub-server/internal/handler | wc -l = 10(skill.go:98 / market.go:46 / session.go:343 / workspace.go:88 / mcp_server.go:104 / provider_binding.go:46 / execution_target.go:235 / agent_team_runs.go:125 / agent_profile.go:152 / message.go:412)。
后果:handler 层放行到 500、repository 层再夹到 200 ⇒ pageSize=300 拿回 200 行 + HTTP 200 + 无任何信号。这正是 paging.go:16-17 自己写下要消灭的行为("turns 'you asked for too many' into 'here is a quarter of a page', with HTTP 200 and no error")。OpenAPI 声明的 200 在「声称执行它的那一层」根本没执行。
修法:10 个 handler 的 6 行两分支 → pageSize = config.ClampPageSize(pageSize, config.MaxListPageSize, config.DefaultPaginationLimit)(message.go 用 MaxMessagePageLimit);删掉 MaxPageLimit 或改名/改注释为 document 家族专用,消掉两条 MUST 冲突;顺手把 8 处 DefaultQuery("pageSize", "50") 字面量统一为 strconv.Itoa(config.DefaultPaginationLimit)(已有 3 处这么写)。写集 ≈11 文件 / 60 行。
切片 2:*Error.HTTPStatus != 0 这一个不变量有 4 种处置
pkg/errcode/error.go:68-71 New(code, message, httpStatus) 不兜底,0 照收
hub-server/internal/handler/response.go:31-34 clamp → 500
hub-server/internal/middleware/response.go:15-18 clamp → 500
hub-server/internal/middleware/timeout.go:270-273 clamp → 504(与上面两处不一致)
pkg/errcode/error.go:101-117 WriteError 完全不 clamp
实测:8 个非测试 errcode.New( 调用点全部传显式非 0 状态,codes.go 里 17 个 &Error{} 字面量全部带 HTTPStatus;只有 2 个测试传 0(middleware/rate_limit_test.go:130、middleware/response_test.go:73)⇒ 3 处 clamp 目前只被测试保持存活,而走 WriteError 的路径拿到 0 状态就没人管。
修法:不变量在构造处成立一次——New 里 if httpStatus == 0 { httpStatus = http.StatusInternalServerError },删掉 3 处 clamp,2 个「传 0」测试改成断言新语义。写集 ≈5 文件 / 20 行。
注:round-62 记录的「9 处 500 clamp」在 42ba064c 上只穷举到 3 处(edge 侧 0 处);本 issue 按实测的 3 处写,不沿用旧数字。
验收
- 两个切片可各自独立成 PR,写集不重叠
pageSize=300 的行为必须有明确断言(夹到 200 且有信号,或按产品裁决改为 400)——不许继续静默
- 契约测试
paging_test.go 保持绿;go test ./internal/config/ ./internal/handler/ ./internal/middleware/;pkg/errcode 全绿
verify-openapi-contract.py 若 pageSize 语义变化需同步(当前基线 153/156/3/0)
负向约束
- 不许改既有测试的既有断言来迁就实现(round-64 lane A 就是因为这条才没照抄
diff_apply.go 的门形态)
- 不许顺手把 edge 也加分页 clamp(edge 无此概念,0 命中)
来源:round-64 lane D 只读探索(#2241),锚点在
42ba064c逐行核实,主机侧已抽验复现(P1-1 实测ClampPageSize在 handler 包命中 = 0、手写 clamp 文件数 = 10、constants.go 两条 MUST 原文已核对)。问题:同一个不变量/默认值有多份互相矛盾的定义,且专门的 helper 被整层绕过
切片 1:分页天花板三份定义打架
hub-server/internal/config/constants.go:20-25MaxListPageSize = 200,注释自称「是 api/openapi.yaml 为共享 PageSize 参数声明的值,所以是那些端点必须实际执行的界」hub-server/internal/config/constants.go:27-30MaxPageLimit = 500,注释自称「所有 Hub handler 的 list 端点 MUST 夹到这个天花板」api/openapi.yaml:7592-7599共享PageSize参数maximum: 200hub-server/internal/config/paging.go:11-33ClampPageSize正是为消灭这类散落而写,paging_test.go:48还把MaxListPageSize == 200钉成契约测试实测:
grep -rln "ClampPageSize(" hub-server/internal/handler | wc -l= 0(14 个调用点全在 repository/service);grep -rln "if pageSize > config.Max" hub-server/internal/handler | wc -l= 10(skill.go:98/market.go:46/session.go:343/workspace.go:88/mcp_server.go:104/provider_binding.go:46/execution_target.go:235/agent_team_runs.go:125/agent_profile.go:152/message.go:412)。后果:handler 层放行到 500、repository 层再夹到 200 ⇒
pageSize=300拿回 200 行 + HTTP 200 + 无任何信号。这正是paging.go:16-17自己写下要消灭的行为("turns 'you asked for too many' into 'here is a quarter of a page', with HTTP 200 and no error")。OpenAPI 声明的 200 在「声称执行它的那一层」根本没执行。修法:10 个 handler 的 6 行两分支 →
pageSize = config.ClampPageSize(pageSize, config.MaxListPageSize, config.DefaultPaginationLimit)(message.go用MaxMessagePageLimit);删掉MaxPageLimit或改名/改注释为 document 家族专用,消掉两条 MUST 冲突;顺手把 8 处DefaultQuery("pageSize", "50")字面量统一为strconv.Itoa(config.DefaultPaginationLimit)(已有 3 处这么写)。写集 ≈11 文件 / 60 行。切片 2:
*Error.HTTPStatus != 0这一个不变量有 4 种处置pkg/errcode/error.go:68-71New(code, message, httpStatus)不兜底,0 照收hub-server/internal/handler/response.go:31-34clamp → 500hub-server/internal/middleware/response.go:15-18clamp → 500hub-server/internal/middleware/timeout.go:270-273clamp → 504(与上面两处不一致)pkg/errcode/error.go:101-117WriteError完全不 clamp实测:8 个非测试
errcode.New(调用点全部传显式非 0 状态,codes.go里 17 个&Error{}字面量全部带HTTPStatus;只有 2 个测试传 0(middleware/rate_limit_test.go:130、middleware/response_test.go:73)⇒ 3 处 clamp 目前只被测试保持存活,而走WriteError的路径拿到 0 状态就没人管。修法:不变量在构造处成立一次——
New里if httpStatus == 0 { httpStatus = http.StatusInternalServerError },删掉 3 处 clamp,2 个「传 0」测试改成断言新语义。写集 ≈5 文件 / 20 行。验收
pageSize=300的行为必须有明确断言(夹到 200 且有信号,或按产品裁决改为 400)——不许继续静默paging_test.go保持绿;go test ./internal/config/ ./internal/handler/ ./internal/middleware/;pkg/errcode全绿verify-openapi-contract.py若 pageSize 语义变化需同步(当前基线 153/156/3/0)负向约束
diff_apply.go的门形态)