mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 19:53:48 +08: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 updates and type conversions
|
||||||
|
|
||||||
> session states after pinning - should unpin existing
|
> session states after pinning - should unpin existing
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import { PrismaClient, User, Workspace } from '@prisma/client';
|
import { PrismaClient, User, Workspace } from '@prisma/client';
|
||||||
import ava, { ExecutionContext, TestFn } from 'ava';
|
import ava, { ExecutionContext, TestFn } from 'ava';
|
||||||
|
|
||||||
import { CopilotPromptInvalid } from '../../base';
|
import { CopilotPromptInvalid, CopilotSessionInvalidInput } from '../../base';
|
||||||
import {
|
import {
|
||||||
CopilotSessionModel,
|
CopilotSessionModel,
|
||||||
UpdateChatSessionData,
|
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;
|
const { copilotSession, db } = t.context;
|
||||||
|
|
||||||
await createTestPrompts(copilotSession, db);
|
await createTestPrompts(copilotSession, db);
|
||||||
|
|
||||||
const sessionId = 'session-update-id';
|
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';
|
const docId = 'doc-update-id';
|
||||||
|
|
||||||
await createTestSession(t, { sessionId });
|
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';
|
const existingPinnedId = 'existing-pinned-session-id';
|
||||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||||
|
|
||||||
|
// should unpin existing when pinning new session
|
||||||
await copilotSession.update(user.id, sessionId, { pinned: true });
|
await copilotSession.update(user.id, sessionId, { pinned: true });
|
||||||
|
|
||||||
const sessionStatesAfterPin = await Promise.all([
|
const sessionStatesAfterPin = await Promise.all([
|
||||||
getSessionState(db, sessionId),
|
getSessionState(db, sessionId),
|
||||||
getSessionState(db, existingPinnedId),
|
getSessionState(db, existingPinnedId),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
t.snapshot(
|
t.snapshot(
|
||||||
sessionStatesAfterPin,
|
sessionStatesAfterPin,
|
||||||
'session states after pinning - should unpin existing'
|
'should unpin existing when pinning new session'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// should unpin the session
|
// test type conversions
|
||||||
{
|
|
||||||
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
|
|
||||||
{
|
{
|
||||||
const conversionSteps: any[] = [];
|
const conversionSteps: any[] = [];
|
||||||
|
|
||||||
let session = await db.aiSession.findUnique({
|
|
||||||
where: { id: sessionId },
|
|
||||||
select: { docId: true, pinned: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
const convertSession = async (
|
const convertSession = async (
|
||||||
step: string,
|
step: string,
|
||||||
data: UpdateChatSessionData
|
data: UpdateChatSessionData
|
||||||
) => {
|
) => {
|
||||||
await copilotSession.update(user.id, sessionId, data);
|
await copilotSession.update(user.id, sessionId, data);
|
||||||
session = await db.aiSession.findUnique({
|
const session = await db.aiSession.findUnique({
|
||||||
where: { id: sessionId },
|
where: { id: sessionId },
|
||||||
select: { docId: true, pinned: true },
|
select: { docId: true, pinned: true },
|
||||||
});
|
});
|
||||||
@@ -349,23 +446,14 @@ test('session updates and type conversions', async t => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
const conversions = [
|
||||||
await convertSession('workspace_to_doc', { docId }); // Workspace → Doc session
|
['pinned_to_doc', { docId, pinned: false }],
|
||||||
await convertSession('doc_to_pinned', { pinned: true }); // Doc → Pinned session
|
['doc_to_workspace', { docId: null }],
|
||||||
await convertSession('pinned_to_workspace', {
|
['workspace_to_pinned', { pinned: true }],
|
||||||
pinned: false,
|
] as const;
|
||||||
docId: null,
|
|
||||||
}); // Pinned → Workspace session
|
|
||||||
await convertSession('workspace_to_pinned', { pinned: true }); // Workspace → Pinned session
|
|
||||||
}
|
|
||||||
|
|
||||||
// not allow convert to action prompt
|
for (const [step, data] of conversions) {
|
||||||
{
|
await convertSession(step, data);
|
||||||
await t.throwsAsync(
|
|
||||||
copilotSession.update(user.id, sessionId, {
|
|
||||||
promptName: 'action-prompt',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
t.snapshot(conversionSteps, 'session type conversion steps');
|
t.snapshot(conversionSteps, 'session type conversion steps');
|
||||||
|
|||||||
@@ -293,18 +293,32 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const session = await this.getExists(
|
const session = await this.getExists(
|
||||||
sessionId,
|
sessionId,
|
||||||
{ id: true, workspaceId: true, docId: true, pinned: true, prompt: true },
|
{
|
||||||
|
id: true,
|
||||||
|
workspaceId: true,
|
||||||
|
docId: true,
|
||||||
|
parentSessionId: true,
|
||||||
|
pinned: true,
|
||||||
|
prompt: true,
|
||||||
|
},
|
||||||
{ userId }
|
{ userId }
|
||||||
);
|
);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
throw new CopilotSessionNotFound();
|
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 (data.promptName) {
|
||||||
if (session.prompt.action) {
|
|
||||||
throw new CopilotSessionInvalidInput(
|
|
||||||
`Cannot update prompt for action: ${session.id}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const prompt = await this.db.aiPrompt.findFirst({
|
const prompt = await this.db.aiPrompt.findFirst({
|
||||||
where: { name: data.promptName },
|
where: { name: data.promptName },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user