- gti-scheduling-mcp: MCP server com ZProAdapter e KlingoAdapter (agendar() completo pros dois) - Workflow n8n 'Agendamento - GChat - Z-PRO': webhook -> AI Agent -> MCP -> Z-PRO, testado com sucesso real - Documentacao: specs, plano, runbook de problemas reais e export do workflow Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
/**
|
|
* test/tools.test.ts
|
|
*
|
|
* Testes unitários para o tratamento de erros das ferramentas MCP.
|
|
*
|
|
* Os handlers devem capturar qualquer erro lançado pelo Adapter e devolver
|
|
* uma resposta MCP estruturada, sem encerrar o servidor e sem vazar
|
|
* detalhes de implementação (stack trace, nomes de classe internos,
|
|
* token). Testes de comportamento específico do ZProAdapter (mapeamento
|
|
* de requisição, classificação de erros HTTP) ficam em
|
|
* `test/zpro-adapter.test.ts`; aqui testamos apenas a camada da tool.
|
|
*/
|
|
|
|
import { describe, it } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { ZProAdapter } from "../src/adapters/zpro/zpro.adapter.js";
|
|
import { createConsultarHorariosHandler } from "../src/tools/consultar-horarios.tool.js";
|
|
import { createAgendarHandler } from "../src/tools/agendar.tool.js";
|
|
import type { ZProConfig } from "../src/config/config.types.js";
|
|
|
|
const config: ZProConfig = {
|
|
provider: "zpro",
|
|
baseUrl: "https://zpro.example.com",
|
|
apiToken: "token-secreto",
|
|
apiId: "api-1",
|
|
};
|
|
|
|
describe("consultar_horarios", () => {
|
|
it("retorna resposta de erro estruturada quando o Z-PRO não suporta a consulta", async () => {
|
|
const adapter = new ZProAdapter(config);
|
|
const handler = createConsultarHorariosHandler(adapter);
|
|
|
|
const result = await handler({ clinicId: "clinic-1" });
|
|
const payload = JSON.parse(result.content[0].text);
|
|
|
|
assert.equal(payload.success, false);
|
|
assert.equal(payload.error.code, "INTERNAL_ERROR");
|
|
});
|
|
|
|
it("não retorna stack trace nem nomes de classe internos na resposta", async () => {
|
|
const adapter = new ZProAdapter(config);
|
|
const handler = createConsultarHorariosHandler(adapter);
|
|
|
|
const result = await handler({ clinicId: "clinic-1" });
|
|
|
|
assert.ok(!result.content[0].text.includes("at "));
|
|
assert.ok(!result.content[0].text.includes("ZProAdapter"));
|
|
assert.ok(!result.content[0].text.includes("ZProApiError"));
|
|
});
|
|
});
|
|
|
|
describe("agendar", () => {
|
|
const failingFetch = async (): Promise<Response> => {
|
|
throw new TypeError("fetch failed");
|
|
};
|
|
const agendarInput = {
|
|
clinicId: "clinic-1",
|
|
startAt: "2026-04-15T14:00:00.000Z",
|
|
endAt: "2026-04-15T15:00:00.000Z",
|
|
patient: { name: "Paciente Exemplo" },
|
|
};
|
|
|
|
it("retorna resposta de erro estruturada quando a chamada ao Z-PRO falha", async () => {
|
|
const adapter = new ZProAdapter(config, { fetchImpl: failingFetch });
|
|
const handler = createAgendarHandler(adapter);
|
|
|
|
const result = await handler(agendarInput);
|
|
const payload = JSON.parse(result.content[0].text);
|
|
|
|
assert.equal(payload.success, false);
|
|
assert.equal(payload.error.code, "INTERNAL_ERROR");
|
|
});
|
|
|
|
it("não vaza o token do Z-PRO na resposta de erro", async () => {
|
|
const adapter = new ZProAdapter(config, { fetchImpl: failingFetch });
|
|
const handler = createAgendarHandler(adapter);
|
|
|
|
const result = await handler(agendarInput);
|
|
|
|
assert.ok(!result.content[0].text.includes(config.apiToken));
|
|
});
|
|
|
|
it("retorna success:true quando o Z-PRO confirma a criação (HTTP 2xx) — não deve parecer falha", async () => {
|
|
const acceptingFetch = async (): Promise<Response> =>
|
|
new Response(JSON.stringify({ id: "algo" }), {
|
|
status: 201,
|
|
headers: { "content-type": "application/json" },
|
|
});
|
|
const adapter = new ZProAdapter(config, { fetchImpl: acceptingFetch });
|
|
const handler = createAgendarHandler(adapter);
|
|
|
|
const result = await handler(agendarInput);
|
|
const payload = JSON.parse(result.content[0].text);
|
|
|
|
assert.equal(payload.success, true);
|
|
assert.equal(payload.data.responseMappingPending, true);
|
|
assert.notEqual(result.isError, true);
|
|
});
|
|
});
|