Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,72 @@ public async Task SaveLayout_MapsPayloadNodes()
Assert.Equal(77, service.LastActiveNodeId);
}

[Fact]
public async Task DeleteSession_ReturnsNoContent_WhenServiceSucceeds()
{
var service = new FakeChatSessionService();
var controller = CreateController(service);

var result = await controller.DeleteSession(55);

Assert.IsType<NoContentResult>(result);
Assert.Equal(55, service.LastSessionId);
}

[Fact]
public async Task GenerateNote_PassesRouteAndOverridesToService()
{
var service = new FakeChatSessionService
{
State = new ChatSessionStateResponse
{
Graph = new ChatSessionGraphResponse
{
Session = new ChatSessionSummaryResponse { Id = 12, Title = "Launch plan" },
RootNodeId = 42,
ActiveNodeId = 42
},
Node = new ChatCanvasNodeDetailResponse
{
SessionId = 12,
NodeId = 99,
Title = "AI Summary"
}
}
};
var controller = CreateController(service);

var result = await controller.GenerateNote(12, 42, new GenerateChatNodeNoteRequest
{
ProviderOverride = "openai",
ModelOverride = "gpt-4o-mini"
});

var okResult = Assert.IsType<OkObjectResult>(result);
Assert.IsType<ChatSessionStateResponse>(okResult.Value);
Assert.Equal(12, service.LastSessionId);
Assert.Equal(42, service.LastNodeId);
Assert.Equal("openai", service.LastProviderOverride);
Assert.Equal("gpt-4o-mini", service.LastModelOverride);
}

[Fact]
public async Task GenerateNote_ReturnsLlmErrorPayload_WhenServiceThrowsLlmException()
{
var service = new FakeChatSessionService
{
ExceptionToThrow = new LlmChatException("Provider key missing", "provider_key_missing", 422)
};
var controller = CreateController(service);

var result = await controller.GenerateNote(12, 42, new GenerateChatNodeNoteRequest());

var objectResult = Assert.IsType<ObjectResult>(result);
Assert.Equal(422, objectResult.StatusCode);
Assert.Equal("Provider key missing", ReadAnonymousProperty<string>(objectResult.Value!, "message"));
Assert.Equal("provider_key_missing", ReadAnonymousProperty<string>(objectResult.Value!, "code"));
}

private static ChatSessionsController CreateController(IChatSessionService service, int userId = 1)
{
var controller = new ChatSessionsController(service);
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ services:
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
ports:
- "3307:3306"
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
Expand Down
33 changes: 11 additions & 22 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

91 changes: 91 additions & 0 deletions frontend/src/services/chatSessionService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

const getMock = vi.fn()
const postMock = vi.fn()
const patchMock = vi.fn()
const deleteMock = vi.fn()

vi.mock('./api', () => ({
default: {
get: getMock,
post: postMock,
patch: patchMock,
delete: deleteMock,
},
}))

describe('chatSessionService', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('creates chat sessions using current payload contract', async () => {
postMock.mockResolvedValueOnce({ data: { graph: { session: { id: 10 } }, node: { nodeId: 20 } } })
const { createChatSession } = await import('./chatSessionService')

const response = await createChatSession({
title: 'Sprint 4',
providerOverride: 'openai',
modelOverride: 'gpt-4o-mini',
})

expect(postMock).toHaveBeenCalledWith('/chat-sessions', {
title: 'Sprint 4',
providerOverride: 'openai',
modelOverride: 'gpt-4o-mini',
})
expect(response.graph.session.id).toBe(10)
})

it('adds content only when provided for session bootstrap', async () => {
postMock.mockResolvedValueOnce({ data: { graph: { session: { id: 15 } }, node: { nodeId: 40 } } })
const { createChatSession } = await import('./chatSessionService')

await createChatSession({ title: 'With content', content: 'Hello node' })

expect(postMock).toHaveBeenCalledWith('/chat-sessions', {
title: 'With content',
providerOverride: undefined,
modelOverride: undefined,
content: 'Hello node',
})
})

it('uses notes endpoint for generated notes', async () => {
postMock.mockResolvedValueOnce({ data: { graph: { session: { id: 4 } }, node: { nodeId: 9 } } })
const { generateChatNodeNote } = await import('./chatSessionService')

await generateChatNodeNote(4, 9, 'gemini', 'gemini-1.5-flash')

expect(postMock).toHaveBeenCalledWith('/chat-sessions/4/nodes/9/notes', {
providerOverride: 'gemini',
modelOverride: 'gemini-1.5-flash',
})
})

it('uses move transfer mode by default when creating message-selection branch', async () => {
postMock.mockResolvedValueOnce({ data: { graph: { session: { id: 4 } }, node: { nodeId: 9 } } })
const { moveChatNodeMessagesToBranch } = await import('./chatSessionService')

await moveChatNodeMessagesToBranch(4, 9, [100, 101])

expect(postMock).toHaveBeenCalledWith('/chat-sessions/4/nodes/9/message-selections/branches', {
messageIds: [100, 101],
transferMode: 'move',
})
})

it('uses branch endpoint with required payload', async () => {
postMock.mockResolvedValueOnce({ data: { graph: { session: { id: 8 } }, node: { nodeId: 18 } } })
const { createChatBranch } = await import('./chatSessionService')

await createChatBranch(8, 18, 501, 'Branch from assistant reply', 'openai', 'gpt-4o-mini')

expect(postMock).toHaveBeenCalledWith('/chat-sessions/8/nodes/18/branches', {
branchFromMessageId: 501,
content: 'Branch from assistant reply',
providerOverride: 'openai',
modelOverride: 'gpt-4o-mini',
})
})
})
9 changes: 9 additions & 0 deletions tests/playwright/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
BASE_API_URL=http://localhost:8080/api
BASE_UI_URL=http://localhost:3000

TEST_USER_EMAIL=your-user@example.com
TEST_USER_PASSWORD=YourPassword123!

# Optional second user. If omitted, cross-user tests are skipped.
TEST_USER2_EMAIL=
TEST_USER2_PASSWORD=
Loading
Loading