Why: Allow consumers to download an immutable module revision without nested source checkouts or local path replacements. Evidence: SELF package tests and the prior canonical-path rejection are recorded in the Dev dependency-workspace task card; remote consumption is replayed against the committed candidate. Impact: Change the module identity and its Go imports while preserving API behavior. Existing historical submodule revisions remain buildable. Refs: Versioned dependencies U1/U3; feature/dependency-workspace
98 lines
2.6 KiB
Go
98 lines
2.6 KiB
Go
package store
|
||
|
||
import (
|
||
"errors"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
core "git-ssh.u1680832.nyat.app/weichen/Agent-System-Core.git"
|
||
)
|
||
|
||
// 协作存储 U3(KTD7):卡片乐观并发。同版本竞争写入最多一个成功,
|
||
// 其余明确 ErrConflict(HTTP 层映射 409);盲写(系统镜像)递增版本。
|
||
|
||
func newVersionedCard(t *testing.T) (core.ActivityCard, string) {
|
||
t.Helper()
|
||
return core.ActivityCard{
|
||
ID: NewID("card"), Title: "版本卡", Status: core.CardActive,
|
||
CreatedAt: time.Now().UTC(), UpdatedAt: time.Now().UTC(),
|
||
}, "o"
|
||
}
|
||
|
||
func TestCardVersionRoundTrip(t *testing.T) {
|
||
st := openTestStore(t)
|
||
c, owner := newVersionedCard(t)
|
||
if err := st.SaveCard(owner, &c, ""); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
v, err := st.CardVersion(owner, c.ID)
|
||
if err != nil || v != 1 {
|
||
t.Fatalf("建卡版本应为 1:v=%d err=%v", v, err)
|
||
}
|
||
// 乐观更新:expected 一致 → 成功且递增。
|
||
if err := st.SaveCardVersioned(owner, &c, "", 1); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if v, _ = st.CardVersion(owner, c.ID); v != 2 {
|
||
t.Fatalf("版本应递增到 2:%d", v)
|
||
}
|
||
// 旧版本重放:明确冲突。
|
||
err = st.SaveCardVersioned(owner, &c, "", 1)
|
||
if !errors.Is(err, ErrConflict) {
|
||
t.Fatalf("旧版本应 ErrConflict:%v", err)
|
||
}
|
||
// 盲写同样递增(系统镜像路径)。
|
||
if err := st.SaveCard(owner, &c, ""); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if v, _ = st.CardVersion(owner, c.ID); v != 3 {
|
||
t.Fatalf("盲写应递增到 3:%d", v)
|
||
}
|
||
}
|
||
|
||
// 并发同版本竞争:N 个写者同 expected 提交,恰一个成功,其余 ErrConflict;
|
||
// 不存在静默覆盖(R3 验收)。
|
||
func TestCardVersionConcurrentSingleWinner(t *testing.T) {
|
||
st := openTestStore(t)
|
||
c, owner := newVersionedCard(t)
|
||
if err := st.SaveCard(owner, &c, ""); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
const writers = 8
|
||
var mu sync.Mutex
|
||
ok, conflict := 0, 0
|
||
var wg sync.WaitGroup
|
||
for i := 0; i < writers; i++ {
|
||
wg.Add(1)
|
||
go func(i int) {
|
||
defer wg.Done()
|
||
dup := c
|
||
dup.Title = "并发写"
|
||
err := st.SaveCardVersioned(owner, &dup, "", 1)
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
switch {
|
||
case err == nil:
|
||
ok++
|
||
case errors.Is(err, ErrConflict):
|
||
conflict++
|
||
default:
|
||
t.Errorf("意外错误:%v", err)
|
||
}
|
||
}(i)
|
||
}
|
||
wg.Wait()
|
||
if ok != 1 || conflict != writers-1 {
|
||
t.Fatalf("应恰一个成功:ok=%d conflict=%d", ok, conflict)
|
||
}
|
||
if v, _ := st.CardVersion(owner, c.ID); v != 2 {
|
||
t.Fatalf("竞争后版本应为 2:%d", v)
|
||
}
|
||
// cardsvc 读侧:GetCardVersion 返回卡与版本。
|
||
_, ver, err := st.GetCardVersion(owner, c.ID)
|
||
if err != nil || ver != 2 {
|
||
t.Fatalf("GetCardVersion:ver=%d err=%v", ver, err)
|
||
}
|
||
}
|