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
181 lines
6.5 KiB
Go
181 lines
6.5 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"sync"
|
|
"testing"
|
|
|
|
modelpolicybiz "github.com/tutti-os/tutti/services/tuttid/biz/modelpolicy"
|
|
)
|
|
|
|
func seedModelPolicyWorkspace(t *testing.T, store *SQLiteStore, workspaceID string) {
|
|
t.Helper()
|
|
now := int64(1700000000000)
|
|
if _, err := store.writeDB.ExecContext(context.Background(), `
|
|
INSERT INTO workspaces (id, name, created_at_unix_ms, updated_at_unix_ms)
|
|
VALUES (?, 'Model Policy WS', ?, ?);
|
|
`, workspaceID, now, now); err != nil {
|
|
t.Fatalf("seed workspace: %v", err)
|
|
}
|
|
}
|
|
|
|
func getAcceptanceState(t *testing.T, store *SQLiteStore, workspaceID, sessionID string) modelpolicybiz.AcceptanceState {
|
|
t.Helper()
|
|
acceptance, err := store.GetAgentSessionAcceptance(context.Background(), workspaceID, sessionID)
|
|
if err != nil {
|
|
t.Fatalf("GetAgentSessionAcceptance() error = %v", err)
|
|
}
|
|
return acceptance.State
|
|
}
|
|
|
|
func putAcceptance(t *testing.T, store *SQLiteStore, workspaceID, sessionID string, state modelpolicybiz.AcceptanceState, runID string) {
|
|
t.Helper()
|
|
if err := store.PutAgentSessionAcceptance(context.Background(), modelpolicybiz.Acceptance{
|
|
WorkspaceID: workspaceID,
|
|
AgentSessionID: sessionID,
|
|
State: state,
|
|
ReviewRunID: runID,
|
|
}); err != nil {
|
|
t.Fatalf("PutAgentSessionAcceptance(%s) error = %v", state, err)
|
|
}
|
|
}
|
|
|
|
func TestAgentSessionAcceptanceLadderUpgradesThenSticks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
store := openTestSQLiteStore(t)
|
|
seedModelPolicyWorkspace(t, store, "ws")
|
|
|
|
// The ladder still advances normally before user acceptance.
|
|
putAcceptance(t, store, "ws", "session-1", modelpolicybiz.AcceptanceAgentClaimed, "")
|
|
if got := getAcceptanceState(t, store, "ws", "session-1"); got != modelpolicybiz.AcceptanceAgentClaimed {
|
|
t.Fatalf("state = %q, want agent_claimed", got)
|
|
}
|
|
putAcceptance(t, store, "ws", "session-1", modelpolicybiz.AcceptanceAutoChecked, "run-1")
|
|
if got := getAcceptanceState(t, store, "ws", "session-1"); got != modelpolicybiz.AcceptanceAutoChecked {
|
|
t.Fatalf("state = %q, want auto_checked", got)
|
|
}
|
|
|
|
// User accepts.
|
|
putAcceptance(t, store, "ws", "session-1", modelpolicybiz.AcceptanceUserAccepted, "")
|
|
|
|
// Later automation writes must not downgrade the terminal user_accepted rung.
|
|
putAcceptance(t, store, "ws", "session-1", modelpolicybiz.AcceptanceAgentClaimed, "")
|
|
putAcceptance(t, store, "ws", "session-1", modelpolicybiz.AcceptanceAutoChecked, "run-2")
|
|
if got := getAcceptanceState(t, store, "ws", "session-1"); got != modelpolicybiz.AcceptanceUserAccepted {
|
|
t.Fatalf("state = %q after later automation writes, want sticky user_accepted", got)
|
|
}
|
|
|
|
// The preserved review run id from before acceptance is untouched too.
|
|
acceptance, err := store.GetAgentSessionAcceptance(ctx, "ws", "session-1")
|
|
if err != nil {
|
|
t.Fatalf("GetAgentSessionAcceptance() error = %v", err)
|
|
}
|
|
if acceptance.ReviewRunID != "" {
|
|
t.Fatalf("review run id = %q, want the user_accepted write's empty value preserved", acceptance.ReviewRunID)
|
|
}
|
|
}
|
|
|
|
func TestAgentSessionAcceptanceUserAcceptedStickyUnderConcurrency(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := openTestSQLiteStore(t)
|
|
seedModelPolicyWorkspace(t, store, "ws")
|
|
putAcceptance(t, store, "ws", "session-1", modelpolicybiz.AcceptanceUserAccepted, "")
|
|
|
|
// Hammer the row with concurrent automation writes; the single writer
|
|
// connection serializes them but the WHERE guard is what preserves the
|
|
// terminal state, not ordering.
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 32; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
_ = store.PutAgentSessionAcceptance(context.Background(), modelpolicybiz.Acceptance{
|
|
WorkspaceID: "ws",
|
|
AgentSessionID: "session-1",
|
|
State: modelpolicybiz.AcceptanceAgentClaimed,
|
|
})
|
|
_ = store.PutAgentSessionAcceptance(context.Background(), modelpolicybiz.Acceptance{
|
|
WorkspaceID: "ws",
|
|
AgentSessionID: "session-1",
|
|
State: modelpolicybiz.AcceptanceAutoChecked,
|
|
ReviewRunID: "run-x",
|
|
})
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
|
|
if got := getAcceptanceState(t, store, "ws", "session-1"); got != modelpolicybiz.AcceptanceUserAccepted {
|
|
t.Fatalf("state = %q after concurrent automation writes, want sticky user_accepted", got)
|
|
}
|
|
}
|
|
|
|
func TestGetAgentSessionAcceptanceMissingReturnsNoRows(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
store := openTestSQLiteStore(t)
|
|
seedModelPolicyWorkspace(t, store, "ws")
|
|
_, err := store.GetAgentSessionAcceptance(context.Background(), "ws", "unknown-session")
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
t.Fatalf("GetAgentSessionAcceptance() error = %v, want sql.ErrNoRows", err)
|
|
}
|
|
}
|
|
|
|
func TestModelPolicyRoundTripAndPlanLookup(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctx := context.Background()
|
|
store := openTestSQLiteStore(t)
|
|
seedModelPolicyWorkspace(t, store, "ws")
|
|
|
|
policy := modelpolicybiz.Policy{
|
|
ID: "pol-1",
|
|
WorkspaceID: "ws",
|
|
Name: "Careful",
|
|
Execution: modelpolicybiz.PlanModelRef{ModelPlanID: "mp-1", Model: "exec"},
|
|
Review: modelpolicybiz.PlanModelRef{ModelPlanID: "mp-2", Model: "review"},
|
|
ReviewRule: modelpolicybiz.ReviewRule{
|
|
Enabled: true,
|
|
Trigger: modelpolicybiz.ReviewTriggerOnTaskComplete,
|
|
MaxRunsPerSession: 5,
|
|
MaxTotalTokensPerSession: 123456,
|
|
},
|
|
}
|
|
if err := store.PutModelPolicy(ctx, policy); err != nil {
|
|
t.Fatalf("PutModelPolicy() error = %v", err)
|
|
}
|
|
|
|
got, err := store.GetModelPolicy(ctx, "ws", "pol-1")
|
|
if err != nil {
|
|
t.Fatalf("GetModelPolicy() error = %v", err)
|
|
}
|
|
if got.Name != "Careful" || got.Execution.ModelPlanID != "mp-1" || got.Review.Model != "review" {
|
|
t.Fatalf("policy = %#v, want round-tripped fields", got)
|
|
}
|
|
if !got.ReviewRule.Enabled || got.ReviewRule.MaxRunsPerSession != 5 || got.ReviewRule.MaxTotalTokensPerSession != 123456 {
|
|
t.Fatalf("review rule = %#v, want round-tripped values", got.ReviewRule)
|
|
}
|
|
|
|
// Plan lookup finds the policy through any role so deletion stays guarded.
|
|
byExec, err := store.ListModelPoliciesByPlan(ctx, "ws", "mp-1")
|
|
if err != nil {
|
|
t.Fatalf("ListModelPoliciesByPlan(mp-1) error = %v", err)
|
|
}
|
|
byReview, err := store.ListModelPoliciesByPlan(ctx, "ws", "mp-2")
|
|
if err != nil {
|
|
t.Fatalf("ListModelPoliciesByPlan(mp-2) error = %v", err)
|
|
}
|
|
if len(byExec) != 1 || len(byReview) != 1 {
|
|
t.Fatalf("plan lookups = exec:%d review:%d, want 1 each", len(byExec), len(byReview))
|
|
}
|
|
|
|
// Missing policy is a typed not-found.
|
|
if _, err := store.GetModelPolicy(ctx, "ws", "pol-missing"); !errors.Is(err, ErrModelPolicyNotFound) {
|
|
t.Fatalf("GetModelPolicy(missing) error = %v, want ErrModelPolicyNotFound", err)
|
|
}
|
|
}
|