@open-mercato/ai-assistant 0.10.1-develop.7657.1.036a08ee9f → 0.10.1-develop.7660.1.c3cfc9211c

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.
Files changed (33) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-STALE-010-bulk-confirm-stale-rows.spec.js +312 -0
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-ACTIONS-STALE-010-bulk-confirm-stale-rows.spec.js.map +7 -0
  4. package/dist/modules/ai_assistant/i18n/de.json +8 -0
  5. package/dist/modules/ai_assistant/i18n/en.json +8 -0
  6. package/dist/modules/ai_assistant/i18n/es.json +8 -0
  7. package/dist/modules/ai_assistant/i18n/ko.json +8 -0
  8. package/dist/modules/ai_assistant/i18n/pl.json +8 -0
  9. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js +17 -0
  10. package/dist/modules/ai_assistant/lib/ai-api-operation-runner.js.map +2 -2
  11. package/dist/modules/ai_assistant/lib/pending-action-batch.js +145 -0
  12. package/dist/modules/ai_assistant/lib/pending-action-batch.js.map +7 -0
  13. package/dist/modules/ai_assistant/lib/pending-action-executor.js +41 -6
  14. package/dist/modules/ai_assistant/lib/pending-action-executor.js.map +2 -2
  15. package/dist/modules/ai_assistant/lib/pending-action-recheck.js +22 -4
  16. package/dist/modules/ai_assistant/lib/pending-action-recheck.js.map +3 -3
  17. package/package.json +6 -6
  18. package/src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-STALE-010-bulk-confirm-stale-rows.spec.ts +349 -0
  19. package/src/modules/ai_assistant/__tests__/integration/pending-action-contract.test.ts +22 -5
  20. package/src/modules/ai_assistant/i18n/de.json +8 -0
  21. package/src/modules/ai_assistant/i18n/en.json +8 -0
  22. package/src/modules/ai_assistant/i18n/es.json +8 -0
  23. package/src/modules/ai_assistant/i18n/ko.json +8 -0
  24. package/src/modules/ai_assistant/i18n/pl.json +8 -0
  25. package/src/modules/ai_assistant/lib/__tests__/ai-api-operation-runner-optimistic-lock.test.ts +214 -0
  26. package/src/modules/ai_assistant/lib/__tests__/pending-action-executor-stale-batch.test.ts +542 -0
  27. package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck-batch.test.ts +158 -0
  28. package/src/modules/ai_assistant/lib/__tests__/pending-action-recheck-i18n.test.ts +146 -0
  29. package/src/modules/ai_assistant/lib/ai-api-operation-runner.ts +28 -0
  30. package/src/modules/ai_assistant/lib/pending-action-batch.ts +217 -0
  31. package/src/modules/ai_assistant/lib/pending-action-executor.ts +48 -4
  32. package/src/modules/ai_assistant/lib/pending-action-recheck.ts +27 -5
  33. package/src/modules/ai_assistant/lib/types.ts +10 -0
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 237 entry points
1
+ [build:ai-assistant] found 239 entry points
2
2
  [build:ai-assistant] built successfully
@@ -0,0 +1,312 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { randomUUID } from "node:crypto";
3
+ import { apiRequest, getAuthToken } from "@open-mercato/core/helpers/integration/api";
4
+ import { withClient } from "@open-mercato/core/helpers/integration/dbFixtures";
5
+ import { createProductFixture, deleteCatalogProductIfExists } from "@open-mercato/core/helpers/integration/catalogFixtures";
6
+ import { getTokenScope, readJsonSafe } from "@open-mercato/core/helpers/integration/generalFixtures";
7
+ import { seedPendingActionInDb, deletePendingActionInDb } from "./helpers/aiAssistantFixtures.js";
8
+ const ACTIONS = "/api/ai_assistant/ai/actions";
9
+ const AGENT_ID = "catalog.merchandising_assistant";
10
+ const BULK_TOOL = "catalog.bulk_update_products";
11
+ async function readProductState(productId) {
12
+ return withClient(async (client) => {
13
+ const result = await client.query(
14
+ "select title, updated_at from catalog_products where id = $1",
15
+ [productId]
16
+ );
17
+ const row = result.rows[0];
18
+ if (!row) throw new Error(`[internal] product ${productId} not found`);
19
+ return { title: row.title, version: new Date(row.updated_at).toISOString() };
20
+ });
21
+ }
22
+ async function attachPreviewRecords(pendingActionId, records) {
23
+ const diffs = records.map((entry) => ({
24
+ recordId: entry.recordId,
25
+ entityType: "catalog.product",
26
+ label: entry.before,
27
+ fieldDiff: [{ field: "title", before: entry.before, after: entry.title }],
28
+ recordVersion: entry.recordVersion
29
+ }));
30
+ await withClient(async (client) => {
31
+ await client.query("update ai_pending_actions set records = $2::jsonb, target_entity_type = $3 where id = $1", [
32
+ pendingActionId,
33
+ JSON.stringify(diffs),
34
+ "catalog.product"
35
+ ]);
36
+ });
37
+ }
38
+ async function createProduct(request, token, label, cleanups) {
39
+ const stamp = randomUUID().slice(0, 8);
40
+ const productId = await createProductFixture(request, token, {
41
+ title: `IT stale ${label} ${stamp}`,
42
+ sku: `IT-STALE-${label}-${stamp}`
43
+ });
44
+ cleanups.push(() => deleteCatalogProductIfExists(request, token, productId));
45
+ return productId;
46
+ }
47
+ test.describe("TC-AI-ACTIONS-STALE-010: bulk confirm never writes rows the recheck marked stale", () => {
48
+ test("a product edited after preview keeps the human edit; the fresh product gets the AI edit", async ({ request }) => {
49
+ test.slow();
50
+ const adminToken = await getAuthToken(request, "admin");
51
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
52
+ const cleanups = [];
53
+ try {
54
+ const productA = await createProduct(request, adminToken, "A", cleanups);
55
+ const productB = await createProduct(request, adminToken, "B", cleanups);
56
+ const previewA = await readProductState(productA);
57
+ const previewB = await readProductState(productB);
58
+ const aiTitleA = `AI A ${randomUUID().slice(0, 8)}`;
59
+ const aiTitleB = `AI B ${randomUUID().slice(0, 8)}`;
60
+ const row = await seedPendingActionInDb({
61
+ tenantId,
62
+ organizationId: organizationId || null,
63
+ createdByUserId: adminId,
64
+ agentId: AGENT_ID,
65
+ toolName: BULK_TOOL,
66
+ normalizedInput: {
67
+ records: [
68
+ { recordId: productA, title: aiTitleA },
69
+ { recordId: productB, title: aiTitleB }
70
+ ]
71
+ }
72
+ });
73
+ cleanups.push(() => deletePendingActionInDb(row.id));
74
+ await attachPreviewRecords(row.id, [
75
+ { recordId: productA, recordVersion: previewA.version, title: aiTitleA, before: previewA.title },
76
+ { recordId: productB, recordVersion: previewB.version, title: aiTitleB, before: previewB.title }
77
+ ]);
78
+ const humanTitleB = `Human B ${randomUUID().slice(0, 8)}`;
79
+ const humanEdit = await apiRequest(request, "PUT", "/api/catalog/products", {
80
+ token: adminToken,
81
+ data: { id: productB, title: humanTitleB }
82
+ });
83
+ expect(humanEdit.ok(), `human edit of B should succeed: ${humanEdit.status()}`).toBeTruthy();
84
+ const afterHumanB = await readProductState(productB);
85
+ expect(afterHumanB.version, "the human edit must move B past its preview version").not.toBe(previewB.version);
86
+ const confirm = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/confirm`, { token: adminToken, data: {} });
87
+ expect(confirm.status()).toBe(200);
88
+ const body = await readJsonSafe(confirm);
89
+ expect(body?.ok).toBe(true);
90
+ expect(body?.pendingAction?.status).toBe("confirmed");
91
+ const failed = body?.pendingAction?.failedRecords ?? [];
92
+ expect(failed.map((entry) => entry.recordId)).toEqual([productB]);
93
+ expect(failed[0]?.error.code).toBe("stale_version");
94
+ expect((await readProductState(productA)).title, "fresh row A is written").toBe(aiTitleA);
95
+ const finalB = await readProductState(productB);
96
+ expect(finalB.title, "stale row B keeps the human edit").toBe(humanTitleB);
97
+ expect(finalB.version, "stale row B is not written at all").toBe(afterHumanB.version);
98
+ } finally {
99
+ for (const cleanup of cleanups.reverse()) {
100
+ await cleanup().catch(() => void 0);
101
+ }
102
+ }
103
+ });
104
+ test("a repeated record runs only its previewed entry and reports the dropped repeat; the unseen entry is never written", async ({ request }) => {
105
+ test.slow();
106
+ const adminToken = await getAuthToken(request, "admin");
107
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
108
+ const cleanups = [];
109
+ try {
110
+ const productC = await createProduct(request, adminToken, "C", cleanups);
111
+ const previewC = await readProductState(productC);
112
+ const approvedTitle = `AI C approved ${randomUUID().slice(0, 8)}`;
113
+ const unseenTitle = `AI C unseen ${randomUUID().slice(0, 8)}`;
114
+ const row = await seedPendingActionInDb({
115
+ tenantId,
116
+ organizationId: organizationId || null,
117
+ createdByUserId: adminId,
118
+ agentId: AGENT_ID,
119
+ toolName: BULK_TOOL,
120
+ normalizedInput: {
121
+ records: [
122
+ { recordId: productC, title: approvedTitle },
123
+ { recordId: productC, title: unseenTitle }
124
+ ]
125
+ }
126
+ });
127
+ cleanups.push(() => deletePendingActionInDb(row.id));
128
+ await attachPreviewRecords(row.id, [
129
+ { recordId: productC, recordVersion: previewC.version, title: approvedTitle, before: previewC.title }
130
+ ]);
131
+ const confirm = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/confirm`, { token: adminToken, data: {} });
132
+ expect(confirm.status()).toBe(200);
133
+ const body = await readJsonSafe(confirm);
134
+ expect(body?.pendingAction?.status).toBe("confirmed");
135
+ const failed = body?.pendingAction?.failedRecords ?? [];
136
+ expect(failed.map((entry) => [entry.recordId, entry.error.code])).toEqual([[productC, "record_duplicate_dropped"]]);
137
+ expect((await readProductState(productC)).title, "only the previewed entry is applied").toBe(approvedTitle);
138
+ } finally {
139
+ for (const cleanup of cleanups.reverse()) {
140
+ await cleanup().catch(() => void 0);
141
+ }
142
+ }
143
+ });
144
+ test("an input row whose id does not exactly match a previewed row is reported and not written", async ({ request }) => {
145
+ test.slow();
146
+ const adminToken = await getAuthToken(request, "admin");
147
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
148
+ const cleanups = [];
149
+ try {
150
+ const productD = await createProduct(request, adminToken, "D", cleanups);
151
+ const productE = await createProduct(request, adminToken, "E", cleanups);
152
+ const previewD = await readProductState(productD);
153
+ const previewE = await readProductState(productE);
154
+ const aiTitleD = `AI D ${randomUUID().slice(0, 8)}`;
155
+ const aiTitleE = `AI E ${randomUUID().slice(0, 8)}`;
156
+ const upperE = productE.toUpperCase();
157
+ expect(upperE, "the fixture id must contain letters").not.toBe(productE);
158
+ const row = await seedPendingActionInDb({
159
+ tenantId,
160
+ organizationId: organizationId || null,
161
+ createdByUserId: adminId,
162
+ agentId: AGENT_ID,
163
+ toolName: BULK_TOOL,
164
+ normalizedInput: {
165
+ records: [
166
+ { recordId: productD, title: aiTitleD },
167
+ { recordId: upperE, title: aiTitleE }
168
+ ]
169
+ }
170
+ });
171
+ cleanups.push(() => deletePendingActionInDb(row.id));
172
+ await attachPreviewRecords(row.id, [
173
+ { recordId: productD, recordVersion: previewD.version, title: aiTitleD, before: previewD.title },
174
+ { recordId: productE, recordVersion: previewE.version, title: previewE.title, before: previewE.title }
175
+ ]);
176
+ const confirm = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/confirm`, { token: adminToken, data: {} });
177
+ expect(confirm.status()).toBe(200);
178
+ const body = await readJsonSafe(confirm);
179
+ expect(body?.pendingAction?.status).toBe("confirmed");
180
+ const failed = body?.pendingAction?.failedRecords ?? [];
181
+ expect(failed.map((entry) => [entry.recordId, entry.error.code])).toEqual([[upperE, "record_not_previewed"]]);
182
+ expect((await readProductState(productD)).title).toBe(aiTitleD);
183
+ const finalE = await readProductState(productE);
184
+ expect(finalE.title, "the row previewed without a change keeps its title").toBe(previewE.title);
185
+ expect(finalE.version).toBe(previewE.version);
186
+ } finally {
187
+ for (const cleanup of cleanups.reverse()) {
188
+ await cleanup().catch(() => void 0);
189
+ }
190
+ }
191
+ });
192
+ test("in the Polish UI a partially stale batch reports its stale row in Polish", async ({ request }) => {
193
+ test.slow();
194
+ const adminToken = await getAuthToken(request, "admin");
195
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
196
+ const cleanups = [];
197
+ try {
198
+ const productF = await createProduct(request, adminToken, "F", cleanups);
199
+ const productG = await createProduct(request, adminToken, "G", cleanups);
200
+ const previewF = await readProductState(productF);
201
+ const previewG = await readProductState(productG);
202
+ const aiTitleF = `AI F ${randomUUID().slice(0, 8)}`;
203
+ const aiTitleG = `AI G ${randomUUID().slice(0, 8)}`;
204
+ const row = await seedPendingActionInDb({
205
+ tenantId,
206
+ organizationId: organizationId || null,
207
+ createdByUserId: adminId,
208
+ agentId: AGENT_ID,
209
+ toolName: BULK_TOOL,
210
+ normalizedInput: {
211
+ records: [
212
+ { recordId: productF, title: aiTitleF },
213
+ { recordId: productG, title: aiTitleG }
214
+ ]
215
+ }
216
+ });
217
+ cleanups.push(() => deletePendingActionInDb(row.id));
218
+ await attachPreviewRecords(row.id, [
219
+ { recordId: productF, recordVersion: previewF.version, title: aiTitleF, before: previewF.title },
220
+ { recordId: productG, recordVersion: previewG.version, title: aiTitleG, before: previewG.title }
221
+ ]);
222
+ const humanTitleG = `Human G ${randomUUID().slice(0, 8)}`;
223
+ const humanEdit = await apiRequest(request, "PUT", "/api/catalog/products", {
224
+ token: adminToken,
225
+ data: { id: productG, title: humanTitleG }
226
+ });
227
+ expect(humanEdit.ok(), `human edit of G should succeed: ${humanEdit.status()}`).toBeTruthy();
228
+ const confirm = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/confirm`, {
229
+ token: adminToken,
230
+ data: {},
231
+ headers: { Cookie: "locale=pl" }
232
+ });
233
+ expect(confirm.status()).toBe(200);
234
+ const body = await readJsonSafe(confirm);
235
+ const failed = body?.pendingAction?.failedRecords ?? [];
236
+ expect(failed.map((entry) => [entry.recordId, entry.error.code, entry.error.message])).toEqual([
237
+ [productG, "stale_version", "Rekord zmieni\u0142 si\u0119 od czasu podgl\u0105du."]
238
+ ]);
239
+ expect((await readProductState(productF)).title).toBe(aiTitleF);
240
+ expect((await readProductState(productG)).title).toBe(humanTitleG);
241
+ } finally {
242
+ for (const cleanup of cleanups.reverse()) {
243
+ await cleanup().catch(() => void 0);
244
+ }
245
+ }
246
+ });
247
+ test("when every previewed row changed, the confirm is refused with 412 listing both rows and nothing is written", async ({ request }) => {
248
+ test.slow();
249
+ const adminToken = await getAuthToken(request, "admin");
250
+ const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);
251
+ const cleanups = [];
252
+ try {
253
+ const productH = await createProduct(request, adminToken, "H", cleanups);
254
+ const productI = await createProduct(request, adminToken, "I", cleanups);
255
+ const previewH = await readProductState(productH);
256
+ const previewI = await readProductState(productI);
257
+ const row = await seedPendingActionInDb({
258
+ tenantId,
259
+ organizationId: organizationId || null,
260
+ createdByUserId: adminId,
261
+ agentId: AGENT_ID,
262
+ toolName: BULK_TOOL,
263
+ normalizedInput: {
264
+ records: [
265
+ { recordId: productH, title: `AI H ${randomUUID().slice(0, 8)}` },
266
+ { recordId: productI, title: `AI I ${randomUUID().slice(0, 8)}` }
267
+ ]
268
+ }
269
+ });
270
+ cleanups.push(() => deletePendingActionInDb(row.id));
271
+ await attachPreviewRecords(row.id, [
272
+ { recordId: productH, recordVersion: previewH.version, title: "AI H", before: previewH.title },
273
+ { recordId: productI, recordVersion: previewI.version, title: "AI I", before: previewI.title }
274
+ ]);
275
+ const humanTitles = /* @__PURE__ */ new Map();
276
+ for (const productId of [productH, productI]) {
277
+ const humanTitle = `Human ${randomUUID().slice(0, 8)}`;
278
+ humanTitles.set(productId, humanTitle);
279
+ const humanEdit = await apiRequest(request, "PUT", "/api/catalog/products", {
280
+ token: adminToken,
281
+ data: { id: productId, title: humanTitle }
282
+ });
283
+ expect(humanEdit.ok(), `human edit should succeed: ${humanEdit.status()}`).toBeTruthy();
284
+ }
285
+ const confirm = await apiRequest(request, "POST", `${ACTIONS}/${row.id}/confirm`, {
286
+ token: adminToken,
287
+ data: {},
288
+ headers: { Cookie: "locale=pl" }
289
+ });
290
+ expect(confirm.status()).toBe(412);
291
+ const body = await readJsonSafe(confirm);
292
+ expect(body?.code).toBe("stale_version");
293
+ expect([...body?.staleRecords ?? []].sort()).toEqual([productH, productI].sort());
294
+ expect(body?.error).toBe(
295
+ "Wszystkie rekordy oczekuj\u0105cej akcji s\u0105 nieaktualne; asystent musi ponownie zaproponowa\u0107 zmian\u0119."
296
+ );
297
+ for (const productId of [productH, productI]) {
298
+ expect((await readProductState(productId)).title, "a refused confirm writes nothing").toBe(humanTitles.get(productId));
299
+ }
300
+ const status = await withClient(async (client) => {
301
+ const result = await client.query("select status from ai_pending_actions where id = $1", [row.id]);
302
+ return result.rows[0]?.status;
303
+ });
304
+ expect(status, "a refused confirm leaves the action pending").toBe("pending");
305
+ } finally {
306
+ for (const cleanup of cleanups.reverse()) {
307
+ await cleanup().catch(() => void 0);
308
+ }
309
+ }
310
+ });
311
+ });
312
+ //# sourceMappingURL=TC-AI-ACTIONS-STALE-010-bulk-confirm-stale-rows.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-ACTIONS-STALE-010-bulk-confirm-stale-rows.spec.ts"],
4
+ "sourcesContent": ["import { test, expect, type APIRequestContext } from '@playwright/test';\nimport { randomUUID } from 'node:crypto';\nimport { apiRequest, getAuthToken } from '@open-mercato/core/helpers/integration/api';\nimport { withClient } from '@open-mercato/core/helpers/integration/dbFixtures';\nimport { createProductFixture, deleteCatalogProductIfExists } from '@open-mercato/core/helpers/integration/catalogFixtures';\nimport { getTokenScope, readJsonSafe } from '@open-mercato/core/helpers/integration/generalFixtures';\nimport { seedPendingActionInDb, deletePendingActionInDb } from './helpers/aiAssistantFixtures';\n\nconst ACTIONS = '/api/ai_assistant/ai/actions';\nconst AGENT_ID = 'catalog.merchandising_assistant';\nconst BULK_TOOL = 'catalog.bulk_update_products';\n\ntype Cleanup = () => Promise<void>;\n\ntype ProductState = { title: string; version: string };\n\ntype ConfirmBody = {\n ok?: boolean;\n pendingAction?: {\n status?: string;\n failedRecords?: Array<{ recordId: string; error: { code: string; message: string } }> | null;\n };\n};\n\nasync function readProductState(productId: string): Promise<ProductState> {\n return withClient(async (client) => {\n const result = await client.query<{ title: string; updated_at: Date }>(\n 'select title, updated_at from catalog_products where id = $1',\n [productId],\n );\n const row = result.rows[0];\n if (!row) throw new Error(`[internal] product ${productId} not found`);\n return { title: row.title, version: new Date(row.updated_at).toISOString() };\n });\n}\n\nasync function attachPreviewRecords(\n pendingActionId: string,\n records: Array<{ recordId: string; recordVersion: string; title: string; before: string }>,\n): Promise<void> {\n const diffs = records.map((entry) => ({\n recordId: entry.recordId,\n entityType: 'catalog.product',\n label: entry.before,\n fieldDiff: [{ field: 'title', before: entry.before, after: entry.title }],\n recordVersion: entry.recordVersion,\n }));\n await withClient(async (client) => {\n await client.query('update ai_pending_actions set records = $2::jsonb, target_entity_type = $3 where id = $1', [\n pendingActionId,\n JSON.stringify(diffs),\n 'catalog.product',\n ]);\n });\n}\n\nasync function createProduct(request: APIRequestContext, token: string, label: string, cleanups: Cleanup[]): Promise<string> {\n const stamp = randomUUID().slice(0, 8);\n const productId = await createProductFixture(request, token, {\n title: `IT stale ${label} ${stamp}`,\n sku: `IT-STALE-${label}-${stamp}`,\n });\n cleanups.push(() => deleteCatalogProductIfExists(request, token, productId));\n return productId;\n}\n\ntest.describe('TC-AI-ACTIONS-STALE-010: bulk confirm never writes rows the recheck marked stale', () => {\n test('a product edited after preview keeps the human edit; the fresh product gets the AI edit', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productA = await createProduct(request, adminToken, 'A', cleanups);\n const productB = await createProduct(request, adminToken, 'B', cleanups);\n const previewA = await readProductState(productA);\n const previewB = await readProductState(productB);\n const aiTitleA = `AI A ${randomUUID().slice(0, 8)}`;\n const aiTitleB = `AI B ${randomUUID().slice(0, 8)}`;\n\n const row = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: BULK_TOOL,\n normalizedInput: {\n records: [\n { recordId: productA, title: aiTitleA },\n { recordId: productB, title: aiTitleB },\n ],\n },\n });\n cleanups.push(() => deletePendingActionInDb(row.id));\n await attachPreviewRecords(row.id, [\n { recordId: productA, recordVersion: previewA.version, title: aiTitleA, before: previewA.title },\n { recordId: productB, recordVersion: previewB.version, title: aiTitleB, before: previewB.title },\n ]);\n\n const humanTitleB = `Human B ${randomUUID().slice(0, 8)}`;\n const humanEdit = await apiRequest(request, 'PUT', '/api/catalog/products', {\n token: adminToken,\n data: { id: productB, title: humanTitleB },\n });\n expect(humanEdit.ok(), `human edit of B should succeed: ${humanEdit.status()}`).toBeTruthy();\n const afterHumanB = await readProductState(productB);\n expect(afterHumanB.version, 'the human edit must move B past its preview version').not.toBe(previewB.version);\n\n const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/confirm`, { token: adminToken, data: {} });\n expect(confirm.status()).toBe(200);\n const body = await readJsonSafe<ConfirmBody>(confirm);\n expect(body?.ok).toBe(true);\n expect(body?.pendingAction?.status).toBe('confirmed');\n const failed = body?.pendingAction?.failedRecords ?? [];\n expect(failed.map((entry) => entry.recordId)).toEqual([productB]);\n expect(failed[0]?.error.code).toBe('stale_version');\n\n expect((await readProductState(productA)).title, 'fresh row A is written').toBe(aiTitleA);\n const finalB = await readProductState(productB);\n expect(finalB.title, 'stale row B keeps the human edit').toBe(humanTitleB);\n expect(finalB.version, 'stale row B is not written at all').toBe(afterHumanB.version);\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n\n test('a repeated record runs only its previewed entry and reports the dropped repeat; the unseen entry is never written', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productC = await createProduct(request, adminToken, 'C', cleanups);\n const previewC = await readProductState(productC);\n const approvedTitle = `AI C approved ${randomUUID().slice(0, 8)}`;\n const unseenTitle = `AI C unseen ${randomUUID().slice(0, 8)}`;\n\n const row = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: BULK_TOOL,\n normalizedInput: {\n records: [\n { recordId: productC, title: approvedTitle },\n { recordId: productC, title: unseenTitle },\n ],\n },\n });\n cleanups.push(() => deletePendingActionInDb(row.id));\n await attachPreviewRecords(row.id, [\n { recordId: productC, recordVersion: previewC.version, title: approvedTitle, before: previewC.title },\n ]);\n\n const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/confirm`, { token: adminToken, data: {} });\n expect(confirm.status()).toBe(200);\n const body = await readJsonSafe<ConfirmBody>(confirm);\n expect(body?.pendingAction?.status).toBe('confirmed');\n const failed = body?.pendingAction?.failedRecords ?? [];\n expect(failed.map((entry) => [entry.recordId, entry.error.code])).toEqual([[productC, 'record_duplicate_dropped']]);\n expect((await readProductState(productC)).title, 'only the previewed entry is applied').toBe(approvedTitle);\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n\n test('an input row whose id does not exactly match a previewed row is reported and not written', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productD = await createProduct(request, adminToken, 'D', cleanups);\n const productE = await createProduct(request, adminToken, 'E', cleanups);\n const previewD = await readProductState(productD);\n const previewE = await readProductState(productE);\n const aiTitleD = `AI D ${randomUUID().slice(0, 8)}`;\n const aiTitleE = `AI E ${randomUUID().slice(0, 8)}`;\n const upperE = productE.toUpperCase();\n expect(upperE, 'the fixture id must contain letters').not.toBe(productE);\n\n const row = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: BULK_TOOL,\n normalizedInput: {\n records: [\n { recordId: productD, title: aiTitleD },\n { recordId: upperE, title: aiTitleE },\n ],\n },\n });\n cleanups.push(() => deletePendingActionInDb(row.id));\n await attachPreviewRecords(row.id, [\n { recordId: productD, recordVersion: previewD.version, title: aiTitleD, before: previewD.title },\n { recordId: productE, recordVersion: previewE.version, title: previewE.title, before: previewE.title },\n ]);\n\n const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/confirm`, { token: adminToken, data: {} });\n expect(confirm.status()).toBe(200);\n const body = await readJsonSafe<ConfirmBody>(confirm);\n expect(body?.pendingAction?.status).toBe('confirmed');\n const failed = body?.pendingAction?.failedRecords ?? [];\n expect(failed.map((entry) => [entry.recordId, entry.error.code])).toEqual([[upperE, 'record_not_previewed']]);\n expect((await readProductState(productD)).title).toBe(aiTitleD);\n const finalE = await readProductState(productE);\n expect(finalE.title, 'the row previewed without a change keeps its title').toBe(previewE.title);\n expect(finalE.version).toBe(previewE.version);\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n\n test('in the Polish UI a partially stale batch reports its stale row in Polish', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productF = await createProduct(request, adminToken, 'F', cleanups);\n const productG = await createProduct(request, adminToken, 'G', cleanups);\n const previewF = await readProductState(productF);\n const previewG = await readProductState(productG);\n const aiTitleF = `AI F ${randomUUID().slice(0, 8)}`;\n const aiTitleG = `AI G ${randomUUID().slice(0, 8)}`;\n\n const row = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: BULK_TOOL,\n normalizedInput: {\n records: [\n { recordId: productF, title: aiTitleF },\n { recordId: productG, title: aiTitleG },\n ],\n },\n });\n cleanups.push(() => deletePendingActionInDb(row.id));\n await attachPreviewRecords(row.id, [\n { recordId: productF, recordVersion: previewF.version, title: aiTitleF, before: previewF.title },\n { recordId: productG, recordVersion: previewG.version, title: aiTitleG, before: previewG.title },\n ]);\n\n const humanTitleG = `Human G ${randomUUID().slice(0, 8)}`;\n const humanEdit = await apiRequest(request, 'PUT', '/api/catalog/products', {\n token: adminToken,\n data: { id: productG, title: humanTitleG },\n });\n expect(humanEdit.ok(), `human edit of G should succeed: ${humanEdit.status()}`).toBeTruthy();\n\n const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/confirm`, {\n token: adminToken,\n data: {},\n headers: { Cookie: 'locale=pl' },\n });\n expect(confirm.status()).toBe(200);\n const body = await readJsonSafe<ConfirmBody>(confirm);\n const failed = body?.pendingAction?.failedRecords ?? [];\n expect(failed.map((entry) => [entry.recordId, entry.error.code, entry.error.message])).toEqual([\n [productG, 'stale_version', 'Rekord zmieni\u0142 si\u0119 od czasu podgl\u0105du.'],\n ]);\n expect((await readProductState(productF)).title).toBe(aiTitleF);\n expect((await readProductState(productG)).title).toBe(humanTitleG);\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n\n test('when every previewed row changed, the confirm is refused with 412 listing both rows and nothing is written', async ({ request }) => {\n test.slow();\n const adminToken = await getAuthToken(request, 'admin');\n const { tenantId, organizationId, userId: adminId } = getTokenScope(adminToken);\n const cleanups: Cleanup[] = [];\n try {\n const productH = await createProduct(request, adminToken, 'H', cleanups);\n const productI = await createProduct(request, adminToken, 'I', cleanups);\n const previewH = await readProductState(productH);\n const previewI = await readProductState(productI);\n\n const row = await seedPendingActionInDb({\n tenantId,\n organizationId: organizationId || null,\n createdByUserId: adminId,\n agentId: AGENT_ID,\n toolName: BULK_TOOL,\n normalizedInput: {\n records: [\n { recordId: productH, title: `AI H ${randomUUID().slice(0, 8)}` },\n { recordId: productI, title: `AI I ${randomUUID().slice(0, 8)}` },\n ],\n },\n });\n cleanups.push(() => deletePendingActionInDb(row.id));\n await attachPreviewRecords(row.id, [\n { recordId: productH, recordVersion: previewH.version, title: 'AI H', before: previewH.title },\n { recordId: productI, recordVersion: previewI.version, title: 'AI I', before: previewI.title },\n ]);\n\n const humanTitles = new Map<string, string>();\n for (const productId of [productH, productI]) {\n const humanTitle = `Human ${randomUUID().slice(0, 8)}`;\n humanTitles.set(productId, humanTitle);\n const humanEdit = await apiRequest(request, 'PUT', '/api/catalog/products', {\n token: adminToken,\n data: { id: productId, title: humanTitle },\n });\n expect(humanEdit.ok(), `human edit should succeed: ${humanEdit.status()}`).toBeTruthy();\n }\n\n const confirm = await apiRequest(request, 'POST', `${ACTIONS}/${row.id}/confirm`, {\n token: adminToken,\n data: {},\n headers: { Cookie: 'locale=pl' },\n });\n expect(confirm.status()).toBe(412);\n const body = await readJsonSafe<{ code?: string; error?: string; staleRecords?: string[] }>(confirm);\n expect(body?.code).toBe('stale_version');\n expect([...(body?.staleRecords ?? [])].sort()).toEqual([productH, productI].sort());\n expect(body?.error).toBe(\n 'Wszystkie rekordy oczekuj\u0105cej akcji s\u0105 nieaktualne; asystent musi ponownie zaproponowa\u0107 zmian\u0119.',\n );\n for (const productId of [productH, productI]) {\n expect((await readProductState(productId)).title, 'a refused confirm writes nothing').toBe(humanTitles.get(productId));\n }\n const status = await withClient(async (client) => {\n const result = await client.query<{ status: string }>('select status from ai_pending_actions where id = $1', [row.id]);\n return result.rows[0]?.status;\n });\n expect(status, 'a refused confirm leaves the action pending').toBe('pending');\n } finally {\n for (const cleanup of cleanups.reverse()) {\n await cleanup().catch(() => undefined);\n }\n }\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -234,6 +234,7 @@
234
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "Die Änderung konnte nicht angewendet werden.",
235
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Aktion fehlgeschlagen",
236
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Mit KI korrigieren",
237
+ "ai_assistant.chat.mutation_cards.result.ignoredEntriesBody": "Die Änderung wurde gespeichert. Einige wiederholte Einträge in der Anfrage wurden ignoriert:",
237
238
  "ai_assistant.chat.mutation_cards.result.partialBody": "Einige Datensätze konnten nicht aktualisiert werden.",
238
239
  "ai_assistant.chat.mutation_cards.result.partialTitle": "Aktion mit Fehlern angewendet",
239
240
  "ai_assistant.chat.mutation_cards.result.successBody": "Die Änderung wurde erfolgreich abgeschlossen.",
@@ -313,6 +314,13 @@
313
314
  "ai_assistant.dock.right": "Rechts andocken",
314
315
  "ai_assistant.errors.moderationBlocked": "Ihre Nachricht wurde vom Inhaltssicherheitsfilter blockiert. Bitte formulieren Sie sie um und versuchen Sie es erneut.",
315
316
  "ai_assistant.errors.moderationUnavailable": "Die Inhaltssicherheitsprüfung ist vorübergehend nicht verfügbar. Bitte versuchen Sie es in einem Moment erneut.",
317
+ "ai_assistant.errors.pendingAllRecordsStale": "Alle ausstehenden Datensätze sind veraltet; der Assistent muss die Änderung neu vorschlagen.",
318
+ "ai_assistant.errors.pendingBatchNotApplicable": "Dieser Stapel entspricht nicht mehr der genehmigten Vorschau und kann daher nicht angewendet werden. Bitte den Assistenten, die Änderung erneut vorzuschlagen.",
319
+ "ai_assistant.errors.pendingRecordDuplicateDropped": "Dieser Datensatz wurde mehrfach aufgeführt. Nur die erste, in der Vorschau angezeigte Änderung wurde angewendet; die wiederholten Einträge wurden ignoriert.",
320
+ "ai_assistant.errors.pendingRecordMissing": "Der Datensatz existiert nicht mehr.",
321
+ "ai_assistant.errors.pendingRecordNotPreviewed": "Dieser Datensatz war nicht Teil der genehmigten Vorschau und wurde daher nicht geändert.",
322
+ "ai_assistant.errors.pendingRecordStale": "Die Datensatzversion hat sich seit der Vorschau geändert.",
323
+ "ai_assistant.errors.pendingSingleRecordStale": "Die Datensatzversion hat sich seit der Vorschau geändert; schlagen Sie die Änderung neu vor.",
316
324
  "ai_assistant.launcher.composerPlaceholder": "Fragen Sie mich etwas…",
317
325
  "ai_assistant.launcher.dialogDescription": "Wähle einen Assistenten. Mit ↑/↓ navigieren, Enter startet, Esc schließt.",
318
326
  "ai_assistant.launcher.dialogTitle": "AI assistants",
@@ -234,6 +234,7 @@
234
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "The mutation could not be applied.",
235
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Action failed",
236
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Fix with AI",
237
+ "ai_assistant.chat.mutation_cards.result.ignoredEntriesBody": "The change was saved. Some repeated entries in the request were ignored:",
237
238
  "ai_assistant.chat.mutation_cards.result.partialBody": "Some records could not be updated.",
238
239
  "ai_assistant.chat.mutation_cards.result.partialTitle": "Action applied with failures",
239
240
  "ai_assistant.chat.mutation_cards.result.successBody": "The mutation completed successfully.",
@@ -313,6 +314,13 @@
313
314
  "ai_assistant.dock.right": "Dock Right",
314
315
  "ai_assistant.errors.moderationBlocked": "Your message was blocked by the content safety filter. Please rephrase and try again.",
315
316
  "ai_assistant.errors.moderationUnavailable": "The content safety check is temporarily unavailable. Please try again in a moment.",
317
+ "ai_assistant.errors.pendingAllRecordsStale": "All pending records are stale; the model must re-propose the batch.",
318
+ "ai_assistant.errors.pendingBatchNotApplicable": "This batch no longer matches the approved preview, so it cannot be applied. Ask the assistant to re-propose the change.",
319
+ "ai_assistant.errors.pendingRecordDuplicateDropped": "This record was listed more than once. Only its first, previewed change was applied; the repeated entries were ignored.",
320
+ "ai_assistant.errors.pendingRecordMissing": "Record no longer exists.",
321
+ "ai_assistant.errors.pendingRecordNotPreviewed": "This record was not part of the approved preview, so it was not changed.",
322
+ "ai_assistant.errors.pendingRecordStale": "Record version changed since preview.",
323
+ "ai_assistant.errors.pendingSingleRecordStale": "Record version changed since preview; re-propose the mutation.",
316
324
  "ai_assistant.launcher.composerPlaceholder": "Ask anything…",
317
325
  "ai_assistant.launcher.dialogDescription": "Pick an assistant. Use ↑/↓ to navigate, Enter to launch, Esc to close.",
318
326
  "ai_assistant.launcher.dialogTitle": "AI assistants",
@@ -234,6 +234,7 @@
234
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "No se pudo aplicar la mutación.",
235
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "La acción falló",
236
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Corregir con IA",
237
+ "ai_assistant.chat.mutation_cards.result.ignoredEntriesBody": "El cambio se guardó. Se ignoraron algunas entradas repetidas de la solicitud:",
237
238
  "ai_assistant.chat.mutation_cards.result.partialBody": "Algunos registros no pudieron actualizarse.",
238
239
  "ai_assistant.chat.mutation_cards.result.partialTitle": "Acción aplicada con fallos",
239
240
  "ai_assistant.chat.mutation_cards.result.successBody": "La mutación se completó correctamente.",
@@ -313,6 +314,13 @@
313
314
  "ai_assistant.dock.right": "Anclar a la derecha",
314
315
  "ai_assistant.errors.moderationBlocked": "Tu mensaje fue bloqueado por el filtro de seguridad de contenido. Reformúlalo e inténtalo de nuevo.",
315
316
  "ai_assistant.errors.moderationUnavailable": "La verificación de seguridad de contenido no está disponible temporalmente. Inténtalo de nuevo en un momento.",
317
+ "ai_assistant.errors.pendingAllRecordsStale": "Todos los registros pendientes están desactualizados; el asistente debe volver a proponer el cambio.",
318
+ "ai_assistant.errors.pendingBatchNotApplicable": "Este lote ya no coincide con la vista previa aprobada, por lo que no se puede aplicar. Pide al asistente que vuelva a proponer el cambio.",
319
+ "ai_assistant.errors.pendingRecordDuplicateDropped": "Este registro aparecía más de una vez. Solo se aplicó su primer cambio, el de la vista previa; las entradas repetidas se ignoraron.",
320
+ "ai_assistant.errors.pendingRecordMissing": "El registro ya no existe.",
321
+ "ai_assistant.errors.pendingRecordNotPreviewed": "Este registro no formaba parte de la vista previa aprobada, por lo que no se modificó.",
322
+ "ai_assistant.errors.pendingRecordStale": "La versión del registro cambió desde la vista previa.",
323
+ "ai_assistant.errors.pendingSingleRecordStale": "La versión del registro cambió desde la vista previa; vuelve a proponer el cambio.",
316
324
  "ai_assistant.launcher.composerPlaceholder": "Pregunte lo que necesite…",
317
325
  "ai_assistant.launcher.dialogDescription": "Elige un asistente. Usa ↑/↓ para navegar, Enter para iniciar, Esc para cerrar.",
318
326
  "ai_assistant.launcher.dialogTitle": "AI assistants",
@@ -234,6 +234,7 @@
234
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "뮤테이션을 적용할 수 없습니다.",
235
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "작업 실패",
236
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "AI로 수정",
237
+ "ai_assistant.chat.mutation_cards.result.ignoredEntriesBody": "변경 사항이 저장되었습니다. 요청에서 반복된 일부 항목은 무시되었습니다:",
237
238
  "ai_assistant.chat.mutation_cards.result.partialBody": "일부 레코드를 업데이트할 수 없었습니다.",
238
239
  "ai_assistant.chat.mutation_cards.result.partialTitle": "일부 실패와 함께 작업이 적용되었습니다",
239
240
  "ai_assistant.chat.mutation_cards.result.successBody": "뮤테이션이 성공적으로 완료되었습니다.",
@@ -313,6 +314,13 @@
313
314
  "ai_assistant.dock.right": "오른쪽 독",
314
315
  "ai_assistant.errors.moderationBlocked": "귀하의 메시지가 콘텐츠 안전 필터에 의해 차단되었습니다. 문구를 바꾸고 다시 시도해 주세요.",
315
316
  "ai_assistant.errors.moderationUnavailable": "콘텐츠 안전 확인을 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도해 주세요.",
317
+ "ai_assistant.errors.pendingAllRecordsStale": "대기 중인 모든 레코드가 오래되었습니다. 어시스턴트가 변경을 다시 제안해야 합니다.",
318
+ "ai_assistant.errors.pendingBatchNotApplicable": "이 일괄 작업이 승인된 미리보기와 더 이상 일치하지 않아 적용할 수 없습니다. 어시스턴트에게 변경 사항을 다시 제안하도록 요청하세요.",
319
+ "ai_assistant.errors.pendingRecordDuplicateDropped": "이 레코드가 두 번 이상 포함되었습니다. 미리보기에 표시된 첫 번째 변경 사항만 적용되었고 반복된 항목은 무시되었습니다.",
320
+ "ai_assistant.errors.pendingRecordMissing": "레코드가 더 이상 존재하지 않습니다.",
321
+ "ai_assistant.errors.pendingRecordNotPreviewed": "이 레코드는 승인된 미리보기에 포함되지 않아 변경되지 않았습니다.",
322
+ "ai_assistant.errors.pendingRecordStale": "미리보기 이후 레코드 버전이 변경되었습니다.",
323
+ "ai_assistant.errors.pendingSingleRecordStale": "미리보기 이후 레코드 버전이 변경되었습니다. 변경을 다시 제안하세요.",
316
324
  "ai_assistant.launcher.composerPlaceholder": "무엇이든 물어보세요…",
317
325
  "ai_assistant.launcher.dialogDescription": "어시스턴트를 선택하세요. 탐색하려면 ↑/↓를 사용하고, 실행하려면 Enter를, 닫으려면 Esc를 사용하세요.",
318
326
  "ai_assistant.launcher.dialogTitle": "AI 어시스턴트",
@@ -234,6 +234,7 @@
234
234
  "ai_assistant.chat.mutation_cards.result.failureBody": "Nie udało się zastosować zmiany.",
235
235
  "ai_assistant.chat.mutation_cards.result.failureTitle": "Akcja nie powiodła się",
236
236
  "ai_assistant.chat.mutation_cards.result.fixWithAi": "Popraw z AI",
237
+ "ai_assistant.chat.mutation_cards.result.ignoredEntriesBody": "Zmiana została zapisana. Niektóre powtórzone wpisy w żądaniu zostały pominięte:",
237
238
  "ai_assistant.chat.mutation_cards.result.partialBody": "Nie udało się zaktualizować niektórych rekordów.",
238
239
  "ai_assistant.chat.mutation_cards.result.partialTitle": "Akcja zastosowana z błędami",
239
240
  "ai_assistant.chat.mutation_cards.result.successBody": "Zmiana została pomyślnie wykonana.",
@@ -313,6 +314,13 @@
313
314
  "ai_assistant.dock.right": "Dokuj po prawej",
314
315
  "ai_assistant.errors.moderationBlocked": "Twoja wiadomość została zablokowana przez filtr bezpieczeństwa treści. Przeformułuj ją i spróbuj ponownie.",
315
316
  "ai_assistant.errors.moderationUnavailable": "Sprawdzanie bezpieczeństwa treści jest chwilowo niedostępne. Spróbuj ponownie za chwilę.",
317
+ "ai_assistant.errors.pendingAllRecordsStale": "Wszystkie rekordy oczekującej akcji są nieaktualne; asystent musi ponownie zaproponować zmianę.",
318
+ "ai_assistant.errors.pendingBatchNotApplicable": "Ta partia nie odpowiada już zatwierdzonemu podglądowi, więc nie można jej zastosować. Poproś asystenta o ponowne zaproponowanie zmiany.",
319
+ "ai_assistant.errors.pendingRecordDuplicateDropped": "Ten rekord pojawił się więcej niż raz. Zastosowano tylko jego pierwszą zmianę z podglądu; powtórzone wpisy zostały pominięte.",
320
+ "ai_assistant.errors.pendingRecordMissing": "Rekord już nie istnieje.",
321
+ "ai_assistant.errors.pendingRecordNotPreviewed": "Ten rekord nie był częścią zatwierdzonego podglądu, więc nie został zmieniony.",
322
+ "ai_assistant.errors.pendingRecordStale": "Rekord zmienił się od czasu podglądu.",
323
+ "ai_assistant.errors.pendingSingleRecordStale": "Rekord zmienił się od czasu podglądu; zaproponuj zmianę ponownie.",
316
324
  "ai_assistant.launcher.composerPlaceholder": "Zapytaj o cokolwiek…",
317
325
  "ai_assistant.launcher.dialogDescription": "Wybierz asystenta. Nawiguj klawiszami ↑/↓, Enter uruchamia, Esc zamyka.",
318
326
  "ai_assistant.launcher.dialogTitle": "Asystenci AI",
@@ -6,6 +6,8 @@ import {
6
6
  getApiRouteManifests
7
7
  } from "@open-mercato/shared/modules/registry";
8
8
  import { hasAllFeatures } from "@open-mercato/shared/security/features";
9
+ import { OPTIMISTIC_LOCK_HEADER_NAME } from "@open-mercato/shared/lib/crud/optimistic-lock-headers";
10
+ import { normalizeIsoToken } from "@open-mercato/shared/lib/crud/optimistic-lock";
9
11
  const MUTATION_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
10
12
  const SYNTHETIC_ORIGIN = "http://internal.local";
11
13
  function normalizeMethod(method) {
@@ -22,6 +24,12 @@ function normalizePath(path) {
22
24
  while (end > 1 && trimmed.charCodeAt(end - 1) === 47) end--;
23
25
  return trimmed.slice(0, end);
24
26
  }
27
+ function expectedRecordVersionFor(ctx, recordId) {
28
+ const versions = ctx.expectedRecordVersions;
29
+ if (!versions || typeof recordId !== "string" || recordId.length === 0) return null;
30
+ const version = Object.prototype.hasOwnProperty.call(versions, recordId) ? versions[recordId] : void 0;
31
+ return typeof version === "string" && version.length > 0 ? version : null;
32
+ }
25
33
  function buildUrl(path, query) {
26
34
  const url = new URL(`${SYNTHETIC_ORIGIN}/api${normalizePath(path)}`);
27
35
  if (query) {
@@ -150,8 +158,16 @@ function createAiApiOperationRunner(ctx, options = {}) {
150
158
  if (manifestPath === null) {
151
159
  return failure(400, `Invalid path encoding for ${method} ${path}`);
152
160
  }
161
+ let expectedUpdatedAt = null;
162
+ if (request.expectedUpdatedAt !== void 0 && request.expectedUpdatedAt !== null) {
163
+ expectedUpdatedAt = normalizeIsoToken(String(request.expectedUpdatedAt));
164
+ if (!expectedUpdatedAt) {
165
+ return failure(400, `Invalid expectedUpdatedAt for ${method} ${path}`);
166
+ }
167
+ }
153
168
  const url = buildUrl(path, request.query);
154
169
  const headers = new Headers();
170
+ if (expectedUpdatedAt) headers.set(OPTIMISTIC_LOCK_HEADER_NAME, expectedUpdatedAt);
155
171
  const requestInit = { method, headers };
156
172
  if (request.body !== void 0 && method !== "GET") {
157
173
  headers.set("content-type", "application/json");
@@ -247,6 +263,7 @@ async function defaultLoadApiRoutes() {
247
263
  }
248
264
  export {
249
265
  createAiApiOperationRunner,
266
+ expectedRecordVersionFor,
250
267
  normalizePath
251
268
  };
252
269
  //# sourceMappingURL=ai-api-operation-runner.js.map