package history import ( "context" "errors" "testing" ) // 本文件锁定 Android 协同 §G4 的存储半边:客户端幂等键(client_msg_id)由 // (session_id, client_msg_id) 的**部分唯一索引**原子裁决,因此判重不是「先查 // 再插」的竞态写法,而是「写入被数据库拒绝,然后读回既有那一行」。 // 部分(WHERE client_msg_id != '')是必须的:否则同一会话的所有空键行会互相 // 冲突,等于禁止一个会话出现第二条消息。 func TestAppendFullDedupesClientMsgID(t *testing.T) { t.Parallel() ctx := context.Background() st := newStore(t) first, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "同一段", ClientMsgID: "cm-1"}) if err != nil { t.Fatalf("第一次写入:%v", err) } // 第二次:换一个 id(像客户端重发时服务端会另铸一个),同一个 clientMsgId。 second, err := st.AppendFull(ctx, Message{ID: "重发时的新 id", SessionID: MainSessionID, Role: RoleUser, Content: "同一段", ClientMsgID: "cm-1"}) if err != nil { t.Fatalf("第二次写入:%v", err) } if second.ID != first.ID { t.Fatalf("第二次返回的 id = %q,want 既有那条 %q(唯一索引裁决,不新写一行)", second.ID, first.ID) } if second.ClientMsgID != "cm-1" { t.Fatalf("第二次返回的消息 = %+v,want 回填的 clientMsgId", second) } msgs, err := st.List(ctx, MainSessionID, 0, 50, "") if err != nil { t.Fatalf("list: %v", err) } if len(msgs) != 1 { t.Fatalf("行数 = %d, want 1(同一个 clientMsgId 只落一条)", len(msgs)) } if msgs[0].ClientMsgID != "cm-1" { t.Fatalf("读回的消息 = %+v,want clientMsgId=cm-1(读接口可见)", msgs[0]) } // 省略幂等键(空串)不参与约束:同一会话的两条空键消息都必须写得进去。 for i := 0; i < 2; i++ { if _, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "无键"}); err != nil { t.Fatalf("第 %d 条空键写入:%v", i+1, err) } } msgs, err = st.List(ctx, MainSessionID, 0, 50, "") if err != nil { t.Fatalf("list: %v", err) } if len(msgs) != 3 { t.Fatalf("行数 = %d, want 3(空 clientMsgId 绝不受唯一索引约束)", len(msgs)) } } func TestAppendDedupesClientMsgIDPerSession(t *testing.T) { t.Parallel() ctx := context.Background() st := newStore(t) // 幂等键只在**会话内**唯一:两个会话用同一个键互不影响 // (client_msg_id 由客户端生成,不同会话之间没有可比性)。 main, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "主会话", ClientMsgID: "cm-x"}) if err != nil { t.Fatalf("主会话写入:%v", err) } sub := MainSessionID + ":sub-1" other, err := st.AppendFull(ctx, Message{SessionID: sub, Role: RoleUser, Content: "子会话", ClientMsgID: "cm-x"}) if err != nil { t.Fatalf("子会话写入:%v", err) } if other.ID == main.ID { t.Fatalf("子会话返回了主会话的行 %q,want 各自一条", main.ID) } } func TestFindByClientMsgID(t *testing.T) { t.Parallel() ctx := context.Background() st := newStore(t) full, err := st.AppendFull(ctx, Message{SessionID: MainSessionID, Role: RoleUser, Content: "定位我", ClientMsgID: "cm-find"}) if err != nil { t.Fatalf("写入:%v", err) } got, err := st.FindByClientMsgID(ctx, MainSessionID, "cm-find") if err != nil { t.Fatalf("按幂等键读取:%v", err) } if got.ID != full.ID || got.ClientMsgID != "cm-find" { t.Fatalf("读回 = %+v,want id=%q clientMsgId=cm-find", got, full.ID) } // 没见过的键与空键都必须是明确的 ErrMessageNotFound(编排器据此判定「不是 // 重复」,而不是把查询错误当成重复)。 for _, key := range []string{"cm-missing", ""} { if _, err := st.FindByClientMsgID(ctx, MainSessionID, key); !errors.Is(err, ErrMessageNotFound) { t.Fatalf("键 %q 的错误 = %v,want wrap of ErrMessageNotFound", key, err) } } // 会话也必须匹配:同一个键在别的会话里不存在。 if _, err := st.FindByClientMsgID(ctx, MainSessionID+":other", "cm-find"); !errors.Is(err, ErrMessageNotFound) { t.Fatalf("跨会话查询的错误 = %v,want wrap of ErrMessageNotFound", err) } }