Files
tutti/packages/agent/daemon/activity/sync_state_persistence_test.go
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

104 lines
2.8 KiB
Go

package agentsessionstore
import (
"context"
"reflect"
"sync"
"testing"
"time"
)
// Regression test for a pre-existing race: sync-state saves used to run after
// entry.mu was released, so HideAgentSession could delete the persisted state
// and then a stale save would resurrect it. Saves now hold entry.mu, which
// serializes them with the hide path's delete.
func TestSyncStatePersistenceSerializesWithHide(t *testing.T) {
store := newBlockingSyncStateStore()
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{}
svc := New(client, WithSyncStateStore(store))
svc.TrackRoom("room-1")
markDone := make(chan struct{})
go func() {
defer close(markDone)
svc.MarkActivitySyncPending("room-1", "session-1", 0, 1, 0)
}()
<-store.saveEntered
hideDone := make(chan struct{})
go func() {
defer close(hideDone)
svc.HideAgentSession("room-1", "session-1")
}()
select {
case <-hideDone:
t.Fatal("HideAgentSession completed while a sync-state save was still in flight")
case <-time.After(50 * time.Millisecond):
}
close(store.saveGate)
<-markDone
<-hideDone
if got := store.operations(); !reflect.DeepEqual(got, []string{"save", "delete"}) {
t.Fatalf("store operations = %#v, want [save delete]", got)
}
if _, resurrected := store.session("session-1"); resurrected {
t.Fatal("hidden session sync state was resurrected by a stale save")
}
}
type blockingSyncStateStore struct {
mu sync.Mutex
ops []string
sessions map[string]WorkspaceAgentSyncState
saveGate chan struct{}
saveEntered chan struct{}
enteredOnce sync.Once
}
func newBlockingSyncStateStore() *blockingSyncStateStore {
return &blockingSyncStateStore{
sessions: make(map[string]WorkspaceAgentSyncState),
saveGate: make(chan struct{}),
saveEntered: make(chan struct{}),
}
}
func (*blockingSyncStateStore) LoadRoomSyncStates(_ context.Context, _ string) (map[string]WorkspaceAgentSyncState, error) {
return nil, nil
}
func (s *blockingSyncStateStore) SaveAgentSyncState(_ context.Context, _ string, state WorkspaceAgentSyncState) error {
s.enteredOnce.Do(func() { close(s.saveEntered) })
<-s.saveGate
s.mu.Lock()
defer s.mu.Unlock()
s.ops = append(s.ops, "save")
s.sessions[state.AgentSessionID] = state
return nil
}
func (s *blockingSyncStateStore) DeleteAgentSyncState(_ context.Context, _ string, agentSessionID string) error {
s.mu.Lock()
defer s.mu.Unlock()
s.ops = append(s.ops, "delete")
delete(s.sessions, agentSessionID)
return nil
}
func (s *blockingSyncStateStore) operations() []string {
s.mu.Lock()
defer s.mu.Unlock()
return append([]string(nil), s.ops...)
}
func (s *blockingSyncStateStore) session(agentSessionID string) (WorkspaceAgentSyncState, bool) {
s.mu.Lock()
defer s.mu.Unlock()
state, ok := s.sessions[agentSessionID]
return state, ok
}