@skillsmith/core 2.1.0 → 2.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/analysis/types.d.ts +2 -0
- package/dist/src/analysis/types.d.ts.map +1 -1
- package/dist/src/analysis/types.js +13 -1
- package/dist/src/analysis/types.js.map +1 -1
- package/dist/src/analytics/AnalyticsRepository.d.ts +4 -0
- package/dist/src/analytics/AnalyticsRepository.d.ts.map +1 -1
- package/dist/src/analytics/AnalyticsRepository.js +26 -44
- package/dist/src/analytics/AnalyticsRepository.js.map +1 -1
- package/dist/src/analytics/schema.d.ts +1 -1
- package/dist/src/analytics/schema.d.ts.map +1 -1
- package/dist/src/analytics/schema.js +68 -0
- package/dist/src/analytics/schema.js.map +1 -1
- package/dist/src/api/client.d.ts +33 -29
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.js +15 -10
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/billing/BillingService.d.ts +139 -0
- package/dist/src/billing/BillingService.d.ts.map +1 -0
- package/dist/src/billing/BillingService.js +393 -0
- package/dist/src/billing/BillingService.js.map +1 -0
- package/dist/src/billing/GDPRComplianceService.d.ts +176 -0
- package/dist/src/billing/GDPRComplianceService.d.ts.map +1 -0
- package/dist/src/billing/GDPRComplianceService.js +361 -0
- package/dist/src/billing/GDPRComplianceService.js.map +1 -0
- package/dist/src/billing/StripeClient.d.ts +177 -0
- package/dist/src/billing/StripeClient.d.ts.map +1 -0
- package/dist/src/billing/StripeClient.js +462 -0
- package/dist/src/billing/StripeClient.js.map +1 -0
- package/dist/src/billing/StripeReconciliationJob.d.ts +95 -0
- package/dist/src/billing/StripeReconciliationJob.d.ts.map +1 -0
- package/dist/src/billing/StripeReconciliationJob.js +405 -0
- package/dist/src/billing/StripeReconciliationJob.js.map +1 -0
- package/dist/src/billing/StripeWebhookHandler.d.ts +92 -0
- package/dist/src/billing/StripeWebhookHandler.d.ts.map +1 -0
- package/dist/src/billing/StripeWebhookHandler.js +409 -0
- package/dist/src/billing/StripeWebhookHandler.js.map +1 -0
- package/dist/src/billing/index.d.ts +18 -0
- package/dist/src/billing/index.d.ts.map +1 -0
- package/dist/src/billing/index.js +19 -0
- package/dist/src/billing/index.js.map +1 -0
- package/dist/src/billing/types.d.ts +266 -0
- package/dist/src/billing/types.d.ts.map +1 -0
- package/dist/src/billing/types.js +23 -0
- package/dist/src/billing/types.js.map +1 -0
- package/dist/src/embeddings/hnsw-store.d.ts +568 -0
- package/dist/src/embeddings/hnsw-store.d.ts.map +1 -0
- package/dist/src/embeddings/hnsw-store.js +805 -0
- package/dist/src/embeddings/hnsw-store.js.map +1 -0
- package/dist/src/embeddings/index.d.ts +2 -0
- package/dist/src/embeddings/index.d.ts.map +1 -1
- package/dist/src/embeddings/index.js +2 -0
- package/dist/src/embeddings/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/learning/PatternStore.d.ts +457 -0
- package/dist/src/learning/PatternStore.d.ts.map +1 -0
- package/dist/src/learning/PatternStore.js +893 -0
- package/dist/src/learning/PatternStore.js.map +1 -0
- package/dist/src/learning/ReasoningBankIntegration.d.ts +403 -0
- package/dist/src/learning/ReasoningBankIntegration.d.ts.map +1 -0
- package/dist/src/learning/ReasoningBankIntegration.js +627 -0
- package/dist/src/learning/ReasoningBankIntegration.js.map +1 -0
- package/dist/src/learning/index.d.ts +15 -0
- package/dist/src/learning/index.d.ts.map +1 -0
- package/dist/src/learning/index.js +15 -0
- package/dist/src/learning/index.js.map +1 -0
- package/dist/src/routing/SONARouter.d.ts +154 -0
- package/dist/src/routing/SONARouter.d.ts.map +1 -0
- package/dist/src/routing/SONARouter.js +679 -0
- package/dist/src/routing/SONARouter.js.map +1 -0
- package/dist/src/routing/index.d.ts +9 -0
- package/dist/src/routing/index.d.ts.map +1 -0
- package/dist/src/routing/index.js +10 -0
- package/dist/src/routing/index.js.map +1 -0
- package/dist/src/routing/types.d.ts +331 -0
- package/dist/src/routing/types.d.ts.map +1 -0
- package/dist/src/routing/types.js +203 -0
- package/dist/src/routing/types.js.map +1 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js +5 -0
- package/dist/src/scripts/__tests__/scan-imported-skills.test.js.map +1 -1
- package/dist/src/security/SkillSandbox.d.ts +156 -0
- package/dist/src/security/SkillSandbox.d.ts.map +1 -0
- package/dist/src/security/SkillSandbox.js +303 -0
- package/dist/src/security/SkillSandbox.js.map +1 -0
- package/dist/src/security/index.d.ts +3 -1
- package/dist/src/security/index.d.ts.map +1 -1
- package/dist/src/security/index.js +5 -1
- package/dist/src/security/index.js.map +1 -1
- package/dist/src/security/rate-limiter/presets.d.ts +12 -0
- package/dist/src/security/rate-limiter/presets.d.ts.map +1 -1
- package/dist/src/security/rate-limiter/presets.js +12 -0
- package/dist/src/security/rate-limiter/presets.js.map +1 -1
- package/dist/src/security/sanitization.d.ts +85 -0
- package/dist/src/security/sanitization.d.ts.map +1 -1
- package/dist/src/security/sanitization.js +133 -0
- package/dist/src/security/sanitization.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.d.ts +23 -0
- package/dist/src/security/scanner/SecurityScanner.d.ts.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.js +232 -28
- package/dist/src/security/scanner/SecurityScanner.js.map +1 -1
- package/dist/src/security/scanner/patterns.d.ts +13 -0
- package/dist/src/security/scanner/patterns.d.ts.map +1 -1
- package/dist/src/security/scanner/patterns.js +51 -0
- package/dist/src/security/scanner/patterns.js.map +1 -1
- package/dist/src/security/scanner/types.d.ts +13 -1
- package/dist/src/security/scanner/types.d.ts.map +1 -1
- package/dist/src/security/scanner/weights.d.ts.map +1 -1
- package/dist/src/security/scanner/weights.js +1 -0
- package/dist/src/security/scanner/weights.js.map +1 -1
- package/dist/src/session/SessionManager.d.ts +7 -0
- package/dist/src/session/SessionManager.d.ts.map +1 -1
- package/dist/src/session/SessionManager.js +117 -10
- package/dist/src/session/SessionManager.js.map +1 -1
- package/dist/src/sync/SyncEngine.d.ts.map +1 -1
- package/dist/src/sync/SyncEngine.js +52 -32
- package/dist/src/sync/SyncEngine.js.map +1 -1
- package/dist/src/testing/MultiLLMProvider.d.ts +374 -0
- package/dist/src/testing/MultiLLMProvider.d.ts.map +1 -0
- package/dist/src/testing/MultiLLMProvider.js +720 -0
- package/dist/src/testing/MultiLLMProvider.js.map +1 -0
- package/dist/src/testing/index.d.ts +8 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +9 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/types.d.ts +3 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/tests/SecurityScanner.test.js +337 -1
- package/dist/tests/SecurityScanner.test.js.map +1 -1
- package/dist/tests/billing/BillingService.test.d.ts +7 -0
- package/dist/tests/billing/BillingService.test.d.ts.map +1 -0
- package/dist/tests/billing/BillingService.test.js +168 -0
- package/dist/tests/billing/BillingService.test.js.map +1 -0
- package/dist/tests/billing/GDPRCompliance.test.d.ts +7 -0
- package/dist/tests/billing/GDPRCompliance.test.d.ts.map +1 -0
- package/dist/tests/billing/GDPRCompliance.test.js +195 -0
- package/dist/tests/billing/GDPRCompliance.test.js.map +1 -0
- package/dist/tests/billing/StripeReconciliation.test.d.ts +7 -0
- package/dist/tests/billing/StripeReconciliation.test.d.ts.map +1 -0
- package/dist/tests/billing/StripeReconciliation.test.js +266 -0
- package/dist/tests/billing/StripeReconciliation.test.js.map +1 -0
- package/dist/tests/billing/stripe-validators.test.d.ts +7 -0
- package/dist/tests/billing/stripe-validators.test.d.ts.map +1 -0
- package/dist/tests/billing/stripe-validators.test.js +107 -0
- package/dist/tests/billing/stripe-validators.test.js.map +1 -0
- package/dist/tests/embeddings/hnsw-store.test.d.ts +7 -0
- package/dist/tests/embeddings/hnsw-store.test.d.ts.map +1 -0
- package/dist/tests/embeddings/hnsw-store.test.js +295 -0
- package/dist/tests/embeddings/hnsw-store.test.js.map +1 -0
- package/dist/tests/integration/neural/e2e-learning.test.d.ts +17 -0
- package/dist/tests/integration/neural/e2e-learning.test.d.ts.map +1 -0
- package/dist/tests/integration/neural/e2e-learning.test.js +238 -0
- package/dist/tests/integration/neural/e2e-learning.test.js.map +1 -0
- package/dist/tests/integration/neural/helpers.d.ts +132 -0
- package/dist/tests/integration/neural/helpers.d.ts.map +1 -0
- package/dist/tests/integration/neural/helpers.js +287 -0
- package/dist/tests/integration/neural/helpers.js.map +1 -0
- package/dist/tests/integration/neural/personalization.test.d.ts +21 -0
- package/dist/tests/integration/neural/personalization.test.d.ts.map +1 -0
- package/dist/tests/integration/neural/personalization.test.js +304 -0
- package/dist/tests/integration/neural/personalization.test.js.map +1 -0
- package/dist/tests/integration/neural/preference-learner.test.d.ts +23 -0
- package/dist/tests/integration/neural/preference-learner.test.d.ts.map +1 -0
- package/dist/tests/integration/neural/preference-learner.test.js +289 -0
- package/dist/tests/integration/neural/preference-learner.test.js.map +1 -0
- package/dist/tests/integration/neural/privacy.test.d.ts +19 -0
- package/dist/tests/integration/neural/privacy.test.d.ts.map +1 -0
- package/dist/tests/integration/neural/privacy.test.js +249 -0
- package/dist/tests/integration/neural/privacy.test.js.map +1 -0
- package/dist/tests/integration/neural/setup.d.ts +175 -0
- package/dist/tests/integration/neural/setup.d.ts.map +1 -0
- package/dist/tests/integration/neural/setup.js +487 -0
- package/dist/tests/integration/neural/setup.js.map +1 -0
- package/dist/tests/integration/neural/signal-collection.test.d.ts +21 -0
- package/dist/tests/integration/neural/signal-collection.test.d.ts.map +1 -0
- package/dist/tests/integration/neural/signal-collection.test.js +232 -0
- package/dist/tests/integration/neural/signal-collection.test.js.map +1 -0
- package/dist/tests/learning/PatternStore.test.d.ts +8 -0
- package/dist/tests/learning/PatternStore.test.d.ts.map +1 -0
- package/dist/tests/learning/PatternStore.test.js +589 -0
- package/dist/tests/learning/PatternStore.test.js.map +1 -0
- package/dist/tests/learning/ReasoningBankIntegration.test.d.ts +8 -0
- package/dist/tests/learning/ReasoningBankIntegration.test.d.ts.map +1 -0
- package/dist/tests/learning/ReasoningBankIntegration.test.js +269 -0
- package/dist/tests/learning/ReasoningBankIntegration.test.js.map +1 -0
- package/dist/tests/routing/SONARouter.test.d.ts +8 -0
- package/dist/tests/routing/SONARouter.test.d.ts.map +1 -0
- package/dist/tests/routing/SONARouter.test.js +400 -0
- package/dist/tests/routing/SONARouter.test.js.map +1 -0
- package/dist/tests/security/ContinuousSecurity.test.js +10 -12
- package/dist/tests/security/ContinuousSecurity.test.js.map +1 -1
- package/dist/tests/security/SkillSandbox.test.d.ts +8 -0
- package/dist/tests/security/SkillSandbox.test.d.ts.map +1 -0
- package/dist/tests/security/SkillSandbox.test.js +321 -0
- package/dist/tests/security/SkillSandbox.test.js.map +1 -0
- package/dist/tests/sync/SyncEngine.test.js +4 -2
- package/dist/tests/sync/SyncEngine.test.js.map +1 -1
- package/dist/tests/testing/MultiLLMProvider.test.d.ts +14 -0
- package/dist/tests/testing/MultiLLMProvider.test.d.ts.map +1 -0
- package/dist/tests/testing/MultiLLMProvider.test.js +438 -0
- package/dist/tests/testing/MultiLLMProvider.test.js.map +1 -0
- package/package.json +16 -3
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-1535: Personalization Engine Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the IPersonalizationEngine interface for applying learned
|
|
5
|
+
* preferences to recommendation results in the Recommendation Learning Loop.
|
|
6
|
+
*
|
|
7
|
+
* Test Cases:
|
|
8
|
+
* 1. shouldPersonalize() returns false with <5 signals
|
|
9
|
+
* 2. shouldPersonalize() returns true with 5+ signals
|
|
10
|
+
* 3. personalizeRecommendations() re-ranks by learned scores
|
|
11
|
+
* 4. Category weight boosts preferred categories
|
|
12
|
+
* 5. Dismiss patterns reduce scores for related skills
|
|
13
|
+
* 6. Uninstall patterns have strongest negative effect
|
|
14
|
+
* 7. Score breakdown shows contributing factors
|
|
15
|
+
* 8. Personalization disabled by user preference
|
|
16
|
+
*
|
|
17
|
+
* @see packages/core/src/learning/interfaces.ts
|
|
18
|
+
* @see docs/execution/phase5-testing-execution.md
|
|
19
|
+
*/
|
|
20
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
21
|
+
import { createNeuralTestContext, cleanupNeuralTestContext, createDefaultProfile, } from './setup.js';
|
|
22
|
+
import { generateContext } from './helpers.js';
|
|
23
|
+
import { SkillCategory } from '../../../src/learning/types.js';
|
|
24
|
+
describe('PersonalizationEngine Integration', () => {
|
|
25
|
+
let ctx;
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
ctx = createNeuralTestContext();
|
|
28
|
+
});
|
|
29
|
+
afterEach(async () => {
|
|
30
|
+
await cleanupNeuralTestContext(ctx);
|
|
31
|
+
});
|
|
32
|
+
describe('Personalization Threshold', () => {
|
|
33
|
+
it('should return false for shouldPersonalize with <5 signals', async () => {
|
|
34
|
+
// Add only 4 signals
|
|
35
|
+
for (let i = 0; i < 4; i++) {
|
|
36
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext());
|
|
37
|
+
}
|
|
38
|
+
const shouldPersonalize = await ctx.personalizationEngine.shouldPersonalize();
|
|
39
|
+
expect(shouldPersonalize).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('should return true for shouldPersonalize with 5+ signals', async () => {
|
|
42
|
+
// Add exactly 5 signals (threshold)
|
|
43
|
+
for (let i = 0; i < 5; i++) {
|
|
44
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext());
|
|
45
|
+
}
|
|
46
|
+
const shouldPersonalize = await ctx.personalizationEngine.shouldPersonalize();
|
|
47
|
+
expect(shouldPersonalize).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('should return true with many signals', async () => {
|
|
50
|
+
// Add 20 signals
|
|
51
|
+
for (let i = 0; i < 20; i++) {
|
|
52
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext());
|
|
53
|
+
}
|
|
54
|
+
const shouldPersonalize = await ctx.personalizationEngine.shouldPersonalize();
|
|
55
|
+
expect(shouldPersonalize).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('Recommendation Re-ranking', () => {
|
|
59
|
+
it('should re-rank recommendations by learned scores', async () => {
|
|
60
|
+
// Build up preference for testing category
|
|
61
|
+
for (let i = 0; i < 10; i++) {
|
|
62
|
+
await ctx.signalCollector.recordAccept(`testing-skill-${i}`, generateContext({ category: SkillCategory.TESTING }));
|
|
63
|
+
}
|
|
64
|
+
// Train the profile
|
|
65
|
+
let profile = createDefaultProfile();
|
|
66
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
67
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
68
|
+
await ctx.profileRepository.saveProfile(profile);
|
|
69
|
+
// Create recommendations with testing skill having lower base score
|
|
70
|
+
const recommendations = [
|
|
71
|
+
{
|
|
72
|
+
skill_id: 'devops-skill',
|
|
73
|
+
base_score: 0.9,
|
|
74
|
+
skill_data: { category: SkillCategory.DEVOPS, trustTier: 'community' },
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
skill_id: 'testing-skill',
|
|
78
|
+
base_score: 0.7, // Lower base score
|
|
79
|
+
skill_data: { category: SkillCategory.TESTING, trustTier: 'community' },
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
skill_id: 'frontend-skill',
|
|
83
|
+
base_score: 0.8,
|
|
84
|
+
skill_data: { category: SkillCategory.FRONTEND, trustTier: 'community' },
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
const personalized = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
88
|
+
// Results should be sorted by personalized_score descending
|
|
89
|
+
for (let i = 1; i < personalized.length; i++) {
|
|
90
|
+
expect(personalized[i - 1].personalized_score).toBeGreaterThanOrEqual(personalized[i].personalized_score);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('Category Weight Boosting', () => {
|
|
95
|
+
it('should boost scores for preferred categories', async () => {
|
|
96
|
+
// Build strong preference for TESTING
|
|
97
|
+
for (let i = 0; i < 15; i++) {
|
|
98
|
+
await ctx.signalCollector.recordAccept(`testing-skill-${i}`, generateContext({ category: SkillCategory.TESTING }));
|
|
99
|
+
await ctx.signalCollector.recordUsage(`testing-skill-${i}`, 'daily');
|
|
100
|
+
}
|
|
101
|
+
// Train profile
|
|
102
|
+
let profile = createDefaultProfile();
|
|
103
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
104
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
105
|
+
await ctx.profileRepository.saveProfile(profile);
|
|
106
|
+
// Test single skill personalization
|
|
107
|
+
const recommendations = [
|
|
108
|
+
{
|
|
109
|
+
skill_id: 'preferred-skill',
|
|
110
|
+
base_score: 0.5,
|
|
111
|
+
skill_data: { category: SkillCategory.TESTING, trustTier: 'community' },
|
|
112
|
+
},
|
|
113
|
+
];
|
|
114
|
+
const [result] = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
115
|
+
// Category boost should be positive
|
|
116
|
+
expect(result.score_breakdown.category_boost).toBeGreaterThan(0);
|
|
117
|
+
expect(result.personalization_applied).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('Dismiss Pattern Effects', () => {
|
|
121
|
+
it('should reduce scores for skills matching dismiss patterns', async () => {
|
|
122
|
+
// Dismiss multiple skills to establish negative patterns
|
|
123
|
+
const dismissedSkills = ['skill-a', 'skill-b', 'skill-c', 'skill-d', 'skill-e'];
|
|
124
|
+
for (const skillId of dismissedSkills) {
|
|
125
|
+
await ctx.signalCollector.recordDismiss(skillId, generateContext());
|
|
126
|
+
}
|
|
127
|
+
// Train profile with dismissals
|
|
128
|
+
let profile = createDefaultProfile();
|
|
129
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
130
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
131
|
+
await ctx.profileRepository.saveProfile(profile);
|
|
132
|
+
// Try to recommend a dismissed skill
|
|
133
|
+
const recommendations = [
|
|
134
|
+
{
|
|
135
|
+
skill_id: 'skill-a', // Previously dismissed
|
|
136
|
+
base_score: 0.9,
|
|
137
|
+
skill_data: { category: SkillCategory.GIT, trustTier: 'verified' },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
skill_id: 'new-skill', // Never seen before
|
|
141
|
+
base_score: 0.7,
|
|
142
|
+
skill_data: { category: SkillCategory.GIT, trustTier: 'verified' },
|
|
143
|
+
},
|
|
144
|
+
];
|
|
145
|
+
const personalized = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
146
|
+
// Find results
|
|
147
|
+
const dismissedResult = personalized.find((r) => r.skill_id === 'skill-a');
|
|
148
|
+
const newResult = personalized.find((r) => r.skill_id === 'new-skill');
|
|
149
|
+
// Dismissed skill should have anti-penalty
|
|
150
|
+
expect(dismissedResult.score_breakdown.anti_penalty).toBeLessThan(0);
|
|
151
|
+
// New skill should have no anti-penalty
|
|
152
|
+
expect(newResult.score_breakdown.anti_penalty).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('Uninstall Impact', () => {
|
|
156
|
+
it('should apply strongest negative effect for uninstalled skills', async () => {
|
|
157
|
+
// Uninstall some skills (strongest negative signal)
|
|
158
|
+
const uninstalledSkills = ['uninstalled-1', 'uninstalled-2'];
|
|
159
|
+
for (const skillId of uninstalledSkills) {
|
|
160
|
+
await ctx.signalCollector.recordAccept(skillId, generateContext());
|
|
161
|
+
await ctx.signalCollector.recordUninstall(skillId, 7);
|
|
162
|
+
}
|
|
163
|
+
// Also dismiss some (weaker negative)
|
|
164
|
+
await ctx.signalCollector.recordDismiss('dismissed-1', generateContext());
|
|
165
|
+
// Train profile
|
|
166
|
+
let profile = createDefaultProfile();
|
|
167
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
168
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
169
|
+
await ctx.profileRepository.saveProfile(profile);
|
|
170
|
+
// Both uninstalled and dismissed should be in negative patterns
|
|
171
|
+
expect(profile.negative_patterns.skill_ids).toContain('uninstalled-1');
|
|
172
|
+
expect(profile.negative_patterns.skill_ids).toContain('dismissed-1');
|
|
173
|
+
// Try to recommend
|
|
174
|
+
const recommendations = [
|
|
175
|
+
{
|
|
176
|
+
skill_id: 'uninstalled-1',
|
|
177
|
+
base_score: 0.9,
|
|
178
|
+
skill_data: { category: SkillCategory.BACKEND },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
skill_id: 'dismissed-1',
|
|
182
|
+
base_score: 0.9,
|
|
183
|
+
skill_data: { category: SkillCategory.BACKEND },
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const personalized = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
187
|
+
// Both should have anti-penalty
|
|
188
|
+
for (const result of personalized) {
|
|
189
|
+
expect(result.score_breakdown.anti_penalty).toBeLessThan(0);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('Score Breakdown', () => {
|
|
194
|
+
it('should show contributing factors in score breakdown', async () => {
|
|
195
|
+
// Build mixed preferences
|
|
196
|
+
for (let i = 0; i < 8; i++) {
|
|
197
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext({
|
|
198
|
+
category: SkillCategory.TESTING,
|
|
199
|
+
trustTier: 'verified',
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
let profile = createDefaultProfile();
|
|
203
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
204
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
205
|
+
await ctx.profileRepository.saveProfile(profile);
|
|
206
|
+
const recommendations = [
|
|
207
|
+
{
|
|
208
|
+
skill_id: 'new-testing-skill',
|
|
209
|
+
base_score: 0.75,
|
|
210
|
+
skill_data: {
|
|
211
|
+
category: SkillCategory.TESTING,
|
|
212
|
+
trustTier: 'verified',
|
|
213
|
+
keywords: ['unit', 'integration'],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
];
|
|
217
|
+
const [result] = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
218
|
+
// Score breakdown should contain all components
|
|
219
|
+
expect(result.score_breakdown).toHaveProperty('category_boost');
|
|
220
|
+
expect(result.score_breakdown).toHaveProperty('trust_boost');
|
|
221
|
+
expect(result.score_breakdown).toHaveProperty('keyword_boost');
|
|
222
|
+
expect(result.score_breakdown).toHaveProperty('anti_penalty');
|
|
223
|
+
// For this profile, we should see positive category and trust boosts
|
|
224
|
+
expect(result.score_breakdown.category_boost).toBeGreaterThan(0);
|
|
225
|
+
expect(result.score_breakdown.trust_boost).toBeGreaterThan(0);
|
|
226
|
+
expect(result.score_breakdown.anti_penalty).toBe(0); // Not a dismissed skill
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe('Personalization Control', () => {
|
|
230
|
+
it('should not apply personalization when below threshold', async () => {
|
|
231
|
+
// Only 3 signals (below threshold of 5)
|
|
232
|
+
for (let i = 0; i < 3; i++) {
|
|
233
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext({ category: SkillCategory.GIT }));
|
|
234
|
+
}
|
|
235
|
+
// Even with profile data, personalization shouldn't apply
|
|
236
|
+
let profile = createDefaultProfile();
|
|
237
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
238
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
239
|
+
await ctx.profileRepository.saveProfile(profile);
|
|
240
|
+
const recommendations = [
|
|
241
|
+
{
|
|
242
|
+
skill_id: 'some-skill',
|
|
243
|
+
base_score: 0.8,
|
|
244
|
+
skill_data: { category: SkillCategory.GIT },
|
|
245
|
+
},
|
|
246
|
+
];
|
|
247
|
+
const [result] = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
248
|
+
// Personalization should not be applied
|
|
249
|
+
expect(result.personalization_applied).toBe(false);
|
|
250
|
+
// Score should equal base score when not personalized
|
|
251
|
+
expect(result.personalized_score).toBe(0.8);
|
|
252
|
+
});
|
|
253
|
+
it('should reset to default when requested', async () => {
|
|
254
|
+
// Build some preferences
|
|
255
|
+
for (let i = 0; i < 10; i++) {
|
|
256
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext());
|
|
257
|
+
}
|
|
258
|
+
let profile = createDefaultProfile();
|
|
259
|
+
const signals = await ctx.signalCollector.getSignals({});
|
|
260
|
+
profile = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
261
|
+
await ctx.profileRepository.saveProfile(profile, 'user-1');
|
|
262
|
+
// Verify profile exists
|
|
263
|
+
expect(await ctx.profileRepository.exists('user-1')).toBe(true);
|
|
264
|
+
// Reset
|
|
265
|
+
await ctx.personalizationEngine.resetToDefault('user-1');
|
|
266
|
+
// Profile should be deleted
|
|
267
|
+
expect(await ctx.profileRepository.exists('user-1')).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe('Edge Cases', () => {
|
|
271
|
+
it('should handle empty recommendations list', async () => {
|
|
272
|
+
const personalized = await ctx.personalizationEngine.personalizeRecommendations([]);
|
|
273
|
+
expect(personalized).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
it('should handle skills without category', async () => {
|
|
276
|
+
// Add signals to meet threshold
|
|
277
|
+
for (let i = 0; i < 5; i++) {
|
|
278
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext());
|
|
279
|
+
}
|
|
280
|
+
const recommendations = [
|
|
281
|
+
{
|
|
282
|
+
skill_id: 'no-category-skill',
|
|
283
|
+
base_score: 0.7,
|
|
284
|
+
skill_data: { trustTier: 'community' }, // No category
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
const [result] = await ctx.personalizationEngine.personalizeRecommendations(recommendations);
|
|
288
|
+
expect(result.skill_id).toBe('no-category-skill');
|
|
289
|
+
expect(result.score_breakdown.category_boost).toBe(0);
|
|
290
|
+
});
|
|
291
|
+
it('should handle user with no profile', async () => {
|
|
292
|
+
// Add signals to meet threshold
|
|
293
|
+
for (let i = 0; i < 5; i++) {
|
|
294
|
+
await ctx.signalCollector.recordAccept(`skill-${i}`, generateContext());
|
|
295
|
+
}
|
|
296
|
+
// No profile saved for 'new-user'
|
|
297
|
+
const profile = await ctx.personalizationEngine.getUserProfile('new-user');
|
|
298
|
+
// Should return default profile
|
|
299
|
+
expect(profile.signal_count).toBe(0);
|
|
300
|
+
expect(profile.version).toBe(1);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
//# sourceMappingURL=personalization.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"personalization.test.js","sourceRoot":"","sources":["../../../../tests/integration/neural/personalization.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-1535: Preference Learner Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the IPreferenceLearner interface for updating user profiles
|
|
5
|
+
* based on interaction signals in the Recommendation Learning Loop.
|
|
6
|
+
*
|
|
7
|
+
* Test Cases:
|
|
8
|
+
* 1. Update profile from single ACCEPT signal
|
|
9
|
+
* 2. Update profile from single DISMISS signal
|
|
10
|
+
* 3. Batch update with 100 signals
|
|
11
|
+
* 4. Weight decay after 30 days
|
|
12
|
+
* 5. Weight bounds enforcement (-2.0 to 2.0)
|
|
13
|
+
* 6. Category weight accumulation
|
|
14
|
+
* 7. Trust tier preference learning
|
|
15
|
+
* 8. Author preference learning
|
|
16
|
+
* 9. Cold start default weights
|
|
17
|
+
* 10. Profile persistence across sessions
|
|
18
|
+
*
|
|
19
|
+
* @see packages/core/src/learning/interfaces.ts
|
|
20
|
+
* @see docs/execution/phase5-testing-execution.md
|
|
21
|
+
*/
|
|
22
|
+
export {};
|
|
23
|
+
//# sourceMappingURL=preference-learner.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preference-learner.test.d.ts","sourceRoot":"","sources":["../../../../tests/integration/neural/preference-learner.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG"}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-1535: Preference Learner Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the IPreferenceLearner interface for updating user profiles
|
|
5
|
+
* based on interaction signals in the Recommendation Learning Loop.
|
|
6
|
+
*
|
|
7
|
+
* Test Cases:
|
|
8
|
+
* 1. Update profile from single ACCEPT signal
|
|
9
|
+
* 2. Update profile from single DISMISS signal
|
|
10
|
+
* 3. Batch update with 100 signals
|
|
11
|
+
* 4. Weight decay after 30 days
|
|
12
|
+
* 5. Weight bounds enforcement (-2.0 to 2.0)
|
|
13
|
+
* 6. Category weight accumulation
|
|
14
|
+
* 7. Trust tier preference learning
|
|
15
|
+
* 8. Author preference learning
|
|
16
|
+
* 9. Cold start default weights
|
|
17
|
+
* 10. Profile persistence across sessions
|
|
18
|
+
*
|
|
19
|
+
* @see packages/core/src/learning/interfaces.ts
|
|
20
|
+
* @see docs/execution/phase5-testing-execution.md
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
23
|
+
import { createNeuralTestContext, cleanupNeuralTestContext, createDefaultProfile, } from './setup.js';
|
|
24
|
+
import { generateSignal } from './helpers.js';
|
|
25
|
+
import { SignalType, SkillCategory, SIGNAL_WEIGHTS, DEFAULT_LEARNING_CONFIG, COLD_START_WEIGHTS, } from '../../../src/learning/types.js';
|
|
26
|
+
describe('PreferenceLearner Integration', () => {
|
|
27
|
+
let ctx;
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
ctx = createNeuralTestContext();
|
|
30
|
+
});
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
await cleanupNeuralTestContext(ctx);
|
|
33
|
+
});
|
|
34
|
+
describe('Single Signal Updates', () => {
|
|
35
|
+
it('should update profile from single ACCEPT signal', async () => {
|
|
36
|
+
const profile = createDefaultProfile();
|
|
37
|
+
const signal = generateSignal({
|
|
38
|
+
type: SignalType.ACCEPT,
|
|
39
|
+
skillId: 'test-skill-1',
|
|
40
|
+
category: SkillCategory.TESTING,
|
|
41
|
+
trustTier: 'verified',
|
|
42
|
+
});
|
|
43
|
+
const updated = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
44
|
+
// Signal count should increment
|
|
45
|
+
expect(updated.signal_count).toBe(1);
|
|
46
|
+
// Category weight should increase (ACCEPT weight is 0.5, learning rate 0.1)
|
|
47
|
+
const expectedCategoryIncrease = SIGNAL_WEIGHTS[SignalType.ACCEPT] * DEFAULT_LEARNING_CONFIG.learning_rate;
|
|
48
|
+
const originalCategoryWeight = COLD_START_WEIGHTS.category_weights[SkillCategory.TESTING] ?? 0;
|
|
49
|
+
expect(updated.category_weights[SkillCategory.TESTING]).toBeCloseTo(originalCategoryWeight + expectedCategoryIncrease, 5);
|
|
50
|
+
// Trust tier weight should increase
|
|
51
|
+
expect(updated.trust_tier_weights['verified']).toBeGreaterThan(profile.trust_tier_weights['verified'] ?? 0);
|
|
52
|
+
// Timestamp should be updated (allow 1 second tolerance for timing)
|
|
53
|
+
const timeDiff = updated.last_updated - profile.last_updated;
|
|
54
|
+
expect(timeDiff).toBeGreaterThanOrEqual(0);
|
|
55
|
+
expect(timeDiff).toBeLessThan(1000);
|
|
56
|
+
});
|
|
57
|
+
it('should update profile from single DISMISS signal', async () => {
|
|
58
|
+
const profile = createDefaultProfile();
|
|
59
|
+
const signal = generateSignal({
|
|
60
|
+
type: SignalType.DISMISS,
|
|
61
|
+
skillId: 'unwanted-skill',
|
|
62
|
+
category: SkillCategory.DEVOPS,
|
|
63
|
+
trustTier: 'experimental',
|
|
64
|
+
});
|
|
65
|
+
const updated = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
66
|
+
// Category weight should decrease (DISMISS weight is -0.3)
|
|
67
|
+
const expectedCategoryDecrease = SIGNAL_WEIGHTS[SignalType.DISMISS] * DEFAULT_LEARNING_CONFIG.learning_rate;
|
|
68
|
+
const originalCategoryWeight = COLD_START_WEIGHTS.category_weights[SkillCategory.DEVOPS] ?? 0;
|
|
69
|
+
expect(updated.category_weights[SkillCategory.DEVOPS]).toBeCloseTo(originalCategoryWeight + expectedCategoryDecrease, 5);
|
|
70
|
+
// Skill should be added to negative patterns
|
|
71
|
+
expect(updated.negative_patterns.skill_ids).toContain('unwanted-skill');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Batch Updates', () => {
|
|
75
|
+
it('should batch update with 100 signals', async () => {
|
|
76
|
+
const profile = createDefaultProfile();
|
|
77
|
+
// Generate 100 signals with varied types and categories
|
|
78
|
+
const signals = [];
|
|
79
|
+
for (let i = 0; i < 100; i++) {
|
|
80
|
+
const types = [
|
|
81
|
+
SignalType.ACCEPT,
|
|
82
|
+
SignalType.DISMISS,
|
|
83
|
+
SignalType.USAGE_DAILY,
|
|
84
|
+
SignalType.USAGE_WEEKLY,
|
|
85
|
+
];
|
|
86
|
+
const categories = Object.values(SkillCategory);
|
|
87
|
+
signals.push(generateSignal({
|
|
88
|
+
type: types[i % types.length],
|
|
89
|
+
skillId: `skill-${i}`,
|
|
90
|
+
category: categories[i % categories.length],
|
|
91
|
+
trustTier: i % 2 === 0 ? 'verified' : 'community',
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
const updated = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
96
|
+
const duration = Date.now() - startTime;
|
|
97
|
+
// Signal count should be 100
|
|
98
|
+
expect(updated.signal_count).toBe(100);
|
|
99
|
+
// Should complete quickly (< 1000ms including test overhead)
|
|
100
|
+
expect(duration).toBeLessThan(1000);
|
|
101
|
+
// Should have updated multiple category weights
|
|
102
|
+
const nonZeroCategoryWeights = Object.values(updated.category_weights).filter((w) => w !== undefined &&
|
|
103
|
+
Math.abs(w - (COLD_START_WEIGHTS.category_weights[SkillCategory.TESTING] ?? 0)) > 0.001);
|
|
104
|
+
expect(nonZeroCategoryWeights.length).toBeGreaterThan(0);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('Weight Decay', () => {
|
|
108
|
+
it('should apply weight decay after 30 days', async () => {
|
|
109
|
+
// Create profile with significant weights
|
|
110
|
+
const profile = createDefaultProfile();
|
|
111
|
+
profile.category_weights = {
|
|
112
|
+
[SkillCategory.TESTING]: 1.5,
|
|
113
|
+
[SkillCategory.GIT]: -1.0,
|
|
114
|
+
[SkillCategory.DEVOPS]: 0.8,
|
|
115
|
+
};
|
|
116
|
+
profile.trust_tier_weights = {
|
|
117
|
+
verified: 1.2,
|
|
118
|
+
community: 0.5,
|
|
119
|
+
};
|
|
120
|
+
profile.keyword_weights = {
|
|
121
|
+
test: 0.9,
|
|
122
|
+
ci: -0.4,
|
|
123
|
+
};
|
|
124
|
+
// Apply decay with default factor (0.95)
|
|
125
|
+
const decayed = await ctx.preferenceLearner.decayWeights(profile);
|
|
126
|
+
// All weights should be reduced by decay factor
|
|
127
|
+
expect(decayed.category_weights[SkillCategory.TESTING]).toBeCloseTo(1.5 * DEFAULT_LEARNING_CONFIG.decay_factor, 5);
|
|
128
|
+
expect(decayed.category_weights[SkillCategory.GIT]).toBeCloseTo(-1.0 * DEFAULT_LEARNING_CONFIG.decay_factor, 5);
|
|
129
|
+
expect(decayed.trust_tier_weights.verified).toBeCloseTo(1.2 * DEFAULT_LEARNING_CONFIG.decay_factor, 5);
|
|
130
|
+
expect(decayed.keyword_weights.test).toBeCloseTo(0.9 * DEFAULT_LEARNING_CONFIG.decay_factor, 5);
|
|
131
|
+
});
|
|
132
|
+
it('should support custom decay factor', async () => {
|
|
133
|
+
const profile = createDefaultProfile();
|
|
134
|
+
profile.category_weights = {
|
|
135
|
+
[SkillCategory.TESTING]: 1.0,
|
|
136
|
+
};
|
|
137
|
+
// Apply aggressive decay (0.8)
|
|
138
|
+
const decayed = await ctx.preferenceLearner.decayWeights(profile, 0.8);
|
|
139
|
+
expect(decayed.category_weights[SkillCategory.TESTING]).toBeCloseTo(0.8, 5);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('Weight Bounds', () => {
|
|
143
|
+
it('should enforce weight bounds (-2.0 to 2.0)', async () => {
|
|
144
|
+
const profile = createDefaultProfile();
|
|
145
|
+
// Generate many ACCEPT signals for the same category to push weight high
|
|
146
|
+
const signals = Array.from({ length: 100 }, () => generateSignal({
|
|
147
|
+
type: SignalType.USAGE_DAILY, // Highest positive weight (1.0)
|
|
148
|
+
category: SkillCategory.TESTING,
|
|
149
|
+
trustTier: 'verified',
|
|
150
|
+
}));
|
|
151
|
+
const updated = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
152
|
+
// Weight should be capped at max bound
|
|
153
|
+
expect(updated.category_weights[SkillCategory.TESTING]).toBeLessThanOrEqual(DEFAULT_LEARNING_CONFIG.weight_bounds.max);
|
|
154
|
+
expect(updated.category_weights[SkillCategory.TESTING]).toBeGreaterThanOrEqual(DEFAULT_LEARNING_CONFIG.weight_bounds.min);
|
|
155
|
+
});
|
|
156
|
+
it('should enforce negative weight bounds', async () => {
|
|
157
|
+
const profile = createDefaultProfile();
|
|
158
|
+
// Generate many UNINSTALL signals to push weight negative
|
|
159
|
+
const signals = Array.from({ length: 100 }, () => generateSignal({
|
|
160
|
+
type: SignalType.UNINSTALL, // Most negative weight (-1.0)
|
|
161
|
+
category: SkillCategory.SECURITY,
|
|
162
|
+
trustTier: 'experimental',
|
|
163
|
+
}));
|
|
164
|
+
const updated = await ctx.preferenceLearner.batchUpdateProfile(profile, signals);
|
|
165
|
+
// Weight should be capped at min bound
|
|
166
|
+
expect(updated.category_weights[SkillCategory.SECURITY]).toBeGreaterThanOrEqual(DEFAULT_LEARNING_CONFIG.weight_bounds.min);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
describe('Category Weight Accumulation', () => {
|
|
170
|
+
it('should accumulate category weights over multiple interactions', async () => {
|
|
171
|
+
let profile = createDefaultProfile();
|
|
172
|
+
const originalWeight = profile.category_weights[SkillCategory.FRONTEND] ?? 0;
|
|
173
|
+
// Simulate 5 accept interactions over time
|
|
174
|
+
for (let i = 0; i < 5; i++) {
|
|
175
|
+
const signal = generateSignal({
|
|
176
|
+
type: SignalType.ACCEPT,
|
|
177
|
+
skillId: `frontend-skill-${i}`,
|
|
178
|
+
category: SkillCategory.FRONTEND,
|
|
179
|
+
});
|
|
180
|
+
profile = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
181
|
+
}
|
|
182
|
+
// Weight should have accumulated
|
|
183
|
+
const expectedIncrease = 5 * SIGNAL_WEIGHTS[SignalType.ACCEPT] * DEFAULT_LEARNING_CONFIG.learning_rate;
|
|
184
|
+
expect(profile.category_weights[SkillCategory.FRONTEND]).toBeCloseTo(Math.min(originalWeight + expectedIncrease, DEFAULT_LEARNING_CONFIG.weight_bounds.max), 5);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('Trust Tier Learning', () => {
|
|
188
|
+
it('should learn trust tier preferences', async () => {
|
|
189
|
+
let profile = createDefaultProfile();
|
|
190
|
+
// User consistently accepts verified skills
|
|
191
|
+
for (let i = 0; i < 10; i++) {
|
|
192
|
+
const signal = generateSignal({
|
|
193
|
+
type: SignalType.ACCEPT,
|
|
194
|
+
skillId: `verified-skill-${i}`,
|
|
195
|
+
trustTier: 'verified',
|
|
196
|
+
});
|
|
197
|
+
profile = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
198
|
+
}
|
|
199
|
+
// User consistently dismisses experimental skills
|
|
200
|
+
for (let i = 0; i < 10; i++) {
|
|
201
|
+
const signal = generateSignal({
|
|
202
|
+
type: SignalType.DISMISS,
|
|
203
|
+
skillId: `experimental-skill-${i}`,
|
|
204
|
+
trustTier: 'experimental',
|
|
205
|
+
});
|
|
206
|
+
profile = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
207
|
+
}
|
|
208
|
+
// Verified should have higher weight than experimental
|
|
209
|
+
expect(profile.trust_tier_weights['verified']).toBeGreaterThan(profile.trust_tier_weights['experimental']);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('Author Preference Learning', () => {
|
|
213
|
+
it('should track negative patterns for dismissed authors', async () => {
|
|
214
|
+
let profile = createDefaultProfile();
|
|
215
|
+
// Dismiss skills from a specific author multiple times
|
|
216
|
+
const authorSkills = ['author-x/skill-1', 'author-x/skill-2', 'author-x/skill-3'];
|
|
217
|
+
for (const skillId of authorSkills) {
|
|
218
|
+
const signal = generateSignal({
|
|
219
|
+
type: SignalType.DISMISS,
|
|
220
|
+
skillId,
|
|
221
|
+
});
|
|
222
|
+
profile = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
223
|
+
}
|
|
224
|
+
// All dismissed skills should be in negative patterns
|
|
225
|
+
for (const skillId of authorSkills) {
|
|
226
|
+
expect(profile.negative_patterns.skill_ids).toContain(skillId);
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe('Cold Start', () => {
|
|
231
|
+
it('should use cold start default weights for new users', () => {
|
|
232
|
+
const profile = createDefaultProfile();
|
|
233
|
+
// Should have default category weights
|
|
234
|
+
expect(profile.category_weights[SkillCategory.TESTING]).toBe(COLD_START_WEIGHTS.category_weights[SkillCategory.TESTING]);
|
|
235
|
+
expect(profile.category_weights[SkillCategory.GIT]).toBe(COLD_START_WEIGHTS.category_weights[SkillCategory.GIT]);
|
|
236
|
+
// Should have default trust tier weights
|
|
237
|
+
expect(profile.trust_tier_weights['verified']).toBe(COLD_START_WEIGHTS.trust_tier_weights['verified']);
|
|
238
|
+
expect(profile.trust_tier_weights['community']).toBe(COLD_START_WEIGHTS.trust_tier_weights['community']);
|
|
239
|
+
// Signal count should be 0
|
|
240
|
+
expect(profile.signal_count).toBe(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('Profile Persistence', () => {
|
|
244
|
+
it('should persist profile across save/load cycles', async () => {
|
|
245
|
+
// Create and modify profile
|
|
246
|
+
let profile = createDefaultProfile();
|
|
247
|
+
for (let i = 0; i < 10; i++) {
|
|
248
|
+
const signal = generateSignal({
|
|
249
|
+
type: SignalType.ACCEPT,
|
|
250
|
+
skillId: `skill-${i}`,
|
|
251
|
+
category: SkillCategory.BACKEND,
|
|
252
|
+
});
|
|
253
|
+
profile = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
254
|
+
}
|
|
255
|
+
// Save profile
|
|
256
|
+
await ctx.profileRepository.saveProfile(profile, 'test-user');
|
|
257
|
+
// Load profile
|
|
258
|
+
const loaded = await ctx.profileRepository.getProfile('test-user');
|
|
259
|
+
expect(loaded).not.toBeNull();
|
|
260
|
+
expect(loaded.signal_count).toBe(10);
|
|
261
|
+
expect(loaded.category_weights[SkillCategory.BACKEND]).toBeCloseTo(profile.category_weights[SkillCategory.BACKEND], 5);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
describe('Configuration', () => {
|
|
265
|
+
it('should use configurable learning rate', async () => {
|
|
266
|
+
// Set a custom learning rate
|
|
267
|
+
ctx.preferenceLearner.setConfig({ learning_rate: 0.5 });
|
|
268
|
+
const profile = createDefaultProfile();
|
|
269
|
+
const signal = generateSignal({
|
|
270
|
+
type: SignalType.ACCEPT,
|
|
271
|
+
skillId: 'test',
|
|
272
|
+
category: SkillCategory.DATABASE,
|
|
273
|
+
});
|
|
274
|
+
const updated = await ctx.preferenceLearner.updateProfile(profile, signal);
|
|
275
|
+
// Weight increase should reflect higher learning rate
|
|
276
|
+
const expectedIncrease = SIGNAL_WEIGHTS[SignalType.ACCEPT] * 0.5;
|
|
277
|
+
const originalWeight = COLD_START_WEIGHTS.category_weights[SkillCategory.DATABASE] ?? 0;
|
|
278
|
+
expect(updated.category_weights[SkillCategory.DATABASE]).toBeCloseTo(originalWeight + expectedIncrease, 5);
|
|
279
|
+
});
|
|
280
|
+
it('should return current config', () => {
|
|
281
|
+
const config = ctx.preferenceLearner.getConfig();
|
|
282
|
+
expect(config.learning_rate).toBe(DEFAULT_LEARNING_CONFIG.learning_rate);
|
|
283
|
+
expect(config.decay_factor).toBe(DEFAULT_LEARNING_CONFIG.decay_factor);
|
|
284
|
+
expect(config.min_signals_threshold).toBe(DEFAULT_LEARNING_CONFIG.min_signals_threshold);
|
|
285
|
+
expect(config.weight_bounds).toEqual(DEFAULT_LEARNING_CONFIG.weight_bounds);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
//# sourceMappingURL=preference-learner.test.js.map
|