Browse Source

add tag and event tests

master
limina1 8 months ago
parent
commit
5e09619bca
  1. 18
      .gitignore
  2. 2
      src/lib/navigator/EventNetwork/index.svelte
  3. 2
      src/routes/visualize/+page.svelte
  4. 457
      tests/unit/relayDeduplication.test.ts
  5. 420
      tests/unit/tagExpansion.test.ts

18
.gitignore vendored

@ -9,21 +9,9 @@ node_modules @@ -9,21 +9,9 @@ node_modules
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
# tests - ignore all test directories and files
/tests/
/test/
/__tests__/
*.test.js
*.test.ts
*.spec.js
*.spec.ts
*.test.svelte
*.spec.svelte
/coverage/
/.nyc_output/
# documentation
/docs/
# tests
/tests/e2e/html-report/*.html
/tests/e2e/test-results/*.last-run.json
# Deno
/.deno/

2
src/lib/navigator/EventNetwork/index.svelte

@ -994,7 +994,7 @@ @@ -994,7 +994,7 @@
});
/**
* Watch for tag expansion depth changes
* Watch for tag expansion changes
*/
$effect(() => {
// Skip if not initialized or no callback

2
src/routes/visualize/+page.svelte

@ -687,7 +687,7 @@ @@ -687,7 +687,7 @@
await fetchProfilesForNewEvents(
newPublications,
newContentEvents,
(progress) => { profileLoadingProgress = progress; },
(progress: { current: number; total: number } | null) => { profileLoadingProgress = progress; },
debug
);

457
tests/unit/relayDeduplication.test.ts

@ -0,0 +1,457 @@ @@ -0,0 +1,457 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import {
deduplicateContentEvents,
deduplicateAndCombineEvents,
isReplaceableEvent,
getEventCoordinate
} from '../../src/lib/utils/eventDeduplication';
// Mock NDKEvent for testing
class MockNDKEvent {
id: string;
kind: number;
pubkey: string;
created_at: number;
content: string;
tags: string[][];
constructor(id: string, kind: number, pubkey: string, created_at: number, dTag: string, content: string = '') {
this.id = id;
this.kind = kind;
this.pubkey = pubkey;
this.created_at = created_at;
this.content = content;
this.tags = [['d', dTag]];
}
tagValue(tagName: string): string | undefined {
const tag = this.tags.find(t => t[0] === tagName);
return tag ? tag[1] : undefined;
}
}
describe('Relay Deduplication Behavior Tests', () => {
let mockEvents: MockNDKEvent[];
beforeEach(() => {
// Create test events with different timestamps
mockEvents = [
// Older version of a publication content event
new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old content'),
// Newer version of the same publication content event
new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'Updated content'),
// Different publication content event
new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-2', 'Different content'),
// Publication index event (should not be deduplicated)
new MockNDKEvent('event4', 30040, 'pubkey1', 1200, 'book-1', 'Index content'),
// Regular text note (should not be deduplicated)
new MockNDKEvent('event5', 1, 'pubkey1', 1300, '', 'Regular note'),
];
});
describe('Addressable Event Deduplication', () => {
it('should keep only the most recent version of addressable events by coordinate', () => {
// Test the deduplication logic for content events
const eventSets = [new Set(mockEvents.filter(e => e.kind === 30041) as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Should have 2 unique coordinates: chapter-1 and chapter-2
expect(result.size).toBe(2);
// Should keep the newer version of chapter-1
const chapter1Event = result.get('30041:pubkey1:chapter-1');
expect(chapter1Event?.id).toBe('event2');
expect(chapter1Event?.content).toBe('Updated content');
// Should keep chapter-2
const chapter2Event = result.get('30041:pubkey1:chapter-2');
expect(chapter2Event?.id).toBe('event3');
});
it('should handle events with missing d-tags gracefully', () => {
const eventWithoutDTag = new MockNDKEvent('event6', 30041, 'pubkey1', 1400, '', 'No d-tag');
eventWithoutDTag.tags = []; // Remove d-tag
const eventSets = [new Set([eventWithoutDTag] as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Should not include events without d-tags
expect(result.size).toBe(0);
});
it('should handle events with missing timestamps', () => {
const eventWithoutTimestamp = new MockNDKEvent('event7', 30041, 'pubkey1', 0, 'chapter-3', 'No timestamp');
const eventWithTimestamp = new MockNDKEvent('event8', 30041, 'pubkey1', 1500, 'chapter-3', 'With timestamp');
const eventSets = [new Set([eventWithoutTimestamp, eventWithTimestamp] as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Should prefer the event with timestamp
const chapter3Event = result.get('30041:pubkey1:chapter-3');
expect(chapter3Event?.id).toBe('event8');
});
});
describe('Mixed Event Type Deduplication', () => {
it('should only deduplicate addressable events (kinds 30000-39999)', () => {
const result = deduplicateAndCombineEvents(
[mockEvents[4]] as NDKEvent[], // Regular text note
new Set([mockEvents[3]] as NDKEvent[]), // Publication index
new Set([mockEvents[0], mockEvents[1], mockEvents[2]] as NDKEvent[]) // Content events
);
// Should have 4 events total:
// - 1 regular text note (not deduplicated)
// - 1 publication index (not deduplicated)
// - 2 unique content events (deduplicated from 3)
expect(result.length).toBe(4);
// Verify the content events were deduplicated
const contentEvents = result.filter(e => e.kind === 30041);
expect(contentEvents.length).toBe(2);
// Verify the newer version was kept
const newerEvent = contentEvents.find(e => e.id === 'event2');
expect(newerEvent).toBeDefined();
});
it('should handle non-addressable events correctly', () => {
const regularEvents = [
new MockNDKEvent('note1', 1, 'pubkey1', 1000, '', 'Note 1'),
new MockNDKEvent('note2', 1, 'pubkey1', 2000, '', 'Note 2'),
new MockNDKEvent('profile1', 0, 'pubkey1', 1500, '', 'Profile 1'),
];
const result = deduplicateAndCombineEvents(
regularEvents as NDKEvent[],
new Set(),
new Set()
);
// All regular events should be included (no deduplication)
expect(result.length).toBe(3);
});
});
describe('Coordinate System Validation', () => {
it('should correctly identify event coordinates', () => {
const event = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'test-chapter');
const coordinate = getEventCoordinate(event as NDKEvent);
expect(coordinate).toBe('30041:pubkey123:test-chapter');
});
it('should handle d-tags with colons correctly', () => {
const event = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'chapter:with:colons');
const coordinate = getEventCoordinate(event as NDKEvent);
expect(coordinate).toBe('30041:pubkey123:chapter:with:colons');
});
it('should return null for non-replaceable events', () => {
const event = new MockNDKEvent('test', 1, 'pubkey123', 1000, '');
const coordinate = getEventCoordinate(event as NDKEvent);
expect(coordinate).toBeNull();
});
});
describe('Replaceable Event Detection', () => {
it('should correctly identify replaceable events', () => {
const addressableEvent = new MockNDKEvent('test', 30041, 'pubkey123', 1000, 'test');
const regularEvent = new MockNDKEvent('test', 1, 'pubkey123', 1000, '');
expect(isReplaceableEvent(addressableEvent as NDKEvent)).toBe(true);
expect(isReplaceableEvent(regularEvent as NDKEvent)).toBe(false);
});
it('should handle edge cases of replaceable event ranges', () => {
const event29999 = new MockNDKEvent('test', 29999, 'pubkey123', 1000, 'test');
const event30000 = new MockNDKEvent('test', 30000, 'pubkey123', 1000, 'test');
const event39999 = new MockNDKEvent('test', 39999, 'pubkey123', 1000, 'test');
const event40000 = new MockNDKEvent('test', 40000, 'pubkey123', 1000, 'test');
expect(isReplaceableEvent(event29999 as NDKEvent)).toBe(false);
expect(isReplaceableEvent(event30000 as NDKEvent)).toBe(true);
expect(isReplaceableEvent(event39999 as NDKEvent)).toBe(true);
expect(isReplaceableEvent(event40000 as NDKEvent)).toBe(false);
});
});
describe('Edge Cases', () => {
it('should handle empty event sets', () => {
const result = deduplicateContentEvents([]);
expect(result.size).toBe(0);
});
it('should handle events with null/undefined values', () => {
const invalidEvent = {
id: undefined,
kind: 30041,
pubkey: 'pubkey1',
created_at: 1000,
tagValue: () => undefined, // Return undefined for d-tag
} as unknown as NDKEvent;
const eventSets = [new Set([invalidEvent])];
const result = deduplicateContentEvents(eventSets);
// Should handle gracefully without crashing
expect(result.size).toBe(0);
});
it('should handle events from different authors with same d-tag', () => {
const event1 = new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'same-chapter', 'Author 1');
const event2 = new MockNDKEvent('event2', 30041, 'pubkey2', 1000, 'same-chapter', 'Author 2');
const eventSets = [new Set([event1, event2] as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Should have 2 events (different coordinates due to different authors)
expect(result.size).toBe(2);
expect(result.has('30041:pubkey1:same-chapter')).toBe(true);
expect(result.has('30041:pubkey2:same-chapter')).toBe(true);
});
});
});
describe('Relay Behavior Simulation', () => {
it('should simulate what happens when relays return duplicate events', () => {
// Simulate a relay that returns multiple versions of the same event
const relayEvents = [
new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'),
new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'),
new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-1', 'Middle version'),
];
// This simulates what a "bad" relay might return
const eventSets = [new Set(relayEvents as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Should only keep the newest version
expect(result.size).toBe(1);
const keptEvent = result.get('30041:pubkey1:chapter-1');
expect(keptEvent?.id).toBe('event2');
expect(keptEvent?.content).toBe('New version');
});
it('should simulate multiple relays returning different versions', () => {
// Simulate multiple relays returning different versions
const relay1Events = [
new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Relay 1 version'),
];
const relay2Events = [
new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'Relay 2 version'),
];
const eventSets = [new Set(relay1Events as NDKEvent[]), new Set(relay2Events as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Should keep the newest version from any relay
expect(result.size).toBe(1);
const keptEvent = result.get('30041:pubkey1:chapter-1');
expect(keptEvent?.id).toBe('event2');
expect(keptEvent?.content).toBe('Relay 2 version');
});
});
describe('Real Relay Deduplication Tests', () => {
// These tests actually query real relays to see if they deduplicate
// Note: These are integration tests and may be flaky due to network conditions
it('should detect if relays are returning duplicate replaceable events', async () => {
// This test queries real relays to see if they return duplicates
// We'll use a known author who has published multiple versions of content
// Known author with multiple publication content events
const testAuthor = 'npub1z4m7gkva6yxgvdyclc7zp0qt69x9zgn8lu8sllg06wx6432h77qs0k97ks';
// Query for publication content events (kind 30041) from this author
// We expect relays to return only the most recent version of each d-tag
// This is a placeholder - in a real test, we would:
// 1. Query multiple relays for the same author's 30041 events
// 2. Check if any relay returns multiple events with the same d-tag
// 3. Verify that if duplicates exist, our deduplication logic handles them
console.log('Note: This test would require actual relay queries to verify deduplication behavior');
console.log('To run this test properly, we would need to:');
console.log('1. Query real relays for replaceable events');
console.log('2. Check if relays return duplicates');
console.log('3. Verify our deduplication logic works on real data');
// For now, we'll just assert that our logic is ready to handle real data
expect(true).toBe(true);
}, 30000); // 30 second timeout for network requests
it('should verify that our deduplication logic works on real relay data', async () => {
// This test would:
// 1. Fetch real events from relays
// 2. Apply our deduplication logic
// 3. Verify that the results are correct
console.log('Note: This test would require actual relay queries');
console.log('To implement this test, we would need to:');
console.log('1. Set up NDK with real relays');
console.log('2. Fetch events for a known author with multiple versions');
console.log('3. Apply deduplication and verify results');
expect(true).toBe(true);
}, 30000);
});
describe('Practical Relay Behavior Analysis', () => {
it('should document what we know about relay deduplication behavior', () => {
// This test documents our current understanding of relay behavior
// based on the code analysis and the comment from onedev
console.log('\n=== RELAY DEDUPLICATION BEHAVIOR ANALYSIS ===');
console.log('\nBased on the code analysis and the comment from onedev:');
console.log('\n1. THEORETICAL BEHAVIOR:');
console.log(' - Relays SHOULD handle deduplication for replaceable events');
console.log(' - Only the most recent version of each coordinate should be stored');
console.log(' - Client-side deduplication should only be needed for cached/local events');
console.log('\n2. REALITY CHECK:');
console.log(' - Not all relays implement deduplication correctly');
console.log(' - Some relays may return multiple versions of the same event');
console.log(' - Network conditions and relay availability can cause inconsistencies');
console.log('\n3. ALEXANDRIA\'S APPROACH:');
console.log(' - Implements client-side deduplication as a safety net');
console.log(' - Uses coordinate system (kind:pubkey:d-tag) for addressable events');
console.log(' - Keeps the most recent version based on created_at timestamp');
console.log(' - Only applies to replaceable events (kinds 30000-39999)');
console.log('\n4. WHY KEEP THE DEDUPLICATION:');
console.log(' - Defensive programming against imperfect relay implementations');
console.log(' - Handles multiple relay sources with different data');
console.log(' - Works with cached events that might be outdated');
console.log(' - Ensures consistent user experience regardless of relay behavior');
console.log('\n5. TESTING STRATEGY:');
console.log(' - Unit tests verify our deduplication logic works correctly');
console.log(' - Integration tests would verify relay behavior (when network allows)');
console.log(' - Monitoring can help determine if relays improve over time');
// This test documents our understanding rather than asserting specific behavior
expect(true).toBe(true);
});
it('should provide recommendations for when to remove deduplication', () => {
console.log('\n=== RECOMMENDATIONS FOR REMOVING DEDUPLICATION ===');
console.log('\nThe deduplication logic should be kept until:');
console.log('\n1. RELAY STANDARDS:');
console.log(' - NIP-33 (replaceable events) is widely implemented by relays');
console.log(' - Relays consistently return only the most recent version');
console.log(' - No major relay implementations return duplicates');
console.log('\n2. TESTING EVIDENCE:');
console.log(' - Real-world testing shows relays don\'t return duplicates');
console.log(' - Multiple relay operators confirm deduplication behavior');
console.log(' - No user reports of duplicate content issues');
console.log('\n3. MONITORING:');
console.log(' - Add logging to track when deduplication is actually used');
console.log(' - Monitor relay behavior over time');
console.log(' - Collect metrics on duplicate events found');
console.log('\n4. GRADUAL REMOVAL:');
console.log(' - Make deduplication configurable (on/off)');
console.log(' - Test with deduplication disabled in controlled environments');
console.log(' - Monitor for issues before removing completely');
console.log('\n5. FALLBACK STRATEGY:');
console.log(' - Keep deduplication as a fallback option');
console.log(' - Allow users to enable it if they experience issues');
console.log(' - Maintain the code for potential future use');
expect(true).toBe(true);
});
});
describe('Logging and Monitoring Tests', () => {
it('should verify that logging works when duplicates are found', () => {
// Mock console.log to capture output
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
// Create events with duplicates
const duplicateEvents = [
new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'),
new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'),
new MockNDKEvent('event3', 30041, 'pubkey1', 1500, 'chapter-1', 'Middle version'),
];
const eventSets = [new Set(duplicateEvents as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Verify the deduplication worked
expect(result.size).toBe(1);
// Verify that logging was called
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('[eventDeduplication] Found 2 duplicate events out of 3 total events')
);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('[eventDeduplication] Reduced to 1 unique coordinates')
);
// Restore console.log
consoleSpy.mockRestore();
});
it('should verify that logging works when no duplicates are found', () => {
// Mock console.log to capture output
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
// Create events without duplicates
const uniqueEvents = [
new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Content 1'),
new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-2', 'Content 2'),
];
const eventSets = [new Set(uniqueEvents as NDKEvent[])];
const result = deduplicateContentEvents(eventSets);
// Verify no deduplication was needed
expect(result.size).toBe(2);
// Verify that logging was called with "no duplicates" message
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('[eventDeduplication] No duplicates found in 2 events')
);
// Restore console.log
consoleSpy.mockRestore();
});
it('should verify that deduplicateAndCombineEvents logging works', () => {
// Mock console.log to capture output
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
// Create events with duplicates
const duplicateEvents = [
new MockNDKEvent('event1', 30041, 'pubkey1', 1000, 'chapter-1', 'Old version'),
new MockNDKEvent('event2', 30041, 'pubkey1', 2000, 'chapter-1', 'New version'),
];
const result = deduplicateAndCombineEvents(
[] as NDKEvent[],
new Set(),
new Set(duplicateEvents as NDKEvent[])
);
// Verify the deduplication worked
expect(result.length).toBe(1);
// Verify that logging was called
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('[eventDeduplication] deduplicateAndCombineEvents: Found 1 duplicate coordinates')
);
// Restore console.log
consoleSpy.mockRestore();
});
});

420
tests/unit/tagExpansion.test.ts

@ -0,0 +1,420 @@ @@ -0,0 +1,420 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import {
fetchTaggedEventsFromRelays,
findTaggedEventsInFetched,
fetchProfilesForNewEvents,
type TagExpansionResult
} from '../../src/lib/utils/tag_event_fetch';
// Mock NDKEvent for testing
class MockNDKEvent {
id: string;
kind: number;
pubkey: string;
created_at: number;
content: string;
tags: string[][];
constructor(id: string, kind: number, pubkey: string, created_at: number, content: string = '', tags: string[][] = []) {
this.id = id;
this.kind = kind;
this.pubkey = pubkey;
this.created_at = created_at;
this.content = content;
this.tags = tags;
}
tagValue(tagName: string): string | undefined {
const tag = this.tags.find(t => t[0] === tagName);
return tag ? tag[1] : undefined;
}
getMatchingTags(tagName: string): string[][] {
return this.tags.filter(tag => tag[0] === tagName);
}
}
// Mock NDK instance
const mockNDK = {
fetchEvents: vi.fn()
};
// Mock the ndkInstance store
vi.mock('../../src/lib/ndk', () => ({
ndkInstance: {
subscribe: vi.fn((fn) => {
fn(mockNDK);
return { unsubscribe: vi.fn() };
})
}
}));
// Mock the profile cache utilities
vi.mock('../../src/lib/utils/profileCache', () => ({
extractPubkeysFromEvents: vi.fn((events: NDKEvent[]) => {
const pubkeys = new Set<string>();
events.forEach(event => {
if (event.pubkey) pubkeys.add(event.pubkey);
});
return pubkeys;
}),
batchFetchProfiles: vi.fn(async (pubkeys: string[], onProgress: (fetched: number, total: number) => void) => {
// Simulate progress updates
onProgress(0, pubkeys.length);
onProgress(pubkeys.length, pubkeys.length);
return [];
})
}));
describe('Tag Expansion Tests', () => {
let mockPublications: MockNDKEvent[];
let mockContentEvents: MockNDKEvent[];
let mockAllEvents: MockNDKEvent[];
beforeEach(() => {
vi.clearAllMocks();
// Create test publication index events (kind 30040)
mockPublications = [
new MockNDKEvent('pub1', 30040, 'author1', 1000, 'Book 1', [
['t', 'bitcoin'],
['t', 'cryptocurrency'],
['a', '30041:author1:chapter-1'],
['a', '30041:author1:chapter-2']
]),
new MockNDKEvent('pub2', 30040, 'author2', 1100, 'Book 2', [
['t', 'bitcoin'],
['t', 'blockchain'],
['a', '30041:author2:chapter-1']
]),
new MockNDKEvent('pub3', 30040, 'author3', 1200, 'Book 3', [
['t', 'ethereum'],
['a', '30041:author3:chapter-1']
])
];
// Create test content events (kind 30041)
mockContentEvents = [
new MockNDKEvent('content1', 30041, 'author1', 1000, 'Chapter 1 content', [['d', 'chapter-1']]),
new MockNDKEvent('content2', 30041, 'author1', 1100, 'Chapter 2 content', [['d', 'chapter-2']]),
new MockNDKEvent('content3', 30041, 'author2', 1200, 'Author 2 Chapter 1', [['d', 'chapter-1']]),
new MockNDKEvent('content4', 30041, 'author3', 1300, 'Author 3 Chapter 1', [['d', 'chapter-1']])
];
// Combine all events for testing
mockAllEvents = [...mockPublications, ...mockContentEvents];
});
describe('fetchTaggedEventsFromRelays', () => {
it('should fetch publications with matching tags from relays', async () => {
// Mock the NDK fetch to return publications with 'bitcoin' tag
const bitcoinPublications = mockPublications.filter(pub =>
pub.tags.some(tag => tag[0] === 't' && tag[1] === 'bitcoin')
);
mockNDK.fetchEvents.mockResolvedValueOnce(new Set(bitcoinPublications as NDKEvent[]));
mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[]));
const existingEventIds = new Set<string>(['existing-event']);
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = await fetchTaggedEventsFromRelays(
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should fetch publications with bitcoin tag
expect(mockNDK.fetchEvents).toHaveBeenCalledWith({
kinds: [30040],
"#t": ['bitcoin'],
limit: 30
});
// Should return the matching publications
expect(result.publications).toHaveLength(2);
expect(result.publications.map(p => p.id)).toContain('pub1');
expect(result.publications.map(p => p.id)).toContain('pub2');
// Should fetch content events for the publications
expect(mockNDK.fetchEvents).toHaveBeenCalledWith({
kinds: [30041, 30818],
"#d": ['chapter-1', 'chapter-2']
});
});
it('should filter out existing events to avoid duplicates', async () => {
mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockPublications as NDKEvent[]));
mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[]));
const existingEventIds = new Set<string>(['pub1']); // pub1 already exists
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = await fetchTaggedEventsFromRelays(
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should exclude pub1 since it already exists
expect(result.publications).toHaveLength(2);
expect(result.publications.map(p => p.id)).not.toContain('pub1');
expect(result.publications.map(p => p.id)).toContain('pub2');
expect(result.publications.map(p => p.id)).toContain('pub3');
});
it('should handle empty tag array gracefully', async () => {
// Mock empty result for empty tags
mockNDK.fetchEvents.mockResolvedValueOnce(new Set());
const existingEventIds = new Set<string>();
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = await fetchTaggedEventsFromRelays(
[],
existingEventIds,
baseEvents,
debug
);
expect(result.publications).toHaveLength(0);
expect(result.contentEvents).toHaveLength(0);
});
});
describe('findTaggedEventsInFetched', () => {
it('should find publications with matching tags in already fetched events', () => {
const existingEventIds = new Set<string>(['existing-event']);
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = findTaggedEventsInFetched(
mockAllEvents as NDKEvent[],
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should find publications with bitcoin tag
expect(result.publications).toHaveLength(2);
expect(result.publications.map(p => p.id)).toContain('pub1');
expect(result.publications.map(p => p.id)).toContain('pub2');
// Should find content events for those publications
expect(result.contentEvents).toHaveLength(4);
expect(result.contentEvents.map(c => c.id)).toContain('content1');
expect(result.contentEvents.map(c => c.id)).toContain('content2');
expect(result.contentEvents.map(c => c.id)).toContain('content3');
expect(result.contentEvents.map(c => c.id)).toContain('content4');
});
it('should exclude base events from search results', () => {
const existingEventIds = new Set<string>(['pub1']); // pub1 is a base event
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = findTaggedEventsInFetched(
mockAllEvents as NDKEvent[],
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should exclude pub1 since it's a base event
expect(result.publications).toHaveLength(1);
expect(result.publications.map(p => p.id)).not.toContain('pub1');
expect(result.publications.map(p => p.id)).toContain('pub2');
});
it('should handle multiple tags (OR logic)', () => {
const existingEventIds = new Set<string>();
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = findTaggedEventsInFetched(
mockAllEvents as NDKEvent[],
['bitcoin', 'ethereum'],
existingEventIds,
baseEvents,
debug
);
// Should find publications with either bitcoin OR ethereum tags
expect(result.publications).toHaveLength(3);
expect(result.publications.map(p => p.id)).toContain('pub1'); // bitcoin
expect(result.publications.map(p => p.id)).toContain('pub2'); // bitcoin
expect(result.publications.map(p => p.id)).toContain('pub3'); // ethereum
});
it('should handle events without tags gracefully', () => {
const eventWithoutTags = new MockNDKEvent('no-tags', 30040, 'author4', 1000, 'No tags');
const allEventsWithNoTags = [...mockAllEvents, eventWithoutTags];
const existingEventIds = new Set<string>();
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = findTaggedEventsInFetched(
allEventsWithNoTags as NDKEvent[],
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should not include events without tags
expect(result.publications.map(p => p.id)).not.toContain('no-tags');
});
});
describe('fetchProfilesForNewEvents', () => {
it('should extract pubkeys and fetch profiles for new events', async () => {
const onProgressUpdate = vi.fn();
const debug = vi.fn();
await fetchProfilesForNewEvents(
mockPublications as NDKEvent[],
mockContentEvents as NDKEvent[],
onProgressUpdate,
debug
);
// Should call progress update with initial state
expect(onProgressUpdate).toHaveBeenCalledWith({ current: 0, total: 3 });
// Should call progress update with final state
expect(onProgressUpdate).toHaveBeenCalledWith({ current: 3, total: 3 });
// Should clear progress at the end
expect(onProgressUpdate).toHaveBeenCalledWith(null);
});
it('should handle empty event arrays gracefully', async () => {
const onProgressUpdate = vi.fn();
const debug = vi.fn();
await fetchProfilesForNewEvents(
[],
[],
onProgressUpdate,
debug
);
// Should not call progress update for empty arrays
expect(onProgressUpdate).not.toHaveBeenCalled();
});
});
describe('Tag Expansion Integration', () => {
it('should demonstrate the complete tag expansion flow', async () => {
// This test simulates the complete flow from the visualize page
// Step 1: Mock relay fetch for 'bitcoin' tag
const bitcoinPublications = mockPublications.filter(pub =>
pub.tags.some(tag => tag[0] === 't' && tag[1] === 'bitcoin')
);
mockNDK.fetchEvents.mockResolvedValueOnce(new Set(bitcoinPublications as NDKEvent[]));
mockNDK.fetchEvents.mockResolvedValueOnce(new Set(mockContentEvents as NDKEvent[]));
const existingEventIds = new Set<string>(['base-event']);
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
// Step 2: Fetch from relays
const relayResult = await fetchTaggedEventsFromRelays(
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
expect(relayResult.publications).toHaveLength(2);
expect(relayResult.contentEvents).toHaveLength(4);
// Step 3: Search in fetched events
const searchResult = findTaggedEventsInFetched(
mockAllEvents as NDKEvent[],
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
expect(searchResult.publications).toHaveLength(2);
expect(searchResult.contentEvents).toHaveLength(4);
// Step 4: Fetch profiles
const onProgressUpdate = vi.fn();
await fetchProfilesForNewEvents(
relayResult.publications,
relayResult.contentEvents,
onProgressUpdate,
debug
);
expect(onProgressUpdate).toHaveBeenCalledWith(null);
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle malformed a-tags gracefully', () => {
const malformedPublication = new MockNDKEvent('malformed', 30040, 'author1', 1000, 'Malformed', [
['t', 'bitcoin'],
['a', 'invalid-tag-format'], // Missing parts
['a', '30041:author1:chapter-1'] // Valid format
]);
const allEventsWithMalformed = [...mockAllEvents, malformedPublication];
const existingEventIds = new Set<string>();
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = findTaggedEventsInFetched(
allEventsWithMalformed as NDKEvent[],
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should still work and include the publication with valid a-tags
expect(result.publications).toHaveLength(3);
expect(result.contentEvents.length).toBeGreaterThan(0);
});
it('should handle events with d-tags containing colons', () => {
const publicationWithColonDTag = new MockNDKEvent('colon-pub', 30040, 'author1', 1000, 'Colon d-tag', [
['t', 'bitcoin'],
['a', '30041:author1:chapter:with:colons']
]);
const contentWithColonDTag = new MockNDKEvent('colon-content', 30041, 'author1', 1100, 'Content with colon d-tag', [
['d', 'chapter:with:colons']
]);
const allEventsWithColons = [...mockAllEvents, publicationWithColonDTag, contentWithColonDTag];
const existingEventIds = new Set<string>();
const baseEvents: NDKEvent[] = [];
const debug = vi.fn();
const result = findTaggedEventsInFetched(
allEventsWithColons as NDKEvent[],
['bitcoin'],
existingEventIds,
baseEvents,
debug
);
// Should handle d-tags with colons correctly
expect(result.publications).toHaveLength(3);
expect(result.contentEvents.map(c => c.id)).toContain('colon-content');
});
});
});
Loading…
Cancel
Save