Skip to content

test: reduce flakiness of workspace update test #19294

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
109 changes: 37 additions & 72 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import type dayjs from "dayjs";
import userAgentParser from "ua-parser-js";
import { OneWayWebSocket } from "../utils/OneWayWebSocket";
import { delay } from "../utils/delay";
import { type FieldError, isApiError } from "./errors";
import { ParameterValidationError, isApiError } from "./errors";
import type {
DynamicParametersRequest,
PostWorkspaceUsageRequest,
Expand Down Expand Up @@ -398,15 +398,6 @@ export class MissingBuildParameters extends Error {
}
}

export class ParameterValidationError extends Error {
constructor(
public readonly versionId: string,
public readonly validations: FieldError[],
) {
super("Parameters are not valid for new template version");
}
}

export type GetProvisionerJobsParams = {
status?: string;
limit?: number;
Expand Down Expand Up @@ -2240,63 +2231,11 @@ class ApiMethods {
const currentBuildParameters = await this.getWorkspaceBuildParameters(
workspace.latest_build.id,
);

let templateParameters: TypesGen.TemplateVersionParameter[] = [];
if (isDynamicParametersEnabled) {
templateParameters = await this.getDynamicParameters(
templateVersionId,
workspace.owner_id,
currentBuildParameters,
);
} else {
templateParameters =
await this.getTemplateVersionRichParameters(templateVersionId);
}

const missingParameters = getMissingParameters(
currentBuildParameters,
newBuildParameters,
templateParameters,
);

if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters, templateVersionId);
}

return this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: templateVersionId,
rich_parameter_values: newBuildParameters,
});
};

/** Steps to update the workspace
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - 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
* - Create a build with the latest version and updated build parameters
*/
updateWorkspace = async (
workspace: TypesGen.Workspace,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
isDynamicParametersEnabled = false,
): Promise<TypesGen.WorkspaceBuild> => {
const [template, oldBuildParameters] = await Promise.all([
this.getTemplate(workspace.template_id),
this.getWorkspaceBuildParameters(workspace.latest_build.id),
]);

const activeVersionId = template.active_version_id;

if (isDynamicParametersEnabled) {
try {
return await this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: activeVersionId,
template_version_id: templateVersionId,
rich_parameter_values: newBuildParameters,
});
} catch (error) {
Expand All @@ -2308,8 +2247,9 @@ class ApiMethods {
error.response.data.validations &&
error.response.data.validations.length > 0
) {
console.log(error.response.data.validations);
throw new ParameterValidationError(
activeVersionId,
templateVersionId,
error.response.data.validations,
);
}
Expand All @@ -2318,38 +2258,63 @@ class ApiMethods {
}

const templateParameters =
await this.getTemplateVersionRichParameters(activeVersionId);
await this.getTemplateVersionRichParameters(templateVersionId);

const missingParameters = getMissingParameters(
oldBuildParameters,
currentBuildParameters,
newBuildParameters,
templateParameters,
);

if (missingParameters.length > 0) {
throw new MissingBuildParameters(missingParameters, activeVersionId);
throw new MissingBuildParameters(missingParameters, templateVersionId);
}

// 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 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."),
throw new Error(
"Workspace stop was canceled, not proceeding with update.",
);
}
}

return this.postWorkspaceBuild(workspace.id, {
transition: "start",
template_version_id: activeVersionId,
template_version_id: templateVersionId,
rich_parameter_values: newBuildParameters,
});
};

/** Steps to update the workspace
* - Get the latest template to access the latest active version
* - Get the current build parameters
* - Get the template parameters
* - 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
* - Create a build with the latest version and updated build parameters
*/
updateWorkspace = async (
workspace: TypesGen.Workspace,
newBuildParameters: TypesGen.WorkspaceBuildParameter[] = [],
isDynamicParametersEnabled = false,
): Promise<TypesGen.WorkspaceBuild> => {
const template = await this.getTemplate(workspace.template_id);

return this.changeWorkspaceVersion(
workspace,
template.active_version_id,
newBuildParameters,
isDynamicParametersEnabled,
);
};

getWorkspaceResolveAutostart = async (
workspaceId: string,
): Promise<TypesGen.ResolveAutostartResponse> => {
Expand Down
9 changes: 9 additions & 0 deletions site/src/api/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,12 @@ export class DetailedError extends Error {
super(message);
}
}

export class ParameterValidationError extends Error {
constructor(
public readonly versionId: string,
public readonly validations: FieldError[],
) {
super("Parameters are not valid for new template version");
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MissingBuildParameters, ParameterValidationError } from "api/api";
import { isApiError } from "api/errors";
import { MissingBuildParameters } from "api/api";
import { ParameterValidationError, isApiError } from "api/errors";
import { type ApiError, getErrorMessage } from "api/errors";
import {
changeVersion,
Expand Down
3 changes: 2 additions & 1 deletion site/src/modules/workspaces/WorkspaceUpdateDialogs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { MissingBuildParameters, ParameterValidationError } from "api/api";
import { MissingBuildParameters } from "api/api";
import { ParameterValidationError } from "api/errors";
import { updateWorkspace } from "api/queries/workspaces";
import type {
TemplateVersion,
Expand Down
67 changes: 16 additions & 51 deletions site/src/pages/WorkspacePage/WorkspacePage.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import * as apiModule from "api/api";
import { ParameterValidationError } from "api/errors";
import type { TemplateVersionParameter, Workspace } from "api/typesGenerated";
import MockServerSocket from "jest-websocket-mock";
import {
Expand Down Expand Up @@ -282,11 +283,9 @@ describe("WorkspacePage", () => {
});

it("requests an update when the user presses Update", async () => {
// Mocks
jest
.spyOn(API, "getWorkspaceByOwnerAndName")
.mockResolvedValueOnce(MockOutdatedWorkspace);

const updateWorkspaceMock = jest
.spyOn(API, "updateWorkspace")
.mockResolvedValueOnce(MockWorkspaceBuild);
Expand All @@ -306,71 +305,37 @@ describe("WorkspacePage", () => {
});
});

it("updates the parameters when they are missing during update", async () => {
// Mocks
it("requires invalid parameters to be updated", async () => {
jest
.spyOn(API, "getWorkspaceByOwnerAndName")
.mockResolvedValueOnce(MockOutdatedWorkspace);
const updateWorkspaceSpy = jest
const updateWorkspaceMock = jest
.spyOn(API, "updateWorkspace")
.mockRejectedValueOnce(
new MissingBuildParameters(
[MockTemplateVersionParameter1, MockTemplateVersionParameter2],
new ParameterValidationError(
MockOutdatedWorkspace.template_active_version_id,
[
{
field: MockTemplateVersionParameter1.name,
detail:
"Required parameter not provided; parameter value is null",
},
],
),
);

// Render
await renderWorkspacePage(MockWorkspace);

// Actions
// Start workspace update
const user = userEvent.setup();
await user.click(screen.getByTestId("workspace-update-button"));
const confirmButton = await screen.findByTestId("confirm-button");
await user.click(confirmButton);

// The update was called
await waitFor(() => {
expect(API.updateWorkspace).toBeCalled();
updateWorkspaceSpy.mockClear();
});

// After trying to update, a new dialog asking for missed parameters should
// be displayed and filled
const dialog = await waitFor(() => screen.findByTestId("dialog"));
const firstParameterInput = within(dialog).getByLabelText(
MockTemplateVersionParameter1.name,
{ exact: false },
);
await user.clear(firstParameterInput);
await user.type(firstParameterInput, "some-value");
const secondParameterInput = within(dialog).getByLabelText(
MockTemplateVersionParameter2.name,
{ exact: false },
);
await user.clear(secondParameterInput);
await user.type(secondParameterInput, "2");
await user.click(
within(dialog).getByRole("button", { name: /update parameters/i }),
);

// Check if the update was called using the values from the form
await waitFor(() => {
expect(API.updateWorkspace).toHaveBeenCalledWith(
MockOutdatedWorkspace,
[
{
name: MockTemplateVersionParameter1.name,
value: "some-value",
},
{
name: MockTemplateVersionParameter2.name,
value: "2",
},
],
false,
);
});
// Dialog should warn the parameters need to be updated
const dialog = await screen.findByTestId("dialog");
await within(dialog).findByText("Update workspace parameters");
await screen.findByText(/go to the workspace parameters page to review/);
});

it("restart the workspace with one time parameters when having the confirmation dialog", async () => {
Expand Down
Loading