From d68da53781bb8d1ec46494fdfdb15f82296ddf12 Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Mon, 14 Sep 2026 07:07:31 +0100 Subject: [PATCH 1/2] Resolve the GPT bootstrap test fixture without assuming a platform Two tests in ad_init.test.ts fail on any Windows checkout, because the helper takes the pathname of a file: URL and reads it as a filesystem path. A URL pathname is not a filesystem path: on Windows it carries a leading slash before the drive letter, so Node resolves it against the current drive and the drive segment doubles. The read then fails with ENOENT ... open 'D:\D:\...\gpt_bootstrap.js'. The fixture is now resolved from the vitest root, which is the lib directory per vitest.config.ts, the same way gpt_bootstrap.test.ts already resolves the same file. That removes the URL-to-path conversion and the Vite /@fs special case along with it, so no branch of the helper depends on the platform. CI runs this suite on ubuntu-latest, where a pathname carries no drive letter, so the fault has always been invisible there and appears only to someone running the suite locally on Windows. --- .../lib/test/integrations/gpt/ad_init.test.ts | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts index b7186518b..efe942590 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts @@ -120,19 +120,16 @@ type TestWindow = Omit & { }; async function runGptBootstrapWithGoogleTag(googletag: object): Promise { - const bootstrapUrl = new URL( - '../../../../../trusted-server-core/src/integrations/gpt_bootstrap.js', - import.meta.url + // Resolved from the vitest root (the lib directory, per vitest.config.ts) + // rather than from `import.meta.url`, which the jsdom environment rewrites + // to a non-file scheme. This is the same approach gpt_bootstrap.test.ts + // takes for the same file, and it makes no assumption about the platform: + // a URL pathname is not a filesystem path, and on Windows it carries a + // leading slash before the drive letter. + const bootstrapPath = path.resolve( + process.cwd(), + '../../trusted-server-core/src/integrations/gpt_bootstrap.js' ); - const urlPath = decodeURIComponent(bootstrapUrl.pathname); - let bootstrapPath: string; - if (urlPath.startsWith('/@fs/')) { - bootstrapPath = urlPath.slice('/@fs'.length); - } else if (bootstrapUrl.protocol === 'file:') { - bootstrapPath = urlPath; - } else { - bootstrapPath = path.resolve(process.cwd(), `.${urlPath}`); - } const bootstrap = await readFile(bootstrapPath, 'utf8'); const runBootstrap = new Function('window', 'googletag', bootstrap) as ( window: Window, From e43ae458bf74cd5d33feb95e0372eb50c3d0570d Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Tue, 15 Sep 2026 06:44:55 +0100 Subject: [PATCH 2/2] Resolve the GPT bootstrap fixture from one file-relative path Every suite that evaluates gpt_bootstrap.js now reads it through GPT_BOOTSTRAP_PATH in test/fixtures/paths.ts, which resolves from import.meta.dirname, as the review on #1162 asked. Node defines that as a platform-native directory path, so no URL is turned into a path, and the result does not depend on the directory the suite is launched from. The seven reads that resolved from process.cwd() use the constant, being the four in ad_init.test.ts and one each in gpt_bootstrap.test.ts, schedule_initial_ad_init.test.ts and spa_hook.test.ts. Their node:path imports go, and so do the two comments explaining why the fixture was not resolved from import.meta.url, which the shared path makes unnecessary. --- .../lib/test/fixtures/paths.ts | 17 +++++++++++ .../lib/test/integrations/gpt/ad_init.test.ts | 30 ++++--------------- .../integrations/gpt/gpt_bootstrap.test.ts | 11 ++----- .../gpt/schedule_initial_ad_init.test.ts | 7 ++--- .../test/integrations/gpt/spa_hook.test.ts | 7 ++--- 5 files changed, 28 insertions(+), 44 deletions(-) create mode 100644 crates/trusted-server-js/lib/test/fixtures/paths.ts diff --git a/crates/trusted-server-js/lib/test/fixtures/paths.ts b/crates/trusted-server-js/lib/test/fixtures/paths.ts new file mode 100644 index 000000000..199b105c1 --- /dev/null +++ b/crates/trusted-server-js/lib/test/fixtures/paths.ts @@ -0,0 +1,17 @@ +import { resolve } from 'node:path'; + +/** + * Absolute path to the edge-injected `gpt_bootstrap.js` that several suites + * evaluate verbatim. + * + * Resolved from `import.meta.dirname`, which Node defines as a platform-native + * directory path. A `file:` URL pathname is not a filesystem path, because on + * Windows it carries a leading slash before the drive letter, so reading it + * directly makes Node resolve it against the current drive and the drive + * segment doubles. Resolving from this module's own directory also keeps the + * path independent of the working directory the suite is launched from. + */ +export const GPT_BOOTSTRAP_PATH = resolve( + import.meta.dirname, + '../../../../trusted-server-core/src/integrations/gpt_bootstrap.js' +); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts index efe942590..51d8cabbf 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/ad_init.test.ts @@ -1,11 +1,10 @@ import { readFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; -import path from 'node:path'; -import { resolve } from 'node:path'; import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest'; import envelope from '../../fixtures/aps-renderer-v1.json'; +import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; import type { AuctionBidData, TsjsApi } from '../../../src/core/types'; import { APS_PREBID_CREATIVE_RUNNER_URL, @@ -120,17 +119,7 @@ type TestWindow = Omit & { }; async function runGptBootstrapWithGoogleTag(googletag: object): Promise { - // Resolved from the vitest root (the lib directory, per vitest.config.ts) - // rather than from `import.meta.url`, which the jsdom environment rewrites - // to a non-file scheme. This is the same approach gpt_bootstrap.test.ts - // takes for the same file, and it makes no assumption about the platform: - // a URL pathname is not a filesystem path, and on Windows it carries a - // leading slash before the drive letter. - const bootstrapPath = path.resolve( - process.cwd(), - '../../trusted-server-core/src/integrations/gpt_bootstrap.js' - ); - const bootstrap = await readFile(bootstrapPath, 'utf8'); + const bootstrap = await readFile(GPT_BOOTSTRAP_PATH, 'utf8'); const runBootstrap = new Function('window', 'googletag', bootstrap) as ( window: Window, googletag: object @@ -142,10 +131,7 @@ type HandoffImplementation = 'bootstrap' | 'bundle'; async function installHandoff(implementation: HandoffImplementation): Promise { if (implementation === 'bootstrap') { - const bootstrap = readFileSync( - resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), - 'utf8' - ); + const bootstrap = readFileSync(GPT_BOOTSTRAP_PATH, 'utf8'); window.eval(bootstrap); return; } @@ -198,10 +184,7 @@ function appendResponsiveSlotElement( } function runGptBootstrap(): void { - const bootstrap = readFileSync( - resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), - 'utf8' - ); + const bootstrap = readFileSync(GPT_BOOTSTRAP_PATH, 'utf8'); window.eval(bootstrap); } @@ -1147,10 +1130,7 @@ describe('installTsAdInit', () => { gptSlotHandoffs: { 'div-ts-fallback': handoff }, }; - const bootstrap = readFileSync( - resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), - 'utf8' - ); + const bootstrap = readFileSync(GPT_BOOTSTRAP_PATH, 'utf8'); window.eval(bootstrap); expect(() => diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts index ab6d646f2..e9ed4a169 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/gpt_bootstrap.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from 'node:fs'; -import path from 'node:path'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { TsjsApi } from '../../../src/core/types'; +import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; /** * Executable coverage for the edge-injected `gpt_bootstrap.js` — the @@ -12,15 +12,8 @@ import type { TsjsApi } from '../../../src/core/types'; * `crates/trusted-server-core/src/integrations/gpt_bootstrap.js` and is * evaluated here verbatim, so the degradation path (fallback `adInit` and * fallback `scheduleInitialAdInit`) is executed, not string-matched. - * - * Vitest runs with the lib directory as cwd (the vitest.config.ts root), so - * the bootstrap is resolved relative to it rather than via import.meta.url, - * which the jsdom environment rewrites to a non-file scheme. */ -const BOOTSTRAP_SOURCE = readFileSync( - path.resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), - 'utf8' -); +const BOOTSTRAP_SOURCE = readFileSync(GPT_BOOTSTRAP_PATH, 'utf8'); // The command queue the bootstrap pushes into: a real array once GPT has // loaded, or the bare `push`-only stub GPT installs before then. diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts index 727300aa1..6ca610547 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/schedule_initial_ad_init.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from 'node:fs'; -import path from 'node:path'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { TsjsApi } from '../../../src/core/types'; +import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; type TestWindow = Window & { googletag?: unknown; @@ -12,10 +12,7 @@ type TestWindow = Window & { const originalPushState = history.pushState.bind(history); const originalReplaceState = history.replaceState.bind(history); -const BOOTSTRAP_SOURCE = readFileSync( - path.resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), - 'utf8' -); +const BOOTSTRAP_SOURCE = readFileSync(GPT_BOOTSTRAP_PATH, 'utf8'); function runBootstrap(): void { new Function(BOOTSTRAP_SOURCE)(); diff --git a/crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts b/crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts index 314348fa8..24ed04431 100644 --- a/crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts +++ b/crates/trusted-server-js/lib/test/integrations/gpt/spa_hook.test.ts @@ -1,9 +1,9 @@ import { readFileSync } from 'node:fs'; -import { resolve } from 'node:path'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import type { TsjsApi } from '../../../src/core/types'; +import { GPT_BOOTSTRAP_PATH } from '../../fixtures/paths'; type TestWindow = Window & { googletag?: unknown; @@ -148,10 +148,7 @@ describe('installSpaAuctionHook', () => { }; if (implementation === 'bootstrap') { - const bootstrap = readFileSync( - resolve(process.cwd(), '../../trusted-server-core/src/integrations/gpt_bootstrap.js'), - 'utf8' - ); + const bootstrap = readFileSync(GPT_BOOTSTRAP_PATH, 'utf8'); window.eval(bootstrap); } await importGptModule();