Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {expect} from "chai";
import {buildVersionPickItems} from "./serverlessVersionPicker";

describe("buildVersionPickItems", () => {
it("marks the top candidate as the recommendation and shows its provenance", () => {
const items = buildVersionPickItems([
{version: "4", score: 150, sources: ["bundleYaml", "notebook"]},
{version: "5", score: 20, sources: ["workspaceDefault"]},
]);

expect(items[0].version).to.equal("4");
// `picked` is set on the top row (cosmetic in single-select, but kept
// for completeness); the star label + first-position ordering are the
// actual recommendation cues.
expect(items[0].picked).to.equal(true);
// Multi-source provenance is summarised in the description.
expect(items[0].description).to.match(/bundle|notebook|2 source/i);
expect(items[1].version).to.equal("5");
expect(items[1].picked).to.equal(false);
});

it("labels each item with the bare version and only stars the picked one", () => {
const items = buildVersionPickItems([
{version: "5", score: 100, sources: ["bundleYaml"]},
{version: "4", score: 50, sources: ["notebook"]},
]);

// The picked item is visually marked; others are the plain version.
expect(items[0].label).to.contain("5");
expect(items[0].label).to.not.equal(items[0].version);
expect(items[1].label).to.equal("4");
});

it("handles a fallback-only list", () => {
const items = buildVersionPickItems([
{version: "5", score: 1, sources: ["fallback"]},
]);

expect(items).to.have.length(1);
expect(items[0].version).to.equal("5");
expect(items[0].picked).to.equal(true);
expect(items[0].description).to.match(/fallback|default/i);
});

it("returns no items for an empty ranking", () => {
expect(buildVersionPickItems([])).to.deep.equal([]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {window} from "vscode";
import {ScoredVersion, VersionSource} from "./serverlessVersionScoring";

/** A pick item derived from a scored version; `version` is the bare value. */
export interface VersionPickItem {
label: string;
description: string;
version: string;
picked: boolean;
}

/** Human-readable provenance labels (the enum keys are internal jargon). */
const SOURCE_LABELS: Record<VersionSource, string> = {
/* eslint-disable @typescript-eslint/naming-convention */
bundleYaml: "bundle config",
notebook: "notebook metadata",
workspaceDefault: "workspace default",
fallback: "default",
/* eslint-enable @typescript-eslint/naming-convention */
};

function describeSources(sources: VersionSource[]): string {
const labels = sources.map((s) => SOURCE_LABELS[s]);
if (labels.length === 0) {
return "";
}
if (labels.length === 1) {
return `from ${labels[0]}`;
}
return `from ${labels.length} sources: ${labels.join(", ")}`;
}

/**
* Build the QuickPick rows for a ranked version list (pure, so the labelling is
* unit-testable without a VS Code host). The first, i.e. highest-scoring,
* candidate is marked as the recommendation: it is listed first and visually
* starred. (`picked` is also set for completeness, but note `showQuickPick`
* only honours it in multi-select mode -- see {@link pickServerlessVersion} --
* so in this single-select picker the star and ordering are what actually
* signal the recommendation.) Every row carries its bare `version` for the
* caller to forward to the CLI, and a `description` summarising where the
* version came from.
*/
export function buildVersionPickItems(
ranked: ScoredVersion[]
): VersionPickItem[] {
return ranked.map((r, i) => {
const picked = i === 0;
return {
label: picked ? `$(star-full) ${r.version}` : r.version,
description: describeSources(r.sources),
version: r.version,
picked,
};
});
}

/**
* Show an always-visible, ranked serverless-version QuickPick with the top
* candidate presented as the recommendation (starred, listed first, and named
* in the placeholder), and return the confirmed bare version (or undefined if
* the user dismissed it). Selection is never silent -- the user must actively
* confirm a row, matching the "no silent auto-selection" requirement.
*/
export async function pickServerlessVersion(
ranked: ScoredVersion[]
): Promise<string | undefined> {
const items = buildVersionPickItems(ranked);
if (items.length === 0) {
return undefined;
}
const selected = await window.showQuickPick(items, {
title: "Select serverless environment version",
placeHolder: `Recommended: ${items[0].version}`,
});
return selected?.version;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {expect} from "chai";
import {
resolveServerlessVersion,
ServerlessVersionResolverDeps,
} from "./serverlessVersionResolver";
import {ScoredVersion, VersionObservation} from "./serverlessVersionScoring";

function makeDeps(
overrides: Partial<ServerlessVersionResolverDeps> = {}
): ServerlessVersionResolverDeps & {
collectCalls: number;
pickedRankings: ScoredVersion[][];
} {
let collectCalls = 0;
const pickedRankings: ScoredVersion[][] = [];
const base: ServerlessVersionResolverDeps = {
collectObservations: async () => [] as VersionObservation[],
pick: async (ranked) => {
pickedRankings.push(ranked);
return ranked[0]?.version;
},
...overrides,
};
// Wrap the (possibly overridden) collector so the call count reflects real
// invocations regardless of which collector a test supplies.
const collect = base.collectObservations;
const deps: ServerlessVersionResolverDeps = {
...base,
collectObservations: async () => {
collectCalls += 1;
return collect();
},
};
const probe = deps as ServerlessVersionResolverDeps & {
collectCalls: number;
pickedRankings: ScoredVersion[][];
};
probe.pickedRankings = pickedRankings;
// Define a *live* accessor so `collectCalls` reflects real invocations at
// assert time (a plain copy would freeze it at 0).
Object.defineProperty(probe, "collectCalls", {
get: () => collectCalls,
enumerable: true,
});
return probe;
}

describe("resolveServerlessVersion", () => {
// The feature-flag gate is the caller's responsibility (see
// ConnectionCommands.selectServerless); this function is only invoked when
// the flow is active, so it always collects, scores, and offers a pick.
it("scores collected observations and returns the confirmed version", async () => {
const deps = makeDeps({
collectObservations: async () => [
{version: "4", source: "bundleYaml"},
{version: "3", source: "notebook"},
],
pick: async (ranked) => ranked[0].version,
});

const version = await resolveServerlessVersion(deps);

// Both versions are in range, so this genuinely exercises weighting:
// bundleYaml (100) outranks notebook (50), so "4" is recommended.
expect(version).to.equal("4");
// The live counter proves collection ran exactly once -- the collector
// runs during resolve (after makeDeps wires this accessor), so a plain
// snapshot copy would read a stale 0.
expect(deps.collectCalls).to.equal(1);
});

it("offers the fallback candidate even when nothing was observed", async () => {
const deps = makeDeps({
collectObservations: async () => [],
});

const version = await resolveServerlessVersion(deps);

// The ranking handed to the picker always contains the fallback "5".
expect(deps.pickedRankings[0].map((r) => r.version)).to.include("5");
expect(version).to.equal("5");
});

it("returns undefined when the user dismisses the picker", async () => {
const deps = makeDeps({
collectObservations: async () => [
{version: "5", source: "workspaceDefault"},
],
pick: async () => undefined,
});

const version = await resolveServerlessVersion(deps);

expect(version).to.equal(undefined);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {workspaceConfigs} from "../../vscode-objs/WorkspaceConfigs";
import {PYTHON_SETUP_FEATURE_ID} from "../../feature-manager/FeatureManager";
import {
scoreServerlessVersions,
VersionObservation,
} from "./serverlessVersionScoring";

/**
* Whether the user has opted into the uv-native Python environment setup.
*
* The feature ships disabled by default (its CLI command is only in custom CLI
* builds); it unlocks when {@link PYTHON_SETUP_FEATURE_ID} is present in
* `databricks.experiments.optInto`. This is the same string the FeatureManager
* matches to unlock the feature, so the flag can never disagree with it. Kept
* in `python-setup/` rather than on `workspaceConfigs` to avoid a circular
* import (WorkspaceConfigs <-> FeatureManager).
*/
export function isPythonSetupEnabled(): boolean {
return workspaceConfigs.experimetalFeatureOverides.includes(
PYTHON_SETUP_FEATURE_ID
);
}

/**
* Injected collaborators for {@link resolveServerlessVersion}. The (I/O-bound)
* observation collection and the QuickPick are seams so the resolution flow is
* unit-testable without a VS Code host, and the pieces wired at the extension
* site can be swapped in later.
*
* The feature-flag gate is deliberately NOT here: callers own it, because
* "feature off" and "user dismissed the picker" need different handling at the
* call site (e.g. the compute picker still enables plain serverless when the
* flag is off, but makes no change on dismissal) -- both of which this function
* would otherwise collapse into a single `undefined`.
*/
export interface ServerlessVersionResolverDeps {
/** Gather raw version observations (bundle YAML, notebooks, workspace default). */
collectObservations: () => Promise<VersionObservation[]>;
/** Present the ranked candidates and return the confirmed bare version. */
pick: (
ranked: ReturnType<typeof scoreServerlessVersions>
) => Promise<string | undefined>;
}

/**
* Resolve the serverless environment version to provision: collect the evidence
* for what version this project should use (bundle YAML, notebooks, workspace
* default), score it, and let the user confirm the best-ranked candidate.
* Returns the confirmed bare version (e.g. "5", the `--serverless-version`
* value) or `undefined` when the user dismisses the picker.
*
* Callers gate this on the feature flag ({@link isPythonSetupEnabled}); it is
* only invoked when the flow is active. It is deliberately independent of the
* legacy `databricks.connect.serverlessDbconnectVersion` setting (a
* DBR/dbconnect version like "17.3"): that is a different namespace consumed by
* the legacy pip flow and must not leak into `--serverless-version`.
*/
export async function resolveServerlessVersion(
deps: ServerlessVersionResolverDeps
): Promise<string | undefined> {
const observations = await deps.collectObservations();
const ranked = scoreServerlessVersions(observations);
return deps.pick(ranked);
}
Loading
Loading