PR 1218, round three: Shut Down VM on Migration Failure

harvester/harvester-ui-extension#1218 by wheatdog, head d73d8aa (unchanged since round two), reviewed 2026-11-10 with Cody's process (my-pr-full-review at 5f9619b) and ours, then merged. Nothing is posted.

Preview: harvesterpr1218r built from d73d8aa, backend the real Harvester .105 on v1.10.0-rc1. Baseline harvestermainr rebuilt from main ee91524 (the upstream tip), same backend. Every save is held in the browser; nothing was written to .105.

Comment now, approve after the rebasegrade A-

The control does what the issue asks, on a real v1.10.0-rc1 server: Disabled by default, both choices save as real booleans with the rest of the setting kept, Use the default value resets it, and older clusters hide it and keep a value set with kubectl. Both passes found the same one real problem; ours adds an optional test nit. Nothing blocks.

What moved since round two

See the flow

Three annotated clips on the same real v1.10.0-rc1 server, light theme, same click path: Advanced > Settings > upgrade-config > ⋮ > Edit Setting. The caption band says what each step shows.

Before (main, 13.5 s): Restore VM, then Node Upgrade Option. No way to choose what happens when a live migration fails.
With this PR (42 s): the new control, Disabled by default; the docs link at the top; Enabled; Save sends a real boolean; the card turns Modified with the field in the JSON; reopening keeps Enabled; Use the default value goes back to Disabled; Cancel sends nothing.
With this PR on a v1.9.1 cluster (20 s): version lowered in the browser, field set with kubectl as the v1.9.1 backport says. No control; changing Restore VM and saving keeps "shutdownVMOnMigrationFailure": true.

The same flow as stills:

1. Settings list
Settings list
Advanced > Settings, the upgrade-config card. Its ⋮ menu has Edit Setting.
2. main: the form today
main: the form today
Restore VM, then Node Upgrade Option.
3. With the PR: the new control
With the PR: the new control
Shut Down VM on Migration Failure, Disabled by default, between the two.
4. Enabled selected
Enabled selected
Nothing else on the form changes.
5. After Save
After Save
The card is marked Modified and the stored JSON carries the field (save held in the browser).
6. On a v1.9.1 cluster
On a v1.9.1 cluster
No control; a value set with kubectl is kept on save.
7. Light theme
Light theme
Both options enabled.
8. Dark theme
Dark theme
Same state.
9. Keyboard focus
Keyboard focus
Tab lands on the group with a visible ring; arrow keys switch the value.
10. What the docs link will show
What the docs link will show
The field's entry on the docs#1170 preview, the page the form's description links to.
11. What a screen reader gets
What a screen reader gets
Left: the PR's form, two unnamed groups. Right: the same component given its label elsewhere in the extension.
The two passes side by side
Cody's process (Pass A)Ours (Pass B)
VerdictHis process ends at pending comments; the verdict is left to the reviewer.Approve, A-.
Findings kept1: the unnamed radio group.2: the unnamed radio group, and the test gap (nit). Plus one question, dropped in the merge.
Checked by bothDefault Disabled; Enabled and Disabled save booleans; other fields kept; Use the default value; hidden on v1.9.1 with a kubectl value kept; keyboard; the conflict.Same.
Only this passBefore/after demo pair plus the v1.9.1 clip; an 8-criterion acceptance table sourced from the thread; found that the page's docs link leads to the section docs#1170 adds; dark theme; his a11y probe in both themes; axe.Seven mutants (the test gap); view mode is read-only; a hand-set string "true"; the aria-label fix proven with a unit test; lint; console errors on both builds; a 16-cell coverage grid.
Missed or wrongThe test gap: test coverage is not one of his default passes, by design.Q1 rested on a misreading of QA's failure on 12172 (an edit to default, not a mid-upgrade change).
Output3 clips (13.5 s, 42 s, 20 s), 11 stills, criteria and verification files, 1 comment taken through his 7 comment phases.review, coverage, checked and draft files; 1 clip (32 s); 9 images.
CostAbout 28 minutes in this session, after setup; it started warm with two earlier rounds of context.17 minutes, 76 tool calls, about 234k tokens; a fresh agent that read the rules and the code from zero.
Findings after the merge
1

The new radio group has no accessible name

Suggestion
both

What happens

The bold <label> above the group isn't tied to it, and the RadioGroup gets no label or aria-label. Chromium's accessibility tree on the PR build shows two radiogroups in a row with no name, each holding just "Enabled" and "Disabled": Restore VM's (already on main) and this new one.

Why it matters

Someone on a screen reader can't tell the two apart, and this one decides whether VMs get shut down during an upgrade.

Simplest fix

Pass the same label text as aria-label: RadioGroup forwards it to the role="radiogroup" element, and nothing changes visually. Pass B proved it with a unit test ([null,null] at the head, the label text with the attribute). The extension's rancher-cluster form already names its group this way, through label.

How each pass got there

Cody's a11y pass: his probe flagged only shared-component noise; the missing name came from reading the AX tree, and his preexisting rule counts a pattern copied into new code as introduced. Ours: the a11y checklist item, checked live with ariaSnapshot.
annotated form
Pass B: what assistive tech gets for each group, the new one outlined.
accessibility tree
Pass A: the AX tree of the PR's form, next to the same component named elsewhere.
2

The save test never goes through the radio group

Nit, optional
ours

What happens

"should save the selected value" sets parseDefaultValue and calls update() itself. Of seven deliberate breakages, two still pass all five tests: dropping @update:value on the new group (the choice would never reach the saved value) and binding it to restoreVM by a copy slip.

Simplest fix

Emit update:value from the group's stub and check the saved JSON: it passes on the real code and catches both (Pass B ran it).

Why only ours

Cody's review never posts nits and doesn't run test coverage by default. Marcelo decides; it's marked optional.
mutation table
Pass B's mutation run against the PR's spec.
3

Dropped: "should the form say the value is read once?"

Pass B, Q1 (optional question)
Dropped
oursdropped

Why it was dropped

The behavior is real, but the support was wrong: Pass B said QA "ran into exactly this" on 12172. QA's failed run edited the field into default, which the upgrade reset; the author's reply corrects that, and the re-run passed. Beyond that, docs#1170 says the value is read once and changes are ignored mid-upgrade, and the form already links to that section. Round two asked for a help line on the same grounds and is dropped for the same reason.
Pass A: Cody's process, pass by pass
PassResult
DemosBefore (main) and after (PR) on the same cluster and theme, plus the v1.9.1 case. The issue has no bug to reproduce; its "before" is the missing control.
Base reviewNothing beyond the passes. The method-not-computed comment is accurate (normalizeValue runs in data()).
1. Acceptance criteria8 criteria from the thread: 3 stated, 3 agreed in discussion, 2 implied. Backend, restart and webhook marked not in this PR.
2. Acceptance verify8 of 8 met, 11 live checks (the first three runs stopped on errors in my script, not the PR).
3. SimplifyNothing.
4. Side effectsNothing: the key, flag and method are read only by this form; the conflict is Mergify's to chase.
5. Component reuseNo new component.
6. Design fidelityNo design linked (the author's "latest design" isn't linked anywhere).
7. AccessibilityHis probe, both themes: only shared-component hits that Restore VM on main has too (its focus-outline hit is a false positive, a ring is drawn). AX tree: the new group has no name, filed. axe 4.10.2: 0 violations.
Comment phasesVerify (holds), refinement, final pass, remove suggestions (the fix pointer came out), condense (194 characters), preexisting (introduced: a copied pattern in new code). 1 comment survives.

Acceptance criteria (from the issue thread) and what the PR build did

CriterionAsked for inVerdict
AC1Control shown on v1.10.0+backend PR: "the form needs a control"Met
AC2Disabled when the field is missingPR test step 3; backend default falseMet
AC3Toggles both ways, saves a booleanPR test step 4; backend test plan step 5Met
AC4Other fields kept on savedocs#1170 and the author on 12172: a value replaces default entirelyMet
AC5Hidden before v1.10.0Author on 12172; docs#1170Met
AC6A kubectl value survives a save on older clustersAuthor on 12172 (implied)Met
AC7Use the default value resets to DisabledBackend default (implied)Met
AC8Light and darkImplied for a visual changeMet

Open question in the criteria file: whether the form should explain the option is not settled anywhere in the thread; the docs PR and the existing docs link carry it.

Pass B: our process

Approve, grade A-

Fresh agent, not shown Pass A. Our checklist, evidence kind on every finding, the coverage grid, and Marcelo's review rules. 2 suggestions, 1 optional question (dropped in the merge).

main and PR side by side
main and the PR on the same cluster.
Pass B's walkthrough (32 s): Enabled, Save with the saved value shown, then v1.9.1 with the control hidden.

Coverage grid

Filecorrectnesstestsa11yi18nconventionserrorsperf
upgrade-config.vueclearedS2S1clearedclearedclearedcleared
upgrade-config.test.jsclearedS2n/an/aclearedn/an/a
feature-flags.jsclearedclearedn/an/aclearedn/an/a
en-us.yamlclearedn/an/aclearedclearedn/an/a

Also seen, out of scope (on main)

view mode
View mode: the new group is disabled, Restore VM's is not (that one is on main).
Round two's findings, now
Round twoNow
Suggestion: nothing explains what each choice does; keep one help line.Dropped. docs#1170 (approved) explains all of it, and the form's description already links to that section.
Nit: the save test bypasses the radio.Still open, re-measured by Pass B with seven mutants; kept as an optional nit.
Left out: the unnamed radio group, since it copies Restore VM.Now in. Both passes flagged it on their own, and Cody's rule counts a pattern copied into new code as introduced. It's the second identical unnamed group in a row.
Outside note: backend PR 12125's description still lists "set the field to true" as an unblock.Still stale there, but docs#1170 now says plainly that changing it mid-upgrade doesn't unblock.
What I'd post

Two inline comments and a short body, in your shape. No preview links. GitHub copies of the clips have the URL band cropped off.

Review body

I tried this on a real v1.10.0-rc1 cluster next to main. The control is Disabled by default, Enabled and Disabled both save as booleans with the rest of the setting kept, Use the default value resets it, and on v1.9.1 the control is hidden and a value set with kubectl survives a save. One accessibility point and an optional test nit inline.

Before this change: [before-main.mp4]

With this change: [after-pr.mp4]

Inline on pkg/harvester/components/settings/upgrade-config.vue line 195

Screen readers get no name for this group. The bold label above it isn't tied to it, so it's announced as just "Enabled / Disabled", the same as the Restore VM group right above it, and the two can't be told apart. Passing the label text to this RadioGroup as its `aria-label` would name it without changing how it looks.
attachment
Attach this image.

Inline on pkg/harvester/components/settings/__tests__/upgrade-config.test.js line 46

Nit, optional: this test sets `parseDefaultValue` and calls `update()` itself, so it never goes through the radio group. Removing `@update:value` from the new group, or binding it to `restoreVM` by mistake, still passes all five tests. Emitting `update:value` from the group's stub would catch both.
attachment
Attach this image.

Submit as Comment, then approve once the conflict is fixed: the push that fixes it would dismiss an approval given now. The second comment is optional. Cody's process would post only the first one, without the fix clause.

What was and wasn't checked