mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
feat(server): allow chat session dangling & pin session support (#12849)
fix AI-181 fix AI-179 fix AI-178 fix PD-2682 fix PD-2683
This commit is contained in:
@@ -135,3 +135,31 @@ Generated by [AVA](https://avajs.dev).
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
## should create different session types and validate prompt constraints
|
||||
|
||||
> should create session with should create workspace session with text prompt
|
||||
|
||||
[
|
||||
{
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
|
||||
> should create session with should create pinned session with text prompt
|
||||
|
||||
[
|
||||
{
|
||||
docId: 'pinned-doc',
|
||||
pinned: true,
|
||||
},
|
||||
]
|
||||
|
||||
> should create session with should create doc session with text prompt
|
||||
|
||||
[
|
||||
{
|
||||
docId: 'normal-doc',
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
|
||||
Binary file not shown.
@@ -48,7 +48,11 @@ import {
|
||||
createCopilotContext,
|
||||
createCopilotMessage,
|
||||
createCopilotSession,
|
||||
createDocCopilotSession,
|
||||
createPinnedCopilotSession,
|
||||
createWorkspaceCopilotSession,
|
||||
forkCopilotSession,
|
||||
getCopilotSession,
|
||||
getHistories,
|
||||
listContext,
|
||||
listContextDocAndFiles,
|
||||
@@ -302,12 +306,8 @@ test('should fork session correctly', async t => {
|
||||
|
||||
// prepare session
|
||||
const { id } = await createWorkspace(app);
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
id,
|
||||
randomUUID(),
|
||||
textPromptName
|
||||
);
|
||||
const docId = randomUUID();
|
||||
const sessionId = await createCopilotSession(app, id, docId, textPromptName);
|
||||
|
||||
let forkedSessionId: string;
|
||||
// should be able to fork session
|
||||
@@ -316,7 +316,7 @@ test('should fork session correctly', async t => {
|
||||
const messageId = await createCopilotMessage(app, sessionId);
|
||||
await chatWithText(app, sessionId, messageId);
|
||||
}
|
||||
const histories = await getHistories(app, { workspaceId: id });
|
||||
const histories = await getHistories(app, { workspaceId: id, docId });
|
||||
const latestMessageId = histories[0].messages.findLast(
|
||||
m => m.role === 'assistant'
|
||||
)?.id;
|
||||
@@ -375,7 +375,7 @@ test('should fork session correctly', async t => {
|
||||
});
|
||||
|
||||
await app.switchUser(u1);
|
||||
const histories = await getHistories(app, { workspaceId: id });
|
||||
const histories = await getHistories(app, { workspaceId: id, docId });
|
||||
const latestMessageId = histories
|
||||
.find(h => h.sessionId === forkedSessionId)
|
||||
?.messages.findLast(m => m.role === 'assistant')?.id;
|
||||
@@ -612,10 +612,11 @@ test('should be able to retry with api', async t => {
|
||||
// normal chat
|
||||
{
|
||||
const { id } = await createWorkspace(app);
|
||||
const docId = randomUUID();
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
id,
|
||||
randomUUID(),
|
||||
docId,
|
||||
textPromptName
|
||||
);
|
||||
const messageId = await createCopilotMessage(app, sessionId);
|
||||
@@ -623,7 +624,7 @@ test('should be able to retry with api', async t => {
|
||||
await chatWithText(app, sessionId, messageId);
|
||||
await chatWithText(app, sessionId, messageId);
|
||||
|
||||
const histories = await getHistories(app, { workspaceId: id });
|
||||
const histories = await getHistories(app, { workspaceId: id, docId });
|
||||
t.deepEqual(
|
||||
histories.map(h => h.messages.map(m => m.content)),
|
||||
[['generate text to text', 'generate text to text']],
|
||||
@@ -634,10 +635,11 @@ test('should be able to retry with api', async t => {
|
||||
// retry chat
|
||||
{
|
||||
const { id } = await createWorkspace(app);
|
||||
const docId = randomUUID();
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
id,
|
||||
randomUUID(),
|
||||
docId,
|
||||
textPromptName
|
||||
);
|
||||
const messageId = await createCopilotMessage(app, sessionId);
|
||||
@@ -646,7 +648,7 @@ test('should be able to retry with api', async t => {
|
||||
await chatWithText(app, sessionId);
|
||||
|
||||
// should only have 1 message
|
||||
const histories = await getHistories(app, { workspaceId: id });
|
||||
const histories = await getHistories(app, { workspaceId: id, docId });
|
||||
t.snapshot(
|
||||
cleanObject(histories),
|
||||
'should be able to list history after retry'
|
||||
@@ -656,10 +658,11 @@ test('should be able to retry with api', async t => {
|
||||
// retry chat with new message id
|
||||
{
|
||||
const { id } = await createWorkspace(app);
|
||||
const docId = randomUUID();
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
id,
|
||||
randomUUID(),
|
||||
docId,
|
||||
textPromptName
|
||||
);
|
||||
const messageId = await createCopilotMessage(app, sessionId);
|
||||
@@ -669,7 +672,7 @@ test('should be able to retry with api', async t => {
|
||||
await chatWithText(app, sessionId, newMessageId, '', true);
|
||||
|
||||
// should only have 1 message
|
||||
const histories = await getHistories(app, { workspaceId: id });
|
||||
const histories = await getHistories(app, { workspaceId: id, docId });
|
||||
t.snapshot(
|
||||
cleanObject(histories),
|
||||
'should be able to list history after retry'
|
||||
@@ -746,10 +749,11 @@ test('should be able to list history', async t => {
|
||||
const { app } = t.context;
|
||||
|
||||
const { id: workspaceId } = await createWorkspace(app);
|
||||
const docId = randomUUID();
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
workspaceId,
|
||||
randomUUID(),
|
||||
docId,
|
||||
textPromptName
|
||||
);
|
||||
|
||||
@@ -757,7 +761,7 @@ test('should be able to list history', async t => {
|
||||
await chatWithText(app, sessionId, messageId);
|
||||
|
||||
{
|
||||
const histories = await getHistories(app, { workspaceId });
|
||||
const histories = await getHistories(app, { workspaceId, docId });
|
||||
t.deepEqual(
|
||||
histories.map(h => h.messages.map(m => m.content)),
|
||||
[['hello', 'generate text to text']],
|
||||
@@ -768,6 +772,7 @@ test('should be able to list history', async t => {
|
||||
{
|
||||
const histories = await getHistories(app, {
|
||||
workspaceId,
|
||||
docId,
|
||||
options: { messageOrder: 'desc' },
|
||||
});
|
||||
t.deepEqual(
|
||||
@@ -809,17 +814,18 @@ test('should reject request that user have not permission', async t => {
|
||||
}
|
||||
|
||||
{
|
||||
const docId = randomUUID();
|
||||
const sessionId = await createCopilotSession(
|
||||
app,
|
||||
workspaceId,
|
||||
randomUUID(),
|
||||
docId,
|
||||
textPromptName
|
||||
);
|
||||
|
||||
const messageId = await createCopilotMessage(app, sessionId);
|
||||
await chatWithText(app, sessionId, messageId);
|
||||
|
||||
const histories = await getHistories(app, { workspaceId });
|
||||
const histories = await getHistories(app, { workspaceId, docId });
|
||||
t.deepEqual(
|
||||
histories.map(h => h.messages.map(m => m.content)),
|
||||
[['generate text to text']],
|
||||
@@ -1072,3 +1078,93 @@ test('should be able to transcript', async t => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('should create different session types and validate prompt constraints', async t => {
|
||||
const { app } = t.context;
|
||||
const { id: workspaceId } = await createWorkspace(app);
|
||||
|
||||
const validateSession = async (
|
||||
description: string,
|
||||
workspaceId: string,
|
||||
createPromise: Promise<string>
|
||||
) => {
|
||||
const sessionId = await createPromise;
|
||||
|
||||
t.truthy(sessionId, description);
|
||||
t.snapshot(
|
||||
cleanObject(
|
||||
[await getCopilotSession(app, workspaceId, sessionId)],
|
||||
['id', 'workspaceId', 'promptName']
|
||||
),
|
||||
`should create session with ${description}`
|
||||
);
|
||||
return sessionId;
|
||||
};
|
||||
|
||||
await validateSession(
|
||||
'should create workspace session with text prompt',
|
||||
workspaceId,
|
||||
createWorkspaceCopilotSession(app, workspaceId, textPromptName)
|
||||
);
|
||||
await validateSession(
|
||||
'should create pinned session with text prompt',
|
||||
workspaceId,
|
||||
createPinnedCopilotSession(app, workspaceId, 'pinned-doc', textPromptName)
|
||||
);
|
||||
await validateSession(
|
||||
'should create doc session with text prompt',
|
||||
workspaceId,
|
||||
createDocCopilotSession(app, workspaceId, 'normal-doc', textPromptName)
|
||||
);
|
||||
});
|
||||
|
||||
test('should list histories for different session types correctly', async t => {
|
||||
const { app } = t.context;
|
||||
const { id: workspaceId } = await createWorkspace(app);
|
||||
const pinnedDocId = 'pinned-doc';
|
||||
const docId = 'normal-doc';
|
||||
|
||||
// create sessions and add messages
|
||||
const [workspaceSessionId, pinnedSessionId, docSessionId] = await Promise.all(
|
||||
[
|
||||
createWorkspaceCopilotSession(app, workspaceId, textPromptName),
|
||||
createPinnedCopilotSession(app, workspaceId, pinnedDocId, textPromptName),
|
||||
createDocCopilotSession(app, workspaceId, docId, textPromptName),
|
||||
]
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
createCopilotMessage(app, workspaceSessionId, 'workspace message'),
|
||||
createCopilotMessage(app, pinnedSessionId, 'pinned message'),
|
||||
createCopilotMessage(app, docSessionId, 'doc message'),
|
||||
]);
|
||||
|
||||
const testHistoryQuery = async (
|
||||
queryDocId: string | undefined,
|
||||
expectedSessionId: string,
|
||||
description: string
|
||||
) => {
|
||||
const histories = await getHistories(app, {
|
||||
workspaceId,
|
||||
docId: queryDocId,
|
||||
});
|
||||
t.is(histories.length, 1, `should return ${description}`);
|
||||
t.is(
|
||||
histories[0].sessionId,
|
||||
expectedSessionId,
|
||||
`should return correct ${description}`
|
||||
);
|
||||
};
|
||||
|
||||
await testHistoryQuery(
|
||||
undefined,
|
||||
workspaceSessionId,
|
||||
'workspace session history'
|
||||
);
|
||||
await testHistoryQuery(
|
||||
pinnedDocId,
|
||||
pinnedSessionId,
|
||||
'pinned session history'
|
||||
);
|
||||
await testHistoryQuery(docId, docSessionId, 'doc session history');
|
||||
});
|
||||
|
||||
@@ -275,7 +275,7 @@ test('should be able to manage chat session', async t => {
|
||||
]);
|
||||
|
||||
const params = { word: 'world' };
|
||||
const commonParams = { docId: 'test', workspaceId: 'test' };
|
||||
const commonParams = { docId: 'test', workspaceId: 'test', pinned: false };
|
||||
|
||||
const sessionId = await session.create({
|
||||
userId,
|
||||
@@ -342,11 +342,12 @@ test('should be able to update chat session prompt', async t => {
|
||||
docId: 'test',
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
pinned: false,
|
||||
});
|
||||
t.truthy(sessionId, 'should create session');
|
||||
|
||||
// Update the session
|
||||
const updatedSessionId = await session.updateSessionPrompt({
|
||||
const updatedSessionId = await session.updateSession({
|
||||
sessionId,
|
||||
promptName: 'Search With AFFiNE AI',
|
||||
userId,
|
||||
@@ -371,7 +372,7 @@ test('should be able to fork chat session', async t => {
|
||||
]);
|
||||
|
||||
const params = { word: 'world' };
|
||||
const commonParams = { docId: 'test', workspaceId: 'test' };
|
||||
const commonParams = { docId: 'test', workspaceId: 'test', pinned: false };
|
||||
// create session
|
||||
const sessionId = await session.create({
|
||||
userId,
|
||||
@@ -494,6 +495,7 @@ test('should be able to process message id', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
@@ -537,6 +539,7 @@ test('should be able to generate with message id', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
@@ -559,6 +562,7 @@ test('should be able to generate with message id', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
@@ -586,6 +590,7 @@ test('should be able to generate with message id', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
@@ -614,6 +619,7 @@ test('should save message correctly', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
@@ -643,6 +649,7 @@ test('should revert message correctly', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const s = (await session.get(sessionId))!;
|
||||
|
||||
@@ -742,6 +749,7 @@ test('should handle params correctly in chat session', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
|
||||
const s = (await session.get(sessionId))!;
|
||||
@@ -1506,6 +1514,7 @@ test('should be able to manage context', async t => {
|
||||
workspaceId: 'test',
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
|
||||
// use mocked embedding client
|
||||
@@ -1729,6 +1738,7 @@ test('should be able to manage workspace embedding', async t => {
|
||||
workspaceId: ws.id,
|
||||
userId,
|
||||
promptName: 'prompt',
|
||||
pinned: false,
|
||||
});
|
||||
const contextSession = await context.create(sessionId);
|
||||
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
# Snapshot report for `src/__tests__/models/copilot-session.spec.ts`
|
||||
|
||||
The actual snapshot is saved in `copilot-session.spec.ts.snap`.
|
||||
|
||||
Generated by [AVA](https://avajs.dev).
|
||||
|
||||
## should list and filter session type
|
||||
|
||||
> workspace sessions should include workspace and pinned sessions
|
||||
|
||||
[
|
||||
{
|
||||
docId: null,
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
docId: null,
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
|
||||
> doc sessions should only include sessions with matching docId
|
||||
|
||||
[
|
||||
{
|
||||
docId: 'doc-id-1',
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
|
||||
> session type identification results
|
||||
|
||||
[
|
||||
{
|
||||
session: {
|
||||
docId: null,
|
||||
pinned: false,
|
||||
},
|
||||
type: 'workspace',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: undefined,
|
||||
pinned: false,
|
||||
},
|
||||
type: 'workspace',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: null,
|
||||
pinned: true,
|
||||
},
|
||||
type: 'pinned',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: 'doc-id-1',
|
||||
pinned: false,
|
||||
},
|
||||
type: 'doc',
|
||||
},
|
||||
]
|
||||
|
||||
## should pin and unpin sessions
|
||||
|
||||
> session states after creating second pinned session
|
||||
|
||||
[
|
||||
{
|
||||
docId: null,
|
||||
id: 'first-session-id',
|
||||
pinned: false,
|
||||
},
|
||||
{
|
||||
docId: null,
|
||||
id: 'second-session-id',
|
||||
pinned: true,
|
||||
},
|
||||
]
|
||||
|
||||
> should return false when no sessions to unpin
|
||||
|
||||
false
|
||||
|
||||
> all sessions should be unpinned after unpin operation
|
||||
|
||||
[
|
||||
{
|
||||
id: 'first-session-id',
|
||||
pinned: false,
|
||||
},
|
||||
{
|
||||
id: 'second-session-id',
|
||||
pinned: false,
|
||||
},
|
||||
{
|
||||
id: 'third-session-id',
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
|
||||
## session updates and type conversions
|
||||
|
||||
> session states after pinning - should unpin existing
|
||||
|
||||
[
|
||||
{
|
||||
docId: null,
|
||||
id: 'session-update-id',
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
docId: null,
|
||||
id: 'existing-pinned-session-id',
|
||||
pinned: false,
|
||||
},
|
||||
]
|
||||
|
||||
> session state after unpinning
|
||||
|
||||
{
|
||||
docId: null,
|
||||
id: 'session-update-id',
|
||||
pinned: false,
|
||||
}
|
||||
|
||||
> session type conversion steps
|
||||
|
||||
[
|
||||
{
|
||||
session: {
|
||||
docId: 'doc-update-id',
|
||||
pinned: false,
|
||||
},
|
||||
step: 'workspace_to_doc',
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: 'doc-update-id',
|
||||
pinned: true,
|
||||
},
|
||||
step: 'doc_to_pinned',
|
||||
type: 'pinned',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: null,
|
||||
pinned: false,
|
||||
},
|
||||
step: 'pinned_to_workspace',
|
||||
type: 'workspace',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: null,
|
||||
pinned: true,
|
||||
},
|
||||
step: 'workspace_to_pinned',
|
||||
type: 'pinned',
|
||||
},
|
||||
]
|
||||
BIN
Binary file not shown.
@@ -5,12 +5,14 @@ import ava, { TestFn } from 'ava';
|
||||
import Sinon from 'sinon';
|
||||
|
||||
import { Config } from '../../base';
|
||||
import { ContextEmbedStatus } from '../../models/common/copilot';
|
||||
import { CopilotContextModel } from '../../models/copilot-context';
|
||||
import { CopilotSessionModel } from '../../models/copilot-session';
|
||||
import { CopilotWorkspaceConfigModel } from '../../models/copilot-workspace';
|
||||
import { UserModel } from '../../models/user';
|
||||
import { WorkspaceModel } from '../../models/workspace';
|
||||
import {
|
||||
ContextEmbedStatus,
|
||||
CopilotContextModel,
|
||||
CopilotSessionModel,
|
||||
CopilotWorkspaceConfigModel,
|
||||
UserModel,
|
||||
WorkspaceModel,
|
||||
} from '../../models';
|
||||
import { createTestingModule, type TestingModule } from '../utils';
|
||||
import { cleanObject } from '../utils/copilot';
|
||||
|
||||
@@ -46,7 +48,7 @@ let docId = 'doc1';
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await t.context.module.initTestingDB();
|
||||
await t.context.copilotSession.createPrompt('prompt-name', 'gpt-4o');
|
||||
await t.context.copilotSession.createPrompt('prompt-name', 'gpt-4.1');
|
||||
user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
import { PrismaClient, User, Workspace } from '@prisma/client';
|
||||
import ava, { ExecutionContext, TestFn } from 'ava';
|
||||
|
||||
import { CopilotPromptInvalid } from '../../base';
|
||||
import {
|
||||
CopilotSessionModel,
|
||||
UpdateChatSessionData,
|
||||
UserModel,
|
||||
WorkspaceModel,
|
||||
} from '../../models';
|
||||
import { createTestingModule, type TestingModule } from '../utils';
|
||||
|
||||
interface Context {
|
||||
module: TestingModule;
|
||||
db: PrismaClient;
|
||||
user: UserModel;
|
||||
workspace: WorkspaceModel;
|
||||
copilotSession: CopilotSessionModel;
|
||||
}
|
||||
|
||||
const test = ava as TestFn<Context>;
|
||||
|
||||
test.before(async t => {
|
||||
const module = await createTestingModule();
|
||||
t.context.user = module.get(UserModel);
|
||||
t.context.workspace = module.get(WorkspaceModel);
|
||||
t.context.copilotSession = module.get(CopilotSessionModel);
|
||||
t.context.db = module.get(PrismaClient);
|
||||
t.context.module = module;
|
||||
});
|
||||
|
||||
let user: User;
|
||||
let workspace: Workspace;
|
||||
|
||||
test.beforeEach(async t => {
|
||||
await t.context.module.initTestingDB();
|
||||
user = await t.context.user.create({
|
||||
email: 'test@affine.pro',
|
||||
});
|
||||
workspace = await t.context.workspace.create(user.id);
|
||||
});
|
||||
|
||||
test.after(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
const createTestPrompts = async (
|
||||
copilotSession: CopilotSessionModel,
|
||||
db: PrismaClient
|
||||
) => {
|
||||
await copilotSession.createPrompt('test-prompt', 'gpt-4.1');
|
||||
await db.aiPrompt.create({
|
||||
data: { name: 'action-prompt', model: 'gpt-4.1', action: 'edit' },
|
||||
});
|
||||
};
|
||||
|
||||
const createTestSession = async (
|
||||
t: ExecutionContext<Context>,
|
||||
overrides: Partial<{
|
||||
sessionId: string;
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
docId: string | null;
|
||||
pinned: boolean;
|
||||
promptName: string;
|
||||
}> = {}
|
||||
) => {
|
||||
const sessionData = {
|
||||
sessionId: randomUUID(),
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
pinned: false,
|
||||
promptName: 'test-prompt',
|
||||
...overrides,
|
||||
};
|
||||
|
||||
await t.context.copilotSession.create(sessionData);
|
||||
return sessionData;
|
||||
};
|
||||
|
||||
const getSessionState = async (db: PrismaClient, sessionId: string) => {
|
||||
const session = await db.aiSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { id: true, pinned: true, docId: true },
|
||||
});
|
||||
return session;
|
||||
};
|
||||
|
||||
test('should list and filter session type', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
const docId = 'doc-id-1';
|
||||
await createTestSession(t, { sessionId: randomUUID() });
|
||||
await createTestSession(t, { sessionId: randomUUID(), pinned: true });
|
||||
await createTestSession(t, { sessionId: randomUUID(), docId });
|
||||
|
||||
// should list sessions
|
||||
{
|
||||
const workspaceSessions = await copilotSession.list(user.id, workspace.id);
|
||||
|
||||
t.snapshot(
|
||||
workspaceSessions.map(s => ({ docId: s.docId, pinned: s.pinned })),
|
||||
'workspace sessions should include workspace and pinned sessions'
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const docSessions = await copilotSession.list(user.id, workspace.id, docId);
|
||||
|
||||
t.snapshot(
|
||||
docSessions.map(s => ({ docId: s.docId, pinned: s.pinned })),
|
||||
'doc sessions should only include sessions with matching docId'
|
||||
);
|
||||
}
|
||||
|
||||
// should identify session types
|
||||
{
|
||||
// check get session type
|
||||
const testCases = [
|
||||
{ docId: null, pinned: false },
|
||||
{ docId: undefined, pinned: false },
|
||||
{ docId: null, pinned: true },
|
||||
{ docId, pinned: false },
|
||||
];
|
||||
|
||||
const sessionTypeResults = testCases.map(session => ({
|
||||
session,
|
||||
type: copilotSession.getSessionType(session),
|
||||
}));
|
||||
|
||||
t.snapshot(sessionTypeResults, 'session type identification results');
|
||||
}
|
||||
});
|
||||
|
||||
test('should check session validation for prompts', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
const docId = randomUUID();
|
||||
const sessionTypes = [
|
||||
{ name: 'workspace', session: { docId: null, pinned: false } },
|
||||
{ name: 'pinned', session: { docId: null, pinned: true } },
|
||||
{ name: 'doc', session: { docId, pinned: false } },
|
||||
];
|
||||
|
||||
// non-action prompts should work for all session types
|
||||
sessionTypes.forEach(({ name, session }) => {
|
||||
t.notThrows(
|
||||
() =>
|
||||
copilotSession.checkSessionPrompt(session, 'test-prompt', undefined),
|
||||
`${name} session should allow non-action prompts`
|
||||
);
|
||||
});
|
||||
|
||||
// action prompts should only work for doc session type
|
||||
{
|
||||
const actionPromptTests = [
|
||||
{
|
||||
name: 'workspace',
|
||||
session: sessionTypes[0].session,
|
||||
shouldThrow: true,
|
||||
},
|
||||
{ name: 'pinned', session: sessionTypes[1].session, shouldThrow: true },
|
||||
{ name: 'doc', session: sessionTypes[2].session, shouldThrow: false },
|
||||
];
|
||||
|
||||
actionPromptTests.forEach(({ name, session, shouldThrow }) => {
|
||||
if (shouldThrow) {
|
||||
t.throws(
|
||||
() =>
|
||||
copilotSession.checkSessionPrompt(session, 'action-prompt', 'edit'),
|
||||
{ instanceOf: CopilotPromptInvalid },
|
||||
`${name} session should reject action prompts`
|
||||
);
|
||||
} else {
|
||||
t.notThrows(
|
||||
() =>
|
||||
copilotSession.checkSessionPrompt(session, 'action-prompt', 'edit'),
|
||||
`${name} session should allow action prompts`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('should pin and unpin sessions', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
const firstSessionId = 'first-session-id';
|
||||
const secondSessionId = 'second-session-id';
|
||||
const thirdSessionId = 'third-session-id';
|
||||
|
||||
// should unpin existing pinned session when creating a new one
|
||||
{
|
||||
await copilotSession.create({
|
||||
sessionId: firstSessionId,
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
promptName: 'test-prompt',
|
||||
pinned: true,
|
||||
});
|
||||
|
||||
const firstSession = await copilotSession.get(firstSessionId);
|
||||
t.truthy(firstSession, 'first session should be created successfully');
|
||||
t.is(firstSession?.pinned, true, 'first session should be pinned');
|
||||
|
||||
// should unpin the first one when creating second pinned session
|
||||
await copilotSession.create({
|
||||
sessionId: secondSessionId,
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
promptName: 'test-prompt',
|
||||
pinned: true,
|
||||
});
|
||||
|
||||
const sessionStatesAfterSecondPin = await Promise.all([
|
||||
getSessionState(db, firstSessionId),
|
||||
getSessionState(db, secondSessionId),
|
||||
]);
|
||||
|
||||
t.snapshot(
|
||||
sessionStatesAfterSecondPin,
|
||||
'session states after creating second pinned session'
|
||||
);
|
||||
}
|
||||
|
||||
// should can unpin a pinned session
|
||||
{
|
||||
await createTestSession(t, { sessionId: thirdSessionId, pinned: true });
|
||||
const unpinResult = await copilotSession.unpin(workspace.id, user.id);
|
||||
t.is(
|
||||
unpinResult,
|
||||
true,
|
||||
'unpin operation should return true when sessions are unpinned'
|
||||
);
|
||||
|
||||
const unpinResultAgain = await copilotSession.unpin(workspace.id, user.id);
|
||||
t.snapshot(
|
||||
unpinResultAgain,
|
||||
'should return false when no sessions to unpin'
|
||||
);
|
||||
}
|
||||
|
||||
// should unpin all sessions
|
||||
{
|
||||
const allSessionsAfterUnpin = await db.aiSession.findMany({
|
||||
where: { id: { in: [firstSessionId, secondSessionId, thirdSessionId] } },
|
||||
select: { pinned: true, id: true },
|
||||
orderBy: { id: 'asc' },
|
||||
});
|
||||
|
||||
t.snapshot(
|
||||
allSessionsAfterUnpin,
|
||||
'all sessions should be unpinned after unpin operation'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('session updates and type conversions', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
const sessionId = 'session-update-id';
|
||||
const docId = 'doc-update-id';
|
||||
|
||||
await createTestSession(t, { sessionId });
|
||||
|
||||
// should unpin existing pinned session
|
||||
{
|
||||
const existingPinnedId = 'existing-pinned-session-id';
|
||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||
|
||||
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 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
|
||||
{
|
||||
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({
|
||||
where: { id: sessionId },
|
||||
select: { docId: true, pinned: true },
|
||||
});
|
||||
conversionSteps.push({
|
||||
step,
|
||||
session,
|
||||
type: copilotSession.getSessionType(session!),
|
||||
});
|
||||
};
|
||||
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
t.snapshot(conversionSteps, 'session type conversion steps');
|
||||
}
|
||||
});
|
||||
@@ -20,8 +20,9 @@ export const cleanObject = (
|
||||
export async function createCopilotSession(
|
||||
app: TestingApp,
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
promptName: string
|
||||
docId: string | null,
|
||||
promptName: string,
|
||||
pinned: boolean = false
|
||||
): Promise<string> {
|
||||
const res = await app.gql(
|
||||
`
|
||||
@@ -29,12 +30,73 @@ export async function createCopilotSession(
|
||||
createCopilotSession(options: $options)
|
||||
}
|
||||
`,
|
||||
{ options: { workspaceId, docId, promptName } }
|
||||
{ options: { workspaceId, docId, promptName, pinned } }
|
||||
);
|
||||
|
||||
return res.createCopilotSession;
|
||||
}
|
||||
|
||||
export async function createWorkspaceCopilotSession(
|
||||
app: TestingApp,
|
||||
workspaceId: string,
|
||||
promptName: string
|
||||
): Promise<string> {
|
||||
return createCopilotSession(app, workspaceId, null, promptName);
|
||||
}
|
||||
|
||||
export async function createPinnedCopilotSession(
|
||||
app: TestingApp,
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
promptName: string
|
||||
): Promise<string> {
|
||||
return createCopilotSession(app, workspaceId, docId, promptName, true);
|
||||
}
|
||||
|
||||
export async function createDocCopilotSession(
|
||||
app: TestingApp,
|
||||
workspaceId: string,
|
||||
docId: string,
|
||||
promptName: string
|
||||
): Promise<string> {
|
||||
return createCopilotSession(app, workspaceId, docId, promptName);
|
||||
}
|
||||
|
||||
export async function getCopilotSession(
|
||||
app: TestingApp,
|
||||
workspaceId: string,
|
||||
sessionId: string
|
||||
): Promise<{
|
||||
id: string;
|
||||
docId: string | null;
|
||||
parentSessionId: string | null;
|
||||
pinned: boolean;
|
||||
promptName: string;
|
||||
}> {
|
||||
const res = await app.gql(
|
||||
`
|
||||
query getCopilotSession(
|
||||
$workspaceId: String!
|
||||
$sessionId: String!
|
||||
) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
session(sessionId: $sessionId) {
|
||||
id
|
||||
docId
|
||||
parentSessionId
|
||||
pinned
|
||||
promptName
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{ workspaceId, sessionId }
|
||||
);
|
||||
|
||||
return res.currentUser?.copilot?.session;
|
||||
}
|
||||
|
||||
export async function updateCopilotSession(
|
||||
app: TestingApp,
|
||||
sessionId: string,
|
||||
|
||||
Reference in New Issue
Block a user