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
191 lines
5.7 KiB
Go
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
|
|
}
|