package domain import ( "strings" "testing" "time" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/epistemics" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/work" ) func TestPlanningInvestigationIsNotCodexPrompt(t *testing.T) { result := planForTest(t, "Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable) requireKinds(t, result, capability.CapabilityKindInspectProject, capability.CapabilityKindReadDatabase, capability.CapabilityKindQueryLogs, capability.CapabilityKindCognitiveReasoning) for _, delegation := range result.Candidate.Delegations { if strings.Contains(strings.ToLower(delegation.ExecutorHint), "codex") { t.Fatalf("planning must not hardcode Codex executor: %#v", delegation) } } if strings.Contains(strings.ToLower(result.Candidate.Summary), "prompt") { t.Fatalf("canonical plan must not be prompt-centric: %q", result.Candidate.Summary) } } func TestPlanningCodeChangeRequiresModifyCodeWithoutExecutor(t *testing.T) { result := planForTest(t, "В Assistant добавь поддержку X.", "DIRECTIVE", epistemics.VerificationNotApplicable) requireKinds(t, result, capability.CapabilityKindModifyCode, capability.CapabilityKindRunTests) for _, delegation := range result.Candidate.Delegations { if delegation.ExecutorHint != "" { t.Fatalf("executor must remain unselected in canonical planning: %#v", delegation) } } } func TestPlanningEmailReviewIsNotCodexSpecific(t *testing.T) { result := planForTest(t, "Посмотри новые письма и скажи, на какие нужно ответить.", "DIRECTIVE", epistemics.VerificationNotApplicable) requireKinds(t, result, capability.CapabilityKindReadEmail, capability.CapabilityKindCognitiveReasoning) requireNoKind(t, result, capability.CapabilityKindModifyCode) } func TestPlanningReminderUsesSchedulingRequirement(t *testing.T) { result := planForTest(t, "Напомни завтра позвонить врачу.", "DIRECTIVE", epistemics.VerificationNotApplicable) requireKinds(t, result, capability.CapabilityKindScheduleReminder) requireNoKind(t, result, capability.CapabilityKindModifyCode) } func TestPlanningRiskyUnverifiedAssertionRequiresVerification(t *testing.T) { result := planForTest(t, "На сервере 300 ГБ свободно, установи большую модель.", "ASSERTION", epistemics.VerificationUnverified) requireKinds(t, result, capability.CapabilityKindVerifyFact) found := false for _, requirement := range result.Candidate.Requirements { if requirement.CapabilityKind == capability.CapabilityKindVerifyFact && requirement.EpistemicPrecondition == epistemics.VerificationUnverified { found = true } } if !found { t.Fatalf("expected unverified verification precondition: %#v", result.Candidate.Requirements) } } func TestPlanningSendEmailPreservesAuthorityBoundary(t *testing.T) { result := planForTest(t, "Отправь Ивану письмо.", "DIRECTIVE", epistemics.VerificationNotApplicable) requireKinds(t, result, capability.CapabilityKindSendEmail) if result.Goal.State == work.GoalCompleted || !strings.Contains(result.Reply, "требует отдельной capability/authority") { t.Fatalf("planning must not claim execution/completion: goal=%#v reply=%q", result.Goal, result.Reply) } } func TestPlanningFallbackPersistsWithoutCognition(t *testing.T) { result := planForTest(t, "Сделай что-нибудь полезное.", "DIRECTIVE", epistemics.VerificationUnknown) if result.Goal.ID == "" || result.Candidate.ID == "" || result.Plan.ID == "" { t.Fatalf("expected persisted planning identities: %#v", result) } if result.Candidate.Source != "deterministic-planner-v1" { t.Fatalf("expected deterministic fallback source: %s", result.Candidate.Source) } } func TestHelpPathSimpleAnswerDoesNotRequireDurableGoalWorkPlan(t *testing.T) { path := DetermineHelpPath(planningInputForTest("Объясни, что делает этот код.", "QUESTION", epistemics.VerificationNotApplicable)) if path.Kind != work.HelpPathCognition || path.NeedsDurableGoal || path.NeedsDurableWork || path.NeedsDurablePlan { t.Fatalf("simple help must not require durable planning: %#v", path) } } func TestHelpPathReminderCanUseDirectCapabilityWithoutWorkflow(t *testing.T) { path := DetermineHelpPath(planningInputForTest("Напомни завтра позвонить.", "DIRECTIVE", epistemics.VerificationNotApplicable)) if path.Kind != work.HelpPathDirectCapability || path.NeedsDurableWork || len(path.SuggestedCapabilityKinds) != 1 || path.SuggestedCapabilityKinds[0] != capability.CapabilityKindScheduleReminder { t.Fatalf("reminder should be direct scheduling capability path: %#v", path) } } func TestHelpPathComplexWorkMaterializesDurablePlanning(t *testing.T) { path := DetermineHelpPath(planningInputForTest("Разберись, почему УПО тормозит.", "REQUEST_TO_INVESTIGATE", epistemics.VerificationNotApplicable)) if path.Kind != work.HelpPathDurableWork || !path.NeedsDurableGoal || !path.NeedsDurableWork || !path.NeedsDurablePlan { t.Fatalf("complex investigation should need durable planning: %#v", path) } } func TestHelpPathCapabilityGapDoesNotBecomeCodexTask(t *testing.T) { path := DetermineHelpPath(planningInputForTest("Подключи интеграцию с неизвестным сервисом, которого сейчас нет.", "DIRECTIVE", epistemics.VerificationNotApplicable)) if path.Kind != work.HelpPathCapabilityGap || !path.CapabilityGap { t.Fatalf("expected capability gap path: %#v", path) } if strings.Contains(strings.ToLower(path.Reply), "codex") { t.Fatalf("capability gap must not be represented as Codex task: %#v", path) } } func planForTest(t *testing.T, message string, act string, verification epistemics.VerificationState) work.PlanningResult { t.Helper() result, err := BuildPlanningResult(planningInputForTest(message, act, verification), time.Unix(100, 0)) if err != nil { t.Fatal(err) } if err := ValidatePlanCandidate(result.Candidate); err != nil { t.Fatal(err) } return result } func planningInputForTest(message string, act string, verification epistemics.VerificationState) work.PlanningInput { return work.PlanningInput{ ConversationID: "conversation-1", UtteranceID: "utterance-1", IntentID: "intent-1", ActorID: identity.ActorID("actor-1"), PrincipalID: identity.PrincipalID("principal-1"), ProjectID: work.ProjectRef("upo"), ProjectDisplayName: "УПО", ProjectProvenance: work.ProjectProvenanceRef("EXPLICIT"), Message: message, SpeechAct: work.SpeechActRef(act), AssertionVerification: verification, ReceivedAtUnixMillis: time.Unix(100, 0).UnixMilli(), } } func requireKinds(t *testing.T, result work.PlanningResult, kinds ...capability.CapabilityKind) { t.Helper() actual := map[capability.CapabilityKind]bool{} for _, requirement := range result.Candidate.Requirements { actual[requirement.CapabilityKind] = true } for _, kind := range kinds { if !actual[kind] { t.Fatalf("missing capability kind %s in %#v", kind, result.Candidate.Requirements) } } } func requireNoKind(t *testing.T, result work.PlanningResult, kind capability.CapabilityKind) { t.Helper() for _, requirement := range result.Candidate.Requirements { if requirement.CapabilityKind == kind { t.Fatalf("unexpected capability kind %s in %#v", kind, result.Candidate.Requirements) } } }