fix: round lighthouse category scores and show unscored categories as n/a (#2924)

The `lighthouse_audit` text summary formatted each category score as
`(score ?? 0) * 100`. That had two problems:

- A `null` category score, which Lighthouse uses for "not scored", was
printed as `0`, so it looked the same as a real score of 0 (#2915).
- Fractional scores showed floating-point noise, e.g. `0.57` printed as
`56.99999999999999` (#2916).

The summary now prints `Math.round(score * 100)`, which matches how the
Lighthouse report renders category scores, and prints `n/a` for a `null`
score. Structured content (`structuredContent.lighthouseResult`) is
unchanged and still carries the raw scores, including `null`. The text
summary is the only place these scores are formatted.

Testing:
- Added a `McpResponse` unit test and snapshot covering `0.57` → `57`, a
real `0` → `0`, and `null` → `n/a`, plus a check that the structured
content keeps the raw `0.57`/`0`/`null`. The new test fails on the old
code.
- `tests/McpResponse.test.ts` and `tests/tools/lighthouse.test.ts` pass
locally.
- `npm run check-format` passes, and `npm run gen` leaves no diff.

Fixes #2915
Fixes #2916
This commit is contained in:
yulunz authored and GitHub committed 2026-10-12 08:53:43 +00:00
1 parent 93e06ab21f
commit 70076808cd
3 files changed
+111 -3

No files matched your search

+5 -3
View File
@@ -1174,9 +1174,11 @@ Call ${handleDialog(this.#args).name} to handle it before continuing.`);
response.push(`URL: ${summary.url}`);
response.push('### Category Scores');
for (const score of summary.scores) {
response.push(
`- ${score.title}: ${(score.score ?? 0) * 100} (${score.id})`,
);
// Lighthouse uses a null score for categories that could not be
// scored and renders scores as Math.round(score * 100).
const formattedScore =
score.score === null ? 'n/a' : Math.round(score.score * 100);
response.push(`- ${score.title}: ${formattedScore} (${score.id})`);
}
response.push('### Audit Summary');
response.push(`Passed: ${summary.audits.passed}`);
+58
View File
@@ -1367,6 +1367,64 @@ exports[`lighthouse > includes lighthouse report paths 2`] = `
}
`;
exports[`lighthouse > rounds category scores and shows unscored categories as n/a 1`] = `
## Lighthouse Audit Results
Mode: navigation
Device: desktop
URL: https://example.com
### Category Scores
- Accessibility: 57 (accessibility)
- SEO: 0 (seo)
- Agentic Browsing: n/a (agentic-browsing)
### Audit Summary
Passed: 10
Failed: 1
Total Timing: 1000ms
### Reports
- /tmp/report.json
- /tmp/report.html
`;
exports[`lighthouse > rounds category scores and shows unscored categories as n/a 2`] = `
{
"lighthouseResult": {
"summary": {
"mode": "navigation",
"device": "desktop",
"url": "https://example.com",
"scores": [
{
"id": "accessibility",
"title": "Accessibility",
"score": 0.57
},
{
"id": "seo",
"title": "SEO",
"score": 0
},
{
"id": "agentic-browsing",
"title": "Agentic Browsing",
"score": null
}
],
"audits": {
"failed": 1,
"passed": 10
},
"timing": {
"total": 1000
}
},
"reports": [
"/tmp/report.json",
"/tmp/report.html"
]
}
}
`;
exports[`third-party developer tools > lists third-party developer tools 1`] = `
## Third-party developer tools
My Tool Group: A group of tools
+48
View File
@@ -930,6 +930,54 @@ describe('lighthouse', () => {
t.assert.snapshot(stabilizeStructuredContent(structuredContent));
});
});
it('rounds category scores and shows unscored categories as n/a', async t => {
await withMcpContext(async (response, context) => {
const lighthouseResult = {
summary: {
mode: 'navigation',
device: 'desktop',
url: 'https://example.com',
scores: [
{
id: 'accessibility',
title: 'Accessibility',
score: 0.57,
},
{
id: 'seo',
title: 'SEO',
score: 0,
},
{
id: 'agentic-browsing',
title: 'Agentic Browsing',
score: null,
},
],
audits: {
failed: 1,
passed: 10,
},
timing: {
total: 1000,
},
},
reports: ['/tmp/report.json', '/tmp/report.html'],
};
response.attachLighthouseResult(lighthouseResult);
const {content, structuredContent} = await response.handle(context);
const text = getTextContent(content[0]);
assert.ok(text.includes('- Accessibility: 57 (accessibility)'));
assert.ok(text.includes('- SEO: 0 (seo)'));
assert.ok(text.includes('- Agentic Browsing: n/a (agentic-browsing)'));
t.assert.snapshot(text);
t.assert.snapshot(stabilizeStructuredContent(structuredContent));
});
});
});
describe('third-party developer tools', () => {