Files
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

394 lines
11 KiB
Go

package agentsessionstore
import (
"context"
"reflect"
"sort"
"sync"
"testing"
"time"
)
func TestAgentActivitySyncerSyncsSessionMessages(t *testing.T) {
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{
Sessions: []ProviderActivitySessionProjection{{
AgentSessionID: "agent-session-1",
SessionOrigin: WorkspaceAgentSessionOriginRuntime,
LifecycleStatus: "active",
EffectiveStatus: "working",
}},
}
client.messages["room-1/agent-session-1"] = []ListSessionMessagesReply{
{
Messages: []WorkspaceAgentSessionMessage{{
ID: 10,
AgentSessionID: "agent-session-1",
MessageID: "message-1",
Role: "user",
Kind: "message",
Payload: map[string]any{"content": "hello"},
Version: 1,
}},
LatestVersion: 1,
HasMore: true,
},
{
Messages: []WorkspaceAgentSessionMessage{{
ID: 11,
AgentSessionID: "agent-session-1",
MessageID: "message-2",
Role: "assistant",
Kind: "message",
Payload: map[string]any{"content": "world"},
Version: 2,
}},
LatestVersion: 2,
},
}
svc := New(client)
svc.TrackRoom("room-1")
syncer := newSessionSyncer(svc, client)
syncer.syncAllRooms(context.Background())
snapshot, ok := svc.GetAgentSnapshot("room-1")
if !ok {
t.Fatal("missing room snapshot")
}
messages := snapshot.SessionMessagesByID["agent-session-1"]
if len(messages) != 2 {
t.Fatalf("messages = %#v, want 2", messages)
}
if messages[0].MessageID != "message-1" || messages[1].MessageID != "message-2" {
t.Fatalf("message order = %#v, want message-1,message-2", messages)
}
if got := client.afterVersionsFor("agent-session-1"); !reflect.DeepEqual(got, []uint64{0, 1}) {
t.Fatalf("AfterVersions = %#v, want [0 1]", got)
}
if got := client.messageOriginsFor("agent-session-1"); !reflect.DeepEqual(got, []string{WorkspaceAgentSessionOriginRuntime, WorkspaceAgentSessionOriginRuntime}) {
t.Fatalf("SessionOrigins = %#v, want runtime paging", got)
}
}
func TestAgentActivitySyncerRemoteMessageEchoOnlyAdvancesCursor(t *testing.T) {
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{
Sessions: []ProviderActivitySessionProjection{{
AgentSessionID: "agent-session-1",
SessionOrigin: WorkspaceAgentSessionOriginRuntime,
LifecycleStatus: "active",
EffectiveStatus: "working",
}},
}
client.messages["room-1/agent-session-1"] = []ListSessionMessagesReply{{
Messages: []WorkspaceAgentSessionMessage{{
ID: 10,
AgentSessionID: "agent-session-1",
MessageID: "message-1",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "hello"},
OccurredAtUnixMS: 1000,
Version: 7,
}},
LatestVersion: 7,
}}
svc := New(client)
svc.TrackRoom("room-1")
svc.updateStateForOrigin("room-1", *client.snapshots["room-1"], WorkspaceAgentSessionOriginRuntime)
var notifyCount int
svc.SetUpdateListener(func(string, WorkspaceAgentSnapshot) {
notifyCount++
})
svc.ApplySessionMessages("room-1", EventSource{AgentID: "agent-session-1"}, "agent-session-1", []WorkspaceAgentSessionMessageUpdate{{
MessageID: "message-1",
Role: "assistant",
Kind: "text",
Status: "completed",
Payload: map[string]any{"text": "hello"},
OccurredAtUnixMS: 1000,
}})
if notifyCount != 1 {
t.Fatalf("notify count after local message = %d, want 1", notifyCount)
}
syncer := newSessionSyncer(svc, client)
syncer.syncRoom(context.Background(), "room-1")
if notifyCount != 1 {
t.Fatalf("notify count after remote echo sync = %d, want unchanged", notifyCount)
}
reply, ok := svc.ListSessionMessages("room-1", "agent-session-1", 0, 10)
if !ok || reply.LatestVersion != 1 || len(reply.Messages) != 1 || reply.Messages[0].Version != 1 {
t.Fatalf("messages reply = %#v, ok=%v, want local cursor metadata preserved", reply, ok)
}
}
func TestAgentActivitySyncerUsesRuntimeSessionOriginForMessageSync(t *testing.T) {
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{
Sessions: []ProviderActivitySessionProjection{
{
AgentSessionID: "runtime-1",
SessionOrigin: WorkspaceAgentSessionOriginRuntime,
LifecycleStatus: "active",
EffectiveStatus: "working",
},
{
AgentSessionID: "runtime-2",
SessionOrigin: WorkspaceAgentSessionOriginRuntime,
LifecycleStatus: "active",
EffectiveStatus: "working",
},
},
}
client.messages["room-1/runtime-1"] = []ListSessionMessagesReply{{}}
client.messages["room-1/runtime-2"] = []ListSessionMessagesReply{{}}
svc := New(client)
svc.TrackRoom("room-1")
syncer := newSessionSyncer(svc, client)
syncer.syncAllRooms(context.Background())
if got := client.messageOriginsFor("runtime-1"); !reflect.DeepEqual(got, []string{WorkspaceAgentSessionOriginRuntime}) {
t.Fatalf("runtime origins = %#v, want runtime", got)
}
if got := client.messageOriginsFor("runtime-2"); !reflect.DeepEqual(got, []string{WorkspaceAgentSessionOriginRuntime}) {
t.Fatalf("runtime origins = %#v, want runtime", got)
}
}
func TestAgentActivitySyncerReplacesRuntimeSessionsWhenSyncingDefaultSnapshot(t *testing.T) {
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{
Sessions: []ProviderActivitySessionProjection{{
AgentSessionID: "remote-runtime-1",
SessionOrigin: WorkspaceAgentSessionOriginRuntime,
LifecycleStatus: "active",
EffectiveStatus: "working",
}},
}
svc := New(client)
svc.TrackRoom("room-1")
svc.updateState("room-1", WorkspaceAgentSnapshot{
Sessions: []ProviderActivitySessionProjection{{
AgentSessionID: "runtime-1",
SessionOrigin: WorkspaceAgentSessionOriginRuntime,
UserID: "user-1",
LifecycleStatus: "active",
EffectiveStatus: "working",
UpdatedAtUnixMS: 100,
}},
})
syncer := newSessionSyncer(svc, client)
syncer.syncRoom(context.Background(), "room-1")
state, ok := svc.GetAgentState("room-1")
if !ok {
t.Fatal("missing room state")
}
got := sessionIDs(state.Sessions)
want := []string{"remote-runtime-1"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("sessions = %#v, want %#v", got, want)
}
}
func TestStoreStartIsIdempotent(t *testing.T) {
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{}
svc := New(client)
svc.TrackRoom("room-1")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
svc.Start(ctx)
svc.Start(ctx)
if !client.waitForListCalls("room-1", 1, time.Second) {
t.Fatal("timed out waiting for initial sync")
}
select {
case <-client.listCallSeen("room-1"):
t.Fatal("second Start launched a duplicate immediate sync")
case <-time.After(100 * time.Millisecond):
}
}
func TestAgentActivitySyncerTriggeredRoomsReturnWhenContextCanceled(t *testing.T) {
client := newFakeSyncerRepository()
client.snapshots["room-1"] = &WorkspaceAgentSnapshot{}
svc := New(client)
svc.TrackRoom("room-1")
syncer := newSessionSyncer(svc, client)
for i := 0; i < cap(syncer.triggers); i++ {
syncer.triggerRoom("room-1")
}
ctx, cancel := context.WithCancel(context.Background())
cancel()
done := make(chan struct{})
go func() {
defer close(done)
syncer.syncTriggeredRooms(ctx, "room-1")
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("syncTriggeredRooms did not return after cancellation")
}
if got := client.listCallCount("room-1"); got != 0 {
t.Fatalf("list calls after canceled trigger drain = %d, want 0", got)
}
}
func TestAgentActivitySyncerTriggerRoomDoesNotBlockWhenFull(t *testing.T) {
syncer := newSessionSyncer(New(nil), nil)
done := make(chan struct{})
go func() {
defer close(done)
for i := 0; i < 128; i++ {
syncer.triggerRoom("room-1")
}
}()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("triggerRoom blocked with a full trigger buffer")
}
}
type fakeSyncerRepository struct {
mu sync.Mutex
snapshots map[string]*WorkspaceAgentSnapshot
snapshotSequences map[string][]*WorkspaceAgentSnapshot
messages map[string][]ListSessionMessagesReply
messageInputs []ListSessionMessagesInput
listCalls map[string]int
listWaiters map[string][]chan struct{}
}
func newFakeSyncerRepository() *fakeSyncerRepository {
return &fakeSyncerRepository{
snapshots: make(map[string]*WorkspaceAgentSnapshot),
snapshotSequences: make(map[string][]*WorkspaceAgentSnapshot),
messages: make(map[string][]ListSessionMessagesReply),
listCalls: make(map[string]int),
listWaiters: make(map[string][]chan struct{}),
}
}
func (f *fakeSyncerRepository) ListAgents(_ context.Context, roomID string) (*WorkspaceAgentSnapshot, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.listCalls[roomID]++
for _, waiter := range f.listWaiters[roomID] {
close(waiter)
}
delete(f.listWaiters, roomID)
if snapshots := f.snapshotSequences[roomID]; len(snapshots) > 0 {
snapshot := snapshots[0]
if len(snapshots) == 1 {
f.snapshots[roomID] = snapshot
}
f.snapshotSequences[roomID] = snapshots[1:]
return snapshot, nil
}
return f.snapshots[roomID], nil
}
func (f *fakeSyncerRepository) ListSessionMessages(_ context.Context, input ListSessionMessagesInput) (*ListSessionMessagesReply, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.messageInputs = append(f.messageInputs, input)
key := input.WorkspaceID + "/" + input.AgentSessionID
pages := f.messages[key]
if len(pages) == 0 {
return &ListSessionMessagesReply{}, nil
}
page := pages[0]
f.messages[key] = pages[1:]
return &page, nil
}
func (f *fakeSyncerRepository) afterVersionsFor(agentSessionID string) []uint64 {
f.mu.Lock()
defer f.mu.Unlock()
afterVersions := make([]uint64, 0, len(f.messageInputs))
for _, input := range f.messageInputs {
if input.AgentSessionID == agentSessionID {
afterVersions = append(afterVersions, input.AfterVersion)
}
}
sort.Slice(afterVersions, func(i, j int) bool { return afterVersions[i] < afterVersions[j] })
return afterVersions
}
func (f *fakeSyncerRepository) messageOriginsFor(agentSessionID string) []string {
f.mu.Lock()
defer f.mu.Unlock()
origins := make([]string, 0, len(f.messageInputs))
for _, input := range f.messageInputs {
if input.AgentSessionID == agentSessionID {
origins = append(origins, input.SessionOrigin)
}
}
return origins
}
func (f *fakeSyncerRepository) listCallCount(roomID string) int {
f.mu.Lock()
defer f.mu.Unlock()
return f.listCalls[roomID]
}
func (f *fakeSyncerRepository) listCallSeen(roomID string) <-chan struct{} {
f.mu.Lock()
defer f.mu.Unlock()
ch := make(chan struct{})
f.listWaiters[roomID] = append(f.listWaiters[roomID], ch)
return ch
}
func (f *fakeSyncerRepository) waitForListCalls(roomID string, count int, timeout time.Duration) bool {
deadline := time.After(timeout)
for {
if f.listCallCount(roomID) >= count {
return true
}
select {
case <-f.listCallSeen(roomID):
case <-deadline:
return f.listCallCount(roomID) >= count
}
}
}
func sessionIDs(sessions []ProviderActivitySessionProjection) []string {
ids := make([]string, 0, len(sessions))
for _, session := range sessions {
if id := session.AgentSessionID; id != "" {
ids = append(ids, id)
}
}
sort.Strings(ids)
return ids
}