entroplain 0.1.0

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.
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Functional test for Entroplain - verify core functionality works.
4
+ """
5
+
6
+ import sys
7
+ import os
8
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
+
10
+ from entroplain import EntropyMonitor, calculate_entropy
11
+
12
+
13
+ def test_entropy_calculation():
14
+ """Test Shannon entropy calculation."""
15
+ print("=" * 60)
16
+ print("TEST: Entropy Calculation")
17
+ print("=" * 60)
18
+
19
+ # Test 1: Uniform distribution (should be ~1.0 for 2 choices)
20
+ entropy = calculate_entropy([-0.693, -0.693]) # log(0.5) ≈ -0.693
21
+ print(f"Uniform distribution entropy: {entropy:.4f}")
22
+ assert 0.99 < entropy < 1.01, f"Expected ~1.0, got {entropy}"
23
+ print("✓ Uniform distribution test passed")
24
+
25
+ # Test 2: Deterministic (should be ~0)
26
+ entropy = calculate_entropy([0.0, -100]) # One certain, one impossible
27
+ print(f"Deterministic entropy: {entropy:.4f}")
28
+ assert entropy < 0.01, f"Expected ~0, got {entropy}"
29
+ print("✓ Deterministic test passed")
30
+
31
+ # Test 3: Empty input
32
+ entropy = calculate_entropy([])
33
+ print(f"Empty input entropy: {entropy}")
34
+ assert entropy == 0.0, f"Expected 0.0, got {entropy}"
35
+ print("✓ Empty input test passed")
36
+
37
+ print()
38
+ return True
39
+
40
+
41
+ def test_valley_detection():
42
+ """Test valley (local minimum) detection."""
43
+ print("=" * 60)
44
+ print("TEST: Valley Detection")
45
+ print("=" * 60)
46
+
47
+ monitor = EntropyMonitor()
48
+
49
+ # Create trajectory: high -> low -> high (valley at index 1)
50
+ trajectory = [0.8, 0.3, 0.7]
51
+ for i, entropy in enumerate(trajectory):
52
+ point = monitor.track(f"token_{i}", entropy)
53
+ print(f" Token {i}: entropy={entropy}, is_valley={point.is_valley}")
54
+
55
+ valleys = monitor.get_valleys()
56
+ print(f"Valleys detected: {valleys}")
57
+ assert len(valleys) == 1, f"Expected 1 valley, got {len(valleys)}"
58
+ assert valleys[0] == (1, 0.3), f"Expected (1, 0.3), got {valleys[0]}"
59
+ print("✓ Valley detection test passed")
60
+
61
+ print()
62
+ return True
63
+
64
+
65
+ def test_velocity_calculation():
66
+ """Test entropy velocity (rate of change)."""
67
+ print("=" * 60)
68
+ print("TEST: Velocity Calculation")
69
+ print("=" * 60)
70
+
71
+ monitor = EntropyMonitor()
72
+
73
+ monitor.track("A", 0.5)
74
+ velocity_initial = monitor.get_velocity()
75
+ print(f"Initial velocity: {velocity_initial}")
76
+ assert velocity_initial == 0.0, "Initial velocity should be 0"
77
+
78
+ monitor.track("B", 0.7)
79
+ velocity = monitor.get_velocity()
80
+ print(f"Velocity after second token: {velocity:.4f}")
81
+ assert abs(velocity - 0.2) < 0.01, f"Expected ~0.2, got {velocity}"
82
+
83
+ print("✓ Velocity calculation test passed")
84
+
85
+ print()
86
+ return True
87
+
88
+
89
+ def test_exit_conditions():
90
+ """Test various exit conditions."""
91
+ print("=" * 60)
92
+ print("TEST: Exit Conditions")
93
+ print("=" * 60)
94
+
95
+ # Test min_tokens requirement
96
+ monitor = EntropyMonitor(min_tokens=10, min_valleys=0)
97
+ for i in range(5):
98
+ monitor.track(f"t_{i}", 0.1) # Low entropy
99
+ should_exit = monitor.should_exit()
100
+ print(f"min_tokens=10, 5 tokens: should_exit={should_exit}")
101
+ assert not should_exit, "Should not exit before min_tokens"
102
+ print("✓ min_tokens requirement works")
103
+
104
+ # Test min_valleys requirement
105
+ monitor = EntropyMonitor(min_tokens=0, min_valleys=3)
106
+ for entropy in [0.8, 0.3, 0.8, 0.4, 0.8]: # Only 2 valleys
107
+ monitor.track("t", entropy)
108
+ should_exit = monitor.should_exit()
109
+ print(f"min_valleys=3, 2 valleys: should_exit={should_exit}")
110
+ assert not should_exit, "Should not exit before min_valleys"
111
+ print("✓ min_valleys requirement works")
112
+
113
+ # Test entropy_drop condition
114
+ monitor = EntropyMonitor(
115
+ entropy_threshold=0.15,
116
+ min_valleys=0,
117
+ min_tokens=5,
118
+ exit_condition="entropy_drop"
119
+ )
120
+ for i in range(10):
121
+ monitor.track(f"t_{i}", 0.1) # Below threshold
122
+ should_exit = monitor.should_exit()
123
+ print(f"entropy_drop, entropy=0.1, threshold=0.15: should_exit={should_exit}")
124
+ assert should_exit, "Should exit when entropy below threshold"
125
+ print("✓ entropy_drop condition works")
126
+
127
+ print()
128
+ return True
129
+
130
+
131
+ def test_stats():
132
+ """Test statistics output."""
133
+ print("=" * 60)
134
+ print("TEST: Statistics")
135
+ print("=" * 60)
136
+
137
+ monitor = EntropyMonitor()
138
+
139
+ trajectory = [0.5, 0.3, 0.7, 0.2, 0.6]
140
+ for i, entropy in enumerate(trajectory):
141
+ monitor.track(f"t_{i}", entropy)
142
+
143
+ stats = monitor.get_stats()
144
+ print(f"Statistics: {stats}")
145
+
146
+ assert stats["token_count"] == 5
147
+ assert stats["min_entropy"] == 0.2
148
+ assert stats["max_entropy"] == 0.7
149
+ assert abs(stats["mean_entropy"] - 0.46) < 0.01
150
+
151
+ print("✓ Statistics test passed")
152
+
153
+ print()
154
+ return True
155
+
156
+
157
+ def test_reset():
158
+ """Test monitor reset."""
159
+ print("=" * 60)
160
+ print("TEST: Reset")
161
+ print("=" * 60)
162
+
163
+ monitor = EntropyMonitor()
164
+
165
+ for i in range(10):
166
+ monitor.track(f"t_{i}", 0.5)
167
+
168
+ assert len(monitor.get_trajectory()) == 10
169
+ print(f"Before reset: {len(monitor.get_trajectory())} tokens")
170
+
171
+ monitor.reset()
172
+
173
+ assert len(monitor.get_trajectory()) == 0
174
+ assert len(monitor.get_valleys()) == 0
175
+ print(f"After reset: {len(monitor.get_trajectory())} tokens")
176
+ print("✓ Reset test passed")
177
+
178
+ print()
179
+ return True
180
+
181
+
182
+ def run_security_audit():
183
+ """Security audit for the codebase."""
184
+ print("=" * 60)
185
+ print("SECURITY AUDIT")
186
+ print("=" * 60)
187
+
188
+ issues = []
189
+ warnings = []
190
+
191
+ # Check 1: No hardcoded secrets
192
+ print("\n[CHECK] Hardcoded secrets...")
193
+ import entroplain.monitor as monitor_module
194
+ import entroplain.providers as providers_module
195
+
196
+ monitor_code = open(monitor_module.__file__).read()
197
+ providers_code = open(providers_module.__file__).read()
198
+ combined = monitor_code + providers_code
199
+
200
+ # Look for potential secrets
201
+ import re
202
+ api_key_patterns = [
203
+ r'sk-[a-zA-Z0-9]{20,}',
204
+ r'sk-ant-[a-zA-Z0-9]{20,}',
205
+ r'nvapi-[a-zA-Z0-9]{20,}',
206
+ r'AIza[a-zA-Z0-9_-]{35}',
207
+ ]
208
+
209
+ for pattern in api_key_patterns:
210
+ matches = re.findall(pattern, combined)
211
+ if matches:
212
+ issues.append(f"Potential API key found: {matches[0][:10]}...")
213
+
214
+ if not issues:
215
+ print("✓ No hardcoded secrets found")
216
+
217
+ # Check 2: Environment variables for auth
218
+ print("\n[CHECK] Authentication handling...")
219
+ if 'os.environ.get' in providers_code:
220
+ print("✓ Uses environment variables for API keys")
221
+ else:
222
+ warnings.append("Consider using environment variables for API keys")
223
+
224
+ # Check 3: Input validation
225
+ print("\n[CHECK] Input validation...")
226
+ if 'if not logprobs' in monitor_code:
227
+ print("✓ Empty input validation present")
228
+
229
+ # Check 4: No eval/exec
230
+ print("\n[CHECK] Dangerous functions...")
231
+ dangerous = ['eval(', 'exec(', 'compile(', '__import__']
232
+ for d in dangerous:
233
+ if d in combined:
234
+ issues.append(f"Dangerous function found: {d}")
235
+
236
+ if not any(d in combined for d in dangerous):
237
+ print("✓ No dangerous eval/exec found")
238
+
239
+ # Check 5: No shell injection vectors
240
+ print("\n[CHECK] Shell injection vectors...")
241
+ if 'subprocess' in combined or 'os.system' in combined:
242
+ issues.append("Potential shell injection vector")
243
+ else:
244
+ print("✓ No shell injection vectors found")
245
+
246
+ # Check 6: Data sanitization
247
+ print("\n[CHECK] Data sanitization...")
248
+ if '1e-10' in monitor_code: # Prevents log(0)
249
+ print("✓ Math sanitization present (prevents log(0))")
250
+
251
+ print("\n" + "=" * 60)
252
+ print("SECURITY SUMMARY")
253
+ print("=" * 60)
254
+
255
+ if issues:
256
+ print(f"\n❌ ISSUES ({len(issues)}):")
257
+ for issue in issues:
258
+ print(f" - {issue}")
259
+ else:
260
+ print("\n✅ No security issues found")
261
+
262
+ if warnings:
263
+ print(f"\n⚠️ WARNINGS ({len(warnings)}):")
264
+ for warning in warnings:
265
+ print(f" - {warning}")
266
+
267
+ return len(issues) == 0
268
+
269
+
270
+ def main():
271
+ """Run all tests."""
272
+ print("\n" + "=" * 60)
273
+ print("ENTROPPLAIN FUNCTIONAL TESTS")
274
+ print("=" * 60 + "\n")
275
+
276
+ all_passed = True
277
+
278
+ try:
279
+ all_passed &= test_entropy_calculation()
280
+ all_passed &= test_valley_detection()
281
+ all_passed &= test_velocity_calculation()
282
+ all_passed &= test_exit_conditions()
283
+ all_passed &= test_stats()
284
+ all_passed &= test_reset()
285
+ all_passed &= run_security_audit()
286
+ except Exception as e:
287
+ print(f"\n❌ TEST FAILED: {e}")
288
+ import traceback
289
+ traceback.print_exc()
290
+ return 1
291
+
292
+ print("\n" + "=" * 60)
293
+ if all_passed:
294
+ print("✅ ALL TESTS PASSED")
295
+ else:
296
+ print("❌ SOME TESTS FAILED")
297
+ print("=" * 60 + "\n")
298
+
299
+ return 0 if all_passed else 1
300
+
301
+
302
+ if __name__ == "__main__":
303
+ sys.exit(main())
@@ -0,0 +1,165 @@
1
+ """Tests for Entroplain entropy monitor."""
2
+
3
+ import pytest
4
+ from entroplain import EntropyMonitor, calculate_entropy
5
+
6
+
7
+ class TestEntropyCalculation:
8
+ """Tests for entropy calculation."""
9
+
10
+ def test_calculate_entropy_uniform(self):
11
+ """Uniform distribution should have maximum entropy."""
12
+ # log(0.5) ≈ -0.693
13
+ entropy = calculate_entropy([-0.693, -0.693], from_probs=False)
14
+ assert 0.99 < entropy < 1.01 # Should be ~1.0 for 50/50
15
+
16
+ def test_calculate_entropy_deterministic(self):
17
+ """Deterministic distribution should have zero entropy."""
18
+ entropy = calculate_entropy([0.0, -100], from_probs=False)
19
+ assert entropy < 0.01 # Should be ~0
20
+
21
+ def test_calculate_entropy_from_probs(self):
22
+ """Should work with probabilities directly."""
23
+ entropy = calculate_entropy([0.5, 0.5], from_probs=True)
24
+ assert 0.99 < entropy < 1.01
25
+
26
+ def test_calculate_entropy_empty(self):
27
+ """Empty input should return zero."""
28
+ entropy = calculate_entropy([])
29
+ assert entropy == 0.0
30
+
31
+
32
+ class TestEntropyMonitor:
33
+ """Tests for the EntropyMonitor class."""
34
+
35
+ def test_track_token(self):
36
+ """Should track tokens and return entropy points."""
37
+ monitor = EntropyMonitor()
38
+
39
+ point = monitor.track("Hello", 0.5)
40
+
41
+ assert point.token == "Hello"
42
+ assert point.entropy == 0.5
43
+ assert point.index == 0
44
+ assert point.is_valley == False # First token can't be valley
45
+
46
+ def test_valley_detection(self):
47
+ """Should detect local minima (valleys)."""
48
+ monitor = EntropyMonitor()
49
+
50
+ # Create trajectory: high -> low -> high (valley in middle)
51
+ monitor.track("A", 0.8)
52
+ monitor.track("B", 0.3) # Valley
53
+ monitor.track("C", 0.7)
54
+
55
+ valleys = monitor.get_valleys()
56
+ assert len(valleys) == 1
57
+ assert valleys[0][0] == 1 # Index of "B"
58
+ assert valleys[0][1] == 0.3
59
+
60
+ def test_velocity_calculation(self):
61
+ """Should calculate entropy velocity (rate of change)."""
62
+ monitor = EntropyMonitor()
63
+
64
+ monitor.track("A", 0.5)
65
+ monitor.track("B", 0.7)
66
+
67
+ velocity = monitor.get_velocity()
68
+ assert abs(velocity - 0.2) < 0.01
69
+
70
+ def test_should_exit_respects_min_tokens(self):
71
+ """Should not exit before min_tokens."""
72
+ monitor = EntropyMonitor(min_tokens=10)
73
+
74
+ for i in range(5):
75
+ monitor.track(f"token_{i}", 0.1) # Low entropy
76
+
77
+ assert not monitor.should_exit() # Too few tokens
78
+
79
+ def test_should_exit_respects_min_valleys(self):
80
+ """Should not exit before min_valleys."""
81
+ monitor = EntropyMonitor(min_valleys=3, min_tokens=0)
82
+
83
+ # Create trajectory with only 2 valleys
84
+ for entropy in [0.8, 0.3, 0.8, 0.4, 0.8]:
85
+ monitor.track("t", entropy)
86
+
87
+ assert len(monitor.get_valleys()) == 2
88
+ assert not monitor.should_exit() # Not enough valleys
89
+
90
+ def test_get_stats(self):
91
+ """Should return correct statistics."""
92
+ monitor = EntropyMonitor()
93
+
94
+ for entropy in [0.5, 0.3, 0.7, 0.2, 0.6]:
95
+ monitor.track("t", entropy)
96
+
97
+ stats = monitor.get_stats()
98
+
99
+ assert stats["token_count"] == 5
100
+ assert stats["min_entropy"] == 0.2
101
+ assert stats["max_entropy"] == 0.7
102
+ assert abs(stats["mean_entropy"] - 0.46) < 0.01
103
+
104
+ def test_reset(self):
105
+ """Should clear all tracked data."""
106
+ monitor = EntropyMonitor()
107
+
108
+ for i in range(10):
109
+ monitor.track("t", 0.5)
110
+
111
+ monitor.reset()
112
+
113
+ assert len(monitor.get_trajectory()) == 0
114
+ assert len(monitor.get_valleys()) == 0
115
+
116
+
117
+ class TestExitConditions:
118
+ """Tests for different exit conditions."""
119
+
120
+ def test_entropy_drop_exit(self):
121
+ """Should exit when entropy drops below threshold."""
122
+ monitor = EntropyMonitor(
123
+ entropy_threshold=0.15,
124
+ min_valleys=0,
125
+ min_tokens=5,
126
+ exit_condition="entropy_drop"
127
+ )
128
+
129
+ for i in range(5):
130
+ monitor.track("t", 0.1) # Below threshold
131
+
132
+ assert monitor.should_exit()
133
+
134
+ def test_velocity_zero_exit(self):
135
+ """Should exit when velocity stabilizes."""
136
+ monitor = EntropyMonitor(
137
+ velocity_threshold=0.01,
138
+ min_tokens=5,
139
+ exit_condition="velocity_zero"
140
+ )
141
+
142
+ # Start varying, then stabilize
143
+ for entropy in [0.5, 0.3, 0.4, 0.39, 0.395, 0.392]:
144
+ monitor.track("t", entropy)
145
+
146
+ # Should exit because velocity is low
147
+ assert monitor.should_exit()
148
+
149
+ def test_combined_exit(self):
150
+ """Combined condition should work."""
151
+ monitor = EntropyMonitor(
152
+ entropy_threshold=0.2,
153
+ velocity_threshold=0.05,
154
+ min_tokens=10,
155
+ min_valleys=2,
156
+ exit_condition="combined"
157
+ )
158
+
159
+ # Create trajectory with valleys and low entropy
160
+ trajectory = [0.8, 0.3, 0.8, 0.3, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15]
161
+ for e in trajectory:
162
+ monitor.track("t", e)
163
+
164
+ # Should exit: entropy is low and velocity is low
165
+ assert monitor.should_exit()
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "declaration": true,
7
+ "declarationMap": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src/**/*"],
18
+ "exclude": ["node_modules", "dist", "**/*.test.ts"]
19
+ }