fix: allow fixing stale parameters when the active version is required (#30660)

This commit is contained in:
McKayla はな
2026-10-13 17:25:42 -06:00
committed by GitHub
parent 0ea3f16dd3
commit 893461a91f
4 changed files with 119 additions and 2 deletions
@@ -2,6 +2,7 @@ import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import MockServerSocket from "jest-websocket-mock";
import { HttpResponse, http } from "msw";
import { useSearchParams } from "react-router";
import type { MockInstance } from "vitest";
import * as apiModule from "#/api/api";
import type { Workspace } from "#/api/typesGenerated";
@@ -22,9 +23,11 @@ import {
MockStartingWorkspace,
MockStoppedWorkspace,
MockTemplate,
MockTemplateVersion2,
MockWorkspace,
MockWorkspaceBuild,
MockWorkspaceBuildDelete,
mockApiError,
} from "#/testHelpers/entities";
import {
type RenderWithAuthOptions,
@@ -386,6 +389,61 @@ describe("WorkspacePage", () => {
});
});
});
it("sends users who must use the active version to its parameters when a retry fails", async () => {
const workspace: Workspace = {
...MockFailedWorkspace,
outdated: true,
template_require_active_version: true,
template_active_version_id: MockTemplateVersion2.id,
};
server.use(
http.post("/api/v2/authcheck", () => {
const permissions: WorkspacePermissions = {
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: false,
deleteFailedWorkspace: false,
};
return HttpResponse.json(permissions);
}),
);
vi.spyOn(API, "retryWorkspace").mockRejectedValueOnce({
...mockApiError({ message: "cannot use non-active version" }),
code: "ERR_BAD_REQUEST",
});
const ParametersPage: React.FC = () => {
const [searchParams] = useSearchParams();
return (
<div>Parameters for {searchParams.get("templateVersionId")}</div>
);
};
await renderWorkspacePage(workspace, {
extraRoutes: [
{
path: "/:username/:workspace/settings/parameters",
element: <ParametersPage />,
},
],
});
const user = userEvent.setup();
const workspaceActions = screen.getByTestId("workspace-actions");
await user.click(
within(workspaceActions).getByRole("button", { name: retryButtonRe }),
);
await user.click(
await screen.findByRole("button", {
name: "Review workspace settings",
}),
);
expect(
await screen.findByText(`Parameters for ${MockTemplateVersion2.id}`),
).toBeInTheDocument();
});
});
describe("Navigation to other pages", () => {
@@ -92,6 +92,15 @@ export const WorkspaceReadyPage: React.FC<WorkspaceReadyPageProps> = ({
ephemeralParameters: TypesGen.TemplateVersionParameter[];
}>({ open: false, action: "start", ephemeralParameters: [] });
// Users who must stay on the active version can only fix parameters
// against the active version, so send them there from the error dialog.
const parametersVersionId =
workspace.outdated &&
workspace.template_require_active_version &&
!permissions.updateWorkspaceVersion
? workspace.template_active_version_id
: workspace.latest_build.template_version_id;
const [isCancelConfirmOpen, setIsCancelConfirmOpen] = useState(false);
const { mutate: mutateRestartWorkspace, isPending: isRestarting } =
@@ -437,7 +446,7 @@ export const WorkspaceReadyPage: React.FC<WorkspaceReadyPageProps> = ({
onClose={() => setWorkspaceErrorDialog({ open: false })}
workspaceOwner={workspace.owner_name}
workspaceName={workspace.name}
templateVersionId={workspace.latest_build.template_version_id}
templateVersionId={parametersVersionId}
isDeleting={false}
/>
</>
@@ -1,9 +1,11 @@
import { screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { act } from "react";
import { API } from "#/api/api";
import type * as TypesGen from "#/api/typesGenerated";
import { createDeferred } from "#/testHelpers/deferred";
import {
MockOutdatedStoppedWorkspaceRequireActiveVersion,
MockPreviewParameter1,
MockPreviewParameter2,
MockPreviewParameter4,
@@ -269,6 +271,50 @@ describe("WorkspaceParametersPage", () => {
await waitFor(() => expect(submitButton).toBeDisabled());
});
it("allows updating to a required active version without permission to change versions", async () => {
const workspace = MockOutdatedStoppedWorkspaceRequireActiveVersion;
vi.mocked(API.getWorkspaceByOwnerAndName)
.mockReset()
.mockResolvedValue(workspace);
vi.spyOn(API, "checkAuthorization").mockResolvedValue({
readWorkspace: true,
shareWorkspace: true,
updateWorkspace: true,
updateWorkspaceVersion: false,
deleteFailedWorkspace: false,
});
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
const [, mockPublisher] = mockDynamicParameterWebSocket();
renderWorkspaceParametersPage(
`/@${workspace.owner_name}/${workspace.name}/settings?templateVersionId=${workspace.template_active_version_id}`,
);
await connectWithInitialParameters(mockPublisher, [
MockPreviewParameter1,
MockPreviewParameter4,
MockPreviewParameter7,
]);
await waitForLoaderToBeRemoved();
const form = screen.getByTestId("form");
const submitButton = within(form).getByRole("button", {
name: /update and start/i,
});
await waitFor(() => expect(submitButton).toBeEnabled());
await userEvent.click(submitButton);
await waitFor(() =>
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(
workspace.id,
expect.objectContaining({
template_version_id: workspace.template_active_version_id,
}),
),
);
});
it("does not clobber edited parameters", async () => {
vi.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValueOnce([]);
@@ -68,10 +68,14 @@ export const WorkspaceParametersPageView: React.FC<
validateOnBlur: true,
});
// Users who must stay on the active version can still edit parameters
// when the form targets the active version, since submitting it is the
// update the template requires.
const disabled =
workspace.outdated &&
workspace.template_require_active_version &&
!canChangeVersions;
!canChangeVersions &&
templateVersionId !== workspace.template_active_version_id;
// Debounce websocket sends to avoid stale responses overwriting
// the form while the user is still typing.