parent
e32c9a814a
commit
06f27e8d6a
@ -0,0 +1,5 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "updated_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX IF EXISTS "ai_session_unique_doc_session_idx";
|
@ -443,6 +443,7 @@ model AiSession {
|
||||
messageCost Int @default(0)
|
||||
tokenCost Int @default(0)
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(3)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(3)
|
||||
deletedAt DateTime? @map("deleted_at") @db.Timestamptz(3)
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
@ -67,6 +67,49 @@ Generated by [AVA](https://avajs.dev).
|
||||
},
|
||||
]
|
||||
|
||||
## should validate session prompt compatibility
|
||||
|
||||
> session prompt validation results
|
||||
|
||||
[
|
||||
{
|
||||
promptType: 'non-action',
|
||||
result: 'success',
|
||||
sessionType: 'workspace',
|
||||
shouldThrow: false,
|
||||
},
|
||||
{
|
||||
promptType: 'action',
|
||||
result: 'CopilotPromptInvalid',
|
||||
sessionType: 'workspace',
|
||||
shouldThrow: true,
|
||||
},
|
||||
{
|
||||
promptType: 'non-action',
|
||||
result: 'success',
|
||||
sessionType: 'pinned',
|
||||
shouldThrow: false,
|
||||
},
|
||||
{
|
||||
promptType: 'action',
|
||||
result: 'CopilotPromptInvalid',
|
||||
sessionType: 'pinned',
|
||||
shouldThrow: true,
|
||||
},
|
||||
{
|
||||
promptType: 'non-action',
|
||||
result: 'success',
|
||||
sessionType: 'doc',
|
||||
shouldThrow: false,
|
||||
},
|
||||
{
|
||||
promptType: 'action',
|
||||
result: 'success',
|
||||
sessionType: 'doc',
|
||||
shouldThrow: false,
|
||||
},
|
||||
]
|
||||
|
||||
## should pin and unpin sessions
|
||||
|
||||
> session states after creating second pinned session
|
||||
@ -105,6 +148,345 @@ Generated by [AVA](https://avajs.dev).
|
||||
},
|
||||
]
|
||||
|
||||
## should handle session updates and type conversions
|
||||
|
||||
> session update validation results
|
||||
|
||||
[
|
||||
{
|
||||
result: 'rejected',
|
||||
sessionType: 'action',
|
||||
update: {
|
||||
docId: 'new-doc',
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'rejected',
|
||||
sessionType: 'action',
|
||||
update: {
|
||||
pinned: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'rejected',
|
||||
sessionType: 'action',
|
||||
update: {
|
||||
promptName: 'test-prompt',
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'success',
|
||||
sessionType: 'forked',
|
||||
update: {
|
||||
pinned: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'success',
|
||||
sessionType: 'forked',
|
||||
update: {
|
||||
promptName: 'test-prompt',
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'rejected',
|
||||
sessionType: 'forked',
|
||||
update: {
|
||||
docId: 'new-doc',
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'success',
|
||||
sessionType: 'regular',
|
||||
update: {
|
||||
promptName: 'test-prompt',
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'rejected',
|
||||
sessionType: 'regular',
|
||||
update: {
|
||||
promptName: 'action-prompt',
|
||||
},
|
||||
},
|
||||
{
|
||||
result: 'rejected',
|
||||
sessionType: 'regular',
|
||||
update: {
|
||||
promptName: 'non-existent-prompt',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
> pinning behavior - should unpin existing when pinning new
|
||||
|
||||
{
|
||||
onlyOneSessionPinned: true,
|
||||
pinnedSessions: 1,
|
||||
totalSessions: 2,
|
||||
unpinnedSessions: 1,
|
||||
}
|
||||
|
||||
> session type conversion steps
|
||||
|
||||
[
|
||||
{
|
||||
sessionState: {
|
||||
hasDocId: true,
|
||||
pinned: false,
|
||||
},
|
||||
step: 'workspace_to_doc',
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
sessionState: {
|
||||
hasDocId: false,
|
||||
pinned: false,
|
||||
},
|
||||
step: 'doc_to_workspace',
|
||||
type: 'workspace',
|
||||
},
|
||||
{
|
||||
sessionState: {
|
||||
hasDocId: false,
|
||||
pinned: true,
|
||||
},
|
||||
step: 'workspace_to_pinned',
|
||||
type: 'pinned',
|
||||
},
|
||||
]
|
||||
|
||||
## should handle session queries, ordering, and filtering
|
||||
|
||||
> comprehensive session query results
|
||||
|
||||
{
|
||||
all_workspace_sessions: {
|
||||
count: 2,
|
||||
sessionTypes: [
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'pinned',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'workspace',
|
||||
},
|
||||
],
|
||||
},
|
||||
doc_sessions_with_messages: {
|
||||
count: 5,
|
||||
sessionTypes: [
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: true,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: true,
|
||||
isFork: false,
|
||||
messageCount: 1,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: true,
|
||||
isFork: false,
|
||||
messageCount: 1,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: true,
|
||||
isFork: false,
|
||||
messageCount: 1,
|
||||
type: 'doc',
|
||||
},
|
||||
],
|
||||
},
|
||||
latest_valid_session: {
|
||||
count: 1,
|
||||
sessionTypes: [
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
],
|
||||
},
|
||||
non_action_sessions: {
|
||||
count: 4,
|
||||
sessionTypes: [
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: true,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
],
|
||||
},
|
||||
non_fork_sessions: {
|
||||
count: 4,
|
||||
sessionTypes: [
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'doc',
|
||||
},
|
||||
],
|
||||
},
|
||||
recent_top3_sessions: {
|
||||
count: 2,
|
||||
sessionTypes: [
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'pinned',
|
||||
},
|
||||
{
|
||||
hasMessages: false,
|
||||
isFork: false,
|
||||
messageCount: 0,
|
||||
type: 'workspace',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
> 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: 'test-doc-id',
|
||||
pinned: false,
|
||||
},
|
||||
type: 'doc',
|
||||
},
|
||||
]
|
||||
|
||||
## should handle fork and session attachment operations
|
||||
|
||||
> fork operation results
|
||||
|
||||
{
|
||||
existingPinnedSessionUnpinned: true,
|
||||
forkResults: [
|
||||
{
|
||||
actualState: {
|
||||
hasDocId: false,
|
||||
hasParent: true,
|
||||
isDocIdCorrect: true,
|
||||
pinned: false,
|
||||
},
|
||||
description: 'workspace fork',
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
actualState: {
|
||||
hasDocId: true,
|
||||
hasParent: true,
|
||||
isDocIdCorrect: true,
|
||||
pinned: false,
|
||||
},
|
||||
description: 'doc fork',
|
||||
success: true,
|
||||
},
|
||||
{
|
||||
actualState: {
|
||||
hasDocId: false,
|
||||
hasParent: true,
|
||||
isDocIdCorrect: true,
|
||||
pinned: true,
|
||||
},
|
||||
description: 'pinned fork',
|
||||
success: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
> attach and detach operation results
|
||||
|
||||
{
|
||||
attachPhase: {
|
||||
bothSessionsPresent: true,
|
||||
docSessionCount: 2,
|
||||
},
|
||||
detachPhase: {
|
||||
originalDocSessionRemains: true,
|
||||
workspaceSessionExists: true,
|
||||
},
|
||||
}
|
||||
|
||||
## should handle session updates and validations
|
||||
|
||||
> should unpin existing when pinning new session
|
||||
@ -130,7 +512,7 @@ Generated by [AVA](https://avajs.dev).
|
||||
docId: 'doc-update-id',
|
||||
pinned: false,
|
||||
},
|
||||
step: 'pinned_to_doc',
|
||||
step: 'workspace_to_doc',
|
||||
type: 'doc',
|
||||
},
|
||||
{
|
||||
@ -151,64 +533,55 @@ Generated by [AVA](https://avajs.dev).
|
||||
},
|
||||
]
|
||||
|
||||
## session updates and type conversions
|
||||
## should create multiple doc sessions and query latest
|
||||
|
||||
> session states after pinning - should unpin existing
|
||||
> multiple doc sessions for same document with order verification
|
||||
|
||||
[
|
||||
{
|
||||
docId: null,
|
||||
id: 'session-update-id',
|
||||
pinned: true,
|
||||
docId: 'multi-session-doc',
|
||||
hasMessages: true,
|
||||
isFirstSession: false,
|
||||
isSecondSession: false,
|
||||
isThirdSession: true,
|
||||
messageCount: 1,
|
||||
},
|
||||
{
|
||||
docId: null,
|
||||
id: 'existing-pinned-session-id',
|
||||
pinned: false,
|
||||
docId: 'multi-session-doc',
|
||||
hasMessages: true,
|
||||
isFirstSession: false,
|
||||
isSecondSession: true,
|
||||
isThirdSession: false,
|
||||
messageCount: 1,
|
||||
},
|
||||
{
|
||||
docId: 'multi-session-doc',
|
||||
hasMessages: true,
|
||||
isFirstSession: true,
|
||||
isSecondSession: false,
|
||||
isThirdSession: false,
|
||||
messageCount: 1,
|
||||
},
|
||||
]
|
||||
|
||||
> session state after unpinning
|
||||
## should query recent topK sessions of different types
|
||||
|
||||
{
|
||||
docId: null,
|
||||
id: 'session-update-id',
|
||||
pinned: false,
|
||||
}
|
||||
|
||||
> session type conversion steps
|
||||
> should include different session types in recent topK query
|
||||
|
||||
[
|
||||
{
|
||||
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',
|
||||
docId: null,
|
||||
pinned: false,
|
||||
type: 'workspace',
|
||||
},
|
||||
{
|
||||
session: {
|
||||
docId: null,
|
||||
pinned: true,
|
||||
},
|
||||
step: 'workspace_to_pinned',
|
||||
docId: null,
|
||||
pinned: true,
|
||||
type: 'pinned',
|
||||
},
|
||||
{
|
||||
docId: null,
|
||||
pinned: false,
|
||||
type: 'workspace',
|
||||
},
|
||||
]
|
||||
|
Binary file not shown.
@ -47,13 +47,20 @@ test.after(async t => {
|
||||
await t.context.module.close();
|
||||
});
|
||||
|
||||
// Test data constants
|
||||
const TEST_PROMPTS = {
|
||||
NORMAL: 'test-prompt',
|
||||
ACTION: 'action-prompt',
|
||||
} as const;
|
||||
|
||||
// Helper functions
|
||||
const createTestPrompts = async (
|
||||
copilotSession: CopilotSessionModel,
|
||||
db: PrismaClient
|
||||
) => {
|
||||
await copilotSession.createPrompt('test-prompt', 'gpt-4.1');
|
||||
await copilotSession.createPrompt(TEST_PROMPTS.NORMAL, 'gpt-4.1');
|
||||
await db.aiPrompt.create({
|
||||
data: { name: 'action-prompt', model: 'gpt-4.1', action: 'edit' },
|
||||
data: { name: TEST_PROMPTS.ACTION, model: 'gpt-4.1', action: 'edit' },
|
||||
});
|
||||
};
|
||||
|
||||
@ -75,7 +82,7 @@ const createTestSession = async (
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
pinned: false,
|
||||
promptName: 'test-prompt',
|
||||
promptName: TEST_PROMPTS.NORMAL,
|
||||
promptAction: null,
|
||||
...overrides,
|
||||
};
|
||||
@ -84,14 +91,62 @@ const createTestSession = async (
|
||||
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;
|
||||
const getSessionStates = async (db: PrismaClient, sessionIds: string[]) => {
|
||||
const sessions = await Promise.all(
|
||||
sessionIds.map(id =>
|
||||
db.aiSession.findUnique({
|
||||
where: { id },
|
||||
select: { id: true, pinned: true, docId: true },
|
||||
})
|
||||
)
|
||||
);
|
||||
return sessions;
|
||||
};
|
||||
|
||||
const addMessagesToSession = async (
|
||||
copilotSession: CopilotSessionModel,
|
||||
sessionId: string,
|
||||
content: string,
|
||||
delayMs: number = 0
|
||||
) => {
|
||||
if (delayMs > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, delayMs));
|
||||
}
|
||||
await copilotSession.updateMessages({
|
||||
sessionId,
|
||||
userId: user.id,
|
||||
prompt: { model: 'gpt-4.1' },
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const createSessionWithMessages = async (
|
||||
t: ExecutionContext<Context>,
|
||||
overrides: Parameters<typeof createTestSession>[1] = {},
|
||||
messageContent?: string,
|
||||
delayMs: number = 0
|
||||
) => {
|
||||
const sessionData = await createTestSession(t, overrides);
|
||||
if (messageContent) {
|
||||
await addMessagesToSession(
|
||||
t.context.copilotSession,
|
||||
sessionData.sessionId,
|
||||
messageContent,
|
||||
delayMs
|
||||
);
|
||||
}
|
||||
return sessionData;
|
||||
};
|
||||
|
||||
// Simplified update assertion helpers
|
||||
type UpdateData = Omit<UpdateChatSessionOptions, 'userId' | 'sessionId'>;
|
||||
|
||||
test('should list and filter session type', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
|
||||
@ -128,12 +183,23 @@ test('should list and filter session type', async t => {
|
||||
docId,
|
||||
});
|
||||
|
||||
t.is(
|
||||
docSessions.length,
|
||||
2,
|
||||
'should return exactly 2 doc sessions for the specified docId'
|
||||
);
|
||||
|
||||
t.true(
|
||||
docSessions.every(s => s.docId === docId),
|
||||
'all returned sessions should have the specified docId'
|
||||
);
|
||||
|
||||
t.snapshot(
|
||||
cleanObject(
|
||||
docSessions.toSorted(s =>
|
||||
s.docId!.localeCompare(s.docId!, undefined, { numeric: true })
|
||||
docSessions.toSorted((a, b) =>
|
||||
a.promptName.localeCompare(b.promptName)
|
||||
),
|
||||
['id', 'userId', 'workspaceId', 'createdAt', 'tokenCost']
|
||||
['id', 'userId', 'workspaceId', 'createdAt', 'updatedAt', 'tokenCost']
|
||||
),
|
||||
'doc sessions should only include sessions with matching docId'
|
||||
);
|
||||
@ -158,65 +224,58 @@ test('should list and filter session type', async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should check session validation for prompts', async t => {
|
||||
test('should validate session prompt compatibility', 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 } },
|
||||
{ name: 'doc', session: { docId: randomUUID(), pinned: false } },
|
||||
];
|
||||
|
||||
// non-action prompts should work for all session types
|
||||
sessionTypes.forEach(({ name, session }) => {
|
||||
t.notThrows(
|
||||
() =>
|
||||
copilotSession.checkSessionPrompt(session, {
|
||||
name: 'test-prompt',
|
||||
action: undefined,
|
||||
}),
|
||||
`${name} session should allow non-action prompts`
|
||||
);
|
||||
});
|
||||
const result = sessionTypes.flatMap(({ name, session }) => [
|
||||
// non-action prompts should work for all session types
|
||||
{
|
||||
sessionType: name,
|
||||
promptType: 'non-action',
|
||||
shouldThrow: false,
|
||||
result: (() => {
|
||||
try {
|
||||
copilotSession.checkSessionPrompt(session, {
|
||||
name: TEST_PROMPTS.NORMAL,
|
||||
action: undefined,
|
||||
});
|
||||
return 'success';
|
||||
} catch (error) {
|
||||
return error instanceof CopilotPromptInvalid
|
||||
? 'CopilotPromptInvalid'
|
||||
: 'unknown';
|
||||
}
|
||||
})(),
|
||||
},
|
||||
// action prompts should only work for doc session type
|
||||
{
|
||||
sessionType: name,
|
||||
promptType: 'action',
|
||||
shouldThrow: name !== 'doc',
|
||||
result: (() => {
|
||||
try {
|
||||
copilotSession.checkSessionPrompt(session, {
|
||||
name: TEST_PROMPTS.ACTION,
|
||||
action: 'edit',
|
||||
});
|
||||
return 'success';
|
||||
} catch (error) {
|
||||
return error instanceof CopilotPromptInvalid
|
||||
? 'CopilotPromptInvalid'
|
||||
: 'unknown';
|
||||
}
|
||||
})(),
|
||||
},
|
||||
]);
|
||||
|
||||
// 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, {
|
||||
name: 'action-prompt',
|
||||
action: 'edit',
|
||||
}),
|
||||
{ instanceOf: CopilotPromptInvalid },
|
||||
`${name} session should reject action prompts`
|
||||
);
|
||||
} else {
|
||||
t.notThrows(
|
||||
() =>
|
||||
copilotSession.checkSessionPrompt(session, {
|
||||
name: 'action-prompt',
|
||||
action: 'edit',
|
||||
}),
|
||||
`${name} session should allow action prompts`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
t.snapshot(result, 'session prompt validation results');
|
||||
});
|
||||
|
||||
test('should pin and unpin sessions', async t => {
|
||||
@ -255,9 +314,9 @@ test('should pin and unpin sessions', async t => {
|
||||
pinned: true,
|
||||
});
|
||||
|
||||
const sessionStatesAfterSecondPin = await Promise.all([
|
||||
getSessionState(db, firstSessionId),
|
||||
getSessionState(db, secondSessionId),
|
||||
const sessionStatesAfterSecondPin = await getSessionStates(db, [
|
||||
firstSessionId,
|
||||
secondSessionId,
|
||||
]);
|
||||
|
||||
t.snapshot(
|
||||
@ -298,177 +357,553 @@ test('should pin and unpin sessions', async t => {
|
||||
}
|
||||
});
|
||||
|
||||
test('should handle session updates and validations', async t => {
|
||||
test('should handle session updates and type conversions', 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';
|
||||
const sessionId = randomUUID();
|
||||
const actionSessionId = randomUUID();
|
||||
const forkedSessionId = randomUUID();
|
||||
const parentSessionId = randomUUID();
|
||||
const docId = randomUUID();
|
||||
|
||||
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 createTestSession(t, { sessionId });
|
||||
await createTestSession(t, {
|
||||
sessionId: actionSessionId,
|
||||
promptName: TEST_PROMPTS.ACTION,
|
||||
promptAction: 'edit',
|
||||
docId,
|
||||
});
|
||||
await createTestSession(t, { sessionId: parentSessionId, docId });
|
||||
await db.aiSession.create({
|
||||
data: {
|
||||
id: forkedSessionId,
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
docId,
|
||||
pinned: false,
|
||||
promptName: TEST_PROMPTS.NORMAL,
|
||||
promptAction: null,
|
||||
parentSessionId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const updateTestCases = [
|
||||
// action sessions should reject all updates
|
||||
{
|
||||
sessionId: actionSessionId,
|
||||
updates: [
|
||||
{ docId: 'new-doc', expected: 'reject' },
|
||||
{ pinned: true, expected: 'reject' },
|
||||
{ promptName: TEST_PROMPTS.NORMAL, expected: 'reject' },
|
||||
],
|
||||
},
|
||||
// forked sessions should reject docId updates but allow others
|
||||
{
|
||||
sessionId: forkedSessionId,
|
||||
updates: [
|
||||
{ pinned: true, expected: 'allow' },
|
||||
{ promptName: TEST_PROMPTS.NORMAL, expected: 'allow' },
|
||||
{ docId: 'new-doc', expected: 'reject' },
|
||||
],
|
||||
},
|
||||
// Regular sessions - prompt validation
|
||||
{
|
||||
sessionId,
|
||||
updates: [
|
||||
{ promptName: TEST_PROMPTS.NORMAL, expected: 'allow' },
|
||||
{ promptName: TEST_PROMPTS.ACTION, expected: 'reject' },
|
||||
{ promptName: 'non-existent-prompt', expected: 'reject' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const updateResults = [];
|
||||
for (const { sessionId: testSessionId, updates } of updateTestCases) {
|
||||
for (const update of updates) {
|
||||
const { expected: _, ...updateData } = update;
|
||||
try {
|
||||
await t.context.copilotSession.update({
|
||||
...updateData,
|
||||
userId: user.id,
|
||||
sessionId: testSessionId,
|
||||
});
|
||||
updateResults.push({
|
||||
sessionType:
|
||||
testSessionId === actionSessionId
|
||||
? 'action'
|
||||
: testSessionId === forkedSessionId
|
||||
? 'forked'
|
||||
: 'regular',
|
||||
update: updateData,
|
||||
result: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
updateResults.push({
|
||||
sessionType:
|
||||
testSessionId === actionSessionId
|
||||
? 'action'
|
||||
: testSessionId === forkedSessionId
|
||||
? 'forked'
|
||||
: 'regular',
|
||||
update: updateData,
|
||||
result:
|
||||
error instanceof CopilotSessionInvalidInput ? 'rejected' : 'error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.snapshot(updateResults, 'session update validation results');
|
||||
|
||||
// session type conversions
|
||||
const existingPinnedId = randomUUID();
|
||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||
|
||||
await copilotSession.update({ userId: user.id, sessionId, pinned: true });
|
||||
|
||||
// pinning behavior
|
||||
const states = await getSessionStates(db, [sessionId, existingPinnedId]);
|
||||
const pinnedCount = states.filter(s => s?.pinned).length;
|
||||
const unpinnedCount = states.filter(s => s && !s.pinned).length;
|
||||
|
||||
t.snapshot(
|
||||
{
|
||||
totalSessions: states.length,
|
||||
pinnedSessions: pinnedCount,
|
||||
unpinnedSessions: unpinnedCount,
|
||||
onlyOneSessionPinned: pinnedCount === 1,
|
||||
},
|
||||
'pinning behavior - should unpin existing when pinning new'
|
||||
);
|
||||
|
||||
// type conversions
|
||||
const conversionSteps = [];
|
||||
const conversions: Array<[string, UpdateData]> = [
|
||||
['workspace_to_doc', { docId, pinned: false }],
|
||||
['doc_to_workspace', { docId: null }],
|
||||
['workspace_to_pinned', { pinned: true }],
|
||||
];
|
||||
|
||||
for (const [step, data] of conversions) {
|
||||
await copilotSession.update({ userId: user.id, sessionId, ...data });
|
||||
const session = await db.aiSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { docId: true, pinned: true },
|
||||
});
|
||||
conversionSteps.push({
|
||||
step,
|
||||
sessionState: {
|
||||
hasDocId: !!session?.docId,
|
||||
pinned: !!session?.pinned,
|
||||
},
|
||||
type: copilotSession.getSessionType(session!),
|
||||
});
|
||||
}
|
||||
|
||||
t.snapshot(conversionSteps, 'session type conversion steps');
|
||||
});
|
||||
|
||||
test('should handle session queries, ordering, and filtering', async t => {
|
||||
const { copilotSession, db } = t.context;
|
||||
await createTestPrompts(copilotSession, db);
|
||||
|
||||
const docId = randomUUID();
|
||||
const sessionIds: string[] = [];
|
||||
const sessionConfigs = [
|
||||
{ type: 'workspace', config: { docId: null, pinned: false } },
|
||||
{ type: 'pinned', config: { docId: null, pinned: true } },
|
||||
{ type: 'doc', config: { docId, pinned: false }, withMessages: true },
|
||||
{
|
||||
type: 'action',
|
||||
config: { docId, promptName: TEST_PROMPTS.ACTION, promptAction: 'edit' },
|
||||
},
|
||||
];
|
||||
|
||||
// create sessions with timing delays for ordering tests
|
||||
for (let i = 0; i < sessionConfigs.length; i++) {
|
||||
const { config, withMessages } = sessionConfigs[i];
|
||||
const sessionId = randomUUID();
|
||||
sessionIds.push(sessionId);
|
||||
|
||||
if (withMessages) {
|
||||
await createSessionWithMessages(
|
||||
t,
|
||||
{ sessionId, ...config },
|
||||
`Message for session ${i}`,
|
||||
100 * i
|
||||
);
|
||||
} else {
|
||||
await createTestSession(t, { sessionId, ...config });
|
||||
}
|
||||
}
|
||||
|
||||
// Create additional doc sessions for multiple doc test
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const sessionId = randomUUID();
|
||||
sessionIds.push(sessionId);
|
||||
await createSessionWithMessages(
|
||||
t,
|
||||
{ sessionId, docId },
|
||||
`Additional doc message ${i}`,
|
||||
200 + 100 * i
|
||||
);
|
||||
}
|
||||
|
||||
// create fork session
|
||||
const parentSessionId = sessionIds[2]; // use first doc session as parent
|
||||
const forkedSessionId = randomUUID();
|
||||
await db.aiSession.create({
|
||||
data: {
|
||||
id: forkedSessionId,
|
||||
workspaceId: workspace.id,
|
||||
userId: user.id,
|
||||
docId: 'forked-doc',
|
||||
docId,
|
||||
pinned: false,
|
||||
promptName: 'test-prompt',
|
||||
promptName: TEST_PROMPTS.NORMAL,
|
||||
promptAction: null,
|
||||
parentSessionId: parentSessionId,
|
||||
parentSessionId,
|
||||
},
|
||||
});
|
||||
|
||||
type UpdateData = Omit<UpdateChatSessionOptions, 'userId' | 'sessionId'>;
|
||||
const assertUpdateThrows = async (
|
||||
t: ExecutionContext<Context>,
|
||||
sessionId: string,
|
||||
updateData: UpdateData,
|
||||
message: string
|
||||
) => {
|
||||
await t.throwsAsync(
|
||||
t.context.copilotSession.update({
|
||||
...updateData,
|
||||
userId: user.id,
|
||||
sessionId,
|
||||
}),
|
||||
{ instanceOf: CopilotSessionInvalidInput },
|
||||
message
|
||||
);
|
||||
};
|
||||
const baseParams = { userId: user.id, workspaceId: workspace.id };
|
||||
const docParams = { ...baseParams, docId };
|
||||
const queryTestCases = [
|
||||
{ name: 'all_workspace_sessions', params: baseParams },
|
||||
{
|
||||
name: 'doc_sessions_with_messages',
|
||||
params: { ...docParams, withMessages: true },
|
||||
},
|
||||
{
|
||||
name: 'recent_top3_sessions',
|
||||
params: { ...baseParams, limit: 3, sessionOrder: 'desc' as const },
|
||||
},
|
||||
{
|
||||
name: 'non_action_sessions',
|
||||
params: { ...docParams, action: false },
|
||||
},
|
||||
{ name: 'non_fork_sessions', params: { ...docParams, fork: false } },
|
||||
{
|
||||
name: 'latest_valid_session',
|
||||
params: {
|
||||
...docParams,
|
||||
limit: 1,
|
||||
sessionOrder: 'desc' as const,
|
||||
action: false,
|
||||
fork: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const assertUpdate = async (
|
||||
t: ExecutionContext<Context>,
|
||||
sessionId: string,
|
||||
updateData: UpdateData,
|
||||
message: string
|
||||
) => {
|
||||
await t.notThrowsAsync(
|
||||
t.context.copilotSession.update({
|
||||
...updateData,
|
||||
userId: user.id,
|
||||
sessionId,
|
||||
}),
|
||||
message
|
||||
);
|
||||
};
|
||||
const queryResults: Record<string, any> = {};
|
||||
for (const { name, params } of queryTestCases) {
|
||||
const sessions = await copilotSession.list(params);
|
||||
queryResults[name] = {
|
||||
count: sessions.length,
|
||||
sessionTypes: sessions.map(s => ({
|
||||
type: copilotSession.getSessionType(s),
|
||||
hasMessages: !!s.messages?.length,
|
||||
messageCount: s.messages?.length || 0,
|
||||
isFork: !!s.parentSessionId,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// case 1: action sessions should reject all updates
|
||||
t.snapshot(queryResults, 'comprehensive session query results');
|
||||
|
||||
// should list sessions appear in correct order
|
||||
{
|
||||
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)}`
|
||||
const docSessionsWithMessages = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
withMessages: true,
|
||||
sessionOrder: 'desc',
|
||||
});
|
||||
|
||||
// check sessions are returned in desc order by updatedAt
|
||||
if (docSessionsWithMessages.length > 1) {
|
||||
for (let i = 1; i < docSessionsWithMessages.length; i++) {
|
||||
const currentSession = docSessionsWithMessages[i - 1];
|
||||
const nextSession = docSessionsWithMessages[i];
|
||||
t.true(
|
||||
currentSession.updatedAt >= nextSession.updatedAt,
|
||||
`sessions should be ordered by updatedAt desc: ${currentSession.updatedAt} >= ${nextSession.updatedAt}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// should update `updatedAt` when updating messages
|
||||
{
|
||||
const oldestDocSession = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
sessionOrder: 'asc',
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
if (oldestDocSession.length > 0) {
|
||||
const sessionId = oldestDocSession[0].id;
|
||||
|
||||
// get initial updatedAt
|
||||
const sessionBeforeUpdate = await db.aiSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { updatedAt: true },
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
await addMessagesToSession(
|
||||
copilotSession,
|
||||
sessionId,
|
||||
'Update to verify sorting'
|
||||
);
|
||||
|
||||
const sessionAfterUpdate = await db.aiSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { updatedAt: true },
|
||||
});
|
||||
t.true(
|
||||
sessionAfterUpdate!.updatedAt > sessionBeforeUpdate!.updatedAt,
|
||||
'updatedAt should be updated after adding messages'
|
||||
);
|
||||
|
||||
// the updated session now should appears first in desc order
|
||||
const sessionsAfterUpdate = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
sessionOrder: 'desc',
|
||||
});
|
||||
t.is(
|
||||
sessionsAfterUpdate[0].id,
|
||||
sessionId,
|
||||
'session with updated messages should appear first in descending order'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// case 2: forked sessions should reject docId updates but allow others
|
||||
// should get latest valid session
|
||||
{
|
||||
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'
|
||||
);
|
||||
}
|
||||
const latestValidSessions = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
limit: 1,
|
||||
sessionOrder: 'desc',
|
||||
action: false,
|
||||
fork: false,
|
||||
});
|
||||
|
||||
// cest 4: session type conversions and pinning behavior
|
||||
{
|
||||
const existingPinnedId = 'existing-pinned-session-id';
|
||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||
if (latestValidSessions.length > 0) {
|
||||
const latestSession = latestValidSessions[0];
|
||||
|
||||
// should unpin existing when pinning new session
|
||||
await copilotSession.update({ userId: user.id, sessionId, pinned: true });
|
||||
// verify this is indeed a non-action, non-fork session
|
||||
t.falsy(
|
||||
latestSession.parentSessionId,
|
||||
'latest session should not be a fork'
|
||||
);
|
||||
t.not(
|
||||
latestSession.promptName,
|
||||
TEST_PROMPTS.ACTION,
|
||||
'latest session should not use action prompt'
|
||||
);
|
||||
|
||||
t.snapshot(
|
||||
[
|
||||
await getSessionState(db, sessionId),
|
||||
await getSessionState(db, existingPinnedId),
|
||||
],
|
||||
'should unpin existing when pinning new session'
|
||||
);
|
||||
}
|
||||
|
||||
// test type conversions
|
||||
{
|
||||
const conversionSteps: any[] = [];
|
||||
const convertSession = async (step: string, data: UpdateData) => {
|
||||
await copilotSession.update({ ...data, userId: user.id, sessionId });
|
||||
const session = await db.aiSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { docId: true, pinned: true },
|
||||
// verify it's the most recently updated among valid sessions
|
||||
const allValidSessions = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId,
|
||||
action: false,
|
||||
fork: false,
|
||||
sessionOrder: 'desc',
|
||||
});
|
||||
conversionSteps.push({
|
||||
step,
|
||||
session,
|
||||
type: copilotSession.getSessionType(session!),
|
||||
});
|
||||
};
|
||||
|
||||
const conversions = [
|
||||
['pinned_to_doc', { docId, pinned: false }],
|
||||
['doc_to_workspace', { docId: null }],
|
||||
['workspace_to_pinned', { pinned: true }],
|
||||
] as const;
|
||||
|
||||
for (const [step, data] of conversions) {
|
||||
await convertSession(step, data);
|
||||
if (allValidSessions.length > 0) {
|
||||
t.is(
|
||||
allValidSessions[0].id,
|
||||
latestSession.id,
|
||||
'latest valid session should be the first in the ordered list'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
t.snapshot(conversionSteps, 'session type conversion steps');
|
||||
}
|
||||
|
||||
// session type identification
|
||||
const sessionTypeTests = [
|
||||
{ docId: null, pinned: false },
|
||||
{ docId: undefined, pinned: false },
|
||||
{ docId: null, pinned: true },
|
||||
{ docId: 'test-doc-id', pinned: false },
|
||||
];
|
||||
|
||||
const sessionTypeResults = sessionTypeTests.map(session => ({
|
||||
session,
|
||||
type: copilotSession.getSessionType(session),
|
||||
}));
|
||||
|
||||
t.snapshot(sessionTypeResults, 'session type identification results');
|
||||
});
|
||||
|
||||
test('should handle fork and session attachment operations', async t => {
|
||||
const { copilotSession } = t.context;
|
||||
await createTestPrompts(copilotSession, t.context.db);
|
||||
|
||||
const parentSessionId = randomUUID();
|
||||
const docId = randomUUID();
|
||||
|
||||
await createSessionWithMessages(
|
||||
t,
|
||||
{ sessionId: parentSessionId, docId },
|
||||
'Original message'
|
||||
);
|
||||
|
||||
const forkTestCases = [
|
||||
{
|
||||
sessionId: randomUUID(),
|
||||
docId: null,
|
||||
pinned: false,
|
||||
description: 'workspace fork',
|
||||
},
|
||||
{ sessionId: randomUUID(), docId, pinned: false, description: 'doc fork' },
|
||||
{
|
||||
sessionId: randomUUID(),
|
||||
docId: null,
|
||||
pinned: true,
|
||||
description: 'pinned fork',
|
||||
},
|
||||
];
|
||||
|
||||
// test unpinning behavior
|
||||
const existingPinnedId = randomUUID();
|
||||
await createTestSession(t, { sessionId: existingPinnedId, pinned: true });
|
||||
|
||||
const performForkOperation = async (
|
||||
copilotSession: CopilotSessionModel,
|
||||
parentSessionId: string,
|
||||
forkConfig: {
|
||||
sessionId: string;
|
||||
docId: string | null;
|
||||
pinned: boolean;
|
||||
}
|
||||
) => {
|
||||
return await copilotSession.fork({
|
||||
sessionId: forkConfig.sessionId,
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: forkConfig.docId,
|
||||
pinned: forkConfig.pinned,
|
||||
parentSessionId,
|
||||
prompt: { name: TEST_PROMPTS.NORMAL, action: null, model: 'gpt-4.1' },
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: 'Original message',
|
||||
createdAt: new Date(),
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
// fork operations
|
||||
const forkResults = await Promise.all(
|
||||
forkTestCases.map(async test => {
|
||||
const returnedId = await performForkOperation(
|
||||
copilotSession,
|
||||
parentSessionId,
|
||||
test
|
||||
);
|
||||
const forkedSession = await copilotSession.get(test.sessionId);
|
||||
return {
|
||||
description: test.description,
|
||||
success: returnedId === test.sessionId,
|
||||
actualState: forkedSession
|
||||
? {
|
||||
hasDocId: !!forkedSession.docId,
|
||||
isDocIdCorrect: forkedSession.docId === test.docId,
|
||||
pinned: forkedSession.pinned,
|
||||
hasParent: !!forkedSession.parentSessionId,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// check if pinned fork unpinned existing session
|
||||
const originalPinned = await copilotSession.get(existingPinnedId);
|
||||
|
||||
t.snapshot(
|
||||
{
|
||||
forkResults,
|
||||
existingPinnedSessionUnpinned: !originalPinned?.pinned,
|
||||
},
|
||||
'fork operation results'
|
||||
);
|
||||
|
||||
// attach/detach operations
|
||||
const workspaceSessionId = randomUUID();
|
||||
const existingDocSessionId = randomUUID();
|
||||
const attachTestDocId = randomUUID();
|
||||
|
||||
// sessions for attach/detach test
|
||||
await createTestSession(t, { sessionId: workspaceSessionId, docId: null });
|
||||
await createTestSession(t, {
|
||||
sessionId: existingDocSessionId,
|
||||
docId: attachTestDocId,
|
||||
});
|
||||
|
||||
// attach: workspace -> doc
|
||||
await copilotSession.update({
|
||||
userId: user.id,
|
||||
sessionId: workspaceSessionId,
|
||||
docId: attachTestDocId,
|
||||
});
|
||||
|
||||
const docSessionsAfterAttach = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: attachTestDocId,
|
||||
});
|
||||
|
||||
// detach: doc -> workspace
|
||||
await copilotSession.update({
|
||||
userId: user.id,
|
||||
sessionId: workspaceSessionId,
|
||||
docId: null,
|
||||
});
|
||||
|
||||
const workspaceSessionsAfterDetach = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
});
|
||||
|
||||
const remainingDocSessions = await copilotSession.list({
|
||||
userId: user.id,
|
||||
workspaceId: workspace.id,
|
||||
docId: attachTestDocId,
|
||||
});
|
||||
|
||||
t.snapshot(
|
||||
{
|
||||
attachPhase: {
|
||||
docSessionCount: docSessionsAfterAttach.length,
|
||||
bothSessionsPresent:
|
||||
docSessionsAfterAttach.some(s => s.id === workspaceSessionId) &&
|
||||
docSessionsAfterAttach.some(s => s.id === existingDocSessionId),
|
||||
},
|
||||
detachPhase: {
|
||||
workspaceSessionExists: workspaceSessionsAfterDetach.some(
|
||||
s => s.id === workspaceSessionId && !s.pinned
|
||||
),
|
||||
originalDocSessionRemains:
|
||||
remainingDocSessions.length === 1 &&
|
||||
remainingDocSessions[0].id === existingDocSessionId,
|
||||
},
|
||||
},
|
||||
'attach and detach operation results'
|
||||
);
|
||||
});
|
||||
|
@ -206,6 +206,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
// save message
|
||||
await this.models.copilotSession.updateMessages({
|
||||
...forkedState,
|
||||
sessionId,
|
||||
messages,
|
||||
});
|
||||
return sessionId;
|
||||
@ -286,18 +287,37 @@ export class CopilotSessionModel extends BaseModel {
|
||||
async list(options: ListSessionOptions) {
|
||||
const { userId, sessionId, workspaceId, docId } = options;
|
||||
|
||||
const extraCondition = [];
|
||||
const conditions: Prisma.AiSessionWhereInput['OR'] = [
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
docId: docId ?? null,
|
||||
id: sessionId ? { equals: sessionId } : undefined,
|
||||
deletedAt: null,
|
||||
prompt:
|
||||
typeof options.action === 'boolean'
|
||||
? options.action
|
||||
? { action: { not: null } }
|
||||
: { action: null }
|
||||
: undefined,
|
||||
parentSessionId:
|
||||
typeof options.fork === 'boolean'
|
||||
? options.fork
|
||||
? { not: null }
|
||||
: null
|
||||
: undefined,
|
||||
},
|
||||
];
|
||||
|
||||
if (!options?.action && options?.fork) {
|
||||
// query forked sessions from other users
|
||||
// only query forked session if fork == true and action == false
|
||||
extraCondition.push({
|
||||
conditions.push({
|
||||
userId: { not: userId },
|
||||
workspaceId: workspaceId,
|
||||
docId: docId ?? null,
|
||||
id: sessionId ? { equals: sessionId } : undefined,
|
||||
prompt: {
|
||||
action: options.action ? { not: null } : null,
|
||||
},
|
||||
prompt: { action: null },
|
||||
// should only find forked session
|
||||
parentSessionId: { not: null },
|
||||
deletedAt: null,
|
||||
@ -305,18 +325,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
}
|
||||
|
||||
return await this.db.aiSession.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
userId,
|
||||
workspaceId,
|
||||
docId: docId ?? null,
|
||||
id: sessionId ? { equals: sessionId } : undefined,
|
||||
deletedAt: null,
|
||||
},
|
||||
...extraCondition,
|
||||
],
|
||||
},
|
||||
where: { OR: conditions },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
@ -327,6 +336,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
promptName: true,
|
||||
tokenCost: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
messages: options.withMessages
|
||||
? {
|
||||
select: {
|
||||
@ -348,8 +358,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
take: options?.limit,
|
||||
skip: options?.skip,
|
||||
orderBy: {
|
||||
// session order is desc by default
|
||||
createdAt: options?.sessionOrder === 'asc' ? 'asc' : 'desc',
|
||||
updatedAt: options?.sessionOrder === 'asc' ? 'asc' : 'desc',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -235,6 +235,12 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
|
||||
@Field(() => String)
|
||||
sessionId!: string;
|
||||
|
||||
@Field(() => String)
|
||||
workspaceId!: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
docId!: string | null;
|
||||
|
||||
@Field(() => Boolean)
|
||||
pinned!: boolean;
|
||||
|
||||
@ -254,6 +260,9 @@ class CopilotHistoriesType implements Partial<ChatHistory> {
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt!: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
updatedAt!: Date;
|
||||
}
|
||||
|
||||
@ObjectType('CopilotQuota')
|
||||
|
@ -298,13 +298,16 @@ export class ChatSessionService {
|
||||
const histories = await Promise.all(
|
||||
sessions.map(
|
||||
async ({
|
||||
id,
|
||||
userId: uid,
|
||||
id,
|
||||
workspaceId,
|
||||
docId,
|
||||
pinned,
|
||||
promptName,
|
||||
tokenCost,
|
||||
messages,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
}) => {
|
||||
try {
|
||||
const prompt = await this.prompt.get(promptName);
|
||||
@ -341,10 +344,13 @@ export class ChatSessionService {
|
||||
|
||||
return {
|
||||
sessionId: id,
|
||||
workspaceId,
|
||||
docId,
|
||||
pinned,
|
||||
action: prompt.action || null,
|
||||
tokens: tokenCost,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
messages: preload.concat(ret.data).map(m => ({
|
||||
...m,
|
||||
attachments: m.attachments
|
||||
|
@ -47,11 +47,14 @@ export type ChatMessage = z.infer<typeof ChatMessageSchema>;
|
||||
export const ChatHistorySchema = z
|
||||
.object({
|
||||
sessionId: z.string(),
|
||||
workspaceId: z.string(),
|
||||
docId: z.string().nullable(),
|
||||
pinned: z.boolean(),
|
||||
action: z.string().nullable(),
|
||||
tokens: z.number(),
|
||||
messages: z.array(ChatMessageSchema),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
@ -237,12 +237,15 @@ type CopilotHistories {
|
||||
"""An mark identifying which view to use to display the session"""
|
||||
action: String
|
||||
createdAt: DateTime!
|
||||
docId: String
|
||||
messages: [ChatMessage!]!
|
||||
pinned: Boolean!
|
||||
sessionId: String!
|
||||
|
||||
"""The number of tokens used in the session"""
|
||||
tokens: Int!
|
||||
updatedAt: DateTime!
|
||||
workspaceId: String!
|
||||
}
|
||||
|
||||
type CopilotInvalidContextDataType {
|
||||
@ -253,22 +256,6 @@ type CopilotMessageNotFoundDataType {
|
||||
messageId: String!
|
||||
}
|
||||
|
||||
enum CopilotModels {
|
||||
DallE3
|
||||
Gpt4Omni
|
||||
Gpt4Omni0806
|
||||
Gpt4OmniMini
|
||||
Gpt4OmniMini0718
|
||||
Gpt41
|
||||
Gpt41Mini
|
||||
Gpt41Nano
|
||||
Gpt410414
|
||||
GptImage
|
||||
TextEmbedding3Large
|
||||
TextEmbedding3Small
|
||||
TextEmbeddingAda002
|
||||
}
|
||||
|
||||
input CopilotPromptConfigInput {
|
||||
frequencyPenalty: Float
|
||||
presencePenalty: Float
|
||||
@ -408,7 +395,7 @@ input CreateCopilotPromptInput {
|
||||
action: String
|
||||
config: CopilotPromptConfigInput
|
||||
messages: [CopilotPromptMessageInput!]!
|
||||
model: CopilotModels!
|
||||
model: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,35 @@
|
||||
query getCopilotLatestDocSession(
|
||||
$workspaceId: String!
|
||||
$docId: String!
|
||||
) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
histories(
|
||||
docId: $docId
|
||||
options: {
|
||||
limit: 1
|
||||
sessionOrder: desc
|
||||
action: false
|
||||
fork: false
|
||||
}
|
||||
) {
|
||||
sessionId
|
||||
workspaceId
|
||||
docId
|
||||
pinned
|
||||
action
|
||||
tokens
|
||||
createdAt
|
||||
updatedAt
|
||||
messages {
|
||||
id
|
||||
role
|
||||
content
|
||||
attachments
|
||||
params
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
query getCopilotRecentSessions(
|
||||
$workspaceId: String!
|
||||
$limit: Int = 10
|
||||
) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
histories(
|
||||
options: {
|
||||
limit: $limit
|
||||
sessionOrder: desc
|
||||
}
|
||||
) {
|
||||
sessionId
|
||||
workspaceId
|
||||
docId
|
||||
pinned
|
||||
action
|
||||
tokens
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -755,6 +755,38 @@ export const forkCopilotSessionMutation = {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getCopilotLatestDocSessionQuery = {
|
||||
id: 'getCopilotLatestDocSessionQuery' as const,
|
||||
op: 'getCopilotLatestDocSession',
|
||||
query: `query getCopilotLatestDocSession($workspaceId: String!, $docId: String!) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
histories(
|
||||
docId: $docId
|
||||
options: {limit: 1, sessionOrder: desc, action: false, fork: false}
|
||||
) {
|
||||
sessionId
|
||||
workspaceId
|
||||
docId
|
||||
pinned
|
||||
action
|
||||
tokens
|
||||
createdAt
|
||||
updatedAt
|
||||
messages {
|
||||
id
|
||||
role
|
||||
content
|
||||
attachments
|
||||
params
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getCopilotSessionQuery = {
|
||||
id: 'getCopilotSessionQuery' as const,
|
||||
op: 'getCopilotSession',
|
||||
@ -775,6 +807,27 @@ export const getCopilotSessionQuery = {
|
||||
}`,
|
||||
};
|
||||
|
||||
export const getCopilotRecentSessionsQuery = {
|
||||
id: 'getCopilotRecentSessionsQuery' as const,
|
||||
op: 'getCopilotRecentSessions',
|
||||
query: `query getCopilotRecentSessions($workspaceId: String!, $limit: Int = 10) {
|
||||
currentUser {
|
||||
copilot(workspaceId: $workspaceId) {
|
||||
histories(options: {limit: $limit, sessionOrder: desc}) {
|
||||
sessionId
|
||||
workspaceId
|
||||
docId
|
||||
pinned
|
||||
action
|
||||
tokens
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
};
|
||||
|
||||
export const updateCopilotSessionMutation = {
|
||||
id: 'updateCopilotSessionMutation' as const,
|
||||
op: 'updateCopilotSession',
|
||||
|
@ -323,11 +323,14 @@ export interface CopilotHistories {
|
||||
/** An mark identifying which view to use to display the session */
|
||||
action: Maybe<Scalars['String']['output']>;
|
||||
createdAt: Scalars['DateTime']['output'];
|
||||
docId: Maybe<Scalars['String']['output']>;
|
||||
messages: Array<ChatMessage>;
|
||||
pinned: Scalars['Boolean']['output'];
|
||||
sessionId: Scalars['String']['output'];
|
||||
/** The number of tokens used in the session */
|
||||
tokens: Scalars['Int']['output'];
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
workspaceId: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export interface CopilotInvalidContextDataType {
|
||||
@ -340,22 +343,6 @@ export interface CopilotMessageNotFoundDataType {
|
||||
messageId: Scalars['String']['output'];
|
||||
}
|
||||
|
||||
export enum CopilotModels {
|
||||
DallE3 = 'DallE3',
|
||||
Gpt4Omni = 'Gpt4Omni',
|
||||
Gpt4Omni0806 = 'Gpt4Omni0806',
|
||||
Gpt4OmniMini = 'Gpt4OmniMini',
|
||||
Gpt4OmniMini0718 = 'Gpt4OmniMini0718',
|
||||
Gpt41 = 'Gpt41',
|
||||
Gpt41Mini = 'Gpt41Mini',
|
||||
Gpt41Nano = 'Gpt41Nano',
|
||||
Gpt410414 = 'Gpt410414',
|
||||
GptImage = 'GptImage',
|
||||
TextEmbedding3Large = 'TextEmbedding3Large',
|
||||
TextEmbedding3Small = 'TextEmbedding3Small',
|
||||
TextEmbeddingAda002 = 'TextEmbeddingAda002',
|
||||
}
|
||||
|
||||
export interface CopilotPromptConfigInput {
|
||||
frequencyPenalty?: InputMaybe<Scalars['Float']['input']>;
|
||||
presencePenalty?: InputMaybe<Scalars['Float']['input']>;
|
||||
@ -515,7 +502,7 @@ export interface CreateCopilotPromptInput {
|
||||
action?: InputMaybe<Scalars['String']['input']>;
|
||||
config?: InputMaybe<CopilotPromptConfigInput>;
|
||||
messages: Array<CopilotPromptMessageInput>;
|
||||
model: CopilotModels;
|
||||
model: Scalars['String']['input'];
|
||||
name: Scalars['String']['input'];
|
||||
}
|
||||
|
||||
@ -3575,6 +3562,41 @@ export type ForkCopilotSessionMutation = {
|
||||
forkCopilotSession: string;
|
||||
};
|
||||
|
||||
export type GetCopilotLatestDocSessionQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
docId: Scalars['String']['input'];
|
||||
}>;
|
||||
|
||||
export type GetCopilotLatestDocSessionQuery = {
|
||||
__typename?: 'Query';
|
||||
currentUser: {
|
||||
__typename?: 'UserType';
|
||||
copilot: {
|
||||
__typename?: 'Copilot';
|
||||
histories: Array<{
|
||||
__typename?: 'CopilotHistories';
|
||||
sessionId: string;
|
||||
workspaceId: string;
|
||||
docId: string | null;
|
||||
pinned: boolean;
|
||||
action: string | null;
|
||||
tokens: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
messages: Array<{
|
||||
__typename?: 'ChatMessage';
|
||||
id: string | null;
|
||||
role: string;
|
||||
content: string;
|
||||
attachments: Array<string> | null;
|
||||
params: Record<string, string> | null;
|
||||
createdAt: string;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type GetCopilotSessionQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
sessionId: Scalars['String']['input'];
|
||||
@ -3600,6 +3622,32 @@ export type GetCopilotSessionQuery = {
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type GetCopilotRecentSessionsQueryVariables = Exact<{
|
||||
workspaceId: Scalars['String']['input'];
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
}>;
|
||||
|
||||
export type GetCopilotRecentSessionsQuery = {
|
||||
__typename?: 'Query';
|
||||
currentUser: {
|
||||
__typename?: 'UserType';
|
||||
copilot: {
|
||||
__typename?: 'Copilot';
|
||||
histories: Array<{
|
||||
__typename?: 'CopilotHistories';
|
||||
sessionId: string;
|
||||
workspaceId: string;
|
||||
docId: string | null;
|
||||
pinned: boolean;
|
||||
action: string | null;
|
||||
tokens: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}>;
|
||||
};
|
||||
} | null;
|
||||
};
|
||||
|
||||
export type UpdateCopilotSessionMutationVariables = Exact<{
|
||||
options: UpdateChatSessionInput;
|
||||
}>;
|
||||
@ -5209,11 +5257,21 @@ export type Queries =
|
||||
variables: CopilotQuotaQueryVariables;
|
||||
response: CopilotQuotaQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getCopilotLatestDocSessionQuery';
|
||||
variables: GetCopilotLatestDocSessionQueryVariables;
|
||||
response: GetCopilotLatestDocSessionQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getCopilotSessionQuery';
|
||||
variables: GetCopilotSessionQueryVariables;
|
||||
response: GetCopilotSessionQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getCopilotRecentSessionsQuery';
|
||||
variables: GetCopilotRecentSessionsQueryVariables;
|
||||
response: GetCopilotRecentSessionsQuery;
|
||||
}
|
||||
| {
|
||||
name: 'getCopilotSessionsQuery';
|
||||
variables: GetCopilotSessionsQueryVariables;
|
||||
|
Loading…
x
Reference in New Issue
Block a user