Files
Assistant/modules/work/domain/planning_test.go
m 4fc85511c4
CI / test (push) Successful in 16s
CI / test (pull_request) Successful in 14s
DEV-000063 intent work planning foundation
2026-08-17 15:10:43 +03:00

162 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}