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`, }, },