fix(ui): name the orphaned keyword rules inside the tier editor before Done

This commit is contained in:
Tin Chi Lo 2026-08-28 09:54:22 -07:00
parent 14a34f45d0
commit b4af020632
5 changed files with 57 additions and 1 deletions

View file

@ -1094,6 +1094,25 @@ describe("ComplexityRouterConfig tier editing", () => {
expect(screen.queryByRole("button", { name: "Edit tiers" })).not.toBeInTheDocument();
});
it("surfaces the caller's orphaned-rule verdict while editing, so Done is not a silent exit", () => {
renderEditor(customValue, { keywordRulesError: "Keyword rule(s) 1 route to a tier this router no longer has" });
expect(
screen.getByText("Keyword rule(s) 1 route to a tier this router no longer has", { exact: false }),
).toBeInTheDocument();
});
it("keeps the orphaned-rule verdict out of the collapsed view, where the submit tooltip owns it", () => {
renderWithProviders(
<ComplexityRouterConfig
{...baseProps}
value={customValue}
onEditingTiersChange={vi.fn()}
keywordRulesError="Keyword rule(s) 1 route to a tier this router no longer has"
/>,
);
expect(screen.queryByText("route to a tier this router no longer has", { exact: false })).not.toBeInTheDocument();
});
it("renders the four built-in tiers before any edit, unchanged", () => {
renderWithProviders(<ComplexityRouterConfig {...baseProps} onEditingTiersChange={vi.fn()} />);
expect(screen.getByRole("button", { name: "Edit tiers" })).toBeInTheDocument();

View file

@ -197,10 +197,11 @@ const TierSetToolbar: React.FC<{
isCustomSet: boolean;
rowCount: number;
rowsError: string | null;
keywordRulesError: string | null | undefined;
onEditingChange: ((editing: boolean) => void) | undefined;
onAdd: () => void;
onRestore: () => void;
}> = ({ editing, isCustomSet, rowCount, rowsError, onEditingChange, onAdd, onRestore }) => (
}> = ({ editing, isCustomSet, rowCount, rowsError, keywordRulesError, onEditingChange, onAdd, onRestore }) => (
<>
<div className="mt-4 flex flex-wrap items-center gap-2">
{editing ? (
@ -234,6 +235,11 @@ const TierSetToolbar: React.FC<{
and an edited set requires the LLM classification method
</span>
)}
{editing && keywordRulesError && (
<span className="block mt-1 text-xs text-destructive">
{keywordRulesError}. Edit the rules under Advanced: Keyword/Semantic Matching, or bring the tier back
</span>
)}
</>
);
@ -419,6 +425,8 @@ interface ComplexityRouterConfigProps {
// rules or semantic matching, so it renders this component without them.
keywordTierRules?: KeywordTierRule[];
onKeywordTierRulesChange?: (rules: KeywordTierRule[]) => void;
/** getKeywordTierRulesError's verdict, owned by the caller: importing it here would be an import cycle. */
keywordRulesError?: string | null;
semanticMatchingEnabled?: boolean;
onSemanticMatchingEnabledChange?: (enabled: boolean) => void;
embeddingModel?: string;
@ -569,6 +577,7 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
onCustomTechnicalKeywordsChange,
keywordTierRules = [],
onKeywordTierRulesChange,
keywordRulesError,
semanticMatchingEnabled = false,
onSemanticMatchingEnabledChange,
embeddingModel,
@ -739,6 +748,7 @@ const ComplexityRouterConfig: React.FC<ComplexityRouterConfigProps> = ({
isCustomSet={Boolean(customTierSet)}
rowCount={tierRows.length}
rowsError={tierRowsError}
keywordRulesError={keywordRulesError}
onEditingChange={onEditingTiersChange}
onAdd={addCustomTier}
onRestore={exitToBuiltInTiers}

View file

@ -283,6 +283,25 @@ describe("AddAutoRouterTab", () => {
expect(screen.queryByText("At least one keyword is required")).not.toBeInTheDocument();
});
it("shows the orphaned-rule reason in the tier editor when a rule's tier is removed", async () => {
const user = userEvent.setup();
vi.mocked(getMissingTiersError).mockReturnValue(null);
renderWithProviders(<Harness />);
await user.type(screen.getByPlaceholderText(/smart_router/i), "orphan-rule-router");
expandDetailedConfiguration();
await user.click(screen.getByText("Advanced: Keyword/Semantic Matching"));
await user.click(screen.getByRole("button", { name: /add keyword rule/i }));
await addKeyword(user, screen.getByText("Keywords 1").closest("div") as HTMLElement, "invoice");
await user.click(screen.getByRole("button", { name: "Edit tiers" }));
await user.click(screen.getByRole("button", { name: "Remove the COMPLEX tier" }));
expect(await screen.findByText(/route to a tier this router no longer has/)).toBeInTheDocument();
expect(screen.getByRole("button", { name: /add auto router/i })).toBeDisabled();
});
it("marks only the offending keyword row, leaving a filled one alone", async () => {
const user = userEvent.setup();
vi.mocked(getMissingTiersError).mockReturnValue(null);

View file

@ -581,6 +581,10 @@ const AddAutoRouterTab: React.FC<AddAutoRouterTabProps> = ({
onCustomTechnicalKeywordsChange={setCustomTechnicalKeywords}
keywordTierRules={keywordTierRules}
onKeywordTierRulesChange={setKeywordTierRules}
keywordRulesError={getKeywordTierRulesError(
keywordTierRules,
activeTierRows(complexityRouterConfig),
)}
semanticMatchingEnabled={semanticMatchingEnabled}
onSemanticMatchingEnabledChange={setSemanticMatchingEnabled}
embeddingModel={embeddingModel}

View file

@ -687,6 +687,10 @@ const EditAutoRouterModal: React.FC<EditAutoRouterModalProps> = ({
onCustomTechnicalKeywordsChange={setCustomTechnicalKeywords}
keywordTierRules={keywordTierRules}
onKeywordTierRulesChange={setKeywordTierRules}
keywordRulesError={getKeywordTierRulesError(
keywordTierRules,
activeTierRows(complexityRouterConfig),
)}
semanticMatchingEnabled={semanticMatchingEnabled}
onSemanticMatchingEnabledChange={setSemanticMatchingEnabled}
embeddingModel={embeddingModel}