fix(flow): 时间轴两处诚实性缺陷(X1)

① round 号跨重启被复用 ⇒ 时间轴把多个真实轮次并成一块(真机实测 web 的 r:1 挤了 4 路、
   startedAt 跨 13:52→21:05)。修法:发号前从落盘播种(MaxRound,json_extract + json_valid 挡坏行),
   每会话每进程只播一次、失败只 WARN 且不重试、并发首轮共享一次读;接口形状零变化
② truncated 一个字段兼两个意思 ⇒ 一页没拉完时前端会说「更早的轮次已过保留期」(假话)。
   新增 retentionBoundaryReached(= 保留期启用 且 已翻到底 且 现存最早一轮 round>1 ⇒ 确有轮次被清扫),
   truncated 语义一字未改;盲区如实登记(整段被清扫会说少、最早一轮落盘失败可能多说)
3 个变异自检(不落账/floor>0/去掉 hasMore 守卫)
This commit is contained in:
AskaEth committed 2026-10-06 21:16:21 +08:00
1 parent b36798265a
commit 2bf3b536dd
9 files changed
+820 -12

No files matched your search

@@ -240,6 +240,54 @@ func TestTimeline_EmptyAndNilDB_边界(t *testing.T) {
}
}
// TestEarlierRoundsSwept_RoundFloorIsTheEvidence_正常 钉住**清扫信号**的判据(切片 X1;
// design/19 §7.1):该会话**完整**的轮次列表里,最早一轮的轮次号 > 1 ⇒ 号更小的轮次曾
// 发过号且已不在库里 ⇒ 「更早的轮次已被保留期清扫删掉」成立;最早一轮就是 1 ⇒ 没有更早
// 的被删,绝不说「已过保留期」。
func TestEarlierRoundsSwept_RoundFloorIsTheEvidence_正常(t *testing.T) {
t.Parallel()
roundsOf := func(rounds ...int) []TimelineRound {
out := make([]TimelineRound, 0, len(rounds))
for _, r := range rounds {
out = append(out, TimelineRound{Key: fmt.Sprintf("r:%d", r), Round: r})
}
return out
}
if EarlierRoundsSwept(roundsOf(1, 2, 3)) {
t.Fatal("最早一轮是第 1 轮 ⇒ 没有更早的被清扫,绝不能说「已过保留期」")
}
if !EarlierRoundsSwept(roundsOf(5, 6)) {
t.Fatal("最早一轮是第 5 轮 ⇒ 号更小的轮次确实发过号、已不在库里 ⇒ 必须为 true")
}
if !EarlierRoundsSwept(roundsOf(3, 7, 12)) {
t.Fatal("乱序/跳号不影响判据:取最小轮次号 > 1 即为 true")
}
// 同一轮的多路(真并行)不改变最早轮次号。
parallel := append(roundsOf(2, 3), TimelineRound{Key: "r:2", Round: 2, Lanes: nil})
if !EarlierRoundsSwept(parallel) {
t.Fatal("同一轮两路时最早的仍是第 2 轮 ⇒ true(多路不该把判据翻成 false)")
}
}
// TestEarlierRoundsSwept_UnknownRoundAndEmptyAreConservative_边界 钉住两条保守口径:
// 轮次号未知的轮次在场(老行 / 在飞路没有号)时判据**不成立**(返回 false,宁少说不多说),
// 空列表同样为 false —— 别把「不知道」说成「被清扫了」。
func TestEarlierRoundsSwept_UnknownRoundAndEmptyAreConservative_边界(t *testing.T) {
t.Parallel()
if EarlierRoundsSwept(nil) || EarlierRoundsSwept([]TimelineRound{}) {
t.Fatal("没有轮次 ⇒ 判据不成立(绝不是「已过保留期」)")
}
mixed := []TimelineRound{
{Key: "t:web-old-1", Round: 0},
{Key: "r:5", Round: 5},
}
if EarlierRoundsSwept(mixed) {
t.Fatal("有轮次号未知的轮次在场 ⇒ 号不可信 ⇒ 必须保守为 false")
}
}
// roundKeys 取出轮次键序列(断言用)。
func roundKeys(rounds []TimelineRound) []string {
out := make([]string, 0, len(rounds))