fix: resolve stopWorkspaceIfRunning common path (#25639)

Closes #24333

This was a common setup in `updateWorkspace()` but was not appropriately
ported to `changeWorkspaceVersion()`. Some tests have been added also to
ensure this works 🙂 Simple smooth and easy.
This commit is contained in:
Jake Howell
2026-05-27 00:01:10 +10:00
committed by GitHub
parent 8a2f28fa6a
commit b4332443b3
2 changed files with 120 additions and 13 deletions
+97
View File
@@ -1,4 +1,5 @@
import {
MockProvisionerJob,
MockStoppedWorkspace,
MockTemplate,
MockTemplateVersion2,
@@ -275,6 +276,102 @@ describe("api.ts", () => {
});
});
describe("changeWorkspaceVersion", () => {
it("stops workspace before changing version if running", async () => {
vi.spyOn(API, "stopWorkspace").mockResolvedValueOnce({
...MockWorkspaceBuild,
transition: "stop",
});
vi.spyOn(API, "waitForBuild").mockResolvedValueOnce({
...MockProvisionerJob,
status: "succeeded",
});
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce(
[],
);
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
template_version_id: MockTemplateVersion2.id,
transition: "start",
});
await API.changeWorkspaceVersion(MockWorkspace, MockTemplateVersion2.id);
expect(API.stopWorkspace).toHaveBeenCalledWith(MockWorkspace.id);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start",
template_version_id: MockTemplateVersion2.id,
rich_parameter_values: [],
});
});
it("does not stop workspace if already stopped", async () => {
vi.spyOn(API, "stopWorkspace");
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce(
[],
);
vi.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
template_version_id: MockTemplateVersion2.id,
transition: "start",
});
await API.changeWorkspaceVersion(
MockStoppedWorkspace,
MockTemplateVersion2.id,
);
expect(API.stopWorkspace).not.toHaveBeenCalled();
});
it("rejects if stop is canceled", async () => {
vi.spyOn(API, "stopWorkspace").mockResolvedValueOnce({
...MockWorkspaceBuild,
transition: "stop",
});
vi.spyOn(API, "waitForBuild").mockResolvedValueOnce({
...MockProvisionerJob,
status: "canceled",
});
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce(
[],
);
vi.spyOn(API, "postWorkspaceBuild");
await expect(
API.changeWorkspaceVersion(MockWorkspace, MockTemplateVersion2.id),
).rejects.toThrow("Workspace stop was canceled");
expect(API.postWorkspaceBuild).not.toHaveBeenCalled();
});
it("throws MissingBuildParameters for missing params", async () => {
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
vi.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
let error = new Error();
try {
await API.changeWorkspaceVersion(
MockStoppedWorkspace,
MockTemplateVersion2.id,
);
} catch (e) {
error = e as Error;
}
expect(error).toBeInstanceOf(MissingBuildParameters);
expect((error as MissingBuildParameters).parameters).toEqual([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
});
});
describe("chat configuration endpoints", () => {
it.each<[string, () => Promise<unknown>, unknown]>([
[
+23 -13
View File
@@ -2498,6 +2498,24 @@ class ApiMethods {
}));
};
/**
* Stops a workspace if it is currently running and waits for the stop
* to complete. Throws if the stop build is canceled.
*/
private stopWorkspaceIfRunning = async (
workspace: TypesGen.Workspace,
): Promise<void> => {
// Workspace is already in a state where it's "stopped".
if (workspace.latest_build.status !== "running") return;
const stopBuild = await this.stopWorkspace(workspace.id);
const awaitedStopBuild = await this.waitForBuild(stopBuild);
if (awaitedStopBuild?.status === "canceled") {
throw new Error("Workspace stop was canceled.");
}
};
/** Steps to change the workspace version
* - Get the latest template to access the latest active version
* - Get the current build parameters
@@ -2505,6 +2523,7 @@ class ApiMethods {
* - Update the build parameters and check if there are missed parameters for
* the new version
* - If there are missing parameters raise an error
* - Stop the workspace if it is already running
* - Create a build with the version and updated build parameters
*/
changeWorkspaceVersion = async (
@@ -2539,6 +2558,8 @@ class ApiMethods {
throw new MissingBuildParameters(missingParameters, templateVersionId);
}
await this.stopWorkspaceIfRunning(workspace);
return this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: templateVersionId,
@@ -2553,7 +2574,7 @@ class ApiMethods {
* - Update the build parameters and check if there are missed parameters for
* the newest version
* - If there are missing parameters raise an error
* - Stop the workspace with the current template version if it is already running
* - Stop the workspace if it is already running
* - Create a build with the latest version and updated build parameters
*/
updateWorkspace = async (
@@ -2585,18 +2606,7 @@ class ApiMethods {
}
}
// Stop the workspace if it is already running.
if (workspace.latest_build.status === "running") {
const stopBuild = await this.stopWorkspace(workspace.id);
const awaitedStopBuild = await this.waitForBuild(stopBuild);
// If the stop is canceled halfway through, we bail.
// This is the same behaviour as restartWorkspace.
if (awaitedStopBuild?.status === "canceled") {
return Promise.reject(
new Error("Workspace stop was canceled, not proceeding with update."),
);
}
}
await this.stopWorkspaceIfRunning(workspace);
try {
return await this.postWorkspaceBuild(workspace.id, {