diff --git a/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.spec.ts b/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.spec.ts new file mode 100644 index 0000000000..668a183cc5 --- /dev/null +++ b/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.spec.ts @@ -0,0 +1,49 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { BatchUpdateRequirementsDto } from './batch-update-requirements.dto'; + +/** + * Mirrors the global ValidationPipe config from main.ts: + * whitelist: true, transform: true, enableImplicitConversion: true + */ +function toDto(plain: Record): BatchUpdateRequirementsDto { + return plainToInstance(BatchUpdateRequirementsDto, plain, { + enableImplicitConversion: true, + }); +} + +describe('BatchUpdateRequirementsDto', () => { + it('accepts a valid batch payload', async () => { + const dto = toDto({ + updates: [{ id: 'frq_1', description: 'Updated description' }], + }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors).toHaveLength(0); + }); + + // ── nested description length (FRAME-2: limit raised 5,000 → 10,000) ─ + it('accepts a nested description at the 10,000-char limit', async () => { + const dto = toDto({ + updates: [{ id: 'frq_1', description: 'x'.repeat(10_000) }], + }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors).toHaveLength(0); + }); + + it('rejects a nested description longer than 10,000 chars', async () => { + const dto = toDto({ + updates: [{ id: 'frq_1', description: 'x'.repeat(10_001) }], + }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors.length).toBeGreaterThan(0); + }); +}); diff --git a/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.ts b/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.ts index e0a8530105..250da0f694 100644 --- a/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.ts +++ b/apps/api/src/framework-editor/requirement/dto/batch-update-requirements.dto.ts @@ -30,7 +30,7 @@ class BatchUpdateRequirementItem { @ApiProperty() @IsString() @IsOptional() - @MaxLength(5000) + @MaxLength(10000) description?: string; @ApiProperty() diff --git a/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.spec.ts b/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.spec.ts new file mode 100644 index 0000000000..693976958b --- /dev/null +++ b/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.spec.ts @@ -0,0 +1,50 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { CreateRequirementDto } from './create-requirement.dto'; + +/** + * Mirrors the global ValidationPipe config from main.ts: + * whitelist: true, transform: true, enableImplicitConversion: true + */ +function toDto(plain: Record): CreateRequirementDto { + return plainToInstance(CreateRequirementDto, plain, { + enableImplicitConversion: true, + }); +} + +const VALID_BASE = { + frameworkId: 'frk_abc123', + name: 'AC-1', + description: 'Access control policy and procedures', +}; + +describe('CreateRequirementDto', () => { + it('accepts a valid payload', async () => { + const dto = toDto(VALID_BASE); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors).toHaveLength(0); + }); + + // ── description length (FRAME-2: limit raised 5,000 → 10,000) ────── + it('accepts a description at the 10,000-char limit', async () => { + const dto = toDto({ ...VALID_BASE, description: 'x'.repeat(10_000) }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors).toHaveLength(0); + }); + + it('rejects a description longer than 10,000 chars', async () => { + const dto = toDto({ ...VALID_BASE, description: 'x'.repeat(10_001) }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe('description'); + }); +}); diff --git a/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.ts b/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.ts index 87181ac569..8c21671c00 100644 --- a/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.ts +++ b/apps/api/src/framework-editor/requirement/dto/create-requirement.dto.ts @@ -22,7 +22,7 @@ export class CreateRequirementDto { @ApiProperty({ example: 'Control environment requirements' }) @IsString() - @MaxLength(5000) + @MaxLength(10000) description: string; @ApiPropertyOptional({ example: 'Access Control' }) diff --git a/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.spec.ts b/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.spec.ts new file mode 100644 index 0000000000..83246f17ed --- /dev/null +++ b/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.spec.ts @@ -0,0 +1,44 @@ +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { UpdateRequirementDto } from './update-requirement.dto'; + +/** + * Mirrors the global ValidationPipe config from main.ts: + * whitelist: true, transform: true, enableImplicitConversion: true + */ +function toDto(plain: Record): UpdateRequirementDto { + return plainToInstance(UpdateRequirementDto, plain, { + enableImplicitConversion: true, + }); +} + +describe('UpdateRequirementDto', () => { + it('accepts a partial update (single field)', async () => { + const dto = toDto({ name: 'AC-2' }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors).toHaveLength(0); + }); + + // ── description length (FRAME-2: limit raised 5,000 → 10,000) ────── + it('accepts a description at the 10,000-char limit', async () => { + const dto = toDto({ description: 'x'.repeat(10_000) }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors).toHaveLength(0); + }); + + it('rejects a description longer than 10,000 chars', async () => { + const dto = toDto({ description: 'x'.repeat(10_001) }); + const errors = await validate(dto, { + whitelist: true, + forbidNonWhitelisted: true, + }); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].property).toBe('description'); + }); +}); diff --git a/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.ts b/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.ts index 9a85ebc715..705707934d 100644 --- a/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.ts +++ b/apps/api/src/framework-editor/requirement/dto/update-requirement.dto.ts @@ -17,7 +17,7 @@ export class UpdateRequirementDto { @ApiPropertyOptional() @IsString() @IsOptional() - @MaxLength(5000) + @MaxLength(10000) description?: string; @ApiPropertyOptional() diff --git a/packages/docs/mcp-server.mdx b/packages/docs/mcp-server.mdx index 03ecbcbf16..da77161a3b 100644 --- a/packages/docs/mcp-server.mdx +++ b/packages/docs/mcp-server.mdx @@ -45,8 +45,12 @@ The `npx -y @trycompai/mcp-server` command you'll see throughout is what handles **Easiest path — drag-and-drop install:** -1. Download the latest [`mcp-server.mcpb`](https://github.com/trycompai/comp/releases) bundle from our GitHub Releases page. -2. Drag the `.mcpb` file directly onto the Claude Desktop window. + + Always serves the latest Comp AI MCP bundle (`.mcpb`) — no version hunting. + + +1. Click **Download for Claude Desktop** above to get the latest `mcp-server.mcpb` bundle. +2. Drag the downloaded `.mcpb` file directly onto the Claude Desktop window. 3. Claude Desktop shows an install prompt — confirm, then paste your Comp AI API key in the extension settings. 4. Restart any open chats. Comp AI tools are now available.