mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-11 17:23:09 +00:00
feat: test that sds doesn't break anything
This commit is contained in:
parent
5bec0f7039
commit
1c03c9074f
23
jest.config.mjs
Normal file
23
jest.config.mjs
Normal file
@ -0,0 +1,23 @@
|
||||
export default {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': ['ts-jest', {
|
||||
tsconfig: {
|
||||
allowJs: true,
|
||||
},
|
||||
}],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js', 'json', 'node'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/index.ts',
|
||||
],
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
};
|
||||
3792
package-lock.json
generated
3792
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,9 @@
|
||||
"build": "vite build",
|
||||
"build:dev": "vite build --mode development",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
@ -69,6 +71,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.0",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.5.5",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
@ -79,9 +82,12 @@
|
||||
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.9",
|
||||
"globals": "^15.9.0",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lovable-tagger": "^1.1.7",
|
||||
"postcss": "^8.4.47",
|
||||
"tailwindcss": "^3.4.11",
|
||||
"ts-jest": "^29.3.4",
|
||||
"typescript": "^5.5.3",
|
||||
"typescript-eslint": "^8.0.1",
|
||||
"vite": "^5.4.1"
|
||||
|
||||
235
src/lib/waku/__tests__/messageManager.test.ts
Normal file
235
src/lib/waku/__tests__/messageManager.test.ts
Normal file
@ -0,0 +1,235 @@
|
||||
import { MessageType } from '../types';
|
||||
import { SDSEnhancedMessage } from '../sds/types';
|
||||
|
||||
// Mock Waku SDK
|
||||
jest.mock('@waku/sdk', () => ({
|
||||
createLightNode: jest.fn().mockResolvedValue({
|
||||
lightPush: {
|
||||
send: jest.fn().mockResolvedValue({ success: true }),
|
||||
},
|
||||
filter: {
|
||||
subscribe: jest.fn().mockResolvedValue({
|
||||
error: null,
|
||||
results: { successes: [{}] },
|
||||
unsubscribe: jest.fn(),
|
||||
}),
|
||||
},
|
||||
store: {
|
||||
queryWithOrderedCallback: jest.fn(),
|
||||
},
|
||||
getConnectedPeers: jest.fn().mockResolvedValue(['peer1']),
|
||||
stop: jest.fn(),
|
||||
}),
|
||||
createEncoder: jest.fn().mockReturnValue({}),
|
||||
createDecoder: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
// Mock other dependencies
|
||||
jest.mock('../lightpush_filter');
|
||||
jest.mock('../store');
|
||||
|
||||
describe('MessageManager with SDS', () => {
|
||||
let MessageManager: any;
|
||||
let messageManager: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Dynamic import to ensure mocks are applied
|
||||
const module = await import('../index');
|
||||
messageManager = module.default;
|
||||
MessageManager = module.MessageManager;
|
||||
});
|
||||
|
||||
describe('updateCache with SDS', () => {
|
||||
it('should handle non-vote messages without SDS', () => {
|
||||
const postMessage = {
|
||||
type: MessageType.POST,
|
||||
id: 'post-1',
|
||||
cellId: 'general',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
// Access private method through prototype
|
||||
MessageManager.prototype.updateCache.call(messageManager, postMessage);
|
||||
|
||||
expect(messageManager.messageCache.posts['post-1']).toEqual(postMessage);
|
||||
});
|
||||
|
||||
it('should update vote cache with new vote', () => {
|
||||
const voteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
MessageManager.prototype.updateCache.call(messageManager, voteMessage);
|
||||
|
||||
const voteKey = 'post-1:0x123';
|
||||
expect(messageManager.messageCache.votes[voteKey]).toEqual(voteMessage);
|
||||
});
|
||||
|
||||
it('should respect causal ordering for votes with SDS metadata', () => {
|
||||
const voteKey = 'post-1:0x123';
|
||||
|
||||
// First vote with lower Lamport timestamp
|
||||
const olderVote: SDSEnhancedMessage = {
|
||||
type: MessageType.VOTE as any,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
MessageManager.prototype.updateCache.call(messageManager, olderVote);
|
||||
expect(messageManager.messageCache.votes[voteKey]).toEqual(olderVote);
|
||||
|
||||
// Newer vote with higher Lamport timestamp
|
||||
const newerVote: SDSEnhancedMessage = {
|
||||
type: MessageType.VOTE as any,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: Date.now() - 1000, // older wall clock time
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 10,
|
||||
causalHistory: ['vote-1'],
|
||||
},
|
||||
};
|
||||
|
||||
MessageManager.prototype.updateCache.call(messageManager, newerVote);
|
||||
expect(messageManager.messageCache.votes[voteKey]).toEqual(newerVote);
|
||||
|
||||
// Attempt to update with older vote
|
||||
const evenOlderVote: SDSEnhancedMessage = {
|
||||
type: MessageType.VOTE as any,
|
||||
id: 'vote-0',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now() + 1000, // newer wall clock time
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 3,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
MessageManager.prototype.updateCache.call(messageManager, evenOlderVote);
|
||||
// Should still have the newer vote
|
||||
expect(messageManager.messageCache.votes[voteKey]).toEqual(newerVote);
|
||||
});
|
||||
|
||||
it('should handle votes without SDS alongside votes with SDS', () => {
|
||||
const voteKey = 'post-1:0x123';
|
||||
|
||||
// Vote without SDS
|
||||
const plainVote = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
MessageManager.prototype.updateCache.call(messageManager, plainVote);
|
||||
expect(messageManager.messageCache.votes[voteKey]).toEqual(plainVote);
|
||||
|
||||
// Vote with SDS but older timestamp
|
||||
const sdsVote: SDSEnhancedMessage = {
|
||||
type: MessageType.VOTE as any,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 500, // older
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 1,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Since existing vote has no SDS, it falls back to timestamp comparison
|
||||
MessageManager.prototype.updateCache.call(messageManager, sdsVote);
|
||||
// Should keep the plain vote as it has newer timestamp
|
||||
expect(messageManager.messageCache.votes[voteKey]).toEqual(plainVote);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backward compatibility', () => {
|
||||
it('should handle all message types without SDS', () => {
|
||||
const messages = [
|
||||
{
|
||||
type: MessageType.CELL,
|
||||
id: 'cell-1',
|
||||
name: 'general',
|
||||
description: 'General discussion',
|
||||
icon: '🏠',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
},
|
||||
{
|
||||
type: MessageType.POST,
|
||||
id: 'post-1',
|
||||
cellId: 'general',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
},
|
||||
{
|
||||
type: MessageType.COMMENT,
|
||||
id: 'comment-1',
|
||||
postId: 'post-1',
|
||||
content: 'Test comment',
|
||||
timestamp: Date.now(),
|
||||
author: '0x456',
|
||||
},
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x789',
|
||||
},
|
||||
{
|
||||
type: MessageType.MODERATE,
|
||||
id: 'mod-1',
|
||||
targetId: 'post-1',
|
||||
action: 'hide',
|
||||
cellId: 'general',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
},
|
||||
];
|
||||
|
||||
messages.forEach(message => {
|
||||
MessageManager.prototype.updateCache.call(messageManager, message);
|
||||
});
|
||||
|
||||
// Verify all messages are cached correctly
|
||||
expect(messageManager.messageCache.cells['cell-1']).toBeDefined();
|
||||
expect(messageManager.messageCache.posts['post-1']).toBeDefined();
|
||||
expect(messageManager.messageCache.comments['comment-1']).toBeDefined();
|
||||
expect(messageManager.messageCache.votes['post-1:0x789']).toBeDefined();
|
||||
expect(messageManager.messageCache.moderations['post-1']).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
175
src/lib/waku/__tests__/vote-integration.test.ts
Normal file
175
src/lib/waku/__tests__/vote-integration.test.ts
Normal file
@ -0,0 +1,175 @@
|
||||
import { MessageType, VoteMessage } from '../types';
|
||||
import { OpchanMessage } from '@/types';
|
||||
|
||||
describe('Vote Integration with SDS', () => {
|
||||
describe('Vote Conflict Resolution', () => {
|
||||
it('should handle rapid vote changes correctly', () => {
|
||||
const votes: VoteMessage[] = [
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-123',
|
||||
value: 1, // upvote
|
||||
timestamp: 1000,
|
||||
author: 'user-abc',
|
||||
},
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-123',
|
||||
value: -1, // change to downvote
|
||||
timestamp: 2000,
|
||||
author: 'user-abc',
|
||||
},
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-3',
|
||||
targetId: 'post-123',
|
||||
value: 1, // change back to upvote
|
||||
timestamp: 3000,
|
||||
author: 'user-abc',
|
||||
},
|
||||
];
|
||||
|
||||
// In the real system, each vote would be enhanced with SDS metadata
|
||||
// The latest vote should always win
|
||||
const latestVote = votes[votes.length - 1];
|
||||
expect(latestVote.value).toBe(1);
|
||||
expect(latestVote.id).toBe('vote-3');
|
||||
});
|
||||
|
||||
it('should handle votes from multiple users independently', () => {
|
||||
const votesByUser: Record<string, VoteMessage> = {};
|
||||
|
||||
const votes: VoteMessage[] = [
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-123',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: 'user-A',
|
||||
},
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-123',
|
||||
value: -1,
|
||||
timestamp: 1100,
|
||||
author: 'user-B',
|
||||
},
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-3',
|
||||
targetId: 'post-123',
|
||||
value: 1,
|
||||
timestamp: 1200,
|
||||
author: 'user-C',
|
||||
},
|
||||
{
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-4',
|
||||
targetId: 'post-123',
|
||||
value: -1,
|
||||
timestamp: 1300,
|
||||
author: 'user-A', // user-A changes vote
|
||||
},
|
||||
];
|
||||
|
||||
// Process votes
|
||||
votes.forEach(vote => {
|
||||
const key = `${vote.targetId}:${vote.author}`;
|
||||
votesByUser[key] = vote;
|
||||
});
|
||||
|
||||
// Verify final state
|
||||
expect(votesByUser['post-123:user-A'].value).toBe(-1);
|
||||
expect(votesByUser['post-123:user-B'].value).toBe(-1);
|
||||
expect(votesByUser['post-123:user-C'].value).toBe(1);
|
||||
|
||||
// Calculate total
|
||||
const total = Object.values(votesByUser).reduce((sum, vote) => sum + vote.value, 0);
|
||||
expect(total).toBe(-1); // 2 downvotes, 1 upvote
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge Cases', () => {
|
||||
it('should handle votes with same timestamp', () => {
|
||||
const timestamp = Date.now();
|
||||
|
||||
const vote1: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-aaa',
|
||||
targetId: 'post-123',
|
||||
value: 1,
|
||||
timestamp,
|
||||
author: 'user-abc',
|
||||
};
|
||||
|
||||
const vote2: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-bbb',
|
||||
targetId: 'post-123',
|
||||
value: -1,
|
||||
timestamp,
|
||||
author: 'user-abc',
|
||||
};
|
||||
|
||||
// With SDS, this would be resolved by Lamport timestamps
|
||||
// Without SDS, it would use ID as tiebreaker
|
||||
// 'vote-bbb' > 'vote-aaa' lexicographically
|
||||
const shouldWin = vote2;
|
||||
expect(shouldWin.id).toBe('vote-bbb');
|
||||
});
|
||||
|
||||
it('should handle missing or malformed votes gracefully', () => {
|
||||
const validVote: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-123',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: 'user-abc',
|
||||
};
|
||||
|
||||
// System should handle partial data
|
||||
const partialVote = {
|
||||
...validVote,
|
||||
value: undefined as any,
|
||||
};
|
||||
|
||||
// In real system, validation would reject this
|
||||
expect(validVote.value).toBeDefined();
|
||||
expect(partialVote.value).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Performance Considerations', () => {
|
||||
it('should handle large numbers of votes efficiently', () => {
|
||||
const startTime = Date.now();
|
||||
const votes: VoteMessage[] = [];
|
||||
|
||||
// Generate 1000 votes
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
votes.push({
|
||||
type: MessageType.VOTE,
|
||||
id: `vote-${i}`,
|
||||
targetId: `post-${i % 10}`, // 10 different posts
|
||||
value: i % 2 === 0 ? 1 : -1,
|
||||
timestamp: startTime + i,
|
||||
author: `user-${i % 100}`, // 100 different users
|
||||
});
|
||||
}
|
||||
|
||||
// Process votes (in real system, this would go through updateCache)
|
||||
const voteMap: Record<string, VoteMessage> = {};
|
||||
votes.forEach(vote => {
|
||||
const key = `${vote.targetId}:${vote.author}`;
|
||||
voteMap[key] = vote;
|
||||
});
|
||||
|
||||
// Should have at most 10 posts * 100 users = 1000 entries
|
||||
expect(Object.keys(voteMap).length).toBeLessThanOrEqual(1000);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -244,3 +244,4 @@ class MessageManager {
|
||||
|
||||
const messageManager = await MessageManager.create();
|
||||
export default messageManager;
|
||||
export { MessageManager };
|
||||
|
||||
239
src/lib/waku/sds/__tests__/minimal-sds.test.ts
Normal file
239
src/lib/waku/sds/__tests__/minimal-sds.test.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { MinimalSDSWrapper } from '../minimal-sds';
|
||||
import { OpchanMessage } from '@/types';
|
||||
import { SDSEnhancedMessage } from '../types';
|
||||
import { MessageType } from '../../types';
|
||||
|
||||
describe('MinimalSDSWrapper', () => {
|
||||
let sds: MinimalSDSWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
sds = new MinimalSDSWrapper();
|
||||
});
|
||||
|
||||
describe('enhanceMessage', () => {
|
||||
it('should not enhance non-vote messages', () => {
|
||||
const postMessage: OpchanMessage = {
|
||||
type: MessageType.POST,
|
||||
id: 'post-1',
|
||||
cellId: 'general',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced = sds.enhanceMessage(postMessage);
|
||||
|
||||
expect(enhanced).toEqual(postMessage);
|
||||
expect('sds' in enhanced).toBe(false);
|
||||
});
|
||||
|
||||
it('should enhance vote messages with SDS metadata', () => {
|
||||
const voteMessage: OpchanMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced = sds.enhanceMessage(voteMessage);
|
||||
|
||||
expect('sds' in enhanced).toBe(true);
|
||||
expect(enhanced.sds).toBeDefined();
|
||||
expect(enhanced.sds?.channelId).toBe('opchan:votes:all');
|
||||
expect(enhanced.sds?.lamportTimestamp).toBe(1);
|
||||
expect(enhanced.sds?.causalHistory).toEqual([]);
|
||||
});
|
||||
|
||||
it('should increment Lamport timestamp for each vote', () => {
|
||||
const vote1: OpchanMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const vote2: OpchanMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-2',
|
||||
value: -1,
|
||||
timestamp: Date.now() + 1000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced1 = sds.enhanceMessage(vote1);
|
||||
const enhanced2 = sds.enhanceMessage(vote2);
|
||||
|
||||
expect(enhanced1.sds?.lamportTimestamp).toBe(1);
|
||||
expect(enhanced2.sds?.lamportTimestamp).toBe(2);
|
||||
});
|
||||
|
||||
it('should maintain causal history', () => {
|
||||
const votes = Array.from({ length: 5 }, (_, i) => ({
|
||||
type: MessageType.VOTE as const,
|
||||
id: `vote-${i}`,
|
||||
targetId: 'post-1',
|
||||
value: i % 2 === 0 ? 1 : -1,
|
||||
timestamp: Date.now() + i * 1000,
|
||||
author: '0x123',
|
||||
}));
|
||||
|
||||
const enhancedVotes = votes.map(vote => sds.enhanceMessage(vote));
|
||||
|
||||
// First vote has empty history
|
||||
expect(enhancedVotes[0].sds?.causalHistory).toEqual([]);
|
||||
|
||||
// Second vote has first vote in history
|
||||
expect(enhancedVotes[1].sds?.causalHistory).toEqual(['vote-0']);
|
||||
|
||||
// Fifth vote has last 3 votes in history
|
||||
expect(enhancedVotes[4].sds?.causalHistory).toEqual(['vote-1', 'vote-2', 'vote-3']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('processIncomingMessage', () => {
|
||||
it('should ignore non-vote messages', () => {
|
||||
const postMessage: OpchanMessage = {
|
||||
type: MessageType.POST,
|
||||
id: 'post-1',
|
||||
cellId: 'general',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
// Should not throw
|
||||
expect(() => sds.processIncomingMessage(postMessage as SDSEnhancedMessage)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should update Lamport timestamp from incoming message', () => {
|
||||
const incomingVote = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-remote',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x456',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 10,
|
||||
causalHistory: ['vote-a', 'vote-b'],
|
||||
},
|
||||
};
|
||||
|
||||
sds.processIncomingMessage(incomingVote);
|
||||
|
||||
// Next local message should have timestamp > 10
|
||||
const localVote: OpchanMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-local',
|
||||
targetId: 'post-2',
|
||||
value: 1,
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced = sds.enhanceMessage(localVote);
|
||||
expect(enhanced.sds?.lamportTimestamp).toBe(12); // max(0, 10) + 1 + 1
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCausallyNewer', () => {
|
||||
it('should use timestamp for messages without SDS metadata', () => {
|
||||
const older = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const newer = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 2000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
expect(sds.isCausallyNewer(newer as SDSEnhancedMessage, older as SDSEnhancedMessage)).toBe(true);
|
||||
expect(sds.isCausallyNewer(older as SDSEnhancedMessage, newer as SDSEnhancedMessage)).toBe(false);
|
||||
});
|
||||
|
||||
it('should use Lamport timestamp for messages with SDS metadata', () => {
|
||||
const older = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 2000, // newer wall clock time
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
const newer = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 1000, // older wall clock time
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 10,
|
||||
causalHistory: ['vote-1'],
|
||||
},
|
||||
};
|
||||
|
||||
// Despite older wall clock time, newer has higher Lamport timestamp
|
||||
expect(sds.isCausallyNewer(newer, older)).toBe(true);
|
||||
expect(sds.isCausallyNewer(older, newer)).toBe(false);
|
||||
});
|
||||
|
||||
it('should use message ID as tiebreaker for equal Lamport timestamps', () => {
|
||||
const messageA = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-aaa',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
const messageB = {
|
||||
type: MessageType.VOTE as const,
|
||||
id: 'vote-bbb',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
// 'vote-bbb' > 'vote-aaa' lexicographically
|
||||
expect(sds.isCausallyNewer(messageB, messageA)).toBe(true);
|
||||
expect(sds.isCausallyNewer(messageA, messageB)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
187
src/lib/waku/sds/__tests__/sds-logic.test.ts
Normal file
187
src/lib/waku/sds/__tests__/sds-logic.test.ts
Normal file
@ -0,0 +1,187 @@
|
||||
import { MessageType, VoteMessage, PostMessage, CommentMessage } from '@/lib/waku/types';
|
||||
import { SDSMetadata } from '../types';
|
||||
|
||||
// Test the SDS logic without importing the implementation
|
||||
describe('SDS Logic Tests', () => {
|
||||
// Helper function to simulate SDS enhancement logic
|
||||
function enhanceMessage(message: any): any {
|
||||
if (message.type !== 'vote') {
|
||||
return message;
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: Date.now(),
|
||||
causalHistory: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper function to simulate causal ordering logic
|
||||
function isCausallyNewer(a: any, b: any): boolean {
|
||||
const hasSDSMetadata = (msg: any): boolean => {
|
||||
return msg.type === 'vote' && 'sds' in msg && msg.sds !== undefined;
|
||||
};
|
||||
|
||||
if (!hasSDSMetadata(a) || !hasSDSMetadata(b)) {
|
||||
return a.timestamp > b.timestamp;
|
||||
}
|
||||
|
||||
if (a.sds.lamportTimestamp !== b.sds.lamportTimestamp) {
|
||||
return a.sds.lamportTimestamp > b.sds.lamportTimestamp;
|
||||
}
|
||||
|
||||
return a.id > b.id;
|
||||
}
|
||||
|
||||
describe('Non-vote messages remain unchanged', () => {
|
||||
it('should not modify post messages', () => {
|
||||
const post: PostMessage = {
|
||||
type: MessageType.POST,
|
||||
id: 'post-1',
|
||||
cellId: 'general',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced = enhanceMessage(post);
|
||||
|
||||
expect(enhanced).toBe(post);
|
||||
expect('sds' in enhanced).toBe(false);
|
||||
});
|
||||
|
||||
it('should not modify comment messages', () => {
|
||||
const comment: CommentMessage = {
|
||||
type: MessageType.COMMENT,
|
||||
id: 'comment-1',
|
||||
postId: 'post-1',
|
||||
content: 'Test comment',
|
||||
timestamp: Date.now(),
|
||||
author: '0x456',
|
||||
};
|
||||
|
||||
const enhanced = enhanceMessage(comment);
|
||||
|
||||
expect(enhanced).toBe(comment);
|
||||
expect('sds' in enhanced).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vote messages get SDS enhancement', () => {
|
||||
it('should add SDS metadata to votes without changing original fields', () => {
|
||||
const vote: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1234567890,
|
||||
author: '0x789',
|
||||
};
|
||||
|
||||
const enhanced = enhanceMessage(vote);
|
||||
|
||||
// Original fields unchanged
|
||||
expect(enhanced.type).toBe(MessageType.VOTE);
|
||||
expect(enhanced.id).toBe('vote-1');
|
||||
expect(enhanced.targetId).toBe('post-1');
|
||||
expect(enhanced.value).toBe(1);
|
||||
expect(enhanced.timestamp).toBe(1234567890);
|
||||
expect(enhanced.author).toBe('0x789');
|
||||
|
||||
// SDS metadata added
|
||||
expect('sds' in enhanced).toBe(true);
|
||||
expect(enhanced.sds).toBeDefined();
|
||||
expect(enhanced.sds.channelId).toBe('opchan:votes:all');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Causal ordering preserves latest vote', () => {
|
||||
it('should correctly identify newer votes', () => {
|
||||
const vote1 = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: []
|
||||
}
|
||||
};
|
||||
|
||||
const vote2 = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 2000,
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 10,
|
||||
causalHistory: ['vote-1']
|
||||
}
|
||||
};
|
||||
|
||||
// Should identify vote2 as newer based on Lamport timestamp
|
||||
expect(isCausallyNewer(vote2, vote1)).toBe(true);
|
||||
expect(isCausallyNewer(vote1, vote2)).toBe(false);
|
||||
});
|
||||
|
||||
it('should use timestamp as fallback when no SDS metadata', () => {
|
||||
const plainVote1 = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
timestamp: 1000,
|
||||
author: '0x123'
|
||||
};
|
||||
|
||||
const plainVote2 = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-2',
|
||||
timestamp: 2000,
|
||||
author: '0x123'
|
||||
};
|
||||
|
||||
expect(isCausallyNewer(plainVote2, plainVote1)).toBe(true);
|
||||
expect(isCausallyNewer(plainVote1, plainVote2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backward compatibility', () => {
|
||||
it('should handle mixed SDS and non-SDS votes', () => {
|
||||
const plainVote = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-plain',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const sdsVote = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-sds',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 500, // older timestamp
|
||||
author: '0x123',
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: [],
|
||||
},
|
||||
};
|
||||
|
||||
// Should fall back to timestamp comparison
|
||||
expect(isCausallyNewer(plainVote, sdsVote)).toBe(true); // plainVote has newer timestamp
|
||||
expect(isCausallyNewer(sdsVote, plainVote)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
152
src/lib/waku/sds/__tests__/sds-non-breaking.test.ts
Normal file
152
src/lib/waku/sds/__tests__/sds-non-breaking.test.ts
Normal file
@ -0,0 +1,152 @@
|
||||
import { MinimalSDSWrapper } from '../minimal-sds';
|
||||
import { MessageType, VoteMessage, PostMessage, CommentMessage } from '@/lib/waku/types';
|
||||
|
||||
// Mock @waku/sdk
|
||||
jest.mock('@waku/sdk', () => ({
|
||||
createEncoder: jest.fn().mockReturnValue({}),
|
||||
createDecoder: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
describe('SDS Non-Breaking Functionality', () => {
|
||||
let sds: MinimalSDSWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
sds = new MinimalSDSWrapper();
|
||||
});
|
||||
|
||||
describe('Non-vote messages remain unchanged', () => {
|
||||
it('should not modify post messages', () => {
|
||||
const post: PostMessage = {
|
||||
type: MessageType.POST,
|
||||
id: 'post-1',
|
||||
cellId: 'general',
|
||||
title: 'Test Post',
|
||||
content: 'Test content',
|
||||
timestamp: Date.now(),
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced = sds.enhanceMessage(post);
|
||||
|
||||
// Should be exactly the same object
|
||||
expect(enhanced).toBe(post);
|
||||
expect('sds' in enhanced).toBe(false);
|
||||
});
|
||||
|
||||
it('should not modify comment messages', () => {
|
||||
const comment: CommentMessage = {
|
||||
type: MessageType.COMMENT,
|
||||
id: 'comment-1',
|
||||
postId: 'post-1',
|
||||
content: 'Test comment',
|
||||
timestamp: Date.now(),
|
||||
author: '0x456',
|
||||
};
|
||||
|
||||
const enhanced = sds.enhanceMessage(comment);
|
||||
|
||||
expect(enhanced).toBe(comment);
|
||||
expect('sds' in enhanced).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Vote messages get SDS enhancement', () => {
|
||||
it('should add SDS metadata to votes without changing original fields', () => {
|
||||
const vote: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1234567890,
|
||||
author: '0x789',
|
||||
};
|
||||
|
||||
const enhanced = sds.enhanceMessage(vote);
|
||||
|
||||
// Original fields unchanged
|
||||
expect(enhanced.type).toBe(MessageType.VOTE);
|
||||
if (enhanced.type === MessageType.VOTE) {
|
||||
expect(enhanced.id).toBe('vote-1');
|
||||
expect(enhanced.targetId).toBe('post-1');
|
||||
expect(enhanced.value).toBe(1);
|
||||
expect(enhanced.timestamp).toBe(1234567890);
|
||||
expect(enhanced.author).toBe('0x789');
|
||||
|
||||
// SDS metadata added
|
||||
expect('sds' in enhanced).toBe(true);
|
||||
if ('sds' in enhanced && enhanced.sds) {
|
||||
expect(enhanced.sds).toBeDefined();
|
||||
expect(enhanced.sds.channelId).toBe('opchan:votes:all');
|
||||
expect(enhanced.sds.lamportTimestamp).toBeGreaterThan(0);
|
||||
expect(Array.isArray(enhanced.sds.causalHistory)).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Causal ordering preserves latest vote', () => {
|
||||
it('should correctly identify newer votes', () => {
|
||||
// Create two votes with SDS metadata
|
||||
const vote1: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-1',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const vote2: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-2',
|
||||
targetId: 'post-1',
|
||||
value: -1,
|
||||
timestamp: 2000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const enhanced1 = sds.enhanceMessage(vote1);
|
||||
const enhanced2 = sds.enhanceMessage(vote2);
|
||||
|
||||
// Second vote should have higher Lamport timestamp
|
||||
if (enhanced1.type === MessageType.VOTE && enhanced2.type === MessageType.VOTE &&
|
||||
'sds' in enhanced1 && enhanced1.sds && 'sds' in enhanced2 && enhanced2.sds) {
|
||||
expect(enhanced2.sds.lamportTimestamp).toBeGreaterThan(enhanced1.sds.lamportTimestamp);
|
||||
|
||||
// Should identify vote2 as newer
|
||||
expect(sds.isCausallyNewer(enhanced2, enhanced1)).toBe(true);
|
||||
expect(sds.isCausallyNewer(enhanced1, enhanced2)).toBe(false);
|
||||
} else {
|
||||
fail('SDS metadata should be added to vote messages');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backward compatibility', () => {
|
||||
it('should handle votes without SDS metadata gracefully', () => {
|
||||
const plainVote: VoteMessage = {
|
||||
type: MessageType.VOTE,
|
||||
id: 'vote-plain',
|
||||
targetId: 'post-1',
|
||||
value: 1,
|
||||
timestamp: 1000,
|
||||
author: '0x123',
|
||||
};
|
||||
|
||||
const sdsVote = {
|
||||
...plainVote,
|
||||
sds: {
|
||||
channelId: 'opchan:votes:all',
|
||||
lamportTimestamp: 5,
|
||||
causalHistory: [],
|
||||
},
|
||||
} as VoteMessage & { sds: { channelId: string; lamportTimestamp: number; causalHistory: string[] } };
|
||||
|
||||
// Should fall back to timestamp comparison
|
||||
expect(sds.isCausallyNewer(sdsVote, plainVote)).toBe(false); // same timestamp
|
||||
|
||||
const newerPlainVote = { ...plainVote, timestamp: 2000 };
|
||||
expect(sds.isCausallyNewer(newerPlainVote, sdsVote)).toBe(true); // newer timestamp
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,6 +1,6 @@
|
||||
import { createEncoder, createDecoder } from "@waku/sdk";
|
||||
import { SDSEnhancedMessage, SDSChannelState } from "./types";
|
||||
import { OpchanMessage } from "../../types";
|
||||
import { SDSEnhancedMessage, SDSChannelState, SDSMetadata } from "./types";
|
||||
import { CellMessage, CommentMessage, PostMessage, VoteMessage, ModerateMessage } from "@/lib/waku/types";
|
||||
|
||||
// For now, use a single channel for all votes to test SDS
|
||||
const VOTE_CHANNEL_ID = "opchan:votes:all";
|
||||
@ -22,7 +22,7 @@ export class MinimalSDSWrapper {
|
||||
}
|
||||
|
||||
// Enhance a message with SDS metadata
|
||||
enhanceMessage(message: OpchanMessage): SDSEnhancedMessage {
|
||||
enhanceMessage(message: CellMessage | PostMessage | CommentMessage | VoteMessage | ModerateMessage): SDSEnhancedMessage {
|
||||
// Only enhance vote messages for minimal implementation
|
||||
if (message.type !== 'vote') {
|
||||
return message;
|
||||
@ -42,8 +42,10 @@ export class MinimalSDSWrapper {
|
||||
state.messageHistory = state.messageHistory.slice(-100);
|
||||
}
|
||||
|
||||
// Return vote message with SDS metadata
|
||||
const voteMessage = message as VoteMessage;
|
||||
return {
|
||||
...message,
|
||||
...voteMessage,
|
||||
sds: {
|
||||
channelId: VOTE_CHANNEL_ID,
|
||||
lamportTimestamp: state.lamportTimestamp,
|
||||
@ -54,7 +56,7 @@ export class MinimalSDSWrapper {
|
||||
|
||||
// Process incoming message with SDS metadata
|
||||
processIncomingMessage(message: SDSEnhancedMessage): void {
|
||||
if (!message.sds || message.type !== 'vote') {
|
||||
if (message.type !== 'vote' || !('sds' in message) || !message.sds) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -77,7 +79,12 @@ export class MinimalSDSWrapper {
|
||||
|
||||
// Check if message A is causally newer than B
|
||||
isCausallyNewer(a: SDSEnhancedMessage, b: SDSEnhancedMessage): boolean {
|
||||
if (!a.sds || !b.sds) {
|
||||
// Type guard to check if message has SDS metadata
|
||||
const hasSDSMetadata = (msg: SDSEnhancedMessage): msg is VoteMessage & { sds: SDSMetadata } => {
|
||||
return msg.type === 'vote' && 'sds' in msg && msg.sds !== undefined;
|
||||
};
|
||||
|
||||
if (!hasSDSMetadata(a) || !hasSDSMetadata(b)) {
|
||||
return a.timestamp > b.timestamp;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { OpchanMessage } from "../../types";
|
||||
import { CellMessage, CommentMessage, PostMessage, VoteMessage, ModerateMessage } from "@/lib/waku/types";
|
||||
|
||||
export interface SDSMetadata {
|
||||
channelId: string;
|
||||
@ -7,9 +7,12 @@ export interface SDSMetadata {
|
||||
bloomFilter?: Uint8Array;
|
||||
}
|
||||
|
||||
export interface SDSEnhancedMessage extends OpchanMessage {
|
||||
sds?: SDSMetadata;
|
||||
}
|
||||
export type SDSEnhancedMessage =
|
||||
| CellMessage
|
||||
| PostMessage
|
||||
| CommentMessage
|
||||
| (VoteMessage & { sds?: SDSMetadata })
|
||||
| ModerateMessage;
|
||||
|
||||
export interface SDSChannelState {
|
||||
channelId: string;
|
||||
|
||||
@ -67,6 +67,7 @@ export interface VoteMessage extends BaseMessage {
|
||||
*/
|
||||
export interface ModerateMessage extends BaseMessage {
|
||||
type: MessageType.MODERATE;
|
||||
id: string;
|
||||
cellId: string;
|
||||
targetType: 'post' | 'comment' | 'user';
|
||||
targetId: string; // postId, commentId, or user address (for user moderation)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user