chromedevtools--chrome-devtools-mcp
44e511f6f6
This adds functions to sanitize the tool call parameters. They are not called as of now since we don't have server side changes landed yet to support these.
231 行
6.9 KiB
TypeScript
231 行
6.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import {describe, it, afterEach, beforeEach} from 'node:test';
|
|
|
|
import sinon from 'sinon';
|
|
|
|
import {DAEMON_CLIENT_NAME} from '../../src/daemon/utils.js';
|
|
import {
|
|
ClearcutLogger,
|
|
sanitizeParams,
|
|
} from '../../src/telemetry/ClearcutLogger.js';
|
|
import type {Persistence} from '../../src/telemetry/persistence.js';
|
|
import {FilePersistence} from '../../src/telemetry/persistence.js';
|
|
import {WatchdogMessageType} from '../../src/telemetry/types.js';
|
|
import {WatchdogClient} from '../../src/telemetry/WatchdogClient.js';
|
|
import {zod} from '../../src/third_party/index.js';
|
|
|
|
describe('ClearcutLogger', () => {
|
|
let mockPersistence: sinon.SinonStubbedInstance<Persistence>;
|
|
let mockWatchdogClient: sinon.SinonStubbedInstance<WatchdogClient>;
|
|
|
|
beforeEach(() => {
|
|
mockPersistence = sinon.createStubInstance(FilePersistence, {
|
|
loadState: Promise.resolve({
|
|
lastActive: '',
|
|
}),
|
|
});
|
|
mockWatchdogClient = sinon.createStubInstance(WatchdogClient);
|
|
});
|
|
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
describe('logToolInvocation', () => {
|
|
it('sends correct payload', async () => {
|
|
const logger = new ClearcutLogger({
|
|
persistence: mockPersistence,
|
|
appVersion: '1.0.0',
|
|
watchdogClient: mockWatchdogClient,
|
|
});
|
|
await logger.logToolInvocation({
|
|
toolName: 'test_tool',
|
|
success: true,
|
|
latencyMs: 123,
|
|
});
|
|
|
|
assert(mockWatchdogClient.send.calledOnce);
|
|
const msg = mockWatchdogClient.send.firstCall.args[0];
|
|
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
|
|
assert.strictEqual(msg.payload.tool_invocation?.tool_name, 'test_tool');
|
|
assert.strictEqual(msg.payload.tool_invocation?.success, true);
|
|
assert.strictEqual(msg.payload.tool_invocation?.latency_ms, 123);
|
|
});
|
|
});
|
|
|
|
describe('setClientName', () => {
|
|
const clients = [
|
|
{name: 'claude-code', expected: 1}, // MCP_CLIENT_CLAUDE_CODE
|
|
{name: 'gemini-cli', expected: 2}, // MCP_CLIENT_GEMINI_CLI
|
|
{name: DAEMON_CLIENT_NAME, expected: 4}, // MCP_CLIENT_DT_MCP_CLI
|
|
{name: 'openclaw-browser', expected: 5}, // MCP_CLIENT_OPENCLAW
|
|
{name: 'codex-mcp-client', expected: 6}, // MCP_CLIENT_CODEX
|
|
{name: 'antigravity-client', expected: 7}, // MCP_CLIENT_ANTIGRAVITY
|
|
];
|
|
|
|
for (const {name, expected} of clients) {
|
|
it(`maps ${name} client correctly`, async () => {
|
|
const logger = new ClearcutLogger({
|
|
persistence: mockPersistence,
|
|
appVersion: '1.0.0',
|
|
watchdogClient: mockWatchdogClient,
|
|
});
|
|
|
|
logger.setClientName(name);
|
|
await logger.logServerStart({headless: true});
|
|
|
|
assert(mockWatchdogClient.send.calledOnce);
|
|
const msg = mockWatchdogClient.send.firstCall.args[0];
|
|
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
|
|
assert.strictEqual(msg.payload.mcp_client, expected);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('logServerStart', () => {
|
|
it('logs flag usage', async () => {
|
|
const logger = new ClearcutLogger({
|
|
persistence: mockPersistence,
|
|
appVersion: '1.0.0',
|
|
watchdogClient: mockWatchdogClient,
|
|
});
|
|
|
|
await logger.logServerStart({headless: true});
|
|
|
|
assert(mockWatchdogClient.send.calledOnce);
|
|
const msg = mockWatchdogClient.send.firstCall.args[0];
|
|
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
|
|
assert.strictEqual(msg.payload.server_start?.flag_usage?.headless, true);
|
|
});
|
|
});
|
|
|
|
describe('logDailyActiveIfNeeded', () => {
|
|
it('logs daily active if needed (lastActive > 24h ago)', async () => {
|
|
const yesterday = new Date();
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
mockPersistence.loadState.resolves({
|
|
lastActive: yesterday.toISOString(),
|
|
});
|
|
|
|
const logger = new ClearcutLogger({
|
|
persistence: mockPersistence,
|
|
appVersion: '1.0.0',
|
|
watchdogClient: mockWatchdogClient,
|
|
});
|
|
|
|
await logger.logDailyActiveIfNeeded();
|
|
|
|
assert(mockWatchdogClient.send.calledOnce);
|
|
const msg = mockWatchdogClient.send.firstCall.args[0];
|
|
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
|
|
assert.ok(msg.payload.daily_active);
|
|
|
|
assert(mockPersistence.saveState.called);
|
|
});
|
|
|
|
it('does not log daily active if not needed (today)', async () => {
|
|
mockPersistence.loadState.resolves({
|
|
lastActive: new Date().toISOString(),
|
|
});
|
|
|
|
const logger = new ClearcutLogger({
|
|
persistence: mockPersistence,
|
|
appVersion: '1.0.0',
|
|
watchdogClient: mockWatchdogClient,
|
|
});
|
|
|
|
await logger.logDailyActiveIfNeeded();
|
|
|
|
assert(mockWatchdogClient.send.notCalled);
|
|
assert(mockPersistence.saveState.notCalled);
|
|
});
|
|
|
|
it('logs daily active with -1 if lastActive is missing', async () => {
|
|
mockPersistence.loadState.resolves({
|
|
lastActive: '',
|
|
});
|
|
|
|
const logger = new ClearcutLogger({
|
|
persistence: mockPersistence,
|
|
appVersion: '1.0.0',
|
|
watchdogClient: mockWatchdogClient,
|
|
});
|
|
|
|
await logger.logDailyActiveIfNeeded();
|
|
|
|
assert(mockWatchdogClient.send.calledOnce);
|
|
const msg = mockWatchdogClient.send.firstCall.args[0];
|
|
assert.strictEqual(msg.type, WatchdogMessageType.LOG_EVENT);
|
|
assert.strictEqual(msg.payload.daily_active?.days_since_last_active, -1);
|
|
assert(mockPersistence.saveState.called);
|
|
});
|
|
});
|
|
|
|
describe('sanitizeParams', () => {
|
|
it('filters out uid and transforms strings and arrays', () => {
|
|
const schema = {
|
|
uid: zod.string(),
|
|
myString: zod.string(),
|
|
myArray: zod.array(zod.string()),
|
|
myNumber: zod.number(),
|
|
myBool: zod.boolean(),
|
|
myEnum: zod.enum(['a', 'b']),
|
|
};
|
|
|
|
const params = {
|
|
uid: 'sensitive',
|
|
myString: 'hello',
|
|
myArray: ['one', 'two'],
|
|
myNumber: 42,
|
|
myBool: true,
|
|
myEnum: 'a' as const,
|
|
};
|
|
|
|
const sanitized = sanitizeParams(params, schema);
|
|
|
|
assert.deepStrictEqual(sanitized, {
|
|
myString_length: 5,
|
|
myArray_count: 2,
|
|
myNumber: 42,
|
|
myBool: true,
|
|
myEnum: 'a',
|
|
});
|
|
});
|
|
|
|
it('throws error for unsupported types', () => {
|
|
const schema = {
|
|
myObj: zod.object({foo: zod.string()}),
|
|
};
|
|
const params = {
|
|
myObj: {foo: 'bar'},
|
|
};
|
|
|
|
assert.throws(
|
|
() => sanitizeParams(params, schema),
|
|
/Unsupported zod type for tool parameter: ZodObject/,
|
|
);
|
|
});
|
|
|
|
it('throws error when value is not of equivalent type', () => {
|
|
const schema = {
|
|
myString: zod.string(),
|
|
};
|
|
const params = {
|
|
myString: 123,
|
|
};
|
|
|
|
assert.throws(
|
|
() => sanitizeParams(params, schema),
|
|
/parameter myString has type ZodString but value 123 is not of equivalent type/,
|
|
);
|
|
});
|
|
});
|
|
});
|