162 lines
7.5 KiB
Go
162 lines
7.5 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|