diff --git a/packages/client-twitter/__tests__/base.test.ts b/packages/client-twitter/__tests__/base.test.ts new file mode 100644 index 00000000000..59a15c33c9a --- /dev/null +++ b/packages/client-twitter/__tests__/base.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { ClientBase } from '../src/base'; +import { IAgentRuntime } from '@elizaos/core'; +import { TwitterConfig } from '../src/environment'; + +describe('Twitter Client Base', () => { + let mockRuntime: IAgentRuntime; + let mockConfig: TwitterConfig; + + beforeEach(() => { + mockRuntime = { + env: { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: 'true', + TWITTER_POST_INTERVAL_MIN: '5', + TWITTER_POST_INTERVAL_MAX: '10', + TWITTER_ACTION_INTERVAL: '5', + TWITTER_ENABLE_ACTION_PROCESSING: 'true', + TWITTER_POST_IMMEDIATELY: 'false', + TWITTER_SEARCH_ENABLE: 'false' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + }, + character: { + style: { + all: ['Test style 1', 'Test style 2'], + post: ['Post style 1', 'Post style 2'] + } + } + } as unknown as IAgentRuntime; + + mockConfig = { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: true, + TWITTER_SEARCH_ENABLE: false, + TWITTER_SPACES_ENABLE: false, + TWITTER_TARGET_USERS: [], + TWITTER_MAX_TWEETS_PER_DAY: 10, + TWITTER_MAX_TWEET_LENGTH: 280, + POST_INTERVAL_MIN: 5, + POST_INTERVAL_MAX: 10, + ACTION_INTERVAL: 5, + ENABLE_ACTION_PROCESSING: true, + POST_IMMEDIATELY: false + }; + }); + + it('should create instance with correct configuration', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client).toBeDefined(); + expect(client.twitterConfig).toBeDefined(); + expect(client.twitterConfig.TWITTER_USERNAME).toBe('testuser'); + expect(client.twitterConfig.TWITTER_DRY_RUN).toBe(true); + }); + + it('should initialize with correct tweet length limit', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client.twitterConfig.TWITTER_MAX_TWEET_LENGTH).toBe(280); + }); + + it('should initialize with correct post intervals', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client.twitterConfig.POST_INTERVAL_MIN).toBe(5); + expect(client.twitterConfig.POST_INTERVAL_MAX).toBe(10); + }); + + it('should initialize with correct action settings', () => { + const client = new ClientBase(mockRuntime, mockConfig); + expect(client.twitterConfig.ACTION_INTERVAL).toBe(5); + expect(client.twitterConfig.ENABLE_ACTION_PROCESSING).toBe(true); + }); +}); diff --git a/packages/client-twitter/__tests__/environment.test.ts b/packages/client-twitter/__tests__/environment.test.ts new file mode 100644 index 00000000000..dccfd0584b1 --- /dev/null +++ b/packages/client-twitter/__tests__/environment.test.ts @@ -0,0 +1,134 @@ +import { describe, it, expect, vi } from 'vitest'; +import { validateTwitterConfig } from '../src/environment'; +import { IAgentRuntime } from '@elizaos/core'; + +describe('Twitter Environment Configuration', () => { + const mockRuntime: IAgentRuntime = { + env: { + TWITTER_USERNAME: 'testuser123', + TWITTER_DRY_RUN: 'true', + TWITTER_SEARCH_ENABLE: 'false', + TWITTER_SPACES_ENABLE: 'false', + TWITTER_TARGET_USERS: 'user1,user2,user3', + TWITTER_MAX_TWEETS_PER_DAY: '10', + TWITTER_MAX_TWEET_LENGTH: '280', + TWITTER_POST_INTERVAL_MIN: '90', + TWITTER_POST_INTERVAL_MAX: '180', + TWITTER_ACTION_INTERVAL: '5', + TWITTER_ENABLE_ACTION_PROCESSING: 'false', + TWITTER_POST_IMMEDIATELY: 'false', + TWITTER_EMAIL: 'test@example.com', + TWITTER_PASSWORD: 'hashedpassword', + TWITTER_2FA_SECRET: '', + TWITTER_POLL_INTERVAL: '120', + TWITTER_RETRY_LIMIT: '5', + ACTION_TIMELINE_TYPE: 'foryou', + MAX_ACTIONS_PROCESSING: '1', + MAX_TWEET_LENGTH: '280' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + } + } as unknown as IAgentRuntime; + + it('should validate correct configuration', async () => { + const config = await validateTwitterConfig(mockRuntime); + expect(config).toBeDefined(); + expect(config.TWITTER_USERNAME).toBe('testuser123'); + expect(config.TWITTER_DRY_RUN).toBe(true); + expect(config.TWITTER_SEARCH_ENABLE).toBe(false); + expect(config.TWITTER_SPACES_ENABLE).toBe(false); + expect(config.TWITTER_TARGET_USERS).toEqual(['user1', 'user2', 'user3']); + expect(config.MAX_TWEET_LENGTH).toBe(280); + expect(config.POST_INTERVAL_MIN).toBe(90); + expect(config.POST_INTERVAL_MAX).toBe(180); + expect(config.ACTION_INTERVAL).toBe(5); + expect(config.ENABLE_ACTION_PROCESSING).toBe(false); + expect(config.POST_IMMEDIATELY).toBe(false); + }); + + it('should validate wildcard username', async () => { + const wildcardRuntime = { + ...mockRuntime, + env: { + ...mockRuntime.env, + TWITTER_USERNAME: '*' + }, + getEnv: function(key: string) { + return this.env[key] || null; + }, + getSetting: function(key: string) { + return this.env[key] || null; + } + } as IAgentRuntime; + + const config = await validateTwitterConfig(wildcardRuntime); + expect(config.TWITTER_USERNAME).toBe('*'); + }); + + it('should validate username with numbers and underscores', async () => { + const validRuntime = { + ...mockRuntime, + env: { + ...mockRuntime.env, + TWITTER_USERNAME: 'test_user_123' + }, + getEnv: function(key: string) { + return this.env[key] || null; + }, + getSetting: function(key: string) { + return this.env[key] || null; + } + } as IAgentRuntime; + + const config = await validateTwitterConfig(validRuntime); + expect(config.TWITTER_USERNAME).toBe('test_user_123'); + }); + + it('should handle empty target users', async () => { + const runtimeWithoutTargets = { + ...mockRuntime, + env: { + ...mockRuntime.env, + TWITTER_TARGET_USERS: '' + }, + getEnv: function(key: string) { + return this.env[key] || null; + }, + getSetting: function(key: string) { + return this.env[key] || null; + } + } as IAgentRuntime; + + const config = await validateTwitterConfig(runtimeWithoutTargets); + expect(config.TWITTER_TARGET_USERS).toHaveLength(0); + }); + + it('should use default values when optional configs are missing', async () => { + const minimalRuntime = { + env: { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: 'true', + TWITTER_EMAIL: 'test@example.com', + TWITTER_PASSWORD: 'hashedpassword', + TWITTER_2FA_SECRET: '', + MAX_TWEET_LENGTH: '280' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + } + } as unknown as IAgentRuntime; + + const config = await validateTwitterConfig(minimalRuntime); + expect(config).toBeDefined(); + expect(config.MAX_TWEET_LENGTH).toBe(280); + expect(config.POST_INTERVAL_MIN).toBe(90); + expect(config.POST_INTERVAL_MAX).toBe(180); + }); +}); diff --git a/packages/client-twitter/__tests__/post.test.ts b/packages/client-twitter/__tests__/post.test.ts new file mode 100644 index 00000000000..7459b68d626 --- /dev/null +++ b/packages/client-twitter/__tests__/post.test.ts @@ -0,0 +1,97 @@ +import { describe, it, expect, vi } from 'vitest'; +import { TwitterPostClient } from '../src/post'; +import { ClientBase } from '../src/base'; +import { IAgentRuntime } from '@elizaos/core'; +import { TwitterConfig } from '../src/environment'; + +describe('Twitter Post Client', () => { + let mockRuntime: IAgentRuntime; + let mockConfig: TwitterConfig; + let baseClient: ClientBase; + + beforeEach(() => { + mockRuntime = { + env: { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: 'true', + TWITTER_POST_INTERVAL_MIN: '5', + TWITTER_POST_INTERVAL_MAX: '10', + TWITTER_ACTION_INTERVAL: '5', + TWITTER_ENABLE_ACTION_PROCESSING: 'true', + TWITTER_POST_IMMEDIATELY: 'false', + TWITTER_SEARCH_ENABLE: 'false', + TWITTER_EMAIL: 'test@example.com', + TWITTER_PASSWORD: 'hashedpassword', + TWITTER_2FA_SECRET: '', + TWITTER_POLL_INTERVAL: '120', + TWITTER_RETRY_LIMIT: '5', + ACTION_TIMELINE_TYPE: 'foryou', + MAX_ACTIONS_PROCESSING: '1', + MAX_TWEET_LENGTH: '280' + }, + getEnv: function (key: string) { + return this.env[key] || null; + }, + getSetting: function (key: string) { + return this.env[key] || null; + }, + character: { + style: { + all: ['Test style 1', 'Test style 2'], + post: ['Post style 1', 'Post style 2'] + } + } + } as unknown as IAgentRuntime; + + mockConfig = { + TWITTER_USERNAME: 'testuser', + TWITTER_DRY_RUN: true, + TWITTER_SEARCH_ENABLE: false, + TWITTER_SPACES_ENABLE: false, + TWITTER_TARGET_USERS: [], + TWITTER_MAX_TWEETS_PER_DAY: 10, + TWITTER_MAX_TWEET_LENGTH: 280, + POST_INTERVAL_MIN: 5, + POST_INTERVAL_MAX: 10, + ACTION_INTERVAL: 5, + ENABLE_ACTION_PROCESSING: true, + POST_IMMEDIATELY: false, + MAX_TWEET_LENGTH: 280 + }; + + baseClient = new ClientBase(mockRuntime, mockConfig); + }); + + it('should create post client instance', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + expect(postClient).toBeDefined(); + expect(postClient.twitterUsername).toBe('testuser'); + expect(postClient['isDryRun']).toBe(true); + }); + + it('should keep tweets under max length when already valid', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + const validTweet = 'This is a valid tweet'; + const result = postClient['trimTweetLength'](validTweet); + expect(result).toBe(validTweet); + expect(result.length).toBeLessThanOrEqual(280); + }); + + it('should cut at last sentence when possible', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + const longTweet = 'First sentence. Second sentence that is quite long. Third sentence that would make it too long.'; + const result = postClient['trimTweetLength'](longTweet); + const lastPeriod = result.lastIndexOf('.'); + expect(lastPeriod).toBeGreaterThan(0); + expect(result.length).toBeLessThanOrEqual(280); + }); + + it('should add ellipsis when cutting within a sentence', () => { + const postClient = new TwitterPostClient(baseClient, mockRuntime); + const longSentence = 'This is an extremely long sentence without any periods that needs to be truncated because it exceeds the maximum allowed length for a tweet on the Twitter platform and therefore must be shortened'; + const result = postClient['trimTweetLength'](longSentence); + const lastSpace = result.lastIndexOf(' '); + expect(lastSpace).toBeGreaterThan(0); + expect(result.length).toBeLessThanOrEqual(280); + }); +}); diff --git a/packages/client-twitter/package.json b/packages/client-twitter/package.json index 2dc3a3543ef..cc66c678a28 100644 --- a/packages/client-twitter/package.json +++ b/packages/client-twitter/package.json @@ -25,12 +25,16 @@ "zod": "3.23.8" }, "devDependencies": { - "tsup": "8.3.5" + "tsup": "8.3.5", + "vitest": "1.1.3", + "@vitest/coverage-v8": "1.1.3" }, "scripts": { "build": "tsup --format esm --dts", "dev": "tsup --format esm --dts --watch", - "lint": "eslint --fix --cache ." + "lint": "eslint --fix --cache .", + "test": "vitest run", + "test:coverage": "vitest run --coverage" }, "peerDependencies": { "whatwg-url": "7.1.0" diff --git a/packages/client-twitter/vitest.config.ts b/packages/client-twitter/vitest.config.ts new file mode 100644 index 00000000000..2e60e80f5dc --- /dev/null +++ b/packages/client-twitter/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['__tests__/**/*.test.ts'], + coverage: { + reporter: ['text', 'json', 'html'], + }, + }, +});