Skip to content

Commit 49bda42

Browse files
committed
test: add unit tests for v1.2.9 features
- ConfigDefaultsTest: validates XNNPACK thread defaults (1-8 range), short gesture distance settings (min/max range validation), and neural prediction parameter bounds - ContractionFrequencyTest: verifies French contraction frequency scoring logic, tests that high-frequency contractions like "qu'est" rank higher than lower-frequency base words like "quest" when using dictionary frequency lookups Coverage for v1.2.9 features: - Tensor reuse: already covered by MemoryPoolTest.kt - XNNPACK threads: ConfigDefaultsTest validates default and range - French contraction frequency: ContractionFrequencyTest validates scoring - Short gesture max distance: ConfigDefaultsTest validates default/range — claude-opus-4-5-20251101
1 parent a4eaa7e commit 49bda42

File tree

2 files changed

+342
-0
lines changed

2 files changed

+342
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package tribixbite.cleverkeys
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import org.junit.Test
5+
6+
/**
7+
* Pure JVM tests for Config.Defaults values.
8+
*
9+
* Tests validate that default values are within expected ranges
10+
* and match documented behavior for v1.2.9 features.
11+
*/
12+
class ConfigDefaultsTest {
13+
14+
// =========================================================================
15+
// ONNX XNNPACK Thread Settings (v1.2.9)
16+
// =========================================================================
17+
18+
@Test
19+
fun `XNNPACK threads default is 2`() {
20+
assertThat(Defaults.ONNX_XNNPACK_THREADS).isEqualTo(2)
21+
}
22+
23+
@Test
24+
fun `XNNPACK threads default is within valid range 1-8`() {
25+
assertThat(Defaults.ONNX_XNNPACK_THREADS).isAtLeast(1)
26+
assertThat(Defaults.ONNX_XNNPACK_THREADS).isAtMost(8)
27+
}
28+
29+
// =========================================================================
30+
// Short Gesture Settings (v1.2.9)
31+
// =========================================================================
32+
33+
@Test
34+
fun `short gestures enabled by default`() {
35+
assertThat(Defaults.SHORT_GESTURES_ENABLED).isTrue()
36+
}
37+
38+
@Test
39+
fun `short gesture min distance default is 28`() {
40+
assertThat(Defaults.SHORT_GESTURE_MIN_DISTANCE).isEqualTo(28)
41+
}
42+
43+
@Test
44+
fun `short gesture max distance default is 141`() {
45+
assertThat(Defaults.SHORT_GESTURE_MAX_DISTANCE).isEqualTo(141)
46+
}
47+
48+
@Test
49+
fun `short gesture min is less than max`() {
50+
assertThat(Defaults.SHORT_GESTURE_MIN_DISTANCE)
51+
.isLessThan(Defaults.SHORT_GESTURE_MAX_DISTANCE)
52+
}
53+
54+
@Test
55+
fun `short gesture min distance is within valid range 10-95`() {
56+
assertThat(Defaults.SHORT_GESTURE_MIN_DISTANCE).isAtLeast(10)
57+
assertThat(Defaults.SHORT_GESTURE_MIN_DISTANCE).isAtMost(95)
58+
}
59+
60+
@Test
61+
fun `short gesture max distance is within valid range 50-200`() {
62+
assertThat(Defaults.SHORT_GESTURE_MAX_DISTANCE).isAtLeast(50)
63+
assertThat(Defaults.SHORT_GESTURE_MAX_DISTANCE).isAtMost(200)
64+
}
65+
66+
// =========================================================================
67+
// Neural Prediction Defaults
68+
// =========================================================================
69+
70+
@Test
71+
fun `neural beam width default is 6`() {
72+
assertThat(Defaults.NEURAL_BEAM_WIDTH).isEqualTo(6)
73+
}
74+
75+
@Test
76+
fun `neural max length default is 20`() {
77+
assertThat(Defaults.NEURAL_MAX_LENGTH).isEqualTo(20)
78+
}
79+
80+
@Test
81+
fun `neural batch beams disabled by default`() {
82+
assertThat(Defaults.NEURAL_BATCH_BEAMS).isFalse()
83+
}
84+
85+
@Test
86+
fun `neural frequency weight default is 0_57`() {
87+
assertThat(Defaults.NEURAL_FREQUENCY_WEIGHT).isWithin(0.01f).of(0.57f)
88+
}
89+
90+
// =========================================================================
91+
// Swipe Typing Defaults
92+
// =========================================================================
93+
94+
@Test
95+
fun `swipe typing enabled by default`() {
96+
assertThat(Defaults.SWIPE_TYPING_ENABLED).isTrue()
97+
}
98+
99+
@Test
100+
fun `swipe trail enabled by default`() {
101+
assertThat(Defaults.SWIPE_TRAIL_ENABLED).isTrue()
102+
}
103+
104+
@Test
105+
fun `word prediction enabled by default`() {
106+
assertThat(Defaults.WORD_PREDICTION_ENABLED).isTrue()
107+
}
108+
109+
// =========================================================================
110+
// Backup/Restore compatibility (v1.2.9)
111+
// =========================================================================
112+
113+
@Test
114+
fun `all neural defaults have valid values for backup restore`() {
115+
// These are used in BackupRestoreManager and must be reasonable
116+
assertThat(Defaults.NEURAL_BEAM_WIDTH).isAtLeast(1)
117+
assertThat(Defaults.NEURAL_BEAM_WIDTH).isAtMost(20)
118+
assertThat(Defaults.NEURAL_MAX_LENGTH).isAtLeast(5)
119+
assertThat(Defaults.NEURAL_MAX_LENGTH).isAtMost(50)
120+
assertThat(Defaults.NEURAL_CONFIDENCE_THRESHOLD).isAtLeast(0f)
121+
assertThat(Defaults.NEURAL_CONFIDENCE_THRESHOLD).isAtMost(1f)
122+
}
123+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package tribixbite.cleverkeys
2+
3+
import com.google.common.truth.Truth.assertThat
4+
import org.junit.Test
5+
6+
/**
7+
* Tests for French contraction frequency scoring (v1.2.9 fix).
8+
*
9+
* The v1.2.9 fix ensures that French contractions like "qu'est" can rank
10+
* higher than their base words like "quest" when the contraction has
11+
* higher frequency in the dictionary.
12+
*
13+
* This is achieved by looking up actual contraction frequencies and using
14+
* VocabularyUtils.calculateCombinedScore for fair comparison.
15+
*/
16+
class ContractionFrequencyTest {
17+
18+
// =========================================================================
19+
// calculateCombinedScore for contraction ranking
20+
// =========================================================================
21+
22+
@Test
23+
fun `high frequency contraction scores higher than low frequency base word`() {
24+
// Simulate "qu'est" (high frequency French contraction) vs "quest" (lower frequency)
25+
val contractionFrequency = 0.9f // Very common French word
26+
val baseWordFrequency = 0.3f // Less common English word
27+
28+
val commonBoost = 1.15f
29+
val confidenceWeight = 0.6f
30+
val frequencyWeight = 0.4f
31+
32+
// Both have same NN confidence (user swiped the same pattern)
33+
val nnConfidence = 0.85f
34+
35+
val contractionScore = VocabularyUtils.calculateCombinedScore(
36+
nnConfidence, contractionFrequency, commonBoost, confidenceWeight, frequencyWeight
37+
)
38+
39+
val baseWordScore = VocabularyUtils.calculateCombinedScore(
40+
nnConfidence, baseWordFrequency, 1.0f, confidenceWeight, frequencyWeight
41+
)
42+
43+
// Contraction should score higher due to higher frequency + common boost
44+
assertThat(contractionScore).isGreaterThan(baseWordScore)
45+
}
46+
47+
@Test
48+
fun `low frequency contraction scores lower than high frequency base word`() {
49+
// When base word is more common, it should rank higher
50+
val contractionFrequency = 0.2f // Rare contraction
51+
val baseWordFrequency = 0.95f // Very common word
52+
53+
val top5000Boost = 1.08f
54+
val commonBoost = 1.15f
55+
val confidenceWeight = 0.6f
56+
val frequencyWeight = 0.4f
57+
58+
val nnConfidence = 0.85f
59+
60+
val contractionScore = VocabularyUtils.calculateCombinedScore(
61+
nnConfidence, contractionFrequency, top5000Boost, confidenceWeight, frequencyWeight
62+
)
63+
64+
val baseWordScore = VocabularyUtils.calculateCombinedScore(
65+
nnConfidence, baseWordFrequency, commonBoost, confidenceWeight, frequencyWeight
66+
)
67+
68+
// Base word should score higher
69+
assertThat(baseWordScore).isGreaterThan(contractionScore)
70+
}
71+
72+
@Test
73+
fun `frequency ranking is fair with equal boosts`() {
74+
// With same boosts, higher frequency should always win
75+
val boost = 1.0f
76+
val confidenceWeight = 0.6f
77+
val frequencyWeight = 0.4f
78+
val nnConfidence = 0.80f
79+
80+
val highFreqScore = VocabularyUtils.calculateCombinedScore(
81+
nnConfidence, 0.9f, boost, confidenceWeight, frequencyWeight
82+
)
83+
84+
val lowFreqScore = VocabularyUtils.calculateCombinedScore(
85+
nnConfidence, 0.3f, boost, confidenceWeight, frequencyWeight
86+
)
87+
88+
assertThat(highFreqScore).isGreaterThan(lowFreqScore)
89+
}
90+
91+
// =========================================================================
92+
// Frequency conversion from rank
93+
// =========================================================================
94+
95+
@Test
96+
fun `frequency rank 0 converts to frequency 1_0`() {
97+
// Rank 0 = most common word, should have frequency 1.0
98+
val rank = 0
99+
val frequency = 1.0f - (rank / 255.0f)
100+
assertThat(frequency).isWithin(0.001f).of(1.0f)
101+
}
102+
103+
@Test
104+
fun `frequency rank 255 converts to near zero frequency`() {
105+
// Rank 255 = rarest tracked word
106+
val rank = 255
107+
val frequency = 1.0f - (rank / 255.0f)
108+
assertThat(frequency).isWithin(0.001f).of(0.0f)
109+
}
110+
111+
@Test
112+
fun `frequency rank 127 converts to mid frequency`() {
113+
// Rank 127 = middle of the range
114+
val rank = 127
115+
val frequency = 1.0f - (rank / 255.0f)
116+
assertThat(frequency).isWithin(0.01f).of(0.5f)
117+
}
118+
119+
// =========================================================================
120+
// Realistic French contraction scenarios
121+
// =========================================================================
122+
123+
@Test
124+
fun `quest_vs_quest_scenario`() {
125+
// In French, "qu'est" (what is) is more common than English "quest"
126+
// Simulated dictionary ranks:
127+
// - "qu'est" rank ~20 (very common in French)
128+
// - "quest" rank ~150 (moderately common in English)
129+
130+
val questRank = 150
131+
val questFreq = 1.0f - (questRank / 255.0f) // ~0.41
132+
133+
val questApostropheRank = 20
134+
val questApostropheFreq = 1.0f - (questApostropheRank / 255.0f) // ~0.92
135+
136+
val commonBoost = 1.15f
137+
val top5000Boost = 1.08f
138+
val confidenceWeight = 0.6f
139+
val frequencyWeight = 0.4f
140+
val nnConfidence = 0.82f
141+
142+
val questScore = VocabularyUtils.calculateCombinedScore(
143+
nnConfidence, questFreq, top5000Boost, confidenceWeight, frequencyWeight
144+
)
145+
146+
val questApostropheScore = VocabularyUtils.calculateCombinedScore(
147+
nnConfidence, questApostropheFreq, commonBoost, confidenceWeight, frequencyWeight
148+
)
149+
150+
// "qu'est" should rank higher due to its much higher frequency
151+
assertThat(questApostropheScore).isGreaterThan(questScore)
152+
}
153+
154+
@Test
155+
fun `jai_vs_jail_scenario`() {
156+
// French "j'ai" (I have) vs English "jail"
157+
// "j'ai" is extremely common in French
158+
// "jail" is moderately common in English
159+
160+
val jailRank = 180
161+
val jailFreq = 1.0f - (jailRank / 255.0f) // ~0.29
162+
163+
val jaiRank = 5
164+
val jaiFreq = 1.0f - (jaiRank / 255.0f) // ~0.98
165+
166+
val commonBoost = 1.15f
167+
val top5000Boost = 1.08f
168+
val confidenceWeight = 0.6f
169+
val frequencyWeight = 0.4f
170+
val nnConfidence = 0.78f
171+
172+
val jailScore = VocabularyUtils.calculateCombinedScore(
173+
nnConfidence, jailFreq, top5000Boost, confidenceWeight, frequencyWeight
174+
)
175+
176+
val jaiScore = VocabularyUtils.calculateCombinedScore(
177+
nnConfidence, jaiFreq, commonBoost, confidenceWeight, frequencyWeight
178+
)
179+
180+
// "j'ai" should rank higher
181+
assertThat(jaiScore).isGreaterThan(jailScore)
182+
}
183+
184+
// =========================================================================
185+
// Edge cases
186+
// =========================================================================
187+
188+
@Test
189+
fun `zero frequency contraction still gets valid score`() {
190+
val score = VocabularyUtils.calculateCombinedScore(
191+
confidence = 0.8f,
192+
frequency = 0.0f,
193+
boost = 1.0f,
194+
confidenceWeight = 0.6f,
195+
frequencyWeight = 0.4f
196+
)
197+
// Should still produce a positive score from confidence alone
198+
assertThat(score).isGreaterThan(0f)
199+
assertThat(score).isWithin(0.001f).of(0.48f) // 0.6 * 0.8 + 0.4 * 0
200+
}
201+
202+
@Test
203+
fun `default contraction frequency 0_6 is reasonable fallback`() {
204+
// When contraction isn't in dictionary, default to 0.6
205+
val defaultFreq = 0.6f
206+
207+
val score = VocabularyUtils.calculateCombinedScore(
208+
confidence = 0.8f,
209+
frequency = defaultFreq,
210+
boost = 1.08f,
211+
confidenceWeight = 0.6f,
212+
frequencyWeight = 0.4f
213+
)
214+
215+
// Score should be reasonable (not too high, not too low)
216+
assertThat(score).isGreaterThan(0.5f)
217+
assertThat(score).isLessThan(1.0f)
218+
}
219+
}

0 commit comments

Comments
 (0)