diff --git a/packages/databricks-vscode/src/python-setup/utils/errorMessages.test.ts b/packages/databricks-vscode/src/python-setup/utils/errorMessages.test.ts index e0ab5e5b6..de1116533 100644 --- a/packages/databricks-vscode/src/python-setup/utils/errorMessages.test.ts +++ b/packages/databricks-vscode/src/python-setup/utils/errorMessages.test.ts @@ -6,6 +6,7 @@ import { getPythonSetupErrorMessage, INSTALL_UV_COMMAND_ID, isIndexUnreachableFailure, + isMissingProjectTableFailure, USE_MANUAL_SETUP_COMMAND_ID, } from "./errorMessages"; import { @@ -27,6 +28,15 @@ const INDEX_UNREACHABLE_CLI_MSG = "error: Failed to fetch: `https://pypi.org/simple/ipykernel/`\n" + " Caused by: tcp connect error: Connection refused (os error 61)"; +/** + * The real CLI text for `errNoProjectTable` (libs/localenv): the merge phase + * refuses to write requires-python because the pyproject.toml has no [project] + * table. Wrapped by the "merge managed regions failed:" prefix exactly as it + * reaches the user (see databricks/databricks-vscode#2177). + */ +const NO_PROJECT_TABLE_CLI_MSG = + "merge managed regions failed: pyproject.toml has no [project] table to hold requires-python"; + /** Build a minimal failed result carrying a specific error. */ function failure( code: PythonSetupErrorCode, @@ -153,6 +163,31 @@ describe("getPythonSetupErrorMessage", () => { ); }); + it("maps a [project]-less E_MERGE to actionable copy, not the generic merge text", () => { + // The errNoProjectTable variant: a valid dependency-groups-only manifest, + // so the copy names the concrete fix instead of the generic merge failure. + const msg = getPythonSetupErrorMessage( + failure("E_MERGE", { + message: NO_PROJECT_TABLE_CLI_MSG, + failurePhase: "merge", + }) + ); + expect(msg).to.match(/\[project\] table/i); + expect(msg).to.match(/requires-python/i); + expect(msg).to.contain("databricks.python.environmentSetup"); + expect(msg).to.not.match(/failed to merge the runtime constraints/i); + }); + + it("keeps the generic merge copy for an E_MERGE that is not the [project]-table case", () => { + const msg = getPythonSetupErrorMessage( + failure("E_MERGE", { + message: "cannot merge: unsupported TOML multi-line string", + failurePhase: "merge", + }) + ); + expect(msg).to.match(/failed to merge the runtime constraints/i); + }); + it("reassures nothing changed when diskMutated is false", () => { const msg = getPythonSetupErrorMessage( failure("E_FETCH", {diskMutated: false, failurePhase: "fetch"}) @@ -388,6 +423,20 @@ describe("getPythonSetupErrorAction", () => { }); }); + it("offers no remediation button for a [project]-less E_MERGE (the fix is a manual edit)", () => { + // Option A: the primary fix (add a [project] table) is a manual edit with + // no one-click command, so this variant carries no button — its guidance + // lives in the message and the output-channel detail instead. + expect( + getPythonSetupErrorAction( + failure("E_MERGE", { + message: NO_PROJECT_TABLE_CLI_MSG, + failurePhase: "merge", + }) + ) + ).to.equal(undefined); + }); + it("offers no action for codes with no clear remediation doc", () => { for (const code of [ "E_USAGE", @@ -541,6 +590,33 @@ describe("formatSetupFailureDetail", () => { expect(detail).to.not.contain("databricks.python.environmentSetup"); }); + it("spells out the [project]-table + manual-mode fixes for a [project]-less E_MERGE", () => { + const detail = formatSetupFailureDetail( + failure("E_MERGE", { + message: NO_PROJECT_TABLE_CLI_MSG, + failurePhase: "merge", + }) + ); + // Still carries the raw CLI error … + expect(detail).to.contain("requires-python"); + // … plus both remediation paths (a concrete [project] table + manual mode). + expect(detail).to.contain("[project]"); + expect(detail).to.contain("databricks.python.environmentSetup"); + expect(detail).to.match(/manual/i); + // This variant has no button (Option A), so no "label: undefined" line. + expect(detail).to.not.contain("undefined"); + }); + + it("adds no [project]-table block for a generic E_MERGE", () => { + const detail = formatSetupFailureDetail( + failure("E_MERGE", { + message: "cannot merge: unsupported TOML multi-line string", + }) + ); + expect(detail).to.not.match(/add a minimal \[project\] table/i); + expect(detail).to.not.contain("databricks.python.environmentSetup"); + }); + it("adds no remediation block for a non-connectivity E_PROVISION", () => { const detail = formatSetupFailureDetail( failure("E_PROVISION", { @@ -784,3 +860,52 @@ describe("isIndexUnreachableFailure", () => { expect(isIndexUnreachableFailure(ok)).to.equal(false); }); }); + +describe("isMissingProjectTableFailure", () => { + it("is true for E_MERGE whose message names a missing [project] table", () => { + expect( + isMissingProjectTableFailure( + failure("E_MERGE", {message: NO_PROJECT_TABLE_CLI_MSG}) + ) + ).to.equal(true); + }); + + it("is false for a generic E_MERGE (e.g. a multiline-string rejection)", () => { + expect( + isMissingProjectTableFailure( + failure("E_MERGE", { + message: "cannot merge: unsupported TOML multi-line string", + }) + ) + ).to.equal(false); + }); + + it("is false for an E_MERGE that references a [project] table without saying there is none", () => { + // Guards the match against a hypothetical future merge defect whose + // message merely mentions the [project] table (e.g. an invalid value in + // it): that is a real bug and must keep its generic copy and bug-report + // prompt, not be relabeled as the user-fixable missing-table case. + expect( + isMissingProjectTableFailure( + failure("E_MERGE", { + message: "failed to rewrite [project] table: invalid TOML", + }) + ) + ).to.equal(false); + }); + + it("is false for a non-E_MERGE code even with a matching message", () => { + // Scoped to E_MERGE: only the merge phase produces errNoProjectTable. + expect( + isMissingProjectTableFailure( + failure("E_WRITE", {message: NO_PROJECT_TABLE_CLI_MSG}) + ) + ).to.equal(false); + }); + + it("is false when there is no error object", () => { + const ok = failure("E_MERGE"); + ok.error = null; + expect(isMissingProjectTableFailure(ok)).to.equal(false); + }); +}); diff --git a/packages/databricks-vscode/src/python-setup/utils/errorMessages.ts b/packages/databricks-vscode/src/python-setup/utils/errorMessages.ts index df3d2663f..4f66c998f 100644 --- a/packages/databricks-vscode/src/python-setup/utils/errorMessages.ts +++ b/packages/databricks-vscode/src/python-setup/utils/errorMessages.ts @@ -128,6 +128,27 @@ export function isIndexUnreachableFailure(result: PythonSetupResult): boolean { return INDEX_CONNECTIVITY_SYMPTOMS.some((s) => msg.includes(s)); } +/** + * True when an E_MERGE failure is the CLI refusing to write requires-python + * because the pyproject.toml has no `[project]` table (libs/localenv's + * `errNoProjectTable`) — a valid, user-fixable manifest shape (a PEP 735 + * dependency-groups-only file), not a merge defect. It gets actionable copy and + * is excluded from the report-a-bug routing, while a generic E_MERGE is + * untouched. The CLI emits no distinct code, so we read its message (mirroring + * {@link isIndexUnreachableFailure}); matching `no [project] table` — not the + * bare phrase — keeps a future merge bug that merely mentions the table from + * being misclassified as this user-fixable case. + */ +export function isMissingProjectTableFailure( + result: PythonSetupResult +): boolean { + const err = result.error; + if (!err || err.code !== "E_MERGE") { + return false; + } + return (err.message?.toLowerCase() ?? "").includes("no [project] table"); +} + /** * Command that flips `databricks.python.environmentSetup` to `manual` for the * current project. Surfaced as the E_FETCH remediation button so a user whose @@ -217,16 +238,36 @@ const INDEX_UNREACHABLE_MESSAGE = "environment variable, or add an index-url to your pip config), then try " + "again. See the logs for details."; +/** + * Popup copy for the `[project]`-less variant of E_MERGE, replacing the generic + * "failed to merge" text. The primary fix is a manual edit (add a `[project]` + * table), so there is no remediation button; {@link formatSetupFailureDetail} + * spells out both fixes for the log. + */ +const MISSING_PROJECT_TABLE_MESSAGE = + "Your pyproject.toml has no [project] table, so setup can't record the " + + "runtime's required Python version (requires-python) there. Add a minimal " + + "[project] table (a name and version) and setup will fill it in — or set " + + '"databricks.python.environmentSetup" to "manual" to skip automated setup ' + + "and use your existing environment as-is."; + export function getPythonSetupErrorMessage(result: PythonSetupResult): string { const err = result.error; if (!err) { return GENERIC; } - // Checked before the per-code map: a blocked index arrives as E_PROVISION, - // whose generic "dependency conflict" copy points at the wrong cause. - const base = isIndexUnreachableFailure(result) - ? INDEX_UNREACHABLE_MESSAGE - : BASE_MESSAGE[err.code]?.(result) ?? GENERIC; + // Checked before the per-code map: both a blocked index (arriving as + // E_PROVISION) and a [project]-less pyproject (arriving as E_MERGE) are told + // apart by the CLI's message, not a distinct code, and their per-code copy + // would misdirect. + let base: string; + if (isIndexUnreachableFailure(result)) { + base = INDEX_UNREACHABLE_MESSAGE; + } else if (isMissingProjectTableFailure(result)) { + base = MISSING_PROJECT_TABLE_MESSAGE; + } else { + base = BASE_MESSAGE[err.code]?.(result) ?? GENERIC; + } return base + diskStateSuffix(result, err); } @@ -403,6 +444,25 @@ export function formatSetupFailureDetail( 'setting to "manual". The extension then uses your existing interpreter/.venv (with its databricks-connect) as-is.' ); } + // The [project]-less variant of E_MERGE: spell out both fixes here so they + // survive the notification being dismissed. The popup only summarises them. + if (isMissingProjectTableFailure(result)) { + lines.push( + "", + "Automated setup writes the runtime's required Python version into " + + "your pyproject.toml's [project] table, but this file has none (a " + + "valid dependency-groups-only manifest). You have two options:", + "", + " 1. Add a minimal [project] table so the pin has a home, for example:", + " [project]", + ' name = "my-project"', + ' version = "0.0.0"', + " Setup fills in requires-python for you — you don't need to set it.", + "", + ' 2. Or skip automated setup and manage the environment yourself: set the "databricks.python.environmentSetup" ' + + 'setting to "manual". The extension then uses your existing interpreter/.venv as-is.' + ); + } // A genuine E_PROVISION conflict gets no report button (it is usually the // user's own dependencies). But if the *published constraints* are what // conflict, that is a defect worth reporting — so offer a soft, conditional diff --git a/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.test.ts b/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.test.ts index 069ee7be0..64e9ef9d8 100644 --- a/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.test.ts +++ b/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.test.ts @@ -24,6 +24,14 @@ const INDEX_UNREACHABLE_CLI_MSG = "error: Failed to fetch: `https://pypi.org/simple/ipykernel/`\n" + " Caused by: tcp connect error: Connection refused (os error 61)"; +/** + * The CLI text for the errNoProjectTable variant of E_MERGE — a valid + * dependency-groups-only pyproject with no [project] table. Used to prove this + * user-fixable manifest shape is NOT report-worthy, unlike a generic E_MERGE. + */ +const NO_PROJECT_TABLE_CLI_MSG = + "merge managed regions failed: pyproject.toml has no [project] table to hold requires-python"; + /** Build a minimal failed result carrying a specific error. */ function failure( code: PythonSetupErrorCode, @@ -99,6 +107,19 @@ describe("reportRepoForResult / isReportWorthy", () => { expect(isReportWorthy(r)).to.equal(false); }); + it("does NOT treat a [project]-less E_MERGE as report-worthy", () => { + // A valid PEP 735 dependency-groups-only manifest is user-fixable, not a + // merge bug (errorMessages gives actionable copy), so it must not prompt a + // bug report — that mis-prompt is what auto-filed issue #2177. Generic + // E_MERGE (above) stays report-worthy. + const r = failure("E_MERGE", { + message: NO_PROJECT_TABLE_CLI_MSG, + failurePhase: "merge", + }); + expect(reportRepoForResult(r)).to.equal(undefined); + expect(isReportWorthy(r)).to.equal(false); + }); + it("does NOT offer a report for preflight/local/network codes", () => { for (const code of [ "E_USAGE", diff --git a/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.ts b/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.ts index 6d4963d79..6a1f3f7b5 100644 --- a/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.ts +++ b/packages/databricks-vscode/src/python-setup/utils/reportSetupIssue.ts @@ -3,6 +3,7 @@ import type { PythonSetupErrorCode, } from "../models/PythonSetupResult"; import type {PrimaryManager} from "../../language/packageManagerDetection"; +import {isMissingProjectTableFailure} from "./errorMessages"; import type {PythonSetupErrorAction} from "./errorMessages"; /** @@ -27,7 +28,9 @@ export type ReportRepo = * constraints have no entry for the runtime (`E_ENV_UNSUPPORTED`) or don't * validate after provisioning (`E_VALIDATE`). * - Extension/CLI behaviour defects → `databricks/databricks-vscode`: merging - * into or writing the user's pyproject.toml broke. + * into or writing the user's pyproject.toml broke. Exception: the + * `[project]`-less variant of `E_MERGE` is a valid, user-fixable manifest + * shape, not a defect — `reportRepoForResult` filters it out (see there). * * `E_PROVISION` is deliberately absent: a uv resolution conflict is usually the * user's own declared dependencies (possibly private packages), not a constraint @@ -77,6 +80,13 @@ export function reportRepoForResult( if (!err) { return undefined; } + // The [project]-less variant of E_MERGE is a valid, user-fixable manifest + // shape (errorMessages gives it actionable copy), not a merge defect — so it + // is not report-worthy, even though a generic E_MERGE is. Prompting a bug + // report for it is what auto-filed databricks/databricks-vscode#2177. + if (isMissingProjectTableFailure(result)) { + return undefined; + } return REPORT_ROUTING[err.code]; }