diff --git a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md index 3886e838d..996552706 100644 --- a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md +++ b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.md @@ -105,6 +105,52 @@ Generated by [AVA](https://avajs.dev). }, ] +## should handle session updates and validations + +> should unpin existing when pinning new session + + [ + { + docId: null, + id: 'session-update-id', + pinned: true, + }, + { + docId: null, + id: 'existing-pinned-session-id', + pinned: false, + }, + ] + +> session type conversion steps + + [ + { + session: { + docId: 'doc-update-id', + pinned: false, + }, + step: 'pinned_to_doc', + type: 'doc', + }, + { + session: { + docId: null, + pinned: false, + }, + step: 'doc_to_workspace', + type: 'workspace', + }, + { + session: { + docId: null, + pinned: true, + }, + step: 'workspace_to_pinned', + type: 'pinned', + }, + ] + ## session updates and type conversions > session states after pinning - should unpin existing diff --git a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.snap b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.snap index 89544e837..441c7f561 100644 Binary files a/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.snap and b/packages/backend/server/src/__tests__/models/__snapshots__/copilot-session.spec.ts.snap differ diff --git a/packages/backend/server/src/__tests__/models/copilot-session.spec.ts b/packages/backend/server/src/__tests__/models/copilot-session.spec.ts index fe47c9142..0e90c2d5c 100644 --- a/packages/backend/server/src/__tests__/models/copilot-session.spec.ts +++ b/packages/backend/server/src/__tests__/models/copilot-session.spec.ts @@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto'; import { PrismaClient, User, Workspace } from '@prisma/client'; import ava, { ExecutionContext, TestFn } from 'ava'; -import { CopilotPromptInvalid } from '../../base'; +import { CopilotPromptInvalid, CopilotSessionInvalidInput } from '../../base'; import { CopilotSessionModel, UpdateChatSessionData, @@ -289,56 +289,153 @@ test('should pin and unpin sessions', async t => { } }); -test('session updates and type conversions', async t => { +test('should handle session updates and validations', async t => { const { copilotSession, db } = t.context; - await createTestPrompts(copilotSession, db); const sessionId = 'session-update-id'; + const actionSessionId = 'action-session-id'; + const parentSessionId = 'parent-session-id'; + const forkedSessionId = 'forked-session-id'; const docId = 'doc-update-id'; await createTestSession(t, { sessionId }); + await createTestSession(t, { + sessionId: actionSessionId, + promptName: 'action-prompt', + promptAction: 'edit', + docId: 'some-doc', + }); + await createTestSession(t, { + sessionId: parentSessionId, + docId: 'parent-doc', + }); + await db.aiSession.create({ + data: { + id: forkedSessionId, + workspaceId: workspace.id, + userId: user.id, + docId: 'forked-doc', + pinned: false, + promptName: 'test-prompt', + promptAction: null, + parentSessionId: parentSessionId, + }, + }); - // should unpin existing pinned session + const assertUpdateThrows = async ( + t: ExecutionContext, + sessionId: string, + updateData: UpdateChatSessionData, + message: string + ) => { + await t.throwsAsync( + t.context.copilotSession.update(user.id, sessionId, updateData), + { instanceOf: CopilotSessionInvalidInput }, + message + ); + }; + + const assertUpdate = async ( + t: ExecutionContext, + sessionId: string, + updateData: UpdateChatSessionData, + message: string + ) => { + await t.notThrowsAsync( + t.context.copilotSession.update(user.id, sessionId, updateData), + message + ); + }; + + // case 1: action sessions should reject all updates + { + const actionUpdates = [ + { docId: 'new-doc' }, + { pinned: true }, + { promptName: 'test-prompt' }, + ]; + for (const data of actionUpdates) { + await assertUpdateThrows( + t, + actionSessionId, + data, + `action session should reject update: ${JSON.stringify(data)}` + ); + } + } + + // case 2: forked sessions should reject docId updates but allow others + { + await assertUpdate( + t, + forkedSessionId, + { pinned: true }, + 'forked session should allow pinned update' + ); + await assertUpdate( + t, + forkedSessionId, + { promptName: 'test-prompt' }, + 'forked session should allow promptName update' + ); + await assertUpdateThrows( + t, + forkedSessionId, + { docId: 'new-doc' }, + 'forked session should reject docId update' + ); + } + + { + // case 3: prompt update validation + await assertUpdate( + t, + sessionId, + { promptName: 'test-prompt' }, + 'should allow valid non-action prompt' + ); + await assertUpdateThrows( + t, + sessionId, + { promptName: 'action-prompt' }, + 'should reject action prompt' + ); + await assertUpdateThrows( + t, + sessionId, + { promptName: 'non-existent-prompt' }, + 'should reject non-existent prompt' + ); + } + + // cest 4: session type conversions and pinning behavior { const existingPinnedId = 'existing-pinned-session-id'; await createTestSession(t, { sessionId: existingPinnedId, pinned: true }); + // should unpin existing when pinning new session await copilotSession.update(user.id, sessionId, { pinned: true }); const sessionStatesAfterPin = await Promise.all([ getSessionState(db, sessionId), getSessionState(db, existingPinnedId), ]); - t.snapshot( sessionStatesAfterPin, - 'session states after pinning - should unpin existing' + 'should unpin existing when pinning new session' ); } - // should unpin the session - { - await copilotSession.update(user.id, sessionId, { pinned: false }); - const sessionStateAfterUnpin = await getSessionState(db, sessionId); - t.snapshot(sessionStateAfterUnpin, 'session state after unpinning'); - } - - // should convert session types + // test type conversions { const conversionSteps: any[] = []; - - let session = await db.aiSession.findUnique({ - where: { id: sessionId }, - select: { docId: true, pinned: true }, - }); - const convertSession = async ( step: string, data: UpdateChatSessionData ) => { await copilotSession.update(user.id, sessionId, data); - session = await db.aiSession.findUnique({ + const session = await db.aiSession.findUnique({ where: { id: sessionId }, select: { docId: true, pinned: true }, }); @@ -349,23 +446,14 @@ test('session updates and type conversions', async t => { }); }; - { - await convertSession('workspace_to_doc', { docId }); // Workspace → Doc session - await convertSession('doc_to_pinned', { pinned: true }); // Doc → Pinned session - await convertSession('pinned_to_workspace', { - pinned: false, - docId: null, - }); // Pinned → Workspace session - await convertSession('workspace_to_pinned', { pinned: true }); // Workspace → Pinned session - } + const conversions = [ + ['pinned_to_doc', { docId, pinned: false }], + ['doc_to_workspace', { docId: null }], + ['workspace_to_pinned', { pinned: true }], + ] as const; - // not allow convert to action prompt - { - await t.throwsAsync( - copilotSession.update(user.id, sessionId, { - promptName: 'action-prompt', - }) - ); + for (const [step, data] of conversions) { + await convertSession(step, data); } t.snapshot(conversionSteps, 'session type conversion steps'); diff --git a/packages/backend/server/src/models/copilot-session.ts b/packages/backend/server/src/models/copilot-session.ts index de516c036..aecc1a898 100644 --- a/packages/backend/server/src/models/copilot-session.ts +++ b/packages/backend/server/src/models/copilot-session.ts @@ -293,18 +293,32 @@ export class CopilotSessionModel extends BaseModel { ): Promise { const session = await this.getExists( sessionId, - { id: true, workspaceId: true, docId: true, pinned: true, prompt: true }, + { + id: true, + workspaceId: true, + docId: true, + parentSessionId: true, + pinned: true, + prompt: true, + }, { userId } ); if (!session) { throw new CopilotSessionNotFound(); } + + // not allow to update action session + if (session.prompt.action) { + throw new CopilotSessionInvalidInput( + `Cannot update action: ${session.id}` + ); + } else if (data.docId && session.parentSessionId) { + throw new CopilotSessionInvalidInput( + `Cannot update docId for forked session: ${session.id}` + ); + } + if (data.promptName) { - if (session.prompt.action) { - throw new CopilotSessionInvalidInput( - `Cannot update prompt for action: ${session.id}` - ); - } const prompt = await this.db.aiPrompt.findFirst({ where: { name: data.promptName }, });