diff --git a/backend/src/__tests__/mocks/twitter-service.mock.ts b/backend/src/__tests__/mocks/twitter-service.mock.ts index 5bf3544..e3ea987 100644 --- a/backend/src/__tests__/mocks/twitter-service.mock.ts +++ b/backend/src/__tests__/mocks/twitter-service.mock.ts @@ -1,4 +1,4 @@ -import { Tweet } from "agent-twitter-client"; +import { SearchMode, Tweet } from "agent-twitter-client"; import { TwitterService } from "../../services/twitter/client"; import { logger } from "../../utils/logger"; @@ -8,9 +8,9 @@ export class MockTwitterService extends TwitterService { private tweetIdCounter: bigint = BigInt(Date.now()); constructor() { - // Pass empty config since we're mocking + // Pass config with the bot's username so mentions are found super({ - username: "mock_user", + username: "curatedotfun", password: "mock_pass", email: "mock@example.com", }); @@ -21,6 +21,47 @@ export class MockTwitterService extends TwitterService { logout: async () => {}, getCookies: async () => [], setCookies: async () => {}, + fetchSearchTweets: async (query: string, count: number, mode: SearchMode) => { + // Filter tweets that match the query (mentions @curatedotfun) + const matchingTweets = this.mockTweets.filter(tweet => + tweet.text?.includes("@curatedotfun") + ); + + // Sort by ID descending (newest first) to match Twitter search behavior + const sortedTweets = [...matchingTweets].sort((a, b) => { + const aId = BigInt(a.id); + const bId = BigInt(b.id); + return bId > aId ? 1 : bId < aId ? -1 : 0; + }); + + return { + tweets: sortedTweets.slice(0, count), + }; + }, + likeTweet: async (tweetId: string) => { + logger.info(`Mock: Liked tweet ${tweetId}`); + return true; + }, + sendTweet: async (message: string, replyToId?: string) => { + const newTweet = this.addMockTweet({ + text: message, + username: "curatedotfun", + inReplyToStatusId: replyToId, + }); + return { + json: async () => ({ + data: { + create_tweet: { + tweet_results: { + result: { + rest_id: newTweet.id, + }, + }, + }, + }, + }), + } as Response; + }, }; } @@ -31,7 +72,7 @@ export class MockTwitterService extends TwitterService { public addMockTweet(tweet: Partial & { inReplyToStatusId?: string }) { const fullTweet: Tweet = { - id: this.getNextTweetId(), + id: tweet.id || this.getNextTweetId(), text: tweet.text || "", username: tweet.username || "test_user", userId: tweet.userId || `mock-user-id-${tweet.username || "test_user"}`, @@ -69,86 +110,12 @@ export class MockTwitterService extends TwitterService { async getUserIdByScreenName(screenName: string): Promise { return this.mockUserIds.get(screenName) || `mock-user-id-${screenName}`; } - async fetchAllNewMentions(): Promise { - // Get the last tweet ID we processed - const lastCheckedId = this.getLastCheckedTweetId(); - - // If we have tweets and no last checked ID, set it to the newest tweet - if (this.mockTweets.length > 0 && !lastCheckedId) { - const newestTweet = this.mockTweets[this.mockTweets.length - 1]; - await this.setLastCheckedTweetId(newestTweet.id); - return [newestTweet]; - } - - // Filter tweets newer than last checked ID - const newTweets = this.mockTweets.filter(tweet => { - if (!lastCheckedId) return true; - return BigInt(tweet.id) > BigInt(lastCheckedId); - }); - - // Update last checked ID if we found new tweets - if (newTweets.length > 0) { - const newestTweet = newTweets[newTweets.length - 1]; - await this.setLastCheckedTweetId(newestTweet.id); - } - - return newTweets; + // Let the parent TwitterService handle the processing logic + return super.fetchAllNewMentions(); } - + async getTweet(tweetId: string): Promise { return this.mockTweets.find((t) => t.id === tweetId) || null; } - - async replyToTweet(tweetId: string, message: string): Promise { - const replyTweet = await this.addMockTweet({ - text: message, - username: "curatedotfun", - inReplyToStatusId: tweetId, - }); - logger.info(`Mock: Replied to tweet ${tweetId} with "${message}"`); - return replyTweet.id; - } - - async likeTweet(tweetId: string): Promise { - logger.info(`Mock: Liked tweet ${tweetId}`); - } - - // Helper methods for test scenarios - async simulateSubmission(contentUrl: string) { - // First create the content tweet - const contentTweet = await this.addMockTweet({ - text: "Original content", - username: "content_creator", - }); - - // Then create the curator's submission as a reply - return this.addMockTweet({ - text: `@curatedotfun !submit ${contentUrl}`, - username: "curator", - userId: "mock-user-id-curator", - timeParsed: new Date(), - inReplyToStatusId: contentTweet.id, - }); - } - - async simulateApprove(submissionTweetId: string, projectId: string) { - return this.addMockTweet({ - text: `@curatedotfun !approve ${projectId}`, - username: "moderator", - userId: "mock-user-id-moderator", - timeParsed: new Date(), - inReplyToStatusId: submissionTweetId, - }); - } - - async simulateReject(submissionTweetId: string, projectId: string, reason: string) { - return this.addMockTweet({ - text: `@curatedotfun !reject ${projectId} ${reason}`, - username: "moderator", - userId: "mock-user-id-moderator", - timeParsed: new Date(), - inReplyToStatusId: submissionTweetId, - }); - } } diff --git a/backend/src/routes/test.ts b/backend/src/routes/test.ts index 518b384..981d9cd 100644 --- a/backend/src/routes/test.ts +++ b/backend/src/routes/test.ts @@ -1,24 +1,27 @@ +import { Tweet } from "agent-twitter-client"; import { Elysia } from "elysia"; import { MockTwitterService } from "../__tests__/mocks/twitter-service.mock"; -import { Tweet } from "agent-twitter-client"; // Create a single mock instance to maintain state const mockTwitterService = new MockTwitterService(); // Helper to create a tweet object -const createTweet = (text: string, username: string): Tweet => ({ - id: Date.now().toString(), - text, - username, - userId: `mock-user-id-${username}`, - timeParsed: new Date(), - hashtags: [], - mentions: [], - photos: [], - urls: [], - videos: [], - thread: [], -}); +const createTweet = (id: string, text: string, username: string, inReplyToStatusId?: string, hashtags?: string[]): Tweet => { + return { + id, + text, + username, + userId: `mock-user-id-${username}`, + timeParsed: new Date(), + hashtags: hashtags ?? [], + mentions: [], + photos: [], + urls: [], + videos: [], + thread: [], + inReplyToStatusId, + }; +}; export const testRoutes = new Elysia({ prefix: "/api/test" }) .guard({ @@ -29,30 +32,15 @@ export const testRoutes = new Elysia({ prefix: "/api/test" }) } }, }) - .get("/tweets", () => { - return mockTwitterService.fetchAllNewMentions(); - }) .post("/tweets", async ({ body }) => { - const { text, username } = body as { text: string; username: string }; - const tweet = createTweet(text, username); + const { id, text, username, inReplyToStatusId, hashtags } = body as { id: string; text: string; username: string; inReplyToStatusId?: string; hashtags?: string[] }; + const tweet = createTweet(id, text, username, inReplyToStatusId, hashtags); mockTwitterService.addMockTweet(tweet); return tweet; }) .post("/reset", () => { mockTwitterService.clearMockTweets(); return { success: true }; - }) - .post("/scenario/approve", async ({ body }) => { - const { projectId } = body as { projectId: string }; - const tweet = createTweet(`@curatedotfun approve ${projectId}`, "curator"); - mockTwitterService.addMockTweet(tweet); - return { success: true, tweet }; - }) - .post("/scenario/reject", async ({ body }) => { - const { projectId, reason } = body as { projectId: string; reason: string }; - const tweet = createTweet(`@curatedotfun reject ${projectId} ${reason}`, "curator"); - mockTwitterService.addMockTweet(tweet); - return { success: true, tweet }; }); // Export for use in tests and for replacing the real service diff --git a/curate.config.test.json b/curate.config.test.json index 81b7d58..22f409f 100644 --- a/curate.config.test.json +++ b/curate.config.test.json @@ -15,7 +15,7 @@ }, "feeds": [ { - "id": "test-feed", + "id": "test", "name": "Test Feed", "description": "Main feed for testing basic functionality", "moderation": { @@ -30,7 +30,9 @@ { "plugin": "@curatedotfun/supabase", "config": { - + "supabaseUrl": "{SUPABASE_URL}", + "supabaseKey": "{SUPABASE_SECRET_KEY}", + "tableName": "test" } } ] @@ -38,7 +40,7 @@ } }, { - "id": "multi-approver", + "id": "multi", "name": "Multi-Approver Test", "description": "Testing multiple approver scenarios", "moderation": { @@ -53,7 +55,7 @@ } }, { - "id": "edge-cases", + "id": "edge", "name": "Edge Cases", "description": "Testing edge cases and error scenarios", "moderation": { diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index e04842c..0d1716f 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -30,6 +30,14 @@ const Header = () => { > How It Works + {process.env.NODE_ENV === "development" && ( + + Test Panel + + )}