@sealab/mcp-server 1.0.2 → 1.0.3

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.
@@ -0,0 +1,3061 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { zodToJsonSchema } from 'zod-to-json-schema';
3
+ import * as apiClientModule from '../client/api-client';
4
+ import * as fs from 'fs';
5
+ import * as os from 'os';
6
+ import * as path from 'path';
7
+ import { normalizeMcpJsonSchema } from '../schema-normalizer';
8
+
9
+ vi.mock('../client/api-client', () => ({
10
+ aiDrawingClient: { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn() },
11
+ handleAxiosError: vi.fn((e) => { throw e; }),
12
+ McpApiError: class McpApiError extends Error {
13
+ constructor(public status: number, message: string) { super(message); }
14
+ },
15
+ }));
16
+
17
+ vi.mock('./ai-drawing-session-review-renderer', () => ({
18
+ renderAiDrawingSessionReview: vi.fn(),
19
+ }));
20
+
21
+ import * as rendererModule from './ai-drawing-session-review-renderer';
22
+ import {
23
+ AiCabinetCoordinateInputSchema,
24
+ AiCabinetCoordinatePatchInputSchema,
25
+ AiCabinetCoordinatesDeleteInputSchema,
26
+ AiCabinetCoordinatesPatchInputListSchema,
27
+ AiCabinetCoordinatesReplaceInputSchema,
28
+ AiCanvasImageDeleteInputSchema,
29
+ AiCanvasImageUploadInputSchema,
30
+ AiCanvasCabinetPlacementInputSchema,
31
+ AiCanvasCabinetPlacementPatchSchema,
32
+ AiCanvasCabinetPlacementsByCanvasInputSchema,
33
+ AiCanvasCabinetPlacementsDeleteInputSchema,
34
+ AiCanvasCabinetPlacementsPatchInputSchema,
35
+ AiCanvasCabinetPlacementsReplaceInputSchema,
36
+ AiCanvasCabinetPlacementsUpsertInputSchema,
37
+ AiCartIdParamSchema,
38
+ AiCartCreateInputSchema,
39
+ AiCartItemIdParamSchema,
40
+ AiCartItemInputSchema,
41
+ AiCartItemPatchSchema,
42
+ AiCartSnapshotInputSchema,
43
+ AiCanvasImageInputSchema,
44
+ AiCanvasMetadataInputSchema,
45
+ AiCanvasMetaSchema,
46
+ AiCanvasProjectionCalibrationSchema,
47
+ ConfigureAiCabinetInputSchema,
48
+ DeleteAiCabinetInputSchema,
49
+ DuplicateAiCabinetInputSchema,
50
+ AiOrderReadyPayloadClearInputSchema,
51
+ AiOrderReadyPayloadReadInputSchema,
52
+ AiOrderReadyPayloadSetInputSchema,
53
+ AiReduxCartImportInputSchema,
54
+ AiDrawingSessionReviewRenderInputSchema,
55
+ AiDrawingSessionCreateInputSchema,
56
+ LinkAiCartToDrawingSessionInputSchema,
57
+ clearAiCartItemOrderReadyPayload,
58
+ configureAiCabinet,
59
+ createAiDrawingSession,
60
+ deleteAiCabinet,
61
+ getAiCartItemOrderReadyPayload,
62
+ importReduxCartItemsToAiCart,
63
+ linkAiCartToDrawingSession,
64
+ setAiCartItemOrderReadyPayload,
65
+ upsertAiCanvasMetadata,
66
+ upsertAiCanvasImage,
67
+ uploadAiCanvasImage,
68
+ deleteAiCanvasImage,
69
+ getAiCanvasCabinetPlacements,
70
+ getAiCanvasCabinetPlacementsByCanvas,
71
+ replaceAiCanvasCabinetPlacements,
72
+ upsertAiCanvasCabinetPlacements,
73
+ patchAiCanvasCabinetPlacements,
74
+ deleteAiCanvasCabinetPlacements,
75
+ replaceAiCabinetCoordinates,
76
+ patchAiCabinetCoordinates,
77
+ deleteAiCabinetCoordinates,
78
+ createAiCart,
79
+ getAiCart,
80
+ getAiCartBySession,
81
+ syncAiCartSnapshot,
82
+ patchAiCartItem,
83
+ deleteAiCartItem,
84
+ duplicateAiCabinet,
85
+ getAiDrawingState,
86
+ renderAiDrawingSessionReviewTool,
87
+ aiDrawingTools,
88
+ } from './ai-drawing';
89
+
90
+ describe('getAiDrawingState', () => {
91
+ beforeEach(() => vi.clearAllMocks());
92
+
93
+ it('calls the AI drawing session state endpoint', async () => {
94
+ const sessionId = '11111111-1111-4111-8111-111111111111';
95
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({
96
+ data: {
97
+ session: { id: sessionId, drawingRevision: 2 },
98
+ revisions: { drawingRevision: 2 },
99
+ },
100
+ });
101
+
102
+ const result = await getAiDrawingState({ sessionId });
103
+
104
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(`/sessions/${sessionId}/state`);
105
+ expect(result).toContain(sessionId);
106
+ expect(result).toContain('drawingRevision');
107
+ });
108
+
109
+ it('returns API error text from the shared error handler', async () => {
110
+ const sessionId = '11111111-1111-4111-8111-111111111111';
111
+ const err = new (apiClientModule.McpApiError as any)(404, 'AI drawing session not found');
112
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockRejectedValue(err);
113
+ vi.mocked(apiClientModule.handleAxiosError).mockImplementation(() => { throw err; });
114
+
115
+ const result = await getAiDrawingState({ sessionId });
116
+
117
+ expect(result).toContain('AI drawing session not found');
118
+ });
119
+ });
120
+
121
+ describe('renderAiDrawingSessionReviewTool', () => {
122
+ const sessionId = '11111111-1111-4111-8111-111111111111';
123
+
124
+ beforeEach(() => vi.clearAllMocks());
125
+
126
+ it('fetches stored AI drawing state and passes it into the renderer', async () => {
127
+ const state = {
128
+ session: { id: sessionId },
129
+ activeCanvases: ['plan'],
130
+ activeCart: { items: [{ id: '22222222-2222-4222-8222-222222222222', positionName: 'BASE_01' }] },
131
+ canvasImages: [],
132
+ canvasCabinetPlacements: [],
133
+ };
134
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({ data: state });
135
+ vi.mocked(rendererModule.renderAiDrawingSessionReview).mockReturnValue({
136
+ sessionId,
137
+ generatedAt: '2026-05-07T00:00:00.000Z',
138
+ visualInspectionRequired: true,
139
+ visualReviewStatus: 'requires_native_vision',
140
+ completionBlockedUntil: 'Open every full-canvas outputImagePath with native vision, record PASS/FAIL/UNCERTAIN per canvas and placement, patch failures, and rerender.',
141
+ visualInspectionChecklist: [
142
+ 'Open every full-canvas overlay with native vision.',
143
+ 'Separate cabinet boxes must not overlap each other.',
144
+ 'Do not include appliances, fixtures, counters, toe kicks, walls, dimension strings, labels, or blank/context areas.',
145
+ ],
146
+ manifestPath: 'D:\\review\\manifest.json',
147
+ canvases: [
148
+ {
149
+ canvasType: 'plan',
150
+ outputImagePath: 'D:\\review\\plan.png',
151
+ imageWidth: 100,
152
+ imageHeight: 80,
153
+ renderedPlacementCount: 1,
154
+ sourceBoundaryReviewTasks: [],
155
+ issues: [],
156
+ },
157
+ ],
158
+ issues: [],
159
+ });
160
+
161
+ const result = await renderAiDrawingSessionReviewTool({
162
+ sessionId,
163
+ canvasTypes: ['plan'],
164
+ outputDir: 'D:\\review',
165
+ includeManifest: true,
166
+ });
167
+ const parsed = JSON.parse(result);
168
+
169
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(`/sessions/${sessionId}/state`);
170
+ expect(rendererModule.renderAiDrawingSessionReview).toHaveBeenCalledWith(state, {
171
+ outputDir: 'D:\\review',
172
+ canvases: ['plan'],
173
+ writeJsonManifest: true,
174
+ });
175
+ expect(parsed).toMatchObject({
176
+ sessionId,
177
+ outputDir: 'D:\\review',
178
+ manifestPath: 'D:\\review\\manifest.json',
179
+ aggregateIssueCount: 0,
180
+ visualReviewRequired: true,
181
+ visualReviewStatus: 'requires_native_vision',
182
+ completionBlockedUntil: expect.stringContaining('PASS/FAIL/UNCERTAIN'),
183
+ machineValidationScope: expect.stringContaining('aggregateIssueCount 0 is not visual approval'),
184
+ completionPolicy: expect.stringContaining('source_boundary_review_required'),
185
+ visualQaChecklist: expect.arrayContaining([
186
+ expect.stringContaining('Separate cabinet boxes must not overlap each other'),
187
+ expect.stringContaining('Do not include appliances, fixtures, counters, toe kicks, walls, dimension strings, labels, or blank/context areas'),
188
+ ]),
189
+ reviewImages: [
190
+ {
191
+ canvasType: 'plan',
192
+ outputImagePath: 'D:\\review\\plan.png',
193
+ imageWidth: 100,
194
+ imageHeight: 80,
195
+ renderedPlacementCount: 1,
196
+ sourceBoundaryReviewTaskCount: 0,
197
+ issueCount: 0,
198
+ },
199
+ ],
200
+ sourceBoundaryReviewTasks: [],
201
+ });
202
+ expect(parsed).not.toHaveProperty('placementReviewCrops');
203
+ expect(parsed.canvases[0]).not.toHaveProperty('placementReviewCrops');
204
+ expect(parsed.canvases[0].sourceBoundaryReviewTasks).toEqual([]);
205
+ });
206
+
207
+ it('materializes uploaded session s3Key backgrounds before rendering', async () => {
208
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ai-review-tool-'));
209
+ const pngBytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00]);
210
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue({
211
+ ok: true,
212
+ headers: { get: vi.fn(() => 'image/png') },
213
+ arrayBuffer: vi.fn(async () => pngBytes.buffer.slice(pngBytes.byteOffset, pngBytes.byteOffset + pngBytes.byteLength)),
214
+ } as unknown as Response);
215
+
216
+ try {
217
+ const state = {
218
+ session: { id: sessionId },
219
+ activeCanvases: ['plan'],
220
+ activeCart: { items: [] },
221
+ canvasImages: [
222
+ {
223
+ canvasType: 'plan',
224
+ imageType: 'background',
225
+ s3Key: 'ai-drawings/session-1/plan/background/plan.png',
226
+ },
227
+ ],
228
+ canvasCabinetPlacements: [],
229
+ };
230
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({ data: state });
231
+ vi.mocked(rendererModule.renderAiDrawingSessionReview).mockReturnValue({
232
+ sessionId,
233
+ generatedAt: '2026-05-07T00:00:00.000Z',
234
+ canvases: [
235
+ {
236
+ canvasType: 'plan',
237
+ outputImagePath: path.join(tempDir, 'plan-review.png'),
238
+ imageWidth: 100,
239
+ imageHeight: 80,
240
+ renderedPlacementCount: 0,
241
+ issues: [],
242
+ },
243
+ ],
244
+ issues: [],
245
+ });
246
+
247
+ await renderAiDrawingSessionReviewTool({
248
+ sessionId,
249
+ canvasTypes: ['plan'],
250
+ outputDir: tempDir,
251
+ });
252
+
253
+ expect(fetchSpy).toHaveBeenCalledWith('https://www.thesealab.com/ai-drawings/session-1/plan/background/plan.png');
254
+ const rendererOptions = vi.mocked(rendererModule.renderAiDrawingSessionReview).mock.calls[0][1];
255
+ const materializedPath = rendererOptions.imagePathByCanvas?.plan;
256
+ expect(materializedPath).toBe(path.join(tempDir, 'ai-drawing-session-review-background-plan.png'));
257
+ expect(fs.existsSync(materializedPath!)).toBe(true);
258
+ } finally {
259
+ fetchSpy.mockRestore();
260
+ fs.rmSync(tempDir, { recursive: true, force: true });
261
+ }
262
+ });
263
+
264
+ it('returns issue-only no-render output without throwing', async () => {
265
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({
266
+ data: { session: { id: sessionId }, activeCanvases: ['north'] },
267
+ });
268
+ vi.mocked(rendererModule.renderAiDrawingSessionReview).mockReturnValue({
269
+ sessionId,
270
+ generatedAt: '2026-05-07T00:00:00.000Z',
271
+ canvases: [
272
+ {
273
+ canvasType: 'north',
274
+ renderedPlacementCount: 0,
275
+ issues: [
276
+ {
277
+ code: 'unsupported_background_image_reference',
278
+ severity: 'warning',
279
+ canvasType: 'north',
280
+ message: 'Canvas north background image is not renderable locally.',
281
+ },
282
+ ],
283
+ },
284
+ ],
285
+ issues: [
286
+ {
287
+ code: 'unsupported_background_image_reference',
288
+ severity: 'warning',
289
+ canvasType: 'north',
290
+ message: 'Canvas north background image is not renderable locally.',
291
+ },
292
+ ],
293
+ });
294
+
295
+ const result = await renderAiDrawingSessionReviewTool({
296
+ sessionId,
297
+ outputDir: 'D:\\review',
298
+ });
299
+ const parsed = JSON.parse(result);
300
+
301
+ expect(parsed.reviewImages).toEqual([]);
302
+ expect(parsed.aggregateIssueCount).toBe(1);
303
+ expect(parsed.perCanvasIssues.north[0]).toMatchObject({
304
+ code: 'unsupported_background_image_reference',
305
+ severity: 'warning',
306
+ });
307
+ });
308
+
309
+ it('validates sessionId UUID and outputDir', () => {
310
+ expect(AiDrawingSessionReviewRenderInputSchema.safeParse({
311
+ sessionId: 'not-a-uuid',
312
+ outputDir: 'D:\\review',
313
+ }).success).toBe(false);
314
+
315
+ expect(AiDrawingSessionReviewRenderInputSchema.safeParse({
316
+ sessionId,
317
+ outputDir: ' ',
318
+ }).success).toBe(false);
319
+
320
+ expect(AiDrawingSessionReviewRenderInputSchema.safeParse({
321
+ sessionId,
322
+ }).success).toBe(false);
323
+ });
324
+ });
325
+
326
+ describe('aiDrawingTools', () => {
327
+ const expectedPhase4ToolNames = [
328
+ 'get_ai_drawing_state',
329
+ 'render_ai_drawing_session_review',
330
+ 'create_ai_drawing_session',
331
+ 'link_ai_cart_to_drawing_session',
332
+ 'upsert_ai_canvas_metadata',
333
+ 'upsert_ai_canvas_image',
334
+ 'upload_ai_canvas_image',
335
+ 'delete_ai_canvas_image',
336
+ 'get_ai_canvas_cabinet_placements',
337
+ 'get_ai_canvas_cabinet_placements_by_canvas',
338
+ 'replace_ai_canvas_cabinet_placements',
339
+ 'upsert_ai_canvas_cabinet_placements',
340
+ 'patch_ai_canvas_cabinet_placements',
341
+ 'delete_ai_canvas_cabinet_placements',
342
+ 'replace_ai_cabinet_coordinates',
343
+ 'patch_ai_cabinet_coordinates',
344
+ 'delete_ai_cabinet_coordinates',
345
+ 'create_ai_cart',
346
+ 'get_ai_cart',
347
+ 'get_ai_cart_by_session',
348
+ 'sync_ai_cart_snapshot',
349
+ 'patch_ai_cart_item',
350
+ 'delete_ai_cart_item',
351
+ 'duplicate_ai_cabinet',
352
+ 'configure_ai_cabinet',
353
+ 'delete_ai_cabinet',
354
+ 'get_ai_cart_item_order_ready_payload',
355
+ 'set_ai_cart_item_order_ready_payload',
356
+ 'clear_ai_cart_item_order_ready_payload',
357
+ 'import_redux_cart_items_to_ai_cart',
358
+ ];
359
+
360
+ it('registers every AI MCP tool exactly once', () => {
361
+ const toolNames = aiDrawingTools.map((tool) => tool.name);
362
+
363
+ expect(toolNames).toEqual(expectedPhase4ToolNames);
364
+ expect(new Set(toolNames).size).toBe(expectedPhase4ToolNames.length);
365
+ });
366
+
367
+ it('registers every Phase 10 placement MCP tool exactly once', () => {
368
+ const toolNames = aiDrawingTools.map((tool) => tool.name);
369
+ const placementToolNames = [
370
+ 'get_ai_canvas_cabinet_placements',
371
+ 'get_ai_canvas_cabinet_placements_by_canvas',
372
+ 'replace_ai_canvas_cabinet_placements',
373
+ 'upsert_ai_canvas_cabinet_placements',
374
+ 'patch_ai_canvas_cabinet_placements',
375
+ 'delete_ai_canvas_cabinet_placements',
376
+ ];
377
+
378
+ for (const name of placementToolNames) {
379
+ expect(toolNames.filter((toolName) => toolName === name)).toHaveLength(1);
380
+ }
381
+ });
382
+
383
+ it('registers every Phase 11 cabinet operation MCP tool exactly once', () => {
384
+ const toolNames = aiDrawingTools.map((tool) => tool.name);
385
+ const operationToolNames = [
386
+ 'duplicate_ai_cabinet',
387
+ 'configure_ai_cabinet',
388
+ 'delete_ai_cabinet',
389
+ ];
390
+
391
+ for (const name of operationToolNames) {
392
+ expect(toolNames.filter((toolName) => toolName === name)).toHaveLength(1);
393
+ }
394
+ });
395
+
396
+ it('registers every Phase 13 order-ready payload MCP tool exactly once', () => {
397
+ const toolNames = aiDrawingTools.map((tool) => tool.name);
398
+ const orderReadyToolNames = [
399
+ 'get_ai_cart_item_order_ready_payload',
400
+ 'set_ai_cart_item_order_ready_payload',
401
+ 'clear_ai_cart_item_order_ready_payload',
402
+ ];
403
+
404
+ for (const name of orderReadyToolNames) {
405
+ expect(toolNames.filter((toolName) => toolName === name)).toHaveLength(1);
406
+ }
407
+ });
408
+
409
+ it('registers the Phase 14 Redux import MCP tool exactly once', () => {
410
+ const toolNames = aiDrawingTools.map((tool) => tool.name);
411
+
412
+ expect(toolNames.filter((toolName) => toolName === 'import_redux_cart_items_to_ai_cart')).toHaveLength(1);
413
+ });
414
+
415
+ it('registers the Phase 16 review renderer MCP tool exactly once', () => {
416
+ const toolNames = aiDrawingTools.map((tool) => tool.name);
417
+
418
+ expect(toolNames.filter((toolName) => toolName === 'render_ai_drawing_session_review')).toHaveLength(1);
419
+ });
420
+
421
+ it('does not register duplicate AI drawing state tools', () => {
422
+ expect(aiDrawingTools.filter((tool) => tool.name === 'get_ai_drawing_state')).toHaveLength(1);
423
+ });
424
+
425
+ it('describes every AI MCP tool as backend-owned AIDrawingTool behavior', () => {
426
+ for (const tool of aiDrawingTools) {
427
+ expect(tool.description).toContain('AIDrawingTool');
428
+ expect(tool.description).toContain('backend-owned');
429
+ }
430
+ });
431
+
432
+ it('describes the review renderer workflow as render, vision inspect, correct, and rerender', () => {
433
+ const tool = aiDrawingTools.find((candidate) => candidate.name === 'render_ai_drawing_session_review');
434
+
435
+ expect(tool?.description).toContain('after writing or updating AI cart items and per-canvas placements');
436
+ expect(tool?.description).toContain('open every returned full-canvas PNG path with native vision');
437
+ expect(tool?.description).toContain('complete every returned sourceBoundaryReviewTasks[] item against the actual source drawing pixels');
438
+ expect(tool?.description).toContain('no per-placement crop artifacts are generated');
439
+ expect(tool?.description).toContain('apply the visual QA checklist');
440
+ expect(tool?.description).toContain('Correct bad placements through the existing AI-only placement tools');
441
+ expect(tool?.description).toContain('render again/rerender');
442
+ expect(tool?.description).toContain('JSON validity');
443
+ expect(tool?.description).toContain('not visual correctness');
444
+ expect(tool?.description).toContain('Completion is blocked until the agent records PASS/FAIL/UNCERTAIN');
445
+ });
446
+
447
+ it('describes native vision as the drawing interpretation source of truth', () => {
448
+ const syncTool = aiDrawingTools.find((candidate) => candidate.name === 'sync_ai_cart_snapshot');
449
+ const placementTool = aiDrawingTools.find((candidate) => candidate.name === 'upsert_ai_canvas_cabinet_placements');
450
+ const reviewTool = aiDrawingTools.find((candidate) => candidate.name === 'render_ai_drawing_session_review');
451
+
452
+ for (const tool of [syncTool, placementTool, reviewTool]) {
453
+ expect(tool?.description).toContain('Native vision on the actual source plan/elevation images is the source of truth');
454
+ expect(tool?.description).toContain('Run native image analysis first');
455
+ expect(tool?.description).toContain('use MCP tools only to persist the reviewed session, cart, placements, and review artifacts');
456
+ expect(tool?.description).toContain('optional trace/diagnostic metadata only');
457
+ expect(tool?.description).toContain('Do not let raw extracted candidates, tool labels, evidence JSON, schema validity, or MCP-derived guesses override');
458
+ }
459
+ });
460
+
461
+ it('describes concrete visual QA failure modes for rendered placement review', () => {
462
+ const tool = aiDrawingTools.find((candidate) => candidate.name === 'render_ai_drawing_session_review');
463
+
464
+ expect(tool?.description).toContain('compare each rectangle against the visible drawing');
465
+ expect(tool?.description).toContain('source drawing pixels are the authority');
466
+ expect(tool?.description).toContain('inside the actual cabinet face or plan footprint boundaries');
467
+ expect(tool?.description).toContain('overlap other cabinets, appliances, fixtures, counters, toe kicks, walls, dimension strings, or blank/context areas');
468
+ expect(tool?.description).toContain('shifted boxes that extend beyond wood/front boundaries');
469
+ expect(tool?.description).toContain('include refrigerator/range/dishwasher/sink appliance surfaces unless the AI cart item is explicitly an appliance panel/opening');
470
+ expect(tool?.description).toContain('same positionName/aiCartItemId across plan and elevation');
471
+ expect(tool?.description).toContain('no obvious visible cabinet is missing');
472
+ expect(tool?.description).toContain('no side/return/context-only face is treated as a primary cabinet');
473
+ expect(tool?.description).toContain('labels do not hide boundary errors');
474
+ expect(tool?.description).toContain('ask the user instead of declaring the review correct');
475
+ });
476
+
477
+ it('tells agents to use dimension and scale anchors for placement sanity checks', () => {
478
+ const placementTool = aiDrawingTools.find((candidate) => candidate.name === 'upsert_ai_canvas_cabinet_placements');
479
+ const reviewTool = aiDrawingTools.find((candidate) => candidate.name === 'render_ai_drawing_session_review');
480
+
481
+ for (const tool of [placementTool, reviewTool]) {
482
+ expect(tool?.description).toContain('Use visible dimensions as scale anchors');
483
+ expect(tool?.description).toContain('read explicit dimension strings');
484
+ expect(tool?.description).toContain('known catalog widths/heights');
485
+ expect(tool?.description).toContain('compute pixels-per-inch per canvas when possible');
486
+ expect(tool?.description).toContain('sanity-check other cabinet sizes and gaps');
487
+ expect(tool?.description).toContain('Do not resize boxes away from visible boundaries just to force a dimension');
488
+ expect(tool?.description).toContain('reinspect the source image and correct or ask the user');
489
+ }
490
+ });
491
+
492
+ it('requires visual review guidance on placement mutation tool descriptions', () => {
493
+ const placementMutationToolNames = [
494
+ 'replace_ai_canvas_cabinet_placements',
495
+ 'upsert_ai_canvas_cabinet_placements',
496
+ 'patch_ai_canvas_cabinet_placements',
497
+ 'delete_ai_canvas_cabinet_placements',
498
+ ];
499
+
500
+ for (const name of placementMutationToolNames) {
501
+ expectDescriptionRequiresVisualReview(name);
502
+ }
503
+ });
504
+
505
+ it('requires visual review guidance on canvas upload and update tool descriptions', () => {
506
+ const canvasWriteToolNames = [
507
+ 'upsert_ai_canvas_metadata',
508
+ 'upsert_ai_canvas_image',
509
+ 'upload_ai_canvas_image',
510
+ 'delete_ai_canvas_image',
511
+ ];
512
+
513
+ for (const name of canvasWriteToolNames) {
514
+ expectDescriptionRequiresVisualReview(name);
515
+ }
516
+ });
517
+
518
+ it('requires visual review guidance on AI cart and cabinet mutation tool descriptions', () => {
519
+ const cartWriteToolNames = [
520
+ 'create_ai_cart',
521
+ 'sync_ai_cart_snapshot',
522
+ 'patch_ai_cart_item',
523
+ 'delete_ai_cart_item',
524
+ 'duplicate_ai_cabinet',
525
+ 'configure_ai_cabinet',
526
+ 'delete_ai_cabinet',
527
+ 'set_ai_cart_item_order_ready_payload',
528
+ 'clear_ai_cart_item_order_ready_payload',
529
+ 'import_redux_cart_items_to_ai_cart',
530
+ ];
531
+
532
+ for (const name of cartWriteToolNames) {
533
+ expectDescriptionRequiresVisualReview(name);
534
+ }
535
+ });
536
+
537
+ it('keeps order-ready and cart guidance tied to catalog, configuration, saved settings, and user choices before READY', () => {
538
+ const orderConfigurationToolNames = [
539
+ 'create_ai_cart',
540
+ 'sync_ai_cart_snapshot',
541
+ 'set_ai_cart_item_order_ready_payload',
542
+ ];
543
+
544
+ for (const name of orderConfigurationToolNames) {
545
+ const tool = aiDrawingTools.find((candidate) => candidate.name === name);
546
+
547
+ expect(tool?.description).toContain('get_article_context');
548
+ expect(tool?.description).toContain('get_article_options');
549
+ expect(tool?.description).toContain('get_my_saved_settings');
550
+ expect(tool?.description).toContain('get_saved_settings_presets');
551
+ expect(tool?.description).toContain('ask the user');
552
+ expect(tool?.description).toContain('READY');
553
+ expect(tool?.description).toContain('ArticleItemSchema-compatible');
554
+ }
555
+ });
556
+
557
+ it('keeps the review renderer schema Gemini-compatible', () => {
558
+ const normalizedSchema = normalizeMcpJsonSchema(
559
+ zodToJsonSchema(AiDrawingSessionReviewRenderInputSchema, { target: 'openApi3' })
560
+ );
561
+
562
+ expect(findArrayTupleItems(normalizedSchema)).toEqual([]);
563
+ expect(findBooleanExclusiveBounds(normalizedSchema)).toEqual([]);
564
+ });
565
+
566
+ it('does not introduce protected legacy strings in the review renderer schema or description', () => {
567
+ const tool = aiDrawingTools.find((candidate) => candidate.name === 'render_ai_drawing_session_review');
568
+ const rendered = JSON.stringify([
569
+ tool?.description,
570
+ normalizeMcpJsonSchema(zodToJsonSchema(AiDrawingSessionReviewRenderInputSchema, { target: 'openApi3' })),
571
+ ]);
572
+ const forbiddenTerms = [
573
+ '/api/mcp/v1/' + 'canvas',
574
+ '/topic/' + 'drawing-tool',
575
+ 'Drawing' + 'ToolTest',
576
+ 'saved' + '_carts',
577
+ 'cabinets' + '_coordinates',
578
+ ];
579
+
580
+ for (const term of forbiddenTerms) {
581
+ expect(rendered).not.toContain(term);
582
+ }
583
+ });
584
+ });
585
+
586
+ describe('AI drawing operation tool handlers', () => {
587
+ const sessionId = '33333333-3333-4333-8333-333333333333';
588
+ const cartId = '11111111-1111-4111-8111-111111111111';
589
+ const itemId = '22222222-2222-4222-8222-222222222222';
590
+ const planViewAssociationEvidence = {
591
+ basis: 'plan_elevation_marker_review' as const,
592
+ elevationCanvasType: 'north' as const,
593
+ detailNumber: '7',
594
+ planMarkerSide: 'right' as const,
595
+ selectedPlanObjectIds: ['P_REF'],
596
+ selectedPlanObjectBboxPx: { x: -0.5, y: 0, width: 3, height: 4 },
597
+ selectedPlanObjectRole: 'appliance_footprint' as const,
598
+ sourcePlanObject: validReviewedPlanObject('P_REF', 'appliance_opening'),
599
+ sharedAnchors: ['appliance_label:REF'],
600
+ conflictingPlanAnchorsRejected: ['fixture_symbol:SINK'],
601
+ evidence: 'Detail 7/A-721 is on the right side of the plan marker; association.selectedPlanObjects contains P_REF. Agent vision/image analysis confirms the matched elevation shows the same refrigerator appliance condition with handles, not the sink side.',
602
+ };
603
+ const sameCanvasReviewedPlanEvidence = {
604
+ ...planViewAssociationEvidence,
605
+ basis: 'same_canvas_reviewed_plan_object' as const,
606
+ evidence: 'This plan placement comes from same-canvas reviewed plan object P_REF; association.selectedPlanObjects contains P_REF. Agent visual image analysis confirms the selected plan object is the REF refrigerator footprint for the matched elevation.',
607
+ };
608
+
609
+ beforeEach(() => vi.clearAllMocks());
610
+
611
+ it('create_ai_drawing_session posts to the AI session endpoint', async () => {
612
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({ data: { id: sessionId } });
613
+
614
+ const result = await createAiDrawingSession({
615
+ sourceType: 'REDUX_SNAPSHOT',
616
+ sourceOrderId: 'ORDER-1',
617
+ sourceSavedCartId: 12,
618
+ activeAiCartId: cartId,
619
+ });
620
+
621
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith('/sessions', {
622
+ sourceType: 'REDUX_SNAPSHOT',
623
+ sourceOrderId: 'ORDER-1',
624
+ sourceSavedCartId: 12,
625
+ activeAiCartId: cartId,
626
+ });
627
+ expect(result).toContain(sessionId);
628
+ });
629
+
630
+ it('link_ai_cart_to_drawing_session puts to the active cart endpoint', async () => {
631
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { id: sessionId, activeAiCartId: cartId } });
632
+
633
+ const result = await linkAiCartToDrawingSession({ sessionId, cartId });
634
+
635
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(`/sessions/${sessionId}/active-cart/${cartId}`);
636
+ expect(result).toContain(cartId);
637
+ });
638
+
639
+ it('upsert_ai_canvas_metadata puts to the AI metadata endpoint', async () => {
640
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { sessionId, canvasType: 'plan' } });
641
+ const canvasMeta = { projectionCalibration: validPlanProjectionCalibration() };
642
+
643
+ const result = await upsertAiCanvasMetadata({
644
+ sessionId,
645
+ canvasType: 'plan',
646
+ canvasMeta,
647
+ activeCanvases: ['plan', 'north'],
648
+ });
649
+
650
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(`/sessions/${sessionId}/metadata/plan`, {
651
+ canvasType: 'plan',
652
+ canvasMeta,
653
+ activeCanvases: ['plan', 'north'],
654
+ });
655
+ expect(result).toContain('plan');
656
+ });
657
+
658
+ it('upsert_ai_canvas_image puts to the AI image endpoint', async () => {
659
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { sessionId, s3Key: 'ai/plan.png' } });
660
+
661
+ const result = await upsertAiCanvasImage({
662
+ sessionId,
663
+ canvasType: 'plan',
664
+ imageType: 'background',
665
+ s3Key: 'ai/plan.png',
666
+ });
667
+
668
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(`/sessions/${sessionId}/images/plan/background`, {
669
+ canvasType: 'plan',
670
+ imageType: 'background',
671
+ s3Key: 'ai/plan.png',
672
+ });
673
+ expect(result).toContain('ai/plan.png');
674
+ });
675
+
676
+ it('upload_ai_canvas_image uploads a local file to the AI multipart endpoint', async () => {
677
+ const tempPath = path.join(os.tmpdir(), `ai-canvas-${Date.now()}.png`);
678
+ fs.writeFileSync(tempPath, Buffer.from([137, 80, 78, 71]));
679
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({
680
+ data: { sessionId, canvasType: 'east', imageType: 'background', s3Key: `ai-drawings/${sessionId}/east/background/file.png` },
681
+ });
682
+
683
+ try {
684
+ const result = await uploadAiCanvasImage({
685
+ sessionId,
686
+ canvasType: 'east',
687
+ imageType: 'background',
688
+ imagePath: tempPath,
689
+ });
690
+
691
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith(
692
+ `/sessions/${sessionId}/images/east/background/upload`,
693
+ expect.anything(),
694
+ expect.objectContaining({ headers: expect.any(Object) })
695
+ );
696
+ expect(result).toContain('ai-drawings');
697
+ } finally {
698
+ fs.unlinkSync(tempPath);
699
+ }
700
+ });
701
+
702
+ it('upload_ai_canvas_image rejects unsupported local file extensions before API call', async () => {
703
+ const result = await uploadAiCanvasImage({
704
+ sessionId,
705
+ canvasType: 'east',
706
+ imageType: 'background',
707
+ imagePath: 'D:\\testing321\\overlay.txt',
708
+ });
709
+
710
+ expect(result).toContain('Unsupported file extension');
711
+ expect(apiClientModule.aiDrawingClient.post).not.toHaveBeenCalled();
712
+ });
713
+
714
+ it('delete_ai_canvas_image deletes through the AI image endpoint', async () => {
715
+ vi.mocked(apiClientModule.aiDrawingClient.delete).mockResolvedValue({
716
+ data: { deleted: true, canvasType: 'plan', imageType: 'background' },
717
+ });
718
+
719
+ const result = await deleteAiCanvasImage({
720
+ sessionId,
721
+ canvasType: 'plan',
722
+ imageType: 'background',
723
+ });
724
+
725
+ expect(apiClientModule.aiDrawingClient.delete).toHaveBeenCalledWith(
726
+ `/sessions/${sessionId}/images/plan/background`
727
+ );
728
+ expect(result).toContain('background');
729
+ });
730
+
731
+ it('get_ai_canvas_cabinet_placements calls the all-placement endpoint', async () => {
732
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({
733
+ data: [{ canvasType: 'plan', positionName: 'BASE_01' }],
734
+ });
735
+
736
+ const result = await getAiCanvasCabinetPlacements({ sessionId });
737
+
738
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(`/sessions/${sessionId}/placements`);
739
+ expect(result).toContain('BASE_01');
740
+ });
741
+
742
+ it('get_ai_canvas_cabinet_placements_by_canvas calls the canvas placement endpoint', async () => {
743
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({
744
+ data: [{ canvasType: 'east', positionName: 'BASE_01' }],
745
+ });
746
+
747
+ const result = await getAiCanvasCabinetPlacementsByCanvas({ sessionId, canvasType: 'east' });
748
+
749
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(`/sessions/${sessionId}/placements/east`);
750
+ expect(result).toContain('east');
751
+ });
752
+
753
+ it('replace_ai_canvas_cabinet_placements puts one-canvas placement replacement payload', async () => {
754
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
755
+ data: [{ canvasType: 'plan', positionName: 'BASE_01', placementRevision: 1 }],
756
+ });
757
+
758
+ const result = await replaceAiCanvasCabinetPlacements({
759
+ sessionId,
760
+ canvasType: 'plan',
761
+ placements: [
762
+ {
763
+ aiCartItemId: itemId,
764
+ positionName: 'BASE_01',
765
+ centerX: 120,
766
+ centerY: 240,
767
+ width: 60,
768
+ height: 80,
769
+ rotation: 0,
770
+ placementSpace: 'IMAGE_PIXELS',
771
+ basisImageWidthPx: 1000,
772
+ basisImageHeightPx: 800,
773
+ source: 'MCP',
774
+ viewAssociationEvidence: sameCanvasReviewedPlanEvidence,
775
+ },
776
+ ],
777
+ });
778
+
779
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(`/sessions/${sessionId}/placements/plan`, {
780
+ placements: [
781
+ {
782
+ aiCartItemId: itemId,
783
+ positionName: 'BASE_01',
784
+ centerX: 120,
785
+ centerY: 240,
786
+ width: 60,
787
+ height: 80,
788
+ rotation: 0,
789
+ placementSpace: 'IMAGE_PIXELS',
790
+ basisImageWidthPx: 1000,
791
+ basisImageHeightPx: 800,
792
+ source: 'MCP',
793
+ },
794
+ ],
795
+ });
796
+ expect(result).toContain('BASE_01');
797
+ });
798
+
799
+ it('upsert_ai_canvas_cabinet_placements posts one-canvas placement upsert payload', async () => {
800
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({
801
+ data: [{ canvasType: 'east', positionName: 'BASE_01', placementRevision: 2 }],
802
+ });
803
+
804
+ const result = await upsertAiCanvasCabinetPlacements({
805
+ sessionId,
806
+ canvasType: 'east',
807
+ placements: [
808
+ {
809
+ positionName: 'BASE_01',
810
+ centerX: 120,
811
+ centerY: 240,
812
+ width: 60,
813
+ height: 80,
814
+ placementSpace: 'IMAGE_PIXELS',
815
+ source: 'MCP',
816
+ },
817
+ ],
818
+ });
819
+
820
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith(`/sessions/${sessionId}/placements/east`, {
821
+ placements: [
822
+ {
823
+ positionName: 'BASE_01',
824
+ centerX: 120,
825
+ centerY: 240,
826
+ width: 60,
827
+ height: 80,
828
+ placementSpace: 'IMAGE_PIXELS',
829
+ source: 'MCP',
830
+ },
831
+ ],
832
+ });
833
+ expect(result).toContain('BASE_01');
834
+ });
835
+
836
+ it('patch_ai_canvas_cabinet_placements patches one-canvas placement payloads', async () => {
837
+ vi.mocked(apiClientModule.aiDrawingClient.patch).mockResolvedValue({
838
+ data: [{ canvasType: 'north', positionName: 'WALL_01', centerX: 90 }],
839
+ });
840
+
841
+ const result = await patchAiCanvasCabinetPlacements({
842
+ sessionId,
843
+ canvasType: 'north',
844
+ placements: [
845
+ {
846
+ positionName: 'WALL_01',
847
+ centerX: 90,
848
+ source: 'AI',
849
+ },
850
+ ],
851
+ });
852
+
853
+ expect(apiClientModule.aiDrawingClient.patch).toHaveBeenCalledWith(`/sessions/${sessionId}/placements/north`, {
854
+ placements: [
855
+ {
856
+ positionName: 'WALL_01',
857
+ centerX: 90,
858
+ source: 'AI',
859
+ },
860
+ ],
861
+ });
862
+ expect(result).toContain('WALL_01');
863
+ });
864
+
865
+ it('delete_ai_canvas_cabinet_placements posts selected position names to one-canvas delete endpoint', async () => {
866
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({
867
+ data: { canvasType: 'east', deletedPositionNames: ['BASE_01'] },
868
+ });
869
+
870
+ const result = await deleteAiCanvasCabinetPlacements({
871
+ sessionId,
872
+ canvasType: 'east',
873
+ positionNames: ['BASE_01'],
874
+ });
875
+
876
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith(`/sessions/${sessionId}/placements/east/delete`, {
877
+ positionNames: ['BASE_01'],
878
+ });
879
+ expect(result).toContain('BASE_01');
880
+ });
881
+
882
+ it('duplicate_ai_cabinet posts to the session cabinet duplicate endpoint with selected placement copy fields', async () => {
883
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({
884
+ data: { duplicateItem: { id: itemId, positionName: 'BASE_01_copy' } },
885
+ });
886
+
887
+ const result = await duplicateAiCabinet({
888
+ sessionId,
889
+ itemId,
890
+ positionName: 'BASE_01_copy',
891
+ sourceCanvasType: 'plan',
892
+ sourcePlacementPositionName: 'BASE_01',
893
+ placementOffsetX: 24,
894
+ placementOffsetY: 12,
895
+ source: 'MCP',
896
+ });
897
+
898
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith(
899
+ `/sessions/${sessionId}/cart-items/${itemId}/duplicate`,
900
+ {
901
+ positionName: 'BASE_01_copy',
902
+ selectedCanvasType: 'plan',
903
+ sourcePlacementPositionName: 'BASE_01',
904
+ placementOffsetX: 24,
905
+ placementOffsetY: 12,
906
+ source: 'MCP',
907
+ }
908
+ );
909
+ expect(result).toContain('BASE_01_copy');
910
+ });
911
+
912
+ it('configure_ai_cabinet patches supported backend item fields and serializes payload objects', async () => {
913
+ vi.mocked(apiClientModule.aiDrawingClient.patch).mockResolvedValue({
914
+ data: { configuredItem: { id: itemId, positionName: 'BASE_02' } },
915
+ });
916
+
917
+ const result = await configureAiCabinet({
918
+ sessionId,
919
+ itemId,
920
+ positionName: 'BASE_02',
921
+ displayName: 'Configured Base',
922
+ serialNumber: 'SN-2',
923
+ frontendItemId: 'frontend-2',
924
+ originalFrontendItemId: 'original-2',
925
+ originalPositionName: 'BASE_01',
926
+ groupId: 'run-1',
927
+ quantity: 1,
928
+ originalQuantity: 1,
929
+ width: 36,
930
+ height: 40,
931
+ depth: 25,
932
+ originalItemPayload: { configured: true },
933
+ source: 'MCP',
934
+ });
935
+
936
+ expect(apiClientModule.aiDrawingClient.patch).toHaveBeenCalledWith(
937
+ `/sessions/${sessionId}/cart-items/${itemId}/configure`,
938
+ {
939
+ positionName: 'BASE_02',
940
+ displayName: 'Configured Base',
941
+ serialNumber: 'SN-2',
942
+ frontendItemId: 'frontend-2',
943
+ originalFrontendItemId: 'original-2',
944
+ originalPositionName: 'BASE_01',
945
+ groupId: 'run-1',
946
+ quantity: 1,
947
+ originalQuantity: 1,
948
+ width: 36,
949
+ height: 40,
950
+ depth: 25,
951
+ originalItemPayload: '{"configured":true}',
952
+ source: 'MCP',
953
+ }
954
+ );
955
+ expect(result).toContain('BASE_02');
956
+ });
957
+
958
+ it('delete_ai_cabinet deletes one session active-cart cabinet item', async () => {
959
+ vi.mocked(apiClientModule.aiDrawingClient.delete).mockResolvedValue({
960
+ data: { deletedItemId: itemId, deletedPositionName: 'BASE_01' },
961
+ });
962
+
963
+ const result = await deleteAiCabinet({ sessionId, itemId });
964
+
965
+ expect(apiClientModule.aiDrawingClient.delete).toHaveBeenCalledWith(
966
+ `/sessions/${sessionId}/cart-items/${itemId}`
967
+ );
968
+ expect(result).toContain('BASE_01');
969
+ });
970
+
971
+ it('get_ai_cart_item_order_ready_payload calls the session item payload endpoint', async () => {
972
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({
973
+ data: { id: itemId, orderReadyStatus: 'READY' },
974
+ });
975
+
976
+ const result = await getAiCartItemOrderReadyPayload({ sessionId, itemId });
977
+
978
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(
979
+ `/sessions/${sessionId}/cart-items/${itemId}/order-ready-payload`
980
+ );
981
+ expect(result).toContain('READY');
982
+ });
983
+
984
+ it('set_ai_cart_item_order_ready_payload rejects arbitrary READY payloads before API call', async () => {
985
+ const result = await setAiCartItemOrderReadyPayload({
986
+ sessionId,
987
+ itemId,
988
+ orderReadyStatus: 'READY',
989
+ orderReadyPayload: { arbitrary: true },
990
+ orderReadySource: 'MCP',
991
+ });
992
+
993
+ expect(result).toContain('READY rejected');
994
+ expect(result).toContain('ArticleItemSchema');
995
+ expect(apiClientModule.aiDrawingClient.put).not.toHaveBeenCalled();
996
+ });
997
+
998
+ it('set_ai_cart_item_order_ready_payload rejects serial and dimensions only READY payloads', async () => {
999
+ const result = await setAiCartItemOrderReadyPayload({
1000
+ sessionId,
1001
+ itemId,
1002
+ orderReadyStatus: 'READY',
1003
+ orderReadyPayload: thinArticleItemPayload(),
1004
+ orderReadySource: 'MCP',
1005
+ });
1006
+
1007
+ expect(result).toContain('READY rejected');
1008
+ expect(result).toContain('serialNumber, positionName, width, height, and depth alone are not enough');
1009
+ expect(apiClientModule.aiDrawingClient.put).not.toHaveBeenCalled();
1010
+ });
1011
+
1012
+ it('set_ai_cart_item_order_ready_payload accepts valid ArticleItemSchema-compatible READY payloads', async () => {
1013
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
1014
+ data: { id: itemId, orderReadyStatus: 'READY', orderReadyRevision: 1 },
1015
+ });
1016
+ const payload = validArticleItemPayload();
1017
+ const drawingClassificationEvidence = validDrawingClassificationEvidence();
1018
+
1019
+ const result = await setAiCartItemOrderReadyPayload({
1020
+ sessionId,
1021
+ itemId,
1022
+ orderReadyStatus: 'READY',
1023
+ orderReadyPayload: payload,
1024
+ orderReadySource: 'MCP',
1025
+ orderReadyMessages: [{ severity: 'info', message: 'Validated with order schema' }],
1026
+ drawingClassificationEvidence,
1027
+ });
1028
+
1029
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(
1030
+ `/sessions/${sessionId}/cart-items/${itemId}/order-ready-payload`,
1031
+ {
1032
+ orderReadyPayload: payload,
1033
+ orderReadyStatus: 'READY',
1034
+ orderReadyMessages: [{ severity: 'info', message: 'Validated with order schema' }],
1035
+ orderReadySource: 'MCP',
1036
+ }
1037
+ );
1038
+ expect(result).toContain('READY');
1039
+ });
1040
+
1041
+ it('set_ai_cart_item_order_ready_payload allows non-base READY without drawing classification evidence by default', async () => {
1042
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
1043
+ data: { id: itemId, orderReadyStatus: 'READY', orderReadyRevision: 1 },
1044
+ });
1045
+
1046
+ const result = await setAiCartItemOrderReadyPayload({
1047
+ sessionId,
1048
+ itemId,
1049
+ orderReadyStatus: 'READY',
1050
+ orderReadyPayload: validUpperArticleItemPayload(),
1051
+ orderReadySource: 'MCP',
1052
+ });
1053
+
1054
+ expect(result).toContain('READY');
1055
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalled();
1056
+ });
1057
+
1058
+ it('set_ai_cart_item_order_ready_payload rejects base READY without body-height exclusion evidence', async () => {
1059
+ const result = await setAiCartItemOrderReadyPayload({
1060
+ sessionId,
1061
+ itemId,
1062
+ orderReadyStatus: 'READY',
1063
+ orderReadyPayload: validArticleItemPayload(),
1064
+ orderReadySource: 'MCP',
1065
+ });
1066
+
1067
+ expect(result).toContain('READY rejected');
1068
+ expect(result).toContain('baseCabinetBodyHeightEvidence');
1069
+ expect(result).toContain('countertop and toekick_plinth excluded');
1070
+ expect(apiClientModule.aiDrawingClient.put).not.toHaveBeenCalled();
1071
+ });
1072
+
1073
+ it('set_ai_cart_item_order_ready_payload leaves visual evidence enforcement to rendered review by default', async () => {
1074
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
1075
+ data: { id: itemId, orderReadyStatus: 'READY', orderReadyRevision: 1 },
1076
+ });
1077
+
1078
+ const result = await setAiCartItemOrderReadyPayload({
1079
+ sessionId,
1080
+ itemId,
1081
+ orderReadyStatus: 'READY',
1082
+ orderReadyPayload: validArticleItemPayload(),
1083
+ orderReadySource: 'MCP',
1084
+ drawingClassificationEvidence: {
1085
+ ...validDrawingClassificationEvidence(),
1086
+ primaryViewPlaneRole: 'return_side_face',
1087
+ frontFaceEvidence: 'Agent vision review says this is a perpendicular side view / return side face.',
1088
+ frontFaceFeatures: ['side_panel_or_return_visible'],
1089
+ semanticTypeEvidence: 'Side-view geometry only; not a primary front-facing cabinet in the north elevation.',
1090
+ },
1091
+ });
1092
+
1093
+ expect(result).toContain('READY');
1094
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalled();
1095
+ });
1096
+
1097
+ it('set_ai_cart_item_order_ready_payload writes needs-configuration and blocked messages safely', async () => {
1098
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
1099
+ data: { id: itemId, orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION' },
1100
+ });
1101
+
1102
+ const needsConfig = await setAiCartItemOrderReadyPayload({
1103
+ sessionId,
1104
+ itemId,
1105
+ orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION',
1106
+ orderReadyMessages: [
1107
+ {
1108
+ severity: 'error',
1109
+ field: 'caseMaterial',
1110
+ message:
1111
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; ask the user for caseMaterial.',
1112
+ },
1113
+ ],
1114
+ orderReadySource: 'MCP',
1115
+ });
1116
+
1117
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(
1118
+ `/sessions/${sessionId}/cart-items/${itemId}/order-ready-payload`,
1119
+ {
1120
+ orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION',
1121
+ orderReadyMessages: [
1122
+ {
1123
+ severity: 'error',
1124
+ field: 'caseMaterial',
1125
+ message:
1126
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; ask the user for caseMaterial.',
1127
+ },
1128
+ ],
1129
+ orderReadySource: 'MCP',
1130
+ }
1131
+ );
1132
+ expect(needsConfig).toContain('NEEDS_AGENT_CONFIGURATION');
1133
+
1134
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
1135
+ data: { id: itemId, orderReadyStatus: 'BLOCKED' },
1136
+ });
1137
+ const blocked = await setAiCartItemOrderReadyPayload({
1138
+ sessionId,
1139
+ itemId,
1140
+ orderReadyStatus: 'BLOCKED',
1141
+ orderReadyMessages: [
1142
+ {
1143
+ severity: 'blocker',
1144
+ field: 'serialNumber',
1145
+ message:
1146
+ 'Tried get_article_context and get_article_options but no catalog match was available; saved settings not applicable until a valid serialNumber is selected.',
1147
+ },
1148
+ ],
1149
+ });
1150
+
1151
+ expect(blocked).toContain('BLOCKED');
1152
+ });
1153
+
1154
+ it('set_ai_cart_item_order_ready_payload persists bare needs-configuration without prose evidence gating', async () => {
1155
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({
1156
+ data: { id: itemId, orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION' },
1157
+ });
1158
+
1159
+ const result = await setAiCartItemOrderReadyPayload({
1160
+ sessionId,
1161
+ itemId,
1162
+ orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION',
1163
+ orderReadyMessages: [
1164
+ { severity: 'error', field: 'caseMaterial', message: 'Ask the user for case material.' },
1165
+ ],
1166
+ orderReadySource: 'MCP',
1167
+ });
1168
+
1169
+ expect(result).toContain('NEEDS_AGENT_CONFIGURATION');
1170
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(
1171
+ `/sessions/${sessionId}/cart-items/${itemId}/order-ready-payload`,
1172
+ expect.objectContaining({
1173
+ orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION',
1174
+ orderReadyMessages: [
1175
+ { severity: 'error', field: 'caseMaterial', message: 'Ask the user for case material.' },
1176
+ ],
1177
+ })
1178
+ );
1179
+ });
1180
+
1181
+ it('clear_ai_cart_item_order_ready_payload calls the session item clear endpoint', async () => {
1182
+ vi.mocked(apiClientModule.aiDrawingClient.delete).mockResolvedValue({
1183
+ data: { id: itemId, orderReadyStatus: 'NOT_PREPARED' },
1184
+ });
1185
+
1186
+ const result = await clearAiCartItemOrderReadyPayload({ sessionId, itemId });
1187
+
1188
+ expect(apiClientModule.aiDrawingClient.delete).toHaveBeenCalledWith(
1189
+ `/sessions/${sessionId}/cart-items/${itemId}/order-ready-payload`
1190
+ );
1191
+ expect(result).toContain('NOT_PREPARED');
1192
+ });
1193
+
1194
+ it('import_redux_cart_items_to_ai_cart posts rich Redux items to the session import endpoint', async () => {
1195
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({
1196
+ data: { sessionId, createdCount: 1, updatedCount: 0 },
1197
+ });
1198
+ const input = validReduxCartImportInput(sessionId);
1199
+
1200
+ const result = await importReduxCartItemsToAiCart(input);
1201
+
1202
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith(
1203
+ `/sessions/${sessionId}/cart-items/import-redux`,
1204
+ {
1205
+ items: [
1206
+ expect.objectContaining({
1207
+ frontendItemId: 'frontend-1',
1208
+ positionName: 'BASE_01',
1209
+ originalItemPayload: input.items[0].originalItemPayload,
1210
+ orderReadyStatus: 'READY',
1211
+ orderReadyPayload: input.items[0].orderReadyPayload,
1212
+ }),
1213
+ ],
1214
+ }
1215
+ );
1216
+ expect(result).toContain('createdCount');
1217
+ });
1218
+
1219
+ it('replace_ai_cabinet_coordinates puts coordinate replacement payload', async () => {
1220
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: [{ positionName: 'BASE_01' }] });
1221
+
1222
+ const result = await replaceAiCabinetCoordinates({
1223
+ sessionId,
1224
+ coordinates: [
1225
+ {
1226
+ positionName: 'BASE_01',
1227
+ aiCartItemId: itemId,
1228
+ x: 12,
1229
+ y: 24,
1230
+ z: 4,
1231
+ rotation: 90,
1232
+ source: 'MCP',
1233
+ coordinateEvidence: validCoordinateEvidence(),
1234
+ },
1235
+ ],
1236
+ });
1237
+
1238
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(`/sessions/${sessionId}/coordinates`, {
1239
+ coordinates: [
1240
+ {
1241
+ positionName: 'BASE_01',
1242
+ aiCartItemId: itemId,
1243
+ x: 12,
1244
+ y: 24,
1245
+ z: 4,
1246
+ rotation: 90,
1247
+ source: 'MCP',
1248
+ },
1249
+ ],
1250
+ });
1251
+ expect(result).toContain('BASE_01');
1252
+ });
1253
+
1254
+ it('patch_ai_cabinet_coordinates patches coordinate payloads by positionName', async () => {
1255
+ vi.mocked(apiClientModule.aiDrawingClient.patch).mockResolvedValue({ data: [{ positionName: 'BASE_01', rotation: 180 }] });
1256
+
1257
+ const result = await patchAiCabinetCoordinates({
1258
+ sessionId,
1259
+ coordinates: [
1260
+ {
1261
+ positionName: 'BASE_01',
1262
+ x: 33,
1263
+ y: 44,
1264
+ z: 6,
1265
+ rotation: 180,
1266
+ source: 'MCP',
1267
+ coordinateEvidence: validCoordinateEvidence(),
1268
+ },
1269
+ ],
1270
+ });
1271
+
1272
+ expect(apiClientModule.aiDrawingClient.patch).toHaveBeenCalledWith(`/sessions/${sessionId}/coordinates`, {
1273
+ coordinates: [
1274
+ {
1275
+ positionName: 'BASE_01',
1276
+ x: 33,
1277
+ y: 44,
1278
+ z: 6,
1279
+ rotation: 180,
1280
+ source: 'MCP',
1281
+ },
1282
+ ],
1283
+ });
1284
+ expect(result).toContain('BASE_01');
1285
+ });
1286
+
1287
+ it('delete_ai_cabinet_coordinates posts to the coordinate delete endpoint', async () => {
1288
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({ data: { deletedPositionNames: ['BASE_01'] } });
1289
+
1290
+ const result = await deleteAiCabinetCoordinates({ sessionId, positionNames: ['BASE_01'] });
1291
+
1292
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith(`/sessions/${sessionId}/coordinates/delete`, {
1293
+ positionNames: ['BASE_01'],
1294
+ });
1295
+ expect(result).toContain('BASE_01');
1296
+ });
1297
+
1298
+ it('AI drawing tool input schemas reject invalid UUIDs, canvas types, and rotations', () => {
1299
+ expect(AiDrawingSessionCreateInputSchema.safeParse({ activeAiCartId: 'bad' }).success).toBe(false);
1300
+ expect(LinkAiCartToDrawingSessionInputSchema.safeParse({ sessionId: 'bad', cartId }).success).toBe(false);
1301
+ expect(AiCanvasMetadataInputSchema.safeParse({ sessionId, canvasType: 'ceiling' }).success).toBe(false);
1302
+ expect(AiCanvasImageInputSchema.safeParse({ sessionId, canvasType: 'plan', imageType: 'thumbnail', s3Key: 'a' }).success).toBe(false);
1303
+ expect(AiCabinetCoordinateInputSchema.safeParse({ positionName: 'BASE_01', x: 1, y: 2, rotation: 45 }).success).toBe(false);
1304
+ expect(AiCabinetCoordinatePatchInputSchema.safeParse({ positionName: 'BASE_01', rotation: 45 }).success).toBe(false);
1305
+ expect(AiCabinetCoordinatesReplaceInputSchema.safeParse({ sessionId: 'bad', coordinates: [] }).success).toBe(false);
1306
+ expect(AiCabinetCoordinatesPatchInputListSchema.safeParse({ sessionId, coordinates: [] }).success).toBe(false);
1307
+ expect(AiCabinetCoordinatesDeleteInputSchema.safeParse({ sessionId, positionNames: [] }).success).toBe(false);
1308
+ expect(AiCabinetCoordinatesReplaceInputSchema.safeParse({
1309
+ sessionId,
1310
+ coordinates: [{ positionName: 'BASE_01', x: 1, y: 2, source: 'MCP' }],
1311
+ }).success).toBe(true);
1312
+ expect(AiCabinetCoordinatesReplaceInputSchema.safeParse({
1313
+ sessionId,
1314
+ coordinates: [{ positionName: 'BASE_01', x: 1, y: 2, source: 'MCP', coordinateEvidence: validCoordinateEvidence() }],
1315
+ }).success).toBe(true);
1316
+ });
1317
+
1318
+ it('AI cabinet operation schemas validate UUIDs and supported fields', () => {
1319
+ expect(DuplicateAiCabinetInputSchema.safeParse({ sessionId: 'bad', itemId }).success).toBe(false);
1320
+ expect(DuplicateAiCabinetInputSchema.safeParse({ sessionId, itemId: 'bad' }).success).toBe(false);
1321
+ expect(ConfigureAiCabinetInputSchema.safeParse({ sessionId: 'bad', itemId, width: 36 }).success).toBe(false);
1322
+ expect(DeleteAiCabinetInputSchema.safeParse({ sessionId, itemId: 'bad' }).success).toBe(false);
1323
+
1324
+ expect(DuplicateAiCabinetInputSchema.safeParse({
1325
+ sessionId,
1326
+ itemId,
1327
+ positionName: 'BASE_02',
1328
+ sourceCanvasType: 'east',
1329
+ sourcePlacementPositionName: 'BASE_01',
1330
+ placementOffsetX: 12,
1331
+ placementOffsetY: 8,
1332
+ source: 'MCP',
1333
+ }).success).toBe(true);
1334
+
1335
+ expect(DuplicateAiCabinetInputSchema.safeParse({
1336
+ sessionId,
1337
+ itemId,
1338
+ sourceCanvasType: 'ceiling',
1339
+ }).success).toBe(false);
1340
+
1341
+ expect(ConfigureAiCabinetInputSchema.safeParse({
1342
+ sessionId,
1343
+ itemId,
1344
+ positionName: 'BASE_02',
1345
+ displayName: 'Configured',
1346
+ serialNumber: 'SN-2',
1347
+ frontendItemId: 'frontend-2',
1348
+ originalFrontendItemId: 'original-2',
1349
+ originalPositionName: 'BASE_01',
1350
+ groupId: 'run-1',
1351
+ quantity: 1,
1352
+ originalQuantity: 2,
1353
+ width: 36,
1354
+ height: 40,
1355
+ depth: 25,
1356
+ originalItemPayload: { configured: true },
1357
+ source: 'MCP',
1358
+ }).success).toBe(true);
1359
+
1360
+ expect(ConfigureAiCabinetInputSchema.safeParse({
1361
+ sessionId,
1362
+ itemId,
1363
+ unsupportedField: true,
1364
+ }).success).toBe(false);
1365
+
1366
+ expect(ConfigureAiCabinetInputSchema.safeParse({
1367
+ sessionId,
1368
+ itemId,
1369
+ quantity: 2,
1370
+ }).success).toBe(false);
1371
+ });
1372
+
1373
+ it('AI canvas placement schemas validate UUIDs canvas types required fields and positive dimensions', () => {
1374
+ expect(AiCanvasCabinetPlacementsByCanvasInputSchema.safeParse({ sessionId: 'bad', canvasType: 'plan' }).success).toBe(false);
1375
+ expect(AiCanvasCabinetPlacementsByCanvasInputSchema.safeParse({ sessionId, canvasType: 'ceiling' }).success).toBe(false);
1376
+
1377
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1378
+ positionName: 'BASE_01',
1379
+ centerX: 10,
1380
+ centerY: 20,
1381
+ width: 30,
1382
+ height: 40,
1383
+ }).success).toBe(true);
1384
+
1385
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1386
+ centerX: 10,
1387
+ centerY: 20,
1388
+ width: 30,
1389
+ height: 40,
1390
+ }).success).toBe(false);
1391
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1392
+ positionName: 'BASE_01',
1393
+ centerY: 20,
1394
+ width: 30,
1395
+ height: 40,
1396
+ }).success).toBe(false);
1397
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1398
+ positionName: 'BASE_01',
1399
+ centerX: 10,
1400
+ centerY: 20,
1401
+ width: 0,
1402
+ height: 40,
1403
+ }).success).toBe(false);
1404
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1405
+ positionName: 'BASE_01',
1406
+ centerX: 10,
1407
+ centerY: 20,
1408
+ width: 30,
1409
+ height: -1,
1410
+ }).success).toBe(false);
1411
+ });
1412
+
1413
+ it('AI canvas placement schemas validate optional placement fields strictly', () => {
1414
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1415
+ positionName: 'BASE_01',
1416
+ centerX: 10,
1417
+ centerY: 20,
1418
+ width: 30,
1419
+ height: 40,
1420
+ aiCartItemId: itemId,
1421
+ rotation: 12.5,
1422
+ placementSpace: 'IMAGE_PIXELS',
1423
+ basisImageWidthPx: 1000,
1424
+ basisImageHeightPx: 800,
1425
+ source: 'MCP',
1426
+ }).success).toBe(true);
1427
+
1428
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1429
+ positionName: 'BASE_01',
1430
+ centerX: 10,
1431
+ centerY: 20,
1432
+ width: 30,
1433
+ height: 40,
1434
+ placementSpace: 'ROOM_INCHES',
1435
+ }).success).toBe(false);
1436
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1437
+ positionName: 'BASE_01',
1438
+ centerX: 10,
1439
+ centerY: 20,
1440
+ width: 30,
1441
+ height: 40,
1442
+ basisImageWidthPx: 0,
1443
+ }).success).toBe(false);
1444
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1445
+ positionName: 'BASE_01',
1446
+ centerX: 10,
1447
+ centerY: 20,
1448
+ width: 30,
1449
+ height: 40,
1450
+ basisImageHeightPx: -5,
1451
+ }).success).toBe(false);
1452
+ expect(AiCanvasCabinetPlacementInputSchema.safeParse({
1453
+ positionName: 'BASE_01',
1454
+ centerX: 10,
1455
+ centerY: 20,
1456
+ width: 30,
1457
+ height: 40,
1458
+ source: 'OTHER',
1459
+ }).success).toBe(false);
1460
+ });
1461
+
1462
+ it('AI canvas placement operation schemas reject invalid lists and unknown fields', () => {
1463
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1464
+ sessionId,
1465
+ canvasType: 'plan',
1466
+ placements: [{ positionName: 'BASE_01', centerX: 1, centerY: 2, width: 3, height: 4 }],
1467
+ }).success).toBe(true);
1468
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1469
+ sessionId,
1470
+ canvasType: 'plan',
1471
+ placements: [{
1472
+ positionName: 'BASE_01',
1473
+ centerX: 1,
1474
+ centerY: 2,
1475
+ width: 3,
1476
+ height: 4,
1477
+ viewAssociationEvidence: sameCanvasReviewedPlanEvidence,
1478
+ }],
1479
+ }).success).toBe(true);
1480
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1481
+ sessionId,
1482
+ canvasType: 'plan',
1483
+ placements: [{
1484
+ positionName: 'BASE_01',
1485
+ centerX: 10,
1486
+ centerY: 2,
1487
+ width: 3,
1488
+ height: 4,
1489
+ viewAssociationEvidence: sameCanvasReviewedPlanEvidence,
1490
+ }],
1491
+ }).success).toBe(true);
1492
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1493
+ sessionId,
1494
+ canvasType: 'plan',
1495
+ placements: [{
1496
+ positionName: 'BASE_01',
1497
+ centerX: 1,
1498
+ centerY: 2,
1499
+ width: 3,
1500
+ height: 4,
1501
+ viewAssociationEvidence: {
1502
+ ...planViewAssociationEvidence,
1503
+ selectedPlanObjectBboxPx: undefined,
1504
+ },
1505
+ }],
1506
+ }).success).toBe(true);
1507
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1508
+ sessionId,
1509
+ canvasType: 'plan',
1510
+ placements: [{
1511
+ positionName: 'BASE_01',
1512
+ centerX: 1,
1513
+ centerY: 2,
1514
+ width: 3,
1515
+ height: 4,
1516
+ viewAssociationEvidence: {
1517
+ ...planViewAssociationEvidence,
1518
+ selectedPlanObjectIds: ['P_REF', 'P_REF_BASE'],
1519
+ },
1520
+ }],
1521
+ }).success).toBe(true);
1522
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1523
+ sessionId,
1524
+ canvasType: 'plan',
1525
+ placements: [{
1526
+ positionName: 'BASE_01',
1527
+ centerX: 1,
1528
+ centerY: 2,
1529
+ width: 3,
1530
+ height: 4,
1531
+ viewAssociationEvidence: {
1532
+ ...planViewAssociationEvidence,
1533
+ selectedPlanObjectIds: ['P_REF'],
1534
+ sourcePlanObject: validReviewedPlanObject('P_REF', 'appliance_opening'),
1535
+ anchorPlanObjectIds: undefined,
1536
+ },
1537
+ }],
1538
+ }).success).toBe(true);
1539
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1540
+ sessionId,
1541
+ canvasType: 'plan',
1542
+ placements: [{
1543
+ positionName: 'BASE_01',
1544
+ centerX: 1,
1545
+ centerY: 2,
1546
+ width: 3,
1547
+ height: 4,
1548
+ viewAssociationEvidence: {
1549
+ ...planViewAssociationEvidence,
1550
+ sourcePlanObject: undefined,
1551
+ },
1552
+ }],
1553
+ }).success).toBe(true);
1554
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1555
+ sessionId,
1556
+ canvasType: 'plan',
1557
+ placements: [{
1558
+ positionName: 'BASE_01',
1559
+ centerX: 1,
1560
+ centerY: 2,
1561
+ width: 3,
1562
+ height: 4,
1563
+ viewAssociationEvidence: {
1564
+ ...planViewAssociationEvidence,
1565
+ selectedPlanObjectRole: 'physical_plan_footprint',
1566
+ sourcePlanObject: validReviewedPlanObject('P_REF', 'appliance_opening'),
1567
+ },
1568
+ }],
1569
+ }).success).toBe(true);
1570
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1571
+ sessionId,
1572
+ canvasType: 'plan',
1573
+ placements: [{
1574
+ positionName: 'UPPER_SIDE',
1575
+ centerX: 1,
1576
+ centerY: 2,
1577
+ width: 3,
1578
+ height: 4,
1579
+ viewAssociationEvidence: {
1580
+ ...planViewAssociationEvidence,
1581
+ selectedPlanObjectRole: 'dashed_overhead_only',
1582
+ },
1583
+ }],
1584
+ }).success).toBe(true);
1585
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1586
+ sessionId,
1587
+ canvasType: 'plan',
1588
+ placements: [{
1589
+ positionName: 'BASE_01',
1590
+ centerX: 1,
1591
+ centerY: 2,
1592
+ width: 3,
1593
+ height: 4,
1594
+ viewAssociationEvidence: {
1595
+ ...planViewAssociationEvidence,
1596
+ basis: 'user_confirmed',
1597
+ evidence: 'User provided the plan/elevation drawings and asked the agent to verify the placement.',
1598
+ },
1599
+ }],
1600
+ }).success).toBe(true);
1601
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1602
+ sessionId,
1603
+ canvasType: 'plan',
1604
+ placements: [{
1605
+ positionName: 'BASE_01',
1606
+ centerX: 1,
1607
+ centerY: 2,
1608
+ width: 3,
1609
+ height: 4,
1610
+ viewAssociationEvidence: {
1611
+ ...planViewAssociationEvidence,
1612
+ basis: 'user_confirmed',
1613
+ selectedPlanObjectRole: 'dashed_overhead_only',
1614
+ userExplicitConfirmation: true,
1615
+ userConfirmationText: 'User confirmed this exact dashed overhead object should be treated as a physical plan placement for BASE_01.',
1616
+ evidence: 'User confirmed this exact dashed overhead object should be treated as a physical plan placement; association.selectedPlanObjects contains P_REF and agent visual image analysis verified the bbox.',
1617
+ },
1618
+ }],
1619
+ }).success).toBe(true);
1620
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1621
+ sessionId,
1622
+ canvasType: 'plan',
1623
+ placements: [],
1624
+ }).success).toBe(false);
1625
+ expect(AiCanvasCabinetPlacementsReplaceInputSchema.safeParse({
1626
+ sessionId,
1627
+ canvasType: 'plan',
1628
+ }).success).toBe(false);
1629
+ expect(AiCanvasCabinetPlacementsUpsertInputSchema.safeParse({
1630
+ sessionId,
1631
+ canvasType: 'east',
1632
+ placements: [{ positionName: 'BASE_01', centerX: 1, centerY: 2, width: 3, height: 4 }],
1633
+ }).success).toBe(true);
1634
+ expect(AiCanvasCabinetPlacementsUpsertInputSchema.safeParse({
1635
+ sessionId,
1636
+ canvasType: 'east',
1637
+ placements: [],
1638
+ }).success).toBe(false);
1639
+ expect(AiCanvasCabinetPlacementsPatchInputSchema.safeParse({
1640
+ sessionId,
1641
+ canvasType: 'east',
1642
+ placements: [{ positionName: 'BASE_01', centerX: 12 }],
1643
+ }).success).toBe(true);
1644
+ expect(AiCanvasCabinetPlacementPatchSchema.safeParse({
1645
+ positionName: 'BASE_01',
1646
+ unsupportedField: true,
1647
+ }).success).toBe(false);
1648
+ expect(AiCanvasCabinetPlacementsPatchInputSchema.safeParse({
1649
+ sessionId,
1650
+ canvasType: 'east',
1651
+ placements: [],
1652
+ }).success).toBe(false);
1653
+ expect(AiCanvasCabinetPlacementsDeleteInputSchema.safeParse({
1654
+ sessionId,
1655
+ canvasType: 'east',
1656
+ positionNames: [],
1657
+ }).success).toBe(false);
1658
+ expect(AiCanvasCabinetPlacementsDeleteInputSchema.safeParse({
1659
+ sessionId,
1660
+ canvasType: 'east',
1661
+ positionNames: [''],
1662
+ }).success).toBe(false);
1663
+ });
1664
+
1665
+ it('projection calibration schema accepts valid plan calibration', () => {
1666
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(validPlanProjectionCalibration()).success).toBe(true);
1667
+ expect(AiCanvasMetaSchema.safeParse({
1668
+ projectionCalibration: validPlanProjectionCalibration(),
1669
+ legacyDisplayMetadata: { stillFlexible: true },
1670
+ }).success).toBe(true);
1671
+ });
1672
+
1673
+ it('projection calibration schema accepts valid east elevation calibration', () => {
1674
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(validElevationProjectionCalibration('east', 90)).success).toBe(true);
1675
+ expect(AiCanvasMetaSchema.safeParse({
1676
+ projectionCalibration: validElevationProjectionCalibration('east', 90),
1677
+ elevationVisibility: { legacy: true },
1678
+ }).success).toBe(true);
1679
+ });
1680
+
1681
+ it('projection calibration schema rejects invalid schemaVersion', () => {
1682
+ const calibration = validPlanProjectionCalibration();
1683
+ calibration.schemaVersion = 'wrong-version';
1684
+
1685
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(calibration).success).toBe(false);
1686
+ expect(AiCanvasMetaSchema.safeParse({ projectionCalibration: calibration }).success).toBe(false);
1687
+ });
1688
+
1689
+ it('projection calibration schema rejects invalid direction values', () => {
1690
+ const calibration = validPlanProjectionCalibration();
1691
+ calibration.roomAxis.xPositiveDirection = 'diagonal';
1692
+
1693
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(calibration).success).toBe(false);
1694
+ });
1695
+
1696
+ it('projection calibration schema rejects invalid rotations', () => {
1697
+ const calibration = validElevationProjectionCalibration('east', 90);
1698
+ calibration.visibilityRule.rotations = [45];
1699
+
1700
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(calibration).success).toBe(false);
1701
+ });
1702
+
1703
+ it('projection calibration schema rejects non-array include and exclude position names', () => {
1704
+ const includeCalibration = validElevationProjectionCalibration('east', 90);
1705
+ includeCalibration.visibilityRule.includePositionNames = 'BASE_01';
1706
+ const excludeCalibration = validElevationProjectionCalibration('east', 90);
1707
+ excludeCalibration.visibilityRule.excludePositionNames = 'BASE_01';
1708
+
1709
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(includeCalibration).success).toBe(false);
1710
+ expect(AiCanvasProjectionCalibrationSchema.safeParse(excludeCalibration).success).toBe(false);
1711
+ });
1712
+
1713
+ it('canvas metadata accepts free-form association notes outside projectionCalibration', () => {
1714
+ expect(AiCanvasMetadataInputSchema.safeParse({
1715
+ sessionId,
1716
+ canvasType: 'west',
1717
+ activeCanvases: ['plan', 'north', 'west'],
1718
+ canvasMeta: {
1719
+ schemaVersion: 'ai-canvas-metadata/v1',
1720
+ sourceFile: 'west.png',
1721
+ imageSizePx: { width: 1665, height: 944 },
1722
+ viewAssociation: {
1723
+ elevationMarker: '2',
1724
+ planMarkerSide: 'left',
1725
+ orientationEvidence: 'Manual visual association from plan marker and appliance anchors.',
1726
+ },
1727
+ z0ReferencePx: 828,
1728
+ floorLineEvidence: 'Base cabinet fronts end above the plinth/toe-kick band.',
1729
+ },
1730
+ }).success).toBe(true);
1731
+ });
1732
+
1733
+ it('metadata tool description tells agents not to put free-form notes in projectionCalibration', () => {
1734
+ const metadataTool = aiDrawingTools.find((tool) => tool.name === 'upsert_ai_canvas_metadata');
1735
+
1736
+ expect(metadataTool?.description).toContain('projectionCalibration is not free-form notes');
1737
+ expect(metadataTool?.description).toContain('For direct IMAGE_PIXELS per-canvas placements, omit projectionCalibration');
1738
+ expect(metadataTool?.description).toContain('sourceFile, imageSizePx, plan/elevation marker association, orientation evidence, z0 notes');
1739
+ expect(metadataTool?.description).toContain('outside projectionCalibration');
1740
+ });
1741
+
1742
+ it('coordinate tool descriptions mention canonical room/global coordinates', () => {
1743
+ const coordinateToolNames = [
1744
+ 'replace_ai_cabinet_coordinates',
1745
+ 'patch_ai_cabinet_coordinates',
1746
+ 'delete_ai_cabinet_coordinates',
1747
+ ];
1748
+
1749
+ for (const name of coordinateToolNames) {
1750
+ const tool = aiDrawingTools.find((candidate) => candidate.name === name);
1751
+ expect(tool?.description).toContain('canonical room/global inches');
1752
+ expect(tool?.description).toContain('Plan projects canonical x/y');
1753
+ }
1754
+ });
1755
+
1756
+ it('metadata tool description mentions canvasMeta projection calibration', () => {
1757
+ const metadataTool = aiDrawingTools.find((tool) => tool.name === 'upsert_ai_canvas_metadata');
1758
+
1759
+ expect(metadataTool?.description).toContain('canvasMeta.projectionCalibration');
1760
+ expect(metadataTool?.description).toContain('ai-canvas-calibration/v1');
1761
+ });
1762
+
1763
+ it('placement tool descriptions include per-canvas canvas-local guidance', () => {
1764
+ const placementToolNames = [
1765
+ 'get_ai_canvas_cabinet_placements',
1766
+ 'get_ai_canvas_cabinet_placements_by_canvas',
1767
+ 'replace_ai_canvas_cabinet_placements',
1768
+ 'upsert_ai_canvas_cabinet_placements',
1769
+ 'patch_ai_canvas_cabinet_placements',
1770
+ 'delete_ai_canvas_cabinet_placements',
1771
+ ];
1772
+
1773
+ for (const name of placementToolNames) {
1774
+ const tool = aiDrawingTools.find((candidate) => candidate.name === name);
1775
+ expect(tool?.description).toContain('per-canvas');
1776
+ expect(tool?.description).toContain('canvas-local visual rectangles');
1777
+ expect(tool?.description).toContain('active linked AI cart');
1778
+ expect(tool?.description).toContain('link to an active AI cart item by aiCartItemId or exact positionName');
1779
+ expect(tool?.description).toContain('read back get_ai_drawing_state or get_ai_cart_by_session');
1780
+ expect(tool?.description).toContain('returned backend aiCartItemId values before writing placements');
1781
+ expect(tool?.description).toContain('If cart sync failed or activeCart/items are missing, stop');
1782
+ expect(tool?.description).toContain('original image pixels');
1783
+ expect(tool?.description).toContain('width/height in these placement tools are visual rectangle pixel dimensions');
1784
+ expect(tool?.description).toContain('Plan rectangles are footprints');
1785
+ expect(tool?.description).toContain('elevation rectangles are primary front faces');
1786
+ expect(tool?.description).toContain('Reuse the same aiCartItemId/positionName');
1787
+ expect(tool?.description).toContain('A plan placement does not make the cabinet appear on an elevation');
1788
+ expect(tool?.description).toContain('every visible cabinet face on north/south/east/west must also get its own placement');
1789
+ expect(tool?.description).toContain('Do not derive plan placements from elevation boxes');
1790
+ expect(tool?.description).toContain('Do not rely on filenames, compass words, object order, or catalog labels alone');
1791
+ expect(tool?.description).toContain('render and inspect the stored session');
1792
+ expect(tool?.description).toContain('use native vision and user confirmation when ambiguous');
1793
+ expect(tool?.description).toContain('do not mutate Redux cart');
1794
+ expect(tool?.description).toContain('stored carts');
1795
+ expect(tool?.description).toContain('production coordinates');
1796
+ }
1797
+ });
1798
+
1799
+ it('cabinet operation tool descriptions include one physical cabinet and shared identity guidance', () => {
1800
+ const operationToolNames = [
1801
+ 'duplicate_ai_cabinet',
1802
+ 'configure_ai_cabinet',
1803
+ 'delete_ai_cabinet',
1804
+ ];
1805
+
1806
+ for (const name of operationToolNames) {
1807
+ const tool = aiDrawingTools.find((candidate) => candidate.name === name);
1808
+ expect(tool?.description).toContain('one physical cabinet');
1809
+ expect(tool?.description).toContain('shared aiCartItemId');
1810
+ expect(tool?.description).toContain('same cabinet must use the same shared aiCartItemId and positionName');
1811
+ expect(tool?.description).toContain('Do not create separate physical AI cart items');
1812
+ expect(tool?.description).toContain('per-canvas placement tools for placement geometry');
1813
+ expect(tool?.description).toContain('do not mutate the Redux cart');
1814
+ expect(tool?.description).toContain('stored carts');
1815
+ expect(tool?.description).toContain('production coordinate tables');
1816
+ expect(tool?.description).toContain('CAD/XML');
1817
+ }
1818
+
1819
+ expect(aiDrawingTools.find((tool) => tool.name === 'duplicate_ai_cabinet')?.description)
1820
+ .toContain('creates a new physical cabinet');
1821
+ expect(aiDrawingTools.find((tool) => tool.name === 'configure_ai_cabinet')?.description)
1822
+ .toContain('linked AI-only placement dimensions');
1823
+ expect(aiDrawingTools.find((tool) => tool.name === 'delete_ai_cabinet')?.description)
1824
+ .toContain('linked AI-only placements');
1825
+ });
1826
+
1827
+ it('AI drawing coordinate schemas preserve x y z and rotation', () => {
1828
+ const replacement = AiCabinetCoordinatesReplaceInputSchema.parse({
1829
+ sessionId,
1830
+ coordinates: [{ positionName: 'BASE_01', x: 1, y: 2, z: 3, rotation: 270 }],
1831
+ });
1832
+ const patch = AiCabinetCoordinatesPatchInputListSchema.parse({
1833
+ sessionId,
1834
+ coordinates: [{ positionName: 'BASE_01', x: 4, y: 5, z: 6, rotation: 180 }],
1835
+ });
1836
+
1837
+ expect(replacement.coordinates[0]).toEqual(expect.objectContaining({ x: 1, y: 2, z: 3, rotation: 270 }));
1838
+ expect(patch.coordinates[0]).toEqual(expect.objectContaining({ x: 4, y: 5, z: 6, rotation: 180 }));
1839
+ });
1840
+ });
1841
+
1842
+ describe('AI cart tool handlers', () => {
1843
+ const cartId = '11111111-1111-4111-8111-111111111111';
1844
+ const itemId = '22222222-2222-4222-8222-222222222222';
1845
+ const sessionId = '33333333-3333-4333-8333-333333333333';
1846
+
1847
+ beforeEach(() => vi.clearAllMocks());
1848
+
1849
+ it('create_ai_cart posts to the AI cart endpoint', async () => {
1850
+ vi.mocked(apiClientModule.aiDrawingClient.post).mockResolvedValue({ data: { id: cartId } });
1851
+
1852
+ const result = await createAiCart({
1853
+ aiDrawingSessionId: sessionId,
1854
+ sourceType: 'REDUX_SNAPSHOT',
1855
+ sourceOrderId: 'ORDER-1',
1856
+ sourceSavedCartId: 123,
1857
+ sourceSnapshotPayload: '{"cart":true}',
1858
+ });
1859
+
1860
+ expect(apiClientModule.aiDrawingClient.post).toHaveBeenCalledWith('/carts', {
1861
+ aiDrawingSessionId: sessionId,
1862
+ sourceType: 'REDUX_SNAPSHOT',
1863
+ sourceOrderId: 'ORDER-1',
1864
+ sourceSavedCartId: 123,
1865
+ sourceSnapshotPayload: '{"cart":true}',
1866
+ });
1867
+ expect(result).toContain(cartId);
1868
+ });
1869
+
1870
+ it('get_ai_cart calls the AI cart read endpoint', async () => {
1871
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({ data: { id: cartId } });
1872
+
1873
+ const result = await getAiCart({ cartId });
1874
+
1875
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(`/carts/${cartId}`);
1876
+ expect(result).toContain(cartId);
1877
+ });
1878
+
1879
+ it('get_ai_cart_by_session calls the AI cart session endpoint', async () => {
1880
+ vi.mocked(apiClientModule.aiDrawingClient.get).mockResolvedValue({ data: { aiDrawingSessionId: sessionId } });
1881
+
1882
+ const result = await getAiCartBySession({ sessionId });
1883
+
1884
+ expect(apiClientModule.aiDrawingClient.get).toHaveBeenCalledWith(`/carts/session/${sessionId}`);
1885
+ expect(result).toContain(sessionId);
1886
+ });
1887
+
1888
+ it('sync_ai_cart_snapshot accepts user explicitly requested saved preset evidence phrasing', () => {
1889
+ const parsed = AiCartSnapshotInputSchema.safeParse({
1890
+ cartId,
1891
+ items: [
1892
+ {
1893
+ positionName: 'B1',
1894
+ serialNumber: 'BC_4DR_1007',
1895
+ quantity: 1,
1896
+ width: 27.5,
1897
+ height: 34.5,
1898
+ depth: 24,
1899
+ },
1900
+ ],
1901
+ orderConfigurationWorkflow: {
1902
+ mode: 'USER_CONFIRMED_ORDER_READY',
1903
+ checkedSerialNumbers: ['BC_4DR_1007'],
1904
+ evidence:
1905
+ 'Called get_article_context and get_article_options for each selected serial number. Checked get_my_saved_settings and get_saved_settings_presets; user explicitly requested the saved preset Transitional - Paint-Prefin, savedSettingsId 26, using exact option strings from the preset.',
1906
+ },
1907
+ });
1908
+
1909
+ expect(parsed.success).toBe(true);
1910
+ });
1911
+
1912
+ it('sync_ai_cart_snapshot puts snapshot payload to the AI cart endpoint', async () => {
1913
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { id: cartId, cartRevision: 1 } });
1914
+
1915
+ const result = await syncAiCartSnapshot({
1916
+ cartId,
1917
+ sourceSnapshotPayload: '{"source":"redux"}',
1918
+ items: [
1919
+ {
1920
+ frontendItemId: 'frontend-1',
1921
+ serialNumber: 'B30',
1922
+ positionName: 'BASE_01',
1923
+ quantity: 2,
1924
+ width: 30,
1925
+ itemPayload: { original: true },
1926
+ },
1927
+ ],
1928
+ orderConfigurationWorkflow: {
1929
+ mode: 'USER_CONFIRMED_ORDER_READY',
1930
+ evidence:
1931
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user selected saved setting Modern Euro.',
1932
+ checkedSerialNumbers: ['B30'],
1933
+ },
1934
+ });
1935
+
1936
+ expect(apiClientModule.aiDrawingClient.put).toHaveBeenCalledWith(`/carts/${cartId}/snapshot`, {
1937
+ sourceSnapshotPayload: '{"source":"redux"}',
1938
+ items: [
1939
+ expect.objectContaining({
1940
+ frontendItemId: 'frontend-1',
1941
+ serialNumber: 'B30',
1942
+ positionName: 'BASE_01',
1943
+ quantity: 2,
1944
+ width: 30,
1945
+ originalItemPayload: JSON.stringify({
1946
+ frontendItemId: 'frontend-1',
1947
+ serialNumber: 'B30',
1948
+ positionName: 'BASE_01',
1949
+ quantity: 2,
1950
+ width: 30,
1951
+ original: true,
1952
+ }),
1953
+ }),
1954
+ ],
1955
+ });
1956
+ expect(result).toContain('cartRevision');
1957
+ });
1958
+
1959
+ it('sync_ai_cart_snapshot synthesizes original payload when itemPayload is omitted', async () => {
1960
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { id: cartId, cartRevision: 1 } });
1961
+
1962
+ await syncAiCartSnapshot({
1963
+ cartId,
1964
+ items: [
1965
+ {
1966
+ serialNumber: 'B30',
1967
+ positionName: 'BASE_01',
1968
+ displayName: '30 inch base',
1969
+ quantity: 1,
1970
+ width: 30,
1971
+ height: 34.5,
1972
+ depth: 24,
1973
+ },
1974
+ ],
1975
+ orderConfigurationWorkflow: {
1976
+ mode: 'USER_CONFIRMED_ORDER_READY',
1977
+ evidence:
1978
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user selected saved setting Modern Euro.',
1979
+ checkedSerialNumbers: ['B30'],
1980
+ },
1981
+ });
1982
+
1983
+ const request = vi.mocked(apiClientModule.aiDrawingClient.put).mock.calls[0][1] as any;
1984
+ expect(request.items[0]).toEqual(expect.objectContaining({
1985
+ serialNumber: 'B30',
1986
+ positionName: 'BASE_01',
1987
+ quantity: 1,
1988
+ width: 30,
1989
+ height: 34.5,
1990
+ depth: 24,
1991
+ }));
1992
+ expect(JSON.parse(request.items[0].originalItemPayload)).toEqual({
1993
+ serialNumber: 'B30',
1994
+ positionName: 'BASE_01',
1995
+ quantity: 1,
1996
+ width: 30,
1997
+ height: 34.5,
1998
+ depth: 24,
1999
+ displayName: '30 inch base',
2000
+ });
2001
+ });
2002
+
2003
+ it('sync_ai_cart_snapshot preserves drawing classification evidence in backend item payloads', async () => {
2004
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { id: cartId, cartRevision: 1 } });
2005
+
2006
+ await syncAiCartSnapshot({
2007
+ cartId,
2008
+ sourceSnapshotPayload: '{"source":"drawing takeoff"}',
2009
+ items: [
2010
+ {
2011
+ serialNumber: 'B30',
2012
+ positionName: 'BASE_01',
2013
+ quantity: 1,
2014
+ width: 30,
2015
+ itemPayload: { original: true },
2016
+ drawingClassificationEvidence: validDrawingClassificationEvidence(),
2017
+ },
2018
+ ],
2019
+ orderConfigurationWorkflow: {
2020
+ mode: 'USER_CONFIRMED_ORDER_READY',
2021
+ evidence:
2022
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user selected saved setting Modern Euro.',
2023
+ checkedSerialNumbers: ['B30'],
2024
+ },
2025
+ });
2026
+
2027
+ const request = vi.mocked(apiClientModule.aiDrawingClient.put).mock.calls[0][1] as any;
2028
+ const originalItemPayload = JSON.parse(request.items[0].originalItemPayload);
2029
+ expect(originalItemPayload.original).toBe(true);
2030
+ expect(originalItemPayload.__aiDrawingClassificationEvidence).toEqual(
2031
+ expect.objectContaining({ primaryViewPlaneRole: 'primary_elevation_face' })
2032
+ );
2033
+ });
2034
+
2035
+ it('sync_ai_cart_snapshot combines synthesized original payload with drawing classification evidence', async () => {
2036
+ vi.mocked(apiClientModule.aiDrawingClient.put).mockResolvedValue({ data: { id: cartId, cartRevision: 1 } });
2037
+
2038
+ await syncAiCartSnapshot({
2039
+ cartId,
2040
+ items: [
2041
+ {
2042
+ serialNumber: 'B30',
2043
+ positionName: 'BASE_01',
2044
+ quantity: 1,
2045
+ width: 30,
2046
+ drawingClassificationEvidence: validDrawingClassificationEvidence(),
2047
+ },
2048
+ ],
2049
+ orderConfigurationWorkflow: {
2050
+ mode: 'USER_CONFIRMED_ORDER_READY',
2051
+ evidence:
2052
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user selected saved setting Modern Euro.',
2053
+ checkedSerialNumbers: ['B30'],
2054
+ },
2055
+ });
2056
+
2057
+ const request = vi.mocked(apiClientModule.aiDrawingClient.put).mock.calls[0][1] as any;
2058
+ const originalItemPayload = JSON.parse(request.items[0].originalItemPayload);
2059
+ expect(originalItemPayload.serialNumber).toBe('B30');
2060
+ expect(originalItemPayload.positionName).toBe('BASE_01');
2061
+ expect(originalItemPayload.__aiDrawingClassificationEvidence).toEqual(
2062
+ expect.objectContaining({ primaryViewPlaneRole: 'primary_elevation_face' })
2063
+ );
2064
+ });
2065
+
2066
+ it('create_ai_cart schema accepts drawing source metadata without semantic evidence gating', async () => {
2067
+ const acceptedWithoutWorkflow = AiCartCreateInputSchema.safeParse({
2068
+ aiDrawingSessionId: sessionId,
2069
+ sourceType: 'REDUX_SNAPSHOT',
2070
+ sourceSnapshotPayload: JSON.stringify({
2071
+ source: 'overlay drawing takeoff',
2072
+ checkedSerialNumbers: ['UC_1D_L_1227'],
2073
+ items: [{ positionName: 'U1', serialNumber: 'UC_1D_L_1227' }],
2074
+ }),
2075
+ });
2076
+ const acceptedWithWorkflow = AiCartCreateInputSchema.safeParse({
2077
+ aiDrawingSessionId: sessionId,
2078
+ sourceType: 'REDUX_SNAPSHOT',
2079
+ sourceSnapshotPayload: JSON.stringify({
2080
+ source: 'overlay drawing takeoff',
2081
+ checkedSerialNumbers: ['UC_1D_L_1227'],
2082
+ }),
2083
+ orderConfigurationWorkflow: {
2084
+ mode: 'USER_CONFIRMED_ORDER_READY',
2085
+ evidence:
2086
+ 'Called get_article_context and get_article_options for UC_1D_L_1227; checked get_my_saved_settings and get_saved_settings_presets; user selected saved setting Modern Euro.',
2087
+ checkedSerialNumbers: ['UC_1D_L_1227'],
2088
+ },
2089
+ });
2090
+
2091
+ expect(acceptedWithoutWorkflow.success).toBe(true);
2092
+ expect(acceptedWithWorkflow.success).toBe(true);
2093
+ });
2094
+
2095
+ it('patch_ai_cart_item patches one canonical AI cart item', async () => {
2096
+ vi.mocked(apiClientModule.aiDrawingClient.patch).mockResolvedValue({ data: { id: cartId, cartRevision: 2 } });
2097
+
2098
+ const result = await patchAiCartItem({
2099
+ cartId,
2100
+ itemId,
2101
+ patch: {
2102
+ positionName: 'BASE_02',
2103
+ displayName: 'Updated Base',
2104
+ itemPayload: { patched: true },
2105
+ },
2106
+ });
2107
+
2108
+ expect(apiClientModule.aiDrawingClient.patch).toHaveBeenCalledWith(`/carts/${cartId}/items/${itemId}`, {
2109
+ positionName: 'BASE_02',
2110
+ displayName: 'Updated Base',
2111
+ originalItemPayload: '{"patched":true}',
2112
+ });
2113
+ expect(result).toContain('cartRevision');
2114
+ });
2115
+
2116
+ it('delete_ai_cart_item deletes one canonical AI cart item', async () => {
2117
+ vi.mocked(apiClientModule.aiDrawingClient.delete).mockResolvedValue({ data: { id: cartId, items: [] } });
2118
+
2119
+ const result = await deleteAiCartItem({ cartId, itemId });
2120
+
2121
+ expect(apiClientModule.aiDrawingClient.delete).toHaveBeenCalledWith(`/carts/${cartId}/items/${itemId}`);
2122
+ expect(result).toContain(cartId);
2123
+ });
2124
+
2125
+ it('tool input schemas reject invalid UUIDs', () => {
2126
+ const toolByName = Object.fromEntries(aiDrawingTools.map((tool) => [tool.name, tool]));
2127
+
2128
+ expect(toolByName.get_ai_cart.inputSchema.safeParse({ cartId: 'bad' }).success).toBe(false);
2129
+ expect(toolByName.get_ai_cart_by_session.inputSchema.safeParse({ sessionId: 'bad' }).success).toBe(false);
2130
+ expect(toolByName.upload_ai_canvas_image.inputSchema.safeParse({ sessionId: 'bad', canvasType: 'plan', imageType: 'background', imagePath: 'D:\\tmp\\file.png' }).success).toBe(false);
2131
+ expect(toolByName.delete_ai_canvas_image.inputSchema.safeParse({ sessionId: 'bad', canvasType: 'plan', imageType: 'background' }).success).toBe(false);
2132
+ expect(toolByName.patch_ai_cart_item.inputSchema.safeParse({ cartId, itemId: 'bad', patch: {} }).success).toBe(false);
2133
+ expect(toolByName.delete_ai_cart_item.inputSchema.safeParse({ cartId: 'bad', itemId }).success).toBe(false);
2134
+ expect(toolByName.get_ai_cart_item_order_ready_payload.inputSchema.safeParse({ sessionId: 'bad', itemId }).success).toBe(false);
2135
+ expect(toolByName.set_ai_cart_item_order_ready_payload.inputSchema.safeParse({
2136
+ sessionId,
2137
+ itemId: 'bad',
2138
+ orderReadyStatus: 'BLOCKED',
2139
+ orderReadyMessages: [{ message: 'Invalid item id' }],
2140
+ }).success).toBe(false);
2141
+ expect(toolByName.clear_ai_cart_item_order_ready_payload.inputSchema.safeParse({ sessionId: 'bad', itemId }).success).toBe(false);
2142
+ });
2143
+
2144
+ it('AI canvas image delete schema accepts only supported canvas and image types', () => {
2145
+ expect(AiCanvasImageDeleteInputSchema.safeParse({
2146
+ sessionId,
2147
+ canvasType: 'south',
2148
+ imageType: 'background',
2149
+ }).success).toBe(true);
2150
+
2151
+ expect(AiCanvasImageDeleteInputSchema.safeParse({
2152
+ sessionId,
2153
+ canvasType: 'ceiling',
2154
+ imageType: 'background',
2155
+ }).success).toBe(false);
2156
+
2157
+ expect(AiCanvasImageDeleteInputSchema.safeParse({
2158
+ sessionId,
2159
+ canvasType: 'plan',
2160
+ imageType: 'thumbnail',
2161
+ }).success).toBe(false);
2162
+ });
2163
+
2164
+ it('AI canvas image upload schema accepts supported canvas and image inputs', () => {
2165
+ expect(AiCanvasImageUploadInputSchema.safeParse({
2166
+ sessionId,
2167
+ canvasType: 'east',
2168
+ imageType: 'background',
2169
+ imagePath: 'D:\\testing321\\unit_i_east_elevation_ai_overlay_preview.png',
2170
+ }).success).toBe(true);
2171
+
2172
+ expect(AiCanvasImageUploadInputSchema.safeParse({
2173
+ sessionId,
2174
+ canvasType: 'ceiling',
2175
+ imageType: 'background',
2176
+ imagePath: 'D:\\testing321\\unit_i_east_elevation_ai_overlay_preview.png',
2177
+ }).success).toBe(false);
2178
+ });
2179
+
2180
+ it('sync snapshot tool schema accepts quantity greater than one item payload', () => {
2181
+ const syncTool = aiDrawingTools.find((tool) => tool.name === 'sync_ai_cart_snapshot');
2182
+ const parsed = syncTool?.inputSchema.safeParse({
2183
+ cartId,
2184
+ items: [{ positionName: 'BASE_01', quantity: 4, itemPayload: { raw: true } }],
2185
+ orderConfigurationWorkflow: {
2186
+ mode: 'USER_CONFIRMED_ORDER_READY',
2187
+ evidence:
2188
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user selected saved setting Modern Euro.',
2189
+ checkedSerialNumbers: ['B30'],
2190
+ },
2191
+ });
2192
+
2193
+ expect(parsed?.success).toBe(true);
2194
+ expect(syncTool?.description).toContain('one deduplicated physical cabinet manifest');
2195
+ expect(syncTool?.description).toContain('plan is only a top-view placement');
2196
+ expect(syncTool?.description).toContain('not one manifest per canvas');
2197
+ expect(syncTool?.description).toContain('orderConfigurationWorkflow is optional audit metadata');
2198
+ expect(syncTool?.description).toContain('visual semantics are not certified by this schema');
2199
+ });
2200
+
2201
+ it('sync snapshot schema accepts persistence-only item manifests without evidence workflow', () => {
2202
+ const syncTool = aiDrawingTools.find((tool) => tool.name === 'sync_ai_cart_snapshot');
2203
+
2204
+ expect(syncTool?.inputSchema.safeParse({
2205
+ cartId,
2206
+ items: [{ positionName: 'BASE_01', quantity: 1 }],
2207
+ }).success).toBe(true);
2208
+
2209
+ expect(syncTool?.inputSchema.safeParse({
2210
+ cartId,
2211
+ items: [{ positionName: 'BASE_01', quantity: 1 }],
2212
+ orderConfigurationWorkflow: {
2213
+ mode: 'USER_CONFIRMED_ORDER_READY',
2214
+ evidence: 'Agent decided configuration can wait.',
2215
+ },
2216
+ }).success).toBe(true);
2217
+
2218
+ expect(syncTool?.inputSchema.safeParse({
2219
+ cartId,
2220
+ items: [{ positionName: 'BASE_01', quantity: 1 }],
2221
+ orderConfigurationWorkflow: {
2222
+ mode: 'USER_APPROVED_UNCONFIGURED_DRAFT',
2223
+ evidence:
2224
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user approved creating an unconfigured planner draft.',
2225
+ },
2226
+ }).success).toBe(false);
2227
+ });
2228
+
2229
+ it('AI cart creation and sync descriptions require order-ready follow-up for agent-created items', () => {
2230
+ const createTool = aiDrawingTools.find((tool) => tool.name === 'create_ai_cart');
2231
+ const syncTool = aiDrawingTools.find((tool) => tool.name === 'sync_ai_cart_snapshot');
2232
+ const patchTool = aiDrawingTools.find((tool) => tool.name === 'patch_ai_cart_item');
2233
+
2234
+ for (const tool of [createTool, syncTool]) {
2235
+ expect(tool?.description).toContain('When the agent creates or syncs AI cart items');
2236
+ expect(tool?.description).toContain('get_article_context');
2237
+ expect(tool?.description).toContain('get_article_options');
2238
+ expect(tool?.description).toContain('get_my_saved_settings');
2239
+ expect(tool?.description).toContain('get_saved_settings_presets');
2240
+ expect(tool?.description).toContain('ask the user for every missing order-critical choice');
2241
+ expect(tool?.description).toContain('set_ai_cart_item_order_ready_payload');
2242
+ expect(tool?.description).toContain('ArticleItemSchema-compatible payload');
2243
+ expect(tool?.description).toContain('Creating positionName, serialNumber, width, height, and depth is not enough');
2244
+ }
2245
+
2246
+ expect(patchTool?.description).toContain('refresh that item with set_ai_cart_item_order_ready_payload');
2247
+ expect(patchTool?.description).toContain('NEEDS_AGENT_CONFIGURATION/BLOCKED');
2248
+ });
2249
+
2250
+ it('AI cart tool descriptions avoid legacy endpoint and tool identifiers in source schema output', () => {
2251
+ const renderedToolMetadata = JSON.stringify(aiDrawingTools.map((tool) => ({
2252
+ name: tool.name,
2253
+ description: tool.description,
2254
+ inputSchema: zodToJsonSchema(tool.inputSchema),
2255
+ })));
2256
+ const forbiddenTerms = [
2257
+ '/api/mcp/v1/' + 'canvas',
2258
+ 'get' + '_drawing_tool_state',
2259
+ 'update' + '_cabinet_coordinates',
2260
+ 'patch' + '_cabinet_coordinates',
2261
+ 'upload' + '_canvas_background_image',
2262
+ '/topic/' + 'drawing-tool',
2263
+ 'Drawing' + 'ToolTest',
2264
+ 'saved' + '_carts',
2265
+ ];
2266
+
2267
+ for (const term of forbiddenTerms) {
2268
+ expect(renderedToolMetadata).not.toContain(term);
2269
+ }
2270
+ });
2271
+
2272
+ it('order-ready tool descriptions include no-guessing order guidance', () => {
2273
+ const toolNames = [
2274
+ 'get_ai_cart_item_order_ready_payload',
2275
+ 'set_ai_cart_item_order_ready_payload',
2276
+ 'clear_ai_cart_item_order_ready_payload',
2277
+ ];
2278
+
2279
+ for (const name of toolNames) {
2280
+ const tool = aiDrawingTools.find((candidate) => candidate.name === name);
2281
+ expect(tool?.description).toContain('serialNumber, positionName, width, height, and depth alone are not order-ready');
2282
+ expect(tool?.description).toContain('get_article_context');
2283
+ expect(tool?.description).toContain('get_article_options');
2284
+ expect(tool?.description).toContain('get_my_saved_settings');
2285
+ expect(tool?.description).toContain('get_saved_settings_presets');
2286
+ expect(tool?.description).toContain('saved setting tools');
2287
+ expect(tool?.description).toContain('ask the user for every required missing order-critical choice');
2288
+ expect(tool?.description).toContain('use exact option strings');
2289
+ expect(tool?.description).toContain('ArticleItemSchema/order-contract validation');
2290
+ expect(tool?.description).toContain('Drawing evidence JSON is optional trace metadata and is not visual certification');
2291
+ expect(tool?.description).toContain('automatically upsert into the browser Redux checkout cart');
2292
+ expect(tool?.description).toContain('Do not tell users to press Seed AI Cart or Apply to cart as the normal workflow');
2293
+ }
2294
+ });
2295
+
2296
+ it('Phase 14 import and order-ready descriptions explain automatic mirroring without normal seed/apply workflow', () => {
2297
+ const importTool = aiDrawingTools.find((candidate) => candidate.name === 'import_redux_cart_items_to_ai_cart');
2298
+ const setReadyTool = aiDrawingTools.find((candidate) => candidate.name === 'set_ai_cart_item_order_ready_payload');
2299
+
2300
+ expect(importTool?.description).toContain('Import rich browser Redux checkout cart items');
2301
+ expect(importTool?.description).toContain('full original Redux cart item payload');
2302
+ expect(importTool?.description).toContain('idempotency');
2303
+ expect(importTool?.description).toContain('positionName alone');
2304
+ expect(importTool?.description).toContain('only for real existing browser checkout cart items');
2305
+ expect(importTool?.description).toContain('do not use it to seed AI cart items from drawing takeoff');
2306
+ expect(importTool?.description).toContain('MCP imports must be READY and order-ready');
2307
+ expect(importTool?.description).toContain('automatically upsert into the browser Redux checkout cart');
2308
+ expect(importTool?.description).toContain('Do not tell users to press Seed AI Cart or Apply to cart as the normal workflow');
2309
+ expect(setReadyTool?.description).toContain('READY AI cart items');
2310
+ expect(setReadyTool?.description).toContain('Non-ready statuses');
2311
+ });
2312
+ });
2313
+
2314
+ describe('AI cart schemas', () => {
2315
+ const cartId = '11111111-1111-4111-8111-111111111111';
2316
+ const itemId = '22222222-2222-4222-8222-222222222222';
2317
+
2318
+ it('parses valid AI cart item snapshot input', () => {
2319
+ const result = AiCartItemInputSchema.parse({
2320
+ frontendItemId: 'frontend-1',
2321
+ serialNumber: 'B30',
2322
+ positionName: 'BASE_01',
2323
+ groupId: 'base-run',
2324
+ width: 30,
2325
+ height: 34.5,
2326
+ depth: 24,
2327
+ displayName: 'Base 30',
2328
+ itemPayload: { raw: true },
2329
+ });
2330
+
2331
+ expect(result.quantity).toBe(1);
2332
+ expect(result.positionName).toBe('BASE_01');
2333
+ expect(result.itemPayload).toEqual({ raw: true });
2334
+ });
2335
+
2336
+ it('accepts quantity greater than one for snapshot input before backend expansion', () => {
2337
+ const result = AiCartItemInputSchema.parse({
2338
+ positionName: 'BASE_01',
2339
+ quantity: 3,
2340
+ });
2341
+
2342
+ expect(result.quantity).toBe(3);
2343
+ });
2344
+
2345
+ it('rejects missing or empty positionName for item input', () => {
2346
+ expect(AiCartItemInputSchema.safeParse({ serialNumber: 'B30' }).success).toBe(false);
2347
+ expect(AiCartItemInputSchema.safeParse({ positionName: '' }).success).toBe(false);
2348
+ });
2349
+
2350
+ it('allows pantry classification evidence by default so visual review can catch mistakes', () => {
2351
+ expect(AiCartItemInputSchema.safeParse({
2352
+ positionName: 'PTRY',
2353
+ serialNumber: 'PC_1D_L_1293',
2354
+ displayName: 'Tall Pantry',
2355
+ drawingClassificationEvidence: {
2356
+ sourceCanvases: ['north'],
2357
+ primaryCanvasType: 'north',
2358
+ classificationBasis: 'same_canvas_visible_geometry',
2359
+ semanticTypeEvidence: 'North elevation same-canvas visible geometry shows REF refrigerator/freezer appliance handles, not a cabinet surround.',
2360
+ },
2361
+ }).success).toBe(true);
2362
+ });
2363
+
2364
+ it('allows sink-base classification evidence by default so visual review can catch mistakes', () => {
2365
+ expect(AiCartItemInputSchema.safeParse({
2366
+ positionName: 'SINK',
2367
+ serialNumber: 'SB24',
2368
+ displayName: 'Sink Base',
2369
+ drawingClassificationEvidence: {
2370
+ sourceCanvases: ['plan', 'north'],
2371
+ primaryCanvasType: 'north',
2372
+ classificationBasis: 'plan_elevation_association',
2373
+ elevationCanvasType: 'north',
2374
+ detailNumber: '7',
2375
+ planMarkerSide: 'right',
2376
+ selectedPlanObjectIds: ['P_BASE_RIGHT'],
2377
+ selectedPlanObjectBboxPx: { x: 720, y: 390, width: 100, height: 110 },
2378
+ sourcePlanObject: validReviewedPlanObject('P_BASE_RIGHT', 'base_cabinet'),
2379
+ sourceElevationObject: validReviewedElevationObject('N_BASE_RIGHT', 'base_cabinet', ['front_panel_boundaries']),
2380
+ semanticTypeEvidence: 'Plan has a sink symbol elsewhere in the drawing, but this north elevation item has no same-canvas sink basin/faucet evidence.',
2381
+ rejectedConflictingEvidence: ['fixture_symbol:SINK from plan detail marker side 6'],
2382
+ },
2383
+ }).success).toBe(true);
2384
+ });
2385
+
2386
+ it('accepts elevation-derived classification only with primary front-face evidence', () => {
2387
+ expect(AiCartItemInputSchema.safeParse({
2388
+ positionName: 'BASE_01',
2389
+ serialNumber: 'B30',
2390
+ displayName: '30 Base Cabinet',
2391
+ drawingClassificationEvidence: {
2392
+ sourceCanvases: ['north'],
2393
+ primaryCanvasType: 'north',
2394
+ classificationBasis: 'same_canvas_visible_geometry',
2395
+ primaryViewPlaneRole: 'primary_elevation_face',
2396
+ frontFaceEvidence: 'Agent visual review confirms front-facing base cabinet doors/drawers in the north elevation.',
2397
+ frontFaceFeatures: ['door_drawer_fronts', 'front_handles_or_pulls'],
2398
+ sourceElevationObject: validReviewedElevationObject('N_BASE_FRONT', 'base_cabinet', ['door_drawer_fronts', 'front_handles_or_pulls']),
2399
+ semanticTypeEvidence: 'Same-canvas north elevation visible geometry shows a front-facing base cabinet with doors/drawers.',
2400
+ },
2401
+ }).success).toBe(true);
2402
+ });
2403
+
2404
+ it('allows self-attested reviewed source objects without review tokens by default', () => {
2405
+ const sourceElevationObject = { ...validReviewedElevationObject('N_BASE_FRONT', 'base_cabinet', ['door_drawer_fronts']) };
2406
+ delete sourceElevationObject.reviewToken;
2407
+
2408
+ expect(AiCartItemInputSchema.safeParse({
2409
+ positionName: 'BASE_01',
2410
+ serialNumber: 'B30',
2411
+ displayName: '30 Base Cabinet',
2412
+ drawingClassificationEvidence: {
2413
+ sourceCanvases: ['north'],
2414
+ primaryCanvasType: 'north',
2415
+ classificationBasis: 'same_canvas_visible_geometry',
2416
+ primaryViewPlaneRole: 'primary_elevation_face',
2417
+ frontFaceEvidence: 'Agent visual review confirms front-facing base cabinet doors/drawers in the north elevation.',
2418
+ frontFaceFeatures: ['door_drawer_fronts'],
2419
+ sourceElevationObject,
2420
+ semanticTypeEvidence: 'Same-canvas north elevation visible geometry shows a front-facing base cabinet with doors.',
2421
+ },
2422
+ }).success).toBe(true);
2423
+ });
2424
+
2425
+ it('allows elevation-derived return side evidence by default so visual review can catch mistakes', () => {
2426
+ expect(AiCartItemInputSchema.safeParse({
2427
+ positionName: 'UPPER_SIDE',
2428
+ serialNumber: 'U12',
2429
+ displayName: 'Upper side view',
2430
+ drawingClassificationEvidence: {
2431
+ sourceCanvases: ['north'],
2432
+ primaryCanvasType: 'north',
2433
+ classificationBasis: 'same_canvas_visible_geometry',
2434
+ primaryViewPlaneRole: 'return_side_face',
2435
+ frontFaceEvidence: 'Agent visual review says this is a perpendicular side view / return side face at the edge of the north elevation.',
2436
+ frontFaceFeatures: ['side_panel_or_return_visible'],
2437
+ sourceElevationObject: {
2438
+ ...validReviewedElevationObject('N_UPPER_SIDE', 'wall_cabinet', ['side_panel_or_return_visible']),
2439
+ viewPlaneRole: 'return_side_face',
2440
+ evidence: 'Reviewed object is a return side face at the edge of the elevation.',
2441
+ },
2442
+ semanticTypeEvidence: 'Visible side-view geometry only; this does not show the primary front face in the north elevation.',
2443
+ },
2444
+ }).success).toBe(true);
2445
+ });
2446
+
2447
+ it('allows elevation-derived classification without a reviewed source object by default', () => {
2448
+ expect(AiCartItemInputSchema.safeParse({
2449
+ positionName: 'N_SNK',
2450
+ serialNumber: 'BC_S_1D_R_1770',
2451
+ displayName: 'Sink base right',
2452
+ drawingClassificationEvidence: {
2453
+ sourceCanvases: ['north'],
2454
+ primaryCanvasType: 'north',
2455
+ classificationBasis: 'same_canvas_visible_geometry',
2456
+ primaryViewPlaneRole: 'primary_elevation_face',
2457
+ frontFaceEvidence: 'Agent claims this is a sink base with a sink basin on the primary elevation face.',
2458
+ frontFaceFeatures: ['sink_basin_or_faucet_on_primary_face', 'front_panel_boundaries'],
2459
+ semanticTypeEvidence: 'Sink base cabinet; sink basin is allegedly visible on this elevation.',
2460
+ },
2461
+ }).success).toBe(true);
2462
+ });
2463
+
2464
+ it('allows invented sink feature evidence by default so visual review can catch mistakes', () => {
2465
+ expect(AiCartItemInputSchema.safeParse({
2466
+ positionName: 'N_SNK',
2467
+ serialNumber: 'BC_S_1D_R_1770',
2468
+ displayName: 'Sink base right',
2469
+ drawingClassificationEvidence: {
2470
+ sourceCanvases: ['north'],
2471
+ primaryCanvasType: 'north',
2472
+ classificationBasis: 'same_canvas_visible_geometry',
2473
+ primaryViewPlaneRole: 'primary_elevation_face',
2474
+ frontFaceEvidence: 'Agent claims this is a sink base with a sink basin on the primary elevation face.',
2475
+ frontFaceFeatures: ['sink_basin_or_faucet_on_primary_face', 'front_panel_boundaries'],
2476
+ sourceElevationObject: validReviewedElevationObject('N_RIGHT_BASE', 'base_cabinet', ['front_panel_boundaries']),
2477
+ semanticTypeEvidence: 'Sink base cabinet; sink basin is allegedly visible on this elevation.',
2478
+ },
2479
+ }).success).toBe(true);
2480
+ });
2481
+
2482
+ it('accepts sink classification only when the reviewed source object is a sink base with sink evidence', () => {
2483
+ expect(AiCartItemInputSchema.safeParse({
2484
+ positionName: 'N_SNK',
2485
+ serialNumber: 'BC_S_1D_R_1770',
2486
+ displayName: 'Sink base right',
2487
+ drawingClassificationEvidence: {
2488
+ sourceCanvases: ['north'],
2489
+ primaryCanvasType: 'north',
2490
+ classificationBasis: 'same_canvas_visible_geometry',
2491
+ primaryViewPlaneRole: 'primary_elevation_face',
2492
+ frontFaceEvidence: 'Reviewed north elevation object shows a sink basin or faucet on the same primary front face.',
2493
+ frontFaceFeatures: ['sink_basin_or_faucet_on_primary_face', 'front_panel_boundaries'],
2494
+ sourceElevationObject: validReviewedElevationObject('N_SINK_FRONT', 'sink_base', ['sink_basin_or_faucet_on_primary_face', 'front_panel_boundaries']),
2495
+ semanticTypeEvidence: 'Sink base cabinet; same reviewed source object carries sink basin/faucet evidence.',
2496
+ },
2497
+ }).success).toBe(true);
2498
+ });
2499
+
2500
+ it('allows reviewed non-sink source objects with sink feature evidence by default', () => {
2501
+ expect(AiCartItemInputSchema.safeParse({
2502
+ positionName: 'RB30',
2503
+ serialNumber: 'BC_1D_L_1002',
2504
+ displayName: 'Right Base',
2505
+ drawingClassificationEvidence: {
2506
+ sourceCanvases: ['north'],
2507
+ primaryCanvasType: 'north',
2508
+ classificationBasis: 'same_canvas_visible_geometry',
2509
+ primaryViewPlaneRole: 'primary_elevation_face',
2510
+ frontFaceEvidence: 'Reviewed north elevation object is a right base panel below the counter line.',
2511
+ frontFaceFeatures: ['sink_basin_or_faucet_on_primary_face', 'front_panel_boundaries'],
2512
+ sourceElevationObject: validReviewedElevationObject('N_RB30', 'base_cabinet', ['sink_basin_or_faucet_on_primary_face', 'front_panel_boundaries']),
2513
+ semanticTypeEvidence: 'Base cabinet with claimed sink feature on a non-sink reviewed object.',
2514
+ },
2515
+ }).success).toBe(true);
2516
+ });
2517
+
2518
+ it('allows elevation sink classification without same-canvas primary-face sink evidence by default', () => {
2519
+ expect(AiCartItemInputSchema.safeParse({
2520
+ positionName: 'I_S30',
2521
+ serialNumber: 'BC_S_2D_1761',
2522
+ displayName: 'Sink Base',
2523
+ drawingClassificationEvidence: {
2524
+ sourceCanvases: ['north'],
2525
+ primaryCanvasType: 'north',
2526
+ classificationBasis: 'same_canvas_visible_geometry',
2527
+ primaryViewPlaneRole: 'primary_elevation_face',
2528
+ frontFaceEvidence: 'Agent visual review shows a base-sized rectangle at the right edge but no sink basin or faucet on the primary face.',
2529
+ frontFaceFeatures: ['front_panel_boundaries'],
2530
+ sourceElevationObject: validReviewedElevationObject('N_RIGHT_BASE', 'base_cabinet', ['front_panel_boundaries']),
2531
+ semanticTypeEvidence: 'North elevation context has no same-canvas sink basin/faucet on the primary front face.',
2532
+ },
2533
+ }).success).toBe(true);
2534
+ });
2535
+
2536
+ it('allows elevation upper classification from side/end-panel features by default', () => {
2537
+ expect(AiCartItemInputSchema.safeParse({
2538
+ positionName: 'I_U20',
2539
+ serialNumber: 'UC_1D_R_1228',
2540
+ displayName: 'Upper side panel',
2541
+ drawingClassificationEvidence: {
2542
+ sourceCanvases: ['north'],
2543
+ primaryCanvasType: 'north',
2544
+ classificationBasis: 'same_canvas_visible_geometry',
2545
+ primaryViewPlaneRole: 'primary_elevation_face',
2546
+ frontFaceEvidence: 'Agent visual review shows a narrow side/end panel at the right edge of the elevation.',
2547
+ frontFaceFeatures: ['side_panel_or_return_visible'],
2548
+ sourceElevationObject: {
2549
+ ...validReviewedElevationObject('N_UPPER_SIDE', 'wall_cabinet', ['side_panel_or_return_visible']),
2550
+ evidence: 'Reviewed object is a side/end panel at the right edge of the elevation.',
2551
+ },
2552
+ semanticTypeEvidence: 'Visible side/end panel context only, not an upper cabinet primary face.',
2553
+ },
2554
+ }).success).toBe(true);
2555
+ });
2556
+
2557
+ it('allows drawing-derived item snapshots without drawing classification evidence by default', () => {
2558
+ expect(AiCartSnapshotInputSchema.safeParse({
2559
+ cartId,
2560
+ sourceSnapshotPayload: '{"source":"drawing takeoff","drawings":["unit_i_plan.png","unit_i_north_elevation.png"]}',
2561
+ items: [{ positionName: 'BASE_01', serialNumber: 'B30' }],
2562
+ orderConfigurationWorkflow: {
2563
+ mode: 'USER_CONFIRMED_ORDER_READY',
2564
+ evidence:
2565
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user confirmed exact order options.',
2566
+ checkedSerialNumbers: ['B30'],
2567
+ },
2568
+ }).success).toBe(true);
2569
+ });
2570
+
2571
+ it('rejects invalid cart and item UUID params', () => {
2572
+ expect(AiCartIdParamSchema.safeParse({ cartId: 'not-a-uuid' }).success).toBe(false);
2573
+ expect(AiCartItemIdParamSchema.safeParse({ cartId, itemId: 'not-a-uuid' }).success).toBe(false);
2574
+ });
2575
+
2576
+ it('accepts supported patch fields and rejects unknown fields', () => {
2577
+ const validPatch = AiCartItemPatchSchema.safeParse({
2578
+ positionName: 'BASE_02',
2579
+ width: 33,
2580
+ displayName: 'Updated Base',
2581
+ itemPayload: { updated: true },
2582
+ });
2583
+ const invalidPatch = AiCartItemPatchSchema.safeParse({
2584
+ positionName: 'BASE_02',
2585
+ unsupportedField: true,
2586
+ });
2587
+
2588
+ expect(validPatch.success).toBe(true);
2589
+ expect(invalidPatch.success).toBe(false);
2590
+ });
2591
+
2592
+ it('parses snapshot schema with UUID cart id and item array', () => {
2593
+ const result = AiCartSnapshotInputSchema.parse({
2594
+ cartId,
2595
+ sourceSnapshotPayload: '{"source":"mcp"}',
2596
+ items: [
2597
+ {
2598
+ positionName: 'BASE_01',
2599
+ quantity: 2,
2600
+ itemPayload: { source: 'cart' },
2601
+ },
2602
+ ],
2603
+ orderConfigurationWorkflow: {
2604
+ mode: 'USER_CONFIRMED_ORDER_READY',
2605
+ evidence:
2606
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; user confirmed exact order options.',
2607
+ checkedSerialNumbers: ['B30'],
2608
+ },
2609
+ });
2610
+
2611
+ expect(result.cartId).toBe(cartId);
2612
+ expect(result.items).toHaveLength(1);
2613
+ expect(result.items[0].quantity).toBe(2);
2614
+ });
2615
+
2616
+ it('schema descriptions avoid legacy MCP canvas and frontend drawing tool identifiers', () => {
2617
+ const renderedSchemas = JSON.stringify([
2618
+ zodToJsonSchema(AiCartIdParamSchema),
2619
+ zodToJsonSchema(AiCartItemIdParamSchema),
2620
+ zodToJsonSchema(AiCartItemInputSchema),
2621
+ zodToJsonSchema(AiCartItemPatchSchema),
2622
+ zodToJsonSchema(AiCartSnapshotInputSchema),
2623
+ zodToJsonSchema(DuplicateAiCabinetInputSchema),
2624
+ zodToJsonSchema(ConfigureAiCabinetInputSchema),
2625
+ zodToJsonSchema(DeleteAiCabinetInputSchema),
2626
+ zodToJsonSchema(AiOrderReadyPayloadReadInputSchema),
2627
+ zodToJsonSchema(AiOrderReadyPayloadSetInputSchema),
2628
+ zodToJsonSchema(AiOrderReadyPayloadClearInputSchema),
2629
+ zodToJsonSchema(AiReduxCartImportInputSchema),
2630
+ ]);
2631
+
2632
+ const forbiddenTerms = [
2633
+ '/api/mcp/v1/' + 'canvas',
2634
+ 'get' + '_drawing_tool_state',
2635
+ 'update' + '_cabinet_coordinates',
2636
+ 'patch' + '_cabinet_coordinates',
2637
+ 'upload' + '_canvas_background_image',
2638
+ '/topic/' + 'drawing-tool',
2639
+ 'Drawing' + 'ToolTest',
2640
+ 'saved' + '_carts',
2641
+ ];
2642
+
2643
+ for (const term of forbiddenTerms) {
2644
+ expect(renderedSchemas).not.toContain(term);
2645
+ }
2646
+ });
2647
+
2648
+ it('order-ready schema rejects thin READY payloads and requires base body-height evidence for base READY payloads', () => {
2649
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2650
+ sessionId: '33333333-3333-4333-8333-333333333333',
2651
+ itemId,
2652
+ orderReadyStatus: 'READY',
2653
+ orderReadyPayload: { arbitrary: true },
2654
+ }).success).toBe(false);
2655
+
2656
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2657
+ sessionId: '33333333-3333-4333-8333-333333333333',
2658
+ itemId,
2659
+ orderReadyStatus: 'READY',
2660
+ orderReadyPayload: thinArticleItemPayload(),
2661
+ }).success).toBe(false);
2662
+
2663
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2664
+ sessionId: '33333333-3333-4333-8333-333333333333',
2665
+ itemId,
2666
+ orderReadyStatus: 'READY',
2667
+ orderReadyPayload: validArticleItemPayload(),
2668
+ }).success).toBe(false);
2669
+
2670
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2671
+ sessionId: '33333333-3333-4333-8333-333333333333',
2672
+ itemId,
2673
+ orderReadyStatus: 'READY',
2674
+ orderReadyPayload: validUpperArticleItemPayload(),
2675
+ }).success).toBe(true);
2676
+
2677
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2678
+ sessionId: '33333333-3333-4333-8333-333333333333',
2679
+ itemId,
2680
+ orderReadyStatus: 'READY',
2681
+ orderReadyPayload: validArticleItemPayload(),
2682
+ drawingClassificationEvidence: validDrawingClassificationEvidence(),
2683
+ }).success).toBe(true);
2684
+ });
2685
+
2686
+ it('order-ready schema allows refrigerator/pantry evidence conflicts by default for visual review', () => {
2687
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2688
+ sessionId: '33333333-3333-4333-8333-333333333333',
2689
+ itemId,
2690
+ orderReadyStatus: 'READY',
2691
+ orderReadyPayload: {
2692
+ ...validArticleItemPayload(),
2693
+ serialNumber: 'PC_1D_L_1293',
2694
+ positionName: 'N_PTRY',
2695
+ },
2696
+ drawingClassificationEvidence: {
2697
+ ...validUpperDrawingClassificationEvidence(),
2698
+ semanticTypeEvidence: 'Agent vision review shows REF refrigerator/freezer handles and appliance footprint.',
2699
+ sourceElevationObject: validReviewedElevationObject('N_REF_FRONT', 'appliance_opening', ['appliance_front_or_handles']),
2700
+ },
2701
+ }).success).toBe(true);
2702
+ });
2703
+
2704
+ it('order-ready schema allows elevation return-side evidence by default for visual review', () => {
2705
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2706
+ sessionId: '33333333-3333-4333-8333-333333333333',
2707
+ itemId,
2708
+ orderReadyStatus: 'READY',
2709
+ orderReadyPayload: validUpperArticleItemPayload(),
2710
+ drawingClassificationEvidence: {
2711
+ ...validUpperDrawingClassificationEvidence(),
2712
+ primaryViewPlaneRole: 'return_side_face',
2713
+ frontFaceEvidence: 'Agent visual review says this is a side view / return side face.',
2714
+ frontFaceFeatures: ['side_panel_or_return_visible'],
2715
+ semanticTypeEvidence: 'Perpendicular return-side geometry visible at the edge of the north elevation.',
2716
+ },
2717
+ }).success).toBe(true);
2718
+ });
2719
+
2720
+ it('order-ready schema accepts non-ready validation messages without prose evidence gates', () => {
2721
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2722
+ sessionId: '33333333-3333-4333-8333-333333333333',
2723
+ itemId,
2724
+ orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION',
2725
+ orderReadyMessages: [
2726
+ {
2727
+ severity: 'error',
2728
+ field: 'caseMaterial',
2729
+ message:
2730
+ 'Called get_article_context and get_article_options for B30; checked get_my_saved_settings and get_saved_settings_presets; ask the user for caseMaterial.',
2731
+ },
2732
+ ],
2733
+ }).success).toBe(true);
2734
+
2735
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2736
+ sessionId: '33333333-3333-4333-8333-333333333333',
2737
+ itemId,
2738
+ orderReadyStatus: 'BLOCKED',
2739
+ orderReadyMessages: [
2740
+ {
2741
+ severity: 'blocker',
2742
+ field: 'serialNumber',
2743
+ message: 'No reliable catalog match was available; ask the user to choose a valid serialNumber.',
2744
+ },
2745
+ ],
2746
+ }).success).toBe(true);
2747
+ });
2748
+
2749
+ it('order-ready schema accepts simple needs-configuration statuses as persistence state', () => {
2750
+ expect(AiOrderReadyPayloadSetInputSchema.safeParse({
2751
+ sessionId: '33333333-3333-4333-8333-333333333333',
2752
+ itemId,
2753
+ orderReadyStatus: 'NEEDS_AGENT_CONFIGURATION',
2754
+ orderReadyMessages: [
2755
+ { severity: 'error', field: 'caseMaterial', message: 'Ask the user for case material.' },
2756
+ ],
2757
+ }).success).toBe(true);
2758
+ });
2759
+
2760
+ it('Redux import schema validates session UUID and rejects empty import arrays', () => {
2761
+ expect(AiReduxCartImportInputSchema.safeParse({
2762
+ sessionId: 'not-a-uuid',
2763
+ items: [validReduxCartImportInput('33333333-3333-4333-8333-333333333333').items[0]],
2764
+ }).success).toBe(false);
2765
+
2766
+ expect(AiReduxCartImportInputSchema.safeParse({
2767
+ sessionId: '33333333-3333-4333-8333-333333333333',
2768
+ items: [],
2769
+ }).success).toBe(false);
2770
+ });
2771
+
2772
+ it('Redux import schema rejects items without full original payload data', () => {
2773
+ const input = validReduxCartImportInput('33333333-3333-4333-8333-333333333333');
2774
+ input.items[0].originalItemPayload = { arbitrary: true };
2775
+
2776
+ expect(AiReduxCartImportInputSchema.safeParse(input).success).toBe(false);
2777
+ });
2778
+
2779
+ it('Redux import schema rejects thin and non-ready MCP imports', () => {
2780
+ const input = validReduxCartImportInput('33333333-3333-4333-8333-333333333333');
2781
+ input.items[0].originalItemPayload = thinArticleItemPayload();
2782
+ delete input.items[0].orderReadyPayload;
2783
+ delete input.items[0].orderReadyStatus;
2784
+
2785
+ expect(AiReduxCartImportInputSchema.safeParse(input).success).toBe(false);
2786
+
2787
+ input.items[0].orderReadyStatus = 'READY';
2788
+ expect(AiReduxCartImportInputSchema.safeParse(input).success).toBe(false);
2789
+
2790
+ input.items[0].orderReadyStatus = 'NOT_PREPARED';
2791
+ input.items[0].orderReadyMessages = [
2792
+ { severity: 'error', message: 'Ask the user for case material before mirroring.' },
2793
+ ];
2794
+ expect(AiReduxCartImportInputSchema.safeParse(input).success).toBe(false);
2795
+ });
2796
+ });
2797
+
2798
+ function validReduxCartImportInput(sessionId: string): any {
2799
+ const payload = validArticleItemPayload();
2800
+ return {
2801
+ sessionId,
2802
+ items: [
2803
+ {
2804
+ frontendItemId: 'frontend-1',
2805
+ originalFrontendItemId: 'frontend-1',
2806
+ positionName: payload.positionName,
2807
+ originalPositionName: payload.positionName,
2808
+ serialNumber: payload.serialNumber,
2809
+ displayName: 'Base 30',
2810
+ quantity: 1,
2811
+ originalQuantity: 1,
2812
+ width: payload.width,
2813
+ height: payload.height,
2814
+ depth: payload.depth,
2815
+ originalItemPayload: payload,
2816
+ orderReadyStatus: 'READY',
2817
+ orderReadyPayload: payload,
2818
+ orderReadySource: 'MCP',
2819
+ },
2820
+ ],
2821
+ };
2822
+ }
2823
+
2824
+ function thinArticleItemPayload(): Record<string, unknown> {
2825
+ return {
2826
+ serialNumber: 'B30',
2827
+ positionName: 'BASE_01',
2828
+ quantity: 1,
2829
+ width: 30,
2830
+ height: 34.5,
2831
+ depth: 24,
2832
+ };
2833
+ }
2834
+
2835
+ function validArticleItemPayload(): Record<string, unknown> {
2836
+ return {
2837
+ serialNumber: 'B30',
2838
+ positionName: 'BASE_01',
2839
+ quantity: 1,
2840
+ width: 30,
2841
+ height: 34.5,
2842
+ depth: 24,
2843
+ caseMaterial: 'White Melamine',
2844
+ frontMaterial: 'White Oak',
2845
+ caseEdge: 'White 1mm',
2846
+ frontEdge: 'White Oak 1mm',
2847
+ backPanelMaterial: 'White Melamine',
2848
+ backPanel: 'Inset',
2849
+ jointMethod: 'Biscuit',
2850
+ };
2851
+ }
2852
+
2853
+ function validUpperArticleItemPayload(): Record<string, unknown> {
2854
+ return {
2855
+ serialNumber: 'UC_1D_R_1228',
2856
+ positionName: 'UPPER_01',
2857
+ quantity: 1,
2858
+ width: 30,
2859
+ height: 42,
2860
+ depth: 12,
2861
+ caseMaterial: 'White Melamine',
2862
+ frontMaterial: 'White Oak',
2863
+ caseEdge: 'White 1mm',
2864
+ frontEdge: 'White Oak 1mm',
2865
+ backPanelMaterial: 'White Melamine',
2866
+ backPanel: 'Inset',
2867
+ jointMethod: 'Biscuit',
2868
+ };
2869
+ }
2870
+
2871
+ function validDrawingClassificationEvidence(): Record<string, unknown> {
2872
+ return {
2873
+ sourceCanvases: ['north'],
2874
+ primaryCanvasType: 'north',
2875
+ classificationBasis: 'same_canvas_visible_geometry',
2876
+ primaryViewPlaneRole: 'primary_elevation_face',
2877
+ frontFaceEvidence: 'Agent vision review confirms this is a primary front-facing base cabinet in the north elevation.',
2878
+ frontFaceFeatures: ['door_drawer_fronts', 'front_handles_or_pulls'],
2879
+ sourceElevationObject: validReviewedElevationObject('N_BASE_FRONT', 'base_cabinet', ['door_drawer_fronts', 'front_handles_or_pulls']),
2880
+ semanticTypeEvidence: 'Same-canvas north elevation visible geometry shows front-facing doors and drawers for this cabinet.',
2881
+ baseCabinetBodyHeightEvidence: validBaseCabinetBodyHeightEvidence(),
2882
+ rejectedConflictingEvidence: [],
2883
+ };
2884
+ }
2885
+
2886
+ function validUpperDrawingClassificationEvidence(): Record<string, unknown> {
2887
+ return {
2888
+ sourceCanvases: ['north'],
2889
+ primaryCanvasType: 'north',
2890
+ classificationBasis: 'same_canvas_visible_geometry',
2891
+ primaryViewPlaneRole: 'primary_elevation_face',
2892
+ frontFaceEvidence: 'Agent vision review confirms this is a primary front-facing upper cabinet in the north elevation.',
2893
+ frontFaceFeatures: ['single_door_front', 'front_handles_or_pulls'],
2894
+ sourceElevationObject: validReviewedElevationObject('N_UPPER_FRONT', 'wall_cabinet', ['single_door_front', 'front_handles_or_pulls']),
2895
+ semanticTypeEvidence: 'Same-canvas north elevation visible geometry shows a front-facing upper wall cabinet.',
2896
+ rejectedConflictingEvidence: [],
2897
+ };
2898
+ }
2899
+
2900
+ function validBaseCabinetBodyHeightEvidence(): Record<string, unknown> {
2901
+ return {
2902
+ cabinetBodyHeightInches: 34.5,
2903
+ measurementBasis: 'called_out_scale_and_known_dimension',
2904
+ scaleEvidence: 'Agent used the called-out elevation scale and known 30 inch cabinet width to measure the cabinet body height.',
2905
+ topBoundaryEvidence: 'Top boundary is the base cabinet case/front below the countertop line; countertop thickness is excluded.',
2906
+ bottomBoundaryEvidence: 'Bottom boundary is the cabinet body above the separate toekick/plinth band and at or above Z0.',
2907
+ excludedElements: ['toekick_plinth', 'countertop', 'floor'],
2908
+ plinthDecision: 'separate_plinth_item_created',
2909
+ countertopDecision: 'excluded_visible_countertop',
2910
+ z0Evidence: 'The measured cabinet body stops above the finished-floor Z0 line and does not include floor context.',
2911
+ cabinetBodyBboxPx: { x: 100, y: 120, width: 200, height: 270 },
2912
+ plinthBboxPx: { x: 100, y: 390, width: 200, height: 35 },
2913
+ countertopBboxPx: { x: 100, y: 90, width: 200, height: 30 },
2914
+ evidence: 'Base cabinet READY height is the body height only and excludes the visible countertop and separated plinth.',
2915
+ };
2916
+ }
2917
+
2918
+ function validReviewedElevationObject(
2919
+ objectId: string,
2920
+ objectType: string,
2921
+ frontFaceFeatures: string[]
2922
+ ): Record<string, unknown> {
2923
+ return reviewedObjectWithToken({
2924
+ objectId,
2925
+ canvasType: 'north',
2926
+ objectType,
2927
+ reviewStatus: 'reviewed_cabinet',
2928
+ viewPlaneRole: 'primary_elevation_face',
2929
+ frontFaceFeatures,
2930
+ bbox: { x: 100, y: 100, width: 50, height: 60 },
2931
+ evidence: 'Reviewed north elevation object from the drawing-analysis artifact with primary face evidence.',
2932
+ });
2933
+ }
2934
+
2935
+ function validReviewedPlanObject(
2936
+ objectId: string,
2937
+ objectType: string
2938
+ ): Record<string, unknown> {
2939
+ return reviewedObjectWithToken({
2940
+ objectId,
2941
+ canvasType: 'plan',
2942
+ objectType,
2943
+ reviewStatus: 'reviewed_cabinet',
2944
+ viewPlaneRole: 'plan_footprint',
2945
+ frontFaceFeatures: [],
2946
+ bbox: { x: 720, y: 390, width: 100, height: 110 },
2947
+ evidence: 'Reviewed plan object from association.selectedPlanObjects and visual plan analysis.',
2948
+ });
2949
+ }
2950
+
2951
+ function reviewedObjectWithToken(subject: Record<string, unknown>): Record<string, unknown> {
2952
+ return {
2953
+ ...subject,
2954
+ reviewToken: `test-review-token-${String(subject.objectId ?? 'object')}`,
2955
+ };
2956
+ }
2957
+
2958
+ function validCoordinateEvidence(): Record<string, unknown> {
2959
+ return {
2960
+ basis: 'validated_plan_placement',
2961
+ sourceCanvasType: 'plan',
2962
+ planPlacementPositionName: 'BASE_01',
2963
+ planPlacementEvidence: {
2964
+ basis: 'plan_elevation_marker_review',
2965
+ elevationCanvasType: 'north',
2966
+ detailNumber: '7',
2967
+ planMarkerSide: 'right',
2968
+ selectedPlanObjectIds: ['P_REF'],
2969
+ selectedPlanObjectBboxPx: { x: -0.5, y: 0, width: 3, height: 4 },
2970
+ selectedPlanObjectRole: 'appliance_footprint',
2971
+ sourcePlanObject: validReviewedPlanObject('P_REF', 'appliance_opening'),
2972
+ evidence: 'association.selectedPlanObjects contains P_REF. Agent visual image analysis confirms this exact selected plan object was used to derive canonical x/y.',
2973
+ },
2974
+ evidence: 'Canonical coordinate derived from the validated plan placement, not from an elevation-local rectangle.',
2975
+ };
2976
+ }
2977
+
2978
+ function validPlanProjectionCalibration(): any {
2979
+ return {
2980
+ schemaVersion: 'ai-canvas-calibration/v1',
2981
+ canvasType: 'plan',
2982
+ imageSizePx: { width: 1000, height: 800 },
2983
+ unit: 'inches',
2984
+ pixelsPerInch: 4,
2985
+ roomOriginPx: { x: 100, y: 700 },
2986
+ roomAxis: {
2987
+ xPositiveDirection: 'right',
2988
+ yPositiveDirection: 'up',
2989
+ },
2990
+ roomSizeInches: { width: 180, depth: 120 },
2991
+ };
2992
+ }
2993
+
2994
+ function validElevationProjectionCalibration(canvasType: 'north' | 'south' | 'east' | 'west', rotation: 0 | 90 | 180 | 270): any {
2995
+ return {
2996
+ schemaVersion: 'ai-canvas-calibration/v1',
2997
+ canvasType,
2998
+ imageSizePx: { width: 675, height: 706 },
2999
+ unit: 'inches',
3000
+ horizontalPixelsPerInch: 4,
3001
+ verticalPixelsPerInch: 4,
3002
+ floorZ0Px: 554,
3003
+ wallType: canvasType,
3004
+ wallPlaneCoordinateInches: 180,
3005
+ horizontalOriginPx: { x: 60, y: 554 },
3006
+ horizontalOriginCoordinateInches: 0,
3007
+ horizontalAxisPositiveDirection: 'right',
3008
+ verticalAxisPositiveDirection: 'up',
3009
+ visibilityRule: {
3010
+ mode: 'rotation',
3011
+ rotations: [rotation],
3012
+ includePositionNames: [],
3013
+ excludePositionNames: [],
3014
+ },
3015
+ };
3016
+ }
3017
+
3018
+ function expectDescriptionRequiresVisualReview(toolName: string): void {
3019
+ const tool = aiDrawingTools.find((candidate) => candidate.name === toolName);
3020
+
3021
+ expect(tool?.description).toContain('render_ai_drawing_session_review');
3022
+ expect(tool?.description).toContain('inspect every returned full-canvas PNG with native vision');
3023
+ expect(tool?.description).toContain('correct wrong placements with existing AI-only placement tools');
3024
+ expect(tool?.description).toContain('rerender');
3025
+ expect(tool?.description).toContain('PASS/FAIL/UNCERTAIN');
3026
+ expect(tool?.description).toContain('JSON validity is not visual correctness');
3027
+ }
3028
+
3029
+ function findArrayTupleItems(value: unknown, path = '$'): string[] {
3030
+ if (!value || typeof value !== 'object') {
3031
+ return [];
3032
+ }
3033
+ if (Array.isArray(value)) {
3034
+ return value.flatMap((item, index) => findArrayTupleItems(item, `${path}[${index}]`));
3035
+ }
3036
+
3037
+ const record = value as Record<string, unknown>;
3038
+ const hits = Array.isArray(record.items) ? [`${path}.items`] : [];
3039
+ return [
3040
+ ...hits,
3041
+ ...Object.entries(record).flatMap(([key, nestedValue]) => findArrayTupleItems(nestedValue, `${path}.${key}`)),
3042
+ ];
3043
+ }
3044
+
3045
+ function findBooleanExclusiveBounds(value: unknown, path = '$'): string[] {
3046
+ if (!value || typeof value !== 'object') {
3047
+ return [];
3048
+ }
3049
+ if (Array.isArray(value)) {
3050
+ return value.flatMap((item, index) => findBooleanExclusiveBounds(item, `${path}[${index}]`));
3051
+ }
3052
+
3053
+ const record = value as Record<string, unknown>;
3054
+ const hits = ['exclusiveMinimum', 'exclusiveMaximum']
3055
+ .filter((key) => typeof record[key] === 'boolean')
3056
+ .map((key) => `${path}.${key}`);
3057
+ return [
3058
+ ...hits,
3059
+ ...Object.entries(record).flatMap(([key, nestedValue]) => findBooleanExclusiveBounds(nestedValue, `${path}.${key}`)),
3060
+ ];
3061
+ }