Repository navigation
fix(accessibility): dogfooding fixes from 3 Nov — failed runs, build compare, re-scan, Local tunnels, report links - #553
Conversation
…f reading it as clean fetchAccessibilityIssues read runs that checked nothing as clean or complete reports: - A failed Website Scanner run exported an empty report and read as "0 issues ✅ All issues retrieved"; a failed Automated Tests build (the API's status for one that completed with no test cases) threw "Failed to fetch report: null". A failed scan run, build or Workflow Analyzer report is now an error that says so, with the scan's reasons from its logs, and no export is started. An app build's "failed" is its tests' result, so it is read as before. Comparing scan runs refuses a failed run, whose empty report read every issue of the other run as resolved or new. - A page that did not load had an error page (Chrome's, or BrowserStack Local's privoxy page) scanned in its place, and its issues came back as the page's. The report parser now sets such pages apart, as the comparison already did, keeping their rows out of the issues, totals, summary and duplicates; the read (and startAccessibilityScan) leads with a note. - A completed scan's pages that failed to scan were simply absent. Its scan logs are read with the export and the first page lists them (redirects aside, as those pages were scanned where they redirected to). With pages skipped, the last page no longer claims every issue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…untrusted data
The reasons a run or a page failed, and the pages a completed run could not scan, come from
the scan logs and the site: the page's address, its HTTP status, where it redirected. They
are now quoted in an untrusted-data block, as the report's issues and page addresses are,
with the sentences around them ("N pages were not scanned …:", "Fix the cause and scan
again.") left outside it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eading both
compareWith on a Workflow Analyzer report, an Automated Tests build or a manual app
report answered with an error ("... is neither"), and agents spent a call working
out that they should read both reports and compare them by hand. The answer now says
so directly, as guidance rather than an error: read both reports with
fetchAccessibilityIssues and match issues by rule and CSS selector on each page (by
rule and component for app reports); an issue in the later report only is new, in
the earlier only is fixed, in both is still open. compareWith naming a report that
is not an app build, for an app build, gets the same guidance.
The compareWith description now says which reports it compares (Scanner runs of one
scan, app builds) and to read both for any other.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compareWith on an Automated Tests (web) build answered "can't be compared here yet" and then, since 6a777ac, told the agent to read both builds and compare them by hand. In MCP dogfooding (3 Nov) 3 of 3 agents passed correct testhub ids and got no compare. compareWith on such a build (buildId, or its report link as reportUrl) now takes another build's id, its report link, or "previous", and compares them: - The older build, by start time, is "before" and the newer "after", whichever order they were passed in. When compareWith is the newer one the answer says so in one line. Both builds are named (name, project, #number, id, start), the names quoted as data. - Two builds of one build name and project are compared by the Automated Tests compare API (GET /api/automated-tests/v1/builds/compare/issues, newReport = after, baseReport = before). It takes the builds in the order given and does not check it. "previous" calls it without baseReport, and the answer names the build the API picked. Its issues are keyed by the API's own signatureKey, and the answer says it matches by that, not by rule, element and page alone. The API told apart a css-orientation-lock issue on <html> before and after a fix added lang to it, and listed it as both fixed and new. With a rule|element|page key on both, agents recounted the dashboard's 6/13/1 to 4/11/3. - Any other pair, or a pair the API refuses (issue-count threshold, "TestRun Not Found") or does not answer (504 after its 30 s, or no answer within 40 s), is compared by matching the two builds' issue exports by rule, element and page, as Scanner runs are. The answer says which method was used, in one line. - Either way the answer gives three lists with counts: new (only in after), fixed (only in before), still open (in both). Each list shows at most 50 issues and says how many more. For the API, a list cut short on its first page (100 rows) is read on its own. - Issues found on a BrowserStack Local or Chrome network error page are set apart as "page did not load". The diff also catches a page whose error-page issues are all duplicates of another page's, as a grouped build's are; before, that page's earlier issues read as fixed. - A build the build list calls "failed" (completed with no test cases) has no accessibility results: the answer says so as guidance and does not compare. "previous" with no earlier completed run of the build's name is said plainly, with no request to report it. The compareWith description now names web builds (21,495 of the 21,500 budget). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…of starting a new one startAccessibilityScan always created a new scan, so re-scanning a page after a fix made a second scan whose runs could not be compared with the first (fetchAccessibilityIssues compareWith failed with "Scan logs not found"). - New optional scanId. When that scan is for this same page (compared as startScan saves URLs: local hosts as bs-local.com, host case and a trailing slash ignored), start a new run of it (POST /scans/:id/new_scan) with its saved settings, wait for it like a new scan, and end with how to compare with compareWith "previous". Without scanId a new scan is created as before. - scanId for another page (or several pages): a new scan, saying why. - Settings passed that differ from the scan's saved ones: no run; each difference and the two ways forward are listed. Only settings the call passed are compared; a login or tunnel can only be checked for being saved at all, which is said. - A re-run keeps the scan's name (a different name passed is mentioned), and a Local scan's re-run says the tunnel must run with the saved identifier. - Re-run refusals as plain results: a run still going (with its run id), a deleted login config, an unknown scan, the plan's message. - Every new scan says how to scan the same page again with its scanId. - The 50-scan plan limit on create is said plainly instead of "open an issue on GitHub". - Shorter wording for a few accessibility argument descriptions, to stay within the description budget with the new argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…art; local-URL limits as plain answers Every public-URL accessibility scan and Live session ran `pkill -f BrowserStackLocal` (`taskkill /IM BrowserStackLocal.exe` on Windows), as did starting a tunnel for a local URL, so a tunnel the user had up (their own, or their test suite's) was killed by an unrelated call. - lib/local.ts no longer kills any process by name. A public URL takes no tunnel action at all. - A local-URL scan without localIdentifier runs through this MCP's own tunnel, started through browserstack-local under a unique mcp-<uuid> identifier, kept in module state and reused while it runs. It is replaced (the old one stopped through its own handle) only once it has died or the credentials change, and stopped by its own pid when the process exits or is signalled. The binary runs tunnels with different --local-identifier values side by side, so the user's keeps running; `force` is dropped from the configured options. Should ours fail to start while another Local is running, the error says to pass that tunnel's name as localIdentifier. - Live sessions (the other caller) start their unidentified tunnel only when no other BrowserStack Local is running, and otherwise use the one that is, leaving it alone. - A local URL the MCP cannot tunnel to (the hosted MCP, or USE_OWN_LOCAL_BINARY_PROCESS) is answered as a plain result with #541's guidance (start BrowserStack Local and pass its name as localIdentifier, or scan a public URL), with no GitHub or submitFeedback line. - The accessibility tools' error wrapper adds a full stop only when the message lacks one, so it no longer writes "..". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Website Scanner dashboard link is resolved by walking the scan list newest first. At the list's default 20 per page, 15 pages reached ~300 scans, so a link to an older scan came back "not found". 25 pages reach ~500, at about 1 s a page (~26 s worst case). Stopgap until the issues API reads by thBuildId (accessibility #5689). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…refusals as plain answers; expert questions within 500 chars
fetchAccessibilityIssues waited up to five minutes inside the call for a report still
finalizing (or an app build still running its rules). Clients cut off a call that long:
the claude.ai connector aborts a tool call silent for 305 s, progress notifications or
not, so the agent got nothing, and one reported a fix as verified that had added 26 issues.
- A first read of a report that is "finalizing" or "rule_processing" now answers at once.
It is not an error: it says no issues were read yet, so the page is not to be called
clean nor a fix verified, and gives the exact call to make again in a minute or two
(pageUrl and compareWith kept), or to call the report stuck after about 10 minutes.
The wait loop and REPORT_STATUS_POLL_MS / REPORT_STATUS_WAIT_MS are gone. A later page,
or a report whose tests are still running, is read as before with its "still
processing" note.
- A cancelled Website Scanner run is answered like a failed one (failedRunResponse): an
error saying it has no results and that 0 issues does not mean the page is clean,
instead of reading its empty export as "0 issues, all retrieved".
- Refusals the user resolves on their own account are explained instead of reported as
faults: the plan's saved-scan limit ("make over N scans"), a plan without the External
APIs add-on, and Website Scanner's "user info missing" (open Website Scanner once in the
dashboard first). Each says what to tell the user and quotes the API's words, with no
"open an issue on GitHub" line. They carry NO_FEEDBACK_HINT, a new symbol-keyed flag in
lib/feedback-hint.ts that is never serialised, so withFeedbackHint does not send the
agent to submitFeedback. (startAccessibilityScan's 402 scan-limit refusal is still
answered first by the scanner's ScanRefusedError; this covers the message wherever else
it reaches the error handler.)
- accessibilityExpert keeps a question within the knowledge base's 500-character limit,
which refused longer ones ("Query is too long"). A longer question keeps its last
sentence, where agents put the question itself, and as much of its opening as fits,
cut at a word break; the answer starts with a note that it was shortened.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the
|
Removes the six new test files and the test cases this PR added to existing files. Keeps only the edits that existing tests need because behaviour changed here: no 5-minute wait on a finalizing report, compareWith guidance on reports with no compare, failed-page wording, and no tunnel killing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolves report-fetcher.ts against GRR regional routing (#489): keeps the cached build lookup, and sends this branch's new Accessibility API calls (Automated Tests compare, build lookups, scan logs, scan overview, re-run, runs list) to the account's region instead of the US host. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…to fix/a11y-dogfooding-3-nov Conflicts: - accessibility.ts (fetchAccessibilityIssues): this branch's flow stays (Automated Tests build compare, readBothToCompare, status read without the 5-minute wait, failed/finalizing answers, unscanned pages). Into it go the report-links pieces: a build id is resolved with orScanRun; under compareWith a scanner run named by its build id is first mapped to its scan's ids (findScanRun), then compared as a scan; otherwise it is read by its build id before the status read, with the scan-list fallback and notes in the preface. The report-links refusal of compareWith on non-app sources is dropped, as this branch now answers those. - scanner.ts (failureDetails): this branch's version, already on scansApi(). - accessibilityScanOutput.test.ts: this branch's imports plus ScanRunNotFoundError; REPORT_STATUS_POLL_MS/REPORT_STATUS_WAIT_MS no longer exist here. MAX_SCAN_LIST_PAGES stays 25. The test files dropped in 7ee4c3d stay dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ocessing report Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…areWith, and compare runs oldest first compareScanRuns took compareWith verbatim as a run id, so a Website Scanner report link or a run's thBuildId failed with "Scan run id must be an Integer", a run id of another scan with "Scan logs not found", and a newer run id swapped new and resolved without a word. compareWith is now resolved as a source is: a link or 40-character id through sourceFromReportUrl / resolveBuild and findScanRun. A run of the same scan is compared with the older run as "before", saying so when the order was swapped. Another scan's run, a build or a report, and a run id the scan's full run list does not hold, get a plain answer naming both reads' arguments. compareWith's description drops "older": the runs are ordered here now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssing
Comparing against a run still processing gave a clean-looking diff ("0 new, 0
resolved, 2 still open") while its issues were still being added, which could
read a fix as verified. The Website Scanner, Automated Tests and app build
compares now check both sides' statuses first: while either is processing or
finalizing, the answer is a plain "not finished yet" naming the run or build
and its status, telling the agent to have the user ask again in a few minutes
and not to call again in this turn.
The Automated Tests compare's "only the issues found so far are compared"
warning goes, as such a build is no longer compared. An app build compare is
dispatched before the finalizing read, so a build still running its rules
gets this answer rather than "call again in a minute or two".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d in the scan list A Website Scanner run's thBuildId passed as buildId, with no link, was refused whenever the API could not yet read a run by its build id: "pass the scan's report link as reportUrl instead", plus the GitHub and submitFeedback lines. An agent then searched for the run itself, and on an old scan gave up. Such an id now walks the scan list with findScanRun, as a link does, under the same 25-page cap and with the same "found in the scan list instead" note, naming the scan and run it found. Only when the walk finds nothing (or the API that reads runs by build id has none) is the answer a plain not-found, saying what to pass instead, without the GitHub or submitFeedback lines. findScanRun's not-found is now a ScanNotInListError, with the same message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dId as its accessibility build
An SDK run prints the Automate build's hashed_id ("stopBinSession
hashed_id=…"), and an agent passing it as buildId was refused, with only a
Website Scanner build id offered as an alternative. It then spent recovery
calls on fetchBuildInsights and listSessions before finding a session link.
A 40-hex id that is neither an Automated Tests nor an App Accessibility build
is now tried as an Automate build, then an App Automate build, through the same
session mapping their dashboard links use, before the scan-list walk. The
answer says which build the id was read as. An id none of them knows says so
in the plain not-found answer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…report yet", not "Failed to fetch report: null" A build stuck in "running" since 2025-09-16 was read as processing, so its export ran anyway and failed with error null: "Failed to fetch report: null. Please open an issue on GitHub", and the agent went searching for another product before handing the question back to the user. When a report is still processing and its export fails, the answer is now a plain "still running (status, started <date>), no report yet". If it started more than 24 hours ago it is called probably stuck, to be run again, rather than something to call again for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for compareWith "previous" compareWith "previous" on a Workflow Analyzer report only said there is no built-in compare and to read both, without saying which report came before. Agents went looking for a way to list reports, hit routing refusals, and the hosted run gave no answer. The earlier report is now found here: the account's report list (newest first) locates this report, and the summaries of the completed reports after it give their start and pages. The newest one before it that scanned one of its pages is named, or else the newest on its site, with its id, its dashboard link and the two reads to make. When none is among the newest reports, the answer says so and to ask the user which report to compare with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ers, describe cursor, and find its previous report A 70-issue manual report came back as one page of 51,132 characters, over Claude Code's 50,000-character limit, so the client saved it to a file. The page budget counted only the bare issue rows, while the answer prints them as indented JSON with the rules and screens they name. An app page (build or manual report) now counts each issue as printed, and each rule's text and screen's screenshot the first time the page names it, against a 35,000 character budget: with the summary, notes and wrappers the whole answer stays under about 40,000. The cursor still counts the bare issues. Agents then passed cursor as an issue index (40, 35) and got the same page again. cursor is now described as the next_page value from the previous answer, not an issue index. compareWith "previous" on a manual report now names the newest finished manual report of the same app recorded before it (found through the report list, by the report's name and then its app's package), with its id, its dashboard link and the two reads to make, as for a Workflow Analyzer report. Descriptions (total unchanged at 21,500): - cursor: "Pagination offset (default 0); with compareWith, issues already listed." (71) -> "The next_page value from the previous answer (default 0), not an issue index." (77) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… page address that parses Follow-up to the "previous" Workflow Analyzer lookup: a page address that does not parse has no site, and two such addresses no longer count as one site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
compareWith's description ended "Others: read both", so for a Workflow Analyzer or manual app report agents never passed "previous" and went searching for the earlier report themselves: in re-runs, 4 of 5 sessions skipped it, and one hosted run again gave no answer. "previous" now finds the earlier report for those too, and the tool itself answers any pair it has no compare for with how to read both, so the description says "previous" works for any report. compareWith: 'Runs of one scan, web/app builds: "previous", scanRunId, build id/link. Others: read both.' (90) -> '"previous" (any report), or a run of the same scan or a web/app build: its id or link.' (86). Total 21,500 -> 21,496. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ack hint as they are on #541 Restores src/lib/local.ts, src/lib/feedback-hint.ts and src/tools/live-utils/start-session.ts to their #541 versions, which puts back killExistingBrowserStackLocalProcesses and drops ensureOwnTunnel, stopOwnTunnels, killOwnTunnelsNow and NO_FEEDBACK_HINT. startScan again starts a tunnel with ensureLocalBinarySetup for a local URL and calls killExistingBrowserStackLocalProcesses for a public one. The refusals for the hosted MCP and for USE_OWN_LOCAL_BINARY_PROCESS with a local URL stay ScanRefusedError plain answers, and account refusals keep their wording without the feedback-hint opt-out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elpers into accessiblity-utils accessibility.ts had grown to 3,337 lines. The helpers this PR added move, unchanged, into accessiblity-utils: - error-messages.ts: asSentence and the account-refusal wording - expert-query.ts: fitting an accessibilityExpert question to 500 characters - report-answers.ts: failed, cancelled, finalizing and no-report-yet answers, unknown build ids, and pages not scanned or that showed an error page - compare-with.ts: what compareWith names (a scan run, the "previous" report), the read-both guidance and the not-finished-yet answer - compare-lists.ts: the export diff, compared lists and not-loaded note that scan-run and web-build comparisons share - web-build-compare.ts: comparing Automated Tests (web) builds - scan-rerun.ts: the saved-settings checks, notes and hints of a re-run shared.ts takes the small helpers of accessibility.ts these need too (the response builders, source names, progress, the scanner client, WCAG names), so neither side imports the other. accessibility.ts re-exports EXPERT_QUERY_MAX_CHARS. No behaviour or tool description changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sarve-shreyas
left a comment
There was a problem hiding this comment.
Reviewed cbe1c17..2649ced (this PR on top of #541). npm run build passes locally on this head: 142 files, 2,103 tests.
Blocking (outside the inline comments): the new behaviour has no new tests. This PR adds web-build compare, scan re-run, compareWith resolution (link / build id / Automate hashed_id), the 40k manual-report paging and expert-question shortening, about 3k lines in all. 7ee4c3d dropped the new test files, and the test diff only rewrites existing cases to match the new behaviour (the suite is one test smaller than #541's). Please bring back tests for at least web-build compare (ordering, refusal of processing/failed builds, the diffExports fallback), rerunAccessibilityScan (same page vs other page, settings mismatch) and the hashed_id path.
The refactor itself looks clean. The moved helpers (sourceArgs, describeSource, exportProgress, scanDashboardNote and others) are unchanged apart from export, and the scanner.ts Local/tunnel code matches #541 after b878a2b.
Non-blocking (outside the diff): stale task id while processing. report-fetcher.ts rememberExport(key, { taskId }) (around L720) stores the task id even when stillProcessing is true. If that export is still pending when the call ends, the next call after the report finishes reuses known.taskId. It then gets the snapshot taken mid-processing and caches it as final. Removing the finalizing wait makes this path more likely. Suggest not remembering the task id while stillProcessing, or storing the flag with it.
Questions I couldn't verify against the API:
- While a re-run is in progress, do the scan list's
scanUrl(thBuildId) andlastScanDetails.scanRunIdpoint at the same run? If not,findScanRunmaps the older run's link to the newer run. - What does Automate return for a 40-hex id it doesn't know, or one from another account (see the hashed_id comment)?
- Does the knowledge base count its 500-char limit in UTF-16 units, code points or bytes?
| : [ | ||
| `${renameReason(name, startResp.nameChanges ?? ["taken"])}, so this scan was started as "${scanName}".`, | ||
| ], | ||
| [rescanHint(String(scanId))], |
There was a problem hiding this comment.
Blocking: rescanHint is added to every scan, including local URLs the MCP tunnelled itself, and re-running those can't reach the page.
For a local URL with no localIdentifier, startScan saves the scan with randomUUID() as its identifier. That tunnel is killed by the next public-URL scan (killExistingBrowserStackLocalProcesses) or by a restart. rerunScan starts no tunnel, and the API doesn't return the saved identifier, so localRerunNote asks the user for an identifier nobody was ever shown.
Scenario: scan http://localhost:3000 without localIdentifier, scan any public URL, then call startAccessibilityScan with the same scanId. The new run scans the Local error page, and compareWith: "previous" shows the page as not loaded.
Suggestion: any one of these works:
- leave
rescanHintoff MCP-tunnelled local scans; - when the saved scan has
localTestingEnabledand nolocalIdentifieris passed, start a new scan instead of re-running; - give MCP-started tunnels a stable identifier and start that tunnel in
rerunAccessibilityScan.
| reportFetcher.reportStatus(run(after)), | ||
| reportFetcher.reportStatus(run(before)), | ||
| ]); | ||
| if (status === "failed" || beforeStatus === "failed") { |
There was a problem hiding this comment.
Non-blocking: only "failed" is refused here. A cancelled run also exports an empty report, and this PR already treats it as failed for reads (the processing.status === "cancelled" branch around L1008). As it stands, comparing against a cancelled run reports every issue of the other run as resolved or new. Suggest treating cancelled the same way and passing the status into failedScanRunResponse.
| // id has other letters too), as the SDK prints it at the end of a run: its sessions name | ||
| // the accessibility build they ran in, as for the build's link. Asked before the scan list. | ||
| if (/^[0-9a-f]{40}$/.test(source.buildId)) { | ||
| const session = await this.automateSession(source.buildId); |
There was a problem hiding this comment.
Non-blocking: this probe ends the call instead of falling through. automateSession throws on any non-404 status and on a 200 with no sessions ("has no sessions"). So a 40-hex id that Automate answers with 401/403/5xx, or an empty Automate build, never reaches the App Automate check or the orScanRun / not-found fallbacks below. Suggest wrapping the probe in try/catch and falling through (you could keep the error as a note if nothing else matches).
| * at limit=50 or 100 the cursor skips scans, so a larger page cannot be used to go faster. | ||
| */ | ||
| export const MAX_SCAN_LIST_PAGES = 15; | ||
| export const MAX_SCAN_LIST_PAGES = 25; |
There was a problem hiding this comment.
Non-blocking: 25 pages at about 1.2 s each is about 30 s per findScanRun walk. A compare where both sides are scanner links or build ids walks twice, which is about 60 s before the export wait (up to EXPORT_WAIT_MS, 45 s). Web builds can also hit the compare API's 40 s timeout, then the diffExports fallback wait (web-build-compare.ts around L340). The EXPORT_WAIT_MS comment says a call stays well under the 60 s request timeout, which no longer holds. Suggest a shared per-call time budget, a small cache of thBuildId → ids, or returning the pending answer after a compare-API timeout.
| : undefined, | ||
| ]); | ||
| // The older build is "before": by start, whichever was passed as compareWith. | ||
| const started = (b: WebBuild) => Date.parse(b.startedAt ?? ""); |
There was a problem hiding this comment.
Non-blocking: when either build has no startedAt, Date.parse("") is NaN, so reversed is always false. An explicit compareWith is then always taken as "before", and new/fixed swap silently if the user passed the newer build there. Suggest falling back to build number / creation order, or saying the order couldn't be determined.
| * that identifier, or the run cannot reach the page. | ||
| */ | ||
| export function localRerunNote(localIdentifier: string | undefined): string { | ||
| return `This scan reaches its page through BrowserStack Local, and the run uses the scan's saved Local settings: a tunnel must be running with the same local identifier the scan was created with${ |
There was a problem hiding this comment.
Non-blocking (wording): the PR body says "If the settings passed differ from the saved ones, nothing runs." But authConfigId (when a login is saved) and localIdentifier (when Local is saved) aren't compared, see uncheckedSettings. The run goes ahead with the saved ones, with only a note. Re-running with a different authConfigId therefore scans with the old login. Either refuse when one of these is passed and can't be checked, or reword the PR body and the scanId description.
| return `Scan ${scanId}'s pages could not be read, so it could not be checked to be for ${pageURL}; a new scan was started.`; | ||
| } | ||
| if (urlList.length === 1) { | ||
| return `Scan ${scanId} is for ${urlList[0]}, not ${pageURL}, so a new scan was started.`; |
There was a problem hiding this comment.
Non-blocking: the saved scan's urlList is printed here without wrapUntrusted, and so is the saved name in the re-run messages in accessibility.ts (the "was not re-run" and "Started a new run" lines). Both come from the account's saved scan, which another user on the account may have created, so they should get the same quoting as the failure reasons. wrapUntrusted itself looks solid (per-call nonce).
| if (query.length <= EXPERT_QUERY_MAX_CHARS) | ||
| return { query, shortened: false }; | ||
| const text = query.trim(); | ||
| const sentences = text.match(/[^.?!]+(?:[.?!]+["')\]]*\s*|$)/g) ?? [text]; |
There was a problem hiding this comment.
Non-blocking: the sentence split only knows ASCII .?!, so CJK text (。?!) is one "sentence" and gets cut at 500, losing the actual question. The same happens whenever the last sentence is over 300 chars. Yet the note in accessibility.ts (~L176) always says the opening and last sentence were sent. Also, .slice on UTF-16 units can split a surrogate pair. Suggest adding the CJK terminators, cutting on a code-point boundary, and making the note match the branch that ran.
| */ | ||
| const SCAN_LIMIT_REFUSAL = /make over \d+ scans/i; | ||
|
|
||
| export const SCAN_LIMIT_MESSAGE = |
There was a problem hiding this comment.
Nit: SCAN_LIMIT_MESSAGE hard-codes "50", while accountRefusal reads the number from the API's message. Use the captured number so the message stays right if the plan limit differs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…robe falls through - An export started while a report is still processing no longer has its task remembered: continued after the report finished, it handed back the mid-processing snapshot as final. - A run found by its build id is cached (per account, bounded), since a build id names one fixed run: a link read again, or both sides of a compare, skip the scan-list walk. The EXPORT_WAIT_MS comment no longer claims a whole call stays under 60 s. - A 40-hex id that Automate or App Automate can't answer about (an HTTP failure other than 404, or an Automate build with no sessions) moves on to the next API instead of ending the call, and the not-found answer says why. Once a session is found, what it says is still the answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; say what a re-run ignores - A local page this MCP tunnelled itself is saved with an identifier nobody was shown, and the tunnel is gone after the next public scan or a restart. Its scan no longer gets the "re-run with scanId" hint, and a re-run asked for a scan saved with Local but no localIdentifier starts a new scan instead, saying why and how to re-run it with its own tunnel. - A re-run can't take a login or a Local identifier: each passed one is now named as not used, with how to scan with it (a new scan), instead of only "could not be checked". - The saved scan's pages and name come from the account, which another user may have set: they are now quoted as untrusted data in re-run answers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ith a failed one A cancelled run has no results and exports an empty report, so compared with it every issue of the other run read as resolved or new. Reads already treat a cancelled run as having no results; compares now do too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d when start times are missing A build with no start time made the order check always false, so a newer build passed as compareWith was used as "before" and new and fixed issues swapped without a word. The order now falls back to the build number for runs of one build name and project; with neither, the passed order is kept and the answer says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… what was kept The sentence split knew only . ? !, so a Chinese or Japanese question was one "sentence" cut at 500, losing the question at its end. 。?! now end sentences too. A cut no longer leaves half a surrogate pair, and the note says whether the opening alone or the opening and last sentence were sent, as it happened. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rd-coded 50 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in a re-run refusal The quoted name sat between "these differ:" and the list, so it read as one of the differences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sarve-shreyas
left a comment
There was a problem hiding this comment.
Re-reviewed 2649ced..85f3f09 (eight fix commits plus the #541 merge). npm run build passes locally: 142 files, 2,103 tests.
Resolved:
- Local re-run (was blocking): no re-run hint for local pages this MCP tunnelled, and a re-run of a Local scan without
localIdentifierstarts a new scan and says why (7d73b1e). - Cancelled runs are refused in compares, like failed ones (4c59ace).
- The Automate hashed_id probe falls through on a non-404 or an empty build, and the not-found answer names the probe failure (3377151).
- Scan-list walks:
thBuildId → idsis cached per account, bounded at 200, and the 60 s claim in theEXPORT_WAIT_MScomment is corrected. - The stale task id while processing is no longer remembered.
- Web-build order: falls back to build number, otherwise says the order is unknown (cbf63d0).
- A passed
authConfigId/localIdentifieron a re-run is now named as not used; the saved name and pages are wrapped as untrusted. - Expert questions: CJK terminators, no split surrogate pair, and the note says what was kept.
- The scan limit comes from the API message.
Still open (blocking): the new behaviour still has no new tests. The suite is 2,103 on this head, the same as before this round, and one fewer than #541's. This round's fixes (re-run local handling, cancelled-run compare, probe fall-through, the build-id cache, NaN ordering) are also untested. Please add tests for at least web-build compare (ordering including the build-number fallback, refusal of processing/failed builds, the diffExports fallback), rerunAccessibilityScan (same vs other page, settings mismatch, Local without identifier), and the hashed_id probe.
Non-blocking: foundScanRuns caches the thBuildId → scanRunId mapping for the life of the process. That is right only if the scan list's scanUrl and lastScanDetails.scanRunId always describe the same run (my open question from last round). If they can disagree while a re-run is starting, a wrong mapping is now cached instead of being corrected on the next walk.
…d the Automate id probe Brings back the web-build compare, re-run and failed-run tests dropped in 7ee4c3d, updated to this PR's current behaviour, and adds cases for the review fixes: - web-build compare: refusing a build still processing, ordering by build number when a start time is missing, and keeping the passed order with a note otherwise; - re-runs: a Local-saved scan without localIdentifier starts a new scan, a local page the MCP tunnelled gets no re-run hint, a login or tunnel passed is named as not used, the saved pages and name are quoted as untrusted, and the scan limit is the API's number; - a 40-hex Automate or App Automate build id: found, falling through on 403/5xx or an empty build, and naming why Automate could not be asked; - compare refused for a cancelled run; - exports: no task kept while a report is processing, a run read by build id reused for 5 minutes at most, and a link's run found once not looked up again; - expert questions: the note matches what was cut, CJK sentence ends, no split character. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sarve-shreyas
left a comment
There was a problem hiding this comment.
Re-reviewed 85f3f09..7397791 (test-only commit). npm run build passes locally: 147 files, 2,205 tests (+102).
Resolved (was blocking): the new behaviour now has tests. Six files cover:
- Web-build compare, in
accessibilityWebBuildCompare.test.ts: ordering including the build-number fallback and unknown order, refusing processing/failed builds, the self-matching fallback, list caps and error pages. - Re-runs, in
accessibilityRescan.test.ts: same vs other page, settings mismatch, login/tunnel not used, Local without identifier starting a new scan, no hint for MCP-tunnelled pages. - The Automate hashed_id probe and its fall-through, in
accessibilityAutomateBuildId.test.ts. - Failed and cancelled runs, including compares.
- Expert-question shortening (CJK, surrogate pairs).
- The build-id cache, the stale task id, and the 5-minute
scanBuildTTL.
I spot-checked the key cases: they assert behaviour (e.g. rerunScan not called, startScan called once, newReport/baseReport order), not only output strings.
No blocking findings left from me. One non-blocking open question from last round remains: foundScanRuns is safe only if the scan list's scanUrl and lastScanDetails.scanRunId always describe the same run.
sarve-shreyas
left a comment
There was a problem hiding this comment.
Approved at 7397791. All findings are fixed and the new behaviour is covered by tests. Build passes locally, 2,205 tests. One non-blocking question is still open: the foundScanRuns cache assumes the scan list's scanUrl and lastScanDetails.scanRunId always describe the same run.
Stack: #527 → #541 → #553 (this, based on #541). Merge in that order.
Fixes from the accessibility MCP dogfooding on 3–4 Nov, combined into one PR, plus #552's changes (closed in favour of this). Each change is its own commit.
A failed run is no longer read as a clean page
Before: a failed Website Scanner run came back as "0 issues ✅ All issues retrieved". A failed Automated Tests build threw "Failed to fetch report: null". Issues found on an error page (Chrome's, or the BrowserStack Local page shown when the tunnel is down) were counted as the real page's issues. Pages a completed scan could not reach were simply missing from the answer.
After: a failed scan run, build or Workflow Analyzer report returns an error that says nothing was checked, with the scan's own reasons, and no export is started. A cancelled scan run gets the same answer. Pages that showed an error page are set apart and left out of the totals, with a note to check the page or tunnel and scan again. The first page of a read lists the pages that were not scanned, and the last page no longer says every issue was retrieved. Comparing against a failed or cancelled run is refused, because every issue would otherwise read as fixed or new.
Scan failure reasons are quoted as untrusted data
Before: failure reasons and page lines from the scan logs (URLs, HTTP statuses, redirect targets) were printed inline with the agent's instructions.
After: they sit in an untrusted-data block, like report issues already do. The guidance around them stays outside the block.
compareWith on a report that has no compare
Before: compareWith on a Workflow Analyzer report or a manual app report returned an error ("… is neither"), and the agent spent a call working out what to do next.
After: the answer tells the agent to read both reports and match issues by rule and selector on each page (by rule and component for app reports). It comes back as guidance, not as an error.
Compare Automated Tests builds
Before: compareWith on an Automated Tests (web) build was not supported. In dogfooding, all 3 agents passed correct testhub ids and got no comparison.
After: compareWith accepts another build's id, its report link, or
"previous". The older build is always treated as "before". When both builds share a build name and project, the dashboard's compare API is used. Otherwise, or when the API refuses or times out, the two exports are matched by rule, element and page. The answer says which method it used and gives new, fixed and still-open lists with counts. Issues found on error pages are set apart. A failed build (no test cases) is reported as such and is not compared.Re-run an earlier scan instead of creating a new one
Before: startAccessibilityScan always created a new scan. Re-scanning a page after a fix therefore produced a second scan, and comparing it with the first failed with "Scan logs not found".
After: a new optional
scanIdre-runs that scan with its saved settings, as long as it scans the same page, socompareWith: "previous"works. If the rule settings passed differ from the saved ones, nothing runs and the answer lists each difference. A login (authConfigId) or Local identifier can't be checked, since the API doesn't return them, so the run goes ahead with the saved ones and says each passed one was not used. A scan saved with Local is re-run only whenlocalIdentifieris passed; otherwise a new scan is started, and says why. A local page the MCP tunnelled itself gets no re-run hint, since its tunnel's identifier was never shown. Expected refusals are plain answers, not errors: a run still in progress, an unknown scan, a deleted login config, or the plan's saved-scan limit. Every new scan of a public page says how to re-run it.Plain answers when a local URL can't be scanned
When the hosted MCP, or
USE_OWN_LOCAL_BINARY_PROCESS, can't tunnel to a local URL, the answer explains how to pass your own tunnel's name. It comes back as a plain answer, not an error with a "report on GitHub" line. BrowserStack Local tunnel handling itself is unchanged from #541.Find older scans from a report link
Before: resolving a Website Scanner dashboard link searched 15 pages (about 300 scans), so a link to an older scan came back "not found".
After: it searches 25 pages (about 500 scans), and a build id it has found once is remembered for the life of the process, so a repeat lookup or compare skips the search. This is a stopgap until the issues API can read by
thBuildId.No 5-minute wait on a finalizing report; account refusals as plain answers; expert questions within 500 characters
Before: fetchAccessibilityIssues could wait up to 5 minutes for a report that was still finalizing. The claude.ai connector cuts off a tool call after about 305 s of silence, so the agent got nothing back. In one case it reported a fix as verified when the fix had added 26 issues. Plan limits, a missing External APIs add-on, and Website Scanner's "user info missing" all read as faults, so agents retried, renamed the scan, or sent submitFeedback. accessibilityExpert questions over 500 characters were refused with "Query is too long".
After:
Friction found in end-to-end testing of this PR, fixed
84 isolated agent sessions on this branch found 7 places where agents needed extra calls, hit errors, or gave no answer:
compareWithis resolved to its run. The older run is always "before" (this also fixes a silent new/fixed swap). A run from another scan gets plain "read both" guidance instead of "Scan run id must be an Integer" or "Scan logs not found".buildIdis found by walking the scan list.hashed_id(as the SDK prints it) is read as that build's accessibility report.compareWith: "previous"on a Workflow Analyzer or manual app report names the earlier report to read.cursoris described as thenext_pagevalue.Re-run: all 13 sessions that had friction are seamless, and 7 regression sessions give the same answers.
Review fixes (6 Nov)
Testing
localIdentifier, the re-run hint), failed and cancelled runs, the 40-character Automate id lookup and its fall-through, export reuse while processing and by build id, the build-id lookup cache, the scan-limit message and expert-question shortening. Tests for code reverted from this PR (Local tunnel handling) are left out.src/tools/accessiblity-utils/(web-build-compare.ts,compare-with.ts,compare-lists.ts,report-answers.ts,scan-rerun.ts,error-messages.ts,expert-query.ts, andshared.tsfor small existing helpers they need), soaccessibility.tsgrows by about 110 lines rather than 1,500.main. The new Accessibility API calls (Automated Tests compare, build lookups, scan logs, scan overview, re-run, runs list) go to the account's region (feat(lib): route Automate, App Automate, Accessibility and Observability to the account's GRR region #489), not the US host.vitest run: 147 files, 2,205 tests pass.tsc,eslintandprettier --checkare clean.thBuildId) andlastScanDetails.scanRunIdnever pointed at different runs (25 samples). The link is absent for the run's first ~25 s, then always carries the new run's own id, so a remembered build id → run mapping stays correct.🤖 Generated with Claude Code