chromedevtools--chrome-devtools-mcp
9a1ebc724b
This adds logging to the read & write operations to the telemetry state file. In particular, - for read, no error is logged if the state file doesn't exist, which is expected to happen when the user is new. An error is logged otherwise (e.g. file format error, errors when reading an existing file like permission issue). - for write, any error in the path will be logged. This commit also puts the instantiation of the `Persistence` object out of the `ClearcutLogger` object to avoid circular dependency.
145 行
4.7 KiB
TypeScript
145 行
4.7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import assert from 'node:assert';
|
|
import crypto from 'node:crypto';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {describe, it, afterEach, beforeEach} from 'node:test';
|
|
|
|
import sinon from 'sinon';
|
|
|
|
import {ClearcutLogger} from '../../src/telemetry/ClearcutLogger.js';
|
|
import {ErrorCode} from '../../src/telemetry/errors.js';
|
|
import * as persistence from '../../src/telemetry/persistence.js';
|
|
import {WatchdogClient} from '../../src/telemetry/WatchdogClient.js';
|
|
|
|
describe('FilePersistence', () => {
|
|
let tmpDir: string;
|
|
let logServerErrorStub: sinon.SinonStub;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = path.join(
|
|
await fs.realpath(os.tmpdir()),
|
|
`telemetry-test-${crypto.randomUUID()}`,
|
|
);
|
|
await fs.mkdir(tmpDir, {recursive: true});
|
|
|
|
ClearcutLogger.resetForTesting();
|
|
const mockWatchdog = sinon.createStubInstance(WatchdogClient);
|
|
const logger = ClearcutLogger.initialize({
|
|
appVersion: '1.0.0',
|
|
persistence: new persistence.FilePersistence(tmpDir),
|
|
watchdogClient: mockWatchdog,
|
|
});
|
|
logServerErrorStub = sinon.stub(logger, 'logServerError');
|
|
});
|
|
|
|
afterEach(async () => {
|
|
sinon.restore();
|
|
ClearcutLogger.resetForTesting();
|
|
await fs.rm(tmpDir, {recursive: true, force: true});
|
|
});
|
|
|
|
describe('loadState', () => {
|
|
it('returns default state and does NOT log telemetry if file does not exist (ENOENT)', async () => {
|
|
const filePersistence = new persistence.FilePersistence(tmpDir);
|
|
const state = await filePersistence.loadState();
|
|
assert.deepStrictEqual(state, {
|
|
lastActive: '',
|
|
});
|
|
assert(logServerErrorStub.notCalled);
|
|
});
|
|
|
|
it('returns default state and LOGS telemetry if load fails due to corruption', async () => {
|
|
const filePath = path.join(tmpDir, 'telemetry_state.json');
|
|
await fs.writeFile(filePath, 'not-valid-json', 'utf-8');
|
|
|
|
const filePersistence = new persistence.FilePersistence(tmpDir);
|
|
const state = await filePersistence.loadState();
|
|
|
|
assert.deepStrictEqual(state, {
|
|
lastActive: '',
|
|
});
|
|
assert(logServerErrorStub.calledOnce);
|
|
assert.deepStrictEqual(logServerErrorStub.firstCall.args[0], {
|
|
errorCode: ErrorCode.ERROR_CODE_PERSISTENCE_FILE_READ_FAILED,
|
|
});
|
|
});
|
|
|
|
it('returns default state and LOGS telemetry if load fails during read stage', async () => {
|
|
const filePath = path.join(tmpDir, 'telemetry_state.json');
|
|
await fs.writeFile(filePath, '{"valid": "json"}', 'utf-8');
|
|
|
|
const readFileStub = sinon
|
|
.stub(fs, 'readFile')
|
|
.rejects(new Error('Synthetic read error'));
|
|
|
|
const filePersistence = new persistence.FilePersistence(tmpDir);
|
|
const state = await filePersistence.loadState();
|
|
|
|
assert.deepStrictEqual(state, {
|
|
lastActive: '',
|
|
});
|
|
assert(logServerErrorStub.calledOnce);
|
|
assert.deepStrictEqual(logServerErrorStub.firstCall.args[0], {
|
|
errorCode: ErrorCode.ERROR_CODE_PERSISTENCE_FILE_READ_FAILED,
|
|
});
|
|
|
|
readFileStub.restore();
|
|
});
|
|
|
|
it('returns stored state if file exists', async () => {
|
|
const expectedState = {
|
|
lastActive: '2023-01-01T00:00:00.000Z',
|
|
};
|
|
await fs.writeFile(
|
|
path.join(tmpDir, 'telemetry_state.json'),
|
|
JSON.stringify(expectedState),
|
|
);
|
|
|
|
const filePersistence = new persistence.FilePersistence(tmpDir);
|
|
const state = await filePersistence.loadState();
|
|
assert.deepStrictEqual(state, expectedState);
|
|
});
|
|
});
|
|
|
|
describe('saveState', () => {
|
|
it('saves state to file', async () => {
|
|
const state = {
|
|
lastActive: '2023-01-01T00:00:00.000Z',
|
|
};
|
|
const filePersistence = new persistence.FilePersistence(tmpDir);
|
|
await filePersistence.saveState(state);
|
|
|
|
const content = await fs.readFile(
|
|
path.join(tmpDir, 'telemetry_state.json'),
|
|
'utf-8',
|
|
);
|
|
assert.deepStrictEqual(JSON.parse(content), state);
|
|
assert(logServerErrorStub.notCalled);
|
|
});
|
|
|
|
it('logs telemetry when failing to save to file', async () => {
|
|
// Force error by replacing directory with a file, causing mkdir to fail.
|
|
const dirPath = path.join(tmpDir, 'blocked_dir');
|
|
await fs.writeFile(dirPath, 'i-am-a-file');
|
|
const filePersistence = new persistence.FilePersistence(dirPath);
|
|
|
|
const state = {
|
|
lastActive: '2023-01-01T00:00:00.000Z',
|
|
};
|
|
await filePersistence.saveState(state);
|
|
|
|
assert(logServerErrorStub.calledOnce);
|
|
assert.deepStrictEqual(logServerErrorStub.firstCall.args[0], {
|
|
errorCode: ErrorCode.ERROR_CODE_PERSISTENCE_FILE_SAVE_FAILED,
|
|
});
|
|
});
|
|
});
|
|
});
|