Skip to content

Commit

Permalink
test: add test
Browse files Browse the repository at this point in the history
  • Loading branch information
appflowy committed Dec 16, 2024
1 parent d6bbfc3 commit 0f6ca3c
Show file tree
Hide file tree
Showing 8 changed files with 466 additions and 323 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/integration_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

name: Integration Test

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'

- name: Install dependencies
run: pnpm install

- name: Run tests
run: pnpm test
6 changes: 5 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ export default {
"^.+.tsx?$": ["ts-jest",{}],
},
testMatch: ['**/*.test.ts', '**/*.test.tsx'],
};
moduleDirectories: ['node_modules', 'lib'],
moduleNameMapper: {
'^@appflowy-chat/(.*)$': '<rootDir>/lib/$1',
},
};
19 changes: 12 additions & 7 deletions lib/types/ai.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChatError, ResponseError } from "./error";
import { ChatError } from "./error";

export const STREAM_METADATA_KEY: string = "0";
export const STREAM_ANSWER_KEY: string = "1";
Expand Down Expand Up @@ -95,10 +95,11 @@ interface QuestionStreamValue {
type: "Answer" | "Metadata";
value: JSONValue;
}

export class QuestionStream {
private stream: AsyncIterableIterator<JSONValue | ResponseError>;
private stream: AsyncIterableIterator<JSONValue | ChatError>;

constructor(stream: AsyncIterableIterator<JSONValue | ResponseError>) {
constructor(stream: AsyncIterableIterator<JSONValue | ChatError>) {
this.stream = stream;
}

Expand All @@ -108,13 +109,13 @@ export class QuestionStream {
}

// Processes the next item in the stream
async next(): Promise<QuestionStreamValue | ResponseError | null> {
async next(): Promise<QuestionStreamValue | ChatError | null> {
const result = await this.stream.next();
if (result.done) return null;

const value = result.value;
if ((value as ResponseError).message) {
return value as ResponseError;
if ((value as ChatError).message) {
return value as ChatError;
}

const jsonValue = value as Record<string, JSONValue>;
Expand All @@ -130,7 +131,11 @@ export class QuestionStream {
}

// Invalid format case
return { message: "Invalid streaming value", code: -1 } as ResponseError;
return { message: "Invalid streaming value", code: -1 } as ChatError;
}

[Symbol.asyncIterator]() {
return this.stream;
}
}

Expand Down
4 changes: 4 additions & 0 deletions lib/types/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,9 @@ export class Response<T> {
isSuccess(): boolean {
return this.error === null;
}

isError(): boolean {
return this.error !== null;
}
}

217 changes: 217 additions & 0 deletions tests/services/chat_message.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/* eslint-disable @typescript-eslint/no-non-null-asserted-optional-chain */
import { MockChatHttpService } from './mock_http_service';
import {
ChatMessageType,
CreateChatMessageParams,
MessageCursor,
QuestionStream,
RepeatedChatMessage,
} from '@appflowy-chat/types/ai';
import { beforeEach, describe, expect, test } from '@jest/globals';

describe('MockChatHttpService - Create and Get Messages', () => {
let chatService: MockChatHttpService;

beforeEach(() => {
chatService = new MockChatHttpService();
// Create a test chat and add some messages
const chatParams = {
chat_id: 'chat1',
name: 'Test Chat',
rag_ids: [],
};
chatService.createChat('workspace1', chatParams);

// Add 5 messages to the chat with message type
for (let i = 0; i < 5; i++) {
const params = {
content: `Message ${i + 1}`,
message_type: ChatMessageType.User, // Adding message_type as User
} as CreateChatMessageParams;
chatService.sendMessage('workspace1', 'chat1', params);
}
});

// --- Test 1: Create Message ---
test('should create a new message with message_type and add it to the chat', async () => {
const params: CreateChatMessageParams = {
content: 'Hello, world!',
message_type: ChatMessageType.User,
};
const response = await chatService.sendMessage(
'workspace1',
'chat1',
params
);

expect(response.isSuccess()).toBe(true);
expect(response.data).toBeTruthy();
expect(response.data?.content).toBe('Hello, world!');

const chat = chatService['chatMap'].get('chat1');
expect(chat?.messages.length).toBe(6); // There should be 6 messages now
expect(chat?.messages[5].content).toBe('Hello, world!');
});

test('should return error when creating a message for a non-existent chat', async () => {
const params: CreateChatMessageParams = {
content: 'This should fail!',
message_type: ChatMessageType.User,
};
const response = await chatService.sendMessage(
'workspace1',
'nonexistent_chat',
params
);

expect(response.isError()).toBe(true);
expect(response.error?.message).toBe('Chat not found');
});

// --- Test 2: Get Messages (Offset Cursor) ---
test('should get messages successfully with offset cursor', async () => {
const cursor: MessageCursor = { type: 'Offset', value: 0 };
const limit = 3;
const response = await chatService.getChatMessages(
'workspace1',
'chat1',
cursor,
limit
);

expect(response.isSuccess()).toBe(true);
const repeatedChatMessage: RepeatedChatMessage | null = response.data;
expect(repeatedChatMessage?.messages.length).toBe(limit);
expect(repeatedChatMessage?.messages[0].content).toBe('Message 1');
expect(repeatedChatMessage?.has_more).toBe(true); // There are more messages to fetch
expect(repeatedChatMessage?.total).toBe(5); // Total number of messages in the chat
});

// --- Test 3: Get Messages (AfterMessageId Cursor) ---
test('should get messages successfully with AfterMessageId cursor', async () => {
const chat = chatService['chatMap'].get('chat1');
const cursor: MessageCursor = {
type: 'AfterMessageId',
value: chat?.messages[2].message_id!,
}; // After message 3
const limit = 2;
const response = await chatService.getChatMessages(
'workspace1',
'chat1',
cursor,
limit
);

expect(response.isSuccess()).toBe(true);
const repeatedChatMessage: RepeatedChatMessage | null = response.data;
expect(repeatedChatMessage?.messages.length).toBe(limit);
expect(repeatedChatMessage?.messages[0].content).toBe('Message 4');
expect(repeatedChatMessage?.messages[1].content).toBe('Message 5');
expect(repeatedChatMessage?.has_more).toBe(false); // No more messages after this
expect(repeatedChatMessage?.total).toBe(5); // Total number of messages in the chat
});

// --- Test 4: Get Messages (BeforeMessageId Cursor) ---
test('should get messages successfully with BeforeMessageId cursor', async () => {
const chat = chatService['chatMap'].get('chat1');
const cursor: MessageCursor = {
type: 'BeforeMessageId',
value: chat?.messages[3].message_id!,
}; // Before message 4
const limit = 3;
const response = await chatService.getChatMessages(
'workspace1',
'chat1',
cursor,
limit
);

expect(response.isSuccess()).toBe(true);
const repeatedChatMessage: RepeatedChatMessage | null = response.data;
expect(repeatedChatMessage?.messages.length).toBe(limit);
expect(repeatedChatMessage?.messages[0].content).toBe('Message 1');
expect(repeatedChatMessage?.messages[1].content).toBe('Message 2');
expect(repeatedChatMessage?.messages[2].content).toBe('Message 3');
expect(repeatedChatMessage?.has_more).toBe(true); // There are more messages before this
expect(repeatedChatMessage?.total).toBe(5); // Total number of messages in the chat
});

// --- Test 5: Get Messages (Invalid Cursor Type) ---
test('should return error when using an unsupported cursor type', async () => {
const cursor: MessageCursor = { type: 'NextBack' };
const limit = 3;
const response = await chatService.getChatMessages(
'workspace1',
'chat1',
cursor,
limit
);

expect(response.isError()).toBe(true);
expect(response.error?.message).toBe(
'NextBack cursor type is not fully implemented'
);
});

// --- Test 6: Get Messages (Chat Not Found) ---
test('should return error when trying to get messages for a non-existent chat', async () => {
const cursor: MessageCursor = { type: 'Offset', value: 0 };
const limit = 3;
const response = await chatService.getChatMessages(
'workspace1',
'nonexistent_chat',
cursor,
limit
);

expect(response.isError()).toBe(true);
expect(response.error?.message).toBe('Chat not found');
});

// --- Test 7: Get Messages (Message Not Found in AfterMessageId Cursor) ---
test('should return error if message id not found in AfterMessageId cursor', async () => {
const cursor: MessageCursor = { type: 'AfterMessageId', value: 9999 }; // Invalid message ID
const limit = 3;
const response = await chatService.getChatMessages(
'workspace1',
'chat1',
cursor,
limit
);

expect(response.isError()).toBe(true);
expect(response.error?.message).toBe('Message not found');
});

// --- Test 8: Stream Message Response ---
test('should stream a message response successfully', async () => {
const chat = chatService['chatMap'].get('chat1');
const message = chat?.messages[1]; // Message with ID 1
if (!message) {
throw new Error('Message not found');
}

const response = await chatService.streamMessageResponse(
'workspace1',
'chat1',
message.message_id
);

expect(response.isSuccess()).toBe(true);
expect(response.data).toBeInstanceOf(QuestionStream);
const streamData = [];
for await (const chunk of response.data!) {
streamData.push(chunk);
}
expect(streamData.length).toBeGreaterThan(0); // Ensure there are chunks in the stream
const expectedStreamData = [
{ '1': 'Streamed answer for message 1' },
{ '0': { meta: 'Metadata for message 1' } },
{ '1': 'Another streamed answer for message 1' },
{ '0': { meta: 'Additional metadata for message 1' } },
];

// Ensure the stream data matches the expected structure
expect(streamData).toEqual(expectedStreamData);
});
});
Loading

0 comments on commit 0f6ca3c

Please sign in to comment.