feat(FN-2597): merge fusion/fn-2597 (auto-resolved)

- feat(FN-2597): complete Step 4 — add docs update and changeset
- test(FN-2597): complete Step 2 — align dashboard tests with reviewer labels
- feat(FN-2597): complete Step 1 — rename validator UI labels to reviewer
This commit is contained in:
Fusion
2026-04-26 12:25:10 -07:00
committed by gsxdsm
parent 80514718ed
commit c1b012129f
17 changed files with 51 additions and 46 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Rename "Validator" to "Reviewer" across all dashboard UI labels and descriptions.

View File

@@ -112,7 +112,7 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
| `validatorModelId` | `string` | `undefined` | Model ID for plan/code reviewers. | | `validatorModelId` | `string` | `undefined` | Model ID for plan/code reviewers. |
| `validatorFallbackProvider` | `string` | `undefined` | Fallback provider for reviewers. | | `validatorFallbackProvider` | `string` | `undefined` | Fallback provider for reviewers. |
| `validatorFallbackModelId` | `string` | `undefined` | Fallback model ID for reviewers. | | `validatorFallbackModelId` | `string` | `undefined` | Fallback model ID for reviewers. |
| `modelPresets` | `ModelPreset[]` | `[]` | Reusable executor/validator model presets. | | `modelPresets` | `ModelPreset[]` | `[]` | Reusable executor/reviewer model presets. |
| `autoSelectModelPreset` | `boolean` | `false` | Auto-select presets by task size. | | `autoSelectModelPreset` | `boolean` | `false` | Auto-select presets by task size. |
| `defaultPresetBySize` | `{ S?: string; M?: string; L?: string }` | `{}` | Mapping for `S`/`M`/`L` → preset ID. | | `defaultPresetBySize` | `{ S?: string; M?: string; L?: string }` | `{}` | Mapping for `S`/`M`/`L` → preset ID. |
| `autoResolveConflicts` | `boolean` | `true` | Enable automatic merge conflict resolution. | | `autoResolveConflicts` | `boolean` | `true` | Enable automatic merge conflict resolution. |

View File

@@ -200,7 +200,7 @@ export function AgentLogViewer({
} }
if (hasValidatorOverride) { if (hasValidatorOverride) {
providers.push({ providers.push({
role: "Validator", role: "Reviewer",
provider: validatorModel!.provider!, provider: validatorModel!.provider!,
modelId: validatorModel!.modelId, modelId: validatorModel!.modelId,
}); });
@@ -307,7 +307,7 @@ export function AgentLogViewer({
)} )}
</div> </div>
<div className="agent-log-model-group"> <div className="agent-log-model-group">
<span className="agent-log-model-label">Validator:</span> <span className="agent-log-model-label">Reviewer:</span>
{hasValidatorOverride ? ( {hasValidatorOverride ? (
<span className="agent-log-model-value"> <span className="agent-log-model-value">
<ProviderIcon provider={validatorModel.provider!} size="sm" /> <ProviderIcon provider={validatorModel.provider!} size="sm" />

View File

@@ -701,7 +701,7 @@ export function ListView({
models={availableModels} models={availableModels}
value={validatorModel} value={validatorModel}
onChange={setValidatorModel} onChange={setValidatorModel}
label="Validator Model" label="Reviewer Model"
noChangeValue="__no_change__" noChangeValue="__no_change__"
noChangeLabel="No change" noChangeLabel="No change"
favoriteProviders={favoriteProviders} favoriteProviders={favoriteProviders}

View File

@@ -2395,7 +2395,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
</span> </span>
<span className="mission-validation-round__feature">{round.featureTitle}</span> <span className="mission-validation-round__feature">{round.featureTitle}</span>
<span className="mission-validation-round__attempts"> <span className="mission-validation-round__attempts">
impl #{round.implementationAttempt} · validator #{round.validatorAttempt} impl #{round.implementationAttempt} · reviewer #{round.validatorAttempt}
</span> </span>
</div> </div>

View File

@@ -237,7 +237,7 @@ export function ModelSelectionModal({
<div className="task-detail-section"> <div className="task-detail-section">
<div className="inline-create-model-row"> <div className="inline-create-model-row">
<label htmlFor="model-selection-validator" className="inline-create-model-label"> <label htmlFor="model-selection-validator" className="inline-create-model-label">
Validator Model Reviewer Model
</label> </label>
<span <span
className={`model-badge ${hasValidatorOverride ? "model-badge-custom" : "model-badge-default"}`} className={`model-badge ${hasValidatorOverride ? "model-badge-custom" : "model-badge-default"}`}
@@ -247,11 +247,11 @@ export function ModelSelectionModal({
</span> </span>
<CustomModelDropdown <CustomModelDropdown
id="model-selection-validator" id="model-selection-validator"
label="Validator Model" label="Reviewer Model"
value={validatorValue} value={validatorValue}
onChange={handleValidatorChange} onChange={handleValidatorChange}
models={models} models={models}
placeholder="Select validator model…" placeholder="Select reviewer model…"
favoriteProviders={favoriteProviders} favoriteProviders={favoriteProviders}
onToggleFavorite={onToggleFavorite} onToggleFavorite={onToggleFavorite}
favoriteModels={favoriteModels} favoriteModels={favoriteModels}

View File

@@ -115,7 +115,7 @@ function selectionsEqual(a: ModelSelection, b: ModelSelection): boolean {
function getSuccessToastMessage(target: "executor" | "validator" | "planning", selection: ModelSelection): string { function getSuccessToastMessage(target: "executor" | "validator" | "planning", selection: ModelSelection): string {
const labels: Record<string, string> = { const labels: Record<string, string> = {
executor: "Executor", executor: "Executor",
validator: "Validator", validator: "Reviewer",
planning: "Planning", planning: "Planning",
}; };
const label = labels[target] || target; const label = labels[target] || target;
@@ -480,7 +480,7 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="validatorModel">Validator Model</label> <label htmlFor="validatorModel">Reviewer Model</label>
<div className="model-selector-current"> <div className="model-selector-current">
{validatorUsingDefault ? ( {validatorUsingDefault ? (
<span className="model-badge model-badge-default"> <span className="model-badge model-badge-default">
@@ -495,12 +495,12 @@ export function ModelSelectorTab({ task, addToast, onTaskUpdated, settings }: Mo
</div> </div>
<CustomModelDropdown <CustomModelDropdown
id="validatorModel" id="validatorModel"
label="Validator Model" label="Reviewer Model"
value={validatorValue} value={validatorValue}
onChange={handleValidatorChange} onChange={handleValidatorChange}
models={availableModels} models={availableModels}
disabled={isSaving} disabled={isSaving}
placeholder="Select validator model…" placeholder="Select reviewer model…"
favoriteProviders={favoriteProviders} favoriteProviders={favoriteProviders}
onToggleFavorite={handleToggleFavorite} onToggleFavorite={handleToggleFavorite}
favoriteModels={favoriteModels} favoriteModels={favoriteModels}

View File

@@ -1483,7 +1483,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
}} }}
> >
{activeModelSubmenu === null ? ( {activeModelSubmenu === null ? (
// Top-level menu with Plan/Executor/Validator choices // Top-level menu with Plan/Executor/Reviewer choices
<div className="model-menu-items"> <div className="model-menu-items">
<button <button
type="button" type="button"
@@ -1527,7 +1527,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
> >
<span className="model-menu-item-label"> <span className="model-menu-item-label">
<Brain size={12} style={{ verticalAlign: "middle", marginRight: 6 }} /> <Brain size={12} style={{ verticalAlign: "middle", marginRight: 6 }} />
Validator Reviewer
</span> </span>
<span className="model-menu-item-value"> <span className="model-menu-item-value">
{hasValidatorOverride {hasValidatorOverride
@@ -1552,7 +1552,7 @@ export function QuickEntryBox({ onCreate, addToast, tasks = [], availableModels,
<div className="model-submenu-header"> <div className="model-submenu-header">
{activeModelSubmenu === "plan" && "Plan Model"} {activeModelSubmenu === "plan" && "Plan Model"}
{activeModelSubmenu === "executor" && "Executor Model"} {activeModelSubmenu === "executor" && "Executor Model"}
{activeModelSubmenu === "validator" && "Validator Model"} {activeModelSubmenu === "validator" && "Reviewer Model"}
</div> </div>
<CustomModelDropdown <CustomModelDropdown
models={loadedModels} models={loadedModels}

View File

@@ -887,13 +887,13 @@ export function SettingsModal({
}, },
{ {
laneId: "validator", laneId: "validator",
label: "Validator Model", label: "Reviewer Model",
globalProviderKey: "validatorGlobalProvider", globalProviderKey: "validatorGlobalProvider",
globalModelKey: "validatorGlobalModelId", globalModelKey: "validatorGlobalModelId",
projectProviderKey: "validatorProvider", projectProviderKey: "validatorProvider",
projectModelKey: "validatorModelId", projectModelKey: "validatorModelId",
helperText: "AI model used for code and specification review.", helperText: "AI model used for code and specification review.",
fallbackOrder: "Project override → Global validator lane → Global default lane → Automatic resolution", fallbackOrder: "Project override → Global reviewer lane → Global default lane → Automatic resolution",
}, },
{ {
laneId: "summarization", laneId: "summarization",
@@ -1701,10 +1701,10 @@ export function SettingsModal({
<small>Used if the planning model fails due to rate limits or provider overload. Defaults to the global fallback model.</small> <small>Used if the planning model fails due to rate limits or provider overload. Defaults to the global fallback model.</small>
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="validatorFallbackModel">Validator Fallback Model</label> <label htmlFor="validatorFallbackModel">Reviewer Fallback Model</label>
<CustomModelDropdown <CustomModelDropdown
id="validatorFallbackModel" id="validatorFallbackModel"
label="Validator Fallback Model" label="Reviewer Fallback Model"
models={availableModels} models={availableModels}
value={form.validatorFallbackProvider && form.validatorFallbackModelId ? `${form.validatorFallbackProvider}/${form.validatorFallbackModelId}` : ""} value={form.validatorFallbackProvider && form.validatorFallbackModelId ? `${form.validatorFallbackProvider}/${form.validatorFallbackModelId}` : ""}
onChange={(val) => { onChange={(val) => {
@@ -1725,7 +1725,7 @@ export function SettingsModal({
favoriteModels={favoriteModels} favoriteModels={favoriteModels}
onToggleModelFavorite={handleToggleModelFavorite} onToggleModelFavorite={handleToggleModelFavorite}
/> />
<small>Used if the validator model fails due to rate limits or provider overload. Defaults to the global fallback model.</small> <small>Used if the reviewer model fails due to rate limits or provider overload. Defaults to the global fallback model.</small>
</div> </div>
</> </>
)} )}
@@ -1849,10 +1849,10 @@ export function SettingsModal({
/> />
</div> </div>
<div className="form-group"> <div className="form-group">
<label htmlFor="preset-validator-model">Validator model</label> <label htmlFor="preset-validator-model">Reviewer model</label>
<CustomModelDropdown <CustomModelDropdown
id="preset-validator-model" id="preset-validator-model"
label="Preset validator model" label="Preset reviewer model"
models={availableModels} models={availableModels}
value={presetDraft.validatorProvider && presetDraft.validatorModelId ? `${presetDraft.validatorProvider}/${presetDraft.validatorModelId}` : ""} value={presetDraft.validatorProvider && presetDraft.validatorModelId ? `${presetDraft.validatorProvider}/${presetDraft.validatorModelId}` : ""}
onChange={(val) => { onChange={(val) => {

View File

@@ -977,10 +977,10 @@ export function TaskForm({
/> />
</div> </div>
<div className="model-select-row"> <div className="model-select-row">
<label htmlFor="validator-model" className="model-select-label">Validator</label> <label htmlFor="validator-model" className="model-select-label">Reviewer</label>
<CustomModelDropdown <CustomModelDropdown
id="validator-model" id="validator-model"
label="Validator Model" label="Reviewer Model"
value={validatorModel} value={validatorModel}
onChange={(value) => { onChange={(value) => {
onPresetModeChange("custom"); onPresetModeChange("custom");

View File

@@ -335,10 +335,10 @@ describe("AgentLogViewer", () => {
const header = container.querySelector("[data-testid='agent-log-model-header']"); const header = container.querySelector("[data-testid='agent-log-model-header']");
expect(header).toBeTruthy(); expect(header).toBeTruthy();
expect(container.querySelector('[data-provider="openai"]')).toBeTruthy(); expect(container.querySelector('[data-provider="openai"]')).toBeTruthy();
expect(header!.textContent).not.toContain("Validator:"); expect(header!.textContent).not.toContain("Reviewer:");
fireEvent.click(screen.getByTestId("agent-log-model-expand")); fireEvent.click(screen.getByTestId("agent-log-model-expand"));
expect(header!.textContent).toContain("Validator:"); expect(header!.textContent).toContain("Reviewer:");
expect(header!.textContent).toContain("openai/gpt-4o"); expect(header!.textContent).toContain("openai/gpt-4o");
}); });

View File

@@ -74,9 +74,9 @@ vi.mock("../ModelSelectionModal", () => ({
<div data-testid="modal-props-has-preset-change">{onPresetChange ? "yes" : "no"}</div> <div data-testid="modal-props-has-preset-change">{onPresetChange ? "yes" : "no"}</div>
<button data-testid="modal-close" onClick={onClose}>Close</button> <button data-testid="modal-close" onClick={onClose}>Close</button>
<button data-testid="modal-select-executor" onClick={() => onExecutorChange("anthropic/claude-sonnet-4-5")}>Select Executor</button> <button data-testid="modal-select-executor" onClick={() => onExecutorChange("anthropic/claude-sonnet-4-5")}>Select Executor</button>
<button data-testid="modal-select-validator" onClick={() => onValidatorChange("openai/gpt-4o")}>Select Validator</button> <button data-testid="modal-select-validator" onClick={() => onValidatorChange("openai/gpt-4o")}>Select Reviewer</button>
<button data-testid="modal-clear-executor" onClick={() => onExecutorChange("")}>Clear Executor</button> <button data-testid="modal-clear-executor" onClick={() => onExecutorChange("")}>Clear Executor</button>
<button data-testid="modal-clear-validator" onClick={() => onValidatorChange("")}>Clear Validator</button> <button data-testid="modal-clear-validator" onClick={() => onValidatorChange("")}>Clear Reviewer</button>
<button data-testid="modal-retry" onClick={onRetry}>Retry</button> <button data-testid="modal-retry" onClick={onRetry}>Retry</button>
</div> </div>
); );

View File

@@ -2220,7 +2220,7 @@ describe("ListView - Bulk Selection", () => {
// Verify the menu has the three options // Verify the menu has the three options
expect(menu.textContent).toContain("Plan"); expect(menu.textContent).toContain("Plan");
expect(menu.textContent).toContain("Executor"); expect(menu.textContent).toContain("Executor");
expect(menu.textContent).toContain("Validator"); expect(menu.textContent).toContain("Reviewer");
}); });
describe("ListView Mobile Cards", () => { describe("ListView Mobile Cards", () => {

View File

@@ -273,7 +273,7 @@ describe("ModelSelectionModal", () => {
"anthropic/claude-sonnet-4-5", "anthropic/claude-sonnet-4-5",
); );
expect(screen.getByTestId("dropdown-label-model-selection-validator").textContent).toBe( expect(screen.getByTestId("dropdown-label-model-selection-validator").textContent).toBe(
"Validator Model", "Reviewer Model",
); );
}); });

View File

@@ -121,7 +121,7 @@ describe("ModelSelectorTab", () => {
await waitForSelectors(); await waitForSelectors();
expect(screen.getByLabelText("Validator Model")).toBeInTheDocument(); expect(screen.getByLabelText("Reviewer Model")).toBeInTheDocument();
expect(screen.getByLabelText("Planning Model")).toBeInTheDocument(); expect(screen.getByLabelText("Planning Model")).toBeInTheDocument();
expect(screen.queryByText("Save")).not.toBeInTheDocument(); expect(screen.queryByText("Save")).not.toBeInTheDocument();
expect(screen.queryByText("Reset")).not.toBeInTheDocument(); expect(screen.queryByText("Reset")).not.toBeInTheDocument();
@@ -155,7 +155,7 @@ describe("ModelSelectorTab", () => {
const executorSection = getSection("Executor Model"); const executorSection = getSection("Executor Model");
expect(within(executorSection!).getByText("Using default")).toBeInTheDocument(); expect(within(executorSection!).getByText("Using default")).toBeInTheDocument();
const validatorSection = getSection("Validator Model"); const validatorSection = getSection("Reviewer Model");
expect(within(validatorSection!).getByText("Using default")).toBeInTheDocument(); expect(within(validatorSection!).getByText("Using default")).toBeInTheDocument();
const planningSection = getSection("Planning Model"); const planningSection = getSection("Planning Model");
@@ -206,7 +206,7 @@ describe("ModelSelectorTab", () => {
await waitForSelectors(); await waitForSelectors();
const validatorSection = getSection("Validator Model"); const validatorSection = getSection("Reviewer Model");
expect(within(validatorSection!).getByText("Using default (openai/gpt-4o)")).toBeInTheDocument(); expect(within(validatorSection!).getByText("Using default (openai/gpt-4o)")).toBeInTheDocument();
}); });
@@ -365,7 +365,7 @@ describe("ModelSelectorTab", () => {
})); }));
}); });
await selectOption("Validator Model", "GPT-4o"); await selectOption("Reviewer Model", "GPT-4o");
await waitFor(() => { await waitFor(() => {
expect(mockUpdateTask).toHaveBeenNthCalledWith(2, "FN-001", expectedModelCall({ expect(mockUpdateTask).toHaveBeenNthCalledWith(2, "FN-001", expectedModelCall({
@@ -441,7 +441,7 @@ describe("ModelSelectorTab", () => {
render(<ModelSelectorTab task={taskWithExecutor} addToast={mockAddToast} />); render(<ModelSelectorTab task={taskWithExecutor} addToast={mockAddToast} />);
await waitForSelectors(); await waitForSelectors();
await selectOption("Validator Model", "GPT-4o"); await selectOption("Reviewer Model", "GPT-4o");
await waitFor(() => { await waitFor(() => {
expect(mockUpdateTask).toHaveBeenCalledWith("FN-001", expectedModelCall({ expect(mockUpdateTask).toHaveBeenCalledWith("FN-001", expectedModelCall({
@@ -469,7 +469,7 @@ describe("ModelSelectorTab", () => {
await waitForSelectors(); await waitForSelectors();
await user.click(getSelector("Validator Model")); await user.click(getSelector("Reviewer Model"));
await user.click(getUseDefaultOption()); await user.click(getUseDefaultOption());
await waitFor(() => { await waitFor(() => {
@@ -517,7 +517,7 @@ describe("ModelSelectorTab", () => {
await waitFor(() => { await waitFor(() => {
expect(getSelector("Executor Model")).toBeDisabled(); expect(getSelector("Executor Model")).toBeDisabled();
expect(getSelector("Validator Model")).toBeDisabled(); expect(getSelector("Reviewer Model")).toBeDisabled();
expect(getSelector("Planning Model")).toBeDisabled(); expect(getSelector("Planning Model")).toBeDisabled();
}); });
@@ -529,7 +529,7 @@ describe("ModelSelectorTab", () => {
await waitFor(() => { await waitFor(() => {
expect(getSelector("Executor Model")).not.toBeDisabled(); expect(getSelector("Executor Model")).not.toBeDisabled();
expect(getSelector("Validator Model")).not.toBeDisabled(); expect(getSelector("Reviewer Model")).not.toBeDisabled();
expect(getSelector("Planning Model")).not.toBeDisabled(); expect(getSelector("Planning Model")).not.toBeDisabled();
}); });
}); });
@@ -619,10 +619,10 @@ describe("ModelSelectorTab", () => {
render(<ModelSelectorTab task={FAKE_TASK} addToast={mockAddToast} />); render(<ModelSelectorTab task={FAKE_TASK} addToast={mockAddToast} />);
await waitForSelectors(); await waitForSelectors();
await selectOption("Validator Model", "GPT-4o"); await selectOption("Reviewer Model", "GPT-4o");
await waitFor(() => { await waitFor(() => {
expect(mockAddToast).toHaveBeenCalledWith("Validator model set to openai/gpt-4o", "success"); expect(mockAddToast).toHaveBeenCalledWith("Reviewer model set to openai/gpt-4o", "success");
}); });
}); });

View File

@@ -797,7 +797,7 @@ describe("QuickEntryBox", () => {
expect(screen.getByTestId("model-nested-menu")).toBeTruthy(); expect(screen.getByTestId("model-nested-menu")).toBeTruthy();
}); });
it("shows Plan, Executor, and Validator options in model menu", () => { it("shows Plan, Executor, and Reviewer options in model menu", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
@@ -837,7 +837,7 @@ describe("QuickEntryBox", () => {
expect(screen.getByTestId("custom-model-dropdown-plan model")).toBeTruthy(); expect(screen.getByTestId("custom-model-dropdown-plan model")).toBeTruthy();
}); });
it("clicking Validator opens submenu with CustomModelDropdown", () => { it("clicking Reviewer opens submenu with CustomModelDropdown", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
@@ -913,7 +913,7 @@ describe("QuickEntryBox", () => {
expect(planItem.classList.contains("model-menu-item--active")).toBe(true); expect(planItem.classList.contains("model-menu-item--active")).toBe(true);
}); });
it("selecting Validator model updates the Validator menu item value", () => { it("selecting Reviewer model updates the Reviewer menu item value", () => {
renderQuickEntryBox({}); renderQuickEntryBox({});
expandQuickEntry(); expandQuickEntry();
const textarea = screen.getByTestId("quick-entry-input"); const textarea = screen.getByTestId("quick-entry-input");
@@ -928,7 +928,7 @@ describe("QuickEntryBox", () => {
// Go back to top-level menu // Go back to top-level menu
fireEvent.click(screen.getByTestId("model-submenu-back")); fireEvent.click(screen.getByTestId("model-submenu-back"));
// Validator menu item should show the selected model // Reviewer menu item should show the selected model
const validatorItem = screen.getByTestId("model-menu-validator"); const validatorItem = screen.getByTestId("model-menu-validator");
expect(validatorItem.textContent).toContain("anthropic/claude-sonnet-4-5"); expect(validatorItem.textContent).toContain("anthropic/claude-sonnet-4-5");
expect(validatorItem.classList.contains("model-menu-item--active")).toBe(true); expect(validatorItem.classList.contains("model-menu-item--active")).toBe(true);

View File

@@ -4891,7 +4891,7 @@ describe("TaskDetailModal", () => {
); );
}); });
await user.click(screen.getByLabelText("Validator Model")); await user.click(screen.getByLabelText("Reviewer Model"));
await user.click(screen.getByText("GPT-4o")); await user.click(screen.getByText("GPT-4o"));
await waitFor(() => { await waitFor(() => {
@@ -4909,7 +4909,7 @@ describe("TaskDetailModal", () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText("Executor Model")).toHaveTextContent("Claude Sonnet 4.5"); expect(screen.getByLabelText("Executor Model")).toHaveTextContent("Claude Sonnet 4.5");
expect(screen.getByLabelText("Validator Model")).toHaveTextContent("GPT-4o"); expect(screen.getByLabelText("Reviewer Model")).toHaveTextContent("GPT-4o");
}); });
}); });