Skip to content

Commit 57d79dd

Browse files
authored
Merge pull request #3388 from trycompai/tofik/cs-722-bug-mcp-issue-serialization-bug
fix(policies): restore content serialization in mcp write path
2 parents 4fbd45f + 862edf7 commit 57d79dd

2 files changed

Lines changed: 79 additions & 1 deletion

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { ValidationPipe } from '@nestjs/common';
2+
import { CreatePolicyDto } from './create-policy.dto';
3+
import { UpdatePolicyDto } from './update-policy.dto';
4+
5+
/**
6+
* Regression test for the MCP/public-API policy content serialization bug.
7+
*
8+
* The global ValidationPipe in main.ts runs with `transform: true` and
9+
* `transformOptions: { enableImplicitConversion: true }`. class-transformer
10+
* then coerces each TipTap node object toward the reflected `Array` design-type
11+
* of `content: unknown[]`, turning `[{...}, {...}]` into `[[], []]` — silently
12+
* blanking the policy body on create/update. These tests exercise the exact
13+
* pipe configuration and assert the structured content survives intact.
14+
*/
15+
const pipe = new ValidationPipe({
16+
whitelist: true,
17+
forbidNonWhitelisted: true,
18+
transform: true,
19+
transformOptions: { enableImplicitConversion: true },
20+
});
21+
22+
const TIPTAP_NODES = [
23+
{
24+
type: 'heading',
25+
attrs: { level: 2, textAlign: null },
26+
content: [{ type: 'text', text: 'Purpose' }],
27+
},
28+
{
29+
type: 'paragraph',
30+
attrs: { textAlign: null },
31+
content: [
32+
{
33+
type: 'text',
34+
text: 'Verify workforce integrity and grant the right access.',
35+
marks: [{ type: 'bold' }],
36+
},
37+
],
38+
},
39+
];
40+
41+
describe('Policy DTO content serialization (ValidationPipe)', () => {
42+
it('preserves structured TipTap content on CreatePolicyDto', async () => {
43+
const result = await pipe.transform(
44+
{ name: 'Access Control Policy', content: TIPTAP_NODES },
45+
{ type: 'body', metatype: CreatePolicyDto },
46+
);
47+
48+
expect(result.content).toEqual(TIPTAP_NODES);
49+
// Guard against the exact regression: nodes must not collapse to `[]`.
50+
expect(result.content[0]).toMatchObject({ type: 'heading' });
51+
expect(result.content).not.toEqual([[], []]);
52+
});
53+
54+
it('preserves structured TipTap content on UpdatePolicyDto', async () => {
55+
const result = await pipe.transform(
56+
{ content: TIPTAP_NODES },
57+
{ type: 'body', metatype: UpdatePolicyDto },
58+
);
59+
60+
expect(result.content).toEqual(TIPTAP_NODES);
61+
expect(result.content[1]).toMatchObject({ type: 'paragraph' });
62+
expect(result.content).not.toEqual([[], []]);
63+
});
64+
65+
it('leaves content untouched when omitted on update', async () => {
66+
const result = await pipe.transform(
67+
{ name: 'Renamed only' },
68+
{ type: 'body', metatype: UpdatePolicyDto },
69+
);
70+
71+
expect(result.content).toBeUndefined();
72+
});
73+
});

‎apps/api/src/policies/dto/create-policy.dto.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ export class CreatePolicyDto {
9090
items: { type: 'object', additionalProperties: true },
9191
})
9292
@IsArray()
93-
@Transform(({ value }) => value)
93+
// Return the raw source value. Under the global ValidationPipe's implicit
94+
// conversion, class-transformer coerces each TipTap node toward the reflected
95+
// Array design-type of `content`, mangling `[{...}, {...}]` into `[[], []]`.
96+
// The transform runs after that coercion, so `value` is already mangled —
97+
// `obj.content` is the untouched original. Do not revert this to `value`.
98+
@Transform(({ obj }) => obj.content)
9499
content: unknown[];
95100

96101
@ApiProperty({

0 commit comments

Comments
 (0)