179 lines
10 KiB
Go
179 lines
10 KiB
Go
package report
|
||
|
||
import "strings"
|
||
|
||
func passSub(id, summary, evidence string) SubItem {
|
||
return SubItem{ID: id, Summary: summary, Status: StatusPass, Evidence: evidence}
|
||
}
|
||
|
||
func untestedSub(id, summary, note string) SubItem {
|
||
return SubItem{ID: id, Summary: summary, Status: StatusUntested, Note: note}
|
||
}
|
||
|
||
var catalogSubs = map[string][]SubItem{
|
||
"F01": {
|
||
passSub("F01a", "开通一端并可 MQTT 登录", "test/accept/accept_test.go runEndpointCreate"),
|
||
passSub("F01b", "批量开通整批校验", "internal/admin/endpoints_test.go TestEndpointImportDuplicateRejectsAll、TestEndpointImportBOMAndCreate"),
|
||
passSub("F01c", "停用", "internal/app/identity/lifecycle_test.go TestF01DisableVoidsScheduledAndRejectsNew;internal/admin/endpoints_test.go TestEndpointDisableKickAndUnlock"),
|
||
passSub("F01d", "删除群主转让", "internal/app/identity/lifecycle_test.go TestF01DeleteOwnerTransfersEarliest"),
|
||
passSub("F01e", "删除后同编号重开不串数据", "internal/app/identity/lifecycle_test.go TestF01DeleteReopenNoOldReceipts"),
|
||
},
|
||
"F02": {
|
||
passSub("F02a", "密码登录后 hello / 会话令牌", "test/accept/accept_test.go;internal/broker/f02_test.go TestF02PasswordLoginReturnsTokenAndHandshake"),
|
||
passSub("F02b", "新设备密码登录后旧设备退出", "internal/broker/f02_test.go TestF02TakenOverByPasswordLogin、TestF02OldTokenRejectedAfterPasswordLogin"),
|
||
passSub("F02c", "换 IP 用令牌重连", "internal/broker/f02_test.go TestF02TokenReconnectDifferentIPKeepsToken"),
|
||
passSub("F02d", "两种密码锁定", "internal/broker/f02_test.go TestF02IPLockDoesNotAffectOtherIP、TestF02EndpointLockAllowsTokenReconnect"),
|
||
passSub("F02e", "重置密码后被踢", "internal/broker/f02_test.go TestF02AdminResetPasswordFatal;cmd/nixmsg/fatal_revoked_integration_test.go TestUplinkResetPasswordFatal"),
|
||
passSub("F02f", "服务器故障不误报密码错误", "internal/broker/f02_test.go TestF02DBErrorClosesWithout086;internal/broker/broker_test.go TestInternalAuthErrorDoesNotReturnBadPassword"),
|
||
},
|
||
"F03": {
|
||
passSub("F03a", "断开后状态及时变离线", "test/accept/rest_accept_test.go;internal/app/presence/presence_test.go TestF03PresenceAndDirectory"),
|
||
passSub("F03b", "目录可列出", "test/accept/rest_accept_test.go;internal/app/presence/presence_test.go TestF03PresenceAndDirectory"),
|
||
untestedSub("F03c", "1000 端全表 1s 内返回", "环境/时长限制,本波不跑"),
|
||
untestedSub("F03d", "真拔网线后心跳超时离线", "环境限制;验收用关连接模拟断线"),
|
||
},
|
||
"F04": {
|
||
passSub("F04a", "只通知订阅了的端", "test/accept/rest_accept_test.go;internal/app/presence/presence_test.go TestF04PresenceWatch"),
|
||
},
|
||
"F05": {
|
||
passSub("F05a", "双端在线单聊送达与确认", "test/accept/accept_test.go runMessagingAccept"),
|
||
passSub("F05b", "崩溃不丢已提交消息", "test/chaos/q3_crash_test.go TestQ3CrashSubmitThenRestart"),
|
||
passSub("F05c", "消息号冲突", "internal/app/message/submit_test.go TestSubmitTable/conflict"),
|
||
passSub("F05d", "密码门生效", "internal/app/message/submit_test.go TestSubmitTable 对话密码用例"),
|
||
passSub("F05e", "配额生效", "internal/app/message/submit_test.go TestSubmitTable/quota_exceeded"),
|
||
},
|
||
"F06": {
|
||
passSub("F06a", "群成员收到同一份、发送者不收到自己的", "test/accept/accept_test.go;internal/app/group/group_test.go TestF06GroupSendMembership"),
|
||
passSub("F06b", "入群前不补", "internal/app/group/group_test.go TestF06GroupSendMembership(发送后入群无旧投递)"),
|
||
},
|
||
"F07": {
|
||
passSub("F07a", "256 KiB 通过", "test/accept/rest_accept_test.go"),
|
||
passSub("F07b", "超出拒绝", "test/accept/rest_accept_test.go body_too_large"),
|
||
passSub("F07c", "接收上限生效", "test/accept/rest_accept_test.go max_receive_bytes=1024"),
|
||
},
|
||
"F08": {
|
||
passSub("F08a", "重启后续传", "test/accept/accept_test.go;test/chaos/q3_crash_test.go"),
|
||
passSub("F08b", "应用层不重复(同号同指纹)", "internal/app/message/submit_test.go TestSubmitTable/idempotent_hit"),
|
||
passSub("F08c", "toxiproxy 弱网保留送达", "test/chaos/q3_weak_test.go TestQ3ToxiproxyOfflineKeepDelivered"),
|
||
untestedSub("F08d", "Linux netem 20% 丢包", "本机 Windows;test/chaos/q3_weak_test.go TestQ3NetemUntested"),
|
||
},
|
||
"F09": {
|
||
passSub("F09a", "离线保留期内上线送达", "test/accept/accept_test.go"),
|
||
passSub("F09b", "超时过期", "internal/app/message/delivery_test.go TestDeliveryStateMachine/F09_keep_ttl_expire_via_cleanup"),
|
||
},
|
||
"F10": {
|
||
passSub("F10a", "短断线送到", "test/accept/rest_accept_test.go"),
|
||
passSub("F10b", "长断线丢弃", "test/accept/rest_accept_test.go"),
|
||
passSub("F10c", "服务器重启后宽限内重连送到", "test/accept/rest_accept_test.go"),
|
||
},
|
||
"F11": {
|
||
passSub("F11a", "发送方离线后到点仍发送", "test/accept/rest_accept_test.go"),
|
||
},
|
||
"F12": {
|
||
passSub("F12a", "延迟窗口内撤回对方收不到", "test/accept/accept_test.go"),
|
||
},
|
||
"F13": {
|
||
passSub("F13a", "未推送必撤成功", "test/accept/accept_test.go"),
|
||
passSub("F13b", "群部分确认得到部分撤回", "internal/app/message/delivery_test.go TestDeliveryStateMachine/F13_recall_race_partial"),
|
||
},
|
||
"F14": {
|
||
passSub("F14a", "回执能补送给当时离线的发送方", "test/accept/rest_accept_test.go"),
|
||
},
|
||
"F15": {
|
||
passSub("F15a", "输一次记住", "test/accept/rest_accept_test.go;internal/app/identity/self_talk_test.go TestF15TalkPasswordAuth"),
|
||
passSub("F15b", "改密失效", "test/accept/rest_accept_test.go;internal/app/identity/self_talk_test.go TestF15ReplyGrantAndChange"),
|
||
passSub("F15c", "回复免密", "test/accept/rest_accept_test.go;internal/app/identity/self_talk_test.go"),
|
||
passSub("F15d", "进群仍要密码", "test/accept/rest_accept_test.go;internal/app/identity/self_talk_test.go TestF15JoinNeedsPasswordDespiteGrant"),
|
||
passSub("F15e", "防多账号轮流猜", "test/accept/rest_accept_test.go"),
|
||
},
|
||
"F16": {
|
||
passSub("F16a", "建群拉成员后可群发", "test/accept/accept_test.go"),
|
||
passSub("F16b", "群主权限", "internal/app/group/group_test.go TestF16CreateAddPasswordAndOwner"),
|
||
passSub("F16c", "退出后不再收到", "internal/app/group/group_test.go TestF16LeaveRemoveDissolve"),
|
||
passSub("F16d", "解散后同编号新群不收旧消息", "internal/app/group/group_test.go TestF16LeaveRemoveDissolve"),
|
||
},
|
||
"F17": {
|
||
passSub("F17a", "管理登录、CSRF、错误密码锁定", "test/accept/accept_test.go runAdminAuth"),
|
||
passSub("F17b", "管端开通", "test/accept/accept_test.go runEndpointCreate"),
|
||
passSub("F17c", "管注册", "test/accept/accept_test.go runRegistration;internal/admin/a3_test.go TestRegistrationToggleAndGenerate"),
|
||
passSub("F17d", "管群", "internal/admin/a3_test.go TestGroupsCRUD"),
|
||
passSub("F17e", "查记录响应无正文", "internal/admin/a3_test.go TestMessageDetailHasNoBody"),
|
||
passSub("F17f", "API 令牌可用且不能越权", "internal/admin/admin_test.go TestAPITokenAuthAndRestrictions"),
|
||
},
|
||
"F18": {
|
||
passSub("F18a", "送达后正文消失", "test/accept/rest_accept_test.go;internal/app/message/delivery_test.go TestDeliveryStateMachine/F18_body_gone_record_zero_idempotent_kept"),
|
||
passSub("F18b", "记录天数 0 时连记录消失", "test/accept/rest_accept_test.go"),
|
||
passSub("F18c", "防重仍在", "test/accept/rest_accept_test.go;internal/app/message/submit_test.go TestSubmitTable/idempotent_hit"),
|
||
},
|
||
"F19": {
|
||
passSub("F19a", "四种 SDK 接入清单(仓库内已有测试,本波不重跑全量)", "sdk/go/itest_checklist_test.go;sdk/js/test/checklist.test.ts;sdk/python/tests/test_checklist.py;sdk/java ChecklistTest"),
|
||
},
|
||
"F20": {
|
||
passSub("F20a", "裸 MQTT WebSocket 登录、收、确认、发", "test/accept/accept_test.go runMessagingAccept"),
|
||
},
|
||
"F21": {
|
||
passSub("F21a", "同一 listen 端口提供后台、WebSocket、注册", "test/accept/accept_test.go"),
|
||
untestedSub("F21b", "裸 TCP MQTT 登录收发", "验收用例只走 WebSocket;listener 首字节识别见 internal/listener/listener_test.go TestIdentifyPlainHTTPAndMQTT,不足以上升为通过"),
|
||
passSub("F21c", "后台可分到单独端口", "internal/listener/listener_test.go TestAdminSeparateMQTTClosedAndAdmin404OnListen"),
|
||
},
|
||
"F22": {
|
||
passSub("F22a", "空目录 admin init + serve 健康检查", "test/accept/accept_test.go runInitHealthz"),
|
||
passSub("F22b", "备份", "cmd/nixmsg/commands_test.go TestBackupVacuumInto"),
|
||
passSub("F22c", "升级迁移前备份", "internal/store/db_test.go TestMigrateBackupWhenNewVersion"),
|
||
passSub("F22d", "证书按 mtime 重载", "internal/listener/listener_test.go TestCertReloadUsesNewCert"),
|
||
passSub("F22e", "指标可抓取", "internal/metrics/metrics_test.go TestMetricsHandlerExposesText;internal/httpx/metrics_test.go TestMetricsGateSharedPort"),
|
||
untestedSub("F22f", "Docker 全量冒烟", "镜像未推送;本波未跑 compose 全量"),
|
||
},
|
||
"F23": {
|
||
passSub("F23a", "注册开关、安全码校验、换码不影响已注册", "test/accept/accept_test.go runRegistration;internal/app/identity/register_test.go TestRegisterF23_*"),
|
||
passSub("F23b", "输错锁定", "internal/app/identity/register_test.go TestRegisterF23_WrongCodeLock"),
|
||
},
|
||
}
|
||
|
||
// CatalogItems 按 PRD §10 拆子项;整项仅在全部子项通过时为通过。
|
||
func CatalogItems() []Item {
|
||
out := make([]Item, 0, len(Features))
|
||
for _, f := range Features {
|
||
subs := catalogSubs[f.ID]
|
||
copied := make([]SubItem, len(subs))
|
||
copy(copied, subs)
|
||
out = append(out, Rollup(Item{ID: f.ID, Subs: copied}))
|
||
}
|
||
return out
|
||
}
|
||
|
||
// CatalogResults 写出带固定时间戳的结果,便于对照表可复现。
|
||
func CatalogResults() Results {
|
||
return Results{
|
||
GeneratedAt: "2026-09-30T08:30:00Z",
|
||
Items: CatalogItems(),
|
||
}
|
||
}
|
||
|
||
// Reconcile 以目录为底;现场探测失败则整项记失败,忽略「部分覆盖却标通过」。
|
||
func Reconcile(live []Item) []Item {
|
||
byLive := make(map[string]Item, len(live))
|
||
for _, it := range live {
|
||
byLive[strings.ToUpper(strings.TrimSpace(it.ID))] = it
|
||
}
|
||
out := CatalogItems()
|
||
for i, it := range out {
|
||
lv, ok := byLive[it.ID]
|
||
if !ok {
|
||
continue
|
||
}
|
||
st, err := parseStatus(lv.Status)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
if st == StatusFail {
|
||
it.Status = StatusFail
|
||
if lv.Note != "" {
|
||
it.Note = lv.Note
|
||
}
|
||
out[i] = it
|
||
}
|
||
}
|
||
return out
|
||
}
|