From 3dafa120a65384a9ab389d833d5b0260239eda97 Mon Sep 17 00:00:00 2001 From: Prashant-7718 Date: Tue, 19 May 2026 15:39:37 +0530 Subject: [PATCH 1/3] regenerate diagram --- packages/sdk/src/index.e2e.test.ts | 26 +++++++++++ packages/sdk/src/index.test.ts | 71 ++++++++++++++++++++++++++++++ packages/sdk/src/index.ts | 22 +++++++++ packages/sdk/src/types.ts | 36 +++++++++++++++ packages/sdk/src/urls.ts | 1 + 5 files changed, 156 insertions(+) diff --git a/packages/sdk/src/index.e2e.test.ts b/packages/sdk/src/index.e2e.test.ts index 99592e1..6e863a1 100644 --- a/packages/sdk/src/index.e2e.test.ts +++ b/packages/sdk/src/index.e2e.test.ts @@ -267,3 +267,29 @@ describe('suggestPrSummary', () => { } }, 60000); // 60 seconds timeout for AI operations }); + +describe('regenerateDiagram', () => { + it('should regenerate a diagram from updated source files', async () => { + const code = `flowchart TD\n A[Start] --> B[Process]\n B --> C[End]`; + const sourceFiles = [ + 'function processOrder(order) {\n validateOrder(order);\n shipOrder(order);\n}', + ]; + + try { + const result = await client.regenerateDiagram({ + code, + sourceFiles, + }); + + // Verify response structure + expect(result).toHaveProperty('result'); + expect(result).toHaveProperty('code'); + expect(['ok', 'fail']).toContain(result.result); + } catch (error) { + if (error instanceof AICreditsLimitExceededError) { + return; // Credits exceeded is acceptable for E2E test + } + throw error; + } + }, 60000); // 60 seconds timeout for AI operations +}); diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index a3d9b3b..e744549 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -220,4 +220,75 @@ describe('MermaidChart', () => { ).rejects.toThrow(AICreditsLimitExceededError); }); }); + + describe('#regenerateDiagram', () => { + beforeEach(async () => { + await client.setAccessToken('test-access-token'); + }); + + it('should POST to the regenerate endpoint with the request body and return response.data', async () => { + const jsonResponse = { + result: 'ok' as const, + code: '```mermaid\nflowchart TD\n A --> B --> C\n```', + solved: true, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const postSpy = vi.spyOn((client as any).axios, 'post').mockResolvedValue({ + data: jsonResponse, + }); + + const requestBody = { + code: 'flowchart TD\n A --> B', + sourceFiles: ['const x = 1;', 'function foo() {}'], + }; + + const result = await client.regenerateDiagram(requestBody); + + expect(postSpy).toHaveBeenCalledWith(URLS.rest.openai.regenerate, requestBody); + expect(result).toEqual(jsonResponse); + }); + + it('should include creditUsage in response when present', async () => { + const jsonResponse = { + result: 'ok' as const, + code: '```mermaid\nflowchart TD\n A --> B\n```', + solved: true, + creditUsage: { + creditsToDeduct: 1, + baseCost: 1, + reason: 'regeneration', + }, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn((client as any).axios, 'post').mockResolvedValue({ + data: jsonResponse, + }); + + const result = await client.regenerateDiagram({ + code: 'flowchart TD\n A --> B', + sourceFiles: ['const x = 1;'], + }); + + expect(result.creditUsage).toEqual(jsonResponse.creditUsage); + }); + + it('should throw AICreditsLimitExceededError on 402', async () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn((client as any).axios, 'post').mockRejectedValue({ + response: { + status: 402, + data: 'AI credits limit exceeded', + }, + }); + + await expect( + client.regenerateDiagram({ + code: 'flowchart TD\n A --> B', + sourceFiles: [], + }), + ).rejects.toThrow(AICreditsLimitExceededError); + }); + }); }); diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index 4ffe783..9200077 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -21,6 +21,8 @@ import type { RepairDiagramResponse, PrSummaryRequest, PrSummaryResponse, + RegenerateDiagramRequest, + RegenerateDiagramResponse, AICreditsUsage, } from './types.js'; import { URLS } from './urls.js'; @@ -351,6 +353,26 @@ export class MermaidChart { } } + /** + * Regenerates a Mermaid diagram based on updated source files using AI. + * + * @param request - `code` (current diagram source) and `sourceFiles` (full contents of related source files) + * @throws {@link AICreditsLimitExceededError} if credits limit exceeded (HTTP 402) + */ + public async regenerateDiagram( + request: RegenerateDiagramRequest, + ): Promise { + try { + const response = await this.axios.post( + URLS.rest.openai.regenerate, + request, + ); + return response.data; + } catch (error: unknown) { + throwIfAICreditsExceeded(error); + } + } + /** * Chat with Mermaid AI about a diagram. * diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts index 029733b..08e4578 100644 --- a/packages/sdk/src/types.ts +++ b/packages/sdk/src/types.ts @@ -114,6 +114,42 @@ export interface PrSummaryResponse { commitMessage: string; } +/** + * Request parameters for regenerating a Mermaid diagram from updated source files. + */ +export interface RegenerateDiagramRequest { + /** The current Mermaid diagram source to be regenerated. */ + code: string; + /** Ordered full contents of the source files that the diagram is based on. */ + sourceFiles: string[]; +} + +/** + * Response from regenerating a Mermaid diagram. + */ +export interface RegenerateDiagramResponse { + /** + * The status of the regeneration: 'ok' if a valid mermaid code block was generated, 'fail' otherwise. + */ + result: 'ok' | 'fail'; + /** + * Markdown message that may contain a valid mermaid code block. + */ + code: string; + /** + * Whether the diagram regeneration was successful. + */ + solved?: boolean; + /** + * Credit usage for client-side deduction (only present when solved). + */ + creditUsage?: { + creditsToDeduct: number; + baseCost: number; + reason: string; + }; +} + /** * Request parameters for chatting with the Mermaid AI about a diagram. */ diff --git a/packages/sdk/src/urls.ts b/packages/sdk/src/urls.ts index 54d53f3..d525eac 100644 --- a/packages/sdk/src/urls.ts +++ b/packages/sdk/src/urls.ts @@ -42,6 +42,7 @@ export const URLS = { openai: { repair: `/rest-api/openai/repair`, prSummary: `/rest-api/openai/pr-summary`, + regenerate: `/rest-api/openai/regenerate`, chat: `/rest-api/openai/chat`, }, }, From 0d7022e0cc44ecf2990e01b032e81f650b97bc8f Mon Sep 17 00:00:00 2001 From: Prashant-7718 Date: Thu, 28 May 2026 13:45:08 +0530 Subject: [PATCH 2/3] added changeset and updated test cases --- .changeset/smart-nails-mix.md | 5 +++ packages/sdk/src/index.e2e.test.ts | 17 ++++++++ packages/sdk/src/index.test.ts | 70 ------------------------------ 3 files changed, 22 insertions(+), 70 deletions(-) create mode 100644 .changeset/smart-nails-mix.md diff --git a/.changeset/smart-nails-mix.md b/.changeset/smart-nails-mix.md new file mode 100644 index 0000000..80bda25 --- /dev/null +++ b/.changeset/smart-nails-mix.md @@ -0,0 +1,5 @@ +--- +'@mermaidchart/sdk': patch +--- + +Add a regenerate method to the SDK that complements our Mermaid Diagram Sync GitHub App functionality. This method should accept the existing Mermaid diagram content along with source file changes, and regenerate the diagram based on those updates while preserving the overall diagram structure and intent. diff --git a/packages/sdk/src/index.e2e.test.ts b/packages/sdk/src/index.e2e.test.ts index 6e863a1..ff85b99 100644 --- a/packages/sdk/src/index.e2e.test.ts +++ b/packages/sdk/src/index.e2e.test.ts @@ -276,6 +276,8 @@ describe('regenerateDiagram', () => { ]; try { + const creditsBefore = await client.getAICredits(); + const result = await client.regenerateDiagram({ code, sourceFiles, @@ -285,6 +287,21 @@ describe('regenerateDiagram', () => { expect(result).toHaveProperty('result'); expect(result).toHaveProperty('code'); expect(['ok', 'fail']).toContain(result.result); + + // When the AI successfully regenerates the diagram, creditUsage should be present + // and credits should have been deducted + if (result.result === 'ok' && result.solved === true) { + expect(result.creditUsage).toMatchObject({ + creditsToDeduct: expect.any(Number), + baseCost: expect.any(Number), + reason: expect.any(String), + }); + + const creditsAfter = await client.getAICredits(); + expect(creditsAfter.aiCredits.remaining).toBe( + creditsBefore.aiCredits.remaining - result.creditUsage!.creditsToDeduct, + ); + } } catch (error) { if (error instanceof AICreditsLimitExceededError) { return; // Credits exceeded is acceptable for E2E test diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index e744549..2a8ae13 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -221,74 +221,4 @@ describe('MermaidChart', () => { }); }); - describe('#regenerateDiagram', () => { - beforeEach(async () => { - await client.setAccessToken('test-access-token'); - }); - - it('should POST to the regenerate endpoint with the request body and return response.data', async () => { - const jsonResponse = { - result: 'ok' as const, - code: '```mermaid\nflowchart TD\n A --> B --> C\n```', - solved: true, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const postSpy = vi.spyOn((client as any).axios, 'post').mockResolvedValue({ - data: jsonResponse, - }); - - const requestBody = { - code: 'flowchart TD\n A --> B', - sourceFiles: ['const x = 1;', 'function foo() {}'], - }; - - const result = await client.regenerateDiagram(requestBody); - - expect(postSpy).toHaveBeenCalledWith(URLS.rest.openai.regenerate, requestBody); - expect(result).toEqual(jsonResponse); - }); - - it('should include creditUsage in response when present', async () => { - const jsonResponse = { - result: 'ok' as const, - code: '```mermaid\nflowchart TD\n A --> B\n```', - solved: true, - creditUsage: { - creditsToDeduct: 1, - baseCost: 1, - reason: 'regeneration', - }, - }; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - vi.spyOn((client as any).axios, 'post').mockResolvedValue({ - data: jsonResponse, - }); - - const result = await client.regenerateDiagram({ - code: 'flowchart TD\n A --> B', - sourceFiles: ['const x = 1;'], - }); - - expect(result.creditUsage).toEqual(jsonResponse.creditUsage); - }); - - it('should throw AICreditsLimitExceededError on 402', async () => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - vi.spyOn((client as any).axios, 'post').mockRejectedValue({ - response: { - status: 402, - data: 'AI credits limit exceeded', - }, - }); - - await expect( - client.regenerateDiagram({ - code: 'flowchart TD\n A --> B', - sourceFiles: [], - }), - ).rejects.toThrow(AICreditsLimitExceededError); - }); - }); }); From eda87b4cf06bcef9d3fe05ad90d8e7a70f728335 Mon Sep 17 00:00:00 2001 From: Prashant-7718 Date: Thu, 28 May 2026 13:49:28 +0530 Subject: [PATCH 3/3] resolve lint errors --- packages/sdk/src/index.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sdk/src/index.test.ts b/packages/sdk/src/index.test.ts index 2a8ae13..a3d9b3b 100644 --- a/packages/sdk/src/index.test.ts +++ b/packages/sdk/src/index.test.ts @@ -220,5 +220,4 @@ describe('MermaidChart', () => { ).rejects.toThrow(AICreditsLimitExceededError); }); }); - });