deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
670
packages/ai-chat/src/common/chat-model-serialization.spec.ts
Normal file
670
packages/ai-chat/src/common/chat-model-serialization.spec.ts
Normal file
@@ -0,0 +1,670 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { ChatAgentLocation } from './chat-agents';
|
||||
import { MutableChatModel } from './chat-model';
|
||||
import { ParsedChatRequest, ParsedChatRequestTextPart, ParsedChatRequestVariablePart, ParsedChatRequestFunctionPart, ParsedChatRequestAgentPart } from './parsed-chat-request';
|
||||
import { ToolRequest } from '@theia/ai-core';
|
||||
import { SerializableTextPart, SerializableVariablePart, SerializableFunctionPart, SerializableAgentPart } from './chat-model-serialization';
|
||||
|
||||
describe('ChatModel Serialization and Restoration', () => {
|
||||
|
||||
function createParsedRequest(text: string): ParsedChatRequest {
|
||||
return {
|
||||
request: { text },
|
||||
parts: [
|
||||
new ParsedChatRequestTextPart(
|
||||
{ start: 0, endExclusive: text.length },
|
||||
text
|
||||
)
|
||||
],
|
||||
toolRequests: new Map(),
|
||||
variables: []
|
||||
};
|
||||
}
|
||||
|
||||
describe('Simple tree serialization', () => {
|
||||
it('should serialize a chat with a single request', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
model.addRequest(createParsedRequest('Hello'));
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
|
||||
expect(serialized.hierarchy).to.be.an('object');
|
||||
expect(serialized.hierarchy!.rootBranchId).to.be.a('string');
|
||||
expect(serialized.hierarchy!.branches).to.be.an('object');
|
||||
expect(serialized.requests).to.have.lengthOf(1);
|
||||
expect(serialized.requests[0].text).to.equal('Hello');
|
||||
});
|
||||
|
||||
it('should serialize a chat with multiple sequential requests', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
model.addRequest(createParsedRequest('First'));
|
||||
model.addRequest(createParsedRequest('Second'));
|
||||
model.addRequest(createParsedRequest('Third'));
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
|
||||
expect(serialized.hierarchy).to.be.an('object');
|
||||
expect(serialized.requests).to.have.lengthOf(3);
|
||||
|
||||
// Verify the hierarchy has 3 branches (one for each request)
|
||||
const branches = Object.values(serialized.hierarchy!.branches);
|
||||
expect(branches).to.have.lengthOf(3);
|
||||
|
||||
// Verify the active path through the tree
|
||||
const rootBranch = serialized.hierarchy!.branches[serialized.hierarchy!.rootBranchId];
|
||||
expect(rootBranch.items).to.have.lengthOf(1);
|
||||
expect(rootBranch.items[0].requestId).to.equal(serialized.requests[0].id);
|
||||
expect(rootBranch.items[0].nextBranchId).to.be.a('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tree serialization with alternatives (edited messages)', () => {
|
||||
it('should serialize a chat with edited messages', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
|
||||
// Add first request
|
||||
const req1 = model.addRequest(createParsedRequest('Original message'));
|
||||
req1.response.complete();
|
||||
|
||||
// Add second request
|
||||
model.addRequest(createParsedRequest('Follow-up'));
|
||||
|
||||
// Edit the first request (creating an alternative)
|
||||
const branch1 = model.getBranch(req1.id);
|
||||
expect(branch1).to.not.be.undefined;
|
||||
branch1!.add(model.addRequest(createParsedRequest('Edited message'), 'agent-1'));
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
|
||||
// Should have 3 requests: original, edited, and follow-up
|
||||
expect(serialized.requests).to.have.lengthOf(3);
|
||||
|
||||
// The root branch should have 2 items (original and edited alternatives)
|
||||
const rootBranch = serialized.hierarchy!.branches[serialized.hierarchy!.rootBranchId];
|
||||
expect(rootBranch.items).to.have.lengthOf(2);
|
||||
expect(rootBranch.items[0].requestId).to.equal(serialized.requests[0].id);
|
||||
expect(rootBranch.items[1].requestId).to.equal(serialized.requests[2].id);
|
||||
|
||||
// The active branch index should point to the most recent alternative
|
||||
expect(rootBranch.activeBranchIndex).to.be.at.least(0);
|
||||
});
|
||||
|
||||
it('should serialize nested alternatives (edited multiple times)', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
|
||||
// Add first request
|
||||
const req1 = model.addRequest(createParsedRequest('First'));
|
||||
req1.response.complete();
|
||||
|
||||
// Add second request
|
||||
const req2 = model.addRequest(createParsedRequest('Second'));
|
||||
req2.response.complete();
|
||||
|
||||
// Edit the second request (creating an alternative)
|
||||
const branch2 = model.getBranch(req2.id);
|
||||
expect(branch2).to.not.be.undefined;
|
||||
const req2edited = model.addRequest(createParsedRequest('Second (edited)'), 'agent-1');
|
||||
branch2!.add(req2edited);
|
||||
|
||||
// Add third request after the edited version
|
||||
model.addRequest(createParsedRequest('Third'));
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
|
||||
// Should have 4 requests total
|
||||
expect(serialized.requests).to.have.lengthOf(4);
|
||||
|
||||
// Find the second-level branch
|
||||
const rootBranch = serialized.hierarchy!.branches[serialized.hierarchy!.rootBranchId];
|
||||
const nextBranchId = rootBranch.items[rootBranch.activeBranchIndex].nextBranchId;
|
||||
expect(nextBranchId).to.be.a('string');
|
||||
|
||||
const secondBranch = serialized.hierarchy!.branches[nextBranchId!];
|
||||
expect(secondBranch.items).to.have.lengthOf(2); // Original and edited
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tree restoration from serialized data', () => {
|
||||
it('should restore a simple chat session', () => {
|
||||
// Create and serialize
|
||||
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
model1.addRequest(createParsedRequest('Hello'));
|
||||
const serialized = model1.toSerializable();
|
||||
|
||||
// Restore
|
||||
const model2 = new MutableChatModel(serialized);
|
||||
|
||||
expect(model2.getRequests()).to.have.lengthOf(1);
|
||||
expect(model2.getRequests()[0].request.text).to.equal('Hello');
|
||||
});
|
||||
|
||||
it('should restore chat with multiple sequential requests', () => {
|
||||
// Create and serialize
|
||||
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
model1.addRequest(createParsedRequest('First'));
|
||||
model1.addRequest(createParsedRequest('Second'));
|
||||
model1.addRequest(createParsedRequest('Third'));
|
||||
const serialized = model1.toSerializable();
|
||||
|
||||
// Restore
|
||||
const model2 = new MutableChatModel(serialized);
|
||||
|
||||
const requests = model2.getRequests();
|
||||
expect(requests).to.have.lengthOf(3);
|
||||
expect(requests[0].request.text).to.equal('First');
|
||||
expect(requests[1].request.text).to.equal('Second');
|
||||
expect(requests[2].request.text).to.equal('Third');
|
||||
});
|
||||
|
||||
it('should restore chat with edited messages (alternatives)', () => {
|
||||
// Create and serialize
|
||||
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const req1 = model1.addRequest(createParsedRequest('Original'));
|
||||
req1.response.complete();
|
||||
|
||||
const branch1 = model1.getBranch(req1.id);
|
||||
const req1edited = model1.addRequest(createParsedRequest('Edited'), 'agent-1');
|
||||
branch1!.add(req1edited);
|
||||
|
||||
const serialized = model1.toSerializable();
|
||||
|
||||
// Verify serialization includes both alternatives
|
||||
expect(serialized.requests).to.have.lengthOf(2);
|
||||
|
||||
// Restore
|
||||
const model2 = new MutableChatModel(serialized);
|
||||
|
||||
// Check that both alternatives are restored
|
||||
const restoredBranch = model2.getBranch(serialized.requests[0].id);
|
||||
expect(restoredBranch).to.not.be.undefined;
|
||||
expect(restoredBranch!.items).to.have.lengthOf(2);
|
||||
expect(restoredBranch!.items[0].element.request.text).to.equal('Original');
|
||||
expect(restoredBranch!.items[1].element.request.text).to.equal('Edited');
|
||||
});
|
||||
|
||||
it('should restore the correct active branch indices', () => {
|
||||
// Create and serialize
|
||||
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const req1 = model1.addRequest(createParsedRequest('Original'));
|
||||
req1.response.complete();
|
||||
|
||||
const branch1 = model1.getBranch(req1.id);
|
||||
const req1edited = model1.addRequest(createParsedRequest('Edited'), 'agent-1');
|
||||
branch1!.add(req1edited);
|
||||
|
||||
// Switch to the edited version
|
||||
branch1!.enable(req1edited);
|
||||
|
||||
const activeBranchIndex1 = branch1!.activeBranchIndex;
|
||||
const serialized = model1.toSerializable();
|
||||
|
||||
// Restore
|
||||
const model2 = new MutableChatModel(serialized);
|
||||
|
||||
const restoredBranch = model2.getBranch(serialized.requests[0].id);
|
||||
expect(restoredBranch).to.not.be.undefined;
|
||||
expect(restoredBranch!.activeBranchIndex).to.equal(activeBranchIndex1);
|
||||
});
|
||||
|
||||
it('should restore a simple session with hierarchy', () => {
|
||||
// Create serialized data with hierarchy
|
||||
const serializedData = {
|
||||
sessionId: 'simple-session',
|
||||
location: ChatAgentLocation.Panel,
|
||||
hierarchy: {
|
||||
rootBranchId: 'branch-root',
|
||||
branches: {
|
||||
'branch-root': {
|
||||
id: 'branch-root',
|
||||
items: [{ requestId: 'request-1' }],
|
||||
activeBranchIndex: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
requests: [
|
||||
{
|
||||
id: 'request-1',
|
||||
text: 'Hello'
|
||||
}
|
||||
],
|
||||
responses: [
|
||||
{
|
||||
id: 'response-1',
|
||||
requestId: 'request-1',
|
||||
isComplete: true,
|
||||
isError: false,
|
||||
content: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Should restore without errors
|
||||
const model = new MutableChatModel(serializedData);
|
||||
expect(model.getRequests()).to.have.lengthOf(1);
|
||||
expect(model.getRequests()[0].request.text).to.equal('Hello');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete round-trip with complex tree', () => {
|
||||
it('should serialize and restore a complex tree structure', () => {
|
||||
// Create a complex chat with multiple edits
|
||||
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
|
||||
// Level 1
|
||||
const req1 = model1.addRequest(createParsedRequest('Level 1 - Original'));
|
||||
req1.response.complete();
|
||||
|
||||
// Level 2
|
||||
const req2 = model1.addRequest(createParsedRequest('Level 2 - Original'));
|
||||
req2.response.complete();
|
||||
|
||||
// Edit Level 1
|
||||
const branch1 = model1.getBranch(req1.id);
|
||||
const req1edited = model1.addRequest(createParsedRequest('Level 1 - Edited'), 'agent-1');
|
||||
branch1!.add(req1edited);
|
||||
|
||||
// Add Level 2 alternative after edited Level 1
|
||||
const req2alt = model1.addRequest(createParsedRequest('Level 2 - Alternative'));
|
||||
req2alt.response.complete();
|
||||
|
||||
// Edit Level 2 alternative
|
||||
const branch2alt = model1.getBranch(req2alt.id);
|
||||
const req2altEdited = model1.addRequest(createParsedRequest('Level 2 - Alternative Edited'), 'agent-1');
|
||||
branch2alt!.add(req2altEdited);
|
||||
|
||||
const serialized = model1.toSerializable();
|
||||
|
||||
// Verify serialization
|
||||
expect(serialized.requests).to.have.lengthOf(5);
|
||||
expect(serialized.hierarchy).to.be.an('object');
|
||||
|
||||
// Restore
|
||||
const model2 = new MutableChatModel(serialized);
|
||||
|
||||
// Verify all requests are present
|
||||
const allRequests = model2.getAllRequests();
|
||||
expect(allRequests).to.have.lengthOf(5);
|
||||
|
||||
// Verify branch structure
|
||||
const restoredBranch1 = model2.getBranches()[0];
|
||||
expect(restoredBranch1.items).to.have.lengthOf(2); // Original + Edited
|
||||
|
||||
// Verify we can navigate the alternatives
|
||||
expect(restoredBranch1.items[0].element.request.text).to.equal('Level 1 - Original');
|
||||
expect(restoredBranch1.items[1].element.request.text).to.equal('Level 1 - Edited');
|
||||
});
|
||||
|
||||
it('should preserve all requests across multiple serialization/restoration cycles', () => {
|
||||
// Create initial model
|
||||
let model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const req1 = model.addRequest(createParsedRequest('Request 1'));
|
||||
req1.response.complete();
|
||||
|
||||
// Cycle 1
|
||||
let serialized = model.toSerializable();
|
||||
model = new MutableChatModel(serialized);
|
||||
|
||||
// Add more requests
|
||||
model.addRequest(createParsedRequest('Request 2'));
|
||||
|
||||
// Cycle 2
|
||||
serialized = model.toSerializable();
|
||||
model = new MutableChatModel(serialized);
|
||||
|
||||
// Add an edit
|
||||
const branch = model.getBranches()[0];
|
||||
const reqEdited = model.addRequest(createParsedRequest('Request 1 - Edited'), 'agent-1');
|
||||
branch.add(reqEdited);
|
||||
|
||||
// Final cycle
|
||||
serialized = model.toSerializable();
|
||||
const finalModel = new MutableChatModel(serialized);
|
||||
|
||||
// Verify all requests are preserved
|
||||
expect(finalModel.getBranches()[0].items).to.have.lengthOf(2);
|
||||
const allRequests = finalModel.getAllRequests();
|
||||
expect(allRequests).to.have.lengthOf(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ParsedChatRequest serialization', () => {
|
||||
it('should serialize and restore a simple text request', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
model.addRequest(createParsedRequest('Hello world'));
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
expect(serialized.requests[0].parsedRequest).to.not.be.undefined;
|
||||
expect(serialized.requests[0].parsedRequest!.parts).to.have.lengthOf(1);
|
||||
expect(serialized.requests[0].parsedRequest!.parts[0].kind).to.equal('text');
|
||||
const textPart = serialized.requests[0].parsedRequest!.parts[0] as SerializableTextPart;
|
||||
expect(textPart.text).to.equal('Hello world');
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredRequest = restored.getRequests()[0];
|
||||
expect(restoredRequest.message.parts).to.have.lengthOf(1);
|
||||
expect(restoredRequest.message.parts[0].kind).to.equal('text');
|
||||
expect(restoredRequest.message.parts[0].text).to.equal('Hello world');
|
||||
});
|
||||
|
||||
it('should serialize and restore a request with variable references', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const parsedRequest: ParsedChatRequest = {
|
||||
request: { text: 'Use #file and #selection' },
|
||||
parts: [
|
||||
new ParsedChatRequestTextPart({ start: 0, endExclusive: 4 }, 'Use '),
|
||||
(() => {
|
||||
const variablePart = new ParsedChatRequestVariablePart(
|
||||
{ start: 4, endExclusive: 9 },
|
||||
'file',
|
||||
undefined
|
||||
);
|
||||
variablePart.resolution = {
|
||||
variable: { id: 'file-var', name: 'file', description: 'Current file' },
|
||||
value: 'file content here'
|
||||
};
|
||||
return variablePart;
|
||||
})(),
|
||||
new ParsedChatRequestTextPart({ start: 9, endExclusive: 14 }, ' and '),
|
||||
(() => {
|
||||
const variablePart = new ParsedChatRequestVariablePart(
|
||||
{ start: 14, endExclusive: 24 },
|
||||
'selection',
|
||||
undefined
|
||||
);
|
||||
variablePart.resolution = {
|
||||
variable: { id: 'sel-var', name: 'selection', description: 'Selected text' },
|
||||
value: 'selected text'
|
||||
};
|
||||
return variablePart;
|
||||
})()
|
||||
],
|
||||
toolRequests: new Map(),
|
||||
variables: [
|
||||
{
|
||||
variable: { id: 'file-var', name: 'file', description: 'Current file' },
|
||||
value: 'file content here'
|
||||
},
|
||||
{
|
||||
variable: { id: 'sel-var', name: 'selection', description: 'Selected text' },
|
||||
value: 'selected text'
|
||||
}
|
||||
]
|
||||
};
|
||||
model.addRequest(parsedRequest);
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
expect(serialized.requests[0].parsedRequest).to.not.be.undefined;
|
||||
expect(serialized.requests[0].parsedRequest!.parts).to.have.lengthOf(4);
|
||||
expect(serialized.requests[0].parsedRequest!.parts[1].kind).to.equal('var');
|
||||
const varPart1 = serialized.requests[0].parsedRequest!.parts[1] as SerializableVariablePart;
|
||||
expect(varPart1.variableId).to.equal('file-var');
|
||||
expect(varPart1.variableName).to.equal('file');
|
||||
expect(varPart1.variableValue).to.equal('file content here');
|
||||
expect(varPart1.variableDescription).to.equal('Current file');
|
||||
expect(serialized.requests[0].parsedRequest!.variables).to.have.lengthOf(2);
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredRequest = restored.getRequests()[0];
|
||||
expect(restoredRequest.message.parts).to.have.lengthOf(4);
|
||||
expect(restoredRequest.message.parts[1].kind).to.equal('var');
|
||||
const varPart = restoredRequest.message.parts[1] as ParsedChatRequestVariablePart;
|
||||
expect(varPart.variableName).to.equal('file');
|
||||
expect(varPart.resolution?.variable.id).to.equal('file-var');
|
||||
expect(varPart.resolution?.value).to.equal('file content here');
|
||||
expect(restoredRequest.message.variables).to.have.lengthOf(2);
|
||||
expect(restoredRequest.message.variables[0].value).to.equal('file content here');
|
||||
expect(varPart.resolution?.variable.description).to.equal('Current file');
|
||||
});
|
||||
|
||||
it('should serialize and restore a request with agent references', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const parsedRequest: ParsedChatRequest = {
|
||||
request: { text: '@codeAgent help me' },
|
||||
parts: [
|
||||
new ParsedChatRequestAgentPart(
|
||||
{ start: 0, endExclusive: 10 },
|
||||
'code-agent-id',
|
||||
'codeAgent'
|
||||
),
|
||||
new ParsedChatRequestTextPart({ start: 10, endExclusive: 19 }, ' help me')
|
||||
],
|
||||
toolRequests: new Map(),
|
||||
variables: []
|
||||
};
|
||||
model.addRequest(parsedRequest, 'code-agent-id');
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
expect(serialized.requests[0].parsedRequest).to.not.be.undefined;
|
||||
expect(serialized.requests[0].parsedRequest!.parts[0].kind).to.equal('agent');
|
||||
const agentPart1 = serialized.requests[0].parsedRequest!.parts[0] as SerializableAgentPart;
|
||||
expect(agentPart1.agentId).to.equal('code-agent-id');
|
||||
expect(agentPart1.agentName).to.equal('codeAgent');
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredRequest = restored.getRequests()[0];
|
||||
expect(restoredRequest.message.parts[0].kind).to.equal('agent');
|
||||
const agentPart = restoredRequest.message.parts[0] as ParsedChatRequestAgentPart;
|
||||
expect(agentPart.agentId).to.equal('code-agent-id');
|
||||
expect(agentPart.agentName).to.equal('codeAgent');
|
||||
});
|
||||
|
||||
it('should serialize and restore a request with tool requests', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const toolRequest: ToolRequest = {
|
||||
id: 'tool-1',
|
||||
name: 'search',
|
||||
description: 'Search the web',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' }
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
handler: async () => 'search results'
|
||||
};
|
||||
const parsedRequest: ParsedChatRequest = {
|
||||
request: { text: 'Search for ~tool-1' },
|
||||
parts: [
|
||||
new ParsedChatRequestTextPart({ start: 0, endExclusive: 11 }, 'Search for '),
|
||||
new ParsedChatRequestFunctionPart({ start: 11, endExclusive: 18 }, toolRequest)
|
||||
],
|
||||
toolRequests: new Map([['tool-1', toolRequest]]),
|
||||
variables: []
|
||||
};
|
||||
model.addRequest(parsedRequest);
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
expect(serialized.requests[0].parsedRequest).to.not.be.undefined;
|
||||
expect(serialized.requests[0].parsedRequest!.parts[1].kind).to.equal('function');
|
||||
const funcPart1 = serialized.requests[0].parsedRequest!.parts[1] as SerializableFunctionPart;
|
||||
expect(funcPart1.toolRequestId).to.equal('tool-1');
|
||||
expect(serialized.requests[0].parsedRequest!.toolRequests).to.have.lengthOf(1);
|
||||
expect(serialized.requests[0].parsedRequest!.toolRequests[0].id).to.equal('tool-1');
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredRequest = restored.getRequests()[0];
|
||||
expect(restoredRequest.message.parts[1].kind).to.equal('function');
|
||||
const funcPart = restoredRequest.message.parts[1] as ParsedChatRequestFunctionPart;
|
||||
expect(funcPart.toolRequest.id).to.equal('tool-1');
|
||||
expect(restoredRequest.message.toolRequests.size).to.equal(1);
|
||||
expect(restoredRequest.message.toolRequests.get('tool-1')).to.not.be.undefined;
|
||||
});
|
||||
|
||||
it('should handle complex mixed requests with all part types', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const toolRequest: ToolRequest = {
|
||||
id: 'analyze-tool',
|
||||
name: 'analyze',
|
||||
parameters: { type: 'object', properties: {} },
|
||||
handler: async () => 'analysis'
|
||||
};
|
||||
const parsedRequest: ParsedChatRequest = {
|
||||
request: { text: '@agent analyze #file using ~analyze-tool' },
|
||||
parts: [
|
||||
new ParsedChatRequestAgentPart({ start: 0, endExclusive: 6 }, 'agent-1', 'agent'),
|
||||
new ParsedChatRequestTextPart({ start: 6, endExclusive: 15 }, ' analyze '),
|
||||
(() => {
|
||||
const varPart = new ParsedChatRequestVariablePart({ start: 15, endExclusive: 20 }, 'file', undefined);
|
||||
varPart.resolution = {
|
||||
variable: { id: 'f', name: 'file', description: 'File' },
|
||||
value: 'code.ts'
|
||||
};
|
||||
return varPart;
|
||||
})(),
|
||||
new ParsedChatRequestTextPart({ start: 20, endExclusive: 27 }, ' using '),
|
||||
new ParsedChatRequestFunctionPart({ start: 27, endExclusive: 41 }, toolRequest)
|
||||
],
|
||||
toolRequests: new Map([['analyze-tool', toolRequest]]),
|
||||
variables: [{ variable: { id: 'f', name: 'file', description: 'File' }, value: 'code.ts' }]
|
||||
};
|
||||
model.addRequest(parsedRequest, 'agent-1');
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
const parsedReqData = serialized.requests[0].parsedRequest!;
|
||||
expect(parsedReqData.parts).to.have.lengthOf(5);
|
||||
expect(parsedReqData.parts[0].kind).to.equal('agent');
|
||||
expect(parsedReqData.parts[2].kind).to.equal('var');
|
||||
expect(parsedReqData.parts[4].kind).to.equal('function');
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredMsg = restored.getRequests()[0].message;
|
||||
expect(restoredMsg.parts).to.have.lengthOf(5);
|
||||
expect(restoredMsg.parts[0].kind).to.equal('agent');
|
||||
expect(restoredMsg.parts[2].kind).to.equal('var');
|
||||
expect(restoredMsg.parts[4].kind).to.equal('function');
|
||||
expect(restoredMsg.toolRequests.size).to.equal(1);
|
||||
expect(restoredMsg.variables).to.have.lengthOf(1);
|
||||
});
|
||||
|
||||
it('should handle fallback for requests without parsedRequest data', () => {
|
||||
const serializedData = {
|
||||
sessionId: 'test-session',
|
||||
location: ChatAgentLocation.Panel,
|
||||
hierarchy: {
|
||||
rootBranchId: 'root',
|
||||
branches: {
|
||||
'root': {
|
||||
id: 'root',
|
||||
items: [{ requestId: 'req-1' }],
|
||||
activeBranchIndex: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
requests: [
|
||||
{
|
||||
id: 'req-1',
|
||||
text: 'Plain text without parsed data'
|
||||
}
|
||||
],
|
||||
responses: []
|
||||
};
|
||||
|
||||
const model = new MutableChatModel(serializedData);
|
||||
const request = model.getRequests()[0];
|
||||
expect(request.message.parts).to.have.lengthOf(1);
|
||||
expect(request.message.parts[0].kind).to.equal('text');
|
||||
expect(request.message.parts[0].text).to.equal('Plain text without parsed data');
|
||||
});
|
||||
|
||||
it('should create placeholder tool requests during deserialization', async () => {
|
||||
const toolRequest: ToolRequest = {
|
||||
id: 'my-tool',
|
||||
name: 'myTool',
|
||||
description: 'My test tool',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' }
|
||||
},
|
||||
required: ['query']
|
||||
},
|
||||
handler: async () => 'tool result'
|
||||
};
|
||||
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const parsedRequest: ParsedChatRequest = {
|
||||
request: { text: 'Use ~my-tool' },
|
||||
parts: [
|
||||
new ParsedChatRequestTextPart({ start: 0, endExclusive: 4 }, 'Use '),
|
||||
new ParsedChatRequestFunctionPart({ start: 4, endExclusive: 12 }, toolRequest)
|
||||
],
|
||||
toolRequests: new Map([['my-tool', toolRequest]]),
|
||||
variables: []
|
||||
};
|
||||
model.addRequest(parsedRequest);
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
expect(serialized.requests[0].parsedRequest).to.not.be.undefined;
|
||||
expect(serialized.requests[0].parsedRequest!.toolRequests).to.have.lengthOf(1);
|
||||
expect(serialized.requests[0].parsedRequest!.toolRequests[0].id).to.equal('my-tool');
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredRequest = restored.getRequests()[0];
|
||||
|
||||
// Verify placeholder was created
|
||||
expect(restoredRequest.message.parts[1].kind).to.equal('function');
|
||||
const funcPart = restoredRequest.message.parts[1] as ParsedChatRequestFunctionPart;
|
||||
expect(funcPart.toolRequest.id).to.equal('my-tool');
|
||||
|
||||
// Verify it's a placeholder (handler should throw about not being restored)
|
||||
try {
|
||||
await funcPart.toolRequest.handler('test-input');
|
||||
expect.fail('Should have thrown');
|
||||
} catch (error) {
|
||||
expect((error as Error).message).to.include('not yet restored');
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve variable arguments during serialization', () => {
|
||||
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
||||
const varPartWithArg = new ParsedChatRequestVariablePart(
|
||||
{ start: 0, endExclusive: 10 },
|
||||
'file',
|
||||
'main.ts'
|
||||
);
|
||||
varPartWithArg.resolution = {
|
||||
variable: { id: 'f', name: 'file', description: 'File variable' },
|
||||
arg: 'main.ts',
|
||||
value: 'file content of main.ts'
|
||||
};
|
||||
const parsedRequest: ParsedChatRequest = {
|
||||
request: { text: '#file:main.ts' },
|
||||
parts: [varPartWithArg],
|
||||
toolRequests: new Map(),
|
||||
variables: [varPartWithArg.resolution]
|
||||
};
|
||||
model.addRequest(parsedRequest);
|
||||
|
||||
const serialized = model.toSerializable();
|
||||
const serializedVar = serialized.requests[0].parsedRequest!.parts[0] as SerializableVariablePart;
|
||||
expect(serializedVar.variableId).to.equal('f');
|
||||
expect(serializedVar.variableArg).to.equal('main.ts');
|
||||
expect(serializedVar.variableValue).to.equal('file content of main.ts');
|
||||
expect(serializedVar.variableDescription).to.equal('File variable');
|
||||
|
||||
const restored = new MutableChatModel(serialized);
|
||||
const restoredPart = restored.getRequests()[0].message.parts[0] as ParsedChatRequestVariablePart;
|
||||
expect(restoredPart.variableArg).to.equal('main.ts');
|
||||
expect(restoredPart.resolution?.variable.id).to.equal('f');
|
||||
expect(restoredPart.resolution?.value).to.equal('file content of main.ts');
|
||||
expect(restoredPart.resolution?.variable.description).to.equal('File variable');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user