2025-06-15 22:41:32 -07:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
2025-11-11 20:06:43 -08:00
|
|
|
import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest';
|
2025-06-15 22:41:32 -07:00
|
|
|
import { CodeAssistServer } from './server.js';
|
|
|
|
|
import { OAuth2Client } from 'google-auth-library';
|
2025-12-16 09:34:05 -08:00
|
|
|
import { UserTierId, ActionStatus } from './types.js';
|
|
|
|
|
import { FinishReason } from '@google/genai';
|
2026-02-17 12:32:30 -05:00
|
|
|
import { LlmRole } from '../telemetry/types.js';
|
2026-03-04 14:27:47 -05:00
|
|
|
import { logInvalidChunk } from '../telemetry/loggers.js';
|
|
|
|
|
import { makeFakeConfig } from '../test-utils/config.js';
|
2025-06-15 22:41:32 -07:00
|
|
|
|
|
|
|
|
vi.mock('google-auth-library');
|
2026-03-04 14:27:47 -05:00
|
|
|
vi.mock('../telemetry/loggers.js', () => ({
|
|
|
|
|
logBillingEvent: vi.fn(),
|
|
|
|
|
logInvalidChunk: vi.fn(),
|
|
|
|
|
}));
|
2025-06-15 22:41:32 -07:00
|
|
|
|
2025-12-16 09:34:05 -08:00
|
|
|
function createTestServer(headers: Record<string, string> = {}) {
|
|
|
|
|
const mockRequest = vi.fn();
|
|
|
|
|
const client = { request: mockRequest } as unknown as OAuth2Client;
|
|
|
|
|
const server = new CodeAssistServer(
|
|
|
|
|
client,
|
|
|
|
|
'test-project',
|
|
|
|
|
{ headers },
|
|
|
|
|
'test-session',
|
|
|
|
|
UserTierId.FREE,
|
|
|
|
|
);
|
|
|
|
|
return { server, mockRequest, client };
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-15 22:41:32 -07:00
|
|
|
describe('CodeAssistServer', () => {
|
2025-08-07 19:58:18 -04:00
|
|
|
beforeEach(() => {
|
|
|
|
|
vi.resetAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:41:32 -07:00
|
|
|
it('should be able to be constructed', () => {
|
|
|
|
|
const auth = new OAuth2Client();
|
2025-08-01 14:37:56 -05:00
|
|
|
const server = new CodeAssistServer(
|
|
|
|
|
auth,
|
|
|
|
|
'test-project',
|
|
|
|
|
{},
|
|
|
|
|
'test-session',
|
|
|
|
|
UserTierId.FREE,
|
|
|
|
|
);
|
2025-06-15 22:41:32 -07:00
|
|
|
expect(server).toBeInstanceOf(CodeAssistServer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call the generateContent endpoint', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server, mockRequest } = createTestServer({
|
|
|
|
|
'x-custom-header': 'test-value',
|
|
|
|
|
});
|
2025-11-11 20:06:43 -08:00
|
|
|
const mockResponseData = {
|
2025-06-15 22:41:32 -07:00
|
|
|
response: {
|
|
|
|
|
candidates: [
|
|
|
|
|
{
|
|
|
|
|
index: 0,
|
|
|
|
|
content: {
|
|
|
|
|
role: 'model',
|
|
|
|
|
parts: [{ text: 'response' }],
|
|
|
|
|
},
|
2025-12-16 09:34:05 -08:00
|
|
|
finishReason: FinishReason.STOP,
|
2025-06-15 22:41:32 -07:00
|
|
|
safetyRatings: [],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
};
|
2025-11-11 20:06:43 -08:00
|
|
|
mockRequest.mockResolvedValue({ data: mockResponseData });
|
2025-06-15 22:41:32 -07:00
|
|
|
|
2025-08-01 14:37:56 -05:00
|
|
|
const response = await server.generateContent(
|
|
|
|
|
{
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
},
|
|
|
|
|
'user-prompt-id',
|
2026-02-17 12:32:30 -05:00
|
|
|
LlmRole.MAIN,
|
2025-08-01 14:37:56 -05:00
|
|
|
);
|
2025-06-15 22:41:32 -07:00
|
|
|
|
2026-02-27 09:26:53 -05:00
|
|
|
expect(mockRequest).toHaveBeenCalledWith({
|
|
|
|
|
url: expect.stringContaining(':generateContent'),
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'x-custom-header': 'test-value',
|
|
|
|
|
},
|
|
|
|
|
responseType: 'json',
|
|
|
|
|
body: expect.any(String),
|
|
|
|
|
signal: undefined,
|
|
|
|
|
retryConfig: {
|
2026-02-27 13:04:43 -05:00
|
|
|
retryDelay: 1000,
|
2026-02-27 09:26:53 -05:00
|
|
|
retry: 3,
|
|
|
|
|
noResponseRetries: 3,
|
|
|
|
|
statusCodesToRetry: [
|
|
|
|
|
[429, 429],
|
|
|
|
|
[499, 499],
|
|
|
|
|
[500, 599],
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
});
|
2025-11-11 20:06:43 -08:00
|
|
|
|
|
|
|
|
const requestBody = JSON.parse(mockRequest.mock.calls[0][0].body);
|
|
|
|
|
expect(requestBody.user_prompt_id).toBe('user-prompt-id');
|
|
|
|
|
expect(requestBody.project).toBe('test-project');
|
|
|
|
|
|
2025-06-15 22:41:32 -07:00
|
|
|
expect(response.candidates?.[0]?.content?.parts?.[0]?.text).toBe(
|
|
|
|
|
'response',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-12-16 09:34:05 -08:00
|
|
|
it('should detect error in generateContent response', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
const mockResponseData = {
|
|
|
|
|
traceId: 'test-trace-id',
|
|
|
|
|
response: {
|
|
|
|
|
candidates: [
|
|
|
|
|
{
|
|
|
|
|
index: 0,
|
|
|
|
|
content: {
|
|
|
|
|
role: 'model',
|
2025-12-17 15:12:59 -08:00
|
|
|
parts: [
|
|
|
|
|
{ text: 'response' },
|
2026-03-04 18:58:39 +00:00
|
|
|
{ functionCall: { name: 'replace', args: {} } },
|
2025-12-17 15:12:59 -08:00
|
|
|
],
|
2025-12-16 09:34:05 -08:00
|
|
|
},
|
|
|
|
|
finishReason: FinishReason.SAFETY,
|
|
|
|
|
safetyRatings: [],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockResponseData });
|
|
|
|
|
|
|
|
|
|
const recordConversationOfferedSpy = vi.spyOn(
|
|
|
|
|
server,
|
|
|
|
|
'recordConversationOffered',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await server.generateContent(
|
|
|
|
|
{
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
},
|
|
|
|
|
'user-prompt-id',
|
2026-02-17 12:32:30 -05:00
|
|
|
LlmRole.MAIN,
|
2025-12-16 09:34:05 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(recordConversationOfferedSpy).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
status: ActionStatus.ACTION_STATUS_ERROR_UNKNOWN,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should record conversation offered on successful generateContent', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
const mockResponseData = {
|
|
|
|
|
traceId: 'test-trace-id',
|
|
|
|
|
response: {
|
|
|
|
|
candidates: [
|
|
|
|
|
{
|
|
|
|
|
index: 0,
|
|
|
|
|
content: {
|
|
|
|
|
role: 'model',
|
2025-12-17 15:12:59 -08:00
|
|
|
parts: [
|
|
|
|
|
{ text: 'response' },
|
2026-03-04 18:58:39 +00:00
|
|
|
{ functionCall: { name: 'replace', args: {} } },
|
2025-12-17 15:12:59 -08:00
|
|
|
],
|
2025-12-16 09:34:05 -08:00
|
|
|
},
|
|
|
|
|
finishReason: FinishReason.STOP,
|
|
|
|
|
safetyRatings: [],
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
sdkHttpResponse: {
|
|
|
|
|
responseInternal: {
|
|
|
|
|
ok: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockResponseData });
|
|
|
|
|
vi.spyOn(server, 'recordCodeAssistMetrics').mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
await server.generateContent(
|
|
|
|
|
{
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
},
|
|
|
|
|
'user-prompt-id',
|
2026-02-17 12:32:30 -05:00
|
|
|
LlmRole.MAIN,
|
2025-12-16 09:34:05 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(server.recordCodeAssistMetrics).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
metrics: expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
conversationOffered: expect.objectContaining({
|
|
|
|
|
traceId: 'test-trace-id',
|
|
|
|
|
status: ActionStatus.ACTION_STATUS_NO_ERROR,
|
|
|
|
|
streamingLatency: expect.objectContaining({
|
|
|
|
|
totalLatency: expect.stringMatching(/\d+s/),
|
|
|
|
|
firstMessageLatency: expect.stringMatching(/\d+s/),
|
|
|
|
|
}),
|
|
|
|
|
}),
|
2025-12-22 12:04:06 -08:00
|
|
|
timestamp: expect.stringMatching(
|
|
|
|
|
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/,
|
|
|
|
|
),
|
2025-12-16 09:34:05 -08:00
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should record conversation offered on generateContentStream', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
const mockStream = new Readable({ read() {} });
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockStream });
|
|
|
|
|
|
|
|
|
|
vi.spyOn(server, 'recordCodeAssistMetrics').mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
const stream = await server.generateContentStream(
|
|
|
|
|
{
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
},
|
|
|
|
|
'user-prompt-id',
|
2026-02-17 12:32:30 -05:00
|
|
|
LlmRole.MAIN,
|
2025-12-16 09:34:05 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const mockResponseData = {
|
|
|
|
|
traceId: 'stream-trace-id',
|
|
|
|
|
response: {
|
2025-12-17 15:12:59 -08:00
|
|
|
candidates: [
|
|
|
|
|
{
|
|
|
|
|
content: {
|
|
|
|
|
parts: [
|
|
|
|
|
{ text: 'chunk' },
|
2026-03-04 18:58:39 +00:00
|
|
|
{ functionCall: { name: 'replace', args: {} } },
|
2025-12-17 15:12:59 -08:00
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-12-16 09:34:05 -08:00
|
|
|
sdkHttpResponse: {
|
|
|
|
|
responseInternal: {
|
|
|
|
|
ok: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
mockStream.push('data: ' + JSON.stringify(mockResponseData) + '\n\n');
|
|
|
|
|
mockStream.push(null);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
for await (const _ of stream) {
|
|
|
|
|
// Consume stream
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(server.recordCodeAssistMetrics).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
metrics: expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
conversationOffered: expect.objectContaining({
|
|
|
|
|
traceId: 'stream-trace-id',
|
|
|
|
|
}),
|
2025-12-22 12:04:06 -08:00
|
|
|
timestamp: expect.stringMatching(
|
|
|
|
|
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/,
|
|
|
|
|
),
|
2025-12-16 09:34:05 -08:00
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should record conversation interaction', async () => {
|
|
|
|
|
const { server } = createTestServer();
|
|
|
|
|
vi.spyOn(server, 'recordCodeAssistMetrics').mockResolvedValue(undefined);
|
|
|
|
|
|
|
|
|
|
const interaction = {
|
|
|
|
|
traceId: 'test-trace-id',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await server.recordConversationInteraction(interaction);
|
|
|
|
|
|
|
|
|
|
expect(server.recordCodeAssistMetrics).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
project: 'test-project',
|
|
|
|
|
metrics: expect.arrayContaining([
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
conversationInteraction: interaction,
|
2025-12-22 12:04:06 -08:00
|
|
|
timestamp: expect.stringMatching(
|
|
|
|
|
/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/,
|
|
|
|
|
),
|
2025-12-16 09:34:05 -08:00
|
|
|
}),
|
|
|
|
|
]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call recordCodeAssistMetrics endpoint', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
mockRequest.mockResolvedValue({ data: {} });
|
|
|
|
|
|
|
|
|
|
const req = {
|
|
|
|
|
project: 'test-project',
|
|
|
|
|
metrics: [],
|
|
|
|
|
};
|
|
|
|
|
await server.recordCodeAssistMetrics(req);
|
|
|
|
|
|
|
|
|
|
expect(mockRequest).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
url: expect.stringContaining(':recordCodeAssistMetrics'),
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: expect.any(String),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-11 20:06:43 -08:00
|
|
|
describe('getMethodUrl', () => {
|
|
|
|
|
const originalEnv = process.env;
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
// Reset the environment variables to their original state
|
|
|
|
|
process.env = { ...originalEnv };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
// Restore the original environment variables
|
|
|
|
|
process.env = originalEnv;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should construct the default URL correctly', () => {
|
|
|
|
|
const server = new CodeAssistServer({} as never);
|
|
|
|
|
const url = server.getMethodUrl('testMethod');
|
|
|
|
|
expect(url).toBe(
|
|
|
|
|
'https://cloudcode-pa.googleapis.com/v1internal:testMethod',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use the CODE_ASSIST_ENDPOINT environment variable if set', () => {
|
|
|
|
|
process.env['CODE_ASSIST_ENDPOINT'] = 'https://custom-endpoint.com';
|
|
|
|
|
const server = new CodeAssistServer({} as never);
|
|
|
|
|
const url = server.getMethodUrl('testMethod');
|
|
|
|
|
expect(url).toBe('https://custom-endpoint.com/v1internal:testMethod');
|
|
|
|
|
});
|
2026-01-30 11:12:04 -08:00
|
|
|
|
|
|
|
|
it('should use the CODE_ASSIST_API_VERSION environment variable if set', () => {
|
|
|
|
|
process.env['CODE_ASSIST_API_VERSION'] = 'v2beta';
|
|
|
|
|
const server = new CodeAssistServer({} as never);
|
|
|
|
|
const url = server.getMethodUrl('testMethod');
|
|
|
|
|
expect(url).toBe('https://cloudcode-pa.googleapis.com/v2beta:testMethod');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use default value if CODE_ASSIST_API_VERSION env var is empty', () => {
|
|
|
|
|
process.env['CODE_ASSIST_API_VERSION'] = '';
|
|
|
|
|
const server = new CodeAssistServer({} as never);
|
|
|
|
|
const url = server.getMethodUrl('testMethod');
|
|
|
|
|
expect(url).toBe(
|
|
|
|
|
'https://cloudcode-pa.googleapis.com/v1internal:testMethod',
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-11-11 20:06:43 -08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call the generateContentStream endpoint and parse SSE', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server, mockRequest } = createTestServer();
|
2025-11-11 20:06:43 -08:00
|
|
|
|
|
|
|
|
// Create a mock readable stream
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const mockResponseData1 = {
|
|
|
|
|
response: { candidates: [{ content: { parts: [{ text: 'Hello' }] } }] },
|
|
|
|
|
};
|
|
|
|
|
const mockResponseData2 = {
|
|
|
|
|
response: { candidates: [{ content: { parts: [{ text: ' World' }] } }] },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockStream });
|
2025-06-15 22:41:32 -07:00
|
|
|
|
2025-08-01 14:37:56 -05:00
|
|
|
const stream = await server.generateContentStream(
|
|
|
|
|
{
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
},
|
|
|
|
|
'user-prompt-id',
|
2026-02-17 12:32:30 -05:00
|
|
|
LlmRole.MAIN,
|
2025-08-01 14:37:56 -05:00
|
|
|
);
|
2025-06-15 22:41:32 -07:00
|
|
|
|
2025-11-11 20:06:43 -08:00
|
|
|
// Push SSE data to the stream
|
|
|
|
|
// Use setTimeout to ensure the stream processing has started
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
mockStream.push('data: ' + JSON.stringify(mockResponseData1) + '\n\n');
|
|
|
|
|
mockStream.push('id: 123\n'); // Should be ignored
|
|
|
|
|
mockStream.push('data: ' + JSON.stringify(mockResponseData2) + '\n\n');
|
|
|
|
|
mockStream.push(null); // End the stream
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
const results = [];
|
2025-06-15 22:41:32 -07:00
|
|
|
for await (const res of stream) {
|
2025-11-11 20:06:43 -08:00
|
|
|
results.push(res);
|
2025-06-15 22:41:32 -07:00
|
|
|
}
|
2025-11-11 20:06:43 -08:00
|
|
|
|
2026-02-27 09:26:53 -05:00
|
|
|
expect(mockRequest).toHaveBeenCalledWith({
|
|
|
|
|
url: expect.stringContaining(':streamGenerateContent'),
|
|
|
|
|
method: 'POST',
|
|
|
|
|
params: { alt: 'sse' },
|
|
|
|
|
responseType: 'stream',
|
|
|
|
|
body: expect.any(String),
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
signal: undefined,
|
2026-02-27 13:04:43 -05:00
|
|
|
retry: false,
|
2026-02-27 09:26:53 -05:00
|
|
|
});
|
2025-11-11 20:06:43 -08:00
|
|
|
|
|
|
|
|
expect(results).toHaveLength(2);
|
|
|
|
|
expect(results[0].candidates?.[0].content?.parts?.[0].text).toBe('Hello');
|
|
|
|
|
expect(results[1].candidates?.[0].content?.parts?.[0].text).toBe(' World');
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-20 20:03:57 +00:00
|
|
|
it('should handle Web ReadableStream in generateContentStream', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
|
|
|
|
|
// Create a mock Web ReadableStream
|
|
|
|
|
const mockWebStream = new ReadableStream({
|
|
|
|
|
start(controller) {
|
|
|
|
|
const mockResponseData = {
|
|
|
|
|
response: {
|
|
|
|
|
candidates: [{ content: { parts: [{ text: 'Hello Web' }] } }],
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
controller.enqueue(
|
|
|
|
|
new TextEncoder().encode(
|
|
|
|
|
'data: ' + JSON.stringify(mockResponseData) + '\n\n',
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
controller.close();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockWebStream });
|
|
|
|
|
|
|
|
|
|
const stream = await server.generateContentStream(
|
|
|
|
|
{
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
},
|
|
|
|
|
'user-prompt-id',
|
|
|
|
|
LlmRole.MAIN,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
for await (const res of stream) {
|
|
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(results).toHaveLength(1);
|
|
|
|
|
expect(results[0].candidates?.[0].content?.parts?.[0].text).toBe(
|
|
|
|
|
'Hello Web',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-11 20:06:43 -08:00
|
|
|
it('should ignore malformed SSE data', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server, mockRequest } = createTestServer();
|
2025-11-11 20:06:43 -08:00
|
|
|
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockStream });
|
|
|
|
|
|
|
|
|
|
const stream = await server.requestStreamingPost('testStream', {});
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
mockStream.push('this is a malformed line\n');
|
|
|
|
|
mockStream.push(null);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
for await (const res of stream) {
|
|
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
expect(results).toHaveLength(0);
|
2025-06-15 22:41:32 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should call the onboardUser endpoint', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
|
|
|
|
|
2025-06-15 22:41:32 -07:00
|
|
|
const mockResponse = {
|
|
|
|
|
name: 'operations/123',
|
|
|
|
|
done: true,
|
|
|
|
|
};
|
2025-06-30 15:41:14 -07:00
|
|
|
vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse);
|
2025-06-15 22:41:32 -07:00
|
|
|
|
|
|
|
|
const response = await server.onboardUser({
|
|
|
|
|
tierId: 'test-tier',
|
|
|
|
|
cloudaicompanionProject: 'test-project',
|
|
|
|
|
metadata: {},
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-30 15:41:14 -07:00
|
|
|
expect(server.requestPost).toHaveBeenCalledWith(
|
2025-06-15 22:41:32 -07:00
|
|
|
'onboardUser',
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
);
|
|
|
|
|
expect(response.name).toBe('operations/123');
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-07 00:38:59 +05:30
|
|
|
it('should call the getOperation endpoint', async () => {
|
|
|
|
|
const { server } = createTestServer();
|
|
|
|
|
|
|
|
|
|
const mockResponse = {
|
|
|
|
|
name: 'operations/123',
|
|
|
|
|
done: true,
|
|
|
|
|
response: {
|
|
|
|
|
cloudaicompanionProject: {
|
|
|
|
|
id: 'test-project',
|
|
|
|
|
name: 'projects/test-project',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
vi.spyOn(server, 'requestGetOperation').mockResolvedValue(mockResponse);
|
|
|
|
|
|
|
|
|
|
const response = await server.getOperation('operations/123');
|
|
|
|
|
|
|
|
|
|
expect(server.requestGetOperation).toHaveBeenCalledWith('operations/123');
|
|
|
|
|
expect(response.name).toBe('operations/123');
|
|
|
|
|
expect(response.response?.cloudaicompanionProject?.id).toBe('test-project');
|
|
|
|
|
expect(response.response?.cloudaicompanionProject?.name).toBe(
|
|
|
|
|
'projects/test-project',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-15 22:41:32 -07:00
|
|
|
it('should call the loadCodeAssist endpoint', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-06-15 22:41:32 -07:00
|
|
|
const mockResponse = {
|
2025-07-18 07:57:54 +09:00
|
|
|
currentTier: {
|
|
|
|
|
id: UserTierId.FREE,
|
|
|
|
|
name: 'Free',
|
|
|
|
|
description: 'free tier',
|
|
|
|
|
},
|
|
|
|
|
allowedTiers: [],
|
|
|
|
|
ineligibleTiers: [],
|
|
|
|
|
cloudaicompanionProject: 'projects/test',
|
2025-06-15 22:41:32 -07:00
|
|
|
};
|
2025-06-30 15:41:14 -07:00
|
|
|
vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse);
|
2025-06-15 22:41:32 -07:00
|
|
|
|
|
|
|
|
const response = await server.loadCodeAssist({
|
|
|
|
|
metadata: {},
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-30 15:41:14 -07:00
|
|
|
expect(server.requestPost).toHaveBeenCalledWith(
|
2025-06-15 22:41:32 -07:00
|
|
|
'loadCodeAssist',
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
);
|
2025-07-18 07:57:54 +09:00
|
|
|
expect(response).toEqual(mockResponse);
|
2025-06-15 22:41:32 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return 0 for countTokens', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-06-18 10:29:42 -07:00
|
|
|
const mockResponse = {
|
|
|
|
|
totalTokens: 100,
|
|
|
|
|
};
|
2025-06-30 15:41:14 -07:00
|
|
|
vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse);
|
2025-06-18 10:29:42 -07:00
|
|
|
|
2025-06-15 22:41:32 -07:00
|
|
|
const response = await server.countTokens({
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
});
|
2025-06-18 10:29:42 -07:00
|
|
|
expect(response.totalTokens).toBe(100);
|
2025-06-15 22:41:32 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw an error for embedContent', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-06-15 22:41:32 -07:00
|
|
|
await expect(
|
|
|
|
|
server.embedContent({
|
|
|
|
|
model: 'test-model',
|
|
|
|
|
contents: [{ role: 'user', parts: [{ text: 'request' }] }],
|
|
|
|
|
}),
|
|
|
|
|
).rejects.toThrow();
|
|
|
|
|
});
|
2025-09-05 19:55:33 -07:00
|
|
|
|
|
|
|
|
it('should handle VPC-SC errors when calling loadCodeAssist', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-09-05 19:55:33 -07:00
|
|
|
const mockVpcScError = {
|
|
|
|
|
response: {
|
|
|
|
|
data: {
|
|
|
|
|
error: {
|
|
|
|
|
details: [
|
|
|
|
|
{
|
|
|
|
|
reason: 'SECURITY_POLICY_VIOLATED',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
vi.spyOn(server, 'requestPost').mockRejectedValue(mockVpcScError);
|
|
|
|
|
|
|
|
|
|
const response = await server.loadCodeAssist({
|
|
|
|
|
metadata: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(server.requestPost).toHaveBeenCalledWith(
|
|
|
|
|
'loadCodeAssist',
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
);
|
|
|
|
|
expect(response).toEqual({
|
|
|
|
|
currentTier: { id: UserTierId.STANDARD },
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-11-03 13:51:22 -08:00
|
|
|
|
2025-11-11 20:06:43 -08:00
|
|
|
it('should re-throw non-VPC-SC errors from loadCodeAssist', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-11-11 20:06:43 -08:00
|
|
|
const genericError = new Error('Something else went wrong');
|
|
|
|
|
vi.spyOn(server, 'requestPost').mockRejectedValue(genericError);
|
|
|
|
|
|
|
|
|
|
await expect(server.loadCodeAssist({ metadata: {} })).rejects.toThrow(
|
|
|
|
|
'Something else went wrong',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(server.requestPost).toHaveBeenCalledWith(
|
|
|
|
|
'loadCodeAssist',
|
|
|
|
|
expect.any(Object),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-03 13:51:22 -08:00
|
|
|
it('should call the listExperiments endpoint with metadata', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-11-03 13:51:22 -08:00
|
|
|
const mockResponse = {
|
|
|
|
|
experiments: [],
|
|
|
|
|
};
|
|
|
|
|
vi.spyOn(server, 'requestPost').mockResolvedValue(mockResponse);
|
|
|
|
|
|
|
|
|
|
const metadata = {
|
2025-11-04 15:09:53 -08:00
|
|
|
ideVersion: 'v0.1.0',
|
2025-11-03 13:51:22 -08:00
|
|
|
};
|
|
|
|
|
const response = await server.listExperiments(metadata);
|
|
|
|
|
|
|
|
|
|
expect(server.requestPost).toHaveBeenCalledWith('listExperiments', {
|
|
|
|
|
project: 'test-project',
|
2025-11-04 15:09:53 -08:00
|
|
|
metadata: { ideVersion: 'v0.1.0', duetProject: 'test-project' },
|
2025-11-03 13:51:22 -08:00
|
|
|
});
|
|
|
|
|
expect(response).toEqual(mockResponse);
|
|
|
|
|
});
|
2025-11-26 20:21:33 -05:00
|
|
|
|
|
|
|
|
it('should call the retrieveUserQuota endpoint', async () => {
|
2025-12-16 09:34:05 -08:00
|
|
|
const { server } = createTestServer();
|
2025-11-26 20:21:33 -05:00
|
|
|
const mockResponse = {
|
|
|
|
|
buckets: [
|
|
|
|
|
{
|
|
|
|
|
modelId: 'gemini-2.5-pro',
|
|
|
|
|
tokenType: 'REQUESTS',
|
|
|
|
|
remainingFraction: 0.75,
|
|
|
|
|
resetTime: '2025-10-22T16:01:15Z',
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
};
|
|
|
|
|
const requestPostSpy = vi
|
|
|
|
|
.spyOn(server, 'requestPost')
|
|
|
|
|
.mockResolvedValue(mockResponse);
|
|
|
|
|
|
|
|
|
|
const req = {
|
|
|
|
|
project: 'projects/my-cloudcode-project',
|
|
|
|
|
userAgent: 'CloudCodePlugin/1.0 (gaghosh)',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const response = await server.retrieveUserQuota(req);
|
|
|
|
|
|
|
|
|
|
expect(requestPostSpy).toHaveBeenCalledWith('retrieveUserQuota', req);
|
|
|
|
|
expect(response).toEqual(mockResponse);
|
|
|
|
|
});
|
2026-03-04 14:27:47 -05:00
|
|
|
|
|
|
|
|
describe('robustness testing', () => {
|
|
|
|
|
it('should not crash on random error objects in loadCodeAssist (isVpcScAffectedUser)', async () => {
|
|
|
|
|
const { server } = createTestServer();
|
|
|
|
|
const errors = [
|
|
|
|
|
null,
|
|
|
|
|
undefined,
|
|
|
|
|
'string error',
|
|
|
|
|
123,
|
|
|
|
|
{ some: 'object' },
|
|
|
|
|
new Error('standard error'),
|
|
|
|
|
{ response: {} },
|
|
|
|
|
{ response: { data: {} } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const err of errors) {
|
|
|
|
|
vi.spyOn(server, 'requestPost').mockRejectedValueOnce(err);
|
|
|
|
|
try {
|
|
|
|
|
await server.loadCodeAssist({ metadata: {} });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
expect(e).toBe(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle randomly fragmented SSE streams gracefully', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
|
|
|
|
|
const fragmentedCases = [
|
|
|
|
|
{
|
|
|
|
|
chunks: ['d', 'ata: {"foo":', ' "bar"}\n\n'],
|
|
|
|
|
expected: [{ foo: 'bar' }],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
chunks: ['data: {"foo": "bar"}\n', '\n'],
|
|
|
|
|
expected: [{ foo: 'bar' }],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
chunks: ['data: ', '{"foo": "bar"}', '\n\n'],
|
|
|
|
|
expected: [{ foo: 'bar' }],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
chunks: ['data: {"foo": "bar"}\n\n', 'data: {"baz": 1}\n\n'],
|
|
|
|
|
expected: [{ foo: 'bar' }, { baz: 1 }],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const { chunks, expected } of fragmentedCases) {
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {
|
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
|
this.push(chunk);
|
|
|
|
|
}
|
|
|
|
|
this.push(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
mockRequest.mockResolvedValueOnce({ data: mockStream });
|
|
|
|
|
|
|
|
|
|
const stream = await server.requestStreamingPost('testStream', {});
|
|
|
|
|
const results = [];
|
|
|
|
|
for await (const res of stream) {
|
|
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
expect(results).toEqual(expected);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should correctly parse valid JSON split across multiple data lines', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
const jsonObj = {
|
|
|
|
|
complex: { structure: [1, 2, 3] },
|
|
|
|
|
bool: true,
|
|
|
|
|
str: 'value',
|
|
|
|
|
};
|
|
|
|
|
const jsonString = JSON.stringify(jsonObj, null, 2);
|
|
|
|
|
const lines = jsonString.split('\n');
|
|
|
|
|
const ssePayload = lines.map((line) => `data: ${line}\n`).join('') + '\n';
|
|
|
|
|
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {
|
|
|
|
|
this.push(ssePayload);
|
|
|
|
|
this.push(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
mockRequest.mockResolvedValueOnce({ data: mockStream });
|
|
|
|
|
|
|
|
|
|
const stream = await server.requestStreamingPost('testStream', {});
|
|
|
|
|
const results = [];
|
|
|
|
|
for await (const res of stream) {
|
|
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
expect(results).toHaveLength(1);
|
|
|
|
|
expect(results[0]).toEqual(jsonObj);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not crash on objects partially matching VPC SC error structure', async () => {
|
|
|
|
|
const { server } = createTestServer();
|
|
|
|
|
const partialErrors = [
|
|
|
|
|
{ response: { data: { error: { details: [{ reason: 'OTHER' }] } } } },
|
|
|
|
|
{ response: { data: { error: { details: [] } } } },
|
|
|
|
|
{ response: { data: { error: {} } } },
|
|
|
|
|
{ response: { data: {} } },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const err of partialErrors) {
|
|
|
|
|
vi.spyOn(server, 'requestPost').mockRejectedValueOnce(err);
|
|
|
|
|
try {
|
|
|
|
|
await server.loadCodeAssist({ metadata: {} });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
expect(e).toBe(err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should correctly ignore arbitrary SSE comments and ID lines and empty lines before data', async () => {
|
|
|
|
|
const { server, mockRequest } = createTestServer();
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
const jsonObj = { foo: 'bar' };
|
|
|
|
|
const jsonString = JSON.stringify(jsonObj);
|
|
|
|
|
|
|
|
|
|
const ssePayload = `id: 123
|
|
|
|
|
:comment
|
|
|
|
|
retry: 100
|
|
|
|
|
|
|
|
|
|
data: ${jsonString}
|
|
|
|
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {
|
|
|
|
|
this.push(ssePayload);
|
|
|
|
|
this.push(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
mockRequest.mockResolvedValueOnce({ data: mockStream });
|
|
|
|
|
|
|
|
|
|
const stream = await server.requestStreamingPost('testStream', {});
|
|
|
|
|
const results = [];
|
|
|
|
|
for await (const res of stream) {
|
|
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
expect(results).toHaveLength(1);
|
|
|
|
|
expect(results[0]).toEqual(jsonObj);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should log InvalidChunkEvent when SSE chunk is not valid JSON', async () => {
|
|
|
|
|
const config = makeFakeConfig();
|
|
|
|
|
const mockRequest = vi.fn();
|
|
|
|
|
const client = { request: mockRequest } as unknown as OAuth2Client;
|
|
|
|
|
const server = new CodeAssistServer(
|
|
|
|
|
client,
|
|
|
|
|
'test-project',
|
|
|
|
|
{},
|
|
|
|
|
'test-session',
|
|
|
|
|
UserTierId.FREE,
|
|
|
|
|
undefined,
|
|
|
|
|
undefined,
|
|
|
|
|
config,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockRequest.mockResolvedValue({ data: mockStream });
|
|
|
|
|
|
|
|
|
|
const stream = await server.requestStreamingPost('testStream', {});
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
mockStream.push('data: { "invalid": json }\n\n');
|
|
|
|
|
mockStream.push(null);
|
|
|
|
|
}, 0);
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
for await (const res of stream) {
|
|
|
|
|
results.push(res);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(results).toHaveLength(0);
|
|
|
|
|
expect(logInvalidChunk).toHaveBeenCalledWith(
|
|
|
|
|
config,
|
|
|
|
|
expect.objectContaining({
|
|
|
|
|
error_message: 'Malformed JSON chunk',
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should safely process random response streams in generateContentStream (consumed/remaining credits)', async () => {
|
|
|
|
|
const { mockRequest, client } = createTestServer();
|
|
|
|
|
const testServer = new CodeAssistServer(
|
|
|
|
|
client,
|
|
|
|
|
'test-project',
|
|
|
|
|
{},
|
|
|
|
|
'test-session',
|
|
|
|
|
UserTierId.FREE,
|
|
|
|
|
undefined,
|
|
|
|
|
{ id: 'test-tier', name: 'tier', availableCredits: [] },
|
|
|
|
|
);
|
|
|
|
|
const { Readable } = await import('node:stream');
|
|
|
|
|
|
|
|
|
|
const streamResponses = [
|
|
|
|
|
{
|
|
|
|
|
traceId: '1',
|
|
|
|
|
consumedCredits: [{ creditType: 'A', creditAmount: '10' }],
|
|
|
|
|
},
|
|
|
|
|
{ traceId: '2', remainingCredits: [{ creditType: 'B' }] },
|
|
|
|
|
{ traceId: '3' },
|
|
|
|
|
{ traceId: '4', consumedCredits: null, remainingCredits: undefined },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const mockStream = new Readable({
|
|
|
|
|
read() {
|
|
|
|
|
for (const resp of streamResponses) {
|
|
|
|
|
this.push(`data: ${JSON.stringify(resp)}\n\n`);
|
|
|
|
|
}
|
|
|
|
|
this.push(null);
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
mockRequest.mockResolvedValueOnce({ data: mockStream });
|
|
|
|
|
vi.spyOn(testServer, 'recordCodeAssistMetrics').mockResolvedValue(
|
|
|
|
|
undefined,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const stream = await testServer.generateContentStream(
|
|
|
|
|
{ model: 'test-model', contents: [] },
|
|
|
|
|
'user-prompt-id',
|
|
|
|
|
LlmRole.MAIN,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
for await (const _ of stream) {
|
|
|
|
|
// Drain stream
|
|
|
|
|
}
|
|
|
|
|
// Should not crash
|
|
|
|
|
});
|
|
|
|
|
});
|
2025-06-15 22:41:32 -07:00
|
|
|
});
|