test: add phase security/smoke/e2e coverage and tighten MCP contract handling
This commit is contained in:
@@ -33,7 +33,8 @@ describe('cleanElementForExcalidraw', () => {
|
||||
|
||||
expect(cleaned).not.toHaveProperty('createdAt');
|
||||
expect(cleaned).not.toHaveProperty('updatedAt');
|
||||
expect(cleaned).not.toHaveProperty('version');
|
||||
// version is kept — it is the Excalidraw element version, not a DB field
|
||||
expect(cleaned).toHaveProperty('version', 3);
|
||||
expect(cleaned).not.toHaveProperty('syncedAt');
|
||||
expect(cleaned).not.toHaveProperty('source');
|
||||
expect(cleaned).not.toHaveProperty('syncTimestamp');
|
||||
@@ -219,6 +220,21 @@ describe('computeElementHash', () => {
|
||||
const hash = computeElementHash([{ id: 'x', version: 1 }]);
|
||||
expect(hash.startsWith('1')).toBe(true);
|
||||
});
|
||||
|
||||
it('is order-stable for same id/version set', () => {
|
||||
const a = [
|
||||
{ id: 'a', version: 1 },
|
||||
{ id: 'b', version: 3 },
|
||||
{ id: 'c', version: 2 },
|
||||
];
|
||||
const b = [
|
||||
{ id: 'c', version: 2 },
|
||||
{ id: 'a', version: 1 },
|
||||
{ id: 'b', version: 3 },
|
||||
];
|
||||
|
||||
expect(computeElementHash(a)).toBe(computeElementHash(b));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── isImageElement ─────────────────────────────────────────
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import {
|
||||
expandLabelsToNative,
|
||||
prepareElementsForScene,
|
||||
} from '../../frontend/src/utils/scenePreparation.js';
|
||||
import type { ServerElement } from '../../frontend/src/utils/elementHelpers.js';
|
||||
|
||||
describe('expandLabelsToNative', () => {
|
||||
it('creates a native bound text element at container center', () => {
|
||||
const input = [{
|
||||
id: 'box-1',
|
||||
type: 'rectangle',
|
||||
x: 100,
|
||||
y: 200,
|
||||
width: 300,
|
||||
height: 120,
|
||||
label: { text: 'Title' },
|
||||
boundElements: [{ id: 'arrow-1', type: 'arrow' }],
|
||||
}];
|
||||
|
||||
const out = expandLabelsToNative(input as any[]);
|
||||
expect(out).toHaveLength(2);
|
||||
|
||||
const container = out.find((el) => el.id === 'box-1') as any;
|
||||
const text = out.find((el) => el.id === 'box-1_label') as any;
|
||||
|
||||
expect(container.boundElements).toEqual([
|
||||
{ id: 'arrow-1', type: 'arrow' },
|
||||
{ id: 'box-1_label', type: 'text' },
|
||||
]);
|
||||
expect(text.containerId).toBe('box-1');
|
||||
expect(text.text).toBe('Title');
|
||||
expect(text.x).toBe(230);
|
||||
expect(text.y).toBe(250);
|
||||
});
|
||||
|
||||
it('passes through elements with no label.text unchanged', () => {
|
||||
const a = { id: 'a', type: 'rectangle', x: 0, y: 0, width: 100, height: 40 };
|
||||
const b = { id: 'b', type: 'text', x: 10, y: 10, text: 'Hello' };
|
||||
const out = expandLabelsToNative([a, b] as any[]);
|
||||
expect(out).toEqual([a, b]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prepareElementsForScene', () => {
|
||||
it('routes native browser-synced elements without conversion', () => {
|
||||
const native = {
|
||||
id: 'native-1',
|
||||
type: 'rectangle',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 50,
|
||||
seed: 123,
|
||||
versionNonce: 456,
|
||||
version: 1,
|
||||
} as any as ServerElement;
|
||||
|
||||
const converter = vi.fn((elements: readonly any[]) =>
|
||||
elements.map((el) => ({ ...el, converted: true }))
|
||||
);
|
||||
|
||||
const out = prepareElementsForScene([native], converter as any);
|
||||
expect(converter).not.toHaveBeenCalled();
|
||||
expect(out).toHaveLength(1);
|
||||
expect((out[0] as any).id).toBe('native-1');
|
||||
expect((out[0] as any).converted).toBeUndefined();
|
||||
});
|
||||
|
||||
it('routes MCP stubs through converter', () => {
|
||||
const stub = {
|
||||
id: 'stub-1',
|
||||
type: 'rectangle',
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 80,
|
||||
height: 40,
|
||||
label: { text: 'Stub' },
|
||||
version: 1,
|
||||
} as ServerElement;
|
||||
|
||||
const converter = vi.fn((elements: readonly any[]) =>
|
||||
elements.map((el) => ({ ...el, converted: true }))
|
||||
);
|
||||
|
||||
const out = prepareElementsForScene([stub], converter as any);
|
||||
expect(converter).toHaveBeenCalledTimes(1);
|
||||
expect(out.some((el) => (el as any).id === 'stub-1' && (el as any).converted)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
// ─── cleanElementForExcalidraw comprehensive ────────────────
|
||||
|
||||
describe('cleanElementForExcalidraw - comprehensive', () => {
|
||||
it('strips all server-only metadata fields', () => {
|
||||
it('strips server-only metadata fields but preserves Excalidraw version', () => {
|
||||
const serverEl = {
|
||||
id: 'el-1',
|
||||
type: 'rectangle',
|
||||
@@ -31,7 +31,8 @@ describe('cleanElementForExcalidraw - comprehensive', () => {
|
||||
const cleaned = cleanElementForExcalidraw(serverEl);
|
||||
expect(cleaned).not.toHaveProperty('createdAt');
|
||||
expect(cleaned).not.toHaveProperty('updatedAt');
|
||||
expect(cleaned).not.toHaveProperty('version');
|
||||
// version is kept — it is the Excalidraw element version, not a DB field
|
||||
expect(cleaned).toHaveProperty('version', 1);
|
||||
expect(cleaned).not.toHaveProperty('syncedAt');
|
||||
expect(cleaned).not.toHaveProperty('source');
|
||||
expect(cleaned).not.toHaveProperty('syncTimestamp');
|
||||
|
||||
Reference in New Issue
Block a user