Skip to content

fix(dq): stale incident status on the Test Case page after reopening a resolved incident (1.13)#30183

Open
manerow wants to merge 3 commits into
1.13from
fix/testcase-incident-stale-pointer-1.13
Open

fix(dq): stale incident status on the Test Case page after reopening a resolved incident (1.13)#30183
manerow wants to merge 3 commits into
1.13from
fix/testcase-incident-stale-pointer-1.13

Conversation

@manerow

@manerow manerow commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Fixes #30168

1.13 backport. The main PR is #30182; this branch carries the read-derivation fix only (the task-first reopen behavior is main-only).

The mental model you need first

When a Data Quality test fails, the system opens an incident: a tracked problem someone has to work. An incident has a lifecycle (New -> Acknowledged -> Assigned -> Resolved) and is identified by a stateId; every status change appends a row to the incident timeline.

Two separate things get written as a test lives its life:

  1. Test results: every run writes a row (pass/fail). When a run fails, that row is stamped with the id of the incident it belongs to (result.incidentId). This stamp is written once, at the moment the result is ingested, and never touched again.
  2. Incident status records: every status change (New/Ack/Assigned/Resolved) writes a row to a separate timeline, all sharing the incident's stateId.

The Test Case page needs to answer one question: "Does this test case have an open incident right now, and what's its status?" It reads a field called TestCase.incidentId to do that. That field is the whole story.

The problem (what the customer saw)

Someone resolved an incident by mistake, then set it back to Acknowledged. The incident itself showed Acknowledged correctly, but the Test Case page kept showing Resolved, until the test failed again.

Why it happened (how it worked before)

TestCase.incidentId was computed from the wrong source: the read paths derived it from the latest test result's stamp (result.incidentId), which is frozen at ingestion time. That answers "which incident did the last run belong to," which is not the same question as "what is the current incident."

Walk the reopen scenario:

  1. Test fails -> incident S1 opens, the failed result is stamped incidentId = S1.
  2. Ack, then Resolved -> all still on S1.
  3. User reopens with Ack. Resolved is a terminal state, so a brand-new incident S2 is created. But no test result points at S2: the last result is still stamped S1.

Now TestCase.incidentId (from the frozen stamp) still says S1, whose latest status is Resolved. So the page shows Resolved even though the live incident is S2/Ack. It only self-corrects on the next failed run, because that writes a new result stamped with the current incident.

What the PR changes

This is a read-derivation fix only (the task-first reopen behavior lands on main in #30182). TestCase.incidentId now means one thing on both read paths: the stateId of the latest unresolved incident record, or null if there is no open incident.

  • TestCaseRepository.getIncidentId (single read) reads the incident timeline (latest record, unresolved only) instead of the result stamp.
  • fetchAndSetTestCaseResultsAndIncidents (bulk/list read) reads the same thing via a new batch query over the incident timeline.

Incident lifecycle is unchanged on this branch: reopening a resolved incident still starts a new incident, but the pointer now follows it, so the page shows the current status.

How it works now (same scenario)

  1. Test fails -> incident S1, page shows the open incident.
  2. Ack -> S1, page shows Ack.
  3. Resolved -> TestCase.incidentId becomes null -> page shows No Incident (correct: nothing is open).
  4. Reopen with Ack -> a new incident S2 opens; the pointer now follows it -> TestCase.incidentId = S2 -> page shows Ack.

Verified on a live 1.13.1 deployment: after Resolved the entity returns null, and after the reopen it returns the new incident and the page shows Ack.

Why this is the right way

  • Single source of truth. "Current incident" is a fact about the incident's own lifecycle, so it's read from the incident timeline, never inferred from a frozen result stamp. The single-read and list paths now agree, and there's no "self-heals on next failure" staleness class left.
  • It matches the product's own model. The product treats incidents as human-managed work items: a passing test does not auto-close an incident; a person must resolve it. Deriving the pointer from "the latest run's stamp" quietly contradicted that. Reading "the latest unresolved incident" is that model.

Two intended, correct side effects fall out of this: after a resolve the page shows "No Incident" instead of a stale Resolved chip, and a currently-passing test that still has an unresolved incident now correctly shows it (because the incident is genuinely still open until someone resolves it).

Testing

  • New TestCaseResourceIT#test_incidentIdDerivation_followsLatestUnresolvedTcrs: derivation matrix over the single GET and the bulk list, with the 1.13 expectations (no incident -> null, New/Ack -> stateId, Resolved -> null, reopen -> new stateId the pointer follows, passing run while open -> stateId).
  • Existing incident tests in TestCaseResourceIT pass.
  • Reproduced the customer scenario end to end on a live 1.13.1 deployment before and after the fix: after Resolved -> Ack the test case entity returns the new incident and the page shows Ack; after Resolved it returns null.

Greptile Summary

This PR derives the current test-case incident from the incident timeline instead of the latest test result. The main changes are:

  • Adds a batch query for the latest incident status per test case.
  • Aligns detail and list responses with unresolved incident state.
  • Adds integration coverage for resolve, reopen, and passing-run behavior.

Confidence Score: 5/5

No additional blocking issue qualifies for this follow-up review.

  • The incident pointer now follows the unresolved lifecycle record.
  • The new test covers the intended behavior across detail and list responses.

Important Files Changed

Filename Overview
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java Adds the batch query used to retrieve the latest incident status for each test case.
openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java Derives incident IDs from unresolved resolution-status records in detail and list reads.
openmetadata-integration-tests/src/test/java/org/openmetadata/it/tests/TestCaseResourceIT.java Adds lifecycle coverage for incident creation, resolution, reopening, and passing results.

Reviews (3): Last reviewed commit: "fix(dq): deterministic tie-breaker for l..." | Re-trigger Greptile

Context used:

  • Context used - CLAUDE.md (source)

@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.

@manerow manerow added backend bug Something isn't working safe to test Add this label to run secure Github workflows on PRs labels Jul 17, 2026
@manerow manerow self-assigned this Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — 1 failure(s), 9 flaky

✅ 3968 passed · ❌ 1 failed · 🟡 9 flaky · ⏭️ 31 skipped

Shard Passed Failed Flaky Skipped
✅ Shard 1 291 0 0 4
✅ Shard 2 0 0 0 0
🟡 Shard 3 735 0 2 7
🟡 Shard 4 781 0 2 2
🟡 Shard 5 756 0 1 9
✅ Shard 6 693 0 0 0
🔴 Shard 7 712 1 4 9

Genuine Failures (failed on all attempts)

Features/AutoPilot.spec.ts › Create Service and check the AutoPilot status (shard 7)
Error: �[2mexpect(�[22m�[31mlocator�[39m�[2m).�[22mtoBeVisible�[2m(�[22m�[2m)�[22m failed

Locator: getByText('AutoPilot agents run completed successfully.')
Expected: visible
Timeout: 60000ms
Error: element(s) not found

Call log:
�[2m  - Expect "toBeVisible" with timeout 60000ms�[22m
�[2m  - waiting for getByText('AutoPilot agents run completed successfully.')�[22m

🟡 9 flaky test(s) (passed on retry)
  • Features/BulkEditEntity.spec.ts › Database Schema (shard 3, 1 retry)
  • Features/DataQuality/TestLibrary.spec.ts › should create, edit, and delete a test definition (shard 3, 1 retry)
  • Features/Permissions/GlossaryPermissions.spec.ts › Team-based permissions work correctly (shard 4, 1 retry)
  • Flow/PersonaFlow.spec.ts › Set default persona for team should work properly (shard 4, 1 retry)
  • Pages/DataContractInheritance.spec.ts › Remove Asset - Inherited contract no longer shown when asset is removed from Data Product (shard 5, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify lineage schema filter selection (shard 7, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: searchIndex (shard 7, 1 retry)
  • Pages/TestSuite.spec.ts › Logical TestSuite (shard 7, 1 retry)
  • Pages/Users.spec.ts › Permissions for table details page for Data Consumer (shard 7, 1 retry)

📦 Download artifacts

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

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@gitar-bot

gitar-bot Bot commented Jul 17, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 1 resolved / 1 findings

Updates TestCase incident derivation to query the incident timeline directly instead of using frozen result stamps, ensuring the UI accurately reflects reopened incidents. Added a deterministic tie-breaker for batch queries to resolve the incident selection ambiguity.

✅ 1 resolved
Edge Case: Latest-incident selection has no tie-breaker on equal timestamps

📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/CollectionDAO.java:8869-8876 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java:433-447 📄 openmetadata-service/src/main/java/org/openmetadata/service/jdbi3/TestCaseRepository.java:1082-1086
getLatestRecordBatchInternal joins on MAX(timestamp) and the single-read getLatestRecord uses ORDER BY timestamp DESC LIMIT 1, neither with a secondary sort key. If two resolution-status records share the same millisecond timestamp for one test case (plausible when a reopen inserts multiple status rows in one transaction), the batch join returns both rows and HashMap.put keeps an arbitrary one, while the single read picks a different arbitrary one — so the derived incidentId can differ between the two paths and the new test's single-vs-bulk cross-check may flake. Consider adding a deterministic tie-breaker (e.g. secondary ORDER BY id) to both queries so latest selection is stable.

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

@github-actions

Copy link
Copy Markdown
Contributor

The Python checkstyle failed.

Please run make py_format and py_format_check in the root of your repository and commit the changes to this PR.
You can also use pre-commit to automate the Python code formatting.

You can install the pre-commit hooks with make install_test precommit_install.

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

backend bug Something isn't working 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