Skip to content

Fixes 30715: block saving a persona AI context rule with an unfinished condition - #30719

Open
pmbrull wants to merge 1 commit into
pmbrull/collate-backend-60-sentryfrom
pmbrull/persona-context-rule-save-validation
Open

Fixes 30715: block saving a persona AI context rule with an unfinished condition#30719
pmbrull wants to merge 1 commit into
pmbrull/collate-backend-60-sentryfrom
pmbrull/persona-context-rule-save-validation

Conversation

@pmbrull

@pmbrull pmbrull commented Jul 30, 2026

Copy link
Copy Markdown
Member

Describe your changes:

Relates to #30715. Stacked on #30716 — base is that PR's branch, so this diff shows only the follow-up. Retarget to main once #30716 merges.

#30716 stopped a half-built condition from crashing the search: {"term":{}} is rejected outright by both OpenSearch and Elasticsearch, so the emitted queryFilter now drops it. That fixes the 500, but leaves a worse-shaped UX bug behind it — dropping the condition silently on save. A rule the user built as "tier is ___" persists as no filter at all, so it matches everything and pulls the entity cap instead of the handful of assets they picked. Nothing tells them.

So the editor now refuses the save while a condition is unfinished, and says so inline, rather than quietly widening the rule. Either the condition is in, or it is out.

QueryBuilderWidgetV1 reports tree validity through a new optional onValidityChange prop. It uses QbUtils.isValidTree with the config react-awesome-query-builder has already extended in its own onChange — the same validity RAQB uses internally, so operators with no value (is_null and friends) are not false-flagged. Only the persona rule editor opts in; Explore and the other query-builder consumers are untouched.

The backend prune from #30716 stays regardless. It is the safety net, not the UX: rules with {"term":{}} are already persisted in persona.contextDefinition today and a UI fix does not heal them, and POST /aiContext/rules is a public API that must not 500 on bad input.

Type of change:

  • Bug fix

High-level design:

N/A — small change.

Tests:

Use cases covered

  • Admin picks a field for a condition, leaves the value empty, hits Save → save is blocked, an inline message explains why, and no POST /aiContext/rules is fired.
  • Admin saves a rule with no filter conditions at all → still saves. This is the regression guard, see the note below.
  • Admin removes the unfinished condition → the error clears and the rule saves.

Unit tests

  • Not applicable to the validity check itself — see the honest note below.
  • Existing jest suites re-run for regressions: QueryBuilderElasticsearchFormatUtils, AdvancedSearchClassBase, RuleQueryBuilderField, AdvancedAssetsFilterField, QueryBuilderWidget, QueryBuilderWidgetV1, ContractSemanticFormTab, and every PersonaAIContext suite.
Test Suites: 13 passed, 13 total
Tests:       147 passed, 147 total

Playwright (UI) tests

  • Added to openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/PersonaAIContext.spec.ts:
    • blocks saving a rule whose condition has no value entered
    • saves a rule that has no filter conditions at all
    • clears the filter error once the unfinished condition is removed

Why Playwright and not jest — please read before reviewing. Every jest suite in this repo mocks react-awesome-query-builder wholesale (jest.mock('@react-awesome-query-builder/antd') in QueryBuilderWidgetV1.test.tsx), so QbUtils is a stub there and a unit test would assert nothing about real isValidTree behaviour. Building the real config standalone does not work either — OM's getTreeConfig output is not complete until RAQB's <Query> extends it, so ConfigUtils.extendConfig throws on it. Playwright is the only place the real query builder actually runs.

The second test is the load-bearing one. isValidTree is RAQB's own validity check and I could not verify locally how it treats a tree with no rules. If it reports an empty tree as invalid, this change would block saving every "match everything" rule — which the schema documents as legal ("Empty selects every entity of the configured type") and which would be a worse regression than the bug being fixed. That test exists to fail loudly in CI if that is the case. I have deliberately not run it locally; please watch that job. If it goes red, the fix is to gate the check on the tree being non-empty, not to loosen the test.

Backend integration tests

  • Not applicable (no backend change in this PR).

Ingestion integration tests

  • Not applicable.

Manual testing performed

None — flagged deliberately. The local stack was unavailable, which is exactly why the verification is pushed into the Playwright job rather than asserted here.

Lint and type-check were run:

  • eslint on the changed spec — 0 errors, 0 warnings.
  • yarn lint:playwright — 0 errors (280 pre-existing warnings elsewhere in the suite, none in the changed file).
  • The changed src files pass the CI organize-imports → eslint → prettier sequence with no diff.
  • tsc --noEmit reports one error in QueryBuilderWidgetV1.test.tsx:297, confirmed pre-existing on the base commit and untouched by this change.

UI screen recording / screenshots:

Not attached — no local stack available to record against. The three Playwright cases document the behaviour end-to-end instead.

i18n

message.persona-context-rule-filter-incomplete added and synced with yarn i18n. All 19 non-English locales carry a real translation rather than the English placeholder yarn i18n inserts. Note pr-pr is Persian, not Portuguese.

Checklist:

  • I have read the CONTRIBUTING document.
  • My PR title is Fixes <issue-number>: <short explanation>
  • My PR is linked to a GitHub issue.
  • I have commented on my code, particularly in hard-to-understand areas.
  • For UI changes: recording explained above.
  • I have added tests and listed them above.

🤖 Generated with Claude Code

Greptile Summary

The PR prevents saving unfinished persona AI-context filters.

  • Adds query-tree validity reporting to QueryBuilderWidgetV1 and forwards it through RuleQueryBuilderField.
  • Blocks persona context-rule submission and displays an inline localized error while the filter is invalid.
  • Adds Playwright coverage for unfinished, empty, and subsequently removed conditions.
  • Adds the new validation message to all supported locale files.

Confidence Score: 4/5

The validity lifecycle needs to be fixed before merging because valid rules can remain blocked after resets while invalid persisted rules can bypass the guard without an edit.

Validity is retained after the displayed filter is reset, but it is not initialized when an existing filter tree loads, causing both false save rejection and missed validation.

Files Needing Attention: openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx; openmetadata-ui/src/main/resources/ui/src/components/common/QueryBuilderWidgetV1/QueryBuilderWidgetV1.tsx

Important Files Changed

Filename Overview
openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx Adds save blocking and inline feedback, but retains stale invalidity across form resets and entity-type changes.
openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/RuleQueryBuilderField.component.tsx Forwards the optional validity callback to the shared query-builder widget.
openmetadata-ui/src/main/resources/ui/src/components/common/QueryBuilderWidgetV1/QueryBuilderWidgetV1.tsx Reports validity only during query-builder changes, leaving initially loaded invalid trees unreported.
openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/PersonaAIContext.spec.ts Adds end-to-end coverage for blocking unfinished conditions while preserving empty-rule saves and recovery after deletion.

Reviews (1): Last reviewed commit: "fix(ui): block saving a persona rule wit..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

The emitted queryFilter now drops a condition that has a field but no value,
because `{"term":{}}` is rejected outright by both search engines. Dropping it
silently on save is its own bug: a rule the user built as "tier is ___" persists
as no filter at all, so it matches everything and pulls the entity cap instead of
the handful of assets they selected.

The rule editor now refuses the save while a condition is unfinished and says so
inline, rather than quietly widening the rule.

QueryBuilderWidgetV1 reports tree validity through a new optional
onValidityChange prop, using the config react-awesome-query-builder has already
extended in its onChange — the same validity RAQB uses internally. Only the
persona rule editor opts in, so Explore and the other query-builder consumers are
untouched.

Playwright covers the three cases, including the regression guard: a rule with no
filter at all must stay saveable, since "empty selects every entity of the
configured type" is documented behaviour.

Relates to #30715.
@pmbrull
pmbrull requested a review from a team as a code owner July 30, 2026 15:45
@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added Ingestion safe to test Add this label to run secure Github workflows on PRs labels Jul 30, 2026
useWatch({ control: form.control, name: 'fullyRendered' }) ?? false;
const maxAssets = useWatch({ control: form.control, name: 'maxAssets' });
const queryFilter = useWatch({ control: form.control, name: 'queryFilter' });
const [filterIncomplete, setFilterIncomplete] = useState(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: filterIncomplete state is never reset, leaving stale save-block

filterIncomplete is only ever cleared when the query builder fires onValidityChange(true) from a user interaction (handleChange in QueryBuilderWidgetV1), which does not run on mount. Neither handleDismiss nor the open/reset effect resets it. So if a user starts an unfinished condition (setting filterIncomplete=true), then closes the drawer and reopens it (for a new rule or a different rule), the inline error stays visible and handleSubmit keeps returning early — blocking the save of a rule that has no filter at all, which the PR explicitly guarantees must remain saveable. Reset the flag when the drawer resets/dismisses.

Also add setFilterIncomplete(false) alongside the form.reset() call inside the open/reset useEffect (line ~672) so reopening for a different rule starts clean.:

const handleDismiss = useCallback(() => {
  previewRequestRef.current++;
  setPreview(undefined);
  setMaxAssetsDraft(undefined);
  setFilterIncomplete(false);
  form.reset(getDefaultRule(rule));
  onClose();
}, [form, onClose, rule]);
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 1 findings

Blocks saving a persona AI context rule with an unfinished condition, but the filterIncomplete state is never reset on mount, leaving a stale save-block.

⚠️ Bug: filterIncomplete state is never reset, leaving stale save-block

📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:156 📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:261-267 📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:413 📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:415-422 📄 openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:668-681

filterIncomplete is only ever cleared when the query builder fires onValidityChange(true) from a user interaction (handleChange in QueryBuilderWidgetV1), which does not run on mount. Neither handleDismiss nor the open/reset effect resets it. So if a user starts an unfinished condition (setting filterIncomplete=true), then closes the drawer and reopens it (for a new rule or a different rule), the inline error stays visible and handleSubmit keeps returning early — blocking the save of a rule that has no filter at all, which the PR explicitly guarantees must remain saveable. Reset the flag when the drawer resets/dismisses.

Also add setFilterIncomplete(false) alongside the form.reset() call inside the open/reset useEffect (line ~672) so reopening for a different rule starts clean.
const handleDismiss = useCallback(() => {
  previewRequestRef.current++;
  setPreview(undefined);
  setMaxAssetsDraft(undefined);
  setFilterIncomplete(false);
  form.reset(getDefaultRule(rule));
  onClose();
}, [form, onClose, rule]);
🤖 Prompt for agents
Code Review: Blocks saving a persona AI context rule with an unfinished condition, but the filterIncomplete state is never reset on mount, leaving a stale save-block.

1. ⚠️ Bug: filterIncomplete state is never reset, leaving stale save-block
   Files: openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:156, openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:261-267, openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:413, openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:415-422, openmetadata-ui/src/main/resources/ui/src/components/Settings/Persona/PersonaAIContext/ContextRuleEditor/ContextRuleEditor.component.tsx:668-681

   `filterIncomplete` is only ever cleared when the query builder fires `onValidityChange(true)` from a user interaction (`handleChange` in QueryBuilderWidgetV1), which does not run on mount. Neither `handleDismiss` nor the open/reset effect resets it. So if a user starts an unfinished condition (setting `filterIncomplete=true`), then closes the drawer and reopens it (for a new rule or a different rule), the inline error stays visible and `handleSubmit` keeps returning early — blocking the save of a rule that has no filter at all, which the PR explicitly guarantees must remain saveable. Reset the flag when the drawer resets/dismisses.

   Fix (Also add setFilterIncomplete(false) alongside the form.reset() call inside the open/reset useEffect (line ~672) so reopening for a different rule starts clean.):
   const handleDismiss = useCallback(() => {
     previewRequestRef.current++;
     setPreview(undefined);
     setMaxAssetsDraft(undefined);
     setFilterIncomplete(false);
     form.reset(getDefaultRule(rule));
     onClose();
   }, [form, onClose, rule]);

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar | Powered by Gitar — free for open source

shouldDirty: true,
});
}}
onValidityChange={(isValid) => setFilterIncomplete(!isValid)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale validity blocks valid saves

When an unfinished condition marks the filter invalid, dismissing and reopening the editor or changing the entity type resets the filter without resetting filterIncomplete. The submit handler therefore continues rejecting the now-valid or empty rule.

// nConfig is the config react-awesome-query-builder has already extended, which isValidTree
// needs. A condition the user started but has not finished reads as invalid, letting the caller
// block the save instead of silently dropping the condition from the emitted filter.
props.onValidityChange?.(QbUtils.isValidTree(nTree, nConfig));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Initial invalid trees bypass guard

When the editor loads an existing rule containing an unfinished persisted condition and the user saves without changing the query tree, validity is never reported because this callback runs only from handleChange. filterIncomplete remains false, so the rule is submitted despite the new validation guard.

@github-actions

Copy link
Copy Markdown
Contributor

Jest test Coverage

UI tests summary

Lines Statements Branches Functions
Coverage: 65%
65.84% (77078/117067) 49.7% (46334/93226) 50.92% (13948/27390)

@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — workflow failed

Validated commit 4b87fe819b11ed2ebe5cb6b50ad6385f1496cfb9 in Playwright run 30558262974, attempt 1.

✅ 575 passed · ❌ 1 failed · 🟡 0 flaky · ⏭️ 5 skipped · 🧰 0 lifecycle flaky

Performance

Blocking targets: ✅ met · Optimization targets: 🟡 in progress

Shard-job maxima below are not the full workflow wall time; the linked run includes build, fixture, planning, and reporting.

🕒 Full workflow signal wall (to summary) 49m 7s

⏱️ Max setup 3m 1s · max shard execution 17m 3s · max shard-job elapsed before upload 20m 5s · reporting 6s

🌐 197.28 requests/attempt · 2.75 app boots/UI scenario · 28.54% common-shard skew

Optimization targets still in progress:

  • Common shard skew was 28.54% (convergence target: at most 15%).
  • Application boot ratio was 2.75 per UI scenario (1685 boots / 612 scenarios; convergence target: at most 1).
Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky
✅ Shard chromium-01 96 0 0 0 0 0
✅ Shard chromium-02 112 0 0 3 0 0
🔴 Shard chromium-03 114 1 0 2 0 0
✅ Shard chromium-04 113 0 0 0 0 0
✅ Shard data-asset-rules-01 61 0 0 0 0 0
✅ Shard domain-isolation-01 14 0 0 0 0 0
✅ Shard global-state-01 23 0 0 0 0 0
✅ Shard ingestion-01 1 0 0 0 0 0
✅ Shard reindex-01 2 0 0 0 0 0
✅ Shard search-01 10 0 0 0 0 0
✅ Shard search-rbac-01 29 0 0 0 0 0

Genuine Failures (failed on all attempts)

Features/PersonaAIContext.spec.tsblocks saving a rule whose condition has no value entered (shard chromium-03)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoHaveCount�[2m(�[22m�[32mexpected�[39m�[2m)�[22m failed  Locator:  getByTestId('delete-condition-button') Expected: �[32m1�[39m Received: �[31m2�[39m Timeout:  15000ms  Call log: �[2m  - Expect "toHaveCount" with timeout 15000ms�[22m �[2m  - waiting for getByTestId('delete-condition-button')�[22m �[2m    19 × locator resolved to 2 elements�[22m �[2m       - unexpected value "2"�[22m 

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Ingestion safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant