mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
fix(server): session update check (#12877)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved validation to prevent updates to sessions with action prompts and restrict certain updates on forked sessions. - **Tests** - Expanded and clarified test coverage for session updates, pinning behavior, and session type conversions, with more explicit error handling and validation scenarios. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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<Context>,
|
||||
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<Context>,
|
||||
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');
|
||||
|
||||
@@ -293,18 +293,32 @@ export class CopilotSessionModel extends BaseModel {
|
||||
): Promise<string> {
|
||||
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 },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user