chrome-devtools-frontend
Version:
Chrome DevTools UI
541 lines (513 loc) • 16.8 kB
text/typescript
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../core/common/common.js';
import * as AiAssistance from './ai_assistance.js';
describe('AiHistoryStorage', () => {
const agent1: AiAssistance.SerializedConversation = {
id: 'id1',
type: AiAssistance.ConversationType.STYLING,
history: [],
isExternal: false,
};
const agent2: AiAssistance.SerializedConversation = {
id: 'id2',
type: AiAssistance.ConversationType.FILE,
history: [],
isExternal: false,
};
const agent3: AiAssistance.SerializedConversation = {
id: 'id3',
type: AiAssistance.ConversationType.NETWORK,
history: [],
isExternal: false,
};
const agent4: AiAssistance.SerializedConversation = {
id: 'id4',
type: AiAssistance.ConversationType.STYLING,
history: [
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageId: 'image-id1',
imageInput: undefined,
},
{
type: AiAssistance.ResponseType.ANSWER,
text: 'answer',
complete: true,
},
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageId: 'image-id2',
imageInput: undefined,
},
],
isExternal: false,
};
const serializedImage1: AiAssistance.SerializedImage = {
id: 'image-id1',
data: 'imageInput',
mimeType: 'image/jpeg',
};
const serializedImage2: AiAssistance.SerializedImage = {
id: 'image-id2',
data: 'imageInput',
mimeType: 'image/jpeg',
};
beforeEach(() => {
let data: Record<string, string> = {};
const dummyStorage = new Common.Settings.SettingsStorage({}, {
get(setting) {
return Promise.resolve(data[setting]);
},
set(setting, value) {
data[setting] = value;
},
clear() {
data = {};
},
remove(setting) {
delete data[setting];
},
register(_setting) {},
});
Common.Settings.Settings.instance({
forceNew: true,
syncedStorage: dummyStorage,
globalStorage: dummyStorage,
localStorage: dummyStorage,
});
});
function getStorage(maxStorageSize?: number) {
return AiAssistance.AiHistoryStorage.instance({forceNew: true, maxStorageSize});
}
it('should create and retrieve history entry', async () => {
const storage = getStorage();
await storage.upsertHistoryEntry(agent1);
assert.deepEqual(
storage.getHistory(),
[{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [],
isExternal: false,
}],
);
await storage.upsertHistoryEntry(agent2);
assert.deepEqual(
storage.getHistory(),
[
{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id2',
type: 'drjones-file' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
],
);
});
it('should update history entries correctly', async () => {
const storage = getStorage();
await storage.upsertHistoryEntry(agent1);
await storage.upsertHistoryEntry(agent2);
await storage.upsertHistoryEntry({
...agent1,
history: [
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
},
],
});
assert.deepEqual(
storage.getHistory(),
[
{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
},
],
isExternal: false,
},
{
id: 'id2',
type: 'drjones-file' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
],
);
await storage.upsertHistoryEntry(agent3);
assert.deepEqual(
storage.getHistory(),
[
{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
},
],
isExternal: false,
},
{
id: 'id2',
type: 'drjones-file' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id3',
type: 'drjones-network-request' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
],
);
assert.deepEqual(
storage.getImageHistory(),
[],
);
await storage.upsertImage(serializedImage1);
await storage.upsertImage(serializedImage2);
await storage.upsertHistoryEntry(agent4);
assert.deepEqual(
storage.getHistory(),
[
{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
},
],
isExternal: false,
},
{
id: 'id2',
type: 'drjones-file' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id3',
type: 'drjones-network-request' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id4',
type: 'freestyler' as AiAssistance.ConversationType,
history: [
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageId: 'image-id1',
imageInput: undefined,
},
{
type: AiAssistance.ResponseType.ANSWER,
text: 'answer',
complete: true,
},
{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageId: 'image-id2',
imageInput: undefined,
},
],
isExternal: false,
},
],
);
assert.deepEqual(
storage.getImageHistory(),
[
{
id: 'image-id1',
data: 'imageInput',
mimeType: 'image/jpeg',
},
{
id: 'image-id2',
data: 'imageInput',
mimeType: 'image/jpeg',
}
],
);
});
it('should delete a single entry', async () => {
const storage = getStorage();
await storage.upsertHistoryEntry(agent1);
await storage.upsertHistoryEntry(agent2);
await storage.upsertHistoryEntry(agent3);
await storage.deleteHistoryEntry('id2');
assert.deepEqual(
storage.getHistory(),
[
{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id3',
type: 'drjones-network-request' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
],
);
});
it('should delete image history entry', async () => {
const storage = getStorage();
await storage.upsertHistoryEntry(agent1);
await storage.upsertHistoryEntry(agent2);
await storage.upsertHistoryEntry(agent3);
await storage.upsertImage(serializedImage1);
await storage.upsertImage(serializedImage2);
await storage.upsertHistoryEntry(agent4);
await storage.deleteHistoryEntry('id4');
assert.deepEqual(
storage.getHistory(),
[
{
id: 'id1',
type: 'freestyler' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id2',
type: 'drjones-file' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
{
id: 'id3',
type: 'drjones-network-request' as AiAssistance.ConversationType,
history: [],
isExternal: false,
},
],
);
assert.deepEqual(
storage.getImageHistory(),
[],
);
});
it('should delete all entries', async () => {
const storage = getStorage();
await storage.upsertHistoryEntry(agent1);
await storage.upsertHistoryEntry(agent2);
await storage.upsertHistoryEntry(agent3);
await storage.upsertImage(serializedImage1);
await storage.upsertImage(serializedImage2);
await storage.upsertHistoryEntry(agent4);
await storage.deleteAll();
assert.deepEqual(
storage.getHistory(),
[],
);
assert.deepEqual(
storage.getImageHistory(),
[],
);
});
it('should limit the amount of stored images', async () => {
const MAX_STORAGE_SIZE = 2;
const storage = getStorage(MAX_STORAGE_SIZE);
await storage.upsertImage({
id: 'image-id1',
data: '1',
mimeType: 'image/jpeg',
});
await storage.upsertHistoryEntry(agent1);
await storage.upsertImage({
id: 'image-id2',
data: '2',
mimeType: 'image/jpeg',
});
await storage.upsertImage({
id: 'image-id3',
data: '3',
mimeType: 'image/jpeg',
});
await storage.upsertHistoryEntry(agent2);
await storage.upsertImage({
id: 'image-id4',
data: '4',
mimeType: 'image/jpeg',
});
await storage.upsertHistoryEntry(agent3);
const imageHistory = storage.getImageHistory();
const imageData1 = imageHistory.find(item => item.id === 'image-id1');
const imageData2 = imageHistory.find(item => item.id === 'image-id2');
const imageData3 = imageHistory.find(item => item.id === 'image-id3');
const imageData4 = imageHistory.find(item => item.id === 'image-id4');
assert.notExists(imageData1);
assert.notExists(imageData2);
assert.deepEqual(imageData3, {
id: 'image-id3',
data: '3',
mimeType: 'image/jpeg',
});
assert.deepEqual(imageData4, {
id: 'image-id4',
data: '4',
mimeType: 'image/jpeg',
});
});
describe('Conversation', () => {
describe('title', () => {
it('should return undefined if there is not USER_QUERY entry in history', () => {
const conversation = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, []);
assert.isUndefined(conversation.title);
});
it('should return full title if the first USER_QUERY is less than 80 characters', () => {
const conversation = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'this is less than 80',
}]);
assert.strictEqual(conversation.title, 'this is less than 80');
});
it('should return first 80 characters of the title with ellipis if the first USER_QUERY is more than 80 characters',
() => {
const conversation = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [
{
type: AiAssistance.ResponseType.USER_QUERY,
query:
'this is more than 80 characters because I\'m just going to keep typing words and words and words until it\'s really, really long, see?',
}
]);
assert.strictEqual(
conversation.title,
'this is more than 80 characters because I\'m just going to keep typing words and …');
});
});
describe('addHistoryItem', () => {
const historyItem1: AiAssistance.ResponseData = {
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageInput: {
inlineData: {
data: '1',
mimeType: 'image/jpeg',
}
},
imageId: 'image-id1',
};
const historyItem2: AiAssistance.ResponseData = {
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageInput: {
inlineData: {
data: '2',
mimeType: 'image/jpeg',
}
},
imageId: 'image-id2',
};
it('should store images and text conversation separately', async () => {
const storage = getStorage();
sinon.stub(AiAssistance.AiHistoryStorage, 'instance').returns(storage);
const conversation1 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id1', false);
await conversation1.addHistoryItem(historyItem1);
const conversation2 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id2', false);
await conversation2.addHistoryItem(historyItem2);
const imageHistory = storage.getImageHistory();
assert.lengthOf(imageHistory, 2);
assert.deepEqual(imageHistory[0], {
id: 'image-id1',
data: '1',
mimeType: 'image/jpeg',
});
assert.deepEqual(imageHistory[1], {
id: 'image-id2',
data: '2',
mimeType: 'image/jpeg',
});
const historyWithoutImages = storage.getHistory();
assert.lengthOf(historyWithoutImages, 2);
assert.deepEqual(historyWithoutImages[0], {
id: 'id1',
type: AiAssistance.ConversationType.STYLING,
history: [{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageId: 'image-id1',
}],
isExternal: false,
});
assert.deepEqual(historyWithoutImages[1], {
id: 'id2',
type: AiAssistance.ConversationType.STYLING,
history: [{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageInput: undefined,
imageId: 'image-id2',
}],
isExternal: false,
});
});
it('should have empty image data for image not present in history', async () => {
const MAX_STORAGE_SIZE = 1;
const storage = getStorage(MAX_STORAGE_SIZE);
sinon.stub(AiAssistance.AiHistoryStorage, 'instance').returns(storage);
const conversation1 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id1', false);
await conversation1.addHistoryItem(historyItem1);
const conversation2 = new AiAssistance.Conversation(AiAssistance.ConversationType.STYLING, [], 'id2', false);
await conversation2.addHistoryItem(historyItem2);
const imageHistory = storage.getImageHistory();
assert.lengthOf(imageHistory, 1);
const historyWithoutImages = storage.getHistory();
assert.lengthOf(historyWithoutImages, 2);
const conversationFromHistory = historyWithoutImages.map(item => {
return new AiAssistance.Conversation(item.type, item.history, item.id, true);
});
assert.lengthOf(conversationFromHistory, 2);
assert.deepEqual(conversationFromHistory[0].history, [{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageInput: {
inlineData: {
data: AiAssistance.NOT_FOUND_IMAGE_DATA,
mimeType: 'image/jpeg',
}
},
imageId: 'image-id1',
}]);
assert.deepEqual(conversationFromHistory[1].history, [{
type: AiAssistance.ResponseType.USER_QUERY,
query: 'text',
imageInput: {
inlineData: {
data: '2',
mimeType: 'image/jpeg',
}
},
imageId: 'image-id2',
}]);
});
});
});
});