Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/sdk/src/index.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
71 changes: 71 additions & 0 deletions packages/sdk/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
22 changes: 22 additions & 0 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import type {
RepairDiagramResponse,
PrSummaryRequest,
PrSummaryResponse,
RegenerateDiagramRequest,
RegenerateDiagramResponse,
AICreditsUsage,
} from './types.js';
import { URLS } from './urls.js';
Expand Down Expand Up @@ -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<RegenerateDiagramResponse> {
try {
const response = await this.axios.post<RegenerateDiagramResponse>(
URLS.rest.openai.regenerate,
request,
);
return response.data;
} catch (error: unknown) {
throwIfAICreditsExceeded(error);
}
}

/**
* Chat with Mermaid AI about a diagram.
*
Expand Down
36 changes: 36 additions & 0 deletions packages/sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
},
},
Expand Down
Loading