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
257 lines
9.2 KiB
Go
257 lines
9.2 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
agentsessionstore "github.com/tutti-os/tutti/packages/agent/daemon/activity"
|
|
agentactivitybiz "github.com/tutti-os/tutti/packages/agent/store-sqlite"
|
|
workspacebiz "github.com/tutti-os/tutti/services/tuttid/biz/workspace"
|
|
)
|
|
|
|
func enqueueAgentSessionResourceCleanupForTest(
|
|
t *testing.T,
|
|
store *SQLiteStore,
|
|
ctx context.Context,
|
|
items []AgentSessionResourceCleanup,
|
|
) {
|
|
t.Helper()
|
|
tx, err := store.writeDB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatalf("begin cleanup queue seed: %v", err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
if err := enqueueAgentSessionResourceCleanupTx(ctx, tx, items); err != nil {
|
|
t.Fatalf("seed cleanup queue: %v", err)
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
t.Fatalf("commit cleanup queue seed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAgentDataMaintenanceStatePersistsAutomaticCompletion(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
|
|
initial, err := store.GetAgentDataMaintenanceState(ctx)
|
|
if err != nil || initial.LastAutomaticPurgeAtUnixMS != 0 {
|
|
t.Fatalf("initial state = %#v, error = %v", initial, err)
|
|
}
|
|
if err := store.MarkAutomaticAgentDataPurgeCompleted(ctx, 1234); err != nil {
|
|
t.Fatalf("MarkAutomaticAgentDataPurgeCompleted() error = %v", err)
|
|
}
|
|
stored, err := store.GetAgentDataMaintenanceState(ctx)
|
|
if err != nil || stored.LastAutomaticPurgeAtUnixMS != 1234 {
|
|
t.Fatalf("stored state = %#v, error = %v", stored, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentSessionResourceCleanupQueueRetriesUntilCompleted(t *testing.T) {
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
item := AgentSessionResourceCleanup{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-1",
|
|
}
|
|
enqueueAgentSessionResourceCleanupForTest(t, store, ctx, []AgentSessionResourceCleanup{item})
|
|
if err := store.FailAgentSessionResourceCleanup(ctx, item.WorkspaceID, item.AgentSessionID, "temporary failure"); err != nil {
|
|
t.Fatalf("FailAgentSessionResourceCleanup() error = %v", err)
|
|
}
|
|
queued, err := store.ListAgentSessionResourceCleanup(ctx, 10)
|
|
if err != nil || len(queued) != 1 || queued[0].AttemptCount != 1 || queued[0].LastError != "temporary failure" {
|
|
t.Fatalf("queued = %#v, error = %v", queued, err)
|
|
}
|
|
if err := store.CompleteAgentSessionResourceCleanup(ctx, item.WorkspaceID, item.AgentSessionID); err != nil {
|
|
t.Fatalf("CompleteAgentSessionResourceCleanup() error = %v", err)
|
|
}
|
|
queued, err = store.ListAgentSessionResourceCleanup(ctx, 10)
|
|
if err != nil || len(queued) != 0 {
|
|
t.Fatalf("completed queue = %#v, error = %v", queued, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentSessionResourceCleanupQueueFencesSessionIDReuse(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
const sourceWorkspaceID = "workspace-cleanup-fence-source"
|
|
const destinationWorkspaceID = "workspace-cleanup-fence-destination"
|
|
if err := store.Create(ctx, workspacebiz.Summary{ID: sourceWorkspaceID, Name: "Cleanup Fence Source"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.Create(ctx, workspacebiz.Summary{ID: destinationWorkspaceID, Name: "Cleanup Fence Destination"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
item := AgentSessionResourceCleanup{WorkspaceID: sourceWorkspaceID, AgentSessionID: "session-1"}
|
|
enqueueAgentSessionResourceCleanupForTest(t, store, ctx, []AgentSessionResourceCleanup{item})
|
|
report := agentactivitybiz.SessionStateReport{
|
|
WorkspaceID: destinationWorkspaceID, AgentSessionID: item.AgentSessionID,
|
|
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
|
|
Provider: "codex", Status: "completed", OccurredAtUnixMS: time.Now().UnixMilli(),
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, report); err == nil || !strings.Contains(err.Error(), "pending cleanup") {
|
|
t.Fatalf("ReportSessionState() error=%v, want pending-cleanup reuse fence", err)
|
|
}
|
|
if err := store.CompleteAgentSessionResourceCleanup(ctx, sourceWorkspaceID, item.AgentSessionID); err != nil {
|
|
t.Fatalf("CompleteAgentSessionResourceCleanup() error = %v", err)
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, report); err != nil {
|
|
t.Fatalf("ReportSessionState() after cleanup error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAgentSessionIdentityChecksSpanWorkspacesAndTombstones(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
for _, workspace := range []workspacebiz.Summary{
|
|
{ID: "workspace-identity-source", Name: "Identity Source"},
|
|
{ID: "workspace-identity-owner", Name: "Identity Owner"},
|
|
} {
|
|
if err := store.Create(ctx, workspace); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
|
|
WorkspaceID: "workspace-identity-owner", AgentSessionID: "shared-session",
|
|
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
|
|
Provider: "codex", Status: "completed", OccurredAtUnixMS: time.Now().UnixMilli(),
|
|
}); err != nil {
|
|
t.Fatalf("ReportSessionState() error = %v", err)
|
|
}
|
|
|
|
exists, err := store.AgentSessionIDExists(ctx, "shared-session")
|
|
if err != nil || !exists {
|
|
t.Fatalf("AgentSessionIDExists()=%v error=%v, want true", exists, err)
|
|
}
|
|
otherLive, err := store.OtherWorkspaceLiveAgentSessionIDExists(
|
|
ctx,
|
|
"workspace-identity-source",
|
|
"shared-session",
|
|
)
|
|
if err != nil || !otherLive {
|
|
t.Fatalf("OtherWorkspaceLiveAgentSessionIDExists()=%v error=%v, want true", otherLive, err)
|
|
}
|
|
if removed, err := store.DeleteSession(ctx, "workspace-identity-owner", "shared-session"); err != nil || !removed {
|
|
t.Fatalf("DeleteSession() removed=%v error=%v", removed, err)
|
|
}
|
|
|
|
exists, err = store.AgentSessionIDExists(ctx, "shared-session")
|
|
if err != nil || !exists {
|
|
t.Fatalf("AgentSessionIDExists() after tombstone=%v error=%v, want true", exists, err)
|
|
}
|
|
otherLive, err = store.OtherWorkspaceLiveAgentSessionIDExists(
|
|
ctx,
|
|
"workspace-identity-source",
|
|
"shared-session",
|
|
)
|
|
if err != nil || otherLive {
|
|
t.Fatalf("OtherWorkspaceLiveAgentSessionIDExists() after tombstone=%v error=%v, want false", otherLive, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentSessionResourceCleanupGlobalReuseFenceUpgradesV3(t *testing.T) {
|
|
ctx := context.Background()
|
|
dbPath := filepath.Join(t.TempDir(), "tuttid.db")
|
|
store, err := OpenSQLiteStore(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.writeDB.ExecContext(ctx, `
|
|
DELETE FROM tuttid_schema_migrations
|
|
WHERE id = ?;
|
|
DROP TRIGGER IF EXISTS trg_workspace_agent_sessions_block_pending_resource_cleanup;
|
|
CREATE TRIGGER trg_workspace_agent_sessions_block_pending_resource_cleanup
|
|
BEFORE INSERT ON workspace_agent_sessions
|
|
WHEN EXISTS (
|
|
SELECT 1
|
|
FROM agent_session_resource_cleanup_queue cleanup
|
|
WHERE cleanup.workspace_id = NEW.workspace_id
|
|
AND cleanup.agent_session_id = NEW.agent_session_id
|
|
)
|
|
BEGIN
|
|
SELECT RAISE(ABORT, 'agent session resources are pending cleanup');
|
|
END;
|
|
`, schemaMigrationAgentDataMaintenanceV4); err != nil {
|
|
t.Fatalf("restore V3 cleanup fence: %v", err)
|
|
}
|
|
if err := store.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
store, err = OpenSQLiteStore(dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
if err := store.Migrate(ctx); err != nil {
|
|
t.Fatalf("Migrate() V4 error = %v", err)
|
|
}
|
|
for _, workspace := range []workspacebiz.Summary{
|
|
{ID: "workspace-v3-source", Name: "V3 Source"},
|
|
{ID: "workspace-v4-destination", Name: "V4 Destination"},
|
|
} {
|
|
if err := store.Create(ctx, workspace); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
enqueueAgentSessionResourceCleanupForTest(t, store, ctx, []AgentSessionResourceCleanup{{
|
|
WorkspaceID: "workspace-v3-source", AgentSessionID: "shared-session",
|
|
}})
|
|
_, err = store.ReportSessionState(ctx, agentactivitybiz.SessionStateReport{
|
|
WorkspaceID: "workspace-v4-destination", AgentSessionID: "shared-session",
|
|
Origin: agentsessionstore.WorkspaceAgentSessionOriginRuntime,
|
|
Provider: "codex", Status: "completed", OccurredAtUnixMS: time.Now().UnixMilli(),
|
|
})
|
|
if err == nil || !strings.Contains(err.Error(), "pending cleanup") {
|
|
t.Fatalf("ReportSessionState() error=%v, want V4 global reuse fence", err)
|
|
}
|
|
}
|
|
|
|
func TestCompactDeletedDataIfSafeRequiresSubstantialFreeSpace(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestSQLiteStore(t)
|
|
compacted, err := store.CompactDeletedDataIfSafe(context.Background())
|
|
if err != nil || compacted {
|
|
t.Fatalf("CompactDeletedDataIfSafe()=%v error=%v, want safe skip", compacted, err)
|
|
}
|
|
}
|
|
|
|
func TestCompactDeletedDataIfSafeReclaimsSmallDisposableDatabase(t *testing.T) {
|
|
store := openTestSQLiteStore(t)
|
|
ctx := context.Background()
|
|
if _, err := store.writeDB.ExecContext(ctx, `
|
|
CREATE TABLE compaction_probe (payload BLOB NOT NULL);
|
|
INSERT INTO compaction_probe(payload) VALUES (zeroblob(12582912));
|
|
DROP TABLE compaction_probe;
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.writeDB.ExecContext(ctx, `PRAGMA wal_checkpoint(TRUNCATE)`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, err := os.Stat(store.dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
compacted, err := store.CompactDeletedDataIfSafe(ctx)
|
|
if err != nil || !compacted {
|
|
t.Fatalf("CompactDeletedDataIfSafe()=%v error=%v", compacted, err)
|
|
}
|
|
after, err := os.Stat(store.dbPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if after.Size() >= before.Size() {
|
|
t.Fatalf("database size after compaction=%d, before=%d", after.Size(), before.Size())
|
|
}
|
|
}
|