Files
tutti/services/tuttid/api/daemon_automation_rules_test.go
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

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)
}
}