185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import crypto from 'node:crypto';
|
|
import {existsSync, rmSync} from 'node:fs';
|
|
import {dirname} from 'node:path';
|
|
import {describe, it, afterEach, beforeEach} from 'node:test';
|
|
|
|
import {
|
|
handleResponse,
|
|
startDaemon,
|
|
stopDaemon,
|
|
} from '../../src/daemon/client.js';
|
|
import {isDaemonRunning} from '../../src/daemon/utils.js';
|
|
|
|
describe('daemon client', () => {
|
|
describe('start/stop', () => {
|
|
let sessionId: string;
|
|
|
|
beforeEach(async () => {
|
|
sessionId = crypto.randomUUID();
|
|
await stopDaemon(sessionId);
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await stopDaemon(sessionId);
|
|
});
|
|
|
|
it('should start and stop daemon', async () => {
|
|
assert.ok(
|
|
!isDaemonRunning(sessionId),
|
|
'Daemon should not be running initially',
|
|
);
|
|
|
|
await startDaemon([], sessionId);
|
|
assert.ok(
|
|
isDaemonRunning(sessionId),
|
|
'Daemon should be running after start',
|
|
);
|
|
|
|
await stopDaemon(sessionId);
|
|
assert.ok(
|
|
!isDaemonRunning(sessionId),
|
|
'Daemon should not be running after stop',
|
|
);
|
|
});
|
|
|
|
it('should handle starting daemon when already running', async () => {
|
|
await startDaemon([], sessionId);
|
|
assert.ok(isDaemonRunning(sessionId), 'Daemon should be running');
|
|
|
|
// Starting again should be a no-op
|
|
await startDaemon([], sessionId);
|
|
assert.ok(isDaemonRunning(sessionId), 'Daemon should still be running');
|
|
});
|
|
|
|
it('should handle stopping daemon when not running', async () => {
|
|
assert.ok(
|
|
!isDaemonRunning(sessionId),
|
|
'Daemon should not be running initially',
|
|
);
|
|
|
|
// Stopping when not running should be a no-op
|
|
await stopDaemon(sessionId);
|
|
assert.ok(
|
|
!isDaemonRunning(sessionId),
|
|
'Daemon should still not be running',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('parsing', () => {
|
|
it('handles MCP response with text format', async () => {
|
|
const textResponse = {content: [{type: 'text' as const, text: 'test'}]};
|
|
assert.strictEqual(await handleResponse(textResponse, 'md'), 'test');
|
|
});
|
|
|
|
it('handles JSON response', async () => {
|
|
const jsonResponse = {
|
|
content: [],
|
|
structuredContent: {
|
|
test: 'data',
|
|
number: 123,
|
|
},
|
|
};
|
|
assert.strictEqual(
|
|
await handleResponse(jsonResponse, 'json'),
|
|
JSON.stringify(jsonResponse.structuredContent),
|
|
);
|
|
});
|
|
|
|
it('handles error response when isError is true', async () => {
|
|
const errorResponse = {
|
|
isError: true,
|
|
content: [{type: 'text' as const, text: 'Something went wrong'}],
|
|
};
|
|
assert.strictEqual(
|
|
await handleResponse(errorResponse, 'md'),
|
|
JSON.stringify(errorResponse.content),
|
|
);
|
|
});
|
|
|
|
it('handles text response when json format is requested but no structured content', async () => {
|
|
const textResponse = {
|
|
content: [{type: 'text' as const, text: 'Fall through text'}],
|
|
};
|
|
assert.deepStrictEqual(
|
|
await handleResponse(textResponse, 'json'),
|
|
JSON.stringify(['Fall through text']),
|
|
);
|
|
});
|
|
|
|
it('supports images', async () => {
|
|
const unsupportedContentResponse = {
|
|
content: [
|
|
{
|
|
type: 'image' as const,
|
|
data: 'base64data',
|
|
mimeType: 'image/png',
|
|
},
|
|
],
|
|
structuredContent: {},
|
|
};
|
|
const response = await handleResponse(unsupportedContentResponse, 'md');
|
|
assert.ok(response.includes('.png'));
|
|
});
|
|
|
|
it('includes saved image file paths in structured JSON responses', async () => {
|
|
const imageContentResponse = {
|
|
content: [
|
|
{
|
|
type: 'text' as const,
|
|
text: 'Took a screenshot.',
|
|
},
|
|
{
|
|
type: 'image' as const,
|
|
data: Buffer.from('image data').toString('base64'),
|
|
mimeType: 'image/png',
|
|
},
|
|
],
|
|
structuredContent: {
|
|
message: 'Took a screenshot.',
|
|
},
|
|
};
|
|
let filePath: string | undefined;
|
|
try {
|
|
const response = await handleResponse(imageContentResponse, 'json');
|
|
const parsed = JSON.parse(response) as {
|
|
message: string;
|
|
images: Array<{filePath: string; mimeType: string}>;
|
|
};
|
|
assert.strictEqual(parsed.message, 'Took a screenshot.');
|
|
assert.strictEqual(parsed.images.length, 1);
|
|
assert.strictEqual(parsed.images[0].mimeType, 'image/png');
|
|
filePath = parsed.images[0].filePath;
|
|
assert.ok(filePath.endsWith('.png'));
|
|
assert.ok(existsSync(filePath));
|
|
} finally {
|
|
if (filePath) {
|
|
rmSync(dirname(filePath), {recursive: true, force: true});
|
|
}
|
|
}
|
|
});
|
|
|
|
it('uses the webp extension for WebP images', async () => {
|
|
const webpContentResponse = {
|
|
content: [
|
|
{
|
|
type: 'image' as const,
|
|
data: 'base64data',
|
|
mimeType: 'image/webp',
|
|
},
|
|
],
|
|
structuredContent: {},
|
|
};
|
|
const response = await handleResponse(webpContentResponse, 'md');
|
|
assert.ok(response.includes('.webp'));
|
|
});
|
|
});
|
|
});
|