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

191 lines
5.7 KiB
Go

package daemon
import (
"context"
"sync"
"testing"
"time"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
func TestOperationSchedulerDeduplicatesActiveOperation(t *testing.T) {
executor := &blockingExecutor{started: make(chan struct{}), release: make(chan struct{})}
scheduler := NewOperationScheduler(context.Background())
if err := scheduler.Bind(executor); err != nil {
t.Fatal(err)
}
if err := scheduler.Schedule(context.Background(), "operation-1"); err != nil {
t.Fatal(err)
}
<-executor.started
if err := scheduler.Schedule(context.Background(), "operation-1"); err != nil {
t.Fatal(err)
}
close(executor.release)
scheduler.Wait()
if executor.calls != 1 {
t.Fatalf("executor calls = %d, want 1", executor.calls)
}
}
type blockingExecutor struct {
mu sync.Mutex
calls int
started chan struct{}
release chan struct{}
}
func (executor *blockingExecutor) ExecuteOperation(context.Context, string) error {
executor.mu.Lock()
executor.calls++
executor.mu.Unlock()
close(executor.started)
<-executor.release
return nil
}
func TestOutboxDispatcherMarksOnlyPublishedEvents(t *testing.T) {
outbox := &memoryOutbox{entries: []market.ChangedEventRecord{{Sequence: 1, Event: market.ChangedEvent{Revision: 2}}}}
publisher := &memoryPublisher{}
dispatcher := OutboxDispatcher{Outbox: outbox, Publisher: publisher, Now: func() time.Time { return time.Unix(10, 0) }}
if err := dispatcher.Flush(context.Background()); err != nil {
t.Fatal(err)
}
if len(publisher.events) != 1 || publisher.events[0].Cursor != 1 || len(outbox.marked) != 1 || outbox.marked[0] != 1 {
t.Fatalf("published=%#v marked=%#v", publisher.events, outbox.marked)
}
}
func TestLifecycleCleanupWorkerUsesFiniteRetentionCutoffs(t *testing.T) {
now := time.Date(2026, 8, 6, 12, 0, 0, 0, time.UTC)
store := &memoryLifecycleCleanupStore{}
worker := LifecycleCleanupWorker{
Store: store,
Now: func() time.Time { return now },
Policy: LifecycleCleanupPolicy{
TerminalOperationRetention: 24 * time.Hour,
PublishedEventRetention: time.Hour,
BatchSize: 37,
},
}
if err := worker.Cleanup(context.Background()); err != nil {
t.Fatal(err)
}
requests := store.snapshot()
if len(requests) != 1 || !requests[0].TerminalOperationsUpdatedThrough.Equal(now.Add(-24*time.Hour)) ||
!requests[0].PublishedEventsPublishedThrough.Equal(now.Add(-time.Hour)) || requests[0].BatchSize != 37 {
t.Fatalf("cleanup requests = %#v", requests)
}
}
func TestLifecycleCleanupWorkerRunsAtStartupAndPeriodically(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
store := &memoryLifecycleCleanupStore{called: make(chan struct{}, 3)}
worker := LifecycleCleanupWorker{
Store: store,
Policy: LifecycleCleanupPolicy{Interval: 5 * time.Millisecond},
}
done := make(chan struct{})
go func() {
defer close(done)
worker.Run(ctx)
}()
for range 2 {
select {
case <-store.called:
case <-time.After(time.Second):
t.Fatal("lifecycle cleanup worker did not run")
}
}
cancel()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("lifecycle cleanup worker did not stop")
}
if len(store.snapshot()) < 2 {
t.Fatalf("cleanup calls = %d, want startup and periodic calls", len(store.snapshot()))
}
}
func TestLifecycleCleanupWorkerDrainsBacklogInBoundedTransactions(t *testing.T) {
store := &memoryLifecycleCleanupStore{results: []market.LifecycleCleanupResult{
{TerminalOperationsDeleted: 5, PublishedEventsDeleted: 5},
{TerminalOperationsDeleted: 5},
{TerminalOperationsDeleted: 1},
}}
worker := LifecycleCleanupWorker{Store: store, Policy: LifecycleCleanupPolicy{BatchSize: 5}}
if err := worker.Cleanup(context.Background()); err != nil {
t.Fatal(err)
}
requests := store.snapshot()
if len(requests) != 3 {
t.Fatalf("cleanup calls = %d, want 3 bounded transactions", len(requests))
}
for _, request := range requests {
if request.BatchSize != 5 {
t.Fatalf("cleanup batch size = %d, want 5", request.BatchSize)
}
}
}
type memoryLifecycleCleanupStore struct {
mu sync.Mutex
requests []market.LifecycleCleanupRequest
results []market.LifecycleCleanupResult
called chan struct{}
}
func (store *memoryLifecycleCleanupStore) CleanupLifecycle(_ context.Context, request market.LifecycleCleanupRequest) (market.LifecycleCleanupResult, error) {
store.mu.Lock()
store.requests = append(store.requests, request)
var result market.LifecycleCleanupResult
if len(store.results) > 0 {
result = store.results[0]
store.results = store.results[1:]
}
store.mu.Unlock()
if store.called != nil {
store.called <- struct{}{}
}
return result, nil
}
func (store *memoryLifecycleCleanupStore) snapshot() []market.LifecycleCleanupRequest {
store.mu.Lock()
defer store.mu.Unlock()
return append([]market.LifecycleCleanupRequest(nil), store.requests...)
}
type memoryOutbox struct {
entries []market.ChangedEventRecord
marked []int64
}
func (outbox *memoryOutbox) PendingChangedEvents(context.Context, int) ([]market.ChangedEventRecord, error) {
pending := make([]market.ChangedEventRecord, 0, len(outbox.entries))
for _, entry := range outbox.entries {
alreadyMarked := false
for _, sequence := range outbox.marked {
alreadyMarked = alreadyMarked || sequence == entry.Sequence
}
if !alreadyMarked {
pending = append(pending, entry)
}
}
return pending, nil
}
func (outbox *memoryOutbox) MarkChangedEventPublished(_ context.Context, sequence int64, _ time.Time) error {
outbox.marked = append(outbox.marked, sequence)
return nil
}
type memoryPublisher struct{ events []market.ChangedEvent }
func (publisher *memoryPublisher) PublishConnectorMarketChanged(_ context.Context, event market.ChangedEvent) error {
publisher.events = append(publisher.events, event)
return nil
}