From 13c06884935a7f4602b89c114a9a46d6a6491417 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Wed, 8 Apr 2026 22:41:00 +0800 Subject: [PATCH 1/5] fix(EdgedriverBinary): switch listing to msedgedriver.microsoft.com/listing.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old Azure Blob container listing API stopped working: https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver?prefix=/&delimiter=/&comp=list started returning HTTP 409 `PublicAccessNotPermitted` around 2026-04-07, after Microsoft disabled public access on the storage account. The old `msedgedriver.azureedge.net` CDN had already stopped resolving in July 2025 (see MicrosoftEdge/EdgeWebDriver#203, MicrosoftEdge/EdgeWebDriver#201, seleniumbase/SeleniumBase#3888). This broke `EdgedriverBinary.fetch()` for any version subpath and made `test/common/adapter/binary/EdgedriverBinary.test.ts` fail on the `items.length >= 3` assertion. Microsoft now hosts Edge WebDriver downloads on `msedgedriver.microsoft.com`, and the same host serves a single JSON dump that replaces the Azure Blob XML listing: GET https://msedgedriver.microsoft.com/listing.json -> { items: [{ isDirectory, name, contentLength, lastModified }, ...], generatedAt: } (This is the endpoint used by Microsoft's own catalog at https://msedgedriver.microsoft.com/catalog/.) The adapter now fetches this listing, filters by the requested version prefix, and exposes each matching file as a `BinaryItem` pointing at `https://msedgedriver.microsoft.com/` for download. Also mock the new endpoint in the EdgedriverBinary test with a fixture (`test/fixtures/msedgedriver-listing.json`) so the test is no longer dependent on live Microsoft endpoints. Known coverage regression: the new `listing.json` only covers Edge WebDriver versions from 112.0.1722.39 onwards (~2003 versions / 9000+ files as of 2026-04-08). Versions older than 112 are no longer downloadable from any public Microsoft endpoint — they return HTTP 404 on `msedgedriver.microsoft.com//edgedriver_*.zip` and HTTP 409 `PublicAccessNotPermitted` on the retired Azure Blob container. This is a Microsoft-side change; cnpmcore users will have to rely on previously mirrored copies for Edge WebDriver versions < 112. No formal Microsoft announcement for this specific storage lockdown, but the migration has been discussed across multiple community projects: - MicrosoftEdge/EdgeWebDriver#183 (azureedge.net CDN sunset, 2025-01) - MicrosoftEdge/EdgeWebDriver#146 (blob listing stopped updating since 125.0, 2024-05) - MicrosoftEdge/EdgeWebDriver#201, #203 (azureedge.net unreachable, 2025-07/08) - seleniumbase/SeleniumBase#3888 (CDN moved to msedgedriver.microsoft.com, fixed in SeleniumBase 4.40.6) Co-Authored-By: Claude Opus 4.6 (1M context) --- app/common/adapter/binary/EdgedriverBinary.ts | 98 +++++++++++-------- .../adapter/binary/EdgedriverBinary.test.ts | 12 ++- test/fixtures/msedgedriver-listing.json | 35 +++++++ 3 files changed, 99 insertions(+), 46 deletions(-) create mode 100644 test/fixtures/msedgedriver-listing.json diff --git a/app/common/adapter/binary/EdgedriverBinary.ts b/app/common/adapter/binary/EdgedriverBinary.ts index e06526771..eb2e72d1c 100644 --- a/app/common/adapter/binary/EdgedriverBinary.ts +++ b/app/common/adapter/binary/EdgedriverBinary.ts @@ -1,10 +1,28 @@ -import path from 'node:path'; - import { SingletonProto } from 'egg'; import { BinaryType } from '../../enum/Binary.ts'; import { AbstractBinary, BinaryAdapter, type BinaryItem, type FetchResult } from './AbstractBinary.ts'; +// Microsoft moved the Edge WebDriver download listing off the public +// Azure Blob container (the old XML listing API returns +// `PublicAccessNotPermitted` since ~2026-04-07). The new source of truth +// is a single JSON dump at https://msedgedriver.microsoft.com/listing.json +// which lists every version-prefixed driver file; individual files are +// downloadable from https://msedgedriver.microsoft.com/. +const EDGEDRIVER_LISTING_URL = 'https://msedgedriver.microsoft.com/listing.json'; +const EDGEDRIVER_DOWNLOAD_BASE = 'https://msedgedriver.microsoft.com/'; + +interface EdgedriverListingEntry { + isDirectory: boolean; + name: string; + contentLength: number; + lastModified: string; +} +interface EdgedriverListing { + items: EdgedriverListingEntry[]; + generatedAt: string; +} + @SingletonProto() @BinaryAdapter(BinaryType.Edgedriver) export class EdgedriverBinary extends AbstractBinary { @@ -169,51 +187,47 @@ export class EdgedriverBinary extends AbstractBinary { } // fetch sub dir - // /foo/ => foo/ + // /126.0.2578.0/ => 126.0.2578.0/ const subDir = dir.slice(1); - // https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver?prefix=124.0.2478.97/&delimiter=/&maxresults=100&restype=container&comp=list - const url = `https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver?prefix=${encodeURIComponent(subDir)}&delimiter=/&maxresults=100&restype=container&comp=list`; - const xml = await this.requestXml(url); - return { items: this.#parseItems(xml), nextParams: null }; - } - - #parseItems(xml: string): BinaryItem[] { + const listing = await this.#fetchListing(); + if (!listing) { + return { items: [], nextParams: null }; + } const items: BinaryItem[] = []; - // 124.0.2478.97/edgedriver_arm64.ziphttps://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/124.0.2478.97/edgedriver_arm64.zipFri, 10 May 2024 18:35:44 GMT0x8DC712000713C139191362application/octet-stream1tjPTf5JU6KKB06Qf1JOGw==BlockBlobunlocked - const fileRe = - /([^<]+?)<\/Name>([^<]+?)<\/Url>([^<]+?)<\/Last-Modified>(?:[^<]+?)<\/Etag>(\d+)<\/Content-Length>/g; - const matchItems = xml.matchAll(fileRe); - for (const m of matchItems) { - const fullname = m[1].trim(); - // - // 124.0.2478.97/edgedriver_arm64.zip - // https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/124.0.2478.97/edgedriver_arm64.zip - // - // Fri, 10 May 2024 18:35:44 GMT - // 0x8DC712000713C13 - // 9191362 - // application/octet-stream - // - // - // 1tjPTf5JU6KKB06Qf1JOGw== - // - // BlockBlob - // unlocked - // - // - // ignore size = 0 dir - const name = path.basename(fullname); - const url = m[2].trim(); - const date = m[3].trim(); - const size = Number.parseInt(m[4].trim()); + for (const entry of listing.items) { + if (entry.isDirectory) continue; + if (!entry.name.startsWith(subDir)) continue; + // Only direct children of `subDir`, not nested paths. + const rest = entry.name.slice(subDir.length); + if (!rest || rest.includes('/')) continue; items.push({ - name, + name: rest, isDir: false, - url, - size, - date, + url: `${EDGEDRIVER_DOWNLOAD_BASE}${entry.name}`, + size: entry.contentLength, + date: entry.lastModified, }); } - return items; + return { items, nextParams: null }; + } + + async #fetchListing(): Promise { + const { data, status, headers } = await this.httpclient.request(EDGEDRIVER_LISTING_URL, { + dataType: 'json', + timeout: 30_000, + followRedirect: true, + gzip: true, + }); + if (status !== 200) { + this.logger.warn( + '[EdgedriverBinary.fetchListing:non-200-status] url: %s, status: %s, headers: %j, data: %j', + EDGEDRIVER_LISTING_URL, + status, + headers, + data, + ); + return; + } + return data as EdgedriverListing; } } diff --git a/test/common/adapter/binary/EdgedriverBinary.test.ts b/test/common/adapter/binary/EdgedriverBinary.test.ts index 6c32ced36..1ded62bce 100644 --- a/test/common/adapter/binary/EdgedriverBinary.test.ts +++ b/test/common/adapter/binary/EdgedriverBinary.test.ts @@ -17,6 +17,10 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { data: await TestUtil.readFixturesFile('edgeupdates.json'), persist: false, }); + app.mockHttpclient('https://msedgedriver.microsoft.com/listing.json', 'GET', { + data: await TestUtil.readFixturesFile('msedgedriver-listing.json'), + persist: false, + }); let result = await binary.fetch('/'); assert.deepEqual(result, { items: [ @@ -63,13 +67,13 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { // { // name: 'edgedriver_win64.zip', // isDir: false, - // url: 'https://msedgewebdriverstorage.blob.core.windows.net/edgewebdriver/126.0.2578.0/edgedriver_win64.zip', + // url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_win64.zip', // size: 9564395, - // date: 'Fri, 10 May 2024 17:04:10 GMT' + // date: '2024-05-10T17:04:10+00:00' // } assert.equal(item.isDir, false); - assert.match(item.name, /^edgedriver_\w+.zip$/); - assert.match(item.url, /^https:\/\//); + assert.match(item.name, /^edgedriver_[\w]+\.zip$/); + assert.match(item.url, /^https:\/\/msedgedriver\.microsoft\.com\//); assert.ok(typeof item.size === 'number'); assert.ok(item.size > 0); assert.ok(item.date); diff --git a/test/fixtures/msedgedriver-listing.json b/test/fixtures/msedgedriver-listing.json new file mode 100644 index 000000000..09b128c33 --- /dev/null +++ b/test/fixtures/msedgedriver-listing.json @@ -0,0 +1,35 @@ +{ + "items": [ + { + "isDirectory": false, + "name": "126.0.2578.0/edgedriver_arm64.zip", + "contentLength": 9085682, + "lastModified": "2024-05-10T17:03:37+00:00" + }, + { + "isDirectory": false, + "name": "126.0.2578.0/edgedriver_mac64.zip", + "contentLength": 10907611, + "lastModified": "2024-05-10T16:37:33+00:00" + }, + { + "isDirectory": false, + "name": "126.0.2578.0/edgedriver_mac64_m1.zip", + "contentLength": 10090619, + "lastModified": "2024-05-10T16:37:33+00:00" + }, + { + "isDirectory": false, + "name": "126.0.2578.0/edgedriver_win32.zip", + "contentLength": 8759537, + "lastModified": "2024-05-10T17:04:29+00:00" + }, + { + "isDirectory": false, + "name": "126.0.2578.0/edgedriver_win64.zip", + "contentLength": 9564395, + "lastModified": "2024-05-10T17:04:10+00:00" + } + ], + "generatedAt": "2026-04-08T13:18:30.7241345+00:00" +} From 34a47e14ec8efca178a4863279ffa53f2d42df5b Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Wed, 8 Apr 2026 22:48:51 +0800 Subject: [PATCH 2/5] test(EdgedriverBinary): cover listing.json request failure path The previous patch added a defensive branch in `#fetchListing()` that logs a warning and returns `undefined` on non-200 responses, and a matching `if (!listing)` branch in `fetch()` that returns an empty `items` array. Neither was exercised by the existing happy-path test, dropping patch coverage to ~82%. Add a second test case that mocks `https://msedgedriver.microsoft.com/listing.json` with `status: 500` and asserts that `fetch('//')` returns an empty item list rather than throwing. This covers both uncovered branches in a single case. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../adapter/binary/EdgedriverBinary.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/common/adapter/binary/EdgedriverBinary.test.ts b/test/common/adapter/binary/EdgedriverBinary.test.ts index 1ded62bce..36e2d0383 100644 --- a/test/common/adapter/binary/EdgedriverBinary.test.ts +++ b/test/common/adapter/binary/EdgedriverBinary.test.ts @@ -79,5 +79,21 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { assert.ok(item.date); } }); + + it('should return empty items when listing.json request fails', async () => { + app.mockHttpclient('https://edgeupdates.microsoft.com/api/products', 'GET', { + data: await TestUtil.readFixturesFile('edgeupdates.json'), + persist: false, + }); + app.mockHttpclient('https://msedgedriver.microsoft.com/listing.json', 'GET', { + data: '', + status: 500, + persist: false, + }); + const result = await binary.fetch('/126.0.2578.0/'); + assert.ok(result); + assert.deepEqual(result.items, []); + assert.equal(result.nextParams, null); + }); }); }); From 4bc09a9d6790051343ff64eeddfac1ff266f4a89 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Wed, 8 Apr 2026 22:58:38 +0800 Subject: [PATCH 3/5] =?UTF-8?q?refactor(EdgedriverBinary):=20address=20rev?= =?UTF-8?q?iew=20feedback=20=E2=80=94=20cache=20listing,=20reuse=20request?= =?UTF-8?q?JSON,=20return=20undefined=20on=20failure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply three pieces of feedback from the PR review: 1. Cache the `listing.json` response per sync task. `listing.json` is a ~9000-entry dump (~500KB). A single sync task calls `fetch('//')` for many versions; previously each call would re-download the whole listing. Cache it in a promise-level field on the adapter and reset it in `initFetch` so each new sync task gets fresh data. 2. Return `undefined` on listing failure instead of `{ items: [] }`. The previous empty-items return value was ambiguous — callers couldn't tell "listing API is down" from "this version genuinely has no files". Returning `undefined` propagates the failure signal cleanly. Also guard against malformed responses by requiring `listing.items` to be an array before trusting it. 3. Reuse `AbstractBinary.requestJSON` instead of re-implementing `this.httpclient.request` with the same options. The helper already handles timeout / followRedirect / gzip / non-200 warn logging. Test updates: - The existing non-200 case now asserts `result === undefined`. - A new caching test mocks `listing.json` with a call counter and verifies two successive `fetch('/126.0.2578.0/')` calls produce identical items while only hitting the network once. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/common/adapter/binary/EdgedriverBinary.ts | 47 +++++++++++++------ .../adapter/binary/EdgedriverBinary.test.ts | 28 +++++++++-- 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/app/common/adapter/binary/EdgedriverBinary.ts b/app/common/adapter/binary/EdgedriverBinary.ts index eb2e72d1c..bf41372fe 100644 --- a/app/common/adapter/binary/EdgedriverBinary.ts +++ b/app/common/adapter/binary/EdgedriverBinary.ts @@ -29,9 +29,15 @@ export class EdgedriverBinary extends AbstractBinary { private dirItems?: { [key: string]: BinaryItem[]; }; + // Promise-level cache for the full `listing.json` dump (~9000 entries). + // A single sync task calls `fetch('//')` for many versions; + // without this cache each call would re-download the listing. + // Reset in `initFetch` so each sync task gets fresh data. + #listingPromise?: Promise; async initFetch() { this.dirItems = undefined; + this.#listingPromise = undefined; } async #syncDirItems() { @@ -190,8 +196,11 @@ export class EdgedriverBinary extends AbstractBinary { // /126.0.2578.0/ => 126.0.2578.0/ const subDir = dir.slice(1); const listing = await this.#fetchListing(); - if (!listing) { - return { items: [], nextParams: null }; + // Return undefined (not an empty-items FetchResult) on listing + // failure so the caller can distinguish "listing unavailable" from + // "this version exists but has no files". + if (!listing?.items) { + return; } const items: BinaryItem[] = []; for (const entry of listing.items) { @@ -212,22 +221,32 @@ export class EdgedriverBinary extends AbstractBinary { } async #fetchListing(): Promise { - const { data, status, headers } = await this.httpclient.request(EDGEDRIVER_LISTING_URL, { - dataType: 'json', - timeout: 30_000, - followRedirect: true, - gzip: true, - }); - if (status !== 200) { + if (!this.#listingPromise) { + this.#listingPromise = this.#loadListing(); + } + return this.#listingPromise; + } + + async #loadListing(): Promise { + try { + // `AbstractBinary.requestJSON` already handles timeout / follow + // redirect / gzip / non-200 warn logging. It returns whatever + // `data` the server sent even on non-200, so we validate the + // shape before trusting it. + const listing = await this.requestJSON(EDGEDRIVER_LISTING_URL); + if (!listing?.items || !Array.isArray(listing.items)) { + return; + } + return listing; + } catch (err) { this.logger.warn( - '[EdgedriverBinary.fetchListing:non-200-status] url: %s, status: %s, headers: %j, data: %j', + '[EdgedriverBinary.loadListing:request-failed] url: %s, error: %s', EDGEDRIVER_LISTING_URL, - status, - headers, - data, + (err as Error).message, ); + // Clear the cached promise so the next sync task retries cleanly. + this.#listingPromise = undefined; return; } - return data as EdgedriverListing; } } diff --git a/test/common/adapter/binary/EdgedriverBinary.test.ts b/test/common/adapter/binary/EdgedriverBinary.test.ts index 36e2d0383..51faf9fa8 100644 --- a/test/common/adapter/binary/EdgedriverBinary.test.ts +++ b/test/common/adapter/binary/EdgedriverBinary.test.ts @@ -80,7 +80,7 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { } }); - it('should return empty items when listing.json request fails', async () => { + it('should return undefined when listing.json request fails', async () => { app.mockHttpclient('https://edgeupdates.microsoft.com/api/products', 'GET', { data: await TestUtil.readFixturesFile('edgeupdates.json'), persist: false, @@ -91,9 +91,29 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { persist: false, }); const result = await binary.fetch('/126.0.2578.0/'); - assert.ok(result); - assert.deepEqual(result.items, []); - assert.equal(result.nextParams, null); + assert.equal(result, undefined); + }); + + it('should cache listing.json across multiple sub-dir fetches', async () => { + app.mockHttpclient('https://edgeupdates.microsoft.com/api/products', 'GET', { + data: await TestUtil.readFixturesFile('edgeupdates.json'), + persist: false, + }); + let listingCalls = 0; + const listingBuffer = await TestUtil.readFixturesFile('msedgedriver-listing.json'); + app.mockHttpclient('https://msedgedriver.microsoft.com/listing.json', 'GET', () => { + listingCalls++; + return { data: listingBuffer, status: 200 }; + }); + // First sub-dir fetch triggers the network request. + const r1 = await binary.fetch('/126.0.2578.0/'); + assert.ok(r1); + assert.ok(r1.items.length >= 3); + // Second sub-dir fetch should hit the cached listing, no extra request. + const r2 = await binary.fetch('/126.0.2578.0/'); + assert.ok(r2); + assert.deepEqual(r2.items, r1.items); + assert.equal(listingCalls, 1); }); }); }); From d5a948bd77cca2f069ee1ffb8ce1c1d7cc94959c Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Wed, 8 Apr 2026 23:12:01 +0800 Subject: [PATCH 4/5] refactor(EdgedriverBinary): generate platform URLs, drop listing.json entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous attempt fetched `https://msedgedriver.microsoft.com/listing.json` and filtered by version prefix. This had two problems: 1. `listing.json` is a ~1.2MB static dump containing every version since 112.0.1722.39 (~9000 entries). It does not support any query/filter parameter — all of `?top`, `?limit`, `?max`, `?pageSize`, `?prefix`, etc. return the same 1212112-byte payload. A per-sync cache helps but still downloads 1.2MB per sync. 2. The promise-level cache in the previous commit leaked across tests because `EdgedriverBinary` is a `@SingletonProto` and the test suite shares one instance. Instead, mirror the approach already used by `FirefoxBinary` and `ChromeForTestingBinary`: don't call any listing API for sub-dirs at all. Microsoft hosts per-version driver files at a stable URL pattern https://msedgedriver.microsoft.com//edgedriver_.zip where `` is one of the six known values observed in the current listing (arm64, linux64, mac64, mac64_m1, win32, win64). The adapter now generates one `BinaryItem` per platform with `ignoreDownloadStatuses: [404]`, so older versions that don't ship every platform (or versions that are gated by Microsoft) are skipped cleanly by cnpmcore's sync pipeline rather than failing the task. Side benefits: - 0 bytes downloaded per sub-dir fetch. - No per-sync cache needed. - No singleton state pollution between tests. - Smaller surface area (removes ~60 lines of listing parse / fallback code, plus the XML `requestXml` dependency in this adapter). Test changes: - The happy path test now asserts the exact 6 generated items with `ignoreDownloadStatuses: [404]` on each. - The "listing request fails" test is removed — there is no listing request anymore. - The `beforeEach` calls `initFetch()` to reset `dirItems` between tests, so the singleton-shared state doesn't leak. - The now-unused `test/fixtures/msedgedriver-listing.json` fixture is removed. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/common/adapter/binary/EdgedriverBinary.ts | 114 ++++++----------- .../adapter/binary/EdgedriverBinary.test.ts | 120 +++++++++--------- test/fixtures/msedgedriver-listing.json | 35 ----- 3 files changed, 99 insertions(+), 170 deletions(-) delete mode 100644 test/fixtures/msedgedriver-listing.json diff --git a/app/common/adapter/binary/EdgedriverBinary.ts b/app/common/adapter/binary/EdgedriverBinary.ts index bf41372fe..22c131c26 100644 --- a/app/common/adapter/binary/EdgedriverBinary.ts +++ b/app/common/adapter/binary/EdgedriverBinary.ts @@ -3,25 +3,32 @@ import { SingletonProto } from 'egg'; import { BinaryType } from '../../enum/Binary.ts'; import { AbstractBinary, BinaryAdapter, type BinaryItem, type FetchResult } from './AbstractBinary.ts'; -// Microsoft moved the Edge WebDriver download listing off the public -// Azure Blob container (the old XML listing API returns -// `PublicAccessNotPermitted` since ~2026-04-07). The new source of truth -// is a single JSON dump at https://msedgedriver.microsoft.com/listing.json -// which lists every version-prefixed driver file; individual files are -// downloadable from https://msedgedriver.microsoft.com/. -const EDGEDRIVER_LISTING_URL = 'https://msedgedriver.microsoft.com/listing.json'; +// Microsoft moved Edge WebDriver binaries to https://msedgedriver.microsoft.com/ +// in July 2025 after `msedgedriver.azureedge.net` was retired, and around +// 2026-04-07 also disabled public access on the legacy Azure Blob container +// that used to host the XML file listing. There is still no paginated/filtered +// listing API — the only "listing" endpoint on the new host is a ~1.2MB static +// JSON dump (`/listing.json`, ~9000 entries covering every version since +// 112.0.1722.39). +// +// To avoid hammering that 1.2MB dump for every version subdirectory during a +// sync, we mirror the approach used by `FirefoxBinary` / `ChromeForTestingBinary` +// and generate the per-version download URLs from a static list of known +// platform filenames. cnpmcore's sync pipeline honors the per-item +// `ignoreDownloadStatuses` field, so any version that doesn't ship a given +// platform (e.g. older builds without `edgedriver_mac64_m1.zip`) gets a clean +// 404 and is skipped rather than failing the sync. const EDGEDRIVER_DOWNLOAD_BASE = 'https://msedgedriver.microsoft.com/'; - -interface EdgedriverListingEntry { - isDirectory: boolean; - name: string; - contentLength: number; - lastModified: string; -} -interface EdgedriverListing { - items: EdgedriverListingEntry[]; - generatedAt: string; -} +// Platform filenames observed in Microsoft's current `listing.json` dump. +// Every version since 112.0.1722.39 ships some subset of these six files. +const EDGEDRIVER_PLATFORM_FILES = [ + 'edgedriver_arm64.zip', + 'edgedriver_linux64.zip', + 'edgedriver_mac64.zip', + 'edgedriver_mac64_m1.zip', + 'edgedriver_win32.zip', + 'edgedriver_win64.zip', +] as const; @SingletonProto() @BinaryAdapter(BinaryType.Edgedriver) @@ -29,15 +36,9 @@ export class EdgedriverBinary extends AbstractBinary { private dirItems?: { [key: string]: BinaryItem[]; }; - // Promise-level cache for the full `listing.json` dump (~9000 entries). - // A single sync task calls `fetch('//')` for many versions; - // without this cache each call would re-download the listing. - // Reset in `initFetch` so each sync task gets fresh data. - #listingPromise?: Promise; async initFetch() { this.dirItems = undefined; - this.#listingPromise = undefined; } async #syncDirItems() { @@ -192,61 +193,20 @@ export class EdgedriverBinary extends AbstractBinary { return { items: this.dirItems?.[dir] ?? [], nextParams: null }; } - // fetch sub dir + // fetch sub dir: generate the known platform filenames for this version. + // We intentionally don't call any listing API — see the file-level + // comment for the rationale. Any platform that doesn't exist for a + // specific version is skipped cleanly via `ignoreDownloadStatuses`. // /126.0.2578.0/ => 126.0.2578.0/ const subDir = dir.slice(1); - const listing = await this.#fetchListing(); - // Return undefined (not an empty-items FetchResult) on listing - // failure so the caller can distinguish "listing unavailable" from - // "this version exists but has no files". - if (!listing?.items) { - return; - } - const items: BinaryItem[] = []; - for (const entry of listing.items) { - if (entry.isDirectory) continue; - if (!entry.name.startsWith(subDir)) continue; - // Only direct children of `subDir`, not nested paths. - const rest = entry.name.slice(subDir.length); - if (!rest || rest.includes('/')) continue; - items.push({ - name: rest, - isDir: false, - url: `${EDGEDRIVER_DOWNLOAD_BASE}${entry.name}`, - size: entry.contentLength, - date: entry.lastModified, - }); - } + const items: BinaryItem[] = EDGEDRIVER_PLATFORM_FILES.map(name => ({ + name, + isDir: false, + url: `${EDGEDRIVER_DOWNLOAD_BASE}${subDir}${name}`, + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + })); return { items, nextParams: null }; } - - async #fetchListing(): Promise { - if (!this.#listingPromise) { - this.#listingPromise = this.#loadListing(); - } - return this.#listingPromise; - } - - async #loadListing(): Promise { - try { - // `AbstractBinary.requestJSON` already handles timeout / follow - // redirect / gzip / non-200 warn logging. It returns whatever - // `data` the server sent even on non-200, so we validate the - // shape before trusting it. - const listing = await this.requestJSON(EDGEDRIVER_LISTING_URL); - if (!listing?.items || !Array.isArray(listing.items)) { - return; - } - return listing; - } catch (err) { - this.logger.warn( - '[EdgedriverBinary.loadListing:request-failed] url: %s, error: %s', - EDGEDRIVER_LISTING_URL, - (err as Error).message, - ); - // Clear the cached promise so the next sync task retries cleanly. - this.#listingPromise = undefined; - return; - } - } } diff --git a/test/common/adapter/binary/EdgedriverBinary.test.ts b/test/common/adapter/binary/EdgedriverBinary.test.ts index 51faf9fa8..4d9371187 100644 --- a/test/common/adapter/binary/EdgedriverBinary.test.ts +++ b/test/common/adapter/binary/EdgedriverBinary.test.ts @@ -9,19 +9,19 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { let binary: EdgedriverBinary; beforeEach(async () => { binary = await app.getEggObject(EdgedriverBinary); + // EdgedriverBinary is a @SingletonProto — reset its per-sync cache so + // each test sees a fresh state (the first `fetch('/')` call populates + // `dirItems` which would otherwise persist across tests). + await binary.initFetch(); }); describe('fetch()', () => { - it('should work', async () => { + it('should list recent stable versions from edgeupdates.microsoft.com', async () => { app.mockHttpclient('https://edgeupdates.microsoft.com/api/products', 'GET', { data: await TestUtil.readFixturesFile('edgeupdates.json'), persist: false, }); - app.mockHttpclient('https://msedgedriver.microsoft.com/listing.json', 'GET', { - data: await TestUtil.readFixturesFile('msedgedriver-listing.json'), - persist: false, - }); - let result = await binary.fetch('/'); + const result = await binary.fetch('/'); assert.deepEqual(result, { items: [ { @@ -55,65 +55,69 @@ describe('test/common/adapter/binary/EdgedriverBinary.test.ts', () => { ], nextParams: null, }); - - const latestVersion = result.items[result.items.length - 1].name; - assert.ok(latestVersion); - assert.equal(latestVersion, '126.0.2578.0/'); - result = await binary.fetch(`/${latestVersion}`); - assert.ok(result); - const items = result.items; - assert.ok(items.length >= 3); - for (const item of items) { - // { - // name: 'edgedriver_win64.zip', - // isDir: false, - // url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_win64.zip', - // size: 9564395, - // date: '2024-05-10T17:04:10+00:00' - // } - assert.equal(item.isDir, false); - assert.match(item.name, /^edgedriver_[\w]+\.zip$/); - assert.match(item.url, /^https:\/\/msedgedriver\.microsoft\.com\//); - assert.ok(typeof item.size === 'number'); - assert.ok(item.size > 0); - assert.ok(item.date); - } }); - it('should return undefined when listing.json request fails', async () => { + it('should generate all known platform driver URLs for a version', async () => { app.mockHttpclient('https://edgeupdates.microsoft.com/api/products', 'GET', { data: await TestUtil.readFixturesFile('edgeupdates.json'), persist: false, }); - app.mockHttpclient('https://msedgedriver.microsoft.com/listing.json', 'GET', { - data: '', - status: 500, - persist: false, - }); const result = await binary.fetch('/126.0.2578.0/'); - assert.equal(result, undefined); - }); - - it('should cache listing.json across multiple sub-dir fetches', async () => { - app.mockHttpclient('https://edgeupdates.microsoft.com/api/products', 'GET', { - data: await TestUtil.readFixturesFile('edgeupdates.json'), - persist: false, - }); - let listingCalls = 0; - const listingBuffer = await TestUtil.readFixturesFile('msedgedriver-listing.json'); - app.mockHttpclient('https://msedgedriver.microsoft.com/listing.json', 'GET', () => { - listingCalls++; - return { data: listingBuffer, status: 200 }; - }); - // First sub-dir fetch triggers the network request. - const r1 = await binary.fetch('/126.0.2578.0/'); - assert.ok(r1); - assert.ok(r1.items.length >= 3); - // Second sub-dir fetch should hit the cached listing, no extra request. - const r2 = await binary.fetch('/126.0.2578.0/'); - assert.ok(r2); - assert.deepEqual(r2.items, r1.items); - assert.equal(listingCalls, 1); + assert.ok(result); + assert.equal(result.nextParams, null); + // Expect all six known platform filenames, pointing at the new CDN, + // with `ignoreDownloadStatuses: [404]` so older versions that don't + // ship every platform get skipped cleanly instead of failing the sync. + assert.deepEqual(result.items, [ + { + name: 'edgedriver_arm64.zip', + isDir: false, + url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_arm64.zip', + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + }, + { + name: 'edgedriver_linux64.zip', + isDir: false, + url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_linux64.zip', + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + }, + { + name: 'edgedriver_mac64.zip', + isDir: false, + url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_mac64.zip', + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + }, + { + name: 'edgedriver_mac64_m1.zip', + isDir: false, + url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_mac64_m1.zip', + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + }, + { + name: 'edgedriver_win32.zip', + isDir: false, + url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_win32.zip', + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + }, + { + name: 'edgedriver_win64.zip', + isDir: false, + url: 'https://msedgedriver.microsoft.com/126.0.2578.0/edgedriver_win64.zip', + size: '-', + date: '-', + ignoreDownloadStatuses: [404], + }, + ]); }); }); }); diff --git a/test/fixtures/msedgedriver-listing.json b/test/fixtures/msedgedriver-listing.json deleted file mode 100644 index 09b128c33..000000000 --- a/test/fixtures/msedgedriver-listing.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "items": [ - { - "isDirectory": false, - "name": "126.0.2578.0/edgedriver_arm64.zip", - "contentLength": 9085682, - "lastModified": "2024-05-10T17:03:37+00:00" - }, - { - "isDirectory": false, - "name": "126.0.2578.0/edgedriver_mac64.zip", - "contentLength": 10907611, - "lastModified": "2024-05-10T16:37:33+00:00" - }, - { - "isDirectory": false, - "name": "126.0.2578.0/edgedriver_mac64_m1.zip", - "contentLength": 10090619, - "lastModified": "2024-05-10T16:37:33+00:00" - }, - { - "isDirectory": false, - "name": "126.0.2578.0/edgedriver_win32.zip", - "contentLength": 8759537, - "lastModified": "2024-05-10T17:04:29+00:00" - }, - { - "isDirectory": false, - "name": "126.0.2578.0/edgedriver_win64.zip", - "contentLength": 9564395, - "lastModified": "2024-05-10T17:04:10+00:00" - } - ], - "generatedAt": "2026-04-08T13:18:30.7241345+00:00" -} From 90ef54bd444939724bfd8cbcabd8844ff79d9481 Mon Sep 17 00:00:00 2001 From: elrrrrrrr Date: Wed, 8 Apr 2026 23:19:41 +0800 Subject: [PATCH 5/5] style(EdgedriverBinary): apply oxfmt formatting oxfmt prefers `(name) => ({...})` over `name => ({...})` for arrow function arguments. Brought up by cnpmcore CI's `fmtcheck` step. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/common/adapter/binary/EdgedriverBinary.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/common/adapter/binary/EdgedriverBinary.ts b/app/common/adapter/binary/EdgedriverBinary.ts index 22c131c26..d0bc928fd 100644 --- a/app/common/adapter/binary/EdgedriverBinary.ts +++ b/app/common/adapter/binary/EdgedriverBinary.ts @@ -199,7 +199,7 @@ export class EdgedriverBinary extends AbstractBinary { // specific version is skipped cleanly via `ignoreDownloadStatuses`. // /126.0.2578.0/ => 126.0.2578.0/ const subDir = dir.slice(1); - const items: BinaryItem[] = EDGEDRIVER_PLATFORM_FILES.map(name => ({ + const items: BinaryItem[] = EDGEDRIVER_PLATFORM_FILES.map((name) => ({ name, isDir: false, url: `${EDGEDRIVER_DOWNLOAD_BASE}${subDir}${name}`,