@skillsmith/core 0.4.14 → 0.4.15
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/README.md +2 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/api/client.d.ts +1 -0
- package/dist/src/api/client.d.ts.map +1 -1
- package/dist/src/api/client.health.d.ts +26 -0
- package/dist/src/api/client.health.d.ts.map +1 -0
- package/dist/src/api/client.health.js +74 -0
- package/dist/src/api/client.health.js.map +1 -0
- package/dist/src/api/client.js +3 -51
- package/dist/src/api/client.js.map +1 -1
- package/dist/src/db/createDatabase.d.ts +9 -1
- package/dist/src/db/createDatabase.d.ts.map +1 -1
- package/dist/src/db/createDatabase.js +9 -1
- package/dist/src/db/createDatabase.js.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.d.ts.map +1 -1
- package/dist/src/db/drivers/sqljsDriver.js +5 -2
- package/dist/src/db/drivers/sqljsDriver.js.map +1 -1
- package/dist/src/db/migrations/v5b-change-type.d.ts +19 -0
- package/dist/src/db/migrations/v5b-change-type.d.ts.map +1 -0
- package/dist/src/db/migrations/v5b-change-type.js +22 -0
- package/dist/src/db/migrations/v5b-change-type.js.map +1 -0
- package/dist/src/db/migrations/v6-advisories.d.ts +32 -0
- package/dist/src/db/migrations/v6-advisories.d.ts.map +1 -0
- package/dist/src/db/migrations/v6-advisories.js +53 -0
- package/dist/src/db/migrations/v6-advisories.js.map +1 -0
- package/dist/src/db/migrations/v7-compatibility.d.ts +21 -0
- package/dist/src/db/migrations/v7-compatibility.d.ts.map +1 -0
- package/dist/src/db/migrations/v7-compatibility.js +23 -0
- package/dist/src/db/migrations/v7-compatibility.js.map +1 -0
- package/dist/src/db/migrations/v8-co-installs.d.ts +21 -0
- package/dist/src/db/migrations/v8-co-installs.d.ts.map +1 -0
- package/dist/src/db/migrations/v8-co-installs.js +33 -0
- package/dist/src/db/migrations/v8-co-installs.js.map +1 -0
- package/dist/src/db/schema.d.ts +5 -16
- package/dist/src/db/schema.d.ts.map +1 -1
- package/dist/src/db/schema.js +37 -22
- package/dist/src/db/schema.js.map +1 -1
- package/dist/src/exports/repositories.d.ts +5 -0
- package/dist/src/exports/repositories.d.ts.map +1 -1
- package/dist/src/exports/repositories.js +17 -0
- package/dist/src/exports/repositories.js.map +1 -1
- package/dist/src/exports/types.d.ts +1 -1
- package/dist/src/exports/types.d.ts.map +1 -1
- package/dist/src/exports/types.js.map +1 -1
- package/dist/src/indexer/SkillParser.d.ts +2 -0
- package/dist/src/indexer/SkillParser.d.ts.map +1 -1
- package/dist/src/indexer/SkillParser.js.map +1 -1
- package/dist/src/learning/PatternStore.d.ts +0 -2
- package/dist/src/learning/PatternStore.d.ts.map +1 -1
- package/dist/src/learning/PatternStore.helpers.d.ts +19 -1
- package/dist/src/learning/PatternStore.helpers.d.ts.map +1 -1
- package/dist/src/learning/PatternStore.helpers.js +51 -0
- package/dist/src/learning/PatternStore.helpers.js.map +1 -1
- package/dist/src/learning/PatternStore.js +5 -37
- package/dist/src/learning/PatternStore.js.map +1 -1
- package/dist/src/repositories/AdvisoryRepository.d.ts +80 -0
- package/dist/src/repositories/AdvisoryRepository.d.ts.map +1 -0
- package/dist/src/repositories/AdvisoryRepository.js +128 -0
- package/dist/src/repositories/AdvisoryRepository.js.map +1 -0
- package/dist/src/repositories/AdvisoryRepository.test.d.ts +6 -0
- package/dist/src/repositories/AdvisoryRepository.test.d.ts.map +1 -0
- package/dist/src/repositories/AdvisoryRepository.test.js +149 -0
- package/dist/src/repositories/AdvisoryRepository.test.js.map +1 -0
- package/dist/src/repositories/CoInstallRepository.d.ts +68 -0
- package/dist/src/repositories/CoInstallRepository.d.ts.map +1 -0
- package/dist/src/repositories/CoInstallRepository.js +122 -0
- package/dist/src/repositories/CoInstallRepository.js.map +1 -0
- package/dist/src/repositories/SkillRepository.d.ts.map +1 -1
- package/dist/src/repositories/SkillRepository.js +16 -5
- package/dist/src/repositories/SkillRepository.js.map +1 -1
- package/dist/src/services/SearchService.helpers.d.ts.map +1 -1
- package/dist/src/services/SearchService.helpers.js +4 -0
- package/dist/src/services/SearchService.helpers.js.map +1 -1
- package/dist/src/services/SearchService.types.d.ts +1 -0
- package/dist/src/services/SearchService.types.d.ts.map +1 -1
- package/dist/src/services/TaskRunner.d.ts +5 -81
- package/dist/src/services/TaskRunner.d.ts.map +1 -1
- package/dist/src/services/TaskRunner.js +7 -68
- package/dist/src/services/TaskRunner.js.map +1 -1
- package/dist/src/services/TaskRunner.process.d.ts +33 -0
- package/dist/src/services/TaskRunner.process.d.ts.map +1 -0
- package/dist/src/services/TaskRunner.process.js +70 -0
- package/dist/src/services/TaskRunner.process.js.map +1 -0
- package/dist/src/services/TaskRunner.types.d.ts +76 -0
- package/dist/src/services/TaskRunner.types.d.ts.map +1 -0
- package/dist/src/services/TaskRunner.types.js +22 -0
- package/dist/src/services/TaskRunner.types.js.map +1 -0
- package/dist/src/services/__tests__/TaskRunner.process.test.d.ts +13 -0
- package/dist/src/services/__tests__/TaskRunner.process.test.d.ts.map +1 -0
- package/dist/src/services/__tests__/TaskRunner.process.test.js +91 -0
- package/dist/src/services/__tests__/TaskRunner.process.test.js.map +1 -0
- package/dist/src/services/quarantine/QuarantineService.d.ts +3 -24
- package/dist/src/services/quarantine/QuarantineService.d.ts.map +1 -1
- package/dist/src/services/quarantine/QuarantineService.js +8 -205
- package/dist/src/services/quarantine/QuarantineService.js.map +1 -1
- package/dist/src/services/quarantine/QuarantineService.multiapproval.d.ts +57 -0
- package/dist/src/services/quarantine/QuarantineService.multiapproval.d.ts.map +1 -0
- package/dist/src/services/quarantine/QuarantineService.multiapproval.js +211 -0
- package/dist/src/services/quarantine/QuarantineService.multiapproval.js.map +1 -0
- package/dist/src/session/SessionManager.d.ts +2 -33
- package/dist/src/session/SessionManager.d.ts.map +1 -1
- package/dist/src/session/SessionManager.js +14 -238
- package/dist/src/session/SessionManager.js.map +1 -1
- package/dist/src/session/SessionManager.memory.d.ts +67 -0
- package/dist/src/session/SessionManager.memory.d.ts.map +1 -0
- package/dist/src/session/SessionManager.memory.js +262 -0
- package/dist/src/session/SessionManager.memory.js.map +1 -0
- package/dist/src/sync/SyncEngine.d.ts +13 -1
- package/dist/src/sync/SyncEngine.d.ts.map +1 -1
- package/dist/src/sync/SyncEngine.js +50 -1
- package/dist/src/sync/SyncEngine.js.map +1 -1
- package/dist/src/testing/MultiLLMProvider.d.ts +5 -6
- package/dist/src/testing/MultiLLMProvider.d.ts.map +1 -1
- package/dist/src/testing/MultiLLMProvider.js +18 -130
- package/dist/src/testing/MultiLLMProvider.js.map +1 -1
- package/dist/src/testing/MultiLLMProvider.metrics.d.ts +33 -0
- package/dist/src/testing/MultiLLMProvider.metrics.d.ts.map +1 -0
- package/dist/src/testing/MultiLLMProvider.metrics.js +87 -0
- package/dist/src/testing/MultiLLMProvider.metrics.js.map +1 -0
- package/dist/src/testing/MultiLLMProvider.selection.d.ts +77 -0
- package/dist/src/testing/MultiLLMProvider.selection.d.ts.map +1 -0
- package/dist/src/testing/MultiLLMProvider.selection.js +151 -0
- package/dist/src/testing/MultiLLMProvider.selection.js.map +1 -0
- package/dist/src/types/skill.d.ts +10 -0
- package/dist/src/types/skill.d.ts.map +1 -1
- package/dist/src/types.d.ts +30 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/versioning/change-classifier.d.ts +38 -0
- package/dist/src/versioning/change-classifier.d.ts.map +1 -0
- package/dist/src/versioning/change-classifier.js +187 -0
- package/dist/src/versioning/change-classifier.js.map +1 -0
- package/dist/src/versioning/change-classifier.test.d.ts +6 -0
- package/dist/src/versioning/change-classifier.test.d.ts.map +1 -0
- package/dist/src/versioning/change-classifier.test.js +275 -0
- package/dist/src/versioning/change-classifier.test.js.map +1 -0
- package/dist/src/versioning/update-risk.d.ts +50 -0
- package/dist/src/versioning/update-risk.d.ts.map +1 -0
- package/dist/src/versioning/update-risk.js +80 -0
- package/dist/src/versioning/update-risk.js.map +1 -0
- package/dist/src/versioning/update-risk.test.d.ts +6 -0
- package/dist/src/versioning/update-risk.test.d.ts.map +1 -0
- package/dist/src/versioning/update-risk.test.js +200 -0
- package/dist/src/versioning/update-risk.test.js.map +1 -0
- package/dist/tests/AuditLogger.edge-cases.test.d.ts +10 -0
- package/dist/tests/AuditLogger.edge-cases.test.d.ts.map +1 -0
- package/dist/tests/AuditLogger.edge-cases.test.js +183 -0
- package/dist/tests/AuditLogger.edge-cases.test.js.map +1 -0
- package/dist/tests/CacheManager.test.d.ts +9 -0
- package/dist/tests/CacheManager.test.d.ts.map +1 -0
- package/dist/tests/CacheManager.test.js +163 -0
- package/dist/tests/CacheManager.test.js.map +1 -0
- package/dist/tests/GitHubIndexer.edge-cases.test.d.ts +10 -0
- package/dist/tests/GitHubIndexer.edge-cases.test.d.ts.map +1 -0
- package/dist/tests/GitHubIndexer.edge-cases.test.js +255 -0
- package/dist/tests/GitHubIndexer.edge-cases.test.js.map +1 -0
- package/dist/tests/SearchService.test.js +71 -0
- package/dist/tests/SearchService.test.js.map +1 -1
- package/dist/tests/SkillVersionRepository.test.d.ts +0 -11
- package/dist/tests/SkillVersionRepository.test.d.ts.map +1 -1
- package/dist/tests/SkillVersionRepository.test.js +131 -194
- package/dist/tests/SkillVersionRepository.test.js.map +1 -1
- package/dist/tests/api/client.health.test.d.ts +15 -0
- package/dist/tests/api/client.health.test.d.ts.map +1 -0
- package/dist/tests/api/client.health.test.js +111 -0
- package/dist/tests/api/client.health.test.js.map +1 -0
- package/dist/tests/db/betterSqlite3Driver.test.d.ts +8 -0
- package/dist/tests/db/betterSqlite3Driver.test.d.ts.map +1 -0
- package/dist/tests/db/betterSqlite3Driver.test.js +88 -0
- package/dist/tests/db/betterSqlite3Driver.test.js.map +1 -0
- package/dist/tests/db/database-abstraction.test.js +12 -0
- package/dist/tests/db/database-abstraction.test.js.map +1 -1
- package/dist/tests/db/schema-migrations.test.d.ts +10 -0
- package/dist/tests/db/schema-migrations.test.d.ts.map +1 -0
- package/dist/tests/db/schema-migrations.test.js +134 -0
- package/dist/tests/db/schema-migrations.test.js.map +1 -0
- package/dist/tests/db/sqljsDriver.test.d.ts +9 -0
- package/dist/tests/db/sqljsDriver.test.d.ts.map +1 -0
- package/dist/tests/db/sqljsDriver.test.js +75 -0
- package/dist/tests/db/sqljsDriver.test.js.map +1 -0
- package/dist/tests/helpers/database.d.ts +32 -0
- package/dist/tests/helpers/database.d.ts.map +1 -0
- package/dist/tests/helpers/database.js +54 -0
- package/dist/tests/helpers/database.js.map +1 -0
- package/dist/tests/learning/PatternStore.helpers.test.d.ts +17 -0
- package/dist/tests/learning/PatternStore.helpers.test.d.ts.map +1 -0
- package/dist/tests/learning/PatternStore.helpers.test.js +301 -0
- package/dist/tests/learning/PatternStore.helpers.test.js.map +1 -0
- package/dist/tests/repositories/CoInstallRepository.test.d.ts +9 -0
- package/dist/tests/repositories/CoInstallRepository.test.d.ts.map +1 -0
- package/dist/tests/repositories/CoInstallRepository.test.js +126 -0
- package/dist/tests/repositories/CoInstallRepository.test.js.map +1 -0
- package/dist/tests/routing/LanguageRouter.test.d.ts +9 -0
- package/dist/tests/routing/LanguageRouter.test.d.ts.map +1 -0
- package/dist/tests/routing/LanguageRouter.test.js +150 -0
- package/dist/tests/routing/LanguageRouter.test.js.map +1 -0
- package/dist/tests/session/SessionManager.memory.test.d.ts +11 -0
- package/dist/tests/session/SessionManager.memory.test.d.ts.map +1 -0
- package/dist/tests/session/SessionManager.memory.test.js +219 -0
- package/dist/tests/session/SessionManager.memory.test.js.map +1 -0
- package/dist/tests/sync/SyncEngine.test.js +32 -18
- package/dist/tests/sync/SyncEngine.test.js.map +1 -1
- package/dist/tests/testing/MultiLLMProvider.metrics.test.d.ts +13 -0
- package/dist/tests/testing/MultiLLMProvider.metrics.test.d.ts.map +1 -0
- package/dist/tests/testing/MultiLLMProvider.metrics.test.js +149 -0
- package/dist/tests/testing/MultiLLMProvider.metrics.test.js.map +1 -0
- package/dist/tests/testing/MultiLLMProvider.selection.test.d.ts +15 -0
- package/dist/tests/testing/MultiLLMProvider.selection.test.d.ts.map +1 -0
- package/dist/tests/testing/MultiLLMProvider.selection.test.js +249 -0
- package/dist/tests/testing/MultiLLMProvider.selection.test.js.map +1 -0
- package/dist/tests/unit/quarantine-query-builder.test.d.ts +10 -0
- package/dist/tests/unit/quarantine-query-builder.test.d.ts.map +1 -0
- package/dist/tests/unit/quarantine-query-builder.test.js +157 -0
- package/dist/tests/unit/quarantine-query-builder.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-2754: PatternStore Helper Functions Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests covering pure math functions, FisherInformationMatrix,
|
|
5
|
+
* and DB-touching helpers extracted to PatternStore.helpers.ts.
|
|
6
|
+
*
|
|
7
|
+
* Categories:
|
|
8
|
+
* - FisherInformationMatrix: construct, update, accumulate, decay,
|
|
9
|
+
* serialize/deserialize round-trip, reset, invalid buffer size
|
|
10
|
+
* - contextToText: empty, partial, full context
|
|
11
|
+
* - computeGradient, cosineSimilarity, importanceWeightedSimilarity (zero-norm edge)
|
|
12
|
+
* - calculatePatternImportance: positive and negative reward
|
|
13
|
+
* - shouldConsolidate: too recent, no patterns, ratio threshold, near max patterns
|
|
14
|
+
* - computeAverageEmbedding: empty table → zero vector, populated → average
|
|
15
|
+
*/
|
|
16
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
17
|
+
import { FisherInformationMatrix, contextToText, computeGradient, cosineSimilarity, importanceWeightedSimilarity, calculatePatternImportance, shouldConsolidate, computeAverageEmbedding, PATTERN_STORE_SCHEMA, } from '../../src/learning/PatternStore.helpers.js';
|
|
18
|
+
import { createDatabase } from '../../src/db/createDatabase.js';
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// FisherInformationMatrix
|
|
21
|
+
// ============================================================================
|
|
22
|
+
describe('FisherInformationMatrix', () => {
|
|
23
|
+
it('constructs with zero importance vector', () => {
|
|
24
|
+
const fim = new FisherInformationMatrix(4);
|
|
25
|
+
expect(fim.getImportanceVector()).toEqual(new Float32Array(4));
|
|
26
|
+
expect(fim.getUpdateCount()).toBe(0);
|
|
27
|
+
expect(fim.getAverageImportance()).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
it('updates importance from a gradient', () => {
|
|
30
|
+
const fim = new FisherInformationMatrix(3);
|
|
31
|
+
const gradient = new Float32Array([1.0, 2.0, 0.0]);
|
|
32
|
+
fim.update(gradient);
|
|
33
|
+
expect(fim.getUpdateCount()).toBe(1);
|
|
34
|
+
// importance = runningSum / updateCount = [1, 4, 0] / 1
|
|
35
|
+
expect(fim.getImportance(0)).toBeCloseTo(1.0);
|
|
36
|
+
expect(fim.getImportance(1)).toBeCloseTo(4.0);
|
|
37
|
+
expect(fim.getImportance(2)).toBeCloseTo(0.0);
|
|
38
|
+
});
|
|
39
|
+
it('accumulates importance over multiple updates', () => {
|
|
40
|
+
const fim = new FisherInformationMatrix(2);
|
|
41
|
+
fim.update(new Float32Array([2.0, 0.0]));
|
|
42
|
+
fim.update(new Float32Array([2.0, 0.0]));
|
|
43
|
+
// runningSum = [4+4, 0] = [8, 0]; importance = [8/2, 0/2] = [4, 0]
|
|
44
|
+
expect(fim.getImportance(0)).toBeCloseTo(4.0);
|
|
45
|
+
expect(fim.getUpdateCount()).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
it('decays importance by decayFactor', () => {
|
|
48
|
+
const fim = new FisherInformationMatrix(2);
|
|
49
|
+
fim.update(new Float32Array([2.0, 4.0]));
|
|
50
|
+
// After update: runningSum=[4, 16], importance=[4, 16]
|
|
51
|
+
fim.decay(0.5);
|
|
52
|
+
// runningSum=[2, 8]; importance = [2, 8] / max(1, 1) = [2, 8]
|
|
53
|
+
expect(fim.getImportance(0)).toBeCloseTo(2.0);
|
|
54
|
+
expect(fim.getImportance(1)).toBeCloseTo(8.0);
|
|
55
|
+
});
|
|
56
|
+
it('serialize and deserialize round-trip preserves values', () => {
|
|
57
|
+
const fim = new FisherInformationMatrix(3);
|
|
58
|
+
fim.update(new Float32Array([1.0, 2.0, 3.0]));
|
|
59
|
+
fim.update(new Float32Array([0.5, 1.0, 1.5]));
|
|
60
|
+
const buffer = fim.serialize();
|
|
61
|
+
const fim2 = new FisherInformationMatrix(3);
|
|
62
|
+
fim2.deserialize(buffer);
|
|
63
|
+
expect(fim2.getUpdateCount()).toBe(2);
|
|
64
|
+
// Values should match within float32 precision
|
|
65
|
+
for (let i = 0; i < 3; i++) {
|
|
66
|
+
expect(fim2.getImportance(i)).toBeCloseTo(fim.getImportance(i), 4);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it('reset clears all values to zero', () => {
|
|
70
|
+
const fim = new FisherInformationMatrix(3);
|
|
71
|
+
fim.update(new Float32Array([1.0, 2.0, 3.0]));
|
|
72
|
+
fim.reset();
|
|
73
|
+
expect(fim.getUpdateCount()).toBe(0);
|
|
74
|
+
expect(fim.getAverageImportance()).toBe(0);
|
|
75
|
+
expect(fim.getImportance(0)).toBe(0);
|
|
76
|
+
});
|
|
77
|
+
it('throws when deserializing buffer that is too small', () => {
|
|
78
|
+
const fim = new FisherInformationMatrix(4);
|
|
79
|
+
const tooSmall = Buffer.alloc(4); // way smaller than needed
|
|
80
|
+
expect(() => fim.deserialize(tooSmall)).toThrow(/Invalid Fisher matrix buffer/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// contextToText
|
|
85
|
+
// ============================================================================
|
|
86
|
+
describe('contextToText', () => {
|
|
87
|
+
it('returns "empty context" for an empty context object', () => {
|
|
88
|
+
const ctx = { installedSkills: [] };
|
|
89
|
+
expect(contextToText(ctx)).toBe('empty context');
|
|
90
|
+
});
|
|
91
|
+
it('returns partial text when only some fields are set', () => {
|
|
92
|
+
const ctx = {
|
|
93
|
+
installedSkills: ['anthropic/commit'],
|
|
94
|
+
timeOfDay: 'morning',
|
|
95
|
+
};
|
|
96
|
+
const text = contextToText(ctx);
|
|
97
|
+
expect(text).toContain('installed: anthropic/commit');
|
|
98
|
+
expect(text).toContain('time: morning');
|
|
99
|
+
expect(text).not.toContain('frameworks');
|
|
100
|
+
});
|
|
101
|
+
it('returns all fields when all are set', () => {
|
|
102
|
+
const ctx = {
|
|
103
|
+
installedSkills: ['anthropic/commit', 'community/jest'],
|
|
104
|
+
frameworks: ['react', 'typescript'],
|
|
105
|
+
keywords: ['testing'],
|
|
106
|
+
timeOfDay: 'afternoon',
|
|
107
|
+
dayType: 'weekday',
|
|
108
|
+
};
|
|
109
|
+
const text = contextToText(ctx);
|
|
110
|
+
expect(text).toContain('installed: anthropic/commit, community/jest');
|
|
111
|
+
expect(text).toContain('frameworks: react, typescript');
|
|
112
|
+
expect(text).toContain('keywords: testing');
|
|
113
|
+
expect(text).toContain('time: afternoon');
|
|
114
|
+
expect(text).toContain('day: weekday');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// computeGradient
|
|
119
|
+
// ============================================================================
|
|
120
|
+
describe('computeGradient', () => {
|
|
121
|
+
it('computes element-wise difference a - b', () => {
|
|
122
|
+
const a = new Float32Array([3.0, 1.0, 2.0]);
|
|
123
|
+
const b = new Float32Array([1.0, 2.0, 1.0]);
|
|
124
|
+
const gradient = computeGradient(a, b);
|
|
125
|
+
expect(gradient[0]).toBeCloseTo(2.0);
|
|
126
|
+
expect(gradient[1]).toBeCloseTo(-1.0);
|
|
127
|
+
expect(gradient[2]).toBeCloseTo(1.0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// cosineSimilarity
|
|
132
|
+
// ============================================================================
|
|
133
|
+
describe('cosineSimilarity', () => {
|
|
134
|
+
it('returns 1.0 for identical non-zero vectors', () => {
|
|
135
|
+
const a = new Float32Array([1.0, 0.0, 0.0]);
|
|
136
|
+
expect(cosineSimilarity(a, a)).toBeCloseTo(1.0);
|
|
137
|
+
});
|
|
138
|
+
it('returns 0 for orthogonal vectors', () => {
|
|
139
|
+
const a = new Float32Array([1.0, 0.0]);
|
|
140
|
+
const b = new Float32Array([0.0, 1.0]);
|
|
141
|
+
expect(cosineSimilarity(a, b)).toBeCloseTo(0.0);
|
|
142
|
+
});
|
|
143
|
+
it('returns 0 when either vector is all zeros (zero-norm edge case)', () => {
|
|
144
|
+
const zero = new Float32Array([0.0, 0.0]);
|
|
145
|
+
const a = new Float32Array([1.0, 2.0]);
|
|
146
|
+
expect(cosineSimilarity(zero, a)).toBe(0);
|
|
147
|
+
expect(cosineSimilarity(a, zero)).toBe(0);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// importanceWeightedSimilarity
|
|
152
|
+
// ============================================================================
|
|
153
|
+
describe('importanceWeightedSimilarity', () => {
|
|
154
|
+
it('computes weighted similarity between two identical vectors', () => {
|
|
155
|
+
const a = new Float32Array([1.0, 0.0]);
|
|
156
|
+
const importance = new Float32Array([1.0, 0.0]);
|
|
157
|
+
const result = importanceWeightedSimilarity(a, a, importance);
|
|
158
|
+
expect(result).toBeCloseTo(1.0);
|
|
159
|
+
});
|
|
160
|
+
it('returns 0 when the weighted norms are zero (zero-norm edge case)', () => {
|
|
161
|
+
const zero = new Float32Array([0.0, 0.0]);
|
|
162
|
+
const importance = new Float32Array([1.0, 1.0]);
|
|
163
|
+
const result = importanceWeightedSimilarity(zero, zero, importance);
|
|
164
|
+
expect(result).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
// ============================================================================
|
|
168
|
+
// calculatePatternImportance
|
|
169
|
+
// ============================================================================
|
|
170
|
+
function makeStoredPattern(reward) {
|
|
171
|
+
return {
|
|
172
|
+
id: 'test-id',
|
|
173
|
+
context: { installedSkills: [] },
|
|
174
|
+
skill: {
|
|
175
|
+
skillId: 'community/test',
|
|
176
|
+
category: 'testing',
|
|
177
|
+
trustTier: 'community',
|
|
178
|
+
keywords: [],
|
|
179
|
+
qualityScore: 80,
|
|
180
|
+
installCount: 100,
|
|
181
|
+
},
|
|
182
|
+
originalScore: 0.8,
|
|
183
|
+
source: 'recommend',
|
|
184
|
+
contextEmbedding: new Float32Array([0.1, 0.2]),
|
|
185
|
+
outcome: { type: reward >= 0 ? 'accept' : 'dismiss', reward },
|
|
186
|
+
importance: 0.5,
|
|
187
|
+
accessCount: 3,
|
|
188
|
+
createdAt: new Date(),
|
|
189
|
+
lastAccessedAt: new Date(),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
describe('calculatePatternImportance', () => {
|
|
193
|
+
it('gives higher importance for positive reward (1.5x multiplier)', () => {
|
|
194
|
+
const pattern = makeStoredPattern(1.0);
|
|
195
|
+
const outcome = { type: 'accept', reward: 1.0 };
|
|
196
|
+
const importance = calculatePatternImportance(pattern, outcome);
|
|
197
|
+
// baseImportance = 1.0 * 1.5 = 1.5, times other factors
|
|
198
|
+
expect(importance).toBeGreaterThan(0);
|
|
199
|
+
});
|
|
200
|
+
it('gives lower importance for negative reward (no 1.5x multiplier)', () => {
|
|
201
|
+
const positivePattern = makeStoredPattern(1.0);
|
|
202
|
+
const negativePattern = makeStoredPattern(-0.5);
|
|
203
|
+
const positiveOutcome = { type: 'accept', reward: 1.0 };
|
|
204
|
+
const negativeOutcome = { type: 'dismiss', reward: -0.5 };
|
|
205
|
+
const importancePos = calculatePatternImportance(positivePattern, positiveOutcome);
|
|
206
|
+
const importanceNeg = calculatePatternImportance(negativePattern, negativeOutcome);
|
|
207
|
+
// positive reward has 1.5x boost, so its importance should be higher
|
|
208
|
+
expect(importancePos).toBeGreaterThan(importanceNeg);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// ============================================================================
|
|
212
|
+
// shouldConsolidate
|
|
213
|
+
// ============================================================================
|
|
214
|
+
function makeConsolidationState(overrides = {}) {
|
|
215
|
+
return {
|
|
216
|
+
totalPatterns: 100,
|
|
217
|
+
patternsSinceLastConsolidation: 5,
|
|
218
|
+
lastConsolidation: null,
|
|
219
|
+
...overrides,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
const ewcConfig = {
|
|
223
|
+
lambda: 5.0,
|
|
224
|
+
fisherDecay: 0.95,
|
|
225
|
+
importanceThreshold: 0.01,
|
|
226
|
+
fisherSampleSize: 100,
|
|
227
|
+
consolidationThreshold: 0.1,
|
|
228
|
+
maxPatterns: 1000,
|
|
229
|
+
};
|
|
230
|
+
describe('shouldConsolidate', () => {
|
|
231
|
+
it('returns false when last consolidation was less than 1 hour ago', () => {
|
|
232
|
+
const state = makeConsolidationState({
|
|
233
|
+
lastConsolidation: new Date(Date.now() - 30 * 60 * 1000), // 30 min ago
|
|
234
|
+
totalPatterns: 200,
|
|
235
|
+
patternsSinceLastConsolidation: 50,
|
|
236
|
+
});
|
|
237
|
+
expect(shouldConsolidate(state, ewcConfig)).toBe(false);
|
|
238
|
+
});
|
|
239
|
+
it('returns false when totalPatterns is 0', () => {
|
|
240
|
+
const state = makeConsolidationState({ totalPatterns: 0, patternsSinceLastConsolidation: 0 });
|
|
241
|
+
expect(shouldConsolidate(state, ewcConfig)).toBe(false);
|
|
242
|
+
});
|
|
243
|
+
it('returns true when newPatternsRatio exceeds consolidationThreshold', () => {
|
|
244
|
+
// 20/100 = 0.2 > 0.1 threshold
|
|
245
|
+
const state = makeConsolidationState({
|
|
246
|
+
totalPatterns: 100,
|
|
247
|
+
patternsSinceLastConsolidation: 20,
|
|
248
|
+
lastConsolidation: new Date(Date.now() - 2 * 60 * 60 * 1000), // 2 hours ago
|
|
249
|
+
});
|
|
250
|
+
expect(shouldConsolidate(state, ewcConfig)).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
it('returns true when totalPatterns > 90% of maxPatterns', () => {
|
|
253
|
+
const state = makeConsolidationState({
|
|
254
|
+
totalPatterns: 950, // > 1000 * 0.9 = 900
|
|
255
|
+
patternsSinceLastConsolidation: 5, // ratio = 5/950 < 0.1
|
|
256
|
+
lastConsolidation: new Date(Date.now() - 2 * 60 * 60 * 1000),
|
|
257
|
+
});
|
|
258
|
+
expect(shouldConsolidate(state, ewcConfig)).toBe(true);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
// ============================================================================
|
|
262
|
+
// computeAverageEmbedding (requires in-memory SQLite)
|
|
263
|
+
// ============================================================================
|
|
264
|
+
describe('computeAverageEmbedding', () => {
|
|
265
|
+
let db;
|
|
266
|
+
beforeEach(() => {
|
|
267
|
+
db = createDatabase(':memory:');
|
|
268
|
+
db.exec(PATTERN_STORE_SCHEMA);
|
|
269
|
+
});
|
|
270
|
+
afterEach(() => {
|
|
271
|
+
db.close();
|
|
272
|
+
});
|
|
273
|
+
it('returns zero vector when the patterns table is empty', async () => {
|
|
274
|
+
const result = await computeAverageEmbedding(db, 100, 4);
|
|
275
|
+
expect(result).toEqual(new Float32Array(4));
|
|
276
|
+
});
|
|
277
|
+
it('returns the average of all stored embeddings', async () => {
|
|
278
|
+
const dims = 4;
|
|
279
|
+
// Store two embeddings: [1,2,3,4] and [3,4,5,6]
|
|
280
|
+
// Expected average: [2,3,4,5]
|
|
281
|
+
const emb1 = new Float32Array([1, 2, 3, 4]);
|
|
282
|
+
const emb2 = new Float32Array([3, 4, 5, 6]);
|
|
283
|
+
const buf1 = Buffer.from(emb1.buffer);
|
|
284
|
+
const buf2 = Buffer.from(emb2.buffer);
|
|
285
|
+
const insertStmt = db.prepare(`
|
|
286
|
+
INSERT INTO patterns (
|
|
287
|
+
pattern_id, context_embedding, skill_id, skill_features, context_data,
|
|
288
|
+
outcome_type, outcome_reward, importance, original_score, source,
|
|
289
|
+
access_count, created_at, last_accessed_at
|
|
290
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, unixepoch(), unixepoch())
|
|
291
|
+
`);
|
|
292
|
+
insertStmt.run('p1', buf1, 'skill1', '{}', '{}', 'accept', 1.0, 0.5, 0.8, 'recommend', 0);
|
|
293
|
+
insertStmt.run('p2', buf2, 'skill2', '{}', '{}', 'accept', 1.0, 0.5, 0.8, 'recommend', 0);
|
|
294
|
+
const result = await computeAverageEmbedding(db, 100, dims);
|
|
295
|
+
expect(result[0]).toBeCloseTo(2.0);
|
|
296
|
+
expect(result[1]).toBeCloseTo(3.0);
|
|
297
|
+
expect(result[2]).toBeCloseTo(4.0);
|
|
298
|
+
expect(result[3]).toBeCloseTo(5.0);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
//# sourceMappingURL=PatternStore.helpers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PatternStore.helpers.test.js","sourceRoot":"","sources":["../../../tests/learning/PatternStore.helpers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for CoInstallRepository (SMI-2761)
|
|
3
|
+
* @module @skillsmith/core/tests/repositories/CoInstallRepository
|
|
4
|
+
*
|
|
5
|
+
* Tests co-install recording, upsert symmetry, and min-count threshold.
|
|
6
|
+
* Uses createTestDatabase() which runs all migrations including v8.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=CoInstallRepository.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoInstallRepository.test.d.ts","sourceRoot":"","sources":["../../../tests/repositories/CoInstallRepository.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tests for CoInstallRepository (SMI-2761)
|
|
3
|
+
* @module @skillsmith/core/tests/repositories/CoInstallRepository
|
|
4
|
+
*
|
|
5
|
+
* Tests co-install recording, upsert symmetry, and min-count threshold.
|
|
6
|
+
* Uses createTestDatabase() which runs all migrations including v8.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
import { createTestDatabase, closeDatabase } from '../helpers/database.js';
|
|
10
|
+
import { CoInstallRepository } from '../../src/repositories/CoInstallRepository.js';
|
|
11
|
+
let db;
|
|
12
|
+
let repo;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
db = createTestDatabase();
|
|
15
|
+
repo = new CoInstallRepository(db);
|
|
16
|
+
// Insert fixture skills so JOIN works in getTopCoInstalls
|
|
17
|
+
db.exec(`
|
|
18
|
+
INSERT INTO skills (id, name, author, description)
|
|
19
|
+
VALUES
|
|
20
|
+
('skill-a', 'skill-a', 'test', 'Skill A description'),
|
|
21
|
+
('skill-b', 'skill-b', 'test', 'Skill B description'),
|
|
22
|
+
('skill-c', 'skill-c', 'test', 'Skill C description');
|
|
23
|
+
`);
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
closeDatabase(db);
|
|
27
|
+
});
|
|
28
|
+
describe('CoInstallRepository', () => {
|
|
29
|
+
describe('getTopCoInstalls — empty table', () => {
|
|
30
|
+
it('returns empty array when no co-installs recorded', () => {
|
|
31
|
+
const results = repo.getTopCoInstalls('skill-a');
|
|
32
|
+
expect(results).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('recordCoInstall — upsert symmetry', () => {
|
|
36
|
+
it('creates (A,B) and (B,A) rows on first call', () => {
|
|
37
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
38
|
+
const forA = repo.getTopCoInstalls('skill-a', 10, 1);
|
|
39
|
+
const forB = repo.getTopCoInstalls('skill-b', 10, 1);
|
|
40
|
+
expect(forA.map((r) => r.skillId)).toContain('skill-b');
|
|
41
|
+
expect(forB.map((r) => r.skillId)).toContain('skill-a');
|
|
42
|
+
});
|
|
43
|
+
it('increments install_count on repeated calls', () => {
|
|
44
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
45
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
46
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
47
|
+
const results = repo.getTopCoInstalls('skill-a', 10, 1);
|
|
48
|
+
const entry = results.find((r) => r.skillId === 'skill-b');
|
|
49
|
+
expect(entry?.installCount).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
it('is idempotent for self-pairs (no-op)', () => {
|
|
52
|
+
// Should not throw, simply return
|
|
53
|
+
expect(() => repo.recordCoInstall('skill-a', 'skill-a')).not.toThrow();
|
|
54
|
+
const results = repo.getTopCoInstalls('skill-a', 10, 1);
|
|
55
|
+
expect(results).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('recordSessionCoInstalls', () => {
|
|
59
|
+
it('records all pairs in a session', () => {
|
|
60
|
+
repo.recordSessionCoInstalls(['skill-a', 'skill-b', 'skill-c']);
|
|
61
|
+
const forA = repo.getTopCoInstalls('skill-a', 10, 1);
|
|
62
|
+
const ids = forA.map((r) => r.skillId);
|
|
63
|
+
expect(ids).toContain('skill-b');
|
|
64
|
+
expect(ids).toContain('skill-c');
|
|
65
|
+
});
|
|
66
|
+
it('is a no-op for fewer than 2 skills', () => {
|
|
67
|
+
expect(() => repo.recordSessionCoInstalls([])).not.toThrow();
|
|
68
|
+
expect(() => repo.recordSessionCoInstalls(['skill-a'])).not.toThrow();
|
|
69
|
+
expect(repo.getTopCoInstalls('skill-a', 10, 1)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('getTopCoInstalls — min-count boundary', () => {
|
|
73
|
+
it('excludes entries below minCount (4 → excluded at default threshold 5)', () => {
|
|
74
|
+
// Record 4 co-installs
|
|
75
|
+
for (let i = 0; i < 4; i++) {
|
|
76
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
77
|
+
}
|
|
78
|
+
const results = repo.getTopCoInstalls('skill-a'); // default minCount=5
|
|
79
|
+
expect(results.find((r) => r.skillId === 'skill-b')).toBeUndefined();
|
|
80
|
+
});
|
|
81
|
+
it('includes entries at exactly minCount (5 → included at default threshold 5)', () => {
|
|
82
|
+
// Record exactly 5 co-installs
|
|
83
|
+
for (let i = 0; i < 5; i++) {
|
|
84
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
85
|
+
}
|
|
86
|
+
const results = repo.getTopCoInstalls('skill-a'); // default minCount=5
|
|
87
|
+
expect(results.find((r) => r.skillId === 'skill-b')).toBeDefined();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('getTopCoInstalls — result shape', () => {
|
|
91
|
+
it('returns skills with name/description/author from skills table', () => {
|
|
92
|
+
for (let i = 0; i < 5; i++) {
|
|
93
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
94
|
+
}
|
|
95
|
+
const results = repo.getTopCoInstalls('skill-a');
|
|
96
|
+
expect(results).toHaveLength(1);
|
|
97
|
+
expect(results[0]).toMatchObject({
|
|
98
|
+
skillId: 'skill-b',
|
|
99
|
+
name: 'skill-b',
|
|
100
|
+
description: 'Skill B description',
|
|
101
|
+
author: 'test',
|
|
102
|
+
installCount: 5,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('respects the limit parameter', () => {
|
|
106
|
+
for (let i = 0; i < 5; i++) {
|
|
107
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
108
|
+
repo.recordCoInstall('skill-a', 'skill-c');
|
|
109
|
+
}
|
|
110
|
+
const results = repo.getTopCoInstalls('skill-a', 1); // limit=1
|
|
111
|
+
expect(results).toHaveLength(1);
|
|
112
|
+
});
|
|
113
|
+
it('orders results by install_count descending', () => {
|
|
114
|
+
for (let i = 0; i < 10; i++)
|
|
115
|
+
repo.recordCoInstall('skill-a', 'skill-b');
|
|
116
|
+
for (let i = 0; i < 5; i++)
|
|
117
|
+
repo.recordCoInstall('skill-a', 'skill-c');
|
|
118
|
+
const results = repo.getTopCoInstalls('skill-a', 10, 1);
|
|
119
|
+
expect(results[0].skillId).toBe('skill-b');
|
|
120
|
+
expect(results[0].installCount).toBe(10);
|
|
121
|
+
expect(results[1].skillId).toBe('skill-c');
|
|
122
|
+
expect(results[1].installCount).toBe(5);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=CoInstallRepository.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CoInstallRepository.test.js","sourceRoot":"","sources":["../../../tests/repositories/CoInstallRepository.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-2756: Wave 3 — LanguageRouter tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the LanguageRouter dispatch layer: adapter registration,
|
|
5
|
+
* extension-based lookup, tryGetAdapter, canHandle, getLanguage,
|
|
6
|
+
* getSupportedLanguages, framework rules aggregation, and dispose.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=LanguageRouter.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LanguageRouter.test.d.ts","sourceRoot":"","sources":["../../../tests/routing/LanguageRouter.test.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-2756: Wave 3 — LanguageRouter tests
|
|
3
|
+
*
|
|
4
|
+
* Tests the LanguageRouter dispatch layer: adapter registration,
|
|
5
|
+
* extension-based lookup, tryGetAdapter, canHandle, getLanguage,
|
|
6
|
+
* getSupportedLanguages, framework rules aggregation, and dispose.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { LanguageRouter } from '../../src/analysis/router.js';
|
|
10
|
+
describe('LanguageRouter', () => {
|
|
11
|
+
// -------------------------------------------------------------------------
|
|
12
|
+
// createWithAllAdapters factory
|
|
13
|
+
// -------------------------------------------------------------------------
|
|
14
|
+
describe('createWithAllAdapters', () => {
|
|
15
|
+
it('creates a router with multiple adapters pre-registered', () => {
|
|
16
|
+
const router = LanguageRouter.createWithAllAdapters();
|
|
17
|
+
expect(router.adapterCount).toBeGreaterThan(0);
|
|
18
|
+
});
|
|
19
|
+
it('creates a router with only specified languages', () => {
|
|
20
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript'] });
|
|
21
|
+
expect(router.getSupportedLanguages()).toContain('typescript');
|
|
22
|
+
// Should have fewer adapters than the full set
|
|
23
|
+
const fullRouter = LanguageRouter.createWithAllAdapters();
|
|
24
|
+
expect(router.adapterCount).toBeLessThanOrEqual(fullRouter.adapterCount);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
// -------------------------------------------------------------------------
|
|
28
|
+
// registerAdapter / unregisterAdapter
|
|
29
|
+
// -------------------------------------------------------------------------
|
|
30
|
+
describe('registerAdapter / unregisterAdapter', () => {
|
|
31
|
+
it('registers an adapter and makes its extension routable', () => {
|
|
32
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript'] });
|
|
33
|
+
expect(router.canHandle('main.ts')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('unregisterAdapter removes extension mappings', () => {
|
|
36
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript'] });
|
|
37
|
+
expect(router.canHandle('main.ts')).toBe(true);
|
|
38
|
+
const removed = router.unregisterAdapter('typescript');
|
|
39
|
+
expect(removed).toBe(true);
|
|
40
|
+
expect(router.canHandle('main.ts')).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
it('unregisterAdapter returns false for unknown language', () => {
|
|
43
|
+
const router = new LanguageRouter();
|
|
44
|
+
const result = router.unregisterAdapter('typescript');
|
|
45
|
+
expect(result).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it('re-registering an adapter replaces the old one', () => {
|
|
48
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript'] });
|
|
49
|
+
const before = router.adapterCount;
|
|
50
|
+
// Re-register the same language
|
|
51
|
+
const tsAdapter = router.getAdapterByLanguage('typescript');
|
|
52
|
+
router.registerAdapter(tsAdapter);
|
|
53
|
+
// Count should not grow
|
|
54
|
+
expect(router.adapterCount).toBe(before);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// -------------------------------------------------------------------------
|
|
58
|
+
// getAdapter / tryGetAdapter / canHandle / getLanguage
|
|
59
|
+
// -------------------------------------------------------------------------
|
|
60
|
+
describe('getAdapter', () => {
|
|
61
|
+
it('returns adapter for a known extension', () => {
|
|
62
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript'] });
|
|
63
|
+
const adapter = router.tryGetAdapter('main.ts');
|
|
64
|
+
expect(adapter).not.toBeNull();
|
|
65
|
+
expect(adapter?.language).toBe('typescript');
|
|
66
|
+
});
|
|
67
|
+
it('throws for unknown extension', () => {
|
|
68
|
+
const router = new LanguageRouter();
|
|
69
|
+
expect(() => router.getAdapter('unknown.xyz')).toThrow(/No adapter registered/);
|
|
70
|
+
});
|
|
71
|
+
it('tryGetAdapter returns null for unknown extension', () => {
|
|
72
|
+
const router = new LanguageRouter();
|
|
73
|
+
expect(router.tryGetAdapter('unknown.xyz')).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('canHandle', () => {
|
|
77
|
+
it('returns true for registered extension', () => {
|
|
78
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['python'] });
|
|
79
|
+
expect(router.canHandle('app.py')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
it('returns false for unregistered extension', () => {
|
|
82
|
+
const router = new LanguageRouter();
|
|
83
|
+
expect(router.canHandle('app.cobol')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe('getLanguage', () => {
|
|
87
|
+
it('returns language for a known file path', () => {
|
|
88
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['python'] });
|
|
89
|
+
expect(router.getLanguage('script.py')).toBe('python');
|
|
90
|
+
});
|
|
91
|
+
it('returns null for unsupported extension', () => {
|
|
92
|
+
const router = new LanguageRouter();
|
|
93
|
+
expect(router.getLanguage('file.cobol')).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
// -------------------------------------------------------------------------
|
|
97
|
+
// getSupportedLanguages / getSupportedExtensions
|
|
98
|
+
// -------------------------------------------------------------------------
|
|
99
|
+
describe('getSupportedLanguages / getSupportedExtensions', () => {
|
|
100
|
+
it('getSupportedLanguages returns registered language keys', () => {
|
|
101
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript', 'python'] });
|
|
102
|
+
const langs = router.getSupportedLanguages();
|
|
103
|
+
expect(langs).toContain('typescript');
|
|
104
|
+
expect(langs).toContain('python');
|
|
105
|
+
});
|
|
106
|
+
it('getSupportedExtensions includes dot-prefixed extensions', () => {
|
|
107
|
+
const router = LanguageRouter.createWithAllAdapters({ languages: ['typescript'] });
|
|
108
|
+
const exts = router.getSupportedExtensions();
|
|
109
|
+
expect(exts.some((e) => e.startsWith('.'))).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
// -------------------------------------------------------------------------
|
|
113
|
+
// getAllFrameworkRules / getFrameworkRules
|
|
114
|
+
// -------------------------------------------------------------------------
|
|
115
|
+
describe('framework rules', () => {
|
|
116
|
+
it('getAllFrameworkRules returns an array', () => {
|
|
117
|
+
const router = LanguageRouter.createWithAllAdapters();
|
|
118
|
+
const rules = router.getAllFrameworkRules();
|
|
119
|
+
expect(Array.isArray(rules)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
it('getFrameworkRules returns empty array for unregistered language', () => {
|
|
122
|
+
const router = new LanguageRouter();
|
|
123
|
+
const rules = router.getFrameworkRules('typescript');
|
|
124
|
+
expect(rules).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// -------------------------------------------------------------------------
|
|
128
|
+
// dispose
|
|
129
|
+
// -------------------------------------------------------------------------
|
|
130
|
+
describe('dispose', () => {
|
|
131
|
+
it('clears adapter registry after dispose', () => {
|
|
132
|
+
const router = LanguageRouter.createWithAllAdapters();
|
|
133
|
+
expect(router.adapterCount).toBeGreaterThan(0);
|
|
134
|
+
router.dispose();
|
|
135
|
+
expect(router.adapterCount).toBe(0);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// -------------------------------------------------------------------------
|
|
139
|
+
// detectLanguageFromContent (disabled)
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
describe('detectLanguageFromContent', () => {
|
|
142
|
+
it('returns language:null and method:none when detection is disabled', () => {
|
|
143
|
+
const router = new LanguageRouter({ enableLanguageDetection: false });
|
|
144
|
+
const result = router.detectLanguageFromContent('#!/usr/bin/env python3\nprint("hello")');
|
|
145
|
+
expect(result.language).toBeNull();
|
|
146
|
+
expect(result.method).toBe('none');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
//# sourceMappingURL=LanguageRouter.test.js.map
|