Skip to content

Commit 7db24fc

Browse files
authored
feat(python-setup): link setup failures to remediation docs (#2125)
## What When `environments setup-local` fails, the extension already surfaces the failing phase, the error code, and a friendly per-code message. But only two codes (uv-missing and a blocked package index) pointed the user at documentation — every other failure left them with no next step. This adds a documentation link per error code, reusing the existing `PythonSetupErrorAction` shape: - A sparse `DOC_LINKS` map in `errorMessages.ts` keyed by error code. - `getPythonSetupErrorAction` returns the mapped link as the failure popup's button for `E_MANAGER_UNSUPPORTED`, `E_PYTHON_INSTALL`, `E_PROVISION` (dependency conflict), `E_NO_TARGET`, `E_RESOLVE`, and `E_ENV_UNSUPPORTED`, on top of the existing uv-missing and blocked-index cases. - `formatSetupFailureDetail` spells the same link into the **output channel**, so it stays reachable after the notification is dismissed. Codes with no reliably-helpful page (`E_USAGE`, `E_NOT_WRITABLE`, `E_WRITE`, `E_MERGE`, `E_VALIDATE`, `E_FETCH`) deliberately stay message-only rather than link to a page that could misdirect. ### Link mapping | Error code | Link | | --- | --- | | `E_UV_MISSING` | uv installation *(existing)* | | `E_PROVISION` (blocked index) | uv index configuration *(existing)* | | `E_MANAGER_UNSUPPORTED` | uv projects | | `E_PYTHON_INSTALL` | uv install Python | | `E_PROVISION` (dependency conflict) | uv resolution | | `E_NO_TARGET`, `E_RESOLVE` | VS Code extension — configure compute | | `E_ENV_UNSUPPORTED` | Databricks Runtime release notes | ## Testing - `yarn test:unit` — all python-setup / `errorMessages` / orchestrator tests pass. - `eslint` + `prettier` clean on the changed files. - All documentation URLs were fetched and confirmed to resolve. This pull request and its description were written by Isaac.
1 parent 315f77d commit 7db24fc

3 files changed

Lines changed: 223 additions & 20 deletions

File tree

packages/databricks-vscode/src/python-setup/controllers/PythonSetupEnvironmentSetup.test.ts

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,9 @@ import {
1313
SUCCESS_REAL_RUN,
1414
SUCCESS_REAL_RUN_WITH_WARNINGS,
1515
ERROR_NO_TARGET,
16+
ERROR_USAGE,
1617
} from "../models/fixtures/setupLocalResults";
17-
import {
18-
PythonSetupErrorAction,
19-
UV_INSTALL_DOCS_URL,
20-
} from "../utils/errorMessages";
18+
import {PythonSetupErrorAction} from "../utils/errorMessages";
2119
import {SetupLocalInvocation} from "../utils/setupLocalArgs";
2220
import {
2321
PythonSetupAttempt,
@@ -374,11 +372,11 @@ describe("PythonSetupEnvironmentSetup.setup", () => {
374372
expect(shown).to.have.length(1);
375373
expect(shown[0].action).to.deep.equal({
376374
label: "Install uv",
377-
url: UV_INSTALL_DOCS_URL,
375+
url: "https://docs.astral.sh/uv/getting-started/installation/",
378376
});
379377
});
380378

381-
it("passes no remediation action for failures other than uv-missing", async () => {
379+
it("offers the mapped documentation action for a doc-linked failure", async () => {
382380
const shown: {action?: PythonSetupErrorAction}[] = [];
383381
const setup = new PythonSetupEnvironmentSetup(
384382
makeDeps({
@@ -391,6 +389,27 @@ describe("PythonSetupEnvironmentSetup.setup", () => {
391389

392390
await setup.setup();
393391

392+
expect(shown).to.have.length(1);
393+
expect(shown[0].action).to.deep.equal({
394+
label: "Configure compute",
395+
url: "https://docs.databricks.com/aws/en/dev-tools/vscode-ext/configure#cluster",
396+
});
397+
});
398+
399+
it("passes no remediation action for a message-only failure", async () => {
400+
// E_USAGE has no doc that reliably helps, so it surfaces the message alone.
401+
const shown: {action?: PythonSetupErrorAction}[] = [];
402+
const setup = new PythonSetupEnvironmentSetup(
403+
makeDeps({
404+
cli: makeCli({resolve: ERROR_USAGE}),
405+
showError: async (_message, _detail, action) => {
406+
shown.push({action});
407+
},
408+
})
409+
);
410+
411+
await setup.setup();
412+
394413
expect(shown).to.have.length(1);
395414
expect(shown[0].action).to.equal(undefined);
396415
});

packages/databricks-vscode/src/python-setup/utils/errorMessages.test.ts

Lines changed: 111 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import {
44
getPythonSetupErrorAction,
55
getPythonSetupErrorMessage,
66
isIndexUnreachableFailure,
7-
UV_INDEX_DOCS_URL,
8-
UV_INSTALL_DOCS_URL,
97
} from "./errorMessages";
108
import {
119
PythonSetupResult,
@@ -265,7 +263,7 @@ describe("getPythonSetupErrorAction", () => {
265263
);
266264
expect(action).to.deep.equal({
267265
label: "Install uv",
268-
url: UV_INSTALL_DOCS_URL,
266+
url: "https://docs.astral.sh/uv/getting-started/installation/",
269267
});
270268
});
271269

@@ -275,16 +273,98 @@ describe("getPythonSetupErrorAction", () => {
275273
);
276274
expect(action).to.deep.equal({
277275
label: "Configure package index",
278-
url: UV_INDEX_DOCS_URL,
276+
url: "https://docs.astral.sh/uv/configuration/indexes/",
279277
});
280278
});
281279

282-
it("offers no action for an ordinary E_PROVISION conflict", () => {
283-
expect(getPythonSetupErrorAction(failure("E_PROVISION"))).to.equal(
284-
undefined
280+
it("points an ordinary E_PROVISION conflict at the uv resolution docs", () => {
281+
// A genuine dependency conflict (no connectivity symptom) is distinct from
282+
// a blocked index: it links to uv's resolution guide, not the index docs.
283+
expect(getPythonSetupErrorAction(failure("E_PROVISION"))).to.deep.equal(
284+
{
285+
label: "Resolve dependency conflicts",
286+
url: "https://docs.astral.sh/uv/concepts/resolution/",
287+
}
285288
);
286289
});
287290

291+
it("points E_MANAGER_UNSUPPORTED at the uv projects docs", () => {
292+
expect(
293+
getPythonSetupErrorAction(
294+
failure("E_MANAGER_UNSUPPORTED", {failurePhase: "preflight"})
295+
)
296+
).to.deep.equal({
297+
label: "Set up a uv project",
298+
url: "https://docs.astral.sh/uv/concepts/projects/",
299+
});
300+
});
301+
302+
it("points E_PYTHON_INSTALL at the uv install-Python docs", () => {
303+
expect(
304+
getPythonSetupErrorAction(failure("E_PYTHON_INSTALL"))
305+
).to.deep.equal({
306+
label: "Install a Python version",
307+
url: "https://docs.astral.sh/uv/guides/install-python/",
308+
});
309+
});
310+
311+
it("points E_NO_TARGET at the compute-selection section of the configure docs", () => {
312+
expect(
313+
getPythonSetupErrorAction(
314+
failure("E_NO_TARGET", {failurePhase: "resolve"})
315+
)
316+
).to.deep.equal({
317+
label: "Configure compute",
318+
url: "https://docs.databricks.com/aws/en/dev-tools/vscode-ext/configure#cluster",
319+
});
320+
});
321+
322+
it("points E_RESOLVE at the compute-selection section of the configure docs", () => {
323+
expect(
324+
getPythonSetupErrorAction(
325+
failure("E_RESOLVE", {failurePhase: "resolve"})
326+
)
327+
).to.deep.equal({
328+
label: "Configure compute",
329+
url: "https://docs.databricks.com/aws/en/dev-tools/vscode-ext/configure#cluster",
330+
});
331+
});
332+
333+
it("points E_ENV_UNSUPPORTED at the Databricks runtime release notes", () => {
334+
expect(
335+
getPythonSetupErrorAction(
336+
failure("E_ENV_UNSUPPORTED", {failurePhase: "fetch"})
337+
)
338+
).to.deep.equal({
339+
label: "Databricks Runtime versions",
340+
url: "https://docs.databricks.com/aws/en/release-notes/runtime/",
341+
});
342+
});
343+
344+
it("offers no action for E_FETCH (deliberately message-only)", () => {
345+
// A generic network/cache failure has no single doc that reliably helps,
346+
// so we avoid pointing the user at an unclear page.
347+
expect(
348+
getPythonSetupErrorAction(
349+
failure("E_FETCH", {failurePhase: "fetch"})
350+
)
351+
).to.equal(undefined);
352+
});
353+
354+
it("offers no action for codes with no clear remediation doc", () => {
355+
for (const code of [
356+
"E_USAGE",
357+
"E_NOT_WRITABLE",
358+
"E_WRITE",
359+
"E_MERGE",
360+
"E_VALIDATE",
361+
] as const) {
362+
expect(getPythonSetupErrorAction(failure(code)), code).to.equal(
363+
undefined
364+
);
365+
}
366+
});
367+
288368
it("offers no action when the result carries no error", () => {
289369
const ok = failure("E_UV_MISSING");
290370
ok.error = null;
@@ -359,6 +439,30 @@ describe("formatSetupFailureDetail", () => {
359439
);
360440
expect(detail).to.not.contain("UV_INDEX_URL");
361441
});
442+
443+
it("appends the documentation link and its label for a code that has one", () => {
444+
const detail = formatSetupFailureDetail(
445+
failure("E_NO_TARGET", {failurePhase: "resolve"})
446+
);
447+
expect(detail).to.contain(
448+
"https://docs.databricks.com/aws/en/dev-tools/vscode-ext/configure#cluster"
449+
);
450+
expect(detail).to.contain("Configure compute");
451+
});
452+
453+
it("appends the uv index docs link for a blocked index", () => {
454+
const detail = formatSetupFailureDetail(
455+
failure("E_PROVISION", {message: INDEX_UNREACHABLE_CLI_MSG})
456+
);
457+
expect(detail).to.contain(
458+
"https://docs.astral.sh/uv/configuration/indexes/"
459+
);
460+
});
461+
462+
it("adds no documentation link for a message-only code", () => {
463+
const detail = formatSetupFailureDetail(failure("E_USAGE"));
464+
expect(detail).to.not.contain("https://");
465+
});
362466
});
363467

364468
describe("isIndexUnreachableFailure", () => {

packages/databricks-vscode/src/python-setup/utils/errorMessages.ts

Lines changed: 87 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,33 @@ export const UV_INSTALL_DOCS_URL =
4141
export const UV_INDEX_DOCS_URL =
4242
"https://docs.astral.sh/uv/configuration/indexes/";
4343

44+
/** uv's project overview — for E_MANAGER_UNSUPPORTED (no uv project present). */
45+
export const UV_PROJECTS_DOCS_URL =
46+
"https://docs.astral.sh/uv/concepts/projects/";
47+
48+
/** uv's Python-version guide — for E_PYTHON_INSTALL. */
49+
export const UV_PYTHON_INSTALL_DOCS_URL =
50+
"https://docs.astral.sh/uv/guides/install-python/";
51+
52+
/** uv's resolution concept page — for a genuine E_PROVISION dependency conflict. */
53+
export const UV_RESOLUTION_DOCS_URL =
54+
"https://docs.astral.sh/uv/concepts/resolution/";
55+
56+
/**
57+
* Databricks VS Code extension "configure your project" guide, anchored at the
58+
* "Select compute for running code and jobs" section — for E_NO_TARGET /
59+
* E_RESOLVE, where the fix is picking a valid cluster or serverless compute in
60+
* the Configuration view. `#cluster` is that section's curated, stable anchor
61+
* (a Databricks-authored `db-defined-anchor`), preferred over the auto-generated
62+
* heading slug, which would break if the heading text changed.
63+
*/
64+
export const DATABRICKS_CONFIGURE_DOCS_URL =
65+
"https://docs.databricks.com/aws/en/dev-tools/vscode-ext/configure#cluster";
66+
67+
/** Databricks Runtime release notes (supported versions) — for E_ENV_UNSUPPORTED. */
68+
export const DATABRICKS_RUNTIME_DOCS_URL =
69+
"https://docs.databricks.com/aws/en/release-notes/runtime/";
70+
4471
/**
4572
* "Cannot reach the host" phrases in uv's error text (matched case-insensitively).
4673
* TLS-interception and proxy-auth (407) are deliberately left out: they need a
@@ -173,20 +200,67 @@ export function getPythonSetupErrorMessage(result: PythonSetupResult): string {
173200
}
174201

175202
/**
176-
* The remediation button, if any: a blocked index → uv's index-config docs, or
177-
* `E_UV_MISSING` → uv's install docs. Other codes are actionable from the message
178-
* and logs alone.
203+
* Documentation link per error code, surfaced both as the failure popup's
204+
* remediation button and — via {@link formatSetupFailureDetail} — in the output
205+
* channel. Sparse by design: codes whose fix no single page reliably explains
206+
* (E_USAGE, E_NOT_WRITABLE, E_WRITE, E_MERGE, E_VALIDATE, E_FETCH) are absent and
207+
* get the message alone rather than a link that might misdirect.
208+
*
209+
* `E_UV_MISSING` lives here so every code-keyed link takes one path; the
210+
* blocked-index variant of `E_PROVISION` cannot — it is told apart by the CLI's
211+
* message, not its code — so it is resolved ahead of this map (see below).
212+
*/
213+
/* eslint-disable @typescript-eslint/naming-convention */
214+
const DOC_LINKS: Partial<Record<PythonSetupErrorCode, PythonSetupErrorAction>> =
215+
{
216+
E_UV_MISSING: {label: "Install uv", url: UV_INSTALL_DOCS_URL},
217+
E_MANAGER_UNSUPPORTED: {
218+
label: "Set up a uv project",
219+
url: UV_PROJECTS_DOCS_URL,
220+
},
221+
E_PYTHON_INSTALL: {
222+
label: "Install a Python version",
223+
url: UV_PYTHON_INSTALL_DOCS_URL,
224+
},
225+
E_PROVISION: {
226+
label: "Resolve dependency conflicts",
227+
url: UV_RESOLUTION_DOCS_URL,
228+
},
229+
E_NO_TARGET: {
230+
label: "Configure compute",
231+
url: DATABRICKS_CONFIGURE_DOCS_URL,
232+
},
233+
E_RESOLVE: {
234+
label: "Configure compute",
235+
url: DATABRICKS_CONFIGURE_DOCS_URL,
236+
},
237+
E_ENV_UNSUPPORTED: {
238+
label: "Databricks Runtime versions",
239+
url: DATABRICKS_RUNTIME_DOCS_URL,
240+
},
241+
};
242+
/* eslint-enable @typescript-eslint/naming-convention */
243+
244+
/**
245+
* The remediation button / doc link for a failure, if any. A blocked index maps
246+
* to uv's index-config docs; every other link is keyed by error code via
247+
* {@link DOC_LINKS}. Codes absent from the map (e.g. E_USAGE, E_FETCH) are
248+
* actionable from the message and logs alone.
179249
*/
180250
export function getPythonSetupErrorAction(
181251
result: PythonSetupResult
182252
): PythonSetupErrorAction | undefined {
253+
const err = result.error;
254+
if (!err) {
255+
return undefined;
256+
}
257+
// A blocked package index arrives as E_PROVISION and is distinguished by the
258+
// CLI's message, not its code — so resolve it before the code-keyed map,
259+
// ahead of E_PROVISION's generic dependency-conflict link.
183260
if (isIndexUnreachableFailure(result)) {
184261
return {label: "Configure package index", url: UV_INDEX_DOCS_URL};
185262
}
186-
if (result.error?.code === "E_UV_MISSING") {
187-
return {label: "Install uv", url: UV_INSTALL_DOCS_URL};
188-
}
189-
return undefined;
263+
return DOC_LINKS[err.code];
190264
}
191265

192266
/**
@@ -241,6 +315,12 @@ export function formatSetupFailureDetail(
241315
" Use index-url (not extra-index-url) so pypi.org is replaced, not merely supplemented."
242316
);
243317
}
318+
// The same doc link the popup offers as a button, spelled out here so the URL
319+
// is reachable from the log even after the notification is dismissed.
320+
const action = getPythonSetupErrorAction(result);
321+
if (action) {
322+
lines.push("", `${action.label}: ${action.url}`);
323+
}
244324
// Bracket with blank lines so the block stands apart from any streamed CLI
245325
// output already in the channel.
246326
return `\n${lines.join("\n")}\n`;

0 commit comments

Comments
 (0)