blob: 068da00a648db6406587dbcac0c1b32721a38620 [file]
// 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 Root from '../root/root.js';
import * as Host from './host.js';
const TEST_MODEL_ID = 'testModelId';
describe('AidaClient', () => {
it('adds no model temperature if there is no aidaTemperature query param', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaTemperature').returns(null);
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
});
stub.restore();
});
it('adds a model temperature', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaTemperature').returns('0.5');
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
options: {
temperature: 0.5,
},
});
stub.restore();
});
it('adds a model temperature of 0', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaTemperature').returns('0');
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
options: {
temperature: 0,
},
});
stub.restore();
});
it('adds no model temperature if the aidaTemperature query param cannot be parsed into a float', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaTemperature').returns('not a number');
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
});
stub.restore();
});
it('adds no model id if there is no aidaModelId query param', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaModelId').returns(null);
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
});
stub.restore();
});
it('adds a model id', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaModelId').returns(TEST_MODEL_ID);
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
options: {
model_id: TEST_MODEL_ID,
},
});
stub.restore();
});
it('adds a model id and temperature', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('aidaModelId').returns(TEST_MODEL_ID);
stub.withArgs('aidaTemperature').returns('0.5');
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
options: {
model_id: TEST_MODEL_ID,
temperature: 0.5,
},
});
stub.restore();
});
it('adds metadata to disallow logging', () => {
const stub = sinon.stub(Root.Runtime.Runtime, 'queryParam');
stub.withArgs('ci_disallowLogging').returns('true');
const request = Host.AidaClient.AidaClient.buildApiRequest('foo');
assert.deepStrictEqual(request, {
input: 'foo',
client: 'CHROME_DEVTOOLS',
metadata: {
disable_user_content_logging: true,
},
});
stub.restore();
});
async function getAllResults(provider: Host.AidaClient.AidaClient): Promise<Host.AidaClient.AidaResponse[]> {
const results = [];
for await (const result of provider.fetch('foo')) {
results.push(result);
}
return results;
}
it('handles chunked response', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation')
.callsFake(async (_, streamId, callback) => {
const response = JSON.stringify([
{textChunk: {text: 'hello '}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'brave '}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'new world!'}},
]);
let first = true;
for (const chunk of response.split(',{')) {
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, first ? chunk : ',{' + chunk);
first = false;
}
callback({statusCode: 200});
});
const provider = new Host.AidaClient.AidaClient();
const results = await getAllResults(provider);
assert.deepStrictEqual(results, [
{explanation: 'hello ', metadata: {rpcGlobalId: 123}},
{explanation: 'hello brave ', metadata: {rpcGlobalId: 123}},
{explanation: 'hello brave new world!', metadata: {rpcGlobalId: 123}},
]);
});
it('handles single square bracket as a chunk', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation')
.callsFake(async (_, streamId, callback) => {
const response = ['[', JSON.stringify({textChunk: {text: 'hello world'}, metadata: {rpcGlobalId: 123}}), ']'];
for (const chunk of response) {
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, chunk);
}
callback({statusCode: 200});
});
const provider = new Host.AidaClient.AidaClient();
const results = await getAllResults(provider);
assert.deepStrictEqual(results, [
{explanation: 'hello world', metadata: {rpcGlobalId: 123}},
]);
});
it('handles chunked response with multiple objects per chunk', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation')
.callsFake(async (_, streamId, callback) => {
const response = JSON.stringify([
{textChunk: {text: 'Friends, Romans, countrymen, lend me your ears;\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'I come to bury Caesar, not to praise him.\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'The evil that men do lives after them;\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'The good is oft interred with their bones;\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'So let it be with Caesar. The noble Brutus\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'Hath told you Caesar was ambitious:\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'If it were so, it was a grievous fault,\n'}, metadata: {rpcGlobalId: 123}},
{textChunk: {text: 'And grievously hath Caesar answer’d it.\n'}, metadata: {rpcGlobalId: 123}},
]);
const chunks = response.split(',{');
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, chunks[0] + ',{' + chunks[1]);
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, ',{' + chunks[2] + ',{' + chunks[3] + ',{' + chunks[4]);
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, ',{' + chunks[5]);
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, ',{' + chunks[6] + ',{' + chunks[7]);
callback({statusCode: 200});
});
const provider = new Host.AidaClient.AidaClient();
const results = await getAllResults(provider);
assert.deepStrictEqual(results, [
{
explanation: 'Friends, Romans, countrymen, lend me your ears;\n' +
'I come to bury Caesar, not to praise him.\n',
metadata: {rpcGlobalId: 123},
},
{
explanation: 'Friends, Romans, countrymen, lend me your ears;\n' +
'I come to bury Caesar, not to praise him.\n' +
'The evil that men do lives after them;\n' +
'The good is oft interred with their bones;\n' +
'So let it be with Caesar. The noble Brutus\n',
metadata: {rpcGlobalId: 123},
},
{
explanation: 'Friends, Romans, countrymen, lend me your ears;\n' +
'I come to bury Caesar, not to praise him.\n' +
'The evil that men do lives after them;\n' +
'The good is oft interred with their bones;\n' +
'So let it be with Caesar. The noble Brutus\n' +
'Hath told you Caesar was ambitious:\n',
metadata: {rpcGlobalId: 123},
},
{
explanation: 'Friends, Romans, countrymen, lend me your ears;\n' +
'I come to bury Caesar, not to praise him.\n' +
'The evil that men do lives after them;\n' +
'The good is oft interred with their bones;\n' +
'So let it be with Caesar. The noble Brutus\n' +
'Hath told you Caesar was ambitious:\n' +
'If it were so, it was a grievous fault,\n' +
'And grievously hath Caesar answer’d it.\n',
metadata: {rpcGlobalId: 123},
},
]);
});
it('handles subsequent code chunks', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation')
.callsFake(async (_, streamId, callback) => {
const response = JSON.stringify([
{textChunk: {text: 'hello '}},
{codeChunk: {code: 'brave '}},
{codeChunk: {code: 'new World()'}},
]);
for (const chunk of response.split(',')) {
await new Promise(resolve => setTimeout(resolve, 0));
Host.ResourceLoader.streamWrite(streamId, chunk);
}
callback({statusCode: 200});
});
const provider = new Host.AidaClient.AidaClient();
const results = (await getAllResults(provider)).map(r => r.explanation);
assert.deepStrictEqual(
results, ['hello ', 'hello \n`````\nbrave \n`````\n', 'hello \n`````\nbrave new World()\n`````\n']);
});
it('throws a readable error on 403', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation').callsArgWith(2, {
'statusCode': 403,
});
const provider = new Host.AidaClient.AidaClient();
try {
await getAllResults(provider);
expect.fail('provider.fetch did not throw');
} catch (err) {
expect(err.message).equals('Server responded: permission denied');
}
});
it('throws an error for other codes', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation').callsArgWith(2, {
'statusCode': 418,
});
const provider = new Host.AidaClient.AidaClient();
try {
await getAllResults(provider);
expect.fail('provider.fetch did not throw');
} catch (err) {
expect(err.message).equals('Request failed: {"statusCode":418}');
}
});
it('throws an error with all details for other failures', async () => {
sinon.stub(Host.InspectorFrontendHost.InspectorFrontendHostInstance, 'doAidaConversation').callsArgWith(2, {
'error': 'Cannot get OAuth credentials',
'detail': '{\'@type\': \'type.googleapis.com/google.rpc.DebugInfo\', \'detail\': \'DETAILS\'}',
});
const provider = new Host.AidaClient.AidaClient();
try {
await getAllResults(provider);
expect.fail('provider.fetch did not throw');
} catch (err) {
expect(err.message)
.equals(
'Cannot send request: Cannot get OAuth credentials {\'@type\': \'type.googleapis.com/google.rpc.DebugInfo\', \'detail\': \'DETAILS\'}');
}
});
});