Skip to content

Commit d3710e4

Browse files
committed
chore: structured content for console
1 parent a5134c6 commit d3710e4

5 files changed

Lines changed: 271 additions & 29 deletions

File tree

src/McpResponse.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
432432
tabId?: string;
433433
networkRequest?: object;
434434
networkRequests?: object[];
435+
detailedConsoleMessage?: object;
436+
consoleMessages?: object[];
435437
} = {};
436438

437439
if (this.#tabId) {
@@ -457,6 +459,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
457459
response.push(
458460
...this.#formatConsoleData(context, data.detailedConsoleMessage),
459461
);
462+
if (data.detailedConsoleMessage) {
463+
structuredContent.detailedConsoleMessage =
464+
data.detailedConsoleMessage.toJSONDetailed();
465+
}
460466

461467
if (this.#networkRequestsOptions?.include) {
462468
let requests = context.getNetworkRequests(
@@ -511,6 +517,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
511517
return message.toString();
512518
}),
513519
);
520+
structuredContent.consoleMessages = data.items.map(message =>
521+
message.toJSON(),
522+
);
514523
} else {
515524
response.push('<no console messages found>');
516525
}

src/formatters/ConsoleFormatter.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ export interface ConsoleFormatterOptions {
1818
resolvedStackTraceForTesting?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
1919
}
2020

21+
export interface ConsoleFormatterJson {
22+
type: string;
23+
text: string;
24+
argsCount: number;
25+
id?: number;
26+
}
27+
28+
export interface ConsoleFormatterJsonDetailed {
29+
id?: number;
30+
type: string;
31+
text: string;
32+
args: unknown[];
33+
stackTrace?: DevTools.DevTools.StackTrace.StackTrace.StackTrace;
34+
}
35+
2136
export class ConsoleFormatter {
2237
#msg: ConsoleMessage | Error;
2338
#resolvedArgs: unknown[] = [];
@@ -171,4 +186,27 @@ export class ConsoleFormatter {
171186
}
172187
return result;
173188
}
189+
toJSON(): ConsoleFormatterJson {
190+
return {
191+
type: this.#getType(),
192+
text: this.#getText(),
193+
argsCount:
194+
this.#msg instanceof Error
195+
? 0
196+
: this.#resolvedArgs.length || this.#msg.args().length,
197+
id: this.#id,
198+
};
199+
}
200+
201+
toJSONDetailed(): ConsoleFormatterJsonDetailed {
202+
return {
203+
id: this.#id,
204+
type: this.#getType(),
205+
text: this.#getText(),
206+
args: this.#getArgs().map(arg =>
207+
typeof arg === 'object' ? arg : String(arg),
208+
),
209+
stackTrace: this.#resolvedStackTrace,
210+
};
211+
}
174212
}

src/formatters/IssueFormatter.ts

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,29 @@ export interface IssueFormatterOptions {
1414
id?: number;
1515
}
1616

17+
export interface IssueFormatterJson {
18+
type: 'issue';
19+
title: string | undefined;
20+
count: number;
21+
id?: number;
22+
}
23+
24+
export interface AffectedResource {
25+
uid?: string;
26+
data?: unknown;
27+
request?: string | number;
28+
}
29+
30+
export interface IssueFormatterJsonDetailed {
31+
id?: number;
32+
type: 'issue';
33+
title: string | undefined;
34+
description: string | undefined;
35+
links?: ReadonlyArray<{link: string; linkTitle: string}>;
36+
affectedResources: AffectedResource[];
37+
}
38+
39+
1740
export class IssueFormatter {
1841
#issue: DevTools.AggregatedIssue;
1942
#options: IssueFormatterOptions;
@@ -59,6 +82,55 @@ export class IssueFormatter {
5982
}
6083
}
6184

85+
const affectedResources = this.#getAffectedResources();
86+
if (affectedResources.length) {
87+
bodyParts.push('### Affected resources');
88+
bodyParts.push(
89+
...affectedResources.map(item => {
90+
const details = [];
91+
if (item.uid) {
92+
details.push(`uid=${item.uid}`);
93+
}
94+
if (item.request) {
95+
details.push(
96+
(typeof item.request === 'number' ? `reqid=` : 'url=') +
97+
item.request,
98+
);
99+
}
100+
if (item.data) {
101+
details.push(`data=${JSON.stringify(item.data)}`);
102+
}
103+
return details.join(' ');
104+
}),
105+
);
106+
}
107+
108+
result.push(`Message: issue> ${bodyParts.join('\n')}`);
109+
110+
return result.join('\n');
111+
}
112+
113+
toJSON(): IssueFormatterJson {
114+
return {
115+
type: 'issue',
116+
title: this.#getTitle(),
117+
count: this.#issue.getAggregatedIssuesCount(),
118+
id: this.#options.id,
119+
};
120+
}
121+
122+
toJSONDetailed(): IssueFormatterJsonDetailed {
123+
return {
124+
id: this.#options.id,
125+
type: 'issue',
126+
title: this.#getTitle(),
127+
description: this.#getDescription(),
128+
links: this.#issue.getDescription()?.links,
129+
affectedResources: this.#getAffectedResources(),
130+
};
131+
}
132+
133+
#getAffectedResources(): AffectedResource[] {
62134
const issues = this.#issue.getAllIssues();
63135
const affectedResources: Array<{
64136
uid?: string;
@@ -73,8 +145,7 @@ export class IssueFormatter {
73145

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

79150
let uid;
80151
let request: number | string | undefined;
@@ -111,7 +182,8 @@ export class IssueFormatter {
111182
);
112183
if (resolvedId) {
113184
request = resolvedId;
114-
delete data.request.requestId;
185+
const requestData = data.request as Record<string, unknown>;
186+
delete requestData.requestId;
115187
}
116188
}
117189
}
@@ -125,31 +197,7 @@ export class IssueFormatter {
125197
request,
126198
});
127199
}
128-
if (affectedResources.length) {
129-
bodyParts.push('### Affected resources');
130-
bodyParts.push(
131-
...affectedResources.map(item => {
132-
const details = [];
133-
if (item.uid) {
134-
details.push(`uid=${item.uid}`);
135-
}
136-
if (item.request) {
137-
details.push(
138-
(typeof item.request === 'number' ? `reqid=` : 'url=') +
139-
item.request,
140-
);
141-
}
142-
if (item.data) {
143-
details.push(`data=${JSON.stringify(item.data)}`);
144-
}
145-
return details.join(' ');
146-
}),
147-
);
148-
}
149-
150-
result.push(`Message: issue> ${bodyParts.join('\n')}`);
151-
152-
return result.join('\n');
200+
return affectedResources;
153201
}
154202

155203
isValid(): boolean {

tests/formatters/ConsoleFormatter.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import assert from 'node:assert';
78
import {describe, it} from 'node:test';
89

910
import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js';
@@ -163,4 +164,77 @@ describe('ConsoleFormatter', () => {
163164
t.assert.snapshot?.(result);
164165
});
165166
});
167+
describe('toJSON', () => {
168+
it('formats a console.log message', async () => {
169+
const message = createMockMessage({
170+
type: () => 'log',
171+
text: () => 'Hello, world!',
172+
});
173+
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
174+
assert.deepStrictEqual(result, {
175+
type: 'log',
176+
text: 'Hello, world!',
177+
argsCount: 0,
178+
id: 1,
179+
});
180+
});
181+
182+
it('formats a console.log message with args', async () => {
183+
const message = createMockMessage({
184+
type: () => 'log',
185+
text: () => 'Processing file:',
186+
args: () => [
187+
{jsonValue: async () => 'file.txt'},
188+
{jsonValue: async () => 'another file'},
189+
],
190+
});
191+
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
192+
assert.deepStrictEqual(result, {
193+
type: 'log',
194+
text: 'Processing file:',
195+
argsCount: 2,
196+
id: 1,
197+
});
198+
});
199+
});
200+
201+
describe('toJSONDetailed', () => {
202+
it('formats a console.log message', async () => {
203+
const message = createMockMessage({
204+
type: () => 'log',
205+
text: () => 'Hello, world!',
206+
});
207+
const result = (
208+
await ConsoleFormatter.from(message, {id: 1})
209+
).toJSONDetailed();
210+
assert.deepStrictEqual(result, {
211+
id: 1,
212+
type: 'log',
213+
text: 'Hello, world!',
214+
args: [],
215+
stackTrace: undefined,
216+
});
217+
});
218+
219+
it('formats a console.log message with args', async () => {
220+
const message = createMockMessage({
221+
type: () => 'log',
222+
text: () => 'Processing file:',
223+
args: () => [
224+
{jsonValue: async () => 'file.txt'},
225+
{jsonValue: async () => 'another file'},
226+
],
227+
});
228+
const result = (
229+
await ConsoleFormatter.from(message, {id: 2, fetchDetailedData: true})
230+
).toJSONDetailed();
231+
assert.deepStrictEqual(result, {
232+
id: 2,
233+
type: 'log',
234+
text: 'Processing file:',
235+
args: ['file.txt', 'another file'],
236+
stackTrace: undefined,
237+
});
238+
});
239+
});
166240
});

tests/formatters/IssueFormatter.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {describe, it, beforeEach, afterEach} from 'node:test';
99

1010
import sinon from 'sinon';
1111

12-
import {IssueFormatter} from '../../src/formatters/IssueFormatter.js';
12+
import {
13+
IssueFormatter,
14+
type IssueFormatterJsonDetailed,
15+
} from '../../src/formatters/IssueFormatter.js';
1316
import {ISSUE_UTILS} from '../../src/issue-descriptions.js';
1417
import {getMockAggregatedIssue} from '../utils.js';
1518

@@ -134,4 +137,74 @@ describe('IssueFormatter', () => {
134137
assert.ok(detailed.includes('Valid Title'));
135138
});
136139
});
140+
describe('toJSON', () => {
141+
it('formats a simplified issue', () => {
142+
const mockAggregatedIssue = getMockAggregatedIssue();
143+
mockAggregatedIssue.getDescription.returns({
144+
file: 'mock.md',
145+
links: [],
146+
});
147+
mockAggregatedIssue.getAggregatedIssuesCount.returns(5);
148+
getIssueDescriptionStub
149+
.withArgs('mock.md')
150+
.returns('# Issue Title\n\nIssue content');
151+
152+
const formatter = new IssueFormatter(mockAggregatedIssue, {id: 1});
153+
assert.deepStrictEqual(formatter.toJSON(), {
154+
type: 'issue',
155+
title: 'Issue Title',
156+
count: 5,
157+
id: 1,
158+
});
159+
});
160+
});
161+
162+
describe('toJSONDetailed', () => {
163+
it('formats a detailed issue', () => {
164+
const testGenericIssue = {
165+
details: () => {
166+
return {
167+
violatingNodeId: 2,
168+
violatingNodeAttribute: 'test',
169+
};
170+
},
171+
};
172+
const mockAggregatedIssue = getMockAggregatedIssue();
173+
const mockDescription = {
174+
file: 'mock.md',
175+
links: [{link: 'http://example.com', linkTitle: 'Link 1'}],
176+
substitutions: new Map([['PLACEHOLDER_VALUE', 'sub value']]),
177+
};
178+
mockAggregatedIssue.getDescription.returns(mockDescription);
179+
180+
mockAggregatedIssue.getAllIssues.returns([testGenericIssue] as any);
181+
182+
const mockDescriptionFileContent =
183+
'# Mock Issue Title\n\nThis is a mock issue description {PLACEHOLDER_VALUE}';
184+
185+
getIssueDescriptionStub
186+
.withArgs('mock.md')
187+
.returns(mockDescriptionFileContent);
188+
189+
const formatter = new IssueFormatter(mockAggregatedIssue, {
190+
id: 5,
191+
});
192+
193+
const result = formatter.toJSONDetailed();
194+
const detailedResult = result as IssueFormatterJsonDetailed;
195+
assert.strictEqual(detailedResult.id, 5);
196+
assert.strictEqual(detailedResult.type, 'issue');
197+
assert.strictEqual(detailedResult.title, 'Mock Issue Title');
198+
assert.strictEqual(
199+
detailedResult.description,
200+
'# Mock Issue Title\n\nThis is a mock issue description sub value',
201+
);
202+
assert.deepStrictEqual(detailedResult.links, mockDescription.links);
203+
assert.strictEqual(detailedResult.affectedResources.length, 1);
204+
assert.deepStrictEqual(detailedResult.affectedResources[0].data, {
205+
violatingNodeAttribute: 'test',
206+
violatingNodeId: 2,
207+
});
208+
});
209+
});
137210
});

0 commit comments

Comments
 (0)