From 26bf2ea7760ff24c642334163af70e2a271c374b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 19:13:22 +0000 Subject: [PATCH 1/5] test(e2e): use route.fallback in ETag strip so spec mocks still apply MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stripEtagConditionalReads catch-all page.route('**/api/v1/**') used route.continue(), which terminates Playwright's route chain and sends the request straight to the network. Because Playwright evaluates matching handlers in reverse registration order and the strip is installed via redirectToHomePage after each spec registers its own more-specific route.fulfill() mocks, the catch-all ran first and shadowed those mocks — forwarding /api/v1/rdf/** and /api/v1/services/ingestionPipelines/** to the real backend. Switch to route.fallback({ headers }) so the header is stripped and control still defers to the earlier spec-specific mocks. Re-enable the two suites that were skipped as a reaction to this regression. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013urWrUkaz75D39KfW3Z84o --- .../ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts | 2 +- .../e2e/Features/ServiceAgentsPauseResume.spec.ts | 2 +- .../src/main/resources/ui/playwright/utils/common.ts | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts index 6114033ef035..9ab449bb3559 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts @@ -79,7 +79,7 @@ async function navigateToGlossaryRelationsGraph( await waitForGraphLoaded(page); } -test.describe.skip('Ontology Explorer — RDF exports (Turtle and RDF/XML)', () => { +test.describe('Ontology Explorer — RDF exports (Turtle and RDF/XML)', () => { test('Turtle (.ttl) option appears in the export menu when RDF is enabled', async ({ browser, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts index a0c4a386981e..dd7b2987dbc2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts @@ -95,7 +95,7 @@ const clickAndAwaitToggle = async (page: Page, testId: string) => { await toggleResponse; }; -test.describe.skip('Service Agents pause and resume', () => { +test.describe('Service Agents pause and resume', () => { test.beforeAll(async ({ browser }) => { const { apiContext, afterAction } = await createNewPage(browser); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 53c69bbfd9a4..134ff4fc2007 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -79,6 +79,11 @@ const etagStripInstalled = new WeakSet(); * the header here forces the server to always return the current body, and it * works regardless of the deployed bundle (unlike the localStorage opt-out, * which depends on the bundle carrying the interceptor guard). + * + * Uses `route.fallback` (not `route.continue`) so this catch-all strips the + * header and then defers to any spec-specific `route.fulfill` mocks registered + * earlier on the same page. `route.continue` would terminate the chain and send + * the request straight to the network, shadowing those mocks. */ export const stripEtagConditionalReads = async (page: Page) => { if (etagStripInstalled.has(page)) { @@ -88,7 +93,7 @@ export const stripEtagConditionalReads = async (page: Page) => { await page.route('**/api/v1/**', async (route) => { const headers = route.request().headers(); delete headers['if-none-match']; - await route.continue({ headers }); + await route.fallback({ headers }); }); }; From 872703eeb1472c673d5395c27290c234bc918a19 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 19:27:21 +0000 Subject: [PATCH 2/5] test(e2e): keep OntologyExplorerRdf exports suite skipped Revert the un-skip of the RDF exports block. The route.fallback fix in common.ts still applies, but leave this suite disabled for now. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_013urWrUkaz75D39KfW3Z84o --- .../ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts index 9ab449bb3559..6114033ef035 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/OntologyExplorerRdf.spec.ts @@ -79,7 +79,7 @@ async function navigateToGlossaryRelationsGraph( await waitForGraphLoaded(page); } -test.describe('Ontology Explorer — RDF exports (Turtle and RDF/XML)', () => { +test.describe.skip('Ontology Explorer — RDF exports (Turtle and RDF/XML)', () => { test('Turtle (.ttl) option appears in the export menu when RDF is enabled', async ({ browser, }) => { From 753f7c53955e7c828e87b28f6f86819bd01c2496 Mon Sep 17 00:00:00 2001 From: Harshit Shah Date: Fri, 24 Jul 2026 11:06:20 +0530 Subject: [PATCH 3/5] Fix data quality dashboard --- .../DataQuality/DataQualityDashboard.spec.ts | 4 +- .../ui/playwright/utils/dataQuality.ts | 43 ++++++++++++++++--- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts index 34fe2c6182ad..37cf1764046a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/DataQuality/DataQualityDashboard.spec.ts @@ -637,14 +637,14 @@ test.describe( ).toContainText('New'); }); - await test.step('Verify Resolved incident for Uniqueness test case on table4 DQ tab', async () => { + await test.step('Verify resolved Uniqueness incident is no longer shown as ongoing', async () => { await visitDataQualityTab(page, table4); await expect( page.locator(`[data-testid="status-badge-${uniquenessTestCaseName}"]`) ).toContainText('Failed'); await expect( page.locator(`[data-testid="${uniquenessTestCaseName}-status"]`) - ).toContainText('Resolved'); + ).toHaveCount(0); }); await test.step('Filter by Certification and verify Uniqueness widget shows 1 Failed test case', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts index 809589db609a..9d110762e064 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/dataQuality.ts @@ -579,12 +579,14 @@ export async function applyDashboardCertificationFilter( } /** - * Polls the incident status API until an incident for `testCaseFqn` appears - * within the [failTs-60s, failTs+120s] window. - * Call this immediately after `addTestCaseResult` to guarantee the incident - * document is indexed before any UI assertions. - * Pass `expectedStatus` to also wait until the incident reaches that resolution - * status (e.g. "Resolved") — useful after posting a status transition. + * Waits until incident data reaches the index consumed by the Data Quality tab. + * + * Ongoing incidents are read from the resolution-status timeline. Resolving an + * incident is different: the backend asynchronously clears TestCase.incidentId + * after the transaction commits, and the Data Quality tab uses that search + * field to decide whether an incident is still active. Therefore, a Resolved + * transition waits for incidentId to disappear from the test-case search + * document instead of polling the resolution-status timeline. */ export async function waitForIncidentToBeIndexed( apiContext: APIRequestContext, @@ -595,6 +597,35 @@ export async function waitForIncidentToBeIndexed( await expect .poll( async () => { + if (expectedStatus === 'Resolved') { + // The Resolved timeline record may be indexed before the targeted + // TestCase search update clears incidentId. Poll the latter because + // it is the state used to render the incident cell in the DQ tab. + const testCaseResponse = await apiContext.get( + '/api/v1/dataQuality/testCases/search/list', + { + params: { + fields: 'incidentId', + limit: 10, + q: testCaseFqn, + }, + } + ); + const testCaseBody = await testCaseResponse.json(); + const testCase = ( + (testCaseBody.data ?? []) as Array<{ + fullyQualifiedName?: string; + incidentId?: string; + }> + ).find((item) => item.fullyQualifiedName === testCaseFqn); + + // A missing test-case document is not equivalent to a cleared + // incidentId; wait until the exact document exists without the field. + return Boolean(testCase) && !testCase?.incidentId; + } + + // Non-resolved incidents remain active, so wait for their matching + // resolution-status record to become searchable. const res = await apiContext.get( `/api/v1/dataQuality/testCases/testCaseIncidentStatus?latest=true` + `&startTs=${failTs - 60_000}` + From 66ef0e98c684b0a57a82ed9f4710666791692a07 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Fri, 24 Jul 2026 12:58:52 +0530 Subject: [PATCH 4/5] Fix the ServiceAgentsPauseResume test --- .../Features/ServiceAgentsPauseResume.spec.ts | 132 ++++++++---------- 1 file changed, 58 insertions(+), 74 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts index dd7b2987dbc2..9250b314d5f3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts @@ -11,45 +11,55 @@ * limitations under the License. */ import { expect, Page, test } from '@playwright/test'; -import { DatabaseServiceClass } from '../../support/entity/service/DatabaseServiceClass'; -import { createNewPage, redirectToHomePage, uuid } from '../../utils/common'; +import { EntityDataClass } from '../../support/entity/EntityDataClass'; +import { createNewPage, uuid } from '../../utils/common'; +import { getEncodedFqn } from '../../utils/entity'; import { getAgentCard } from '../../utils/serviceIngestion'; // use the admin user to login test.use({ storageState: 'playwright/.auth/admin.json' }); -const service = new DatabaseServiceClass(); let pipelineName = ''; /** - * Pause/resume is a single `toggleIngestion` endpoint that flips `enabled` - * server-side, and the backend forwards the pause to Airflow. The tests below - * mock both the pipeline listing and the toggle call so the menu's - * enabled → pause / disabled → resume mapping can be asserted without an + * The real `toggleIngestion` flip lives behind Airflow: it only sets + * `enabled=false` when Airflow's `disable` endpoint returns 200, which needs a + * deployed DAG. This suite never deploys one (and `Features` CI has no Airflow), + * so the live call returns 404 and leaves `enabled` untouched. We therefore mock + * the Airflow boundary — the toggle call — as a stateful flip, and rewrite the + * reads it feeds (list + single-agent refetch) so the UI's + * enabled → pause / disabled → resume mapping is exercised end-to-end without an * ingestion container. */ -const mockPipelineEnabledState = async ( - page: Page, - initialEnabled: boolean -) => { +const mockToggleFlow = async (page: Page, initialEnabled: boolean) => { let enabled = initialEnabled; + const rewriteEnabled = (pipeline: { name: string; enabled?: boolean }) => + pipeline.name === pipelineName ? { ...pipeline, enabled } : pipeline; + await page.route('**/api/v1/services/ingestionPipelines?*', async (route) => { const response = await route.fetch(); const body = await response.json(); await route.fulfill({ response, - json: { - ...body, - data: (body.data ?? []).map( - (pipeline: { name: string; enabled?: boolean }) => - pipeline.name === pipelineName ? { ...pipeline, enabled } : pipeline - ), - }, + json: { ...body, data: (body.data ?? []).map(rewriteEnabled) }, }); }); + // Single-agent refetch after a terminal SSE progress event + // (`/services/ingestionPipelines/name/{fqn}`) — left un-mocked it would return + // the real `enabled` and clobber the flip. + await page.route( + '**/api/v1/services/ingestionPipelines/name/*', + async (route) => { + const response = await route.fetch(); + const body = await response.json(); + + await route.fulfill({ response, json: rewriteEnabled(body) }); + } + ); + await page.route( '**/api/v1/services/ingestionPipelines/toggleIngestion/*', async (route) => { @@ -58,49 +68,37 @@ const mockPipelineEnabledState = async ( await route.fulfill({ status: 200, json: { enabled } }); } ); + + await page.route( + '**/api/v1/services/ingestionPipelines/progress/service/**', + (route) => route.fulfill({ status: 204, body: '' }) + ); }; -const openAgentActions = async (page: Page) => { +const openCloseAgentActions = async (page: Page, isOpen = true) => { const agentCard = getAgentCard(page, pipelineName); await agentCard.getByTestId('more-actions').click(); - await page.getByTestId('actions-dropdown').waitFor(); -}; - -const visitAgentsTab = async (page: Page) => { - await redirectToHomePage(page); - await service.visitEntityPage(page); - await page.getByTestId('data-assets-header').waitFor(); - await page.getByTestId('agents').click(); - - const metadataSubTab = page.getByTestId('metadata-sub-tab'); - if (await metadataSubTab.isVisible()) { - await metadataSubTab.click(); + if (isOpen) { + await page.getByTestId('actions-dropdown').waitFor(); } - - await expect(getAgentCard(page, pipelineName)).toBeVisible(); }; -const clickAndAwaitToggle = async (page: Page, testId: string) => { - const toggleResponse = page.waitForResponse( - (response) => - response - .url() - .includes('/services/ingestionPipelines/toggleIngestion/') && - response.request().method() === 'POST' +const visitAgentsTab = async (page: Page, serviceFQN: string) => { + await page.goto( + `/service/databaseServices/${getEncodedFqn(serviceFQN)}/agents/metadata` ); + await page.getByTestId('data-assets-header').waitFor(); - await page.getByTestId(testId).click(); - - await toggleResponse; + await expect(getAgentCard(page, pipelineName)).toBeVisible(); }; test.describe('Service Agents pause and resume', () => { + const service = EntityDataClass.databaseService; + test.beforeAll(async ({ browser }) => { const { apiContext, afterAction } = await createNewPage(browser); - await service.create(apiContext); - pipelineName = `pw-pause-resume-${uuid()}`; const pipelineResponse = await apiContext.post( '/api/v1/services/ingestionPipelines', @@ -124,48 +122,34 @@ test.describe('Service Agents pause and resume', () => { await afterAction(); }); - test.afterAll(async ({ browser }) => { - const { apiContext, afterAction } = await createNewPage(browser); - - await service.delete(apiContext); - await afterAction(); - }); - - test('should pause an enabled agent and offer resume afterwards', async ({ + test('should offer pause for an enabled agent and resume in disabled state', async ({ page, }) => { - await mockPipelineEnabledState(page, true); - await visitAgentsTab(page); + await mockToggleFlow(page, true); + await visitAgentsTab(page, service.entityResponseData.fullyQualifiedName); - await openAgentActions(page); + await openCloseAgentActions(page); await expect(page.getByTestId('pause-button')).toBeVisible(); await expect(page.getByTestId('resume-button')).toBeHidden(); - await clickAndAwaitToggle(page, 'pause-button'); + const toggleResponse = page.waitForResponse( + '/api/v1/services/ingestionPipelines/toggleIngestion/*' + ); + const getPipelines = page.waitForResponse( + '/api/v1/services/ingestionPipelines?fields=*' + ); - await openAgentActions(page); + await page.getByTestId('pause-button').click(); - await expect(page.getByTestId('resume-button')).toBeVisible(); - await expect(page.getByTestId('pause-button')).toBeHidden(); - }); + await toggleResponse; + await getPipelines; - test('should resume a paused agent and offer pause afterwards', async ({ - page, - }) => { - await mockPipelineEnabledState(page, false); - await visitAgentsTab(page); + await page.getByTestId('actions-dropdown').waitFor({ state: 'hidden' }); - await openAgentActions(page); + await openCloseAgentActions(page); await expect(page.getByTestId('resume-button')).toBeVisible(); await expect(page.getByTestId('pause-button')).toBeHidden(); - - await clickAndAwaitToggle(page, 'resume-button'); - - await openAgentActions(page); - - await expect(page.getByTestId('pause-button')).toBeVisible(); - await expect(page.getByTestId('resume-button')).toBeHidden(); }); }); From 9a92610f01a387313980f6df6a8d83ebea8bbc28 Mon Sep 17 00:00:00 2001 From: Aniket Katkar Date: Fri, 24 Jul 2026 15:26:32 +0530 Subject: [PATCH 5/5] Work on the comments --- .../e2e/Features/ServiceAgentsPauseResume.spec.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts index 9250b314d5f3..3e7333532766 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ServiceAgentsPauseResume.spec.ts @@ -75,13 +75,11 @@ const mockToggleFlow = async (page: Page, initialEnabled: boolean) => { ); }; -const openCloseAgentActions = async (page: Page, isOpen = true) => { +const openAgentActions = async (page: Page) => { const agentCard = getAgentCard(page, pipelineName); await agentCard.getByTestId('more-actions').click(); - if (isOpen) { - await page.getByTestId('actions-dropdown').waitFor(); - } + await page.getByTestId('actions-dropdown').waitFor(); }; const visitAgentsTab = async (page: Page, serviceFQN: string) => { @@ -128,7 +126,7 @@ test.describe('Service Agents pause and resume', () => { await mockToggleFlow(page, true); await visitAgentsTab(page, service.entityResponseData.fullyQualifiedName); - await openCloseAgentActions(page); + await openAgentActions(page); await expect(page.getByTestId('pause-button')).toBeVisible(); await expect(page.getByTestId('resume-button')).toBeHidden(); @@ -147,7 +145,7 @@ test.describe('Service Agents pause and resume', () => { await page.getByTestId('actions-dropdown').waitFor({ state: 'hidden' }); - await openCloseAgentActions(page); + await openAgentActions(page); await expect(page.getByTestId('resume-button')).toBeVisible(); await expect(page.getByTestId('pause-button')).toBeHidden();