package main import ( "context" "errors" "testing" "time" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/epistemics" "git.cin.su/m/Assistant/contracts/interaction" selfcontract "git.cin.su/m/Assistant/contracts/self" ) type learningEpistemicsStub struct { experiences []epistemics.ExperienceRecord } func (s *learningEpistemicsStub) SaveSystemExpectation(context.Context, epistemics.SystemExpectation) error { return nil } func (s *learningEpistemicsStub) SaveExperienceRecord(_ context.Context, value epistemics.ExperienceRecord) error { s.experiences = append(s.experiences, value) return nil } func (s *learningEpistemicsStub) SaveReflectionCandidate(context.Context, epistemics.ReflectionCandidate) error { return nil } type learningCapabilityStub struct { loadCalls int saveCalls int saved []capability.SkillProfile } type learningSelfStub struct { signals int needs int ledgers int } type failingLearningSelfStub struct{} func (failingLearningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error { return errors.New("self repository unavailable") } func (failingLearningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error { return errors.New("self repository unavailable") } func (failingLearningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error { return errors.New("self repository unavailable") } func (s *learningSelfStub) SaveLearningSignal(context.Context, selfcontract.LearningSignal) error { s.signals++ return nil } func (s *learningSelfStub) SaveImprovementNeed(context.Context, selfcontract.ImprovementNeed) error { s.needs++ return nil } func (s *learningSelfStub) SaveLearningLedger(context.Context, selfcontract.LearningLedgerEntry) error { s.ledgers++ return nil } func (s *learningCapabilityStub) LoadSkillProfile(context.Context, capability.SkillProfileID) (capability.SkillProfile, error) { s.loadCalls++ return capability.SkillProfile{}, nil } func (s *learningCapabilityStub) SaveSkillProfile(_ context.Context, value capability.SkillProfile) error { s.saveCalls++ s.saved = append(s.saved, value) return nil } func TestVerifiedDelegationCreatesCandidateNotActiveSkill(t *testing.T) { e := &learningEpistemicsStub{} c := &learningCapabilityStub{} r := learningRecorder{epistemics: e, capability: c} r.recordDelegationOutcome(context.Background(), delegationLearningInput{Intent: interaction.OriginalHumanIntent{ID: "i"}, Utterance: interaction.HumanUtterance{ID: "u"}, DelegationID: "d", RunID: "r", ImplementationID: "impl", OutcomeRef: "outcome", VerificationRefs: []epistemics.EvidenceRef{"verified"}, Verified: true, At: time.UnixMilli(1000)}) if len(c.saved) != 1 || c.saved[0].Status != capability.SkillCandidate || c.saved[0].VerifiedOutcomeCount != 1 { t.Fatalf("candidate=%#v", c.saved) } } func TestGenericCorrectionDoesNotDegradeReminderSkill(t *testing.T) { epistemicsStore := &learningEpistemicsStub{} capabilityStore := &learningCapabilityStub{} recorder := learningRecorder{epistemics: epistemicsStore, capability: capabilityStore} recorder.recordCorrection(context.Background(), correctionLearningInput{ Intent: interaction.OriginalHumanIntent{ID: "intent-1"}, Utterance: interaction.HumanUtterance{ID: "utterance-1"}, At: time.UnixMilli(1000), }) if capabilityStore.loadCalls != 0 || capabilityStore.saveCalls != 0 { t.Fatalf("generic correction must not mutate reminder skill: load=%d save=%d", capabilityStore.loadCalls, capabilityStore.saveCalls) } if len(epistemicsStore.experiences) != 1 || epistemicsStore.experiences[0].SkillRef != "" { t.Fatalf("generic correction must remain evidence without a reminder skill reference: %#v", epistemicsStore.experiences) } } func TestCorrectionWritesBoundedSelfEvidenceWithoutChangingResponsePath(t *testing.T) { epistemicsStore := &learningEpistemicsStub{} selfStore := &learningSelfStub{} recorder := learningRecorder{epistemics: epistemicsStore, self: selfStore} recorder.recordCorrection(context.Background(), correctionLearningInput{ Intent: interaction.OriginalHumanIntent{ID: "intent-2"}, Utterance: interaction.HumanUtterance{ID: "utterance-2", PrincipalID: "principal-2"}, At: time.UnixMilli(2000), }) if selfStore.signals != 1 || selfStore.needs != 1 || selfStore.ledgers != 1 { t.Fatalf("self evidence counts: signals=%d needs=%d ledgers=%d", selfStore.signals, selfStore.needs, selfStore.ledgers) } if len(epistemicsStore.experiences) != 1 { t.Fatalf("completed learning path was changed") } } func TestSelfRepositoryFailureDoesNotDiscardUserFacingLearningPath(t *testing.T) { epistemicsStore := &learningEpistemicsStub{} recorder := learningRecorder{epistemics: epistemicsStore, self: failingLearningSelfStub{}} recorder.recordCorrection(context.Background(), correctionLearningInput{ Intent: interaction.OriginalHumanIntent{ID: "intent-failure"}, Utterance: interaction.HumanUtterance{ID: "utterance-failure", PrincipalID: "principal-failure"}, At: time.UnixMilli(3000), }) if len(epistemicsStore.experiences) != 1 { t.Fatalf("SELF repository failure must not discard the completed epistemics response path") } }