chapter 30 relation lab, seed=302026, db=social30

CASE 30-A :: 公开账号：关注生效、写事件、投影消费后粉丝列表与计数一致
  note follow(user-0001 -> user-0002) = CHANGED(ACTIVE,v1,seq1)
  ok   公开账号直接 ACTIVE (expected=ACTIVE, actual=ACTIVE)
  ok   首个有效版本为 1 (expected=1, actual=1)
  ok   写入 1 条事件 (expected=1, actual=1)
  ok   投影计数 = 1 (expected=1, actual=1)
  ok   投影与真相源无差异

CASE 30-B :: 私密账号：关注进入 PENDING，重复请求不产生新事件，审批后 ACTIVE，非 PENDING 不能审批
  note request=CHANGED(PENDING,v1,seq2) repeat=NOOP(PENDING,v1,already PENDING)
  ok   私密账号进入 PENDING (expected=PENDING, actual=PENDING)
  ok   重复请求为 NOOP (expected=NOOP, actual=NOOP)
  note approve=CHANGED(ACTIVE,v2,seq3)
  ok   审批后 ACTIVE (expected=ACTIVE, actual=ACTIVE)
  ok   审批使版本变为 2 (expected=2, actual=2)
  ok   对 ACTIVE 再次审批被拒绝: not pending (expected=REJECTED, actual=REJECTED)
  note request=CHANGED(PENDING,v1,seq4) reject=CHANGED(REMOVED,v2,seq5)
  ok   拒绝后 REMOVED (expected=REMOVED, actual=REMOVED)
  ok   PENDING 与被拒请求不计入粉丝数 (expected=1, actual=1)

CASE 30-C :: 边界：自关注、未关注时取消、取消后再关注版本继续递增、拉黑后关注被拒、解除拉黑不恢复旧关系
  ok   自关注被拒 (expected=REJECTED, actual=REJECTED)
  ok   未关注时取消为 NOOP (expected=NOOP, actual=NOOP)
  ok   NOOP 不写事件 (expected=5, actual=5)
  note follow/unfollow/follow -> CHANGED(ACTIVE,v3,seq8)
  ok   再关注得到版本 3，旧版本不会被重用 (expected=3, actual=3)
  ok   拉黑在同一事务内移除关系 (expected=REMOVED, actual=REMOVED)
  note blocked follow both ways -> REJECTED(REMOVED,v4,blocked) / REJECTED(REMOVED,v0,blocked)
  ok   双向关注都被拒
  ok   解除拉黑后旧关系仍为 REMOVED (expected=REMOVED, actual=REMOVED)

CASE 30-D :: 16 个线程同时关注同一对：一行关系、版本 1、恰好 1 条事件
  note CHANGED=1 NOOP=15 errors=0
  ok   无异常 (expected=0, actual=0)
  ok   只有一个请求改变状态 (expected=1, actual=1)
  ok   版本 = 1 (expected=1, actual=1)
  ok   事件 = 1 (expected=1, actual=1)

CASE 30-E :: 8 线程 × 150 次随机关注/取消交错于 6 个关系对：版本连续、末事件与真相源一致
  ok   无异常 (expected=0, actual=0)
  note state-changing ops=595 events=595
  ok   每对事件版本为 1..v 且 v 等于真相源版本
  ok   每对最高版本事件的状态等于真相源状态
  ok   改变状态的操作数 = 事件数 (expected=595, actual=595)

CASE 30-F :: 单对乱序：ACTIVE v1 / REMOVED v2 按 v2、v1、v1 到达
  note arrivals v2,v1,v1 -> [applied, stale, dup]
  ok   旧版本被判 stale，重复被判 dup (expected=[applied, stale, dup], actual=[applied, stale, dup])
  ok   投影保持 REMOVED (expected=REMOVED, actual=REMOVED)
  ok   投影与真相源无差异

CASE 30-G :: 追加 300 对、每对 2–4 次关注/取消；全量事件 30% 重复后随机乱序，4 个消费者线程并发应用；对照组按到达顺序直接覆盖
  note truth after workload: ACTIVE=112 REMOVED=201
  note events=1485 deliveries=1942 seed=302026
  note naive: resurrected=100 missing=37 wrongCounts=43
  ok   negative control exposes: 按到达顺序应用时已取消的关系复活 (100 对)
  note versioned: verdicts={applied=580, dup=457, stale=905} resurrected=0 missing=0 wrongCounts=0
  ok   无异常 (expected=0, actual=0)
  ok   版本化消费后投影与真相源无差异
  ok   连续水位 = 事件总数 (expected=1485, actual=1485)

CASE 30-H :: 关注先锁关系行再检查拉黑；拉黑事务在关注事务持锁期间到达并等待行锁
  note follow result=CHANGED(ACTIVE,v1,seq1486) final relation=REMOVED block rows=1 blockTxWaitedOnRowLock=true
  ok   无异常 (expected=0, actual=0)
  ok   观察到拉黑事务在 pg_locks 中等待行锁
  ok   拉黑后关系为 REMOVED (expected=REMOVED, actual=REMOVED)
  ok   block 行存在 (expected=1, actual=1)

CASE 30-I :: 负向对照：关注先检查拉黑再加锁，检查与加锁之间拉黑事务完整提交
  note follow result=CHANGED(ACTIVE,v1,seq1488) final relation=ACTIVE block rows=1
  ok   无异常 (expected=0, actual=0)
  ok   negative control exposes: 拉黑已提交，关系仍为 ACTIVE

CASE 30-J :: 拉黑与双向关注并发 40 轮（真实线程，无插桩）：不存在被拉黑仍有效的关系
  note rounds=40 blockRows=40
  ok   40 轮拉黑全部提交 (expected=40, actual=40)
  note errors=0 violations=0
  ok   无异常（死锁会以 40P01 重试） (expected=0, actual=0)
  ok   拉黑存在时双向关系均为 REMOVED (expected=0, actual=0)

CASE 30-K :: 撤销后发布新内容：投影滞后时，只读投影泄露；内容令牌与新鲜令牌回退真相源
  note P1 acl_token=1614 watermark=1614
  ok   撤销前 P1 可读 (expected=true, actual=true)
  note block seq=1615 P2 acl_token=1615 watermark=1614 (projection not drained)
  note TRUTH           P1=DENY/truth P2=DENY/truth
  note PROJECTION_ONLY P1=ALLOW/projection P2=ALLOW/projection
  note CONTENT_TOKEN   P1=ALLOW/projection P2=DENY/fallback-truth
  note FRESH_TOKEN     P1=DENY/fallback-truth P2=DENY/fallback-truth
  ok   negative control exposes: 只读投影时，被拉黑者读到拉黑之后发布的 P2
  ok   真相源拒绝 P2 (expected=false, actual=false)
  ok   内容令牌拒绝 P2（回退真相源） (expected=false, actual=false)
  ok   新鲜令牌拒绝 P1 (expected=false, actual=false)
  ok   内容令牌对旧内容 P1 仍允许：这是该模式声明的滞后窗口
  note after drain watermark=1615 P1 CONTENT_TOKEN=DENY/projection
  ok   投影追上后，内容令牌模式也拒绝 P1 (expected=false, actual=false)

CASE 30-L :: 恢复：清空投影，先投递 60% 后消费者丢失进度重启，从头再次乱序投递全量
  note after partial: watermark=3/1615 diff=72,39,53
  note after restart replay: watermark=1615 diff=0,0,0
  ok   中途水位低于事件总数（确有未完成部分）
  ok   重放后投影与真相源无差异
  ok   水位追平 (expected=1615, actual=1615)

CASE 30-M :: 可见性边界：PUBLIC 对非粉丝可读、拉黑后各模式均拒绝；SELF 只对作者可读
  note stranger PUBLIC CONTENT_TOKEN=ALLOW/public-block-check
  ok   非粉丝在四种模式下都能读 PUBLIC
  note after block stranger PUBLIC PROJECTION_ONLY=DENY/public-block-check
  ok   拉黑后四种模式都拒绝 PUBLIC（拉黑直接读 block 表）
  note fan SELF=DENY/self-only owner SELF=ALLOW/owner
  ok   有效粉丝读 SELF 被拒 (expected=false, actual=false)
  ok   作者读 SELF 允许 (expected=true, actual=true)
  ok   解除拉黑后 PUBLIC 恢复可读 (expected=true, actual=true)
tx retries observed (40001/40P01): 0

SUMMARY passed=58 failed=0
