@ds9-solutions/mcp 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +43 -0
- package/README.md +53 -0
- package/content/agents/code-reviewer.agent.md +52 -22
- package/content/agents/db-analyser.agent.md +121 -0
- package/content/agents/security-reviewer.agent.md +119 -37
- package/content/agents/test-writer.agent.md +84 -35
- package/content/skills/dart/SKILL.md +139 -0
- package/content/skills/dart/examples/entity.dart +37 -0
- package/content/skills/dart/examples/error_handling.dart +36 -0
- package/content/skills/dart/examples/order_test.dart +34 -0
- package/content/skills/dart/examples/repository.dart +57 -0
- package/content/skills/dart/references/checklist.md +12 -0
- package/content/skills/dart/references/documentation.md +26 -0
- package/content/skills/dart/references/naming.md +27 -0
- package/content/skills/dart-frog/SKILL.md +208 -0
- package/content/skills/dart-frog/examples/auth_middleware.dart +50 -0
- package/content/skills/dart-frog/examples/controller.dart +75 -0
- package/content/skills/dart-frog/examples/dto.dart +41 -0
- package/content/skills/dart-frog/examples/error_response.dart +35 -0
- package/content/skills/dart-frog/examples/exceptions.dart +60 -0
- package/content/skills/dart-frog/examples/middleware_composition.dart +34 -0
- package/content/skills/dart-frog/examples/model.dart +20 -0
- package/content/skills/dart-frog/examples/port.dart +17 -0
- package/content/skills/dart-frog/examples/repository.dart +56 -0
- package/content/skills/dart-frog/examples/route_collection.dart +25 -0
- package/content/skills/dart-frog/examples/route_item.dart +24 -0
- package/content/skills/dart-frog/examples/route_test.dart +33 -0
- package/content/skills/dart-frog/examples/service.dart +51 -0
- package/content/skills/dart-frog/examples/service_test.dart +55 -0
- package/content/skills/dart-frog/references/checklist.md +31 -0
- package/content/skills/dart-frog/references/error-handling.md +48 -0
- package/content/skills/dart-frog/references/fpdart-taskeither.md +53 -0
- package/content/skills/dart-frog/references/layers.md +43 -0
- package/content/skills/dart-frog/references/middlewares-di.md +45 -0
- package/content/skills/dart-frog/references/project-structure.md +67 -0
- package/content/skills/dart-frog/references/routing.md +53 -0
- package/content/skills/dart-frog/references/testing.md +24 -0
- package/content/skills/docker/SKILL.md +3 -3
- package/content/skills/driftorm/SKILL.md +249 -0
- package/content/skills/flutter/SKILL.md +419 -0
- package/content/skills/hex-pattern-dart/SKILL.md +83 -0
- package/content/skills/hex-pattern-dart/examples/adapter-in.md +24 -0
- package/content/skills/hex-pattern-dart/examples/adapter-out.md +20 -0
- package/content/skills/hex-pattern-dart/examples/errors.md +25 -0
- package/content/skills/hex-pattern-dart/examples/port.md +11 -0
- package/content/skills/hex-pattern-dart/examples/service.md +20 -0
- package/content/skills/hex-pattern-dart/examples/structure-tree.md +28 -0
- package/content/skills/hex-pattern-dart/references/anti-patterns.md +16 -0
- package/content/skills/hex-pattern-dart/references/checklist.md +34 -0
- package/content/skills/hex-pattern-dart/references/dependency-injection.md +32 -0
- package/content/skills/hex-pattern-dart/references/error-layers.md +27 -0
- package/content/skills/hex-pattern-dart/references/layers-and-dependency-rule.md +29 -0
- package/content/skills/hex-pattern-dart/references/persistence.md +23 -0
- package/content/skills/hex-pattern-dart/references/ports-and-adapters.md +33 -0
- package/content/skills/hex-pattern-dart/references/testing.md +30 -0
- package/content/skills/mysql/SKILL.md +3 -3
- package/content/skills/typescript/SKILL.md +4 -4
- package/dist/src/content.js +10 -2
- package/dist/src/init.js +1 -0
- package/dist/src/search.js +20 -12
- package/dist/src/server.js +60 -4
- package/dist/src/skill-files.js +125 -0
- package/package.json +2 -2
- package/content/agents/db-specialist.agent.md +0 -78
- package/content/agents/nestjs-api-designer.agent.md +0 -61
- package/content/skills/hexagonal/SKILL.md +0 -156
- package/content/skills/jest-testing/SKILL.md +0 -88
- package/content/skills/nestjs/SKILL.md +0 -100
- package/content/skills/typeorm/SKILL.md +0 -119
- package/dist/test/mcp.test.js +0 -112
package/dist/src/server.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { rankByMetadata, searchBodies } from "./search.js";
|
|
12
|
+
import { isSafeRelativePath, MAX_PATH_LENGTH, SKILL_ID } from "./skill-files.js";
|
|
12
13
|
// Enviado ao host no handshake. Hosts sem suporte nativo a skills/agentes dependem disto (e do
|
|
13
14
|
// bloco no AGENTS.md) para saber que devem consultar o servidor antes de implementar.
|
|
14
15
|
const INSTRUCTIONS = [
|
|
15
16
|
"Servidor de diretrizes oficiais da DS9 (skills por tecnologia e subagentes especializados).",
|
|
16
17
|
"Antes de implementar, revisar ou testar código, chame `recommend_for_task` com a descrição da tarefa",
|
|
17
18
|
"e depois `get_skill` / `get_agent` para ler as diretrizes recomendadas e segui-las.",
|
|
19
|
+
"Se a skill indicar arquivos complementares, use `list_skill_files` e `get_skill_file` para ler exemplos e referências sob demanda.",
|
|
18
20
|
].join(" ");
|
|
19
21
|
// Monta a resposta padrão de uma tool. `isError` sinaliza ao LLM que a chamada falhou, mas sem
|
|
20
22
|
// derrubar o protocolo (erro de uso != erro do servidor).
|
|
@@ -31,8 +33,21 @@ export function createServer(entries, version = "0.0.0") {
|
|
|
31
33
|
// Helpers de consulta sobre as entradas.
|
|
32
34
|
const byKind = (kind) => entries.filter((e) => e.kind === kind);
|
|
33
35
|
const find = (kind, id) => entries.find((e) => e.kind === kind && e.id === id);
|
|
34
|
-
const render = (e) => `# ${e.kind === "skill" ? "Skill" : "Agente"}: ${e.id}\n\n> ${e.description}\n\n${e.body}
|
|
35
|
-
|
|
36
|
+
const render = (e) => `# ${e.kind === "skill" ? "Skill" : "Agente"}: ${e.id}\n\n> ${e.description}\n\n${e.body}` +
|
|
37
|
+
(e.files.length
|
|
38
|
+
? `\n\n---\nArquivos complementares (${e.files.length}): use \`list_skill_files\` e \`get_skill_file\` com skill="${e.id}" para exemplos e referências sob demanda.`
|
|
39
|
+
: "");
|
|
40
|
+
const listing = (items) => items
|
|
41
|
+
.map((e) => `- ${e.id}: ${e.description}${e.files.length ? ` (+${e.files.length} arquivos)` : ""}`)
|
|
42
|
+
.join("\n") || "(nenhum resultado)";
|
|
43
|
+
// Erros de skill/arquivo nunca citam caminhos do disco: só ids e caminhos do índice.
|
|
44
|
+
const skillOrError = (skill) => {
|
|
45
|
+
const entry = find("skill", skill);
|
|
46
|
+
return entry
|
|
47
|
+
? { entry }
|
|
48
|
+
: { error: text(`skill "${skill}" não encontrada. Disponíveis:\n${listing(byKind("skill"))}`, true) };
|
|
49
|
+
};
|
|
50
|
+
const fileList = (e) => e.files.map((f) => `- ${f.path} (${f.size} bytes)${f.description ? `: ${f.description}` : ""}`).join("\n");
|
|
36
51
|
// Skills e agentes têm exatamente a mesma API (list/get/prompt/resource). Em vez de duplicar o
|
|
37
52
|
// código, registramos os dois em um laço; só mudam os nomes e os rótulos.
|
|
38
53
|
for (const [kind, plural, label] of [
|
|
@@ -63,7 +78,7 @@ export function createServer(entries, version = "0.0.0") {
|
|
|
63
78
|
}
|
|
64
79
|
return text(render(entry));
|
|
65
80
|
});
|
|
66
|
-
// Um prompt por entrada (ex.: `skill-
|
|
81
|
+
// Um prompt por entrada (ex.: `skill-hex-pattern-dart`). Fica estático porque o catálogo é conhecido na
|
|
67
82
|
// inicialização do servidor.
|
|
68
83
|
for (const entry of byKind(kind)) {
|
|
69
84
|
server.registerPrompt(`${kind}-${entry.id}`, { description: entry.description }, () => ({
|
|
@@ -91,6 +106,45 @@ export function createServer(entries, version = "0.0.0") {
|
|
|
91
106
|
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: render(entry) }] };
|
|
92
107
|
});
|
|
93
108
|
}
|
|
109
|
+
// list_skill_files / get_skill_file — carga sob demanda dos arquivos complementares de uma skill.
|
|
110
|
+
// Só leem o índice em memória montado no carregamento; o cliente nunca informa caminho de disco.
|
|
111
|
+
const skillSchema = z
|
|
112
|
+
.string()
|
|
113
|
+
.regex(SKILL_ID, "id de skill inválido")
|
|
114
|
+
.describe("Id da skill (veja list_skills)");
|
|
115
|
+
server.registerTool("list_skill_files", {
|
|
116
|
+
description: "Lista os arquivos complementares (exemplos, referências) de uma skill DS9, com tamanho e descrição.",
|
|
117
|
+
inputSchema: { skill: skillSchema },
|
|
118
|
+
annotations: { readOnlyHint: true },
|
|
119
|
+
}, async ({ skill }) => {
|
|
120
|
+
const { entry, error } = skillOrError(skill);
|
|
121
|
+
if (!entry)
|
|
122
|
+
return error;
|
|
123
|
+
return text(entry.files.length
|
|
124
|
+
? `Arquivos da skill "${skill}" (leia com get_skill_file):\n${fileList(entry)}`
|
|
125
|
+
: `A skill "${skill}" não possui arquivos complementares.`);
|
|
126
|
+
});
|
|
127
|
+
server.registerTool("get_skill_file", {
|
|
128
|
+
description: "Retorna o conteúdo de um arquivo complementar de uma skill DS9. Use o caminho exato de list_skill_files.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
skill: skillSchema,
|
|
131
|
+
path: z
|
|
132
|
+
.string()
|
|
133
|
+
.max(MAX_PATH_LENGTH)
|
|
134
|
+
.describe("Caminho relativo listado por list_skill_files, ex.: examples/entity.dart"),
|
|
135
|
+
},
|
|
136
|
+
annotations: { readOnlyHint: true },
|
|
137
|
+
}, async ({ skill, path }) => {
|
|
138
|
+
const { entry, error } = skillOrError(skill);
|
|
139
|
+
if (!entry)
|
|
140
|
+
return error;
|
|
141
|
+
// Busca exata no índice: caminhos fora dele (inclusive `..`, absolutos etc.) simplesmente não existem.
|
|
142
|
+
const file = isSafeRelativePath(path) ? entry.files.find((f) => f.path === path) : undefined;
|
|
143
|
+
if (!file) {
|
|
144
|
+
return text(`Arquivo não encontrado na skill "${skill}". Arquivos válidos:\n${fileList(entry) || "(nenhum)"}`, true);
|
|
145
|
+
}
|
|
146
|
+
return text(`# ${skill}/${file.path}\n\n${file.text}`);
|
|
147
|
+
});
|
|
94
148
|
// search_guidelines — para dúvidas pontuais ("como nomear migrations?") sem ler arquivos inteiros.
|
|
95
149
|
server.registerTool("search_guidelines", {
|
|
96
150
|
description: "Busca um termo no conteúdo de todas as skills e agentes DS9 e devolve as linhas mais relevantes.",
|
|
@@ -99,7 +153,9 @@ export function createServer(entries, version = "0.0.0") {
|
|
|
99
153
|
}, async ({ query, limit }) => {
|
|
100
154
|
const hits = searchBodies(entries, query, limit ?? 8);
|
|
101
155
|
return text(hits.length
|
|
102
|
-
? hits
|
|
156
|
+
? hits
|
|
157
|
+
.map((h) => `[${h.kind}:${h.id}${h.file ? `:${h.file}` : ""}:L${h.line}] ${h.text}`)
|
|
158
|
+
.join("\n")
|
|
103
159
|
: "Nenhum trecho encontrado.");
|
|
104
160
|
});
|
|
105
161
|
// recommend_for_task — é a "ativação automática" possível via MCP: compensa a falta de roteamento
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arquivos complementares de uma skill (`skills/<id>/examples/`, `references/` etc.).
|
|
3
|
+
*
|
|
4
|
+
* Segurança — por que é uma allowlist e não uma sanitização de caminho:
|
|
5
|
+
* - tudo é indexado UMA vez, no carregamento, pelo próprio servidor;
|
|
6
|
+
* - `get_skill_file` só encontra caminhos que estão nesse índice (comparação exata de string);
|
|
7
|
+
* - o texto vem da memória: nenhuma chamada de tool toca o disco nem monta caminho com entrada do
|
|
8
|
+
* cliente. Não há como escapar da pasta da skill porque o cliente nunca fornece um caminho de
|
|
9
|
+
* arquivo, apenas uma chave de busca.
|
|
10
|
+
* A varredura ainda aplica limites e ignora symlinks, ocultos e extensões fora da lista.
|
|
11
|
+
*/
|
|
12
|
+
import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
13
|
+
import { join, sep } from "node:path";
|
|
14
|
+
export const ALLOWED_EXTENSIONS = new Set([
|
|
15
|
+
".md",
|
|
16
|
+
".dart",
|
|
17
|
+
".yaml",
|
|
18
|
+
".yml",
|
|
19
|
+
".json",
|
|
20
|
+
".txt",
|
|
21
|
+
]);
|
|
22
|
+
export const MAX_FILE_BYTES = 100 * 1024;
|
|
23
|
+
export const MAX_FILES_PER_SKILL = 200;
|
|
24
|
+
export const MAX_DEPTH = 4;
|
|
25
|
+
export const MAX_PATH_LENGTH = 200;
|
|
26
|
+
// SKILL.md é servido por `get_skill`; README.md não é copiado pelo `sync-content`, então ignorá-lo
|
|
27
|
+
// aqui mantém o comportamento igual em desenvolvimento e em produção.
|
|
28
|
+
const IGNORED_NAMES = new Set(["skill.md", "readme.md", "node_modules"]);
|
|
29
|
+
/** Id de skill válido: é o nome da pasta, em kebab-case. */
|
|
30
|
+
export const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
31
|
+
// eslint-disable-next-line no-control-regex
|
|
32
|
+
const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
|
|
33
|
+
/**
|
|
34
|
+
* Valida o formato do caminho informado pelo cliente. É uma barreira extra de entrada; a proteção
|
|
35
|
+
* real é a busca exata no índice. Rejeita tudo que não seja um caminho relativo simples com `/`.
|
|
36
|
+
*/
|
|
37
|
+
export function isSafeRelativePath(path) {
|
|
38
|
+
if (path.length === 0 || path.length > MAX_PATH_LENGTH)
|
|
39
|
+
return false;
|
|
40
|
+
if (CONTROL_CHARS.test(path))
|
|
41
|
+
return false;
|
|
42
|
+
if (path.includes("\\") || path.includes(":") || path.includes("%"))
|
|
43
|
+
return false;
|
|
44
|
+
if (path.startsWith("/"))
|
|
45
|
+
return false;
|
|
46
|
+
return path.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
|
|
47
|
+
}
|
|
48
|
+
const UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
49
|
+
/** Primeira linha útil do arquivo: título Markdown ou comentário `///`/`//`. */
|
|
50
|
+
function describe(text) {
|
|
51
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
52
|
+
const line = raw.trim();
|
|
53
|
+
if (!line)
|
|
54
|
+
continue;
|
|
55
|
+
const cleaned = line.replace(/^(#+|\/\/\/?|\*|>)\s*/, "").trim();
|
|
56
|
+
if (cleaned && cleaned !== "---")
|
|
57
|
+
return cleaned.slice(0, 160);
|
|
58
|
+
}
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Varre a pasta de uma skill e devolve os arquivos complementares permitidos, em ordem estável.
|
|
63
|
+
* Problemas (arquivo grande, extensão proibida, symlink etc.) viram avisos e o arquivo é ignorado:
|
|
64
|
+
* um item ruim nunca derruba o carregamento das demais skills.
|
|
65
|
+
*/
|
|
66
|
+
export function scanSkillFiles(skillDir, warnings) {
|
|
67
|
+
const files = [];
|
|
68
|
+
const rootReal = realpathSync(skillDir);
|
|
69
|
+
const walk = (dir, prefix, depth) => {
|
|
70
|
+
for (const name of readdirSync(dir).sort()) {
|
|
71
|
+
const rel = prefix ? `${prefix}/${name}` : name;
|
|
72
|
+
const abs = join(dir, name);
|
|
73
|
+
if (name.startsWith(".") || IGNORED_NAMES.has(name.toLowerCase()))
|
|
74
|
+
continue;
|
|
75
|
+
const stat = lstatSync(abs);
|
|
76
|
+
if (stat.isSymbolicLink()) {
|
|
77
|
+
warnings.push(`"${rel}" ignorado: link simbólico`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (stat.isDirectory()) {
|
|
81
|
+
if (depth >= MAX_DEPTH)
|
|
82
|
+
warnings.push(`"${rel}" ignorado: profundidade acima de ${MAX_DEPTH}`);
|
|
83
|
+
else
|
|
84
|
+
walk(abs, rel, depth + 1);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (!stat.isFile())
|
|
88
|
+
continue;
|
|
89
|
+
const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
|
|
90
|
+
if (!ALLOWED_EXTENSIONS.has(ext)) {
|
|
91
|
+
warnings.push(`"${rel}" ignorado: extensão não permitida`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
95
|
+
warnings.push(`"${rel}" ignorado: maior que ${MAX_FILE_BYTES} bytes`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (files.length >= MAX_FILES_PER_SKILL) {
|
|
99
|
+
warnings.push(`"${rel}" ignorado: limite de ${MAX_FILES_PER_SKILL} arquivos por skill`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (!rel.split("/").every((s) => isSafeRelativePath(s))) {
|
|
103
|
+
warnings.push(`"${rel}" ignorado: nome de arquivo inválido`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Confinamento: o arquivo real precisa estar dentro da pasta da skill.
|
|
107
|
+
const real = realpathSync(abs);
|
|
108
|
+
if (!real.startsWith(rootReal + sep)) {
|
|
109
|
+
warnings.push(`"${rel}" ignorado: fora da pasta da skill`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let text;
|
|
113
|
+
try {
|
|
114
|
+
text = UTF8.decode(readFileSync(real));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
warnings.push(`"${rel}" ignorado: não é texto UTF-8 válido`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
files.push({ path: rel, size: stat.size, description: describe(text), text });
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
walk(skillDir, "", 1);
|
|
124
|
+
return files;
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ds9-solutions/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Servidor MCP da DS9: expõe skills e subagentes oficiais para qualquer IDE/LLM com suporte a MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"ds9-mcp": "dist/src/index.js"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
|
-
"dist",
|
|
11
|
+
"dist/src",
|
|
12
12
|
"content"
|
|
13
13
|
],
|
|
14
14
|
"engines": {
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: DB Specialist
|
|
3
|
-
description: Identifica o banco de dados e o ORM usados no projeto, analisa models/entities, migrations, índices e o impacto de performance das queries contra o schema. Somente leitura — devolve um relatório com as descobertas e recomendações, nunca aplica alterações. Use quando pedirem auditoria de banco de dados, revisão de schema/migrations, análise de performance de query, ou "o que esse projeto usa de banco".
|
|
4
|
-
tools: ["read", "search"]
|
|
5
|
-
disable-model-invocation: false
|
|
6
|
-
user-invocable: true
|
|
7
|
-
is_background: true
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# DB Specialist — DS9
|
|
11
|
-
|
|
12
|
-
Você é um especialista em persistência: somente leitura, produz um relatório técnico sobre o banco de dados e o ORM do projeto analisado.
|
|
13
|
-
|
|
14
|
-
## 1. Identificar o stack de dados
|
|
15
|
-
|
|
16
|
-
- Banco de dados: procure em `package.json` (drivers como `mysql2`, `pg`, `mongodb`), `docker-compose.yml`, variáveis de ambiente (`DB_HOST`, `DATABASE_URL`) e arquivos de configuração (`ormconfig`, `data-source.ts`).
|
|
17
|
-
- ORM/query builder: procure dependências (`typeorm`, `prisma`, `sequelize`, `knex`, `mongoose`) e a estrutura de pastas (`*.entity.ts`, `schema.prisma`, `migrations/`).
|
|
18
|
-
- Se o projeto usa MySQL + TypeORM (padrão DS9), aplique as skills `mysql` e `typeorm` como critério de avaliação. Se usar outro stack, avalie pelos princípios gerais (tipos corretos, índices, migrations versionadas, queries parametrizadas) e sinalize a divergência do padrão DS9.
|
|
19
|
-
|
|
20
|
-
## 2. Analisar models/entities
|
|
21
|
-
|
|
22
|
-
- Tipos de coluna coerentes com o domínio (dinheiro em `decimal`, não `float`; datas em tipo de data, não `varchar`).
|
|
23
|
-
- Chaves primárias e estratégia de ID consistente entre entidades.
|
|
24
|
-
- Relações (`@ManyToOne`, `@OneToMany`, etc.) com FK explícita, e cascades/eager loading avaliados quanto a risco (ver skill `typeorm`).
|
|
25
|
-
- Soft delete, auditoria (`created_at`/`updated_at`) presentes onde o domínio exige.
|
|
26
|
-
|
|
27
|
-
## 3. Analisar migrations
|
|
28
|
-
|
|
29
|
-
- Toda alteração de schema tem migration versionada (nenhuma evidência de alteração manual direta)?
|
|
30
|
-
- Migrations têm `up`/`down` reversível? Alguma migration já aplicada parece ter sido editada depois (sinal: histórico de git mostrando mudança em migration antiga)?
|
|
31
|
-
- Alterações potencialmente perigosas em produção (`DROP COLUMN`, `ALTER` bloqueante em tabela grande) sem cuidado explícito documentado.
|
|
32
|
-
|
|
33
|
-
## 4. Analisar índices
|
|
34
|
-
|
|
35
|
-
- FKs têm índice correspondente?
|
|
36
|
-
- Índices compostos existentes fazem sentido para os padrões de consulta observados no código (repositórios/queries)?
|
|
37
|
-
- Índices redundantes ou ausentes em colunas usadas com frequência em `WHERE`/`ORDER BY`/`JOIN`.
|
|
38
|
-
- Existe índice único garantindo invariantes de negócio no banco (não só na aplicação)?
|
|
39
|
-
|
|
40
|
-
## 5. Analisar queries e impacto de performance
|
|
41
|
-
|
|
42
|
-
- `SELECT *` em código de produção; queries sem `LIMIT`/paginação em listagens.
|
|
43
|
-
- Risco de N+1 (loop fazendo uma query por item, ou relation `eager` carregando grafos grandes).
|
|
44
|
-
- Queries com filtro em coluna sem índice correspondente (risco de full table scan).
|
|
45
|
-
- SQL concatenado com valor de usuário (risco de SQL Injection) em vez de query parametrizada/QueryBuilder.
|
|
46
|
-
- Transações ausentes em operações que alteram múltiplas tabelas com invariantes relacionadas.
|
|
47
|
-
|
|
48
|
-
## Formato do relatório
|
|
49
|
-
|
|
50
|
-
```text
|
|
51
|
-
## Stack de dados
|
|
52
|
-
- Banco: ...
|
|
53
|
-
- ORM: ...
|
|
54
|
-
- Observações de configuração (synchronize, migrationsRun, etc.): ...
|
|
55
|
-
|
|
56
|
-
## Achados — Models/Entities
|
|
57
|
-
[severidade] local — descrição — recomendação
|
|
58
|
-
|
|
59
|
-
## Achados — Migrations
|
|
60
|
-
[severidade] local — descrição — recomendação
|
|
61
|
-
|
|
62
|
-
## Achados — Índices
|
|
63
|
-
[severidade] local — descrição — recomendação
|
|
64
|
-
|
|
65
|
-
## Achados — Queries/Performance
|
|
66
|
-
[severidade] local — descrição — recomendação
|
|
67
|
-
|
|
68
|
-
## Resumo
|
|
69
|
-
Total por severidade e prioridade sugerida de correção.
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Severidade: 🔴 crítica (risco de perda de dados/corrupção/indisponibilidade) | 🟠 alta (performance ruim em produção, falta de integridade) | 🟡 média (dívida técnica) | ⚪ baixa (observação/sugestão).
|
|
73
|
-
|
|
74
|
-
## Não fazer
|
|
75
|
-
|
|
76
|
-
- Não editar entities, migrations ou qualquer arquivo — este agente é exclusivamente de leitura e relatório.
|
|
77
|
-
- Não executar migrations nem comandos que alterem o banco.
|
|
78
|
-
- Não recomendar mudança de stack (trocar de ORM/banco) a menos que explicitamente perguntado; o foco é avaliar o que já existe.
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: NestJS API Designer
|
|
3
|
-
description: Analisa, revisa e cria contratos de endpoints REST em projetos NestJS da DS9 — rotas, DTOs de entrada/saída, códigos de status HTTP, versionamento de API e documentação OpenAPI/Swagger. Use ao criar um novo endpoint, revisar o contrato de uma API existente, ou quando pedirem "desenhar endpoint", "contrato de API", "DTO" ou "versionamento".
|
|
4
|
-
tools: ["read", "search", "edit"]
|
|
5
|
-
disable-model-invocation: false
|
|
6
|
-
user-invocable: true
|
|
7
|
-
is_background: true
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# NestJS API Designer — DS9
|
|
11
|
-
|
|
12
|
-
Projeta e revisa contratos de API REST em NestJS, seguindo as skills `nestjs` e `hexagonal` (controllers são adapters de entrada, sem regra de negócio) e `typescript` (tipagem estrita).
|
|
13
|
-
|
|
14
|
-
## O que analisar/produzir
|
|
15
|
-
|
|
16
|
-
### 1. Rotas
|
|
17
|
-
|
|
18
|
-
- Recurso no plural, hierarquia clara (`/orders`, `/orders/:id`, `/orders/:id/items`).
|
|
19
|
-
- Verbo HTTP correto para a ação: `GET` (leitura, idempotente), `POST` (criação ou ação não idempotente), `PUT` (substituição completa), `PATCH` (atualização parcial), `DELETE` (remoção).
|
|
20
|
-
- Nomes de rota não repetem o nome do recurso já presente no controller (`@Controller('orders')` + `@Get(':id')`, não `@Get('orders/:id')`).
|
|
21
|
-
|
|
22
|
-
### 2. DTOs
|
|
23
|
-
|
|
24
|
-
- Um DTO de entrada por operação (`CreateOrderDto`, `UpdateOrderDto` — nunca reaproveitar o mesmo DTO para create e update se os campos obrigatórios diferem).
|
|
25
|
-
- Um DTO de saída dedicado (`OrderResponseDto`), nunca a entidade de domínio/persistência exposta diretamente (ver skill `hexagonal`).
|
|
26
|
-
- Toda propriedade do DTO de entrada tem decorator de validação (`class-validator`) compatível com a regra de negócio real (ex.: `@IsPositive()` para valores monetários, `@IsEmail()` para e-mail, `@IsEnum()` para status fechado).
|
|
27
|
-
- DTOs aninhados usam `@ValidateNested()` + `@Type(() => SubDto)`.
|
|
28
|
-
- Documente cada propriedade com `@ApiProperty({ description, example })` (ou `@ApiPropertyOptional` quando opcional).
|
|
29
|
-
|
|
30
|
-
### 3. Códigos de status
|
|
31
|
-
|
|
32
|
-
- `201 Created` em `POST` que cria recurso (com `@HttpCode(HttpStatus.CREATED)` se o Nest não aplicar por padrão).
|
|
33
|
-
- `200 OK` para `GET`/`PATCH`/`PUT` bem-sucedidos com corpo de resposta.
|
|
34
|
-
- `204 No Content` para `DELETE` bem-sucedido sem corpo.
|
|
35
|
-
- `400 Bad Request` para falha de validação (já automático via `ValidationPipe`), `404 Not Found` para recurso inexistente, `409 Conflict` para violação de invariante única (ex.: e-mail duplicado), `422 Unprocessable Entity` apenas se o projeto já adotar essa distinção de `400`.
|
|
36
|
-
- Documente cada status possível com `@ApiResponse({ status, description, type })`.
|
|
37
|
-
|
|
38
|
-
### 4. Versionamento
|
|
39
|
-
|
|
40
|
-
- Toda API nova decide explicitamente a estratégia de versionamento do projeto (padrão recomendado: URI versioning do Nest — `app.enableVersioning({ type: VersioningType.URI, defaultVersion: '1' })`, rotas expostas como `/v1/orders`).
|
|
41
|
-
- Breaking changes em um contrato existente exigem nova versão (`/v2/...`), nunca alteração destrutiva silenciosa na versão corrente.
|
|
42
|
-
- Se o projeto já usa outra estratégia (header, media type), siga a convenção existente em vez de introduzir uma segunda.
|
|
43
|
-
|
|
44
|
-
### 5. OpenAPI/Swagger
|
|
45
|
-
|
|
46
|
-
- Todo controller tem `@ApiTags('orders')`; toda rota tem `@ApiOperation({ summary })`.
|
|
47
|
-
- Gere/atualize a configuração do `SwaggerModule` se o projeto ainda não a tiver, mantendo o documento sincronizado com os DTOs reais (não escrever spec OpenAPI manual divergente do código).
|
|
48
|
-
|
|
49
|
-
## Processo ao revisar um contrato existente
|
|
50
|
-
|
|
51
|
-
1. Ler o controller, DTOs de entrada/saída e o use case associado.
|
|
52
|
-
2. Checar os 5 pontos acima e listar divergências.
|
|
53
|
-
3. Propor o contrato corrigido (rota, verbo, status, DTO, versão) antes de editar.
|
|
54
|
-
4. Aplicar a edição apenas nos arquivos de contrato (controller, DTOs, decorators Swagger) — nunca na lógica de negócio do use case/domínio.
|
|
55
|
-
|
|
56
|
-
## Não fazer
|
|
57
|
-
|
|
58
|
-
- Não expor entidade de domínio/persistência como corpo de resposta.
|
|
59
|
-
- Não reutilizar um DTO de criação para atualização parcial sem revisar `required`/`optional` de cada campo.
|
|
60
|
-
- Não introduzir uma segunda estratégia de versionamento no mesmo projeto.
|
|
61
|
-
- Não alterar regra de negócio do use case — este agente cuida apenas do contrato HTTP (ver skill `hexagonal` para a fronteira adapter/use case).
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: hexagonal
|
|
3
|
-
description: Diretrizes de arquitetura hexagonal (ports & adapters) aplicadas a projetos NestJS da DS9. Use ao criar, revisar ou refatorar módulos, definir camadas (domain/application/infrastructure), portas (interfaces) e adapters (controllers, repositórios, clients externos), ou quando o usuário pedir "arquitetura hexagonal", "ports and adapters" ou organização de um novo módulo/feature.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Arquitetura Hexagonal (Ports & Adapters) em NestJS
|
|
7
|
-
|
|
8
|
-
Padrão arquitetural oficial da DS9 para backends NestJS. Objetivo: isolar as regras de negócio (domínio) de frameworks, banco de dados e protocolos de entrada/saída, tornando o core testável e substituível.
|
|
9
|
-
|
|
10
|
-
## Camadas e responsabilidades
|
|
11
|
-
|
|
12
|
-
```text
|
|
13
|
-
src/
|
|
14
|
-
└── <contexto>/ # bounded context / módulo de negócio
|
|
15
|
-
├── domain/ # núcleo — sem dependência de NestJS, TypeORM, HTTP, etc.
|
|
16
|
-
│ ├── entities/ # entidades e value objects (regras e invariantes)
|
|
17
|
-
│ ├── ports/ # interfaces (contratos) que o domínio precisa
|
|
18
|
-
│ │ ├── in/ # casos de uso expostos (ex.: CreateOrderUseCase)
|
|
19
|
-
│ │ └── out/ # dependências externas (ex.: OrderRepositoryPort)
|
|
20
|
-
│ └── errors/ # exceções de domínio
|
|
21
|
-
├── application/ # orquestração dos casos de uso (services)
|
|
22
|
-
│ └── use-cases/ # implementa as portas "in", orquestra portas "out"
|
|
23
|
-
└── infrastructure/ # adapters concretos — tudo que é "detalhe"
|
|
24
|
-
├── adapters/
|
|
25
|
-
│ ├── in/ # entrada: controllers REST, consumers de fila, resolvers GraphQL
|
|
26
|
-
│ └── out/ # saída: repositórios TypeORM, HTTP clients, gateways de pagamento
|
|
27
|
-
└── <contexto>.module.ts # module NestJS que faz o "wiring" via injeção de dependência
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
### Regra de dependência (inversão obrigatória)
|
|
31
|
-
|
|
32
|
-
- `domain/` **não importa nada** de `infrastructure/`, `application/`, `@nestjs/*`, `typeorm`, `express`, etc. Só TypeScript puro.
|
|
33
|
-
- `application/` depende apenas de `domain/` (entidades e ports).
|
|
34
|
-
- `infrastructure/` depende de `domain/` e `application/`, nunca o contrário.
|
|
35
|
-
- Sentido das setas: `infrastructure` → `application` → `domain`. Nunca inverter.
|
|
36
|
-
|
|
37
|
-
## Ports (interfaces)
|
|
38
|
-
|
|
39
|
-
- Toda porta é uma `interface` TypeScript definida em `domain/ports/`.
|
|
40
|
-
- Ports de saída (`out`) representam o que o domínio **precisa** do mundo externo (persistência, mensageria, serviços externos). Nomeie como `<Algo>Port` (ex.: `OrderRepositoryPort`, `PaymentGatewayPort`).
|
|
41
|
-
- Ports de entrada (`in`) representam casos de uso que o domínio **oferece**. Nomeie como `<Ação><Entidade>UseCase` (ex.: `CreateOrderUseCase`).
|
|
42
|
-
- Use `Symbol`/token de injeção dedicado para cada porta (evita colisão e permite múltiplas implementações):
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
// domain/ports/out/order-repository.port.ts
|
|
46
|
-
export interface OrderRepositoryPort {
|
|
47
|
-
save(order: Order): Promise<void>;
|
|
48
|
-
findById(id: string): Promise<Order | null>;
|
|
49
|
-
}
|
|
50
|
-
export const ORDER_REPOSITORY_PORT = Symbol('ORDER_REPOSITORY_PORT');
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Adapters
|
|
54
|
-
|
|
55
|
-
- **Adapter de entrada (`in`)**: controller HTTP, consumer de fila, cron job, resolver GraphQL. Responsabilidade única: traduzir o protocolo externo (DTO/request) para a chamada do caso de uso, e o retorno do caso de uso para a resposta do protocolo. Nunca contém regra de negócio.
|
|
56
|
-
- **Adapter de saída (`out`)**: implementação concreta de uma port `out` (ex.: `TypeOrmOrderRepository implements OrderRepositoryPort`, usando as diretrizes da skill `typeorm`). Fica em `infrastructure/adapters/out/`.
|
|
57
|
-
- DTOs de entrada/saída (`class-validator`/`class-transformer`) vivem no adapter de entrada, nunca no domínio.
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
// infrastructure/adapters/out/typeorm-order.repository.ts
|
|
61
|
-
@Injectable()
|
|
62
|
-
export class TypeOrmOrderRepository implements OrderRepositoryPort {
|
|
63
|
-
constructor(@InjectRepository(OrderEntity) private readonly repo: Repository<OrderEntity>) {}
|
|
64
|
-
|
|
65
|
-
async save(order: Order): Promise<void> {
|
|
66
|
-
await this.repo.save(OrderMapper.toPersistence(order));
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async findById(id: string): Promise<Order | null> {
|
|
70
|
-
const row = await this.repo.findOneBy({ id });
|
|
71
|
-
return row ? OrderMapper.toDomain(row) : null;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Wiring (module NestJS)
|
|
77
|
-
|
|
78
|
-
O module conecta a porta ao adapter concreto via `useClass`/token:
|
|
79
|
-
|
|
80
|
-
```typescript
|
|
81
|
-
@Module({
|
|
82
|
-
controllers: [OrderController],
|
|
83
|
-
providers: [
|
|
84
|
-
CreateOrderUseCase,
|
|
85
|
-
{ provide: ORDER_REPOSITORY_PORT, useClass: TypeOrmOrderRepository },
|
|
86
|
-
],
|
|
87
|
-
})
|
|
88
|
-
export class OrderModule {}
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Casos de uso recebem a porta por injeção, nunca a implementação concreta:
|
|
92
|
-
|
|
93
|
-
```typescript
|
|
94
|
-
@Injectable()
|
|
95
|
-
export class CreateOrderUseCase {
|
|
96
|
-
constructor(@Inject(ORDER_REPOSITORY_PORT) private readonly orders: OrderRepositoryPort) {}
|
|
97
|
-
}
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
## Transações cross-aggregate (Unit of Work)
|
|
101
|
-
|
|
102
|
-
Quando um caso de uso precisa alterar **mais de um agregado atrás de ports diferentes** na mesma operação atômica (ex.: criar pedido + baixar estoque), não force esse acoplamento dentro de um único repositório concreto. Use uma port dedicada de coordenação transacional:
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
// domain/ports/out/unit-of-work.port.ts
|
|
106
|
-
export interface UnitOfWorkPort {
|
|
107
|
-
run<T>(work: () => Promise<T>): Promise<T>;
|
|
108
|
-
}
|
|
109
|
-
export const UNIT_OF_WORK_PORT = Symbol('UNIT_OF_WORK_PORT');
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
- O use case injeta `UnitOfWorkPort` além das ports dos agregados envolvidos, e executa as chamadas das ports **dentro** do `run()`:
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
@Injectable()
|
|
116
|
-
export class CreateOrderUseCase {
|
|
117
|
-
constructor(
|
|
118
|
-
@Inject(UNIT_OF_WORK_PORT) private readonly uow: UnitOfWorkPort,
|
|
119
|
-
@Inject(ORDER_REPOSITORY_PORT) private readonly orders: OrderRepositoryPort,
|
|
120
|
-
@Inject(STOCK_REPOSITORY_PORT) private readonly stock: StockRepositoryPort,
|
|
121
|
-
) {}
|
|
122
|
-
|
|
123
|
-
async execute(cmd: CreateOrderCommand): Promise<Order> {
|
|
124
|
-
return this.uow.run(async () => {
|
|
125
|
-
const order = Order.create(cmd);
|
|
126
|
-
await this.stock.decrement(cmd.productId, cmd.quantity);
|
|
127
|
-
await this.orders.save(order);
|
|
128
|
-
return order;
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
- O `UnitOfWorkPort` é a **única** peça com permissão de abrir uma transação técnica compartilhada; sua implementação concreta fica em `infrastructure/adapters/out/` (ver skill `typeorm` para a implementação com `DataSource.transaction()`).
|
|
135
|
-
- Não implemente coordenação cross-aggregate escrevendo em duas entidades de tabelas diferentes dentro do repositório de um único agregado (ex.: `OrderRepository` não deve tocar `StockEntity`) — isso quebra a regra de "repositório só implementa a port do seu próprio agregado".
|
|
136
|
-
|
|
137
|
-
## Mappers
|
|
138
|
-
|
|
139
|
-
- Toda tradução entre entidade de domínio e modelo de persistência (`OrderEntity` do TypeORM) passa por um `Mapper` dedicado (`toDomain` / `toPersistence`) em `infrastructure/adapters/out/mappers/`.
|
|
140
|
-
- O domínio nunca conhece decorators de ORM (`@Entity`, `@Column`).
|
|
141
|
-
|
|
142
|
-
## Checklist ao criar/revisar um módulo
|
|
143
|
-
|
|
144
|
-
1. O domínio compila sem `@nestjs/*` e sem `typeorm` nos imports?
|
|
145
|
-
2. Toda dependência externa do domínio está atrás de uma `port`?
|
|
146
|
-
3. Regras de negócio e invariantes estão nas entidades/use-cases, não nos controllers?
|
|
147
|
-
4. Existe um teste unitário do use-case usando um fake/mock da port (sem subir banco/HTTP)?
|
|
148
|
-
5. O adapter de entrada só faz tradução de protocolo (sem `if` de regra de negócio)?
|
|
149
|
-
6. Erros de domínio são classes próprias (`OrderNotFoundError`), traduzidas para HTTP só no adapter de entrada (`ExceptionFilter`)?
|
|
150
|
-
|
|
151
|
-
## Não fazer
|
|
152
|
-
|
|
153
|
-
- Não injete `Repository<Entity>` do TypeORM diretamente em use-cases ou entidades de domínio.
|
|
154
|
-
- Não coloque decorators de validação HTTP (`class-validator`) em entidades de domínio.
|
|
155
|
-
- Não exponha entidades de persistência (`*Entity`) fora de `infrastructure/`.
|
|
156
|
-
- Não crie "god services" que misturam orquestração de use-case com acesso direto a dados.
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: jest-testing
|
|
3
|
-
description: Diretrizes de testes com Jest para projetos NestJS/TypeScript da DS9 — testes unitários de use cases/services, testes de integração de controllers, mocks de ports, cobertura e convenções de nomenclatura. Use ao escrever/revisar testes, criar mocks/fakes, configurar `jest.config`, ou quando o usuário pedir "teste unitário", "teste de integração" ou "cobertura de testes".
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Jest — Diretrizes de Teste DS9
|
|
7
|
-
|
|
8
|
-
Jest é o test runner padrão para projetos TypeScript/NestJS da DS9. Esta skill cobre convenções de teste; a organização em camadas segue a skill `hexagonal` e as convenções de framework seguem a skill `nestjs`.
|
|
9
|
-
|
|
10
|
-
## Estrutura e nomenclatura
|
|
11
|
-
|
|
12
|
-
- Arquivo de teste ao lado do arquivo testado: `order.use-case.ts` → `order.use-case.spec.ts` (unitário) ou `*.e2e-spec.ts` (ponta a ponta, pasta `test/`).
|
|
13
|
-
- `describe` nomeia a unidade testada (`describe('CreateOrderUseCase', ...)`); `it`/`test` descreve o comportamento esperado em linguagem natural (`it('deve lançar OrderNotFoundError quando o pedido não existe', ...)`), nunca o nome do método (`it('deve testar execute', ...)` é proibido).
|
|
14
|
-
- Um `describe` por método/comportamento público quando a classe tiver múltiplos cenários relevantes.
|
|
15
|
-
|
|
16
|
-
## Pirâmide de testes no projeto
|
|
17
|
-
|
|
18
|
-
| Camada | O que testar | Ferramenta | Sobe banco/HTTP real? |
|
|
19
|
-
|---|---|---|---|
|
|
20
|
-
| Domínio (entities, value objects) | Invariantes e regras de negócio puras | Jest puro | Não |
|
|
21
|
-
| Use cases / application | Orquestração, chamando ports mockadas | Jest + mocks das `ports` | Não |
|
|
22
|
-
| Adapters de saída (repositórios TypeORM) | Mapeamento correto entidade ↔ persistência | Jest + banco de teste (ou testcontainers, ver skill `docker`) | Sim (banco de teste isolado) |
|
|
23
|
-
| Controllers / adapters de entrada | Contrato HTTP: status, shape, validação | Jest + `supertest` (`*.e2e-spec.ts`) | Sim (app NestJS em memória, banco de teste) |
|
|
24
|
-
|
|
25
|
-
A maior parte dos testes deve estar nas duas primeiras camadas (rápidos, sem I/O); testes de integração/e2e cobrem os caminhos críticos, não todo caso de borda.
|
|
26
|
-
|
|
27
|
-
## Mocks de Ports (hexagonal)
|
|
28
|
-
|
|
29
|
-
- Use cases são testados mockando as `ports` (interfaces), nunca subindo banco/HTTP real. Prefira um *fake* em memória simples a um mock excessivamente configurado quando a port tem poucos métodos:
|
|
30
|
-
|
|
31
|
-
```typescript
|
|
32
|
-
class InMemoryOrderRepository implements OrderRepositoryPort {
|
|
33
|
-
private readonly rows = new Map<string, Order>();
|
|
34
|
-
async save(order: Order): Promise<void> { this.rows.set(order.id, order); }
|
|
35
|
-
async findById(id: string): Promise<Order | null> { return this.rows.get(id) ?? null; }
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('CreateOrderUseCase', () => {
|
|
39
|
-
it('deve salvar o pedido com status PENDING', async () => {
|
|
40
|
-
const repo = new InMemoryOrderRepository();
|
|
41
|
-
const useCase = new CreateOrderUseCase(repo);
|
|
42
|
-
|
|
43
|
-
const order = await useCase.execute({ customerId: 'c1', items: [...] });
|
|
44
|
-
|
|
45
|
-
expect(order.status).toBe('PENDING');
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
- Para mocks com `jest.fn()`, tipar sempre contra a interface da port (`jest.Mocked<OrderRepositoryPort>`), nunca `any`.
|
|
51
|
-
- Não mocke o que você não é dono (bibliotecas externas/SDKs) diretamente nos testes de domínio — isole-as atrás de uma port e mocke a port.
|
|
52
|
-
|
|
53
|
-
## Testes de controller (NestJS)
|
|
54
|
-
|
|
55
|
-
- Monte o módulo real via `Test.createTestingModule({ imports: [OrderModule] })`, sobrescrevendo apenas as dependências externas (`overrideProvider(ORDER_REPOSITORY_PORT)`), para validar o wiring de DI de verdade.
|
|
56
|
-
- Use `supertest` contra a instância HTTP da aplicação Nest (`app.getHttpServer()`), verificando status code, shape do body e erros de validação (400) com payload inválido.
|
|
57
|
-
|
|
58
|
-
## Assertions
|
|
59
|
-
|
|
60
|
-
- Prefira matchers específicos (`toEqual`, `toMatchObject`, `toThrow(OrderNotFoundError)`) a `toBeTruthy()`/`toBeDefined()` genéricos quando o valor exato importa.
|
|
61
|
-
- Para erros de domínio, sempre assertar a **classe/tipo** do erro (`expect(fn).rejects.toThrow(OrderNotFoundError)`), não só a mensagem em texto.
|
|
62
|
-
- Nunca deixe um teste sem nenhuma asserção (`it` vazio/`TODO`) marcado como passando; use `it.todo('...')` ou `.skip` explicitamente se o teste ainda não foi implementado.
|
|
63
|
-
|
|
64
|
-
## Isolamento e determinismo
|
|
65
|
-
|
|
66
|
-
- Cada teste deve poder rodar isoladamente e em qualquer ordem — sem estado global compartilhado entre `it`s (use `beforeEach` para recriar fakes/mocks).
|
|
67
|
-
- Nunca dependa de `Date.now()`/hora real sem controlar o clock (`jest.useFakeTimers()`); nunca dependa de ordem de `Promise`/timing sem `await` explícito.
|
|
68
|
-
- Testes de integração usam um banco de dados de teste próprio (schema isolado por execução), nunca o banco de dev/homolog compartilhado. Se o projeto usar testcontainers para subir esse banco, siga a skill `docker` para a imagem/configuração do container.
|
|
69
|
-
|
|
70
|
-
## Cobertura
|
|
71
|
-
|
|
72
|
-
- Cobertura mínima definida pelo projeto é validada no CI (`jest --coverage`); regras de negócio no domínio/use-cases devem ter cobertura próxima de 100% dos branches (if/else, early-return de erro).
|
|
73
|
-
- Cobertura alta não substitui qualidade das asserções — não escreva teste só para "bater número" sem validar comportamento real.
|
|
74
|
-
|
|
75
|
-
## Checklist ao escrever/revisar testes
|
|
76
|
-
|
|
77
|
-
1. O teste de use case mocka a port (sem banco/HTTP real)?
|
|
78
|
-
2. Nomes de `describe`/`it` descrevem comportamento, não implementação?
|
|
79
|
-
3. Erros de domínio são testados por tipo/classe, não só mensagem?
|
|
80
|
-
4. O teste é determinístico (sem dependência de tempo real/ordem)?
|
|
81
|
-
5. Testes de controller cobrem pelo menos um caso de validação inválida (400) além do caminho feliz?
|
|
82
|
-
|
|
83
|
-
## Não fazer
|
|
84
|
-
|
|
85
|
-
- Não usar `any` em mocks de ports.
|
|
86
|
-
- Não deixar `it.skip`/`xit` sem um comentário explicando o motivo e um acompanhamento (issue/TODO).
|
|
87
|
-
- Não testar detalhes de implementação privada (métodos privados) — teste pelo comportamento público.
|
|
88
|
-
- Não compartilhar estado mutável entre testes sem reset em `beforeEach`/`afterEach`.
|