From 17d214b4a4a75a39fc499784dd2e65f81b7cdcf1 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 4 Mar 2026 11:20:49 -0500 Subject: [PATCH] fix(site): resolve WS/HTTP race condition on workspace parameters page (#22556) ## Problem Flaky e2e test: `update workspace, new required, mutable parameter added` ``` Error: Timed out 15000ms waiting for expect(locator).toHaveValue(expected) Locator: getByTestId('parameter-field-Sixth parameter').locator('input') Expected string: "99" Received string: "" ``` ## Root Cause When the workspace parameters page loads, the WebSocket sends an initial response with template defaults. For parameters with no default (like `sixth_parameter`), the server returns `{valid: false, value: ""}`. On first render, `useSyncFormParameters` sees this invalid server value and overwrites the form's correctly-autofilled value ("99" from the previous build) with "". ## Fix When the server value is `{valid: false}`, preserve the current form value instead of overwriting with "". This prevents the sync hook from clobbering autofilled values before the server has had a chance to process them. ## Verification - TypeScript: zero type errors - Biome lint: clean - Unit tests: 2/2 passing - **E2E soak test: 849/854 passed across 854 runs (99.5% pass rate)** - 0 occurrences of the original flake (empty value on settings page) - 5 residual failures are a separate pre-existing race in `fillParameters` where user input is overwritten during the 500ms debounce window --- .../modules/hooks/useSyncFormParameters.ts | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/site/src/modules/hooks/useSyncFormParameters.ts b/site/src/modules/hooks/useSyncFormParameters.ts index dee1f5aef8..13146b99dd 100644 --- a/site/src/modules/hooks/useSyncFormParameters.ts +++ b/site/src/modules/hooks/useSyncFormParameters.ts @@ -27,16 +27,30 @@ export function useSyncFormParameters({ useEffect(() => { if (!parameters) return; const currentFormValues = formValuesRef.current; - - const newParameterValues = parameters.map((param) => ({ - name: param.name, - value: param.value.valid ? param.value.value : "", - })); - const currentFormValuesMap = new Map( currentFormValues.map((value) => [value.name, value.value]), ); + const newParameterValues = parameters.map((param) => { + // When the server value is not valid (e.g., the initial + // WebSocket response before any user input is sent), + // preserve the current form value. This prevents the sync + // hook from overwriting autofilled values (from the + // previous build) with empty strings before the server + // has had a chance to process them. + if (!param.value.valid) { + const existingValue = currentFormValuesMap.get(param.name); + if (existingValue !== undefined) { + return { name: param.name, value: existingValue }; + } + } + + return { + name: param.name, + value: param.value.valid ? param.value.value : "", + }; + }); + const isChanged = currentFormValues.length !== newParameterValues.length || newParameterValues.some(