133 lines
5.2 KiB
Go
133 lines
5.2 KiB
Go
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")
|
|
}
|
|
}
|