test(repo): 建 (d1) 判据 —— 上报非空 list 时不得删除其它 workspace 的行(**今天可写、现在红、修好即绿**)

pi `e77154d1` §三 指出我"把 (d) 整体归入 due"是**反方向的错**: 判据的**可得性**本身要复核 ——
把今天就能给的判据当成"要等未来才能给",余额里就挂着一个今天就能变绿的缺口。
我先写仓内探针逐条跑(跑完即删),四条读数:

  [修前] 播下 /A=2 → B 上报 /B=1        ⇒ (d1) FAIL: /A = 0,期望 2   ★本缺陷
  [修前] T\L @DELETE前 = [/A]            ⇒ 响 ✓(我那形状确实抓得到真缺陷)
  [修好] 共存 /A=2 /B=2 → /A 仍 = 2      ⇒ (d1) PASS ⇒ 修好即绿 ✓
  [修好] T\L @DELETE前 = [/A]            ⇒ ★ 假阳:修好后每次心跳都常鸣
  [修好] 与"实际删除域"比 destroyed = [] ⇒ 不响 ✓ 无假阳

⇒ ★★ 同时**撤回我 §三 提的 `T\L ⇒ WARN` 形状**(记入 DEBTS 补记之九):
   根因: "即将被销毁"由 **DELETE 的谓词**决定,不是由 T\L 决定。
   修前 DELETE 域 = 整个 agent ⊋ L ⇒ T\L 恰等于被销毁集合(碰巧对)
   修后 DELETE 域 = 按 ws 删 = L       ⇒ 被销毁 = ∅,而 T\L 仍非空 ⇒ **恒假阳**
   而修好后表里天然共存多 ws(那正是修复目标)⇒ **每次心跳常鸣**
   ⇒ 落进本仓「**还清了反而红**」那个坑 —— 我为 (d) 拒绝超前断言的理由,
     在我自己提的形状里以假阳形式复现了。忠实形状: `destroyed = 被本次 DELETE 移除
     且未被本次 list 重插的 ws`(与实际删除域同源 ⇒ 修前响/修后不响,且 [] 时仍覆盖)
⇒ ★★ (d) 拆两半: **d1 = 本条**(每项自带 Workspace ⇒ 不需要请求级字段 ⇒ 今日可判);
   **d2 = 上报 [] 时只清自己那个 ws**(需要"这次上报属于谁")⇒ 与 scope 字段同 due

自查: 探针第一版把"取 T"写在 Replace **之后** ⇒ 量到删除后的表 ⇒ 结论会全反
      (据此差点得出"漏报"的相反结论);已把取 T 排到 B 上报**之前**重测。
      教训: **观测点必须与被观测的判据在同一时刻** —— 与"判据要锚定到它防的那个动作"同一条。
测试: ./internal/repo/ 245 通过、唯一红项即本条(它断言的正是尚未修复的缺陷)
登记: platform-mirror-d1-cross-workspace(余额 28→29);-run Debt ⇒ ok
This commit is contained in:
2026-09-26 09:15:30 +08:00
parent ad05b6ce92
commit 9311612358
2 changed files with 80 additions and 1 deletions

File diff suppressed because one or more lines are too long

View File

@ -262,6 +262,77 @@ func TestPlatformSessionsWithoutSlugAreSkipped(t *testing.T) {
}
}
// ★★ (d1) 上报**非空** list 时,不得删除其它 workspace 的行。
//
// 背景(docs/DEBTS.json `platform-mirror-replace-domain-too-wide` 补记之八/之九):
// `ReplacePlatformSessions` 的 DELETE 域是整个 `agent_name`,而**上报域**是每个上报者自己那个
// workspace —— 两者不等("上报域 ⊊ 替换域")。opencode 的插件按 directory 实例化、
// `AGENT_NAME` 却是单一常量,于是每个实例的心跳都会把别的目录**整批擦掉**。
// 活库实测:200 样本 × 0.3s 里 25% 的时刻镜像为空、9 个目录轮流出场。
//
// ★ 这条**不依赖**请求级 scope 字段:每项 `PlatformSession` 自带 `Workspace`,
// 所以**今天就能写、现在就是红、修好即绿**。请求级字段只与 (d2)(上报 `[]`)有关。
//
// ⚠️ 本条**当前为红**——它断言的正是那个尚未修复的缺陷。
// 按本仓惯例,红的判据要么标 `[FAIL]` 形态、要么随修复一起转绿;
// 这里用 `t.Errorf` 而不是 `t.Fatalf`,好让"为什么红"直接显示在测试输出里。
func TestReplacePlatformSessionsKeepsOtherWorkspaces(t *testing.T) {
setupTestDB(t)
seedPlatformAgent(t, "opencode")
ctx := context.Background()
// 上报者 1:/A 下两条
if err := ReplacePlatformSessions(ctx, "opencode", []PlatformSession{
{PlatformID: "a1", Workspace: "/A", Slug: "a1"},
{PlatformID: "a2", Workspace: "/A", Slug: "a2"},
}); err != nil {
t.Fatalf("A 上报: %v", err)
}
// 上报者 2:只报 /B(list **非空**,每项都带 Workspace)
if err := ReplacePlatformSessions(ctx, "opencode", []PlatformSession{
{PlatformID: "b1", Workspace: "/B", Slug: "b1"},
}); err != nil {
t.Fatalf("B 上报: %v", err)
}
// 其它 workspace 的行数不得变化
var aCount int
if err := db.DB.QueryRowContext(ctx,
`SELECT count(*) FROM agent_platform_sessions WHERE agent_name = $1 AND workspace = $2`,
"opencode", "/A").Scan(&aCount); err != nil {
t.Fatalf("统计 /A: %v", err)
}
if aCount != 2 {
t.Errorf("(d1) 上报 /B 之后 /A 的行数 = %d,期望 2 —— 其它 workspace 被误擦;"+
"现存 workspace = %v(DELETE 域应与上报域一致,即只清本次上报的那些)",
aCount, workspacesOf(t, ctx, "opencode"))
}
}
// workspacesOf 列出某 agent 当前镜像里的全部 workspace(与行数),供失败信息里列出**元素**
// 而不是只给一个数 —— 范围要靠元素才可复核(清单 ⑫′)。
func workspacesOf(t *testing.T, ctx context.Context, agent string) map[string]int {
t.Helper()
rows, err := db.DB.QueryContext(ctx,
`SELECT workspace, count(*) FROM agent_platform_sessions WHERE agent_name = $1 GROUP BY workspace`,
agent)
if err != nil {
t.Fatalf("列 workspace: %v", err)
}
defer rows.Close()
out := map[string]int{}
for rows.Next() {
var ws string
var n int
if err := rows.Scan(&ws, &n); err != nil {
t.Fatalf("scan: %v", err)
}
out[ws] = n
}
return out
}
// 上报里的重复 platform_id 不该让整次事务失败(主键冲突)。
func TestReplacePlatformSessionsDedupes(t *testing.T) {
setupTestDB(t)