Browse Source

fix(smartfetch): harden jsdom loading smoke

Balogun Feranmi 6 days ago
parent
commit
eafafaa273
3 changed files with 84 additions and 19 deletions
  1. 38 13
      scripts/verify-release-artifact.ts
  2. 28 2
      src/utils/jsdom.test.ts
  3. 18 4
      src/utils/jsdom.ts

+ 38 - 13
scripts/verify-release-artifact.ts

@@ -200,7 +200,45 @@ function verifyFreshInstall(tarballPath: string) {
       "if (pkg?.id !== 'oh-my-opencode-slim') throw new Error('default export has an unexpected plugin id');",
       "if (typeof pkg.server !== 'function') throw new Error('default export is missing a server plugin factory');",
       "if (typeof pkg.setup !== 'function') throw new Error('default export is missing a v2 setup factory');",
+      'const asyncNoop = async () => ({});',
+      'const client = new Proxy({}, {',
+      '  get(_target, property) {',
+      "    if (property === 'app') return { log: asyncNoop };",
+      "    if (property === 'session') return { abort: asyncNoop };",
+      '    return new Proxy({}, { get: () => asyncNoop });',
+      '  },',
+      '});',
+      'globalThis.fetch = async () => new Response(',
+      "  '<!doctype html><html><head><title>Release smoke</title></head><body><main><h1>Release smoke</h1><p>packaged jsdom extraction works</p></main></body></html>',",
+      "  { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } },",
+      ');',
+      'const plugin = await pkg.server({',
+      '  client,',
+      '  directory: process.cwd(),',
+      '  worktree: process.cwd(),',
+      "  serverUrl: new URL('http://127.0.0.1:4096'),",
+      '});',
+      'const webfetch = plugin?.tool?.webfetch;',
+      "if (typeof webfetch?.execute !== 'function') throw new Error('server plugin did not register webfetch');",
+      'const result = await webfetch.execute({',
+      "  url: 'https://example.com/release-smoke',",
+      "  format: 'markdown',",
+      '  timeout: 10,',
+      '  extract_main: true,',
+      "  prefer_llms_txt: 'never',",
+      '  include_metadata: false,',
+      '  save_binary: false,',
+      '}, {',
+      '  ask: async () => undefined,',
+      '  metadata: () => undefined,',
+      '  abort: new AbortController().signal,',
+      '  directory: process.cwd(),',
+      "  sessionID: 'release-smoke',",
+      '});',
+      "if (!String(result).includes('packaged jsdom extraction works')) throw new Error('packaged webfetch did not extract the expected document');",
+      'await plugin.dispose?.();',
       "console.log('package loads');",
+      "console.log('packaged webfetch constructs and extracts a document');",
       'process.exit(0);',
     ].join('\n');
     console.log('Importing installed package entrypoint...');
@@ -208,19 +246,6 @@ function verifyFreshInstall(tarballPath: string) {
       cwd: installDir,
     });
 
-    const jsdomSmokeScript = [
-      "import { JSDOM } from 'jsdom';",
-      "const dom = new JSDOM('<p>hello</p>');",
-      "if (dom.window.document.querySelector('p')?.textContent !== 'hello') throw new Error('JSDOM did not construct the expected document');",
-      'dom.window.close();',
-      "console.log('jsdom constructs a document');",
-      'process.exit(0);',
-    ].join('\n');
-    console.log('Importing installed jsdom and constructing a document...');
-    run('node', ['--input-type=module', '--eval', jsdomSmokeScript], {
-      cwd: installDir,
-    });
-
     const tuiSmokeScript = [
       "import pkg from 'oh-my-opencode-slim/tui';",
       "if (pkg?.id !== 'oh-my-opencode-slim:tui') throw new Error('TUI export has an unexpected plugin id');",

+ 28 - 2
src/utils/jsdom.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, test } from 'bun:test';
-import { loadJSDOM, probeJSDOM } from './jsdom';
+import { createJSDOMLoader, loadJSDOM, probeJSDOM } from './jsdom';
 
 describe('jsdom loader', () => {
   test('loads jsdom and constructs a basic document', async () => {
@@ -13,12 +13,38 @@ describe('jsdom loader', () => {
   }, 15_000);
 
   test('shares the in-flight module load', async () => {
-    const [first, second] = await Promise.all([loadJSDOM(), loadJSDOM()]);
+    const jsdom = await loadJSDOM();
+    let loadCount = 0;
+    const load = createJSDOMLoader(async () => {
+      loadCount += 1;
+      await Promise.resolve();
+      return jsdom;
+    });
+
+    const firstPromise = load();
+    const secondPromise = load();
+    const [first, second] = await Promise.all([firstPromise, secondPromise]);
 
+    expect(loadCount).toBe(1);
+    expect(firstPromise).toBe(secondPromise);
     expect(first.JSDOM).toBe(second.JSDOM);
     expect(first.VirtualConsole).toBe(second.VirtualConsole);
   });
 
+  test('does not cache a failed load', async () => {
+    const jsdom = await loadJSDOM();
+    let loadCount = 0;
+    const load = createJSDOMLoader(async () => {
+      loadCount += 1;
+      if (loadCount === 1) throw new Error('transient jsdom failure');
+      return jsdom;
+    });
+
+    await expect(load()).rejects.toThrow('transient jsdom failure');
+    await expect(load()).resolves.toBe(jsdom);
+    expect(loadCount).toBe(2);
+  });
+
   test('reports loader failures without throwing from the probe', async () => {
     const result = await probeJSDOM(async () => {
       throw new Error('jsdom unavailable');

+ 18 - 4
src/utils/jsdom.ts

@@ -4,22 +4,36 @@ import { pathToFileURL } from 'node:url';
 export type JsdomModule = typeof import('jsdom');
 type JsdomLoader = () => Promise<JsdomModule>;
 
-let jsdomPromise: Promise<JsdomModule> | undefined;
-
 async function importJSDOM(): Promise<JsdomModule> {
   const require = createRequire(import.meta.url);
   const entrypoint = require.resolve('jsdom');
   return (await import(pathToFileURL(entrypoint).href)) as JsdomModule;
 }
 
+export function createJSDOMLoader(loader: JsdomLoader): JsdomLoader {
+  let promise: Promise<JsdomModule> | undefined;
+
+  return () => {
+    if (promise) return promise;
+
+    const pending = loader().catch((error) => {
+      if (promise === pending) promise = undefined;
+      throw error;
+    });
+    promise = pending;
+    return pending;
+  };
+}
+
+const loadJSDOMFromFile = createJSDOMLoader(importJSDOM);
+
 /**
  * Resolve jsdom to its installed file before importing it. This keeps
  * transitive relative asset loads anchored to a real filesystem URL instead
  * of the host's bundled module URL.
  */
 export function loadJSDOM(): Promise<JsdomModule> {
-  jsdomPromise ??= importJSDOM();
-  return jsdomPromise;
+  return loadJSDOMFromFile();
 }
 
 /**