feat(gateway)!: 后台思考档位精确化(+ 面板三态 + 契约 v0.69/v0.70)
- gateway:优先读落库的 rec.Urgency(精确档位,与安静时段窗口无关);仅空串才回落到既有下限推导,并打 urgencyLowerBound 标记(**只在推导值上为 true**)
- 面板三态:精确(例常/紧急(高)/紧急·安全域)|下限(一律加 ≥ 前缀)|未知;下限悬停说明写清「为什么旧行只能是下限」
- 实时帧零协议改动:Record 的 JSON 键就是 urgency ⇒ 帧自然带上;wsParse 早已防御性读取(只加一处 urgencyLowerBound 防御读)
- 契约:v0.69(directMsgPerMin 透传修复,逐字照抄附录 A)与 v0.70(精确化 + 更正 §4.10(b)「实时帧不带这两键」)一起登记
- 变异实测:旧行不标下限 ⇒ 3 条测试红(结构体对比);前端去掉 ≥ ⇒ 渲染测试红
- ⚠️ 诚实边界(§7.3 已登记):实时旧记录刷新前显示「档位未知」、刷新后显示「≥ 例常」——同一条记录两路下的已知差异
This commit is contained in:
1 parent
f9c5b56a2f
commit
a0abf0199a
11 files changed
+390
-131
No files matched your search
@@ -125,11 +125,11 @@ type AdminUserRecord struct {
|
||||
// 它们直接来自后端已存的 PushResult,因此面板不必靠猜来解释
|
||||
// 一次静默。
|
||||
//
|
||||
// Urgency / QuietHoursBypassed(v0.68)回答「这一轮有多紧急、是不是
|
||||
// 穿透了安静时段」。二者都**只在存档可证时**出现,缺席 ≠ 常规档位:
|
||||
// 推导依据见 admin_backthink_urgency.go(那一轮的 `At` 落在安静时段窗口内
|
||||
// 时,判定级联已把档位锁死到可证的范围;窗口之外的轮次存档里没有档位,
|
||||
// 因此不下发,而不是编一个 routine)。
|
||||
// Urgency / UrgencyLowerBound / QuietHoursBypassed 回答「这一轮有多紧急、
|
||||
// 是不是穿透了安静时段」。三者都**只在存档可证时**出现,缺席 ≠ 常规档位:
|
||||
// 取值来源与优先级见 admin_backthink_urgency.go —— 优先读每轮落库的**精确**
|
||||
// 档位;只有 `urgencyLowerBound:true` 的行才是**推导出的下限**(旧行按安静时段
|
||||
// 窗口规则反推),窗口之外且无落库档位的旧行干脆不下发,而不是编一个 routine。
|
||||
type AdminBackthinkItem struct {
|
||||
ID string `json:"id"`
|
||||
Trigger string `json:"trigger"`
|
||||
@@ -143,10 +143,17 @@ type AdminBackthinkItem struct {
|
||||
// Bypassed 表示这是事件驱动的推送、绕过了限频闸("该发就发")。
|
||||
Bypassed bool `json:"bypassed"`
|
||||
At int64 `json:"at"`
|
||||
// Urgency 是这一轮紧急度的**存档可证下限**(urgency 档位词汇:
|
||||
// routine | warning | critical)。`warning` 读作「≥ high 档(可能更高)」;
|
||||
// 无法证明时省略该键(面板显示「未知」)。
|
||||
// Urgency 是这一轮的紧急度档位(urgency 档位词汇:routine | warning |
|
||||
// critical)。**默认读到的是该轮实际生效的精确档位**(每一轮随记录落库);
|
||||
// 只有与 `urgencyLowerBound:true` 同时出现时才读作「≥ 该档(可能更高)」;
|
||||
// 无法证明时省略该键(面板显示「未知」,**不是**「例常」)。
|
||||
Urgency string `json:"urgency,omitempty"`
|
||||
// UrgencyLowerBound 为 true 时表示 Urgency 是**推导出的下限**而不是精确档位:
|
||||
// 该行没有落库档位(`backthink_records.urgency` 列出现之前的旧行,或从未走到
|
||||
// 档位判定点的行),值由安静时段窗口规则反推(见 admin_backthink_urgency.go)。
|
||||
// 只在为 true 时出现:精确档位与「无档位」都不必在线上区分。客户端**不得**把
|
||||
// 它显示成精确档位。
|
||||
UrgencyLowerBound bool `json:"urgencyLowerBound,omitempty"`
|
||||
// QuietHoursBypassed 为 true 时表示这一轮**穿透了安静时段**(轮次时间落在
|
||||
// 安静时段窗口内且该轮判定要推送)。只在为 true 时出现:安静时段之外的轮次
|
||||
// 与「没穿透」不必在线上区分,缺席即「没有这件事」。
|
||||
|
||||
@@ -8,53 +8,64 @@ import (
|
||||
)
|
||||
|
||||
// 本文件把「这一轮后台思考有多紧急、是不是穿透了安静时段」变成**存档里可证**的
|
||||
// 事实(切片 P4)。它刻意只读 `backthink.Record` 已有的字段 —— 本片不动
|
||||
// `internal/services/backthink/**`(通用紧急度通道的落库属那片的面):
|
||||
// 事实。档位有两个来源,**优先级明确**(X2b):
|
||||
//
|
||||
// - `Record.ShouldPush` 是那一轮的打扰判定结果;
|
||||
// - `Record.At` 是那一轮的墙钟时间(库里存 UTC,取钟点前必须换回本地
|
||||
// 时区,因为安静时段是**本地钟点**窗口);
|
||||
// - 安静时段窗口取**进程生效**的 `QUIET_START_HOUR`/`QUIET_END_HOUR`
|
||||
// (`config.Background`,与 backthink 的 cfg 同源:组合根把同一对值交给它)。
|
||||
// 1. **精确档位**:`Record.Urgency` 由 backthink 在档位判定点原样落库,是那一轮
|
||||
// **实际生效**的档位。非空 ⇒ 直接下发,**不标记下限**,也不受安静时段窗口约束
|
||||
// (它与 `At` 无关,是独立落库的事实,因此时间戳坏掉的行同样如实下发)。
|
||||
// 2. **下限推导**(只对 `Record.Urgency` 为空的行:该列出现之前的旧行,以及从未走到
|
||||
// 档位判定点的行 —— 系统事件降级、思考失败):用安静时段窗口规则推导,
|
||||
// 结果**必须**带 `urgencyLowerBound` 标记,绝不能被读成精确档位。推导依据:
|
||||
// - `Record.ShouldPush` 是那一轮的打扰判定结果;
|
||||
// - `Record.At` 是那一轮的墙钟时间(库里存 UTC,取钟点前必须换回本地时区,
|
||||
// 因为安静时段是**本地钟点**窗口);
|
||||
// - 安静时段窗口取**进程生效**的 `QUIET_START_HOUR`/`QUIET_END_HOUR`
|
||||
// (`config.Background`,与 backthink 的 cfg 同源:组合根把同一对值交给它)。
|
||||
//
|
||||
// 由 `backthink.Decide` 的判定级联(`decide.go` 的 baseDecide)可证:
|
||||
// 由 `backthink.Decide` 的判定级联(`decide.go` 的 baseDecide)可证:
|
||||
// - 安静时段内被**放行** ⇒ 该轮重要度必然 ≥ `ImportanceHigh`(更低档位在安静
|
||||
// 时段一律被扣下)⇒ 档位**至少** warning(high)。这就是 `quietHoursBypassed`
|
||||
// 的全部依据,不是启发式。
|
||||
// - 安静时段内被**扣下** ⇒ 档位不足以放行(也可能是她自己的结论要求不推,
|
||||
// 见 `Decide` 的首个分支)⇒ 只能给出「不低于最低档」这个下限。
|
||||
// - 安静时段**之外** ⇒ 判定根本不看档位(「想不想说」由她自己的结论决定),
|
||||
// 并且推送成功的行其 `PushResult.Detail` 已被投递说明覆盖 ⇒ 存档里没有任何
|
||||
// 字段能反推档位。此时**不下发** urgency(面板显示「未知」),绝不猜。
|
||||
//
|
||||
// - 安静时段内被**放行** ⇒ 该轮重要度必然 ≥ `ImportanceHigh`(更低档位在安静
|
||||
// 时段一律被扣下)⇒ 这一轮**穿透了安静时段**,档位**至少**是 `warning`
|
||||
// (high)。这就是 `quietHoursBypassed` 的全部依据,不是启发式。
|
||||
// - 安静时段内被**扣下** ⇒ 档位 < high ⇒ `routine`,未穿透。
|
||||
// - 安静时段**之外** ⇒ 判定根本不看档位(「想不想说」由她自己的结论决定),
|
||||
// 并且推送成功的行其 `PushResult.Detail` 已被投递说明覆盖 ⇒ 存档里没有任何
|
||||
// 字段能反推档位。此时**不下发** urgency(面板显示「未知」),绝不猜。
|
||||
//
|
||||
// 因此 `AdminBackthinkItem.Urgency` 的语义是「**存档可证的档位下限**」而不是
|
||||
// 「精确档位」:`warning` 读作「≥ high(含 critical)」。要做到精确档位,需要
|
||||
// backthink 把每轮的 `Import` 落进 `Record`(本片不做,已在契约 §4.10(c) 与
|
||||
// §7.4-17 如实登记)。
|
||||
// fail-safe:`At` 为零值(时间戳格式错乱的行,见 backthink.store.parseTime 的兜底)
|
||||
// 时不判下限、也不判穿透 —— 宁可不显示,也不把零值时间当成凌晨而谎报一轮穿透。
|
||||
|
||||
// backthinkQuietOutcome 是「该轮与安静时段的关系」的推导结果。
|
||||
// backthinkQuietOutcome 是「该轮紧急度与安静时段的关系」的推导结果。
|
||||
type backthinkQuietOutcome struct {
|
||||
// urgency 是档位下限标签(urgency.Level 的词汇),无法证明时为空串。
|
||||
// urgency 是档位标签(urgency.Level 的词汇),无事实时为空串。
|
||||
urgency string
|
||||
// urgencyLowerBound 报告 urgency 是**推导出的下限**而不是该轮的精确档位。
|
||||
// 只在旧行(没有落库档位)上为 true;精确档位与「无档位」都是 false。
|
||||
urgencyLowerBound bool
|
||||
// bypassed 报告该轮是否穿透了安静时段(安静时段内仍被放行)。
|
||||
bypassed bool
|
||||
}
|
||||
|
||||
// backthinkQuietOutcomeOf 推导一条存档记录可证的紧急度下限与穿透标记。
|
||||
//
|
||||
// `at` 为零值(时间戳格式错乱的行,见 backthink.store.parseTime 的兜底)时一律
|
||||
// 返回「无事实」:宁可不显示,也不把零值时间当成凌晨而谎报一轮穿透。
|
||||
// backthinkQuietOutcomeOf 推导一条存档记录可证的紧急度与穿透标记。
|
||||
func backthinkQuietOutcomeOf(rec backthink.Record, startHour, endHour int) backthinkQuietOutcome {
|
||||
if rec.At.IsZero() {
|
||||
return backthinkQuietOutcome{}
|
||||
}
|
||||
if !quietHoursActiveAt(startHour, endHour, rec.At.Local()) {
|
||||
return backthinkQuietOutcome{}
|
||||
// 精确档位优先:X2a 起该列随每轮落库,读到即事实(与窗口、与 At 都无关)。
|
||||
out := backthinkQuietOutcome{urgency: rec.Urgency}
|
||||
if rec.At.IsZero() || !quietHoursActiveAt(startHour, endHour, rec.At.Local()) {
|
||||
return out
|
||||
}
|
||||
if rec.ShouldPush {
|
||||
return backthinkQuietOutcome{urgency: urgency.LevelWarning.String(), bypassed: true}
|
||||
out.bypassed = true
|
||||
}
|
||||
return backthinkQuietOutcome{urgency: urgency.LevelRoutine.String()}
|
||||
if out.urgency != "" {
|
||||
return out
|
||||
}
|
||||
// 旧行:窗口规则给出的**下限**,必须带上标记。
|
||||
out.urgencyLowerBound = true
|
||||
out.urgency = urgency.LevelRoutine.String()
|
||||
if rec.ShouldPush {
|
||||
out.urgency = urgency.LevelWarning.String()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// quietHoursActiveAt 报告 at 是否落在安静时段窗口内。口径与既有三处逐字一致
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -12,8 +13,9 @@ import (
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/urgency"
|
||||
)
|
||||
|
||||
// 本文件是 P4「后台思考行紧急度 / 穿透安静时段」的测试:钉住
|
||||
// 「穿透标记 ⇔ 档位下限」的对应关系,以及「不可证时不下发」的诚实边界。
|
||||
// 本文件是 P4「后台思考行紧急度 / 穿透安静时段」与其 X2b 扩展(精确档位 + 下限标记)
|
||||
// 的测试:钉住「穿透标记 ⇔ 档位」的对应关系、「精确 vs 只是下限 vs 未知」三态在线上的
|
||||
// 可分辨性,以及「不可证时不下发」的诚实边界。
|
||||
|
||||
// quietCfg 构造安静时段 = 23:00-07:00(跨午夜)的生效配置。
|
||||
func quietCfg() *config.Config {
|
||||
@@ -67,22 +69,22 @@ func TestAdminBackthink_QuietHoursBypassMatchesUrgency(t *testing.T) {
|
||||
}
|
||||
// ① 安静时段内被放行 ⇒ 穿透 + 档位下限 warning(= 至少 high)。
|
||||
bypass := byID["3"]
|
||||
if !bypass.QuietHoursBypassed || bypass.Urgency != urgency.LevelWarning.String() {
|
||||
t.Fatalf("穿透行 = %+v, want urgency=%s + quietHoursBypassed=true", bypass, urgency.LevelWarning)
|
||||
if !bypass.QuietHoursBypassed || bypass.Urgency != urgency.LevelWarning.String() || !bypass.UrgencyLowerBound {
|
||||
t.Fatalf("穿透行 = %+v, want urgency=%s + quietHoursBypassed=true + 下限标记", bypass, urgency.LevelWarning)
|
||||
}
|
||||
// ② 安静时段内被扣下 ⇒ routine,且不得标成穿透。
|
||||
held := byID["2"]
|
||||
if held.QuietHoursBypassed || held.Urgency != urgency.LevelRoutine.String() {
|
||||
t.Fatalf("静默行 = %+v, want urgency=routine 且不穿透", held)
|
||||
if held.QuietHoursBypassed || held.Urgency != urgency.LevelRoutine.String() || !held.UrgencyLowerBound {
|
||||
t.Fatalf("静默行 = %+v, want urgency=routine(带下限标记)且不穿透", held)
|
||||
}
|
||||
// ③ 安静时段之外 ⇒ 两个键都不下发(存档不可证)。
|
||||
day := byID["1"]
|
||||
if day.QuietHoursBypassed || day.Urgency != "" {
|
||||
if day.QuietHoursBypassed || day.Urgency != "" || day.UrgencyLowerBound {
|
||||
t.Fatalf("白天行 = %+v, want 两个键都缺席", day)
|
||||
}
|
||||
// 线上形状:真穿透的行必须带这两个键;不可证的行必须**不带**它们(不是 false)。
|
||||
if !strings.Contains(body, `"urgency":"warning"`) || !strings.Contains(body, `"quietHoursBypassed":true`) {
|
||||
t.Fatalf("响应缺少穿透/档位键:%s", body)
|
||||
// 线上形状:真穿透的行必须带这三个键;不可证的行必须**不带**它们(不是 false)。
|
||||
if !strings.Contains(body, `"urgency":"warning"`) || !strings.Contains(body, `"urgencyLowerBound":true`) || !strings.Contains(body, `"quietHoursBypassed":true`) {
|
||||
t.Fatalf("响应缺少穿透/档位/下限键:%s", body)
|
||||
}
|
||||
if !strings.Contains(body, `"urgency":"routine"`) {
|
||||
t.Fatalf("响应缺少被扣下那轮的档位:%s", body)
|
||||
@@ -166,3 +168,139 @@ func TestBackthinkQuietOutcomeOf_ReadsLocalHour(t *testing.T) {
|
||||
t.Fatalf("UTC 存储的本地凌晨 2 点必须判为穿透:%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackthinkQuietOutcomeOf_ExactUrgencyWinsOverWindow 钉住取值优先级(X2b):
|
||||
// 落库档位非空 ⇒ **精确值**(与 `At`、窗口都无关,也**不标**下限);空 ⇒ 窗口推导
|
||||
// (一定**标**下限);窗口外且无档位 ⇒ 无事实(空,不标)。
|
||||
func TestBackthinkQuietOutcomeOf_ExactUrgencyWinsOverWindow(t *testing.T) {
|
||||
t.Parallel()
|
||||
// 精确档位:零值时间、白天、窗口内一律逐字下发,且都不是下限。
|
||||
for _, at := range []time.Time{{}, localAt(12), localAt(2)} {
|
||||
got := backthinkQuietOutcomeOf(
|
||||
backthink.Record{At: at, Urgency: urgency.LevelCritical.String(), ShouldPush: false}, 23, 7)
|
||||
if got.urgency != urgency.LevelCritical.String() || got.urgencyLowerBound {
|
||||
t.Fatalf("at=%v 的精确档位 = %+v, want critical 且无下限标记", at, got)
|
||||
}
|
||||
}
|
||||
// 旧行(空档位)落在窗口内 ⇒ 值来自推导,必须标下限。
|
||||
pushed := backthinkQuietOutcomeOf(backthink.Record{At: localAt(2), ShouldPush: true}, 23, 7)
|
||||
if pushed.urgency != urgency.LevelWarning.String() || !pushed.urgencyLowerBound || !pushed.bypassed {
|
||||
t.Fatalf("窗口内放行的旧行 = %+v, want warning + 下限标记 + 穿透", pushed)
|
||||
}
|
||||
held := backthinkQuietOutcomeOf(backthink.Record{At: localAt(2), ShouldPush: false}, 23, 7)
|
||||
if held.urgency != urgency.LevelRoutine.String() || !held.urgencyLowerBound || held.bypassed {
|
||||
t.Fatalf("窗口内扣下的旧行 = %+v, want routine + 下限标记 + 不穿透", held)
|
||||
}
|
||||
// 窗口外的旧行:没有事实,什么都不下发(也不标下限)。
|
||||
day := backthinkQuietOutcomeOf(backthink.Record{At: localAt(12), ShouldPush: true}, 23, 7)
|
||||
if day.urgency != "" || day.urgencyLowerBound || day.bypassed {
|
||||
t.Fatalf("窗口外的旧行 = %+v, want 全空", day)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdminBackthink_ExactUrgencyIsNotLowerBound 是 X2b 的线上判据:带落库档位的新行
|
||||
// ⇒ 精确档位且**不带**下限标记;空档位的旧行 ⇒ 窗口推导且**带**下限标记;窗口外且
|
||||
// 无档位 ⇒ 键缺席(未知)。三态必须在线形状上可分辨 —— 「不许谎报」就是这条。
|
||||
func TestAdminBackthink_ExactUrgencyIsNotLowerBound(t *testing.T) {
|
||||
t.Parallel()
|
||||
feed := &stubBackthinkFeed{records: []backthink.Record{
|
||||
{ // ① 新行 + 安静时段内 + 放行 ⇒ 精确 critical(不是「≥ 高」的下限),仍算穿透。
|
||||
ID: 40, Trigger: "iot.device.smoke", At: localAt(2), Conclusion: "烟雾告警", Urgency: "critical",
|
||||
ShouldPush: true,
|
||||
PushResult: backthink.PushResult{Pushed: true, Route: backthink.RouteActiveClient, Bypassed: true, Detail: "delivered"},
|
||||
},
|
||||
{ // ② 新行 + 白天 ⇒ 精确 routine(窗口规则给不出任何档位,精确值照样可得)。
|
||||
ID: 39, Trigger: "heartbeat", At: localAt(12), Conclusion: "白天的一轮", Urgency: "routine",
|
||||
ShouldPush: false,
|
||||
PushResult: backthink.PushResult{Route: backthink.RouteNone, Detail: "conclusion requested no push"},
|
||||
},
|
||||
{ // ③ 新行 + 时间戳坏掉 ⇒ 精确档位与窗口无关,如实下发;但不判穿透。
|
||||
ID: 38, Trigger: "broken clock", Urgency: "warning", ShouldPush: true,
|
||||
},
|
||||
{ // ④ 旧行(空档位)+ 安静时段内 + 放行 ⇒ 下限 warning + 下限标记。
|
||||
ID: 37, Trigger: "legacy.pushed", At: localAt(2), Conclusion: "旧行(窗口内放行)", ShouldPush: true,
|
||||
PushResult: backthink.PushResult{Pushed: true, Route: backthink.RouteIMBridge, Detail: "delivered via IM bridge"},
|
||||
},
|
||||
{ // ⑤ 旧行(空档位)+ 安静时段内 + 扣下 ⇒ 下限 routine + 下限标记。
|
||||
ID: 36, Trigger: "legacy.held", At: localAt(3), Conclusion: "旧行(窗口内扣下)", ShouldPush: false,
|
||||
PushResult: backthink.PushResult{Route: backthink.RouteNone, Detail: "quiet hours; not interrupting"},
|
||||
},
|
||||
{ // ⑥ 旧行(空档位)+ 白天 ⇒ 键缺席(未知),绝不猜一个 routine。
|
||||
ID: 35, Trigger: "legacy.day", At: localAt(12), Conclusion: "旧行(白天)", ShouldPush: true,
|
||||
PushResult: backthink.PushResult{Pushed: true, Route: backthink.RouteActiveClient, Detail: "delivered"},
|
||||
},
|
||||
}}
|
||||
_, ts, jwt := buildEnv(t, quietCfg(), Deps{Backthink: feed})
|
||||
status, raw, items := listBackthinkItems(t, ts, signAny(t, jwt, "acct", auth.RoleOwner))
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("status = %d, body = %s", status, raw)
|
||||
}
|
||||
if len(items) != 6 {
|
||||
t.Fatalf("items = %d, want 6", len(items))
|
||||
}
|
||||
byID := make(map[string]AdminBackthinkItem, len(items))
|
||||
for _, it := range items {
|
||||
byID[it.ID] = it
|
||||
}
|
||||
for _, want := range []struct {
|
||||
id, urgency string
|
||||
bypassed bool
|
||||
}{
|
||||
{"40", urgency.LevelCritical.String(), true},
|
||||
{"39", urgency.LevelRoutine.String(), false},
|
||||
{"38", urgency.LevelWarning.String(), false},
|
||||
} {
|
||||
got := byID[want.id]
|
||||
if got.Urgency != want.urgency || got.UrgencyLowerBound || got.QuietHoursBypassed != want.bypassed {
|
||||
t.Fatalf("精确行 %s = %+v, want urgency=%s + 无下限标记 + bypassed=%v", want.id, got, want.urgency, want.bypassed)
|
||||
}
|
||||
}
|
||||
for _, want := range []struct {
|
||||
id, urgency string
|
||||
bypassed bool
|
||||
}{
|
||||
{"37", urgency.LevelWarning.String(), true},
|
||||
{"36", urgency.LevelRoutine.String(), false},
|
||||
} {
|
||||
got := byID[want.id]
|
||||
if got.Urgency != want.urgency || !got.UrgencyLowerBound || got.QuietHoursBypassed != want.bypassed {
|
||||
t.Fatalf("旧行 %s = %+v, want urgency=%s + 下限标记 + bypassed=%v", want.id, got, want.urgency, want.bypassed)
|
||||
}
|
||||
}
|
||||
if day := byID["35"]; day.Urgency != "" || day.UrgencyLowerBound || day.QuietHoursBypassed {
|
||||
t.Fatalf("窗口外的旧行 = %+v, want 三个键都缺席", day)
|
||||
}
|
||||
// 线上形状(按 id 看**实际出现了哪些键**):下限键只许出现在旧行上,且恒为 true。
|
||||
wire := wireKeysByID(t, raw)
|
||||
for _, id := range []string{"40", "39", "38", "35"} {
|
||||
if _, ok := wire[id]["urgencyLowerBound"]; ok {
|
||||
t.Fatalf("id=%s 不是下限行,却带了 urgencyLowerBound:%v", id, wire[id])
|
||||
}
|
||||
}
|
||||
for _, id := range []string{"37", "36"} {
|
||||
if v, ok := wire[id]["urgencyLowerBound"]; !ok || v != true {
|
||||
t.Fatalf("id=%s 是下限行,urgencyLowerBound 必须显式为 true:%v", id, wire[id])
|
||||
}
|
||||
}
|
||||
if _, ok := wire["35"]["urgency"]; ok {
|
||||
t.Fatalf("窗口外且无档位的旧行不得带 urgency:%v", wire["35"])
|
||||
}
|
||||
}
|
||||
|
||||
// wireKeysByID 按 id 给出每个条目在原始 JSON 里**实际出现了哪些键**——解码到结构体后
|
||||
// 「键缺席」与「false」不可区分,而本切片的判据正是「键在不在」(对齐既有测试的口径)。
|
||||
func wireKeysByID(t *testing.T, raw []byte) map[string]map[string]any {
|
||||
t.Helper()
|
||||
var list struct {
|
||||
Data []map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &list); err != nil {
|
||||
t.Fatalf("decode raw: %v (%s)", err, raw)
|
||||
}
|
||||
out := make(map[string]map[string]any, len(list.Data))
|
||||
for _, m := range list.Data {
|
||||
id, _ := m["id"].(string)
|
||||
out[id] = m
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -391,8 +391,9 @@ func (s *Server) adminListBackthink(w http.ResponseWriter, r *http.Request) {
|
||||
Detail: rec.PushResult.Detail,
|
||||
Bypassed: rec.PushResult.Bypassed,
|
||||
At: rec.At.UnixMilli(),
|
||||
// 紧急度下限 / 是否穿透安静时段(v0.68):只报存档可证的部分。
|
||||
// 紧急度(优先精确档位,旧行带下限标记)/ 是否穿透安静时段:只报存档可证的部分。
|
||||
Urgency: quiet.urgency,
|
||||
UrgencyLowerBound: quiet.urgencyLowerBound,
|
||||
QuietHoursBypassed: quiet.bypassed,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user