File size: 12,187 Bytes
d796d00
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
38391a8
3f78ea8
d796d00
 
38391a8
 
d796d00
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
38391a8
3f78ea8
d796d00
 
38391a8
 
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
 
 
d796d00
 
 
 
 
 
 
 
 
3f78ea8
d796d00
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
38391a8
d796d00
 
 
 
 
38391a8
d796d00
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
3f78ea8
d796d00
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
"""Input validation and stress tests for HearthNet.

Tests edge cases, large datasets, and system limits.
Validates backend input validation and sanitization.
"""

from __future__ import annotations

import asyncio
import pytest
import tempfile
from pathlib import Path


class TestInputValidation:
    """Verify backend input validation and sanitization."""

    @pytest.mark.asyncio
    async def test_chat_empty_recipient_rejected(self):
        """Chat service should reject messages with missing recipient."""
        from hearthnet.node import InMemoryNetwork

        net = InMemoryNetwork()
        node = net.add_node("val-chat", "Val Chat", "ed25519:val_chat")
        node.install_demo_services()

        result = await node.bus.call(
            "chat.send",
            (1, 0),
            {"input": {"recipient": "", "body": "test message"}},
        )
        # Should return error
        assert "error" in result, "Should reject empty recipient"
        print(f"\n  Empty recipient rejected: {result.get('error')}")

    @pytest.mark.asyncio
    async def test_chat_self_message_rejected(self):
        """Chat service should reject sending to self."""
        from hearthnet.node import InMemoryNetwork

        net = InMemoryNetwork()
        node = net.add_node("val-self", "Val Self", "ed25519:val_self")
        node.install_demo_services()

        result = await node.bus.call(
            "chat.send",
            (1, 0),
            {"input": {"recipient": "val-self", "body": "test"}},
        )
        # Should return error
        assert "error" in result, "Should reject self-send"
        print(f"\n  Self-send rejected: {result.get('error')}")

    @pytest.mark.asyncio
    async def test_embedding_max_texts_enforced(self):
        """Embedding service should enforce max text limit."""
        from hearthnet.services.embedding.service import EmbeddingService
        from hearthnet.bus.capability import RouteRequest
        from hearthnet.constants import EMBED_MAX_TEXTS

        svc = EmbeddingService()

        # Try to embed too many texts
        too_many = ["text"] * (EMBED_MAX_TEXTS + 10)
        req = RouteRequest(
            capability="embedding.embed",
            version_req=(1, 0),
            body={"input": {"texts": too_many, "normalize": False}},
            caller="test",
            trace_id="t1",
        )
        result = await svc.handle_embed(req)

        if "error" in result:
            print(f"\n  Max texts enforced: {result.get('error')}")
            msg = str(result.get("message", result.get("error", ""))).lower()
            assert "too many" in msg or "bad_request" in result.get("error", "")

    @pytest.mark.asyncio
    async def test_embedding_max_chars_enforced(self):
        """Embedding service should enforce max character limit."""
        from hearthnet.services.embedding.service import EmbeddingService
        from hearthnet.bus.capability import RouteRequest
        from hearthnet.constants import EMBED_MAX_CHARS

        svc = EmbeddingService()

        # Text that's too long
        too_long_text = "x" * (EMBED_MAX_CHARS + 100)
        req = RouteRequest(
            capability="embedding.embed",
            version_req=(1, 0),
            body={"input": {"texts": [too_long_text], "normalize": False}},
            caller="test",
            trace_id="t1",
        )
        result = await svc.handle_embed(req)

        if "error" in result:
            print(f"\n  Max chars enforced: {result.get('error')}")
            msg = str(result.get("message", result.get("error", ""))).lower()
            assert "too long" in msg or "bad_request" in result.get("error", "")

    @pytest.mark.asyncio
    async def test_file_invalid_base64_rejected(self):
        """File service should reject invalid base64."""
        from hearthnet.services.files.service import FileService
        from hearthnet.bus.capability import RouteRequest

        svc = FileService()
        req = RouteRequest(
            capability="file.put",
            version_req=(1, 0),
            body={"input": {"filename": "test.txt", "data_b64": "not@valid@base64!!!"}},
            caller="test",
            trace_id="t1",
        )
        result = await svc.handle_put(req)

        assert result.get("error") is not None, "Should reject invalid base64"
        print(f"\n  Invalid base64 rejected: {result.get('error')}")

    @pytest.mark.asyncio
    async def test_file_missing_cid_returns_error(self):
        """File service should return error for missing CID."""
        from hearthnet.services.files.service import FileService
        from hearthnet.bus.capability import RouteRequest

        svc = FileService()
        req = RouteRequest(
            capability="file.get",
            version_req=(1, 0),
            body={"input": {"cid": ""}},
            caller="test",
            trace_id="t1",
        )
        result = await svc.handle_get(req)

        assert result.get("error") is not None, "Should reject missing CID"
        print(f"\n  Missing CID returns error: {result.get('error')}")


class TestStressConditions:
    """Stress tests for edge cases and limits."""

    @pytest.mark.asyncio
    async def test_marketplace_many_listings(self):
        """Marketplace should handle multiple listings."""
        from hearthnet.node import InMemoryNetwork

        net = InMemoryNetwork()
        node = net.add_node("stress-market", "Stress Market", "ed25519:stress_m")
        node.install_demo_services()

        # Post many listings
        for i in range(20):
            result = await node.bus.call(
                "market.post",
                (1, 0),
                {
                    "input": {
                        "title": f"Listing {i}",
                        "body": f"Description {i}",
                        "category": "info",
                    }
                },
            )
            assert "output" in result, f"Failed posting {i}"

        # List should work
        list_result = await node.bus.call(
            "market.list",
            (1, 0),
            {"input": {"limit": 100}},
        )
        listings = list_result.get("output", {}).get(
            "posts", list_result.get("output", {}).get("listings", [])
        )
        print(f"\n  Posted {len(listings)} marketplace listings")
        assert len(listings) >= 10, f"Expected >= 10 listings, got {len(listings)}"

    def test_large_blob_chunking(self):
        """Blob chunker should handle large files."""
        from hearthnet.blobs.chunker import chunk_blob

        # 5MB blob
        large_data = b"x" * (5 * 1024 * 1024)

        manifest, chunks = chunk_blob(large_data)

        # Verify integrity
        reassembled = b"".join(chunks)
        assert reassembled == large_data, "Reassembled data should match original"
        assert len(chunks) > 5, "Should have multiple chunks for large file"
        print(f"\n  Large blob: {len(chunks)} chunks, reassembled correctly")

    def test_event_log_many_entries(self):
        """Event log should handle many entries."""
        from hearthnet.events.log import EventLog
        import gc

        td = tempfile.mkdtemp()
        try:
            log = EventLog(Path(td) / "stress.db", "stress-community")

            # Add many events
            for i in range(50):
                log.append_local(
                    "community.member.joined",
                    f"author-{i % 5}",
                    {"index": i, "data": f"event data {i}"},
                )

            # Query should still work
            events = log.since(0, limit=100)
            assert len(events) >= 45, f"Only {len(events)}/50 events stored"
            print(f"\n  Event log: {len(events)} entries stored and retrieved")

            if hasattr(log, "_conn") and log._conn:
                log._conn.close()
            del log
            gc.collect()
        finally:
            import shutil

            try:
                shutil.rmtree(td, ignore_errors=True)
            except Exception:
                pass

    @pytest.mark.asyncio
    async def test_concurrent_marketplace_posts(self):
        """Should handle concurrent marketplace postings."""
        from hearthnet.node import InMemoryNetwork

        net = InMemoryNetwork()
        node = net.add_node("stress-concurrent", "Stress Concurrent", "ed25519:stress_c")
        node.install_demo_services()

        async def post_listing(i):
            try:
                result = await node.bus.call(
                    "market.post",
                    (1, 0),
                    {
                        "input": {
                            "title": f"Concurrent {i}",
                            "body": f"Desc {i}",
                            "category": "info",
                        }
                    },
                )
                return "output" in result
            except Exception:
                return False

        # Post 15 concurrently
        tasks = [post_listing(i) for i in range(15)]
        results = await asyncio.gather(*tasks)

        successful = sum(1 for r in results if r)
        print(f"\n  Concurrent posts: {successful}/15 succeeded")
        assert successful >= 10, f"Only {successful}/15 concurrent posts succeeded"


class TestComplexityEdgeCases:
    """Test edge cases and complexity scenarios."""

    @pytest.mark.asyncio
    async def test_unicode_content_handling(self):
        """Services should handle unicode content correctly."""
        from hearthnet.node import InMemoryNetwork

        net = InMemoryNetwork()
        node = net.add_node("unicode-test", "Unicode Test", "ed25519:unicode")
        node.install_demo_services()

        # Send unicode message
        result = await node.bus.call(
            "chat.send",
            (1, 0),
            {
                "input": {
                    "recipient": "other-node",
                    "body": "Hello 你好 مرحبا Привет 🚀✨",
                }
            },
        )
        # Should handle without crashing
        assert isinstance(result, dict), "Should return result dict"
        print(f"\n  Unicode handling: OK (result keys: {list(result.keys())})")

    def test_malformed_json_handling(self):
        """Event log should handle edge cases gracefully."""
        from hearthnet.events.log import EventLog
        import gc

        td = tempfile.mkdtemp()
        try:
            log = EventLog(Path(td) / "edge.db", "edge-community")

            # Try to handle edge case events
            try:
                log.append_local("edge.event", "", {"data": None})
            except Exception as e:
                print(f"\n  Edge case handled gracefully: {type(e).__name__}")
                pass  # Should not crash

            if hasattr(log, "_conn") and log._conn:
                log._conn.close()
            del log
            gc.collect()
        finally:
            import shutil

            try:
                shutil.rmtree(td, ignore_errors=True)
            except Exception:
                pass

    @pytest.mark.asyncio
    async def test_rag_with_empty_corpus(self):
        """RAG should handle queries on empty corpus gracefully."""
        from hearthnet.node import InMemoryNetwork

        net = InMemoryNetwork()
        node = net.add_node("rag-empty", "RAG Empty", "ed25519:rag_empty")
        node.install_demo_services(corpus="empty-corpus")

        # Query without ingesting anything
        try:
            result = await node.bus.call(
                "rag.query",
                (1, 0),
                {
                    "params": {"corpus": "empty-corpus"},
                    "input": {"query": "test query", "limit": 5},
                },
            )
            chunks = result.get("output", {}).get("chunks", [])
            # Should return empty list, not crash
            assert isinstance(chunks, list), "Should return list"
            print(f"\n  Empty RAG corpus handled: returned {len(chunks)} chunks (OK)")
        except Exception as e:
            print(f"\n  Empty RAG query raised: {type(e).__name__} (acceptable)")