Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 11 additions & 27 deletions src/McpResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
tabId?: string;
networkRequest?: object;
networkRequests?: object[];
consoleMessage?: object;
consoleMessages?: object[];
} = {};

if (this.#tabId) {
Expand All @@ -454,9 +456,12 @@ Call ${handleDialog.name} to handle it before continuing.`);
structuredContent.networkRequest =
data.detailedNetworkRequest.toJSONDetailed();
}
response.push(
...this.#formatConsoleData(context, data.detailedConsoleMessage),
);

if (data.detailedConsoleMessage) {
response.push(data.detailedConsoleMessage.toStringDetailed());
structuredContent.consoleMessage =
data.detailedConsoleMessage.toJSONDetailed();
}

if (this.#networkRequestsOptions?.include) {
let requests = context.getNetworkRequests(
Expand Down Expand Up @@ -503,13 +508,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
this.#consoleDataOptions.pagination,
);
response.push(...data.info);
response.push(
...data.items.map(message => {
if (message instanceof IssueFormatter) {
return message.toString();
}
return message.toString();
}),
response.push(...data.items.map(message => message.toString()));
structuredContent.consoleMessages = data.items.map(message =>
Comment thread
OrKoN marked this conversation as resolved.
message.toJSON(),
);
} else {
response.push('<no console messages found>');
Expand Down Expand Up @@ -559,23 +560,6 @@ Call ${handleDialog.name} to handle it before continuing.`);
};
}

#formatConsoleData(
context: McpContext,
data: ConsoleFormatter | IssueFormatter | undefined,
): string[] {
const response: string[] = [];
if (!data) {
return response;
}

if (data instanceof IssueFormatter) {
response.push(data.toStringDetailed());
} else {
response.push(data.toStringDetailed());
}
return response;
}

resetResponseLineForTesting() {
this.#textResponseLines = [];
}
Expand Down
23 changes: 23 additions & 0 deletions src/formatters/ConsoleFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,27 @@ export class ConsoleFormatter {
}
return result;
}
toJSON(): object {
return {
type: this.#getType(),
text: this.#getText(),
argsCount:
this.#msg instanceof Error
? 0
: this.#resolvedArgs.length || this.#msg.args().length,
id: this.#id,
};
}

toJSONDetailed(): object {
return {
id: this.#id,
type: this.#getType(),
text: this.#getText(),
args: this.#getArgs().map(arg =>
typeof arg === 'object' ? arg : String(arg),
),
stackTrace: this.#resolvedStackTrace,
};
}
}
90 changes: 62 additions & 28 deletions src/formatters/IssueFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export interface IssueFormatterOptions {
id?: number;
}

export interface AffectedResource {
uid?: string;
data?: unknown;
request?: string | number;
}

export class IssueFormatter {
#issue: DevTools.AggregatedIssue;
#options: IssueFormatterOptions;
Expand Down Expand Up @@ -59,6 +65,55 @@ export class IssueFormatter {
}
}

const affectedResources = this.#getAffectedResources();
if (affectedResources.length) {
bodyParts.push('### Affected resources');
bodyParts.push(
...affectedResources.map(item => {
const details = [];
if (item.uid) {
details.push(`uid=${item.uid}`);
}
if (item.request) {
details.push(
(typeof item.request === 'number' ? `reqid=` : 'url=') +
item.request,
);
}
if (item.data) {
details.push(`data=${JSON.stringify(item.data)}`);
}
return details.join(' ');
}),
);
}

result.push(`Message: issue> ${bodyParts.join('\n')}`);

return result.join('\n');
}

toJSON(): object {
return {
type: 'issue',
title: this.#getTitle(),
count: this.#issue.getAggregatedIssuesCount(),
id: this.#options.id,
};
}

toJSONDetailed(): object {
return {
id: this.#options.id,
type: 'issue',
title: this.#getTitle(),
description: this.#getDescription(),
links: this.#issue.getDescription()?.links,
affectedResources: this.#getAffectedResources(),
};
}

#getAffectedResources(): AffectedResource[] {
const issues = this.#issue.getAllIssues();
const affectedResources: Array<{
uid?: string;
Expand All @@ -73,8 +128,10 @@ export class IssueFormatter {

// We send the remaining details as untyped JSON because the DevTools
// frontend code is currently not re-usable.
// eslint-disable-next-line
const data = structuredClone(details) as any;
const data = structuredClone(details) as unknown as Record<
string,
unknown
>;

let uid;
let request: number | string | undefined;
Expand Down Expand Up @@ -111,7 +168,8 @@ export class IssueFormatter {
);
if (resolvedId) {
request = resolvedId;
delete data.request.requestId;
const requestData = data.request as Record<string, unknown>;
delete requestData.requestId;
}
}
}
Expand All @@ -125,31 +183,7 @@ export class IssueFormatter {
request,
});
}
if (affectedResources.length) {
bodyParts.push('### Affected resources');
bodyParts.push(
...affectedResources.map(item => {
const details = [];
if (item.uid) {
details.push(`uid=${item.uid}`);
}
if (item.request) {
details.push(
(typeof item.request === 'number' ? `reqid=` : 'url=') +
item.request,
);
}
if (item.data) {
details.push(`data=${JSON.stringify(item.data)}`);
}
return details.join(' ');
}),
);
}

result.push(`Message: issue> ${bodyParts.join('\n')}`);

return result.join('\n');
return affectedResources;
}

isValid(): boolean {
Expand Down
74 changes: 74 additions & 0 deletions tests/formatters/ConsoleFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'node:assert';
import {describe, it} from 'node:test';

import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js';
Expand Down Expand Up @@ -163,4 +164,77 @@ describe('ConsoleFormatter', () => {
t.assert.snapshot?.(result);
});
});
describe('toJSON', () => {
it('formats a console.log message', async () => {
const message = createMockMessage({
type: () => 'log',
text: () => 'Hello, world!',
});
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
assert.deepStrictEqual(result, {
type: 'log',
text: 'Hello, world!',
argsCount: 0,
id: 1,
});
});

it('formats a console.log message with args', async () => {
const message = createMockMessage({
type: () => 'log',
text: () => 'Processing file:',
args: () => [
{jsonValue: async () => 'file.txt'},
{jsonValue: async () => 'another file'},
],
});
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
assert.deepStrictEqual(result, {
type: 'log',
text: 'Processing file:',
argsCount: 2,
id: 1,
});
});
});

describe('toJSONDetailed', () => {
it('formats a console.log message', async () => {
const message = createMockMessage({
type: () => 'log',
text: () => 'Hello, world!',
});
const result = (
await ConsoleFormatter.from(message, {id: 1})
).toJSONDetailed();
assert.deepStrictEqual(result, {
id: 1,
type: 'log',
text: 'Hello, world!',
args: [],
stackTrace: undefined,
});
});

it('formats a console.log message with args', async () => {
const message = createMockMessage({
type: () => 'log',
text: () => 'Processing file:',
args: () => [
{jsonValue: async () => 'file.txt'},
{jsonValue: async () => 'another file'},
],
});
const result = (
await ConsoleFormatter.from(message, {id: 2, fetchDetailedData: true})
).toJSONDetailed();
assert.deepStrictEqual(result, {
id: 2,
type: 'log',
text: 'Processing file:',
args: ['file.txt', 'another file'],
stackTrace: undefined,
});
});
});
});
72 changes: 72 additions & 0 deletions tests/formatters/IssueFormatter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,76 @@ describe('IssueFormatter', () => {
assert.ok(detailed.includes('Valid Title'));
});
});
describe('toJSON', () => {
it('formats a simplified issue', () => {
const mockAggregatedIssue = getMockAggregatedIssue();
mockAggregatedIssue.getDescription.returns({
file: 'mock.md',
links: [],
});
mockAggregatedIssue.getAggregatedIssuesCount.returns(5);
getIssueDescriptionStub
.withArgs('mock.md')
.returns('# Issue Title\n\nIssue content');

const formatter = new IssueFormatter(mockAggregatedIssue, {id: 1});
assert.deepStrictEqual(formatter.toJSON(), {
type: 'issue',
title: 'Issue Title',
count: 5,
id: 1,
});
});
});

describe('toJSONDetailed', () => {
it('formats a detailed issue', () => {
const testGenericIssue = {
details: () => {
return {
violatingNodeId: 2,
violatingNodeAttribute: 'test',
};
},
};
const mockAggregatedIssue = getMockAggregatedIssue();
const mockDescription = {
file: 'mock.md',
links: [{link: 'http://example.com', linkTitle: 'Link 1'}],
substitutions: new Map([['PLACEHOLDER_VALUE', 'sub value']]),
};
mockAggregatedIssue.getDescription.returns(mockDescription);
// @ts-expect-error stubbed generic issue does not match the complete type.
mockAggregatedIssue.getAllIssues.returns([testGenericIssue]);

const mockDescriptionFileContent =
'# Mock Issue Title\n\nThis is a mock issue description {PLACEHOLDER_VALUE}';

getIssueDescriptionStub
.withArgs('mock.md')
.returns(mockDescriptionFileContent);

const formatter = new IssueFormatter(mockAggregatedIssue, {
id: 5,
});

const detailedResult = formatter.toJSONDetailed() as unknown as Record<
string,
object
> & {affectedResources: Array<{data: object}>};
assert.strictEqual(detailedResult.id, 5);
assert.strictEqual(detailedResult.type, 'issue');
assert.strictEqual(detailedResult.title, 'Mock Issue Title');
assert.strictEqual(
detailedResult.description,
'# Mock Issue Title\n\nThis is a mock issue description sub value',
);
assert.deepStrictEqual(detailedResult.links, mockDescription.links);
assert.strictEqual(detailedResult.affectedResources.length, 1);
assert.deepStrictEqual(detailedResult.affectedResources[0].data, {
violatingNodeAttribute: 'test',
violatingNodeId: 2,
});
});
});
});