ci: add shared<->Hub REST contract gate (A#3; completes P2 gate set) - #1467
Conversation
Hub-frontend hubClient call paths (app/shared/src/hubClient*.ts) must each resolve to a route registered in hub-server/internal/router/router.go. Catches client<->server REST contract drift. Empirical baseline (current master): 0 drift across 89 unique client paths - all resolve to a registered hub route (incl. /edge/* paths the hub serves). Negative test (injected /client/this-does-not-exist-on-hub) fails exit 1. Wired into checks.yml validate job and asserted in verify-ci-gates.ps1, following the #1463 gate convention (scripts/verify/ + validate step + policy assertion). Note: A#4 (verify-shared-barrel, Edge-export ban) and A#5 (verify-hub-layering, handler!=repository) were already shipped in #1463 (d4b6c13); this PR completes the audit's 5 gate candidates by adding the only remaining one, A#3.
📝 WalkthroughWalkthroughAdds a PowerShell verifier for Hub server/client REST route drift, wires it into the ChangesShared REST contract verification
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Validate as GitHub Actions validate job
participant Verifier as verify-shared-rest-contract.ps1
participant Router as hub-server router.go
participant Clients as Hub-frontend hubClient*.ts
Validate->>Verifier: run contract verification
Verifier->>Router: read declared routes
Verifier->>Clients: scan client paths
Verifier->>Verifier: normalize and compare paths
Verifier-->>Validate: return PASS or exit 1
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/verify/verify-ci-gates.ps1`:
- Around line 189-191: Update Assert-StepContinueOnError and its validation of
the hard-blocking “Verify Shared REST contract Hub-client to Hub-router” step so
any presence of continue-on-error causes failure, regardless of whether its
value is literal or expression-based. Preserve the existing checks for steps
that are explicitly allowed to continue on error.
In `@scripts/verify/verify-shared-rest-contract.ps1`:
- Around line 134-153: The drift counter in the route comparison loop only
tracks failures, making the allowlisted success message unreachable and
misreporting results. Update the loop around $KnownDefects to maintain a
separate count for allowlisted entries, increment it in the $allowed branch, and
use that count in the final success reporting while preserving $drift for actual
unmatched paths.
- Around line 59-85: Add Go-aware block-comment stripping to the router source
before processing it, removing /* ... */ content while preserving line
boundaries so both the iterative prefix resolution and route extraction loops
operate only on active code. Replace the current //-only filtering in the
$prefix and $hubRoutes parsing paths with the cleaned router lines, while
retaining support for line comments.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: cdff300e-5294-4354-89e8-8fedb6943f87
📒 Files selected for processing (3)
.github/workflows/checks.ymlscripts/verify/verify-ci-gates.ps1scripts/verify/verify-shared-rest-contract.ps1
| Assert-Contains $validate "Verify Shared REST contract Hub-client to Hub-router" "validate job must run the Shared REST contract verifier" | ||
| Assert-Contains $validate "scripts/verify/verify-shared-rest-contract\.ps1" "validate job must call scripts/verify/verify-shared-rest-contract.ps1" | ||
| Assert-StepContinueOnError $validate "Verify Shared REST contract Hub-client to Hub-router" $false |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== git status/diff =="
git status --short --branch || true
git diff --check || true
echo "== target file outline/sections =="
sed -n '1,240p' scripts/verify/verify-ci-gates.ps1 | nl -ba | sed -n '1,240p'
echo "== Assert-StepContinueOnError definitions/usages =="
rg -n "Assert-StepContinueOnError|continue-on-error" scripts/verify/verify-ci-gates.ps1 .github 2>/dev/null || trueRepository: TokenDanceLab/AgentHub
Length of output: 277
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== git status/diff =="
git status --short --branch || true
git diff --check || true
echo "== target file outline/sections =="
sed -n '1,240p' scripts/verify/verify-ci-gates.ps1 | nl -ba | sed -n '1,240p'
echo "== Assert-StepContinueOnError definitions/usages =="
rg -n "Assert-StepContinueOnError|continue-on-error" scripts/ verify scripts/verify/verify-ci-gates.ps1 .github 2>/dev/null || trueRepository: TokenDanceLab/AgentHub
Length of output: 277
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== status/diff =="
git status --short --branch || true
git diff --check || true
echo "== file sizes =="
wc -l scripts/verify/verify-ci-gates.ps1 2>/dev/null || true
echo "== target section =="
sed -n '1,240p' scripts/verify/verify-ci-gates.ps1
echo "== Assert-StepContinueOnError definitions/usages =="
rg -n "Assert-StepContinueOnError|continue-on-error" scripts/verify/verify-ci-gates.ps1 .github 2>/dev/null || true
echo "== behavioral probe: current pattern outcomes =="
python3 - <<'PY'
import re, pathlib
p = pathlib.Path('scripts/verify/verify-ci-gates.ps1')
s = p.read_text() if p.exists() else ''
print("script_exists:", p.exists())
print("contains_assert", "Assert-StepContinueOnError" in s)
print("contains_expected_lines:")
for pat in [
r'Assert-Contains \$validate "Verify Shared REST contract Hub-client to Hub-router".*validate job must run the Shared REST contract verifier',
r'Assert-Contains \$validate "scripts/verify/verify-shared-rest-contract\.ps1".*validate job must call scripts/verify/verify-shared-rest-contract.ps1',
r'Assert-StepContinueOnError \$validate "Verify Shared REST contract Hub-client to Hub-router" \$false'
]:
print(" pattern", pat)
print(" matches", bool(re.search(pat, s, re.S)))
for step in ['literal false', 'github expression true', 'dynamic property expression true', 'literal true']:
step_header = 'Some REST contract check'
line = {
'literal false': 'continue-on-error: false',
'github expression true': 'continue-on-error: ${{ true }}',
'dynamic property expression true': 'continue-on-error: ${{ true }}',
'literal true': 'continue-on-error: true',
}[step]
yaml = f' - name: {step_header}\n uses: some/action\n continue-on-error: {line}'
# Mimic current pattern-ish check: it should not match if contains literal true
matches = re.search(r'current step should not have continue-on-error: true', yaml) is not None
print(step, "-> current check would pass:", not matches, "value:", line)
PYRepository: TokenDanceLab/AgentHub
Length of output: 21967
Reject any continue-on-error value for hard-blocking steps.
Assert-StepContinueOnError(..., $false) only checks continue-on-error: true, so an expression like continue-on-error: ${{ true }} can still pass while disabling the gate. For expected hard-blocking steps, fail whenever continue-on-error is present instead of only rejecting the literal true.
🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)
[warning] Missing BOM encoding for non-ASCII encoded file 'verify-ci-gates.ps1'
(PSUseBOMForUnicodeEncodedFile)
[info] 189-189: Cmdlet 'Assert-Contains' has positional parameter. Please use named parameters instead of positional parameters when calling a command.
(PSAvoidUsingPositionalParameters)
[info] 190-190: Cmdlet 'Assert-Contains' has positional parameter. Please use named parameters instead of positional parameters when calling a command.
(PSAvoidUsingPositionalParameters)
[info] 191-191: Cmdlet 'Assert-StepContinueOnError' has positional parameter. Please use named parameters instead of positional parameters when calling a command.
(PSAvoidUsingPositionalParameters)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/verify/verify-ci-gates.ps1` around lines 189 - 191, Update
Assert-StepContinueOnError and its validation of the hard-blocking “Verify
Shared REST contract Hub-client to Hub-router” step so any presence of
continue-on-error causes failure, regardless of whether its value is literal or
expression-based. Preserve the existing checks for steps that are explicitly
allowed to continue on error.
| $routerSrc = Get-Content -LiteralPath $routerPath -Raw | ||
| $routerLines = $routerSrc -split "`r?`n" | ||
|
|
||
| # Resolve group prefixes iteratively (public := r.Group(...), child := parent.Group(...)) | ||
| $prefix = @{} | ||
| $changed = $true | ||
| while ($changed) { | ||
| $changed = $false | ||
| foreach ($ln in $routerLines) { | ||
| if ($ln -match '^\s*//') { continue } | ||
| if ($ln -match '(\w+)\s*:=\s*(\w+)\.Group\("([^"]*)"') { | ||
| $var = $Matches[1]; $parent = $Matches[2]; $seg = $Matches[3] | ||
| if ($prefix.ContainsKey($var)) { continue } | ||
| if ($parent -eq 'r') { $prefix[$var] = $seg; $changed = $true } | ||
| elseif ($prefix.ContainsKey($parent)) { $prefix[$var] = $prefix[$parent] + $seg; $changed = $true } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| $hubRoutes = @{} | ||
| foreach ($ln in $routerLines) { | ||
| if ($ln -match '^\s*//') { continue } | ||
| foreach ($m in [regex]::Matches($ln, '(\w+)\.(GET|POST|PUT|DELETE|PATCH)\("([^"]*)"')) { | ||
| $var = $m.Groups[1].Value; $path = $m.Groups[3].Value | ||
| $base = if ($var -eq 'r') { '' } elseif ($prefix.ContainsKey($var)) { $prefix[$var] } else { continue } | ||
| $full = ($base + $path) -replace ':(\w+)', '{param}' | ||
| $hubRoutes[$full] = $true |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Git status/check:\n'
git status --short --branch || true
git diff --check || true
printf '\nTarget file outline/size:\n'
if [ -f scripts/verify/verify-shared-rest-contract.ps1 ]; then
wc -l scripts/verify/verify-shared-rest-contract.ps1
sed -n '1,140p' scripts/verify/verify-shared-rest-contract.ps1
else
fd -i 'verify-shared-rest-contract' .
fi
printf '\nSearch for comment stripping or regex related to group/route extraction:\n'
rg -n "Get-Content|Comment|strip|///|(/\*|\\\*\/)|group\.GET|\.Group\(|(\w+)\.GET|(\w+)\.POST|(\w+)\.PUT|(\w+)\.DELETE|(\w+)\.PATCH" scripts/verify/verify-shared-rest-contract.ps1 || trueRepository: TokenDanceLab/AgentHub
Length of output: 8010
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Router file relevant sections:\n'
if [ -f hub-server/internal/router/router.go ]; then
wc -l hub-server/internal/router/router.go
rg -n 'Group\(|GET|POST|PUT|DELETE|PATCH|//|/\*' hub-server/internal/router/router.go || true
else
fd -i '^router\.go$' hub-server /
fi
printf '\nPowerShell regex behavior probe for line-skipping/regex matching on Go comment forms:\n'
powershell -NoProfile -NonInteractive -Command '
$routerSrc = @'
// line-commented group := r.Group("admin")
/* block-commented group.GET("/admin-users") */
commented line with group.GET("/admin-users") in it
group := r.Group("admin")
group.GET("/users")
'@
$routerLines = $routerSrc -split "`r?`n"
$prefix = @{}
$changed = $true
while ($changed) {
$changed = $false
foreach ($ln in $routerLines) {
if ($ln -match '^\s*//') { continue }
if ($ln -match '(\w+)\s*:=\s*(\w+)\.Group\("([^"]*)")') {
$var = $Matches[1]; $parent = $Matches[2]; $seg = $Matches[3]
if ($prefix.ContainsKey($var)) { continue }
if ($parent -eq "r") { $prefix[$var] = $seg; $changed = $true }
elseif ($prefix.ContainsKey($parent)) { $prefix[$var] = $prefix[$parent] + $seg; $changed = $true }
}
}
}
Print = out StringJoin New-Object system.StringJoin `n
$prefix.GetEnumerator() | Sort-Object Key | ForEach-Object { "Prefix $($_.Key) -> $($_.Value)" } | Join-Pipeline -Separator "`n"
$hubRoutes = @{}
foreach ($ln in $routerLines) {
if ($ln -match '^\s*//') { continue }
foreach ($m in [regex]::Matches($ln, "(\w+)\.(GET|POST|PUT|DELETE|PATCH)\(\"([^\"]*)\"")) {
$var = $m.Groups[1].Value; $path = $m.Groups[3].Value
$base = if ($var -eq "r") { "" } elseif ($prefix.ContainsKey($var)) { $prefix[$var] } else { continue }
$full = ($base + $path) -replace ":(\w+)", "{param}"
$hubRoutes[$full] = $true
}
}
"Routes:"
$hubRoutes.Keys | Sort-Object | ForEach-Object { "$_" } | Join-Pipeline -Separator "`n"
'
' || true
printf '\nPOSIX-aware Go comment extraction probe:\n'
python3 - <<'PY'
import re, pathlib
src = (pathlib.Path('hub-server/internal/router/router.go').read_text() if pathlib.Path('hub-server/internal/router/router.go').exists() else '')
without_comments = re.sub(r'/\*.*?\*/', '', src, flags=re.S)
without_comments = re.sub(r'//.*$', '', without_comments, flags=re.M)
line_skipped = '\n'.join(line for line in src.splitlines() if not line.lstrip().startswith('//'))
print('contains_block_group_GET=', bool(re.search(r'/\*[\s\S]*?group\.GET', src)))
print('contains_block_assignment=', bool(re.search(r'/\*[\s\S]*?\w+\s*:=\s*\w+\.Group', src)))
print('no_block_group_GET_after_strip=', not bool(re.search(r'group\.GET', without_comments)))
print('line_skipped_group_GET=', bool(re.search(r'group\.GET', line_skipped)))
PYRepository: TokenDanceLab/AgentHub
Length of output: 13425
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Router.go exact comment lines surrounding group declarations (if any):\n'
python3 - <<'PY'
import re
from pathlib import Path
p=Path('hub-server/internal/router/router.go')
if p.exists():
lines=p.read_text().splitlines()
for i,l in enumerate(lines,1):
if re.search(r'^\s*(public|client|auth|comments|sessions|edge|cloud|web)\s*:=', l):
print(f'{i}: {l!r}')
for i,l in enumerate(lines,1):
if 'r.Group' in l or '.Group(' in l:
print(f'{i}: {l!r}')
PY
printf '\nPowerShell regex behavior probe from stdin instead:\n'
cat > /tmp/parser_probe.ps1 <<'PS'
$tests = @"
// line-commented group := r.Group("/admin")
/* block-commented group.GET("/admin-users") */
commented line with group.GET("/admin-users") in it
group := r.Group("/admin")
group.GET("/users")
"@
$lines = $tests -split "`r?`n"
$prefix = @{}
$changed = $true
while ($changed) {
$changed = $false
foreach ($ln in $lines) {
if ($ln -match '^\s*//') { continue }
if ($ln -match '(\w+)\s*:=\s*(\w+)\.Group\("([^"]*)') {
$var = $Matches[1]; $parent = $Matches[2]; $seg = $Matches[3]
if ($prefix.ContainsKey($var)) { continue }
if ($parent -eq 'r') { $prefix[$var] = $seg; $changed = $true }
elseif ($prefix.ContainsKey($parent)) { $prefix[$var] = $prefix[$parent] + $seg; $changed = $true }
}
}
}
"Parsed prefixes:"
$prefix.GetEnumerator() | Sort-Object Key | ForEach-Object { " $($_.Key) -> $($_.Value)" } | Join-Pipeline -Separator "`n"
$routes = @{}
foreach ($ln in $lines) {
if ($ln -match '^\s*//') { continue }
foreach ($m in [regex]::Matches($ln, '(\w+)\.(GET|POST|PUT|DELETE|PATCH)\("([^"]*)"')) {
$var = $m.Groups[1].Value; $path = $m.Groups[3].Value
$base = if ($var -eq 'r') { '' } elseif ($prefix.ContainsKey($var)) { $prefix[$var] } else { continue }
$full = ($base + $path) -replace ':(\w+)', '{param}'
$routes[$full] = $true
}
}
"Parsed routes:"
$routes.Keys | Sort-Object | ForEach-Object { " $_" } | Join-Pipeline -Separator "`n"
PS
pwsh -NoProfile -NonInteractive /tmp/parser_probe.ps1Repository: TokenDanceLab/AgentHub
Length of output: 2063
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Router current comment forms around active group/route declarations (lines 50-100):\n'
sed -n '50,100p' hub-server/internal/router/router.go | cat -n
printf '\nPowerShell regex behavior probe for line-skipping/regex matching on Go comment forms:\n'
pwsh -NoProfile -NonInteractive -Command '
$routerSrc = @"
// line-commented group := r.Group("/admin")
/* block-commented group.GET("/admin-users") */
commented line with group.GET("/admin-users") in it
group := r.Group("/admin")
group.GET("/users")
"@
$routerLines = $routerSrc -split "`r?`n"
$prefix = @{}
$changed = $true
while ($changed) {
$changed = $false
foreach ($ln in $routerLines) {
if ($ln -match "^\s*//") { continue }
if ($ln -match "(\w+)\s*:=\s*(\w+)\.Group\(\"([^\"]*\")") {
$var = $Matches[1]; $parent = $Matches[2]; $seg = $Matches[3]
if ($prefix.ContainsKey($var)) { continue }
if ($parent -eq "r") { $prefix[$var] = $seg; $changed = $true }
elseif ($prefix.ContainsKey($parent)) { $prefix[$var] = $prefix[$parent] + $seg; $changed = $true }
}
}
}
Write-Host "Parsed prefixes:"
$prefix.GetEnumerator() | Sort-Object Key | ForEach-Object { Write-Host " $($_.Name) -> $($_.Value)" }
$hubRoutes = @{}
foreach ($ln in $routerLines) {
if ($ln -match "^\s*//") { continue }
foreach ($m in [regex]::Matches($ln, "(\w+)\.(GET|POST|PUT|DELETE|PATCH)\(\"([^\"]*\")")) {
$var = $m.Groups[1].Value; $path = $m.Groups[3].Value
$base = if ($var -eq "r") { "" } elseif ($prefix.ContainsKey($var)) { $prefix[$var] } elseif ($path -like "*admin-users*") { $true }
$full = ($base + $path) -replace ":(\w+)", "{param}"
$hubRoutes[$full] = $true
}
}
Write-Host "Parsed routes:"
$hubRoutes.Keys | Sort-Object | ForEach-Object { Write-Host " $_" }
'Repository: TokenDanceLab/AgentHub
Length of output: 2982
Use Go-aware comment stripping before route extraction.
The parser only skips //-only lines, so group.GET("/admin-users") inside a /* ... */ block is still added to the declared route set. Strip /* ... */ comments for both group prefix resolution and route extraction.
🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)
[warning] Missing BOM encoding for non-ASCII encoded file 'verify-shared-rest-contract.ps1'
(PSUseBOMForUnicodeEncodedFile)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/verify/verify-shared-rest-contract.ps1` around lines 59 - 85, Add
Go-aware block-comment stripping to the router source before processing it,
removing /* ... */ content while preserving line boundaries so both the
iterative prefix resolution and route extraction loops operate only on active
code. Replace the current //-only filtering in the $prefix and $hubRoutes
parsing paths with the cleaned router lines, while retaining support for line
comments.
| $drift = 0 | ||
| foreach ($k in ($normPaths.Keys | Sort-Object)) { | ||
| if ($hubRoutes.ContainsKey($k)) { continue } | ||
| $allowed = $false | ||
| foreach ($d in $KnownDefects) { if ($d -eq $k) { $allowed = $true; break } } | ||
| if ($allowed) { | ||
| Write-Host " KNOWN DEFECT client path has no hub route: $k (allowlisted)" -ForegroundColor Yellow | ||
| } else { | ||
| $sample = ($rawPaths | Where-Object { (Normalize-ClientPath $_) -eq $k } | Select-Object -First 1) -join "" | ||
| Fail "client path has no matching hub route: $k (from '$sample')" | ||
| $drift++ | ||
| } | ||
| } | ||
|
|
||
| if ($Failed -eq 0) { | ||
| if ($drift -eq 0) { | ||
| Pass "all Hub-client call paths resolve to a registered hub route ($($normPaths.Count) unique path(s))" | ||
| } else { | ||
| Pass "no new client<->hub contract drift ($drift known-defect note(s) in allowlist)" | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix allowlisted-drift reporting.
$drift increments only in the branch that also calls Fail, so the $drift -ne 0 success branch is unreachable. Once $KnownDefects is populated, the script incorrectly reports that every path resolves. Track allowlisted entries separately and use that count in the success message.
🧰 Tools
🪛 PSScriptAnalyzer (1.25.0)
[warning] Missing BOM encoding for non-ASCII encoded file 'verify-shared-rest-contract.ps1'
(PSUseBOMForUnicodeEncodedFile)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/verify/verify-shared-rest-contract.ps1` around lines 134 - 153, The
drift counter in the route comparison loop only tracks failures, making the
allowlisted success message unreachable and misreporting results. Update the
loop around $KnownDefects to maintain a separate count for allowlisted entries,
increment it in the $allowed branch, and use that count in the final success
reporting while preserving $drift for actual unmatched paths.
* docs: record P2/P3 audit closure in MASTER.md (#1467, #1469-#1473) Add A#3 Hub<->router REST contract gate row (#1467) and a 2026-07-29 session-log entry covering P2 closure + P3 adjudication (D-V1 DEFER via #867; 5 PLAN issues #1469-#1473; A-V2 covered by #1463; A-V5 adopted). * docs(progress): A-V2 澄清 + #1468 门禁入 14 层索引 修正 P3 裁决记录:A-V2 非「由 #1463 覆盖」。p2 关门前补 A-V2 专属门禁 PR #1468 (shared-UI 禁运行时 import hubClient,类型导入放行),与 #1463「平台↛共享Edge」 是依赖图相反两侧的互补面,master 上 0 违规。#1468 rebase 至最新 master 解 checks.yml/verify-ci-gates.ps1 加法冲突,anti-cheat 通过,MERGEABLE 待 CI。 14 层门禁索引补 shared-rest-contract · shared-ui-hubclient。 * docs(progress): 同步 MASTER tip 至 #1468 (14 层门禁全合入) #1468 A-V2 shared-UI hubClient 门禁已合入 master 54c2186。 - live tip 更新至 54c2186 - 14 层 validate 门禁全部合入(shared-rest-contract + shared-ui-hubclient) - A7 候选余量「shared UI hubClient」标为 A-V2 已闭环 - 综合审计 top10 落地 6/10
Summary
Adds the only remaining architecture gate from the audit's 5 gate-ifiable
candidates (audit-A "门禁化候选" list): A#3 — shared<->Hub REST contract
validator.
A#3 — scripts/verify/verify-shared-rest-contract.ps1
Hub-frontend
hubClientcall paths (app/shared/src/hubClient*.ts,notably hubClientPayloadPaths.ts) must each resolve to a route registered in
hub-server/internal/router/router.go. Catches client<->server REST contract
drift (client calling an endpoint the Hub does not serve).
Normalization (compared on route shape):
(the Hub registers the slash form; client uses requestWithFallback)
Empirical baseline (current master)
route (incl. the /edge/* paths the hub itself serves).
makes the gate fail (exit 1) as expected. Gate logic verified.
Wiring (follows #1463 convention)
validatejob step in .github/workflows/checks.ymlTest plan
Follow-ups (recommendations, not blocking)
repository directly (whitelisted in ci: 门禁扩展 — shared 边界/barrel/handler 分层/conventions #1463's gate). Suggest routing the
migration-status health probe through a HealthService to clear the
known-defect note.
surface is a net/http mux with wildcard sub-paths + gateway rewriting, so a
strict check is higher-effort; recommend a dedicated follow-up gate.
Summary by CodeRabbit
Bug Fixes
Quality Improvements