From 26e7b058b087bba9f5381dc2eba8485ccb45ded9 Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Tue, 14 Jul 2026 14:06:41 +0200 Subject: [PATCH 1/3] test(e2e): capture notebook cell errors in run_dbconnect diagnostics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* When a run_dbconnect notebook/magic test times out waiting for an output file, `dumpNotebookDiagnostics` is supposed to reveal why the cell didn't emit. But its only error source is an Output-channel scrape via wdio-vscode-service, whose locator is `select[title="Tasks"]` — stale on the pinned VS Code (^1.86.0). On the Jul-14 CI run it threw "Can't call $$ on element with selector select[title=\"Tasks\"]" and returned zero channels, so the kernel's actual cell error/traceback was never captured. The only ground truth left was the .webm video, which showed the kernel now starts (the ipykernel fix from #2006 works) but a later cell (`%run`, or the `%sql`->`_sqldf` cell) either never runs or emits no output — undiagnosable from CI logs alone. *What* - Add `dumpActiveNotebookCells()`: reads the active notebook's cells, execution summaries (executionOrder/success), and output items via the VS Code API through `browser.executeWorkbench` — no DOM selector, so a cell error or a never-run cell is captured directly. - Add `dumpVscodeLogFiles()`: dumps VS Code's on-disk logs (extension host + Output-channel logs) resolved from `vscode.env.logUri`, another selector-independent source for the kernel/DBConnect error. - Wire both into `dumpNotebookDiagnostics` ahead of the existing Output-channel scrape, which is kept as best-effort (its failure is already caught and now clearly documented as a stale locator). - Diagnostics-only: runs solely on the failure path, changes no assertions or product code. *Verification* - `tsc --noEmit -p src/test/e2e/tsconfig.json`: 0 errors. - eslint + prettier on the file: clean. - (e2e itself runs only in the databricks-eng CI; this commit's purpose is to make the next CI run capture the cell error so the underlying cell-execution failure can be root-caused.) Co-authored-by: Isaac --- .../src/test/e2e/run_dbconnect.ucws.e2e.ts | 111 +++++++++++++++++- 1 file changed, 105 insertions(+), 6 deletions(-) diff --git a/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts b/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts index 7e077b851..afd305d20 100644 --- a/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts +++ b/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts @@ -97,6 +97,95 @@ async function listFilesRecursive(dir: string, base = dir): Promise { return out; } +// Dumps the active notebook's cell outputs straight from the VS Code API. This +// is the ground truth for "did the cell error out?": a cell that raised shows +// up here as an `error` output item with the name/message/stack, and a cell +// that never ran has an empty `executionOrder`. We read it via +// `executeWorkbench` (runs inside the extension host with the full `vscode` +// API) so it does not depend on any DOM selector — unlike the Output-channel +// scrape below, whose `select[title="Tasks"]` locator is stale on current +// VS Code and silently returns nothing. +async function dumpActiveNotebookCells() { + try { + const cells = await browser.executeWorkbench((vscode) => { + const editor = (vscode.window as any).activeNotebookEditor; + const doc = editor?.notebook; + if (!doc) { + return ""; + } + return doc + .getCells() + .map((cell: any) => { + const outputs = (cell.outputs ?? []).flatMap((o: any) => + (o.items ?? []).map((item: any) => { + const text = Buffer.from(item.data).toString( + "utf8" + ); + return ` [${item.mime}] ${text}`; + }) + ); + return [ + `cell #${cell.index} (${ + cell.kind === 2 ? "code" : "markup" + }), ` + + `executionOrder=${ + cell.executionSummary?.executionOrder ?? + "" + }, ` + + `success=${ + cell.executionSummary?.success ?? "" + }`, + ...outputs, + ].join("\n"); + }) + .join("\n"); + }); + console.log("=== active notebook cells (via VS Code API) ===\n", cells); + } catch (e) { + console.log("could not read active notebook cells:", e); + } +} + +// Dumps VS Code's on-disk logs (extension host + every Output-channel log the +// window has written). The Jupyter/DBConnect kernel error lands in one of these +// files even when the in-UI Output panel can't be scraped, so this is a +// selector-independent way to capture it. +async function dumpVscodeLogFiles() { + let logsRoot: string | undefined; + try { + logsRoot = await browser.executeWorkbench( + (vscode) => vscode.env.logUri?.fsPath + ); + } catch (e) { + console.log("could not resolve VS Code logs directory:", e); + return; + } + if (!logsRoot) { + console.log("VS Code logs directory unavailable"); + return; + } + let logFiles: string[]; + try { + logFiles = (await listFilesRecursive(logsRoot)).filter((f) => + f.endsWith(".log") + ); + } catch (e) { + console.log(`could not list logs under "${logsRoot}":`, e); + return; + } + console.log(`=== VS Code log files under "${logsRoot}" ===`, logFiles); + for (const rel of logFiles) { + try { + const text = await fs.readFile(path.join(logsRoot, rel), "utf-8"); + if (text.trim().length > 0) { + console.log(`=== log "${rel}" ===\n${text}`); + } + } catch (e) { + console.log(`could not read log "${rel}":`, e); + } + } +} + // Dumps whatever we can observe about the notebook run when an output file // never shows up (or never gets the expected content). Runs only on failure, // so the extra work never slows down the happy path. @@ -104,9 +193,10 @@ async function listFilesRecursive(dir: string, base = dir): Promise { // It answers the two questions a missing output file raises: (1) did the cell // write the file somewhere else? — we list the whole workspace tree, since a // cwd mismatch would drop it in the project root rather than `nested/`; and -// (2) did the cell error out? — the notebook kernel logs to its own VS Code -// Output channel (not the default one), so we enumerate every channel and dump -// each, which surfaces a NameError/kernel failure wherever it landed. +// (2) did the cell error out (or never run)? — we read the notebook's cell +// outputs and execution summaries via the VS Code API, and dump the on-disk +// log files. Both are selector-independent. The Output-channel scrape is kept +// last as best-effort, since its locator is stale on current VS Code. async function dumpNotebookDiagnostics( filePath: string, expectedContent: string, @@ -136,9 +226,18 @@ async function dumpNotebookDiagnostics( } } - // Every Output channel, so the notebook kernel's cell error (which does not - // go to the default channel) is captured rather than whatever channel - // happens to be selected. + // Cell outputs + execution summaries via the VS Code API (the reliable + // source for a cell error/traceback or a never-run cell). + await dumpActiveNotebookCells(); + + // On-disk VS Code logs (extension host + Output-channel logs), which + // capture the kernel/DBConnect error even when the panel can't be scraped. + await dumpVscodeLogFiles(); + + // Best-effort Output-channel scrape. Kept for completeness, but its + // `select[title="Tasks"]` locator is stale on current VS Code and often + // returns nothing — so a failure here is logged and ignored, never fatal to + // the rest of the dump. try { const workbench = await browser.getWorkbench(); const view = await workbench.getBottomBar().openOutputView(); From 5b80ab31cafba4becba53910eeab646342c2bc27 Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Tue, 14 Jul 2026 16:50:51 +0200 Subject: [PATCH 2/3] test(e2e): dump all open notebook documents, not just the active editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* The first diagnostics pass (dumpActiveNotebookCells) read `window.activeNotebookEditor`, which for the `%sql` magic test printed "". That `.py` "Databricks notebook" (`# Databricks notebook source` + `# MAGIC %sql`) runs in an Interactive Window whose document is NOT the active notebook editor — the focused editor is the plain `.py` text editor — so the active-editor-only view missed exactly the failing cell we need to see. As a result the run captured the notebook.ipynb failure (cell #1 `%run` never ran) but still could not show why the `%sql`->`_sqldf` cell produced no output. *What* - Rename `dumpActiveNotebookCells` -> `dumpOpenNotebookCells` and iterate `vscode.workspace.notebookDocuments` (every open notebook doc), printing per document its uri + notebookType and per cell the executionOrder/success and output items. This captures the Interactive Window that runs the `.py` Databricks notebook. - Update the call site and comments accordingly. Still diagnostics-only, failure-path-only; no product or assertion changes. *Verification* - `tsc --noEmit -p src/test/e2e/tsconfig.json`: 0 errors. - eslint + prettier on the file: clean. - Purpose is to make the next e2e run surface the `%sql`/`_sqldf` cell error so the magic-comments failure can be root-caused with evidence. Co-authored-by: Isaac --- .../src/test/e2e/run_dbconnect.ucws.e2e.ts | 90 +++++++++++-------- 1 file changed, 52 insertions(+), 38 deletions(-) diff --git a/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts b/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts index afd305d20..2e8973075 100644 --- a/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts +++ b/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts @@ -97,52 +97,64 @@ async function listFilesRecursive(dir: string, base = dir): Promise { return out; } -// Dumps the active notebook's cell outputs straight from the VS Code API. This -// is the ground truth for "did the cell error out?": a cell that raised shows -// up here as an `error` output item with the name/message/stack, and a cell -// that never ran has an empty `executionOrder`. We read it via +// Dumps every open notebook document's cell outputs straight from the VS Code +// API. This is the ground truth for "did the cell error out?": a cell that +// raised shows up here as an `error` output item with the name/message/stack, +// and a cell that never ran has an empty `executionOrder`. We read it via // `executeWorkbench` (runs inside the extension host with the full `vscode` // API) so it does not depend on any DOM selector — unlike the Output-channel // scrape below, whose `select[title="Tasks"]` locator is stale on current // VS Code and silently returns nothing. -async function dumpActiveNotebookCells() { +// +// We iterate `workspace.notebookDocuments` rather than just +// `window.activeNotebookEditor` because the Databricks `.py` "notebook" +// (`# Databricks notebook source` + `# MAGIC %sql`) runs in an Interactive +// Window whose document is NOT the active notebook editor — the focused editor +// is the plain `.py` text editor, so the active-editor-only view reports "no +// notebook" and misses exactly the failing cell we need to see. +async function dumpOpenNotebookCells() { try { const cells = await browser.executeWorkbench((vscode) => { - const editor = (vscode.window as any).activeNotebookEditor; - const doc = editor?.notebook; - if (!doc) { - return ""; + const formatCell = (cell: any) => { + const outputs = (cell.outputs ?? []).flatMap((o: any) => + (o.items ?? []).map((item: any) => { + const text = Buffer.from(item.data).toString("utf8"); + return ` [${item.mime}] ${text}`; + }) + ); + return [ + ` cell #${cell.index} (${ + cell.kind === 2 ? "code" : "markup" + }), ` + + `executionOrder=${ + cell.executionSummary?.executionOrder ?? "" + }, ` + + `success=${cell.executionSummary?.success ?? ""}`, + ...outputs, + ].join("\n"); + }; + + const docs = vscode.workspace.notebookDocuments ?? []; + if (docs.length === 0) { + return ""; } - return doc - .getCells() - .map((cell: any) => { - const outputs = (cell.outputs ?? []).flatMap((o: any) => - (o.items ?? []).map((item: any) => { - const text = Buffer.from(item.data).toString( - "utf8" - ); - return ` [${item.mime}] ${text}`; - }) + return docs + .map((doc: any) => { + const header = `notebook "${doc.uri?.toString()}" (${ + doc.notebookType + }), ${doc.cellCount} cell(s)`; + return [header, ...doc.getCells().map(formatCell)].join( + "\n" ); - return [ - `cell #${cell.index} (${ - cell.kind === 2 ? "code" : "markup" - }), ` + - `executionOrder=${ - cell.executionSummary?.executionOrder ?? - "" - }, ` + - `success=${ - cell.executionSummary?.success ?? "" - }`, - ...outputs, - ].join("\n"); }) - .join("\n"); + .join("\n\n"); }); - console.log("=== active notebook cells (via VS Code API) ===\n", cells); + console.log( + "=== open notebook documents (via VS Code API) ===\n", + cells + ); } catch (e) { - console.log("could not read active notebook cells:", e); + console.log("could not read open notebook documents:", e); } } @@ -226,9 +238,11 @@ async function dumpNotebookDiagnostics( } } - // Cell outputs + execution summaries via the VS Code API (the reliable - // source for a cell error/traceback or a never-run cell). - await dumpActiveNotebookCells(); + // Cell outputs + execution summaries for every open notebook document via + // the VS Code API (the reliable source for a cell error/traceback or a + // never-run cell, including the Interactive Window used by the `.py` + // Databricks notebook). + await dumpOpenNotebookCells(); // On-disk VS Code logs (extension host + Output-channel logs), which // capture the kernel/DBConnect error even when the panel can't be scraped. From 28c6ffc8a8d0cad61b4fa262a4f7515f0497a58e Mon Sep 17 00:00:00 2001 From: "@rugpanov" Date: Tue, 14 Jul 2026 17:20:23 +0200 Subject: [PATCH 3/3] test(e2e): inline the notebook-dump callback to avoid "__name is not defined" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit *Why* The previous commit's `dumpOpenNotebookCells` defined a named nested arrow (`const formatCell = (cell) => …`) inside the `browser.executeWorkbench` callback. WebdriverIO v9 transpiles the spec with tsx (esbuild, keepNames on), which rewrites a named binding as `const formatCell = __name(() => …, "formatCell")`. `executeWorkbench` serializes the callback via `.toString()` and evals it in the extension host, where the esbuild `__name` helper does not exist — so the callback threw "__name is not defined" and the whole dump was lost (CI run 29342709724 printed "could not read open notebook documents: Error: __name is not defined"). *What* - Inline the per-cell formatting directly into the `.map()` callbacks so there is no named nested binding for keepNames to wrap. Verified with the real tsx toolchain: `fn.toString()` of the inlined version contains no `__name`, whereas the named-const version emits `__name(...,"formatCell")`. - Add a comment warning that this callback must stay inline-arrow-only. - Behavior otherwise unchanged: still iterates `workspace.notebookDocuments` and prints per-cell executionOrder/success/outputs on the failure path only. *Verification* - Transpiled the actual file with the repo's esbuild+keepNames: 0 `__name` occurrences inside `dumpOpenNotebookCells`. - `tsc --noEmit -p src/test/e2e/tsconfig.json`: 0 errors; eslint + prettier: clean. Co-authored-by: Isaac --- .../src/test/e2e/run_dbconnect.ucws.e2e.ts | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts b/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts index 2e8973075..9e46e701a 100644 --- a/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts +++ b/packages/databricks-vscode/src/test/e2e/run_dbconnect.ucws.e2e.ts @@ -114,26 +114,11 @@ async function listFilesRecursive(dir: string, base = dir): Promise { // notebook" and misses exactly the failing cell we need to see. async function dumpOpenNotebookCells() { try { + // NB: everything here must stay inline arrow functions. This callback is + // serialized and run in the extension host, so a named nested function + // (e.g. `const formatCell = () => …`) gets an esbuild `__name(...)` + // wrapper that is undefined there and throws "__name is not defined". const cells = await browser.executeWorkbench((vscode) => { - const formatCell = (cell: any) => { - const outputs = (cell.outputs ?? []).flatMap((o: any) => - (o.items ?? []).map((item: any) => { - const text = Buffer.from(item.data).toString("utf8"); - return ` [${item.mime}] ${text}`; - }) - ); - return [ - ` cell #${cell.index} (${ - cell.kind === 2 ? "code" : "markup" - }), ` + - `executionOrder=${ - cell.executionSummary?.executionOrder ?? "" - }, ` + - `success=${cell.executionSummary?.success ?? ""}`, - ...outputs, - ].join("\n"); - }; - const docs = vscode.workspace.notebookDocuments ?? []; if (docs.length === 0) { return ""; @@ -143,9 +128,30 @@ async function dumpOpenNotebookCells() { const header = `notebook "${doc.uri?.toString()}" (${ doc.notebookType }), ${doc.cellCount} cell(s)`; - return [header, ...doc.getCells().map(formatCell)].join( - "\n" - ); + const cellLines = doc.getCells().map((cell: any) => { + const outputs = (cell.outputs ?? []).flatMap((o: any) => + (o.items ?? []).map( + (item: any) => + ` [${item.mime}] ${Buffer.from( + item.data + ).toString("utf8")}` + ) + ); + return [ + ` cell #${cell.index} (${ + cell.kind === 2 ? "code" : "markup" + }), ` + + `executionOrder=${ + cell.executionSummary?.executionOrder ?? + "" + }, ` + + `success=${ + cell.executionSummary?.success ?? "" + }`, + ...outputs, + ].join("\n"); + }); + return [header, ...cellLines].join("\n"); }) .join("\n\n"); });