mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-30 00:07:02 +02:00
fix(core): Preserve code-builder thread suffix when parsing session (#30829)
Co-authored-by: n8n-cat-bot[bot] <n8n-cat-bot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Declan Carroll <declan@n8n.io>
This commit is contained in:
parent
e9631b336f
commit
483752e8df
|
|
@ -417,9 +417,10 @@ export class AiWorkflowBuilderService {
|
|||
this.onTelemetryEvent('Builder replied to user message', properties);
|
||||
}
|
||||
|
||||
async getSessions(workflowId: string | undefined, user?: IUser) {
|
||||
async getSessions(workflowId: string | undefined, user?: IUser, isCodeBuilder?: boolean) {
|
||||
const userId = user?.id?.toString();
|
||||
return await this.sessionManager.getSessions(workflowId, userId, 'code-builder');
|
||||
const agentType = isCodeBuilder ? 'code-builder' : undefined;
|
||||
return await this.sessionManager.getSessions(workflowId, userId, agentType);
|
||||
}
|
||||
|
||||
async getBuilderInstanceCredits(
|
||||
|
|
|
|||
|
|
@ -550,7 +550,7 @@ describe('AiWorkflowBuilderService', () => {
|
|||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
'test-user-id',
|
||||
'code-builder',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -578,6 +578,19 @@ describe('AiWorkflowBuilderService', () => {
|
|||
lastUpdated: '2023-12-01T12:00:00Z',
|
||||
});
|
||||
expect(result.sessions[0].messages).toHaveLength(2);
|
||||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
'test-user-id',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should request code-builder threads when isCodeBuilder is true', async () => {
|
||||
const workflowId = 'test-workflow';
|
||||
(mockSessionManager.getSessions as jest.Mock).mockResolvedValue({ sessions: [] });
|
||||
|
||||
await service.getSessions(workflowId, mockUser, true);
|
||||
|
||||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
'test-user-id',
|
||||
|
|
@ -597,7 +610,7 @@ describe('AiWorkflowBuilderService', () => {
|
|||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
'test-user-id',
|
||||
'code-builder',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -621,7 +634,7 @@ describe('AiWorkflowBuilderService', () => {
|
|||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
'test-user-id',
|
||||
'code-builder',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -645,7 +658,7 @@ describe('AiWorkflowBuilderService', () => {
|
|||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
'test-user-id',
|
||||
'code-builder',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -658,11 +671,7 @@ describe('AiWorkflowBuilderService', () => {
|
|||
const result = await service.getSessions(workflowId);
|
||||
|
||||
expect(result.sessions).toEqual([]);
|
||||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
undefined,
|
||||
'code-builder',
|
||||
);
|
||||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(workflowId, undefined, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -709,7 +718,7 @@ describe('AiWorkflowBuilderService', () => {
|
|||
expect(mockSessionManager.getSessions).toHaveBeenCalledWith(
|
||||
workflowId,
|
||||
'test-user-id',
|
||||
'code-builder',
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@ import { Z } from '../../zod-class';
|
|||
|
||||
export class AiSessionRetrievalRequestDto extends Z.class({
|
||||
workflowId: z.string().optional(),
|
||||
codeBuilder: z.boolean().optional(),
|
||||
}) {}
|
||||
|
|
|
|||
|
|
@ -530,6 +530,51 @@ describe('AiController', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getSessions', () => {
|
||||
it('should call workflowBuilderService.getSessions with correct parameters', async () => {
|
||||
const mockSessions = { sessions: [] };
|
||||
const payload = { workflowId: 'workflow123' };
|
||||
|
||||
workflowBuilderService.getSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
const result = await controller.getSessions(request, response, payload);
|
||||
|
||||
expect(workflowBuilderService.getSessions).toHaveBeenCalledWith(
|
||||
payload.workflowId,
|
||||
request.user,
|
||||
undefined,
|
||||
);
|
||||
expect(result).toEqual(mockSessions);
|
||||
});
|
||||
|
||||
it('should forward the codeBuilder flag to the service', async () => {
|
||||
const mockSessions = { sessions: [] };
|
||||
const payload = { workflowId: 'workflow123', codeBuilder: true as const };
|
||||
|
||||
workflowBuilderService.getSessions.mockResolvedValue(mockSessions);
|
||||
|
||||
const result = await controller.getSessions(request, response, payload);
|
||||
|
||||
expect(workflowBuilderService.getSessions).toHaveBeenCalledWith(
|
||||
payload.workflowId,
|
||||
request.user,
|
||||
true,
|
||||
);
|
||||
expect(result).toEqual(mockSessions);
|
||||
});
|
||||
|
||||
it('should throw InternalServerError when service throws an error', async () => {
|
||||
const payload = { workflowId: 'workflow123' };
|
||||
const mockError = new Error('Database error');
|
||||
|
||||
workflowBuilderService.getSessions.mockRejectedValue(mockError);
|
||||
|
||||
await expect(controller.getSessions(request, response, payload)).rejects.toThrow(
|
||||
InternalServerError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('truncateMessages', () => {
|
||||
it('should call workflowBuilderService.truncateMessagesAfter with correct parameters', async () => {
|
||||
const payload = {
|
||||
|
|
|
|||
|
|
@ -251,7 +251,11 @@ export class AiController {
|
|||
@Body payload: AiSessionRetrievalRequestDto,
|
||||
) {
|
||||
try {
|
||||
const sessions = await this.workflowBuilderService.getSessions(payload.workflowId, req.user);
|
||||
const sessions = await this.workflowBuilderService.getSessions(
|
||||
payload.workflowId,
|
||||
req.user,
|
||||
payload.codeBuilder,
|
||||
);
|
||||
return sessions;
|
||||
} catch (e) {
|
||||
assert(e instanceof Error);
|
||||
|
|
|
|||
|
|
@ -69,6 +69,27 @@ describe('WorkflowBuilderSessionRepository', () => {
|
|||
it('should throw error for empty thread ID', async () => {
|
||||
await expect(repository.getSession('')).rejects.toThrow('Invalid thread ID format: ');
|
||||
});
|
||||
|
||||
it('should strip the code-builder "-code" suffix from the userId', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(null);
|
||||
|
||||
const userUuid = 'af6cc09d-e809-41c6-9e92-ca26dfd11487';
|
||||
await repository.getSession(`workflow-wf123-user-${userUuid}-code`);
|
||||
|
||||
expect(entityManager.findOne).toHaveBeenCalledWith(WorkflowBuilderSession, {
|
||||
where: { workflowId: 'wf123', userId: userUuid },
|
||||
});
|
||||
});
|
||||
|
||||
it('should not strip "-code" from a non-suffixed userId', async () => {
|
||||
entityManager.findOne.mockResolvedValueOnce(null);
|
||||
|
||||
await repository.getSession('workflow-wf123-user-user456');
|
||||
|
||||
expect(entityManager.findOne).toHaveBeenCalledWith(WorkflowBuilderSession, {
|
||||
where: { workflowId: 'wf123', userId: 'user456' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
|
|
|
|||
|
|
@ -76,8 +76,11 @@ export class WorkflowBuilderSessionRepository
|
|||
}
|
||||
|
||||
private parseThreadId(threadId: string): { workflowId: string; userId: string } {
|
||||
// Format: "workflow-{workflowId}-user-{userId}"
|
||||
const match = threadId.match(/^workflow-(.+)-user-(.+)$/);
|
||||
// Format: "workflow-{workflowId}-user-{userId}" with an optional "-code" suffix
|
||||
// for the code-builder agent thread variant. Strip the suffix before parsing so
|
||||
// the greedy userId capture doesn't swallow it (userId is a uuid column in PG).
|
||||
const normalized = threadId.endsWith('-code') ? threadId.slice(0, -'-code'.length) : threadId;
|
||||
const match = normalized.match(/^workflow-(.+)-user-(.+)$/);
|
||||
if (!match) {
|
||||
throw new Error(`Invalid thread ID format: ${threadId}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ describe('WorkflowBuilderService', () => {
|
|||
const result = await service.getSessions('workflow-123', mockUser);
|
||||
|
||||
expect(MockedAiWorkflowBuilderService).toHaveBeenCalledTimes(1);
|
||||
expect(mockAiService.getSessions).toHaveBeenCalledWith('workflow-123', mockUser);
|
||||
expect(mockAiService.getSessions).toHaveBeenCalledWith('workflow-123', mockUser, undefined);
|
||||
expect(result).toEqual(mockSessions);
|
||||
});
|
||||
|
||||
|
|
@ -286,9 +286,21 @@ describe('WorkflowBuilderService', () => {
|
|||
|
||||
const result = await service.getSessions(undefined, mockUser);
|
||||
|
||||
expect(mockAiService.getSessions).toHaveBeenCalledWith(undefined, mockUser);
|
||||
expect(mockAiService.getSessions).toHaveBeenCalledWith(undefined, mockUser, undefined);
|
||||
expect(result).toEqual(mockSessions);
|
||||
});
|
||||
|
||||
it('should forward the isCodeBuilder flag to the inner service', async () => {
|
||||
const mockSessions = { sessions: [] };
|
||||
|
||||
const mockAiService = mock<AiWorkflowBuilderService>();
|
||||
(mockAiService.getSessions as jest.Mock).mockResolvedValue(mockSessions);
|
||||
MockedAiWorkflowBuilderService.mockImplementation(() => mockAiService);
|
||||
|
||||
await service.getSessions('workflow-123', mockUser, true);
|
||||
|
||||
expect(mockAiService.getSessions).toHaveBeenCalledWith('workflow-123', mockUser, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onCreditsUpdated callback', () => {
|
||||
|
|
|
|||
|
|
@ -187,9 +187,9 @@ export class WorkflowBuilderService {
|
|||
yield* service.chat(payload, user, abortSignal);
|
||||
}
|
||||
|
||||
async getSessions(workflowId: string | undefined, user: IUser) {
|
||||
async getSessions(workflowId: string | undefined, user: IUser, isCodeBuilder?: boolean) {
|
||||
const service = await this.getService();
|
||||
const sessions = await service.getSessions(workflowId, user);
|
||||
const sessions = await service.getSessions(workflowId, user, isCodeBuilder);
|
||||
return sessions;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user