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
199 lines
8.6 KiB
Go
199 lines
8.6 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
tuttigenerated "github.com/tutti-os/tutti/services/tuttid/api/generated"
|
|
automationrulebiz "github.com/tutti-os/tutti/services/tuttid/biz/automationrule"
|
|
modelplanbiz "github.com/tutti-os/tutti/services/tuttid/biz/modelplan"
|
|
workspacedata "github.com/tutti-os/tutti/services/tuttid/data/workspace"
|
|
automationruleservice "github.com/tutti-os/tutti/services/tuttid/service/automationrule"
|
|
)
|
|
|
|
type stubAutomationRuleService struct {
|
|
rules []automationrulebiz.Rule
|
|
rule automationrulebiz.Rule
|
|
override automationrulebiz.SessionOverride
|
|
hasOverride bool
|
|
err error
|
|
putInput automationruleservice.PutRuleInput
|
|
setOverride automationrulebiz.SessionOverride
|
|
getCalls int
|
|
updateCalls int
|
|
}
|
|
|
|
func (s *stubAutomationRuleService) ListRules(context.Context, string) ([]automationrulebiz.Rule, error) {
|
|
return s.rules, s.err
|
|
}
|
|
|
|
func (s *stubAutomationRuleService) GetRule(context.Context, string, string) (automationrulebiz.Rule, error) {
|
|
s.getCalls++
|
|
return s.rule, s.err
|
|
}
|
|
|
|
func (s *stubAutomationRuleService) CreateRule(_ context.Context, input automationruleservice.PutRuleInput) (automationrulebiz.Rule, error) {
|
|
s.putInput = input
|
|
return s.rule, s.err
|
|
}
|
|
|
|
func (s *stubAutomationRuleService) UpdateRule(_ context.Context, input automationruleservice.PutRuleInput) (automationrulebiz.Rule, error) {
|
|
s.updateCalls++
|
|
s.putInput = input
|
|
return s.rule, s.err
|
|
}
|
|
|
|
func TestUpdateAutomationRuleDelegatesExistenceCheckToService(t *testing.T) {
|
|
service := &stubAutomationRuleService{err: workspacedata.ErrAutomationRuleNotFound}
|
|
api := DaemonAPI{
|
|
AutomationRuleService: service,
|
|
PreferencesService: gateTestPreferences(map[string]bool{AutomationRulesFeatureFlag: true}, nil),
|
|
}
|
|
response, err := api.UpdateAutomationRule(context.Background(), tuttigenerated.UpdateAutomationRuleRequestObject{
|
|
WorkspaceID: "ws", AutomationRuleID: "automation-rule:missing",
|
|
Body: &tuttigenerated.PutAutomationRuleRequest{
|
|
Name: "Missing", Trigger: tuttigenerated.AutomationRuleTriggerOnTaskComplete,
|
|
Target: tuttigenerated.AutomationRuleTarget{Kind: tuttigenerated.AutomationRuleTargetKindAgent, WorkspaceAgentId: stringPointer("workspace-agent:target")},
|
|
Permissions: tuttigenerated.AutomationRulePermissions{},
|
|
Budget: tuttigenerated.AutomationRuleBudget{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("UpdateAutomationRule() error = %v", err)
|
|
}
|
|
if _, ok := response.(tuttigenerated.UpdateAutomationRule404JSONResponse); !ok {
|
|
t.Fatalf("UpdateAutomationRule() response = %T", response)
|
|
}
|
|
if service.getCalls != 0 || service.updateCalls != 1 {
|
|
t.Fatalf("service calls get=%d update=%d, want 0/1", service.getCalls, service.updateCalls)
|
|
}
|
|
}
|
|
|
|
func (s *stubAutomationRuleService) DeleteRule(context.Context, string, string) error { return s.err }
|
|
|
|
func (s *stubAutomationRuleService) GetSessionOverride(context.Context, string, string) (automationrulebiz.SessionOverride, bool, error) {
|
|
return s.override, s.hasOverride, s.err
|
|
}
|
|
|
|
func (s *stubAutomationRuleService) SetSessionOverride(_ context.Context, override automationrulebiz.SessionOverride) (automationrulebiz.SessionOverride, error) {
|
|
s.setOverride = override
|
|
return s.override, s.err
|
|
}
|
|
|
|
func testAutomationRule() automationrulebiz.Rule {
|
|
now := time.Unix(1_700_000_000, 0).UTC()
|
|
return automationrulebiz.Rule{
|
|
ID: "automation-rule:one", WorkspaceID: "ws", Name: "Launch follow-up", Enabled: true,
|
|
Trigger: automationrulebiz.TriggerOnTaskComplete,
|
|
Target: automationrulebiz.Target{
|
|
Kind: automationrulebiz.TargetAgent, WorkspaceAgentID: "workspace-agent:reviewer",
|
|
},
|
|
Permissions: automationrulebiz.PermissionPolicy{PermissionModeID: "workspace-write", AllowedTools: []string{"terminal"}},
|
|
Budget: automationrulebiz.Budget{MaxRunsPerSession: 2, MaxTotalTokensPerSession: 40_000},
|
|
Prompt: "Check correctness", CreatedAt: now, UpdatedAt: now,
|
|
}
|
|
}
|
|
|
|
func TestCreateAutomationRuleMapsRequestAndProjection(t *testing.T) {
|
|
service := &stubAutomationRuleService{rule: testAutomationRule()}
|
|
api := DaemonAPI{
|
|
AutomationRuleService: service,
|
|
PreferencesService: gateTestPreferences(map[string]bool{AutomationRulesFeatureFlag: true}, nil),
|
|
}
|
|
response, err := api.CreateAutomationRule(context.Background(), tuttigenerated.CreateAutomationRuleRequestObject{
|
|
WorkspaceID: "ws",
|
|
Body: &tuttigenerated.PutAutomationRuleRequest{
|
|
Name: "Launch follow-up", Enabled: true,
|
|
Trigger: tuttigenerated.AutomationRuleTriggerOnTaskComplete,
|
|
Target: tuttigenerated.AutomationRuleTarget{
|
|
Kind: tuttigenerated.AutomationRuleTargetKindAgent, WorkspaceAgentId: stringPointer("workspace-agent:reviewer"),
|
|
},
|
|
Permissions: tuttigenerated.AutomationRulePermissions{PermissionModeId: stringPointer("workspace-write"), AllowedTools: []string{"terminal"}},
|
|
Budget: tuttigenerated.AutomationRuleBudget{MaxRunsPerSession: 2, MaxTotalTokensPerSession: 40_000},
|
|
Prompt: "Check correctness",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateAutomationRule() error = %v", err)
|
|
}
|
|
created, ok := response.(tuttigenerated.CreateAutomationRule201JSONResponse)
|
|
if !ok {
|
|
t.Fatalf("CreateAutomationRule() response = %T", response)
|
|
}
|
|
if created.Id != "automation-rule:one" || created.Target.WorkspaceAgentId == nil || *created.Target.WorkspaceAgentId != "workspace-agent:reviewer" {
|
|
t.Fatalf("CreateAutomationRule() projection = %#v", created)
|
|
}
|
|
if service.putInput.Target.WorkspaceAgentID != "workspace-agent:reviewer" || service.putInput.Permissions.PermissionModeID != "workspace-write" {
|
|
t.Fatalf("CreateAutomationRule() input = %#v", service.putInput)
|
|
}
|
|
}
|
|
|
|
func TestGetAutomationRuleReturnsSpecificNotFoundCode(t *testing.T) {
|
|
api := DaemonAPI{
|
|
AutomationRuleService: &stubAutomationRuleService{err: workspacedata.ErrAutomationRuleNotFound},
|
|
PreferencesService: gateTestPreferences(map[string]bool{AutomationRulesFeatureFlag: true}, nil),
|
|
}
|
|
response, err := api.GetAutomationRule(context.Background(), tuttigenerated.GetAutomationRuleRequestObject{
|
|
WorkspaceID: "ws", AutomationRuleID: "automation-rule:missing",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetAutomationRule() error = %v", err)
|
|
}
|
|
notFound, ok := response.(tuttigenerated.GetAutomationRule404JSONResponse)
|
|
if !ok {
|
|
t.Fatalf("GetAutomationRule() response = %T", response)
|
|
}
|
|
if notFound.Error.Code != tuttigenerated.AutomationRuleNotFound {
|
|
t.Fatalf("GetAutomationRule() code = %q", notFound.Error.Code)
|
|
}
|
|
}
|
|
|
|
func TestSetAgentSessionAutomationRuleOverrideMapsRuleSelection(t *testing.T) {
|
|
now := time.Unix(1_700_000_100, 0).UTC()
|
|
service := &stubAutomationRuleService{override: automationrulebiz.SessionOverride{
|
|
WorkspaceID: "ws", AgentSessionID: "session-1", RuleIDs: []string{"rule-1"}, UpdatedAt: now,
|
|
}}
|
|
api := DaemonAPI{
|
|
AutomationRuleService: service,
|
|
PreferencesService: gateTestPreferences(map[string]bool{AutomationRulesFeatureFlag: true}, nil),
|
|
}
|
|
response, err := api.SetAgentSessionAutomationRuleOverride(context.Background(), tuttigenerated.SetAgentSessionAutomationRuleOverrideRequestObject{
|
|
WorkspaceID: "ws", AgentSessionID: "session-1",
|
|
Body: &tuttigenerated.SetAgentSessionAutomationRuleOverrideRequest{RuleIds: []string{"rule-1"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SetAgentSessionAutomationRuleOverride() error = %v", err)
|
|
}
|
|
updated, ok := response.(tuttigenerated.SetAgentSessionAutomationRuleOverride200JSONResponse)
|
|
if !ok || len(updated.RuleIds) != 1 || updated.RuleIds[0] != "rule-1" || updated.UpdatedAt == nil {
|
|
t.Fatalf("SetAgentSessionAutomationRuleOverride() response = %#v (%T)", response, response)
|
|
}
|
|
if len(service.setOverride.RuleIDs) != 1 || service.setOverride.RuleIDs[0] != "rule-1" {
|
|
t.Fatalf("SetAgentSessionAutomationRuleOverride() input = %#v", service.setOverride)
|
|
}
|
|
}
|
|
|
|
func TestAutomationRuleRoutesAreRegistered(t *testing.T) {
|
|
service := &stubAutomationRuleService{rules: []automationrulebiz.Rule{testAutomationRule()}}
|
|
mux := http.NewServeMux()
|
|
RegisterRoutes(mux, NewRoutes(DaemonAPI{
|
|
AutomationRuleService: service,
|
|
PreferencesService: gateTestPreferences(map[string]bool{AutomationRulesFeatureFlag: true}, nil),
|
|
}))
|
|
response := httptest.NewRecorder()
|
|
mux.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/v1/workspaces/ws/automation-rules", nil))
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d; body: %s", response.Code, http.StatusOK, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGeneratedModelPlanIncludesRevision(t *testing.T) {
|
|
generated := generatedModelPlan(modelplanbiz.PublicPlan{Revision: 7, Models: []modelplanbiz.Model{}})
|
|
if generated.Revision != 7 {
|
|
t.Fatalf("generatedModelPlan() revision = %d, want 7", generated.Revision)
|
|
}
|
|
}
|