feat(server): public /maintenance/site-status route + tests; P9 evidence
- isPublicRoute exempts site-status (anonymous banner polling) - httptest coverage: anonymous 200 + site_notice payload; route table guards - test report + playwright evidence (docker mcr playwright v1.63 noble vs real dev stack, 1 passed)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# P9 web-sysadmin 残差收口 测试报告
|
||||
|
||||
## 一、 测试背景 (Background)
|
||||
|
||||
MATRIX `web-sysadmin` 行尾注三项 deferred:Playwright 实跑、KB 运维动作、白名单项接消费方。总控 P9 批次授权收口。
|
||||
|
||||
## 二、 测试目标 (Test Objectives)
|
||||
|
||||
1. `ui.maintenance_notice` / `ui.read_only_mode` 白名单设置接运行时消费方(公开状态端点 + 前端横幅)。
|
||||
2. admin-console Playwright spec 对真实 dev 栈实跑取证。
|
||||
3. KB 运维动作给出做/不做结论与依据。
|
||||
|
||||
## 三、 测试方案与环境 (Test Plan & Environment)
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| OS | Linux 7.0.0-31-generic |
|
||||
| worktree | `.worktrees/websysadmin-residual-2026-09-25`(base dev `c5027788`) |
|
||||
| dev 栈 | 本机 `bin/agent-server`(:8080,PG `agent_system_dev@5433` schema 72) |
|
||||
| Playwright 通道 | docker `mcr.microsoft.com/playwright:v1.63.0-noble`(宿主缺 libatk 且无 sudo,改镜像跑;`--network host` 对真实 dev 栈) |
|
||||
| 证据目录 | `assets/2026-09-25-websysadmin-residual/` |
|
||||
|
||||
## 四、 预期结果 (Expected Results)
|
||||
|
||||
- 白名单消费方:PUT `ui.maintenance_notice` → 公开 status 返回 notice(本批代码合入后生效;运行中旧版本服务不体现新字段属预期)。
|
||||
- Playwright:`admin console smoke` 登录→管理台→8 分组渲染→返回工作台 PASS。
|
||||
- 公开路由:`/api/v1/maintenance/site-status` 匿名 200。
|
||||
|
||||
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
|
||||
|
||||
### 5.1 白名单消费方(Go 层)
|
||||
|
||||
- **Input**:`maintenance.NewService(fakeStore{ErrNotFound})` + `SetSiteSettings`(返回 notice/readOnly)
|
||||
- **Output**:`TestCurrentStatusCarriesSiteSettings`、`TestCurrentSiteStatus`、`TestCurrentStatusWindowPlusSite` 全 PASS(notice/readOnly 贯穿无窗口/有窗口路径)
|
||||
- **判定**:PASS
|
||||
- 耗时:<1s
|
||||
|
||||
### 5.2 公开端点
|
||||
|
||||
- **Input**:`TestMaintenanceSiteStatusPublic` 匿名 GET + `TestIsPublicRouteMaintenanceSiteStatus` 路由豁免表
|
||||
- **Output**:均 PASS(200 + `site_notice` 状态 + notice/readOnly 字段)
|
||||
- **判定**:PASS
|
||||
|
||||
### 5.3 Playwright 实跑(真实 dev 栈)
|
||||
|
||||
- **Input**:docker 镜像内 `node node_modules/playwright/cli.js test e2e/admin-console.spec.ts --project=chromium`,`RUN_UI_ACCEPTANCE=1`,`BASE_URL=http://127.0.0.1:8080`,admin 真实凭据
|
||||
- **Output**:
|
||||
```
|
||||
Running 1 test using 1 worker
|
||||
✓ 1 [chromium] › e2e/admin-console.spec.ts:22:1 › admin console smoke: enter, nav renders, exit back to workbench (1.2s)
|
||||
1 passed (1.8s)
|
||||
```
|
||||
- **判定**:PASS(登录→管理台→8 分组→返回工作台全链路真实栈)
|
||||
- **性能与耗时**:单用例 1.2s
|
||||
- **证据**:`assets/2026-09-25-websysadmin-residual/playwright-report-main/`、`playwright-output-main/`
|
||||
|
||||
### 5.4 运行中 dev 栈写读回环(旧二进制)
|
||||
|
||||
- **Input**:admin PUT `ui.maintenance_notice` = "P9 验收:系统今晚 23:30 例行维护演练"
|
||||
- **Output**:200 + 白名单返回更新值(写入生效);旧二进制 `/maintenance/status` 无 notice 字段(新端点未部署,属预期)
|
||||
- **判定**:PASS(写入路径与既有审计链路正常;新消费端点待 dev 重建后生效)
|
||||
|
||||
### 5.5 KB 运维动作
|
||||
|
||||
- **结论**:不做(超边界)。`internal/kb` 为纯只读检索客户端,无 rebuild/index 写路径;重建/索引属共享基础设施容器 `shuimu-kb-shared` 侧能力(见 `scripts/migrate_shared_kb.sh` 口径)。管理台 KB 分组维持只读连接拓扑展示。
|
||||
|
||||
## 六、 自动化测试套件汇总 (Test Suite Execution)
|
||||
|
||||
| 套件 | 结果 |
|
||||
|---|---|
|
||||
| `go test -short ./internal/server/ ./internal/maintenance/ ./internal/sysadmin/` | PASS(3 包,含 5 个新用例) |
|
||||
| `cd web && npm test` | PASS 34/34 |
|
||||
| `cd web && npm run build` | PASS |
|
||||
| docker Playwright admin-console | PASS 1/1 |
|
||||
| `go vet`(maintenance/sysadmin/server) | PASS |
|
||||
|
||||
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
|
||||
|
||||
- P9 三项残差收口完成:白名单消费方已接线(代码层全绿)、Playwright 实跑取证 PASS、KB 运维登记不做(边界依据明确)。
|
||||
- 后续:dev 栈重建后 `site-status` 端点生效,可补一条真实 HTTP notice 展示证据;其余白名单键(`chat.default_session_role` 等)消费方属各自业务模块后续任务。
|
||||
@@ -21,7 +21,8 @@ func isPublicRoute(method, path string) bool {
|
||||
if path == "/" || path == "/health" || path == "/api/v1/health" ||
|
||||
path == "/ready" || path == "/api/v1/ready" ||
|
||||
path == "/api/v1/vault/status" ||
|
||||
path == "/api/v1/maintenance/status" {
|
||||
path == "/api/v1/maintenance/status" ||
|
||||
path == "/api/v1/maintenance/site-status" {
|
||||
return true
|
||||
}
|
||||
if method == http.MethodPost && path == "/api/v1/auth/login" {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"chat-agent-system/internal/maintenance"
|
||||
)
|
||||
|
||||
// P9:site-status 为公开端点(登录页横幅轮询),匿名可达且不落鉴权错误。
|
||||
func TestMaintenanceSiteStatusPublic(t *testing.T) {
|
||||
svc := maintenance.NewService(&fakeSiteStore{})
|
||||
svc.SetSiteSettings(func(ctx context.Context) (string, bool) {
|
||||
return "公告内容", true
|
||||
})
|
||||
s := &Server{}
|
||||
s.setOverrideMaintenance(svc)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /api/v1/maintenance/site-status", s.handleMaintenanceSiteStatus)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
resp, err := http.Get(ts.URL + "/api/v1/maintenance/site-status")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("status = %d", resp.StatusCode)
|
||||
}
|
||||
var view maintenance.StatusView
|
||||
if err := json.NewDecoder(resp.Body).Decode(&view); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if view.Notice != "公告内容" || !view.ReadOnly || view.Status != "site_notice" {
|
||||
t.Fatalf("site view = %+v", view)
|
||||
}
|
||||
}
|
||||
|
||||
// isPublicRoute 必须豁免 site-status(匿名横幅)。
|
||||
func TestIsPublicRouteMaintenanceSiteStatus(t *testing.T) {
|
||||
if !isPublicRoute(http.MethodGet, "/api/v1/maintenance/site-status") {
|
||||
t.Fatal("site-status must be public")
|
||||
}
|
||||
if !isPublicRoute(http.MethodGet, "/api/v1/maintenance/status") {
|
||||
t.Fatal("status must stay public")
|
||||
}
|
||||
if isPublicRoute(http.MethodGet, "/api/v1/admin/maintenance") {
|
||||
t.Fatal("admin maintenance must not be public")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeSiteStore struct{}
|
||||
|
||||
func (f *fakeSiteStore) GetOpen(ctx context.Context) (*maintenance.Window, error) {
|
||||
return nil, maintenance.ErrNotFound
|
||||
}
|
||||
func (f *fakeSiteStore) GetByID(ctx context.Context, id int64) (*maintenance.Window, error) {
|
||||
return nil, maintenance.ErrNotFound
|
||||
}
|
||||
func (f *fakeSiteStore) Insert(ctx context.Context, w maintenance.Window) (maintenance.Window, error) {
|
||||
return w, nil
|
||||
}
|
||||
func (f *fakeSiteStore) UpdateStatus(ctx context.Context, id int64, from, to string) (maintenance.Window, error) {
|
||||
return maintenance.Window{}, nil
|
||||
}
|
||||
func (f *fakeSiteStore) ListRecent(ctx context.Context, limit int) ([]maintenance.Window, error) {
|
||||
return nil, nil
|
||||
}
|
||||
Reference in New Issue
Block a user