From 361d8f30fbd774f7887653a1dd9cd4eefb99ca4a Mon Sep 17 00:00:00 2001 From: Anton Nekipelov <226657+anton-107@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:50:20 +0200 Subject: [PATCH] fix(bundle): match modern CLI run URL in JobRunStatus.parseId The databricks CLI changed the job-run URL it prints from the legacy "#job//run/" fragment to the modern "/jobs//runs/?o=.." path. JobRunStatus.parseId only matched the singular "/run/" form, so with a current CLI the extension never captured the run id: startPolling() never ran and the 60s no-run-id timer flipped the tree item to a permanent false "Timeout while fetching run status" for jobs that actually succeeded. Match both "/run/" and "/runs/" with /\/runs?\/(\d+)/, and use \d+ instead of \d* so a stdout chunk splitting right after "/run(s)/" cannot yield an empty capture (parseInt("") === NaN). Add unit tests pinning the CLI-output contract for both the modern and legacy URL forms. Co-authored-by: Isaac --- .../src/bundle/run/JobRunStatus.test.ts | 100 ++++++++++++++++++ .../src/bundle/run/JobRunStatus.ts | 8 +- 2 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 packages/databricks-vscode/src/bundle/run/JobRunStatus.test.ts diff --git a/packages/databricks-vscode/src/bundle/run/JobRunStatus.test.ts b/packages/databricks-vscode/src/bundle/run/JobRunStatus.test.ts new file mode 100644 index 000000000..38c9d3d59 --- /dev/null +++ b/packages/databricks-vscode/src/bundle/run/JobRunStatus.test.ts @@ -0,0 +1,100 @@ +import {expect} from "chai"; +import {mock, instance, when} from "ts-mockito"; +import {install, Clock} from "@sinonjs/fake-timers"; +import {JobRunStatus} from "./JobRunStatus"; +import {AuthProvider} from "../../configuration/auth/AuthProvider"; + +// These tests pin the CLI-output contract that JobRunStatus.parseId depends on. +// The run id is scraped from the run URL the `databricks bundle run` CLI prints +// to the terminal. When the CLI changed the URL format from the legacy +// "#job//run/" fragment to the modern "/jobs//runs/?o=.." path +// (databricks/cli), the old singular-"/run/" regex silently stopped matching and +// the extension showed a permanent false "Timeout while fetching run status" for +// jobs that actually succeeded. These tests guard against that class of drift. +describe("JobRunStatus.parseId", () => { + let clock: Clock; + let authProvider: AuthProvider; + + beforeEach(() => { + // The BundleRunStatus base class arms a 60s "no run id -> timeout" timer + // in its constructor; fake timers keep it from leaking past the test. + clock = install(); + authProvider = mock(); + // parseId calls startPolling() on a successful match, which awaits + // getWorkspaceClient(). Return a never-resolving promise so no real + // network happens and polling never advances runState past what we + // assert. startPolling is wrapped in @onError({throw:false}), so this + // can never reject into the test. + when(authProvider.getWorkspaceClient()).thenReturn( + new Promise(() => {}) as any + ); + }); + + afterEach(() => { + clock.uninstall(); + }); + + function newStatus() { + return new JobRunStatus(instance(authProvider)); + } + + it("extracts the run id from the modern '/jobs//runs/' URL", () => { + const status = newStatus(); + status.parseId( + "Run URL: https://adb-123.azuredatabricks.net/jobs/600749519600807/runs/931844287868435?o=2059256957719798" + ); + expect(status.runId).to.equal("931844287868435"); + }); + + it("extracts the run id from the 'Run available at ...' modern URL", () => { + const status = newStatus(); + status.parseId( + "Run available at https://adb-123.azuredatabricks.net/jobs/600749519600807/runs/931844287868435?o=2059256957719798" + ); + expect(status.runId).to.equal("931844287868435"); + }); + + it("extracts the run id from the legacy '#job//run/' fragment URL", () => { + const status = newStatus(); + status.parseId( + "https://adb-123.azuredatabricks.net/?o=2059256957719798#job/600749519600807/run/931844287868435" + ); + expect(status.runId).to.equal("931844287868435"); + }); + + it("does not capture the job id from a '/jobs/' URL without a run segment", () => { + const status = newStatus(); + status.parseId( + "https://adb-123.azuredatabricks.net/jobs/600749519600807" + ); + expect(status.runId).to.be.undefined; + }); + + it("does not set an empty run id when a chunk splits right after '/runs/'", () => { + // The CLI output arrives in stdout chunks. A split immediately after + // "/runs/" must not produce an empty capture (parseInt("") === NaN). + const status = newStatus(); + status.parseId( + "Run URL: https://adb-123.azuredatabricks.net/jobs/1/runs/" + ); + expect(status.runId).to.be.undefined; + }); + + it("ignores output that contains no run URL", () => { + const status = newStatus(); + status.parseId("Deploying resources..."); + expect(status.runId).to.be.undefined; + }); + + it("keeps the first run id it sees and ignores later output", () => { + const status = newStatus(); + status.parseId( + "Run URL: https://adb-123.azuredatabricks.net/jobs/1/runs/111" + ); + expect(status.runId).to.equal("111"); + status.parseId( + "Run URL: https://adb-123.azuredatabricks.net/jobs/2/runs/222" + ); + expect(status.runId).to.equal("111"); + }); +}); diff --git a/packages/databricks-vscode/src/bundle/run/JobRunStatus.ts b/packages/databricks-vscode/src/bundle/run/JobRunStatus.ts index 68e7a9d0e..460361dc6 100644 --- a/packages/databricks-vscode/src/bundle/run/JobRunStatus.ts +++ b/packages/databricks-vscode/src/bundle/run/JobRunStatus.ts @@ -24,7 +24,13 @@ export class JobRunStatus extends BundleRunStatus { if (this.runId !== undefined || this.runState !== "unknown") { return; } - const match = output.match(/.*\/run\/(\d*).*/); + // The CLI prints a run URL to the terminal, from which we extract the + // run id. CLI >= v1.5.0 prints the modern "/jobs//runs/?o=.." + // URL, while older CLIs printed the legacy "#job//run/" + // fragment. `runs?` matches both. `\d+` (not `\d*`) requires at least + // one digit, so a stdout chunk that splits right after "/run(s)/" does + // not produce an empty capture (parseInt("") === NaN). + const match = output.match(/\/runs?\/(\d+)/); if (match === null) { return; }