holosphere 1.1.20 → 2.0.0-alpha1

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.
Files changed (147) hide show
  1. package/.env.example +36 -0
  2. package/.eslintrc.json +16 -0
  3. package/.prettierrc.json +7 -0
  4. package/LICENSE +162 -38
  5. package/README.md +483 -367
  6. package/bin/holosphere-activitypub.js +158 -0
  7. package/cleanup-test-data.js +204 -0
  8. package/examples/demo.html +1333 -0
  9. package/examples/example-bot.js +197 -0
  10. package/package.json +47 -87
  11. package/scripts/check-bundle-size.js +54 -0
  12. package/scripts/check-quest-ids.js +77 -0
  13. package/scripts/import-holons.js +578 -0
  14. package/scripts/publish-to-relay.js +101 -0
  15. package/scripts/read-example.js +186 -0
  16. package/scripts/relay-diagnostic.js +59 -0
  17. package/scripts/relay-example.js +179 -0
  18. package/scripts/resync-to-relay.js +245 -0
  19. package/scripts/revert-import.js +196 -0
  20. package/scripts/test-hybrid-mode.js +108 -0
  21. package/scripts/test-local-storage.js +63 -0
  22. package/scripts/test-nostr-direct.js +55 -0
  23. package/scripts/test-read-data.js +45 -0
  24. package/scripts/test-write-read.js +63 -0
  25. package/scripts/verify-import.js +95 -0
  26. package/scripts/verify-relay-data.js +139 -0
  27. package/src/ai/aggregation.js +319 -0
  28. package/src/ai/breakdown.js +511 -0
  29. package/src/ai/classifier.js +217 -0
  30. package/src/ai/council.js +228 -0
  31. package/src/ai/embeddings.js +279 -0
  32. package/src/ai/federation-ai.js +324 -0
  33. package/src/ai/h3-ai.js +955 -0
  34. package/src/ai/index.js +112 -0
  35. package/src/ai/json-ops.js +225 -0
  36. package/src/ai/llm-service.js +205 -0
  37. package/src/ai/nl-query.js +223 -0
  38. package/src/ai/relationships.js +353 -0
  39. package/src/ai/schema-extractor.js +218 -0
  40. package/src/ai/spatial.js +293 -0
  41. package/src/ai/tts.js +194 -0
  42. package/src/content/social-protocols.js +168 -0
  43. package/src/core/holosphere.js +273 -0
  44. package/src/crypto/secp256k1.js +259 -0
  45. package/src/federation/discovery.js +334 -0
  46. package/src/federation/hologram.js +1042 -0
  47. package/src/federation/registry.js +386 -0
  48. package/src/hierarchical/upcast.js +110 -0
  49. package/src/index.js +2669 -0
  50. package/src/schema/validator.js +91 -0
  51. package/src/spatial/h3-operations.js +110 -0
  52. package/src/storage/backend-factory.js +125 -0
  53. package/src/storage/backend-interface.js +142 -0
  54. package/src/storage/backends/activitypub/server.js +653 -0
  55. package/src/storage/backends/activitypub-backend.js +272 -0
  56. package/src/storage/backends/gundb-backend.js +233 -0
  57. package/src/storage/backends/nostr-backend.js +136 -0
  58. package/src/storage/filesystem-storage-browser.js +41 -0
  59. package/src/storage/filesystem-storage.js +138 -0
  60. package/src/storage/global-tables.js +81 -0
  61. package/src/storage/gun-async.js +281 -0
  62. package/src/storage/gun-wrapper.js +221 -0
  63. package/src/storage/indexeddb-storage.js +122 -0
  64. package/src/storage/key-storage-simple.js +76 -0
  65. package/src/storage/key-storage.js +136 -0
  66. package/src/storage/memory-storage.js +59 -0
  67. package/src/storage/migration.js +338 -0
  68. package/src/storage/nostr-async.js +811 -0
  69. package/src/storage/nostr-client.js +939 -0
  70. package/src/storage/nostr-wrapper.js +211 -0
  71. package/src/storage/outbox-queue.js +208 -0
  72. package/src/storage/persistent-storage.js +109 -0
  73. package/src/storage/sync-service.js +164 -0
  74. package/src/subscriptions/manager.js +142 -0
  75. package/test-ai-real-api.js +202 -0
  76. package/tests/unit/ai/aggregation.test.js +295 -0
  77. package/tests/unit/ai/breakdown.test.js +446 -0
  78. package/tests/unit/ai/classifier.test.js +294 -0
  79. package/tests/unit/ai/council.test.js +262 -0
  80. package/tests/unit/ai/embeddings.test.js +384 -0
  81. package/tests/unit/ai/federation-ai.test.js +344 -0
  82. package/tests/unit/ai/h3-ai.test.js +458 -0
  83. package/tests/unit/ai/index.test.js +304 -0
  84. package/tests/unit/ai/json-ops.test.js +307 -0
  85. package/tests/unit/ai/llm-service.test.js +390 -0
  86. package/tests/unit/ai/nl-query.test.js +383 -0
  87. package/tests/unit/ai/relationships.test.js +311 -0
  88. package/tests/unit/ai/schema-extractor.test.js +384 -0
  89. package/tests/unit/ai/spatial.test.js +279 -0
  90. package/tests/unit/ai/tts.test.js +279 -0
  91. package/tests/unit/content.test.js +332 -0
  92. package/tests/unit/contract/core.test.js +88 -0
  93. package/tests/unit/contract/crypto.test.js +198 -0
  94. package/tests/unit/contract/data.test.js +223 -0
  95. package/tests/unit/contract/federation.test.js +181 -0
  96. package/tests/unit/contract/hierarchical.test.js +113 -0
  97. package/tests/unit/contract/schema.test.js +114 -0
  98. package/tests/unit/contract/social.test.js +217 -0
  99. package/tests/unit/contract/spatial.test.js +110 -0
  100. package/tests/unit/contract/subscriptions.test.js +128 -0
  101. package/tests/unit/contract/utils.test.js +159 -0
  102. package/tests/unit/core.test.js +152 -0
  103. package/tests/unit/crypto.test.js +328 -0
  104. package/tests/unit/federation.test.js +234 -0
  105. package/tests/unit/gun-async.test.js +252 -0
  106. package/tests/unit/hierarchical.test.js +399 -0
  107. package/tests/unit/integration/scenario-01-geographic-storage.test.js +74 -0
  108. package/tests/unit/integration/scenario-02-federation.test.js +76 -0
  109. package/tests/unit/integration/scenario-03-subscriptions.test.js +102 -0
  110. package/tests/unit/integration/scenario-04-validation.test.js +129 -0
  111. package/tests/unit/integration/scenario-05-hierarchy.test.js +125 -0
  112. package/tests/unit/integration/scenario-06-social.test.js +135 -0
  113. package/tests/unit/integration/scenario-07-persistence.test.js +130 -0
  114. package/tests/unit/integration/scenario-08-authorization.test.js +161 -0
  115. package/tests/unit/integration/scenario-09-cross-dimensional.test.js +139 -0
  116. package/tests/unit/integration/scenario-10-cross-holosphere-capabilities.test.js +357 -0
  117. package/tests/unit/integration/scenario-11-cross-holosphere-federation.test.js +410 -0
  118. package/tests/unit/integration/scenario-12-capability-federated-read.test.js +719 -0
  119. package/tests/unit/performance/benchmark.test.js +85 -0
  120. package/tests/unit/schema.test.js +213 -0
  121. package/tests/unit/spatial.test.js +158 -0
  122. package/tests/unit/storage.test.js +195 -0
  123. package/tests/unit/subscriptions.test.js +328 -0
  124. package/tests/unit/test-data-permanence-debug.js +197 -0
  125. package/tests/unit/test-data-permanence.js +340 -0
  126. package/tests/unit/test-key-persistence-fixed.js +148 -0
  127. package/tests/unit/test-key-persistence.js +172 -0
  128. package/tests/unit/test-relay-permanence.js +376 -0
  129. package/tests/unit/test-second-node.js +95 -0
  130. package/tests/unit/test-simple-write.js +89 -0
  131. package/vite.config.js +49 -0
  132. package/vitest.config.js +20 -0
  133. package/FEDERATION.md +0 -213
  134. package/compute.js +0 -298
  135. package/content.js +0 -980
  136. package/federation.js +0 -1234
  137. package/global.js +0 -736
  138. package/hexlib.js +0 -335
  139. package/hologram.js +0 -183
  140. package/holosphere-bundle.esm.js +0 -33256
  141. package/holosphere-bundle.js +0 -33287
  142. package/holosphere-bundle.min.js +0 -39
  143. package/holosphere.d.ts +0 -601
  144. package/holosphere.js +0 -719
  145. package/node.js +0 -246
  146. package/schema.js +0 -139
  147. package/utils.js +0 -302
@@ -0,0 +1,279 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { TTS, VOICES, MODELS } from '../../../src/ai/tts.js';
3
+
4
+ describe('Unit: TTS', () => {
5
+ let tts;
6
+ let mockOpenAI;
7
+
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+
11
+ mockOpenAI = {
12
+ audio: {
13
+ speech: {
14
+ create: vi.fn()
15
+ }
16
+ }
17
+ };
18
+
19
+ tts = new TTS(mockOpenAI);
20
+ });
21
+
22
+ describe('Constants', () => {
23
+ it('should export VOICES', () => {
24
+ expect(VOICES.ALLOY).toBe('alloy');
25
+ expect(VOICES.ECHO).toBe('echo');
26
+ expect(VOICES.FABLE).toBe('fable');
27
+ expect(VOICES.ONYX).toBe('onyx');
28
+ expect(VOICES.NOVA).toBe('nova');
29
+ expect(VOICES.SHIMMER).toBe('shimmer');
30
+ });
31
+
32
+ it('should export MODELS', () => {
33
+ expect(MODELS.TTS_1).toBe('tts-1');
34
+ expect(MODELS.TTS_1_HD).toBe('tts-1-hd');
35
+ });
36
+ });
37
+
38
+ describe('Constructor', () => {
39
+ it('should initialize with OpenAI client', () => {
40
+ expect(tts.openai).toBe(mockOpenAI);
41
+ expect(tts.defaultVoice).toBe(VOICES.ECHO);
42
+ expect(tts.defaultModel).toBe(MODELS.TTS_1);
43
+ });
44
+ });
45
+
46
+ describe('speak', () => {
47
+ it('should convert text to speech', async () => {
48
+ const mockArrayBuffer = new ArrayBuffer(100);
49
+ mockOpenAI.audio.speech.create.mockResolvedValue({
50
+ arrayBuffer: () => Promise.resolve(mockArrayBuffer)
51
+ });
52
+
53
+ const result = await tts.speak('Hello world');
54
+
55
+ expect(mockOpenAI.audio.speech.create).toHaveBeenCalledWith({
56
+ model: 'tts-1',
57
+ voice: 'echo',
58
+ input: 'Hello world',
59
+ speed: 1.0,
60
+ response_format: 'mp3'
61
+ });
62
+ expect(Buffer.isBuffer(result)).toBe(true);
63
+ });
64
+
65
+ it('should accept custom options', async () => {
66
+ mockOpenAI.audio.speech.create.mockResolvedValue({
67
+ arrayBuffer: () => Promise.resolve(new ArrayBuffer(10))
68
+ });
69
+
70
+ await tts.speak('Text', {
71
+ voice: VOICES.NOVA,
72
+ model: MODELS.TTS_1_HD,
73
+ speed: 1.5,
74
+ responseFormat: 'opus'
75
+ });
76
+
77
+ expect(mockOpenAI.audio.speech.create).toHaveBeenCalledWith({
78
+ model: 'tts-1-hd',
79
+ voice: 'nova',
80
+ input: 'Text',
81
+ speed: 1.5,
82
+ response_format: 'opus'
83
+ });
84
+ });
85
+
86
+ it('should throw error on API failure', async () => {
87
+ mockOpenAI.audio.speech.create.mockRejectedValue(new Error('API Error'));
88
+
89
+ await expect(tts.speak('Text'))
90
+ .rejects.toThrow('TTS failed: API Error');
91
+ });
92
+ });
93
+
94
+ describe('speakBase64', () => {
95
+ it('should return base64 encoded audio', async () => {
96
+ const mockData = Buffer.from('audio data');
97
+ mockOpenAI.audio.speech.create.mockResolvedValue({
98
+ arrayBuffer: () => Promise.resolve(mockData.buffer)
99
+ });
100
+
101
+ const result = await tts.speakBase64('Hello');
102
+
103
+ expect(typeof result).toBe('string');
104
+ // Base64 string should be decodable
105
+ expect(() => Buffer.from(result, 'base64')).not.toThrow();
106
+ });
107
+ });
108
+
109
+ describe('speakDataUrl', () => {
110
+ it('should return data URL for audio', async () => {
111
+ const mockData = Buffer.from('audio data');
112
+ mockOpenAI.audio.speech.create.mockResolvedValue({
113
+ arrayBuffer: () => Promise.resolve(mockData.buffer)
114
+ });
115
+
116
+ const result = await tts.speakDataUrl('Hello');
117
+
118
+ expect(result).toMatch(/^data:audio\/mpeg;base64,/);
119
+ });
120
+
121
+ it('should use correct mime type for format', async () => {
122
+ const mockData = Buffer.from('audio');
123
+ mockOpenAI.audio.speech.create.mockResolvedValue({
124
+ arrayBuffer: () => Promise.resolve(mockData.buffer)
125
+ });
126
+
127
+ const result = await tts.speakDataUrl('Text', { responseFormat: 'opus' });
128
+
129
+ expect(result).toMatch(/^data:audio\/opus;base64,/);
130
+ });
131
+ });
132
+
133
+ describe('setDefaultVoice', () => {
134
+ it('should set default voice', () => {
135
+ tts.setDefaultVoice(VOICES.NOVA);
136
+ expect(tts.defaultVoice).toBe(VOICES.NOVA);
137
+ });
138
+
139
+ it('should throw error for invalid voice', () => {
140
+ expect(() => tts.setDefaultVoice('invalid'))
141
+ .toThrow('Invalid voice: invalid');
142
+ });
143
+ });
144
+
145
+ describe('setDefaultModel', () => {
146
+ it('should set default model', () => {
147
+ tts.setDefaultModel(MODELS.TTS_1_HD);
148
+ expect(tts.defaultModel).toBe(MODELS.TTS_1_HD);
149
+ });
150
+
151
+ it('should throw error for invalid model', () => {
152
+ expect(() => tts.setDefaultModel('invalid'))
153
+ .toThrow('Invalid model: invalid');
154
+ });
155
+ });
156
+
157
+ describe('getVoices', () => {
158
+ it('should return list of available voices', () => {
159
+ const voices = TTS.getVoices();
160
+
161
+ expect(voices).toContain('alloy');
162
+ expect(voices).toContain('echo');
163
+ expect(voices).toContain('fable');
164
+ expect(voices).toContain('onyx');
165
+ expect(voices).toContain('nova');
166
+ expect(voices).toContain('shimmer');
167
+ expect(voices).toHaveLength(6);
168
+ });
169
+ });
170
+
171
+ describe('getModels', () => {
172
+ it('should return list of available models', () => {
173
+ const models = TTS.getModels();
174
+
175
+ expect(models).toContain('tts-1');
176
+ expect(models).toContain('tts-1-hd');
177
+ expect(models).toHaveLength(2);
178
+ });
179
+ });
180
+
181
+ describe('estimateDuration', () => {
182
+ it('should estimate duration for text', () => {
183
+ // ~150 words per minute = 2.5 words per second
184
+ const duration = TTS.estimateDuration('one two three four five six seven eight nine ten');
185
+
186
+ // 10 words / 150 wpm * 60 = 4 seconds
187
+ expect(duration).toBeCloseTo(4, 1);
188
+ });
189
+
190
+ it('should adjust for speed', () => {
191
+ const normalSpeed = TTS.estimateDuration('word word word word', 1.0);
192
+ const doubleSpeed = TTS.estimateDuration('word word word word', 2.0);
193
+
194
+ expect(doubleSpeed).toBeCloseTo(normalSpeed / 2, 1);
195
+ });
196
+
197
+ it('should handle empty text', () => {
198
+ const duration = TTS.estimateDuration('');
199
+ // Empty string split produces [""] with 1 word count, so it's ~0.4 seconds
200
+ expect(duration).toBeCloseTo(0.4, 1);
201
+ });
202
+ });
203
+
204
+ describe('splitText', () => {
205
+ it('should return single chunk for short text', () => {
206
+ const chunks = TTS.splitText('Short text');
207
+ expect(chunks).toHaveLength(1);
208
+ expect(chunks[0]).toBe('Short text');
209
+ });
210
+
211
+ it('should split long text by sentences', () => {
212
+ const longText = 'First sentence. ' + 'A'.repeat(4000) + '. Last sentence.';
213
+ const chunks = TTS.splitText(longText, 4000);
214
+
215
+ expect(chunks.length).toBeGreaterThan(1);
216
+ chunks.forEach(chunk => {
217
+ expect(chunk.length).toBeLessThanOrEqual(4100); // Allow some margin
218
+ });
219
+ });
220
+
221
+ it('should respect maxChars parameter', () => {
222
+ const text = 'Sentence one. Sentence two. Sentence three. Sentence four.';
223
+ const chunks = TTS.splitText(text, 30);
224
+
225
+ expect(chunks.length).toBeGreaterThan(1);
226
+ });
227
+
228
+ it('should handle text with different sentence endings', () => {
229
+ const text = 'Question? Exclamation! Statement. Another one.';
230
+ const chunks = TTS.splitText(text, 20);
231
+
232
+ expect(chunks.length).toBeGreaterThan(1);
233
+ });
234
+ });
235
+
236
+ describe('speakLong', () => {
237
+ it('should split and speak long text', async () => {
238
+ const mockData = Buffer.from('audio');
239
+ mockOpenAI.audio.speech.create.mockResolvedValue({
240
+ arrayBuffer: () => Promise.resolve(mockData.buffer)
241
+ });
242
+
243
+ const longText = 'First sentence. Second sentence. Third sentence.';
244
+ const result = await tts.speakLong(longText, { maxChars: 20 });
245
+
246
+ expect(Array.isArray(result)).toBe(true);
247
+ expect(result.length).toBeGreaterThan(1);
248
+ result.forEach(buffer => {
249
+ expect(Buffer.isBuffer(buffer)).toBe(true);
250
+ });
251
+ });
252
+
253
+ it('should use default maxChars when not specified', async () => {
254
+ const mockData = Buffer.from('audio');
255
+ mockOpenAI.audio.speech.create.mockResolvedValue({
256
+ arrayBuffer: () => Promise.resolve(mockData.buffer)
257
+ });
258
+
259
+ const shortText = 'Short text';
260
+ const result = await tts.speakLong(shortText);
261
+
262
+ // Short text should produce single buffer
263
+ expect(result).toHaveLength(1);
264
+ });
265
+
266
+ it('should process chunks in parallel', async () => {
267
+ const mockData = Buffer.from('audio');
268
+ mockOpenAI.audio.speech.create.mockResolvedValue({
269
+ arrayBuffer: () => Promise.resolve(mockData.buffer)
270
+ });
271
+
272
+ const text = 'One. Two. Three. Four. Five.';
273
+ await tts.speakLong(text, { maxChars: 10 });
274
+
275
+ // Should make multiple parallel calls
276
+ expect(mockOpenAI.audio.speech.create.mock.calls.length).toBeGreaterThan(1);
277
+ });
278
+ });
279
+ });
@@ -0,0 +1,332 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import HoloSphere from '../../src/index.js';
3
+
4
+ describe('Unit: Content Module', () => {
5
+ let hs;
6
+ let testHolonId;
7
+
8
+ beforeEach(async () => {
9
+ hs = new HoloSphere({ relays: [], appName: 'test-content-unit' });
10
+ testHolonId = await hs.toHolon(37.7749, -122.4194, 9);
11
+ });
12
+
13
+ describe('Nostr event format validation (NIP-01)', () => {
14
+ it('should validate NIP-01 event structure', async () => {
15
+ const validEvent = {
16
+ kind: 1,
17
+ content: 'Hello Nostr',
18
+ tags: [],
19
+ created_at: Math.floor(Date.now() / 1000)
20
+ };
21
+
22
+ await expect(
23
+ hs.publishNostr(validEvent, testHolonId)
24
+ ).resolves.toBe(true);
25
+ });
26
+
27
+ it('should reject event without required kind field', async () => {
28
+ const invalidEvent = {
29
+ content: 'Missing kind',
30
+ tags: [],
31
+ created_at: Math.floor(Date.now() / 1000)
32
+ };
33
+
34
+ await expect(
35
+ hs.publishNostr(invalidEvent, testHolonId)
36
+ ).rejects.toThrow('ValidationError');
37
+ });
38
+
39
+ it('should reject event without content field', async () => {
40
+ const invalidEvent = {
41
+ kind: 1,
42
+ tags: [],
43
+ created_at: Math.floor(Date.now() / 1000)
44
+ };
45
+
46
+ await expect(
47
+ hs.publishNostr(invalidEvent, testHolonId)
48
+ ).rejects.toThrow('ValidationError');
49
+ });
50
+
51
+ it('should validate tags array format', async () => {
52
+ const eventWithTags = {
53
+ kind: 1,
54
+ content: 'Tagged post',
55
+ tags: [
56
+ ['e', 'event-id'],
57
+ ['p', 'pubkey'],
58
+ ['t', 'hashtag']
59
+ ],
60
+ created_at: Math.floor(Date.now() / 1000)
61
+ };
62
+
63
+ await expect(
64
+ hs.publishNostr(eventWithTags, testHolonId)
65
+ ).resolves.toBe(true);
66
+ });
67
+
68
+ it('should validate created_at is a number', async () => {
69
+ const invalidEvent = {
70
+ kind: 1,
71
+ content: 'Test',
72
+ tags: [],
73
+ created_at: 'not-a-number'
74
+ };
75
+
76
+ await expect(
77
+ hs.publishNostr(invalidEvent, testHolonId)
78
+ ).rejects.toThrow('ValidationError');
79
+ });
80
+
81
+ it('should support different event kinds', async () => {
82
+ const textNote = { kind: 1, content: 'Text', tags: [], created_at: Date.now() / 1000 };
83
+ const metadata = { kind: 0, content: '{}', tags: [], created_at: Date.now() / 1000 };
84
+ const reaction = { kind: 7, content: '+', tags: [], created_at: Date.now() / 1000 };
85
+
86
+ await expect(hs.publishNostr(textNote, testHolonId)).resolves.toBe(true);
87
+ await expect(hs.publishNostr(metadata, testHolonId)).resolves.toBe(true);
88
+ await expect(hs.publishNostr(reaction, testHolonId)).resolves.toBe(true);
89
+ });
90
+ });
91
+
92
+ describe('ActivityPub object format validation', () => {
93
+ it('should validate ActivityPub object structure', async () => {
94
+ const validObject = {
95
+ '@context': 'https://www.w3.org/ns/activitystreams',
96
+ type: 'Note',
97
+ content: 'Hello ActivityPub'
98
+ };
99
+
100
+ await expect(
101
+ hs.publishActivityPub(validObject, testHolonId)
102
+ ).resolves.toBe(true);
103
+ });
104
+
105
+ it('should require @context field', async () => {
106
+ const invalidObject = {
107
+ type: 'Note',
108
+ content: 'Missing context'
109
+ };
110
+
111
+ await expect(
112
+ hs.publishActivityPub(invalidObject, testHolonId)
113
+ ).rejects.toThrow('ValidationError');
114
+ });
115
+
116
+ it('should require type field', async () => {
117
+ const invalidObject = {
118
+ '@context': 'https://www.w3.org/ns/activitystreams',
119
+ content: 'Missing type'
120
+ };
121
+
122
+ await expect(
123
+ hs.publishActivityPub(invalidObject, testHolonId)
124
+ ).rejects.toThrow('ValidationError');
125
+ });
126
+
127
+ it('should support different ActivityPub types', async () => {
128
+ const note = {
129
+ '@context': 'https://www.w3.org/ns/activitystreams',
130
+ type: 'Note',
131
+ content: 'A note'
132
+ };
133
+
134
+ const article = {
135
+ '@context': 'https://www.w3.org/ns/activitystreams',
136
+ type: 'Article',
137
+ name: 'Article Title',
138
+ content: 'Article content'
139
+ };
140
+
141
+ await expect(hs.publishActivityPub(note, testHolonId)).resolves.toBe(true);
142
+ await expect(hs.publishActivityPub(article, testHolonId)).resolves.toBe(true);
143
+ });
144
+
145
+ it('should validate JSON-LD context', async () => {
146
+ const validObject = {
147
+ '@context': [
148
+ 'https://www.w3.org/ns/activitystreams',
149
+ { customField: 'https://example.com/ns#customField' }
150
+ ],
151
+ type: 'Note',
152
+ content: 'Extended context'
153
+ };
154
+
155
+ await expect(
156
+ hs.publishActivityPub(validObject, testHolonId)
157
+ ).resolves.toBe(true);
158
+ });
159
+ });
160
+
161
+ describe('Protocol adapter interface', () => {
162
+ it('should use Nostr adapter for Nostr events', async () => {
163
+ const event = {
164
+ kind: 1,
165
+ content: 'Test',
166
+ tags: [],
167
+ created_at: Math.floor(Date.now() / 1000)
168
+ };
169
+
170
+ await hs.publishNostr(event, testHolonId);
171
+
172
+ const results = await hs.querySocial(testHolonId, { protocol: 'nostr' });
173
+ expect(results.some(r => r.content === 'Test')).toBe(true);
174
+ });
175
+
176
+ it('should use ActivityPub adapter for ActivityPub objects', async () => {
177
+ const object = {
178
+ '@context': 'https://www.w3.org/ns/activitystreams',
179
+ type: 'Note',
180
+ content: 'Test AP'
181
+ };
182
+
183
+ await hs.publishActivityPub(object, testHolonId);
184
+
185
+ const results = await hs.querySocial(testHolonId, { protocol: 'activitypub' });
186
+ expect(results.some(r => r.content === 'Test AP')).toBe(true);
187
+ });
188
+
189
+ it('should distinguish between protocols', async () => {
190
+ await hs.publishNostr({
191
+ kind: 1,
192
+ content: 'Nostr',
193
+ tags: [],
194
+ created_at: Math.floor(Date.now() / 1000)
195
+ }, testHolonId);
196
+
197
+ await hs.publishActivityPub({
198
+ '@context': 'https://www.w3.org/ns/activitystreams',
199
+ type: 'Note',
200
+ content: 'ActivityPub'
201
+ }, testHolonId);
202
+
203
+ const nostrResults = await hs.querySocial(testHolonId, { protocol: 'nostr' });
204
+ const apResults = await hs.querySocial(testHolonId, { protocol: 'activitypub' });
205
+
206
+ expect(nostrResults.length).toBeGreaterThan(0);
207
+ expect(apResults.length).toBeGreaterThan(0);
208
+ });
209
+ });
210
+
211
+ describe('Stigmergic access level filtering', () => {
212
+ it('should filter by public access level', async () => {
213
+ const results = await hs.querySocial(testHolonId, {
214
+ accessLevel: 'public'
215
+ });
216
+
217
+ expect(Array.isArray(results)).toBe(true);
218
+ });
219
+
220
+ it('should filter by protected access level', async () => {
221
+ const results = await hs.querySocial(testHolonId, {
222
+ accessLevel: 'protected'
223
+ });
224
+
225
+ expect(Array.isArray(results)).toBe(true);
226
+ });
227
+
228
+ it('should filter by private access level', async () => {
229
+ const results = await hs.querySocial(testHolonId, {
230
+ accessLevel: 'private'
231
+ });
232
+
233
+ expect(Array.isArray(results)).toBe(true);
234
+ });
235
+
236
+ it('should return all content when accessLevel is "all"', async () => {
237
+ const results = await hs.querySocial(testHolonId, {
238
+ accessLevel: 'all'
239
+ });
240
+
241
+ expect(Array.isArray(results)).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe('Content transformation (to/from HoloSphere format)', () => {
246
+ it('should transform Nostr event to HoloSphere format', async () => {
247
+ const nostrEvent = {
248
+ kind: 1,
249
+ content: 'Transform test',
250
+ tags: [],
251
+ created_at: Math.floor(Date.now() / 1000)
252
+ };
253
+
254
+ await hs.publishNostr(nostrEvent, testHolonId);
255
+
256
+ // Should be stored in HoloSphere format
257
+ const stored = await hs.querySocial(testHolonId, { protocol: 'nostr' });
258
+ expect(stored[0]).toHaveProperty('kind');
259
+ expect(stored[0]).toHaveProperty('content');
260
+ });
261
+
262
+ it('should transform ActivityPub object to HoloSphere format', async () => {
263
+ const apObject = {
264
+ '@context': 'https://www.w3.org/ns/activitystreams',
265
+ type: 'Note',
266
+ content: 'Transform test'
267
+ };
268
+
269
+ await hs.publishActivityPub(apObject, testHolonId);
270
+
271
+ // Should be stored in HoloSphere format
272
+ const stored = await hs.querySocial(testHolonId, { protocol: 'activitypub' });
273
+ expect(stored[0]).toHaveProperty('type');
274
+ expect(stored[0]).toHaveProperty('content');
275
+ });
276
+
277
+ it('should preserve original data during transformation', async () => {
278
+ const original = {
279
+ kind: 1,
280
+ content: 'Original content',
281
+ tags: [['t', 'test']],
282
+ created_at: Math.floor(Date.now() / 1000)
283
+ };
284
+
285
+ await hs.publishNostr(original, testHolonId);
286
+
287
+ const retrieved = await hs.querySocial(testHolonId, { protocol: 'nostr' });
288
+ const item = retrieved.find(r => r.content === 'Original content');
289
+
290
+ expect(item.kind).toBe(1);
291
+ expect(item.tags).toEqual([['t', 'test']]);
292
+ });
293
+ });
294
+
295
+ describe('Protocol-specific signatures', () => {
296
+ it('should add signature to Nostr events', async () => {
297
+ const event = {
298
+ kind: 1,
299
+ content: 'Signed event',
300
+ tags: [],
301
+ created_at: Math.floor(Date.now() / 1000)
302
+ };
303
+
304
+ await hs.publishNostr(event, testHolonId);
305
+
306
+ const published = await hs.querySocial(testHolonId, { protocol: 'nostr' });
307
+ const signedEvent = published.find(e => e.content === 'Signed event');
308
+
309
+ expect(signedEvent).toHaveProperty('sig');
310
+ expect(signedEvent).toHaveProperty('pubkey');
311
+ });
312
+
313
+ it('should verify Nostr event signatures', async () => {
314
+ const event = {
315
+ kind: 1,
316
+ content: 'Verify test',
317
+ tags: [],
318
+ created_at: Math.floor(Date.now() / 1000)
319
+ };
320
+
321
+ await hs.publishNostr(event, testHolonId);
322
+
323
+ const published = await hs.querySocial(testHolonId, { protocol: 'nostr' });
324
+ const signedEvent = published.find(e => e.content === 'Verify test');
325
+
326
+ if (signedEvent && signedEvent.sig && signedEvent.pubkey) {
327
+ const isValid = await hs.verifyNostrEvent(signedEvent);
328
+ expect(isValid).toBe(true);
329
+ }
330
+ });
331
+ });
332
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import HoloSphere from '../../../src/index.js';
3
+
4
+ describe('Contract: Core Initialization', () => {
5
+ it('should create instance with default config', () => {
6
+ const hs = new HoloSphere({ relays: [] });
7
+ expect(hs).toBeInstanceOf(HoloSphere);
8
+ });
9
+
10
+ it('should create instance with custom appName', () => {
11
+ const hs = new HoloSphere({ relays: [], appName: 'testapp' });
12
+ expect(hs).toBeInstanceOf(HoloSphere);
13
+ });
14
+
15
+ it('should create instance with custom peers', () => {
16
+ const hs = new HoloSphere({
17
+ appName: 'testapp',
18
+ peers: ['https://relay.example.com/gun']
19
+ });
20
+ expect(hs).toBeInstanceOf(HoloSphere);
21
+ });
22
+
23
+ it('should create instance with radisk config', () => {
24
+ const hs = new HoloSphere({
25
+ appName: 'testapp',
26
+ radisk: { path: './test-data' }
27
+ });
28
+ expect(hs).toBeInstanceOf(HoloSphere);
29
+ });
30
+
31
+ it('should create instance with custom log level', () => {
32
+ const hs = new HoloSphere({
33
+ appName: 'testapp',
34
+ logLevel: 'DEBUG'
35
+ });
36
+ expect(hs).toBeInstanceOf(HoloSphere);
37
+ });
38
+
39
+ it('should throw TypeError for invalid config', () => {
40
+ expect(() => new HoloSphere({ relays: [], appName: 123 })).toThrow(TypeError);
41
+ });
42
+
43
+ it('should have all expected methods', () => {
44
+ const hs = new HoloSphere({ relays: [] });
45
+
46
+ // Spatial operations
47
+ expect(typeof hs.toHolon).toBe('function');
48
+ expect(typeof hs.getParents).toBe('function');
49
+ expect(typeof hs.getChildren).toBe('function');
50
+ expect(typeof hs.isValidH3).toBe('function');
51
+
52
+ // Data operations
53
+ expect(typeof hs.write).toBe('function');
54
+ expect(typeof hs.read).toBe('function');
55
+ expect(typeof hs.update).toBe('function');
56
+ expect(typeof hs.delete).toBe('function');
57
+
58
+ // Schema operations
59
+ expect(typeof hs.setSchema).toBe('function');
60
+ expect(typeof hs.getSchema).toBe('function');
61
+ expect(typeof hs.clearSchema).toBe('function');
62
+
63
+ // Federation operations
64
+ expect(typeof hs.federate).toBe('function');
65
+ expect(typeof hs.getFederatedData).toBe('function');
66
+ expect(typeof hs.unfederate).toBe('function');
67
+
68
+ // Hierarchical operations
69
+ expect(typeof hs.upcast).toBe('function');
70
+
71
+ // Subscription operations
72
+ expect(typeof hs.subscribe).toBe('function');
73
+
74
+ // Crypto operations
75
+ expect(typeof hs.sign).toBe('function');
76
+ expect(typeof hs.verify).toBe('function');
77
+ expect(typeof hs.issueCapability).toBe('function');
78
+ expect(typeof hs.verifyCapability).toBe('function');
79
+
80
+ // Social operations
81
+ expect(typeof hs.publishNostr).toBe('function');
82
+ expect(typeof hs.publishActivityPub).toBe('function');
83
+ expect(typeof hs.querySocial).toBe('function');
84
+
85
+ // Utils
86
+ expect(typeof hs.metrics).toBe('function');
87
+ });
88
+ });