This commit is contained in:
devin-ai-integration[bot] 2026-09-23 14:51:35 +00:00 • committed by GitHub
commit 41d914e647
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 186 additions and 15 deletions

View file

@ -0,0 +1,37 @@
import { fireEvent, renderWithProviders, screen } from "../../../tests/test-utils";
import { vi } from "vitest";
import AdaptiveRoutingConfig from "./AdaptiveRoutingConfig";
import { ComplexityRouterConfigValue } from "./ComplexityRouterConfig";
const adaptiveValue: ComplexityRouterConfigValue = {
adaptive: true,
adaptive_eligible: "all",
tier_distance_penalty: 0.2,
};
describe("AdaptiveRoutingConfig", () => {
it("keeps the tier distance penalty empty while it is being edited, then commits the new penalty", () => {
const onChange = vi.fn();
renderWithProviders(<AdaptiveRoutingConfig value={adaptiveValue} onChange={onChange} />);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
expect(input).toHaveValue(null);
expect(onChange).not.toHaveBeenCalled();
fireEvent.change(input, { target: { value: "0.5" } });
expect(onChange).toHaveBeenCalledWith({ ...adaptiveValue, tier_distance_penalty: 0.5 });
});
it("restores the committed tier distance penalty after an empty field loses focus", () => {
renderWithProviders(<AdaptiveRoutingConfig value={adaptiveValue} onChange={vi.fn()} />);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
fireEvent.blur(input);
expect(input).toHaveValue(0.2);
});
});

View file

@ -1,5 +1,5 @@
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { NumberInput } from "@/components/shared/NumberInput";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Slider } from "@/components/ui/slider";
@ -42,8 +42,8 @@ const AdaptiveRoutingConfig: React.FC<AdaptiveRoutingConfigProps> = ({ value, on
onChange({ ...value, adaptive_eligible: eligible });
};
const handleTierDistancePenaltyChange = (penalty: number | null) => {
onChange({ ...value, tier_distance_penalty: penalty ?? DEFAULT_TIER_DISTANCE_PENALTY });
const handleTierDistancePenaltyChange = (penalty: number) => {
onChange({ ...value, tier_distance_penalty: penalty });
};
return (
@ -120,12 +120,9 @@ const AdaptiveRoutingConfig: React.FC<AdaptiveRoutingConfigProps> = ({ value, on
{adaptiveEligible === "all" && (
<div>
<strong className="mb-1 block font-semibold">Tier Distance Penalty</strong>
<Input
type="number"
<NumberInput
value={tierDistancePenalty}
onChange={(event) =>
handleTierDistancePenaltyChange(event.target.value === "" ? null : event.target.valueAsNumber)
}
onValueChange={handleTierDistancePenaltyChange}
min={0}
step={0.1}
className="w-full"

View file

@ -1,4 +1,4 @@
import { renderWithProviders, screen } from "../../../tests/test-utils";
import { fireEvent, renderWithProviders, screen } from "../../../tests/test-utils";
import userEvent from "@testing-library/user-event";
import { vi } from "vitest";
import SemanticKeywordMatching from "./SemanticKeywordMatching";
@ -44,6 +44,23 @@ describe("SemanticKeywordMatching", () => {
expect(screen.getByText("An embedding model is required")).toBeInTheDocument();
});
it("keeps the minimum match score empty while it is being edited, then commits the new score", () => {
const onMatchThresholdChange = vi.fn();
renderWithProviders(
<SemanticKeywordMatching {...baseProps} onMatchThresholdChange={onMatchThresholdChange} matchThreshold={0.5} />,
);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
expect(input).toHaveValue(null);
expect(onMatchThresholdChange).not.toHaveBeenCalled();
fireEvent.change(input, { target: { value: "0.8" } });
expect(onMatchThresholdChange).toHaveBeenCalledWith(0.8);
});
it("hides the validation error once an embedding model is set", () => {
renderWithProviders(
<SemanticKeywordMatching {...baseProps} showValidationErrors={true} embeddingModel="voyage-3-5" />,

View file

@ -1,7 +1,7 @@
import { Info } from "lucide-react";
import { SimpleTooltip } from "@/components/ui/tooltip";
import { SearchSelect } from "@/components/shared/SearchSelect";
import { Input } from "@/components/ui/input";
import { NumberInput } from "@/components/shared/NumberInput";
import { Switch } from "@/components/ui/switch";
import React from "react";
import { ModelGroup } from "@/components/llm_calls/fetch_models";
@ -74,12 +74,9 @@ const SemanticKeywordMatching: React.FC<SemanticKeywordMatchingProps> = ({
</div>
<div>
<span className="mb-1 block text-sm font-medium">Minimum match score</span>
<Input
type="number"
<NumberInput
value={matchThreshold}
onChange={(event) =>
onMatchThresholdChange(event.target.value === "" ? DEFAULT_MATCH_THRESHOLD : event.target.valueAsNumber)
}
onValueChange={onMatchThresholdChange}
min={0}
max={1}
step={0.05}

View file

@ -0,0 +1,87 @@
import { useState } from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import { vi } from "vitest";
import { NumberInput } from "./NumberInput";
const STORED_VALUE = 3;
const StatefulHarness = () => {
const [value, setValue] = useState(STORED_VALUE);
return (
<>
<NumberInput value={value} onValueChange={setValue} min={0} />
<output>{value}</output>
</>
);
};
describe("NumberInput", () => {
it("should report the typed number", () => {
const onValueChange = vi.fn();
render(<NumberInput value={3} onValueChange={onValueChange} />);
fireEvent.change(screen.getByRole("spinbutton"), { target: { value: "7" } });
expect(onValueChange).toHaveBeenCalledWith(7);
});
it("should commit nothing while the field holds no number", () => {
const onValueChange = vi.fn();
render(<NumberInput value={3} onValueChange={onValueChange} />);
fireEvent.change(screen.getByRole("spinbutton"), { target: { value: "" } });
expect(onValueChange).not.toHaveBeenCalled();
});
it("should stay empty once cleared, keeping the stored value untouched", () => {
render(<StatefulHarness />);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
expect(input).toHaveValue(null);
expect(screen.getByRole("status")).toHaveTextContent(String(STORED_VALUE));
});
it("should accept a fresh number typed into the cleared field", () => {
render(<StatefulHarness />);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
fireEvent.change(input, { target: { value: "5" } });
expect(input).toHaveValue(5);
expect(screen.getByRole("status")).toHaveTextContent("5");
});
it("should show the stored value again once the cleared field is blurred", () => {
render(<StatefulHarness />);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
fireEvent.blur(input);
expect(input).toHaveValue(STORED_VALUE);
});
it("should follow the parent value after the draft is committed", () => {
const { rerender } = render(<NumberInput value={3} onValueChange={vi.fn()} />);
const input = screen.getByRole("spinbutton");
fireEvent.change(input, { target: { value: "" } });
fireEvent.blur(input);
rerender(<NumberInput value={9} onValueChange={vi.fn()} />);
expect(input).toHaveValue(9);
});
it("should call a caller-supplied blur handler", () => {
const onBlur = vi.fn();
render(<NumberInput value={3} onValueChange={vi.fn()} onBlur={onBlur} />);
fireEvent.blur(screen.getByRole("spinbutton"));
expect(onBlur).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,36 @@
import * as React from "react";
import { Input } from "@/components/ui/input";
type NumberInputProps = Omit<React.ComponentProps<"input">, "type" | "value" | "onChange"> & {
value: number;
onValueChange: (value: number) => void;
};
const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(
({ value, onValueChange, onBlur, ...props }, ref) => {
const [draft, setDraft] = React.useState<string | null>(null);
return (
<Input
{...props}
ref={ref}
type="number"
value={draft ?? String(value)}
onChange={(event) => {
setDraft(event.target.value);
if (!Number.isNaN(event.target.valueAsNumber)) {
onValueChange(event.target.valueAsNumber);
}
}}
onBlur={(event) => {
setDraft(null);
onBlur?.(event);
}}
/>
);
},
);
NumberInput.displayName = "NumberInput";
export { NumberInput };