@open-mercato/webhooks 0.6.6-develop.5641.1.45d172106d → 0.6.6-develop.5654.1.ca21e35f26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -152,6 +152,7 @@ test.describe("TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
152
152
  ).catch(() => void 0);
153
153
  const row = page.getByRole("row", { name: new RegExp(`QA Lock 043 (?:bumped )?${stamp}\\b`) }).first();
154
154
  await expect(row, "created webhook should appear in the list").toBeVisible({ timeout: 2e4 });
155
+ await page.waitForLoadState("networkidle");
155
156
  const bump = await apiRequest(page.request, "PUT", `${WEBHOOKS_API}/${webhookId}`, {
156
157
  token,
157
158
  data: { description: `bumped-${stamp}` }
@@ -170,7 +171,7 @@ test.describe("TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
170
171
  const dialog = page.getByRole("alertdialog");
171
172
  await expect(dialog, "confirm dialog should open").toBeVisible({ timeout: 1e4 });
172
173
  await dialog.getByRole("button", { name: /^(confirm|delete)$/i }).click();
173
- await expectConflictBanner(page);
174
+ await expectConflictBanner(page, { timeout: 2e4 });
174
175
  } finally {
175
176
  if (webhookId) await deleteWebhook(page.request, token, webhookId);
176
177
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/webhooks/__integration__/TC-LOCK-OSS-043.spec.ts"],
4
- "sourcesContent": ["import { test, expect, type APIRequestContext } from '@playwright/test'\nimport { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth'\nimport {\n putWithLock,\n expectConflictBody,\n expectConflictBanner,\n resolveApiUrl,\n} from '@open-mercato/core/modules/core/__integration__/helpers/optimisticLockUi'\nimport { OPTIMISTIC_LOCK_HEADER_NAME } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\n/**\n * TC-LOCK-OSS-043 \u2014 webhooks + inbox settings + data-sync schedule\n * optimistic-lock conflict contract (WHK-01, INB-01, SYNC-01).\n *\n * All three surfaces guard writes with `enforceCommandOptimisticLock` inside\n * pure command/API routes \u2014 none expose a dedicated `data-crud-field-id`\n * CrudForm edit page for the locked record, so the conflict contract is proven\n * at the API level: capture the record's `updated_at`, advance it out-of-band\n * via a header-less write (the strictly-additive path always succeeds and bumps\n * `updated_at`), then replay the now-stale write with the original\n * expected-version header \u2192 409 `optimistic_lock_conflict`.\n *\n * - WHK-01: `PUT /api/webhooks/<id>` and `DELETE /api/webhooks/<id>`\n * (`packages/webhooks/src/modules/webhooks/api/webhooks/[id]/route.ts`,\n * `resourceKind: 'webhooks.endpoint'`).\n * - INB-01: `PATCH /api/inbox_ops/settings`\n * (`packages/core/src/modules/inbox_ops/api/settings/route.ts`,\n * `resourceKind: 'inbox_ops.settings'`). The settings row is a per-tenant\n * singleton, so the test bumps + restores `workingLanguage` instead of\n * creating/deleting a fixture.\n * - SYNC-01: `POST /api/data_sync/schedules`\n * (`packages/core/src/modules/data_sync/api/schedules/route.ts` \u2192\n * `SyncScheduleService.saveSchedule`, `resourceKind: 'data_sync.schedule'`).\n * The collection POST reads the expected-version header; the service enforces\n * the lock when a row with the same (integrationId, entityType, direction)\n * key already exists.\n */\n\nconst WEBHOOKS_API = '/api/webhooks'\nconst INBOX_SETTINGS_API = '/api/inbox_ops/settings'\nconst SCHEDULES_API = '/api/data_sync/schedules'\n\ntype InboxSettings = { id: string; workingLanguage: string; updatedAt: string }\n\nasync function createWebhook(\n request: APIRequestContext,\n token: string,\n stamp: number,\n): Promise<{ id: string; updatedAt: string }> {\n const created = await apiRequest(request, 'POST', WEBHOOKS_API, {\n token,\n data: {\n name: `QA Lock 043 ${stamp}`,\n url: `https://example.com/qa-lock-043-${stamp}`,\n subscribedEvents: ['qa.lock.test'],\n },\n })\n expect(created.status(), 'POST webhook should be 201').toBe(201)\n const body = (await created.json()) as { id?: string }\n expect(typeof body.id, 'webhook creation should return an id').toBe('string')\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${body.id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const detailBody = (await detail.json()) as { updatedAt?: string }\n expect(typeof detailBody.updatedAt, 'webhook should expose updatedAt').toBe('string')\n return { id: body.id as string, updatedAt: detailBody.updatedAt as string }\n}\n\nasync function readWebhookUpdatedAt(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<string> {\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const body = (await detail.json()) as { updatedAt?: string }\n return body.updatedAt as string\n}\n\nasync function deleteWebhook(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<void> {\n const current = await readWebhookUpdatedAt(request, token, id).catch(() => undefined)\n await request\n .fetch(resolveApiUrl(`${WEBHOOKS_API}/${id}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(current ? { [OPTIMISTIC_LOCK_HEADER_NAME]: current } : {}),\n },\n })\n .catch(() => undefined)\n}\n\nasync function patchInboxSettings(\n request: APIRequestContext,\n token: string,\n body: Record<string, unknown>,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(INBOX_SETTINGS_API), {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: body,\n })\n}\n\nasync function readInboxSettings(\n request: APIRequestContext,\n token: string,\n): Promise<InboxSettings> {\n const response = await apiRequest(request, 'GET', INBOX_SETTINGS_API, { token })\n expect(response.status(), 'GET inbox settings should be 200').toBe(200)\n const body = (await response.json()) as { settings?: InboxSettings | null }\n expect(body.settings, 'tenant inbox settings singleton should exist').toBeTruthy()\n return body.settings as InboxSettings\n}\n\nasync function postSchedule(\n request: APIRequestContext,\n token: string,\n scheduleValue: string,\n integrationId: string,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(SCHEDULES_API), {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: {\n integrationId,\n entityType: 'products',\n direction: 'import',\n scheduleType: 'cron',\n scheduleValue,\n timezone: 'UTC',\n fullSync: false,\n isEnabled: true,\n },\n })\n}\n\ntest.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule optimistic lock', () => {\n test('WHK-01 stale webhook PUT is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n // Advance updated_at out-of-band via a header-less PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Replay the now-stale write \u2192 409.\n const conflict = await putWithLock(\n page.request,\n token,\n `${WEBHOOKS_API}/${webhookId}`,\n { name: `QA Lock 043 stale ${stamp}` },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('WHK-01 stale webhook DELETE is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n const conflict = await page.request.fetch(resolveApiUrl(`${WEBHOOKS_API}/${webhookId}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n [OPTIMISTIC_LOCK_HEADER_NAME]: staleUpdatedAt,\n },\n })\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n // WHK-01 (browser UI): now ACTIVE. The product fix is committed/verified \u2014 the webhooks\n // server DELETE returns the structured 409 (proven by the active \"WHK-01 stale webhook\n // DELETE is refused with a 409\" API test) and the list page sends the lock header\n // (`buildOptimisticLockHeader(row.updatedAt)`) + routes the resulting 409 through\n // surfaceRecordConflict. The RowActions menu opens on CLICK of the \"Open actions\" kebab and\n // renders the items in a portal on document.body (role=\"menuitem\"); the confirm dialog is a\n // role=\"alertdialog\" with a \"Confirm\" button. We drive that choreography robustly here:\n // wait for the filtered list GET to settle, open the kebab, click Delete, confirm, then\n // assert the unified conflict bar (never the success toast).\n test('WHK-01 stale webhook DELETE in the list surfaces the conflict bar', async ({ page }) => {\n // Heavy browser flow (login + list load + portalled RowActions menu) routinely\n // exceeds Playwright's 20s default on a loaded ephemeral shard; opt into the\n // sanctioned per-test budget (see TC-LOCK-OSS-029). Global bump is disallowed.\n test.setTimeout(60_000)\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n\n await login(page, 'admin')\n await page.goto('/backend/webhooks')\n // Wait for the list GET to settle before interacting. The list reflects the\n // freshly created fixture immediately (newest-first), so a unique name makes\n // the row unambiguous.\n await page\n .waitForResponse(\n (response) =>\n response.url().includes('/api/webhooks') &&\n response.request().method() === 'GET' &&\n response.ok(),\n { timeout: 20_000 },\n )\n .catch(() => undefined)\n\n // The out-of-band PUT below renames the fixture to \"QA Lock 043 bumped <stamp>\";\n // tolerate both names so a mid-test list re-render cannot orphan the row locator.\n const row = page\n .getByRole('row', { name: new RegExp(`QA Lock 043 (?:bumped )?${stamp}\\\\b`) })\n .first()\n await expect(row, 'created webhook should appear in the list').toBeVisible({ timeout: 20_000 })\n\n // Advance updated_at out-of-band \u2192 the in-page row token is now stale.\n // NOTE: bump description (not name) so the row locator stays valid even if\n // the list re-fetches after the PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { description: `bumped-${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Open the row's RowActions kebab (opens on click) and trigger Delete. The\n // menu renders in a portal on document.body, so query it at the page level.\n // The list re-renders as data settles, so the menu can detach between \"open\"\n // and \"click\" \u2014 retry (re)open-menu + click-Delete atomically (same fix as\n // TC-LOCK-OSS-029); the confirm dialog gates the DELETE, so a repeated\n // click is safe.\n const kebab = row.getByRole('button', { name: /open actions/i })\n const deleteItem = page.getByRole('menuitem', { name: /^delete$/i })\n await expect(async () => {\n if (!(await deleteItem.isVisible().catch(() => false))) {\n await kebab.click({ timeout: 2_000 }).catch(() => {})\n await expect(deleteItem).toBeVisible({ timeout: 1_500 })\n }\n await deleteItem.click({ timeout: 2_000 })\n }).toPass({ timeout: 30_000 })\n\n // Confirm the destructive alertdialog (the row still holds the stale\n // updated_at captured at render time, so the DELETE 409s).\n const dialog = page.getByRole('alertdialog')\n await expect(dialog, 'confirm dialog should open').toBeVisible({ timeout: 10_000 })\n await dialog.getByRole('button', { name: /^(confirm|delete)$/i }).click()\n\n // The client must route the 409 to the unified conflict bar, never the\n // success toast.\n await expectConflictBanner(page)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('INB-01 stale inbox-settings PATCH is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const before = await readInboxSettings(page.request, token)\n const originalLanguage = before.workingLanguage\n const staleUpdatedAt = before.updatedAt\n const bumpLanguage = originalLanguage === 'de' ? 'en' : 'de'\n const staleLanguage = originalLanguage === 'es' ? 'pl' : 'es'\n try {\n // Advance updated_at out-of-band via a header-less PATCH.\n const bump = await patchInboxSettings(page.request, token, { workingLanguage: bumpLanguage })\n expect(bump.status(), 'out-of-band inbox PATCH should succeed').toBeLessThan(300)\n\n // Replay the now-stale write with the original expected-version \u2192 409.\n const conflict = await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: staleLanguage },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n // Restore the original working language with a fresh lock token.\n const current = await readInboxSettings(page.request, token).catch(() => null)\n if (current) {\n await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: originalLanguage },\n current.updatedAt,\n ).catch(() => undefined)\n }\n }\n })\n\n test('SYNC-01 stale data-sync schedule save is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n const integrationId = `qa-lock-043-${stamp}`\n let scheduleId: string | null = null\n try {\n const created = await postSchedule(page.request, token, '0 * * * *', integrationId)\n expect(created.status(), 'POST schedule should be 201').toBe(201)\n const createdBody = (await created.json()) as { id?: string; updatedAt?: string }\n scheduleId = createdBody.id ?? null\n const staleUpdatedAt = createdBody.updatedAt\n expect(typeof scheduleId, 'schedule creation should return an id').toBe('string')\n expect(typeof staleUpdatedAt, 'schedule should expose updatedAt').toBe('string')\n\n // Advance updated_at out-of-band via the header-less [id] PUT route.\n const bump = await apiRequest(page.request, 'PUT', `${SCHEDULES_API}/${scheduleId}`, {\n token,\n data: { scheduleValue: '5 * * * *' },\n })\n expect(bump.status(), 'out-of-band schedule PUT should succeed').toBeLessThan(300)\n\n // Replay the save for the same (integrationId, entityType, direction) key\n // with the now-stale expected-version \u2192 409.\n const conflict = await postSchedule(\n page.request,\n token,\n '9 * * * *',\n integrationId,\n staleUpdatedAt as string,\n )\n await expectConflictBody(conflict)\n } finally {\n if (scheduleId) {\n await apiRequest(page.request, 'DELETE', `${SCHEDULES_API}/${scheduleId}`, { token }).catch(\n () => undefined,\n )\n }\n }\n })\n})\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { test, expect, type APIRequestContext } from '@playwright/test'\nimport { getAuthToken, apiRequest } from '@open-mercato/core/modules/core/__integration__/helpers/api'\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth'\nimport {\n putWithLock,\n expectConflictBody,\n expectConflictBanner,\n resolveApiUrl,\n} from '@open-mercato/core/modules/core/__integration__/helpers/optimisticLockUi'\nimport { OPTIMISTIC_LOCK_HEADER_NAME } from '@open-mercato/shared/lib/crud/optimistic-lock-headers'\n\n/**\n * TC-LOCK-OSS-043 \u2014 webhooks + inbox settings + data-sync schedule\n * optimistic-lock conflict contract (WHK-01, INB-01, SYNC-01).\n *\n * All three surfaces guard writes with `enforceCommandOptimisticLock` inside\n * pure command/API routes \u2014 none expose a dedicated `data-crud-field-id`\n * CrudForm edit page for the locked record, so the conflict contract is proven\n * at the API level: capture the record's `updated_at`, advance it out-of-band\n * via a header-less write (the strictly-additive path always succeeds and bumps\n * `updated_at`), then replay the now-stale write with the original\n * expected-version header \u2192 409 `optimistic_lock_conflict`.\n *\n * - WHK-01: `PUT /api/webhooks/<id>` and `DELETE /api/webhooks/<id>`\n * (`packages/webhooks/src/modules/webhooks/api/webhooks/[id]/route.ts`,\n * `resourceKind: 'webhooks.endpoint'`).\n * - INB-01: `PATCH /api/inbox_ops/settings`\n * (`packages/core/src/modules/inbox_ops/api/settings/route.ts`,\n * `resourceKind: 'inbox_ops.settings'`). The settings row is a per-tenant\n * singleton, so the test bumps + restores `workingLanguage` instead of\n * creating/deleting a fixture.\n * - SYNC-01: `POST /api/data_sync/schedules`\n * (`packages/core/src/modules/data_sync/api/schedules/route.ts` \u2192\n * `SyncScheduleService.saveSchedule`, `resourceKind: 'data_sync.schedule'`).\n * The collection POST reads the expected-version header; the service enforces\n * the lock when a row with the same (integrationId, entityType, direction)\n * key already exists.\n */\n\nconst WEBHOOKS_API = '/api/webhooks'\nconst INBOX_SETTINGS_API = '/api/inbox_ops/settings'\nconst SCHEDULES_API = '/api/data_sync/schedules'\n\ntype InboxSettings = { id: string; workingLanguage: string; updatedAt: string }\n\nasync function createWebhook(\n request: APIRequestContext,\n token: string,\n stamp: number,\n): Promise<{ id: string; updatedAt: string }> {\n const created = await apiRequest(request, 'POST', WEBHOOKS_API, {\n token,\n data: {\n name: `QA Lock 043 ${stamp}`,\n url: `https://example.com/qa-lock-043-${stamp}`,\n subscribedEvents: ['qa.lock.test'],\n },\n })\n expect(created.status(), 'POST webhook should be 201').toBe(201)\n const body = (await created.json()) as { id?: string }\n expect(typeof body.id, 'webhook creation should return an id').toBe('string')\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${body.id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const detailBody = (await detail.json()) as { updatedAt?: string }\n expect(typeof detailBody.updatedAt, 'webhook should expose updatedAt').toBe('string')\n return { id: body.id as string, updatedAt: detailBody.updatedAt as string }\n}\n\nasync function readWebhookUpdatedAt(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<string> {\n const detail = await apiRequest(request, 'GET', `${WEBHOOKS_API}/${id}`, { token })\n expect(detail.status(), 'GET webhook detail should be 200').toBe(200)\n const body = (await detail.json()) as { updatedAt?: string }\n return body.updatedAt as string\n}\n\nasync function deleteWebhook(\n request: APIRequestContext,\n token: string,\n id: string,\n): Promise<void> {\n const current = await readWebhookUpdatedAt(request, token, id).catch(() => undefined)\n await request\n .fetch(resolveApiUrl(`${WEBHOOKS_API}/${id}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(current ? { [OPTIMISTIC_LOCK_HEADER_NAME]: current } : {}),\n },\n })\n .catch(() => undefined)\n}\n\nasync function patchInboxSettings(\n request: APIRequestContext,\n token: string,\n body: Record<string, unknown>,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(INBOX_SETTINGS_API), {\n method: 'PATCH',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: body,\n })\n}\n\nasync function readInboxSettings(\n request: APIRequestContext,\n token: string,\n): Promise<InboxSettings> {\n const response = await apiRequest(request, 'GET', INBOX_SETTINGS_API, { token })\n expect(response.status(), 'GET inbox settings should be 200').toBe(200)\n const body = (await response.json()) as { settings?: InboxSettings | null }\n expect(body.settings, 'tenant inbox settings singleton should exist').toBeTruthy()\n return body.settings as InboxSettings\n}\n\nasync function postSchedule(\n request: APIRequestContext,\n token: string,\n scheduleValue: string,\n integrationId: string,\n lockValue?: string,\n) {\n return request.fetch(resolveApiUrl(SCHEDULES_API), {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n ...(lockValue !== undefined ? { [OPTIMISTIC_LOCK_HEADER_NAME]: lockValue } : {}),\n },\n data: {\n integrationId,\n entityType: 'products',\n direction: 'import',\n scheduleType: 'cron',\n scheduleValue,\n timezone: 'UTC',\n fullSync: false,\n isEnabled: true,\n },\n })\n}\n\ntest.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule optimistic lock', () => {\n test('WHK-01 stale webhook PUT is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n // Advance updated_at out-of-band via a header-less PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Replay the now-stale write \u2192 409.\n const conflict = await putWithLock(\n page.request,\n token,\n `${WEBHOOKS_API}/${webhookId}`,\n { name: `QA Lock 043 stale ${stamp}` },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('WHK-01 stale webhook DELETE is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n const staleUpdatedAt = webhook.updatedAt\n\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { name: `QA Lock 043 bumped ${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n const conflict = await page.request.fetch(resolveApiUrl(`${WEBHOOKS_API}/${webhookId}`), {\n method: 'DELETE',\n headers: {\n Authorization: `Bearer ${token}`,\n 'Content-Type': 'application/json',\n [OPTIMISTIC_LOCK_HEADER_NAME]: staleUpdatedAt,\n },\n })\n await expectConflictBody(conflict)\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n // WHK-01 (browser UI): now ACTIVE. The product fix is committed/verified \u2014 the webhooks\n // server DELETE returns the structured 409 (proven by the active \"WHK-01 stale webhook\n // DELETE is refused with a 409\" API test) and the list page sends the lock header\n // (`buildOptimisticLockHeader(row.updatedAt)`) + routes the resulting 409 through\n // surfaceRecordConflict. The RowActions menu opens on CLICK of the \"Open actions\" kebab and\n // renders the items in a portal on document.body (role=\"menuitem\"); the confirm dialog is a\n // role=\"alertdialog\" with a \"Confirm\" button. We drive that choreography robustly here:\n // wait for the filtered list GET to settle, open the kebab, click Delete, confirm, then\n // assert the unified conflict bar (never the success toast).\n test('WHK-01 stale webhook DELETE in the list surfaces the conflict bar', async ({ page }) => {\n // Heavy browser flow (login + list load + portalled RowActions menu) routinely\n // exceeds Playwright's 20s default on a loaded ephemeral shard; opt into the\n // sanctioned per-test budget (see TC-LOCK-OSS-029). Global bump is disallowed.\n test.setTimeout(60_000)\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n let webhookId: string | null = null\n try {\n const webhook = await createWebhook(page.request, token, stamp)\n webhookId = webhook.id\n\n await login(page, 'admin')\n await page.goto('/backend/webhooks')\n // Wait for the list GET to settle before interacting. The list reflects the\n // freshly created fixture immediately (newest-first), so a unique name makes\n // the row unambiguous.\n await page\n .waitForResponse(\n (response) =>\n response.url().includes('/api/webhooks') &&\n response.request().method() === 'GET' &&\n response.ok(),\n { timeout: 20_000 },\n )\n .catch(() => undefined)\n\n // The out-of-band PUT below renames the fixture to \"QA Lock 043 bumped <stamp>\";\n // tolerate both names so a mid-test list re-render cannot orphan the row locator.\n const row = page\n .getByRole('row', { name: new RegExp(`QA Lock 043 (?:bumped )?${stamp}\\\\b`) })\n .first()\n await expect(row, 'created webhook should appear in the list').toBeVisible({ timeout: 20_000 })\n\n // Let ALL of the list's initial GETs settle (it fires a second fetch once the\n // org scope resolves) before bumping out-of-band. Otherwise a late settle GET\n // can land AFTER the PUT and refresh the in-page row token to the new value,\n // so the subsequent DELETE carries a fresh token and succeeds (200) instead of\n // 409ing \u2014 the historic flake (the list ended up empty, no conflict surfaced).\n await page.waitForLoadState('networkidle')\n\n // Advance updated_at out-of-band \u2192 the in-page row token is now stale.\n // NOTE: bump description (not name) so the row locator stays valid even if\n // the list re-fetches after the PUT.\n const bump = await apiRequest(page.request, 'PUT', `${WEBHOOKS_API}/${webhookId}`, {\n token,\n data: { description: `bumped-${stamp}` },\n })\n expect(bump.status(), 'out-of-band webhook PUT should succeed').toBeLessThan(300)\n\n // Open the row's RowActions kebab (opens on click) and trigger Delete. The\n // menu renders in a portal on document.body, so query it at the page level.\n // The list re-renders as data settles, so the menu can detach between \"open\"\n // and \"click\" \u2014 retry (re)open-menu + click-Delete atomically (same fix as\n // TC-LOCK-OSS-029); the confirm dialog gates the DELETE, so a repeated\n // click is safe.\n const kebab = row.getByRole('button', { name: /open actions/i })\n const deleteItem = page.getByRole('menuitem', { name: /^delete$/i })\n await expect(async () => {\n if (!(await deleteItem.isVisible().catch(() => false))) {\n await kebab.click({ timeout: 2_000 }).catch(() => {})\n await expect(deleteItem).toBeVisible({ timeout: 1_500 })\n }\n await deleteItem.click({ timeout: 2_000 })\n }).toPass({ timeout: 30_000 })\n\n // Confirm the destructive alertdialog (the row still holds the stale\n // updated_at captured at render time, so the DELETE 409s).\n const dialog = page.getByRole('alertdialog')\n await expect(dialog, 'confirm dialog should open').toBeVisible({ timeout: 10_000 })\n await dialog.getByRole('button', { name: /^(confirm|delete)$/i }).click()\n\n // The client must route the 409 to the unified conflict bar, never the\n // success toast. Allow a larger surfacing budget than the 10s default for the\n // portalled RowActions + confirm dialog + guarded-mutation pipeline on a\n // loaded CI shard.\n await expectConflictBanner(page, { timeout: 20_000 })\n } finally {\n if (webhookId) await deleteWebhook(page.request, token, webhookId)\n }\n })\n\n test('INB-01 stale inbox-settings PATCH is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const before = await readInboxSettings(page.request, token)\n const originalLanguage = before.workingLanguage\n const staleUpdatedAt = before.updatedAt\n const bumpLanguage = originalLanguage === 'de' ? 'en' : 'de'\n const staleLanguage = originalLanguage === 'es' ? 'pl' : 'es'\n try {\n // Advance updated_at out-of-band via a header-less PATCH.\n const bump = await patchInboxSettings(page.request, token, { workingLanguage: bumpLanguage })\n expect(bump.status(), 'out-of-band inbox PATCH should succeed').toBeLessThan(300)\n\n // Replay the now-stale write with the original expected-version \u2192 409.\n const conflict = await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: staleLanguage },\n staleUpdatedAt,\n )\n await expectConflictBody(conflict)\n } finally {\n // Restore the original working language with a fresh lock token.\n const current = await readInboxSettings(page.request, token).catch(() => null)\n if (current) {\n await patchInboxSettings(\n page.request,\n token,\n { workingLanguage: originalLanguage },\n current.updatedAt,\n ).catch(() => undefined)\n }\n }\n })\n\n test('SYNC-01 stale data-sync schedule save is refused with a 409 conflict', async ({ page }) => {\n const token = await getAuthToken(page.request, 'admin')\n const stamp = Date.now()\n const integrationId = `qa-lock-043-${stamp}`\n let scheduleId: string | null = null\n try {\n const created = await postSchedule(page.request, token, '0 * * * *', integrationId)\n expect(created.status(), 'POST schedule should be 201').toBe(201)\n const createdBody = (await created.json()) as { id?: string; updatedAt?: string }\n scheduleId = createdBody.id ?? null\n const staleUpdatedAt = createdBody.updatedAt\n expect(typeof scheduleId, 'schedule creation should return an id').toBe('string')\n expect(typeof staleUpdatedAt, 'schedule should expose updatedAt').toBe('string')\n\n // Advance updated_at out-of-band via the header-less [id] PUT route.\n const bump = await apiRequest(page.request, 'PUT', `${SCHEDULES_API}/${scheduleId}`, {\n token,\n data: { scheduleValue: '5 * * * *' },\n })\n expect(bump.status(), 'out-of-band schedule PUT should succeed').toBeLessThan(300)\n\n // Replay the save for the same (integrationId, entityType, direction) key\n // with the now-stale expected-version \u2192 409.\n const conflict = await postSchedule(\n page.request,\n token,\n '9 * * * *',\n integrationId,\n staleUpdatedAt as string,\n )\n await expectConflictBody(conflict)\n } finally {\n if (scheduleId) {\n await apiRequest(page.request, 'DELETE', `${SCHEDULES_API}/${scheduleId}`, { token }).catch(\n () => undefined,\n )\n }\n }\n })\n})\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/webhooks",
3
- "version": "0.6.6-develop.5641.1.45d172106d",
3
+ "version": "0.6.6-develop.5654.1.ca21e35f26",
4
4
  "description": "Webhooks module for Open Mercato — Standard Webhooks compliant outbound/inbound delivery",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -69,19 +69,19 @@
69
69
  }
70
70
  },
71
71
  "dependencies": {
72
- "@open-mercato/core": "0.6.6-develop.5641.1.45d172106d",
73
- "@open-mercato/queue": "0.6.6-develop.5641.1.45d172106d",
74
- "@open-mercato/ui": "0.6.6-develop.5641.1.45d172106d",
72
+ "@open-mercato/core": "0.6.6-develop.5654.1.ca21e35f26",
73
+ "@open-mercato/queue": "0.6.6-develop.5654.1.ca21e35f26",
74
+ "@open-mercato/ui": "0.6.6-develop.5654.1.ca21e35f26",
75
75
  "svix": "^1.95.2"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "@mikro-orm/postgresql": "^7.0.14",
79
- "@open-mercato/shared": "0.6.6-develop.5641.1.45d172106d",
79
+ "@open-mercato/shared": "0.6.6-develop.5654.1.ca21e35f26",
80
80
  "react": "^19.0.0",
81
81
  "react-dom": "^19.0.0"
82
82
  },
83
83
  "devDependencies": {
84
- "@open-mercato/shared": "0.6.6-develop.5641.1.45d172106d",
84
+ "@open-mercato/shared": "0.6.6-develop.5654.1.ca21e35f26",
85
85
  "@types/jest": "^30.0.0",
86
86
  "@types/react": "^19.2.17",
87
87
  "@types/react-dom": "^19.2.3",
@@ -253,6 +253,13 @@ test.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
253
253
  .first()
254
254
  await expect(row, 'created webhook should appear in the list').toBeVisible({ timeout: 20_000 })
255
255
 
256
+ // Let ALL of the list's initial GETs settle (it fires a second fetch once the
257
+ // org scope resolves) before bumping out-of-band. Otherwise a late settle GET
258
+ // can land AFTER the PUT and refresh the in-page row token to the new value,
259
+ // so the subsequent DELETE carries a fresh token and succeeds (200) instead of
260
+ // 409ing — the historic flake (the list ended up empty, no conflict surfaced).
261
+ await page.waitForLoadState('networkidle')
262
+
256
263
  // Advance updated_at out-of-band → the in-page row token is now stale.
257
264
  // NOTE: bump description (not name) so the row locator stays valid even if
258
265
  // the list re-fetches after the PUT.
@@ -285,8 +292,10 @@ test.describe('TC-LOCK-OSS-043: webhooks + inbox settings + data-sync schedule o
285
292
  await dialog.getByRole('button', { name: /^(confirm|delete)$/i }).click()
286
293
 
287
294
  // The client must route the 409 to the unified conflict bar, never the
288
- // success toast.
289
- await expectConflictBanner(page)
295
+ // success toast. Allow a larger surfacing budget than the 10s default for the
296
+ // portalled RowActions + confirm dialog + guarded-mutation pipeline on a
297
+ // loaded CI shard.
298
+ await expectConflictBanner(page, { timeout: 20_000 })
290
299
  } finally {
291
300
  if (webhookId) await deleteWebhook(page.request, token, webhookId)
292
301
  }