sentry-test 1.0.0__py3-none-any.whl
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.
- sentry_test-1.0.0.dist-info/METADATA +265 -0
- sentry_test-1.0.0.dist-info/RECORD +29 -0
- sentry_test-1.0.0.dist-info/WHEEL +5 -0
- sentry_test-1.0.0.dist-info/entry_points.txt +2 -0
- sentry_test-1.0.0.dist-info/licenses/LICENSE +21 -0
- sentry_test-1.0.0.dist-info/top_level.txt +1 -0
- sentrytest/__init__.py +3 -0
- sentrytest/adapters/__init__.py +1 -0
- sentrytest/adapters/case_specs.py +244 -0
- sentrytest/adapters/local_tools.py +320 -0
- sentrytest/adapters/toml_config.py +9 -0
- sentrytest/application/__init__.py +0 -0
- sentrytest/application/analyze.py +190 -0
- sentrytest/application/cases.py +115 -0
- sentrytest/application/coverage_context.py +13 -0
- sentrytest/application/dimensions.py +91 -0
- sentrytest/application/error_paths.py +139 -0
- sentrytest/application/impact.py +70 -0
- sentrytest/application/reporting.py +208 -0
- sentrytest/application/traceability.py +159 -0
- sentrytest/cli.py +206 -0
- sentrytest/domain/__init__.py +1 -0
- sentrytest/domain/catalog.py +78 -0
- sentrytest/domain/models.py +100 -0
- sentrytest/domain/rules.py +62 -0
- sentrytest/init_project.py +89 -0
- sentrytest/ports/__init__.py +1 -0
- sentrytest/ports/inputs.py +32 -0
- sentrytest/skills.py +211 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sentry-test
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Test quality intelligence for change-oriented reviews
|
|
5
|
+
Author: Lucas Nicolau Ferreira
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Lucas Nicolau Ferreira
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Sentry
|
|
34
|
+
|
|
35
|
+
CLI de qualidade de teste orientada a mudança. O `sentry` cria a pasta da spec, valida a matriz de casos em markdown, roda a suíte, lê o diff e a cobertura, e emite um veredito auditável — e deixa o fluxo escrito para o agente de IA que for implementar a mudança.
|
|
36
|
+
|
|
37
|
+
Markdown é a fonte da intenção: a CLI parseia e valida `CASES.md` e `PROMPT.md` — ela nunca os escreve. O agente de IA faz a redação; o Sentry garante estrutura, rastreabilidade e veredito.
|
|
38
|
+
|
|
39
|
+
> Não confundir com o [Sentry da getsentry](https://sentry.io) (monitoramento de erros). Este projeto é distribuído como `sentry-test`.
|
|
40
|
+
|
|
41
|
+
## A divisão de responsabilidade
|
|
42
|
+
|
|
43
|
+
- **O agente de IA declara intenção** — escreve o `CASES.md`: requisito, camada, tipo, prioridade, entrada, resultado esperado.
|
|
44
|
+
- **O Sentry mede a realidade** — decide status, teste associado, evidência e veredito.
|
|
45
|
+
|
|
46
|
+
O agente nunca escreve status. O Sentry nunca chama um modelo. É isso que torna o veredito auditável e reproduzível.
|
|
47
|
+
|
|
48
|
+
## Instalação
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install sentry-test
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Requer Python 3.11+. Confira com `sentry --version` (imprime a versão do pacote).
|
|
55
|
+
|
|
56
|
+
Se faltar `pytest` ou `coverage`, o `init` avisa. Para instalar junto:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sentry init --install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Setup
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
cd seu-projeto
|
|
66
|
+
sentry init
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Cria `.sentry/` (specs, execuções, relatórios e banco), o `sentry.toml`, as entradas do `.gitignore`, e escreve o fluxo em dois lugares:
|
|
70
|
+
|
|
71
|
+
- **Skill `sentry-cases`** em `.claude/skills/sentry-cases/SKILL.md` — carregada automaticamente pelo Claude Code e por agentes que seguem essa convenção.
|
|
72
|
+
- **`AGENT-SENTRY.md`** na raiz do projeto — o mesmo fluxo em markdown puro, sem depender de convenção de nenhuma ferramenta.
|
|
73
|
+
|
|
74
|
+
Para outros agentes (Cursor, Windsurf, Codex, opencode), aponte-os para o `AGENT-SENTRY.md` no arquivo de regras que cada um já usa — por exemplo, uma linha em `.cursor/rules` ou `AGENTS.md`:
|
|
75
|
+
|
|
76
|
+
```markdown
|
|
77
|
+
Para escrever ou revisar casos de teste, siga AGENT-SENTRY.md.
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`init` é idempotente: não apaga histórico, não sobrescreve configuração existente, não duplica estruturas.
|
|
81
|
+
|
|
82
|
+
## O fluxo
|
|
83
|
+
|
|
84
|
+
1. `sentry new "cadastro de cliente"` — cria `.sentry/specs/cadastro-de-cliente/` com `PROMPT.md` (pedido preservado) e `CASES.md` em branco
|
|
85
|
+
2. **`sentry-cases`** — seu agente pergunta o que estiver ambíguo e preenche o `CASES.md` seguindo o template
|
|
86
|
+
3. `sentry check cadastro-de-cliente` — estrutura válida? classes de equivalência do catálogo cobertas?
|
|
87
|
+
4. seu agente liga cada caso ao teste real com o marcador `# cenario: <nome exato do caso>`
|
|
88
|
+
5. `sentry run --spec cadastro-de-cliente --run-tests` — roda a suíte, lê diff e cobertura, aplica as regras, persiste
|
|
89
|
+
6. `sentry report` / `sentry history` — releitura e comparação entre execuções
|
|
90
|
+
|
|
91
|
+
Todo passo também funciona sem agente, pelos comandos abaixo.
|
|
92
|
+
|
|
93
|
+
## Comandos
|
|
94
|
+
|
|
95
|
+
Código de saída `0` em sucesso; veja a tabela de códigos adiante.
|
|
96
|
+
|
|
97
|
+
| Comando | O que faz |
|
|
98
|
+
| --- | --- |
|
|
99
|
+
| `sentry init [--install]` | Prepara o repositório: `.sentry/`, `sentry.toml`, `.gitignore`, guia de agente e skills. Com `--install`, instala as dependências ausentes. |
|
|
100
|
+
| `sentry new <nome> [--prompt "..."] [--json]` | Cria a pasta da spec com slug derivado do nome. `--json` emite template, vocabulário aceito e classes cobradas, para o agente consumir. |
|
|
101
|
+
| `sentry check [<slug>\|all]` | Valida `CASES.md`: estrutura, vocabulário e cobrança de classes de equivalência. `all` valida todas as specs juntas. |
|
|
102
|
+
| `sentry run [--spec <slug>\|all] [--run-tests]` | Executa a análise e persiste. Sem `--run-tests` não há cobertura, e o veredito tende a `inconclusivo`. |
|
|
103
|
+
| `sentry report` | Exibe o último relatório (`.sentry/reports/latest.md`). |
|
|
104
|
+
| `sentry history` | Lista execuções e compara as duas últimas: cobertura, testes, achados novos, resolvidos e persistentes. |
|
|
105
|
+
| `sentry clear [--keep-last N] [--yes]` | Poda execuções e relatórios antigos. Sem `--yes` apenas mostra o que sairia — apagar histórico é irreversível. Nunca toca em `.sentry/specs/`. |
|
|
106
|
+
|
|
107
|
+
## Códigos de saída
|
|
108
|
+
|
|
109
|
+
Quatro estados distinguíveis, para separar "código mal testado" de "meu ambiente quebrou":
|
|
110
|
+
|
|
111
|
+
| Código | Significado |
|
|
112
|
+
| --- | --- |
|
|
113
|
+
| `0` | aprovado |
|
|
114
|
+
| `1` | aprovado com ressalvas |
|
|
115
|
+
| `2` | reprovado |
|
|
116
|
+
| `3` | inconclusivo ou erro de infraestrutura |
|
|
117
|
+
|
|
118
|
+
Erro de infraestrutura nunca produz veredito aprovado: uma suíte que não conseguiu rodar é diferente de uma suíte que reprovou.
|
|
119
|
+
|
|
120
|
+
`sentry check` mantém semântica própria: `0` estrutura válida, `1` erros estruturais, `2` não foi possível resolver a spec.
|
|
121
|
+
|
|
122
|
+
## Skills
|
|
123
|
+
|
|
124
|
+
Geradas para cada agente configurado; o `AGENT-SENTRY.md` cobre os demais.
|
|
125
|
+
|
|
126
|
+
| Workflow | O que o agente faz |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `sentry-cases` | Recebe o pedido em texto livre, cria a spec, **pergunta antes de escrever** toda ambiguidade que mude um caso, preenche o `CASES.md`, liga cada caso ao teste com `# cenario:` e roda `check` até fechar limpo. Nunca escreve status. |
|
|
129
|
+
|
|
130
|
+
## Regras determinísticas
|
|
131
|
+
|
|
132
|
+
Dez regras, com severidade configurável por projeto.
|
|
133
|
+
|
|
134
|
+
| Regra | Severidade padrão | Dispara quando |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `test-failing` | crítica | a suíte tem teste falhando |
|
|
137
|
+
| `case-spec-invalid` | crítica | o `CASES.md` tem erro estrutural |
|
|
138
|
+
| `changed-code-uncovered` | alta | cobertura do código alterado é zero |
|
|
139
|
+
| `scenario-without-test` | alta | caso declarado sem teste associado |
|
|
140
|
+
| `error-path-without-test` | alta | `raise`/`throw` em linha alterada que nenhum teste executou |
|
|
141
|
+
| `missing-equivalence-class` | alta | classe exigida pelo catálogo que nenhum caso cobre |
|
|
142
|
+
| `coverage-below-threshold` | alta | cobertura do código alterado abaixo do limiar declarado |
|
|
143
|
+
| `requirement-without-scenario` | média | requisito sem cenário correspondente |
|
|
144
|
+
| `coverage-missing` | média | não foi possível calcular a cobertura do código alterado |
|
|
145
|
+
| `global-coverage-below-threshold` | média | cobertura global abaixo do limiar declarado |
|
|
146
|
+
|
|
147
|
+
Sem limiar declarado, o Sentry não inventa um mínimo. Quem define "quanto basta" é o projeto, e o relatório registra o número aplicado.
|
|
148
|
+
|
|
149
|
+
## Catálogo de classes de equivalência
|
|
150
|
+
|
|
151
|
+
Tabela fixa de situações que precisam de teste, **por tipo de campo**. Não gera casos: cobra os que o agente deixou de declarar.
|
|
152
|
+
|
|
153
|
+
Tipos conhecidos: `cpf`, `cnpj`, `email`, `senha`, `data`, `telefone`, `cep`, `inteiro`, `decimal`, `texto`, `rota`.
|
|
154
|
+
|
|
155
|
+
Uma classe que não faz sentido para o campo pode ser dispensada **com justificativa**, em vez de virar caso artificial ou cobrança eterna:
|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
## Classes não aplicáveis
|
|
159
|
+
|
|
160
|
+
- **exclude/tamanho-maximo-excedido**: é parâmetro de configuração, não campo de formulário
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A dispensa remove o achado, mas fica registrada no relatório — nada some em silêncio.
|
|
164
|
+
|
|
165
|
+
## Dimensões de cobertura
|
|
166
|
+
|
|
167
|
+
Cada uma reporta `coberta`, `parcial`, `não coberta` ou `não aplicável`, com evidência.
|
|
168
|
+
|
|
169
|
+
| Dimensão | De onde tira a evidência |
|
|
170
|
+
| --- | --- |
|
|
171
|
+
| requisitos e regras de negócio | cenários da spec com teste associado |
|
|
172
|
+
| APIs, persistência, transações e integrações | casos de tipo `contrato`/`integração` e camada `integração` |
|
|
173
|
+
| exceções, resiliência e recuperação | caminhos de erro alterados executados por algum teste |
|
|
174
|
+
| segurança e autorização | campos de tipo `rota` com todas as classes de acesso cobertas |
|
|
175
|
+
|
|
176
|
+
`não aplicável` é distinto de `não coberta`: um projeto sem rotas não é punido na dimensão de segurança.
|
|
177
|
+
|
|
178
|
+
## Configuração
|
|
179
|
+
|
|
180
|
+
`sentry.toml` na raiz, versionável, sem segredos. Tudo é opcional além do que o `init` já escreve.
|
|
181
|
+
|
|
182
|
+
```toml
|
|
183
|
+
[project]
|
|
184
|
+
name = "meu-projeto"
|
|
185
|
+
|
|
186
|
+
[specs]
|
|
187
|
+
path = ".sentry/specs"
|
|
188
|
+
|
|
189
|
+
[test] # qualquer executor que exporte JUnit XML
|
|
190
|
+
command = "npx jest"
|
|
191
|
+
junit_xml = "reports/junit.xml" # sem isto, o Sentry injeta --junitxml (pytest)
|
|
192
|
+
|
|
193
|
+
[tests] # onde procurar testes
|
|
194
|
+
paths = ["tests"] # padrão: tests, test, spec, __tests__
|
|
195
|
+
|
|
196
|
+
[coverage] # relatório gerado pela suíte do próprio projeto
|
|
197
|
+
path = "coverage/lcov.info"
|
|
198
|
+
format = "lcov" # opcional: detectado pelo conteúdo quando omitido
|
|
199
|
+
|
|
200
|
+
[analysis]
|
|
201
|
+
run_tests_by_default = false
|
|
202
|
+
timeout_seconds = 300
|
|
203
|
+
exclude = ["frontend/"] # diretórios fora do escopo da análise
|
|
204
|
+
|
|
205
|
+
[policy.thresholds] # sem isto, nenhum mínimo é cobrado
|
|
206
|
+
changed_coverage = 85
|
|
207
|
+
global_coverage = 90
|
|
208
|
+
|
|
209
|
+
[policy.severities] # sobrescreve a severidade de qualquer regra
|
|
210
|
+
coverage-missing = "alta"
|
|
211
|
+
|
|
212
|
+
[catalog.fields] # tipos de campo do seu domínio
|
|
213
|
+
matricula = ["vazio", "formato-invalido", "valida"]
|
|
214
|
+
|
|
215
|
+
[dimensions] # eixos que não se aplicam ao projeto
|
|
216
|
+
disabled = []
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`.sentry/` guarda specs, execuções, relatórios e o banco. Fica fora do Git, com uma exceção deliberada: `.sentry/reports/latest.md` é versionado, para o veredito aparecer no diff da PR sem que o revisor precise rodar o Sentry.
|
|
220
|
+
|
|
221
|
+
O histórico é mantido indefinidamente, e cresce a cada execução. Para podar:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
sentry clear --keep-last 10 # mostra o que sairia
|
|
225
|
+
sentry clear --keep-last 10 --yes # remove
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
As specs nunca são removidas: são intenção declarada, não evidência gerada.
|
|
229
|
+
|
|
230
|
+
## Stacks suportadas
|
|
231
|
+
|
|
232
|
+
A **derivação** — do pedido à matriz de casos — é agnóstica de linguagem: o `CASES.md` é markdown e o catálogo raciocina sobre tipo de dado, não sobre código.
|
|
233
|
+
|
|
234
|
+
A **verificação** depende do formato de intercâmbio que sua suíte exporta, não da ferramenta:
|
|
235
|
+
|
|
236
|
+
| Capacidade | Suporte |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| Execução da suíte | qualquer comando que exporte **JUnit XML** — pytest, Jest, Vitest, `go test` (gotestsum), Surefire, `dotnet test`, RSpec, PHPUnit |
|
|
239
|
+
| Cobertura | **lcov** (nyc, c8, Jest, simplecov), **Cobertura XML** (JaCoCo, coverlet), **coverage.py** (JSON) — detectados pelo conteúdo |
|
|
240
|
+
| Rastreabilidade caso↔teste | `.py`, `.js`/`.jsx`/`.ts`/`.tsx`, `.go`, `.java`/`.kt`, `.cs`, `.rb`, `.php`, `.rs` — e o marcador `cenario:` funciona em qualquer comentário |
|
|
241
|
+
| Caminhos de erro | por AST em Python; por padrão sintático (`throw`, `catch`, `panic`, `rescue`, `panic!`) nas demais |
|
|
242
|
+
| Análise de impacto | 12 extensões de código-fonte |
|
|
243
|
+
|
|
244
|
+
A detecção de caminho de erro fora de Python é menos precisa que AST, e o relatório registra essa diferença como limitação — nunca a esconde.
|
|
245
|
+
|
|
246
|
+
Camada `frontend` é recusada de propósito: sem adaptador que a verifique, um caso declarado ficaria preso em `não coberto` para sempre.
|
|
247
|
+
|
|
248
|
+
## Local-first
|
|
249
|
+
|
|
250
|
+
Nenhuma telemetria, nenhuma chamada externa, nenhum envio de código ou diff. Todo o histórico permanece na máquina.
|
|
251
|
+
|
|
252
|
+
## Desenvolvimento
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
python -m pip install -e .
|
|
256
|
+
python -m pytest
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
O Sentry se analisa: `sentry run --spec all --run-tests` na raiz do repositório
|
|
260
|
+
casa os casos declarados em `.sentry/specs/` com as funções de teste reais e
|
|
261
|
+
reporta as quatro dimensões.
|
|
262
|
+
|
|
263
|
+
## Licença
|
|
264
|
+
|
|
265
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
sentry_test-1.0.0.dist-info/licenses/LICENSE,sha256=w2VXTcpLsShFLnGAOyIAXTJgzh1fE35Pc0tzeld0VDM,1078
|
|
2
|
+
sentrytest/__init__.py,sha256=35jw87a9NAUeOFJnDPpzOjaRE4xAjG2Qrskk9ZJGNRA,63
|
|
3
|
+
sentrytest/cli.py,sha256=J2_2QV8N00D-My9x8x-kelVUN5zwiotExBFTwMaldbE,11113
|
|
4
|
+
sentrytest/init_project.py,sha256=ty-PO5FEW9UvfiC0KdcKVtgOOlUvIx5HRAHn4cEoitQ,4745
|
|
5
|
+
sentrytest/skills.py,sha256=__0X6EGxuABByhULfRyhn37IRH2-LM1Vmib0cu6s0h4,8981
|
|
6
|
+
sentrytest/adapters/__init__.py,sha256=sHs7IzwyJBkCa8MSKMO5_lbcmxZxgYbQt0DTRFoOjGU,45
|
|
7
|
+
sentrytest/adapters/case_specs.py,sha256=RiO6Uqtbga09Liuxnwqu9SYEhjTM7TIC_cqC52GxP6k,8795
|
|
8
|
+
sentrytest/adapters/local_tools.py,sha256=IAFIQaatl8Lt2GPJr8bx0NulFztuxOQdTOUdYAsTyBU,16006
|
|
9
|
+
sentrytest/adapters/toml_config.py,sha256=hnvpaM_2ksNeckjbf_QyTYTvqrjvOi-B7dlCr6T7fbg,248
|
|
10
|
+
sentrytest/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
sentrytest/application/analyze.py,sha256=b0UhDuOI-1b85a8tIph9VjuKyMu9TpTqwJ8NszYO4w0,13931
|
|
12
|
+
sentrytest/application/cases.py,sha256=t_gn5g4DXvE_vECwdJlK0El5tDsaYRa6ORlo3hZv-OQ,4780
|
|
13
|
+
sentrytest/application/coverage_context.py,sha256=nnoANIM9Y1P1FVYXYh1_ZS2if5nyRavSu1ugravRBH8,931
|
|
14
|
+
sentrytest/application/dimensions.py,sha256=QNtrklzlneKMcFcoCgiXvhK0qUr9TPDtHNY6thRbKJo,4272
|
|
15
|
+
sentrytest/application/error_paths.py,sha256=HetpiqzTcoldOL0q9ezBJ1ZOpJFxu89KmreNSXhn4pI,5478
|
|
16
|
+
sentrytest/application/impact.py,sha256=Hxdaw2Sjxdas-J5OaaTy6GghjrFkRhgeZbniRiy0l5Q,3487
|
|
17
|
+
sentrytest/application/reporting.py,sha256=GKia_LmG62kUCxsPhRkq8049qRTlxAFTYYnD3BHHIWQ,11022
|
|
18
|
+
sentrytest/application/traceability.py,sha256=ZjYbhQSQhYRwdDCEO7HCf122PEGVQGha0vGEwfrTerY,8070
|
|
19
|
+
sentrytest/domain/__init__.py,sha256=SRfF7oDVlOOAi6nGKiJIUK6B_arqYLO9iSMp-2IZZps,21
|
|
20
|
+
sentrytest/domain/catalog.py,sha256=v9XDZOHsTa7L2r_B1gI992FGoq_OAIvg3FJZV09DSlc,3949
|
|
21
|
+
sentrytest/domain/models.py,sha256=BsEP65y8o-Maa1jhHhFaWVkLTzOxY4w_mVuyOCV1MXs,3831
|
|
22
|
+
sentrytest/domain/rules.py,sha256=7t4I6CdbxUyjB_USDS4i8D1Ps7xlbiEK4iAgotBKe3w,5757
|
|
23
|
+
sentrytest/ports/__init__.py,sha256=PN4XmYk6B1tg4awK-1AC-tKDBGwUtDPHH3XP1mYGzDA,21
|
|
24
|
+
sentrytest/ports/inputs.py,sha256=fXJTV8RypTjTNmkZ27BcBLMwP7qKtxMg0tcGWYHxTD8,1850
|
|
25
|
+
sentry_test-1.0.0.dist-info/METADATA,sha256=eyeX-ZIK44me6QKOn8dk1AhM13GNu6Zko1QKaRtJd4M,12686
|
|
26
|
+
sentry_test-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
sentry_test-1.0.0.dist-info/entry_points.txt,sha256=fgtkXdY-icVXreQV1exA2Qrqv5Qx5CBIdc0XHjAPCHY,47
|
|
28
|
+
sentry_test-1.0.0.dist-info/top_level.txt,sha256=_tdeT-T3OV7Hfd-jq_AkbutODFLUJUfboZtCML-Qs9s,11
|
|
29
|
+
sentry_test-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucas Nicolau Ferreira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALING IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sentrytest
|
sentrytest/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adaptadores das integrações externas."""
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Leitura e validação da matriz de casos declarada em CASES.md.
|
|
2
|
+
|
|
3
|
+
O agente de IA escreve o arquivo seguindo TEMPLATE; o Sentry apenas parseia e
|
|
4
|
+
valida a estrutura. Nenhuma inferência é feita sobre o conteúdo em texto livre.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import unicodedata
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
TEMPLATE = """# <Título da funcionalidade>
|
|
14
|
+
|
|
15
|
+
## Prompt
|
|
16
|
+
|
|
17
|
+
<o pedido original em texto livre, preservado literalmente>
|
|
18
|
+
|
|
19
|
+
## Campos
|
|
20
|
+
|
|
21
|
+
- **<nome do campo>**: <tipo> — <regra declarada>
|
|
22
|
+
|
|
23
|
+
## Caso: <nome curto e único>
|
|
24
|
+
|
|
25
|
+
- **Requisito:** <o que do prompt este caso verifica>
|
|
26
|
+
- **Camada:** backend | integração
|
|
27
|
+
- **Tipo:** unitário | integração | contrato
|
|
28
|
+
- **Prioridade:** crítica | alta | média | baixa
|
|
29
|
+
- **Classe:** <campo>/<classe de equivalência>
|
|
30
|
+
- **Dado:** <pré-condição>
|
|
31
|
+
- **Quando:** <ação>
|
|
32
|
+
- **Então:** <resultado esperado>
|
|
33
|
+
- **Entrada:** `campo = valor`
|
|
34
|
+
|
|
35
|
+
## Classes não aplicáveis
|
|
36
|
+
|
|
37
|
+
- **<campo>/<classe>**: <motivo pelo qual esta classe do catálogo não se aplica aqui>
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
LAYERS = ("backend", "integração")
|
|
41
|
+
TEST_TYPES = ("unitário", "integração", "contrato")
|
|
42
|
+
PRIORITIES = ("crítica", "alta", "média", "baixa")
|
|
43
|
+
|
|
44
|
+
_TITLE = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
|
|
45
|
+
_SECTION = re.compile(r"^##\s+(.+?)\s*\n([\s\S]*?)(?=\n##\s|\Z)", re.MULTILINE)
|
|
46
|
+
_FIELD = re.compile(r"^-\s+\*\*(.+?)\*\*:\s*(\S+)\s*(?:[—-]\s*(.*))?$", re.MULTILINE)
|
|
47
|
+
_NOT_APPLICABLE = re.compile(r"^-\s+\*\*(.+?)/(.+?)\*\*:\s*(.*?)\s*$", re.MULTILINE)
|
|
48
|
+
_BULLET = re.compile(r"^-\s+\*\*(.+?):\*\*\s*(.*)$", re.MULTILINE)
|
|
49
|
+
_INPUT_PAIR = re.compile(r"([A-Za-z_][\w.]*)\s*=\s*(\"[^\"]*\"|'[^']*'|[^,]+)")
|
|
50
|
+
|
|
51
|
+
_REQUIRED_BULLETS = ("requisito", "camada", "tipo", "prioridade", "dado", "quando", "entao")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class FieldSpec:
|
|
56
|
+
name: str
|
|
57
|
+
type: str
|
|
58
|
+
rule: str = ""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class CaseSpec:
|
|
63
|
+
name: str
|
|
64
|
+
requirement: str
|
|
65
|
+
layer: str
|
|
66
|
+
test_type: str
|
|
67
|
+
priority: str
|
|
68
|
+
given: str
|
|
69
|
+
when: str
|
|
70
|
+
then: str
|
|
71
|
+
equivalence_class: str | None = None
|
|
72
|
+
input_data: dict[str, str] = None
|
|
73
|
+
|
|
74
|
+
def __post_init__(self):
|
|
75
|
+
if self.input_data is None:
|
|
76
|
+
object.__setattr__(self, "input_data", {})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True)
|
|
80
|
+
class NotApplicableClass:
|
|
81
|
+
field: str
|
|
82
|
+
class_name: str
|
|
83
|
+
reason: str
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class CaseDocument:
|
|
88
|
+
title: str
|
|
89
|
+
prompt: str
|
|
90
|
+
fields: tuple[FieldSpec, ...] = ()
|
|
91
|
+
cases: tuple[CaseSpec, ...] = ()
|
|
92
|
+
not_applicable: tuple[NotApplicableClass, ...] = ()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _normalize(text: str) -> str:
|
|
96
|
+
stripped = unicodedata.normalize("NFKD", text.strip().casefold())
|
|
97
|
+
return "".join(char for char in stripped if not unicodedata.combining(char))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _match_enum(value: str, allowed: tuple[str, ...]) -> str | None:
|
|
101
|
+
normalized = _normalize(value)
|
|
102
|
+
for candidate in allowed:
|
|
103
|
+
if _normalize(candidate) == normalized:
|
|
104
|
+
return candidate
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_input_data(raw: str) -> dict[str, str]:
|
|
109
|
+
content = raw.strip().strip("`").strip()
|
|
110
|
+
if not content:
|
|
111
|
+
return {}
|
|
112
|
+
result: dict[str, str] = {}
|
|
113
|
+
for name, value in _INPUT_PAIR.findall(content):
|
|
114
|
+
cleaned = value.strip()
|
|
115
|
+
if len(cleaned) >= 2 and cleaned[0] == cleaned[-1] and cleaned[0] in "\"'":
|
|
116
|
+
cleaned = cleaned[1:-1]
|
|
117
|
+
result[name] = cleaned
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _parse_fields(body: str) -> tuple[FieldSpec, ...]:
|
|
122
|
+
fields = []
|
|
123
|
+
for name, type_name, rule in _FIELD.findall(body):
|
|
124
|
+
name = name.strip()
|
|
125
|
+
if not name or name.startswith("<"):
|
|
126
|
+
continue
|
|
127
|
+
fields.append(FieldSpec(name, type_name.strip(), (rule or "").strip()))
|
|
128
|
+
return tuple(fields)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _parse_not_applicable(body: str) -> tuple[NotApplicableClass, ...]:
|
|
132
|
+
items = []
|
|
133
|
+
for field_name, class_name, reason in _NOT_APPLICABLE.findall(body):
|
|
134
|
+
field_name, class_name, reason = field_name.strip(), class_name.strip(), reason.strip()
|
|
135
|
+
if not field_name or field_name.startswith("<"):
|
|
136
|
+
continue
|
|
137
|
+
items.append(NotApplicableClass(field_name, class_name, reason))
|
|
138
|
+
return tuple(items)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _parse_case(name: str, body: str) -> CaseSpec:
|
|
142
|
+
bullets = {_normalize(key): value.strip() for key, value in _BULLET.findall(body)}
|
|
143
|
+
return CaseSpec(
|
|
144
|
+
name=name,
|
|
145
|
+
requirement=bullets.get("requisito", ""),
|
|
146
|
+
layer=bullets.get("camada", ""),
|
|
147
|
+
test_type=bullets.get("tipo", ""),
|
|
148
|
+
priority=bullets.get("prioridade", ""),
|
|
149
|
+
given=bullets.get("dado", ""),
|
|
150
|
+
when=bullets.get("quando", ""),
|
|
151
|
+
then=bullets.get("entao", ""),
|
|
152
|
+
equivalence_class=bullets.get("classe") or None,
|
|
153
|
+
input_data=_parse_input_data(bullets.get("entrada", "")),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_cases(text: str) -> CaseDocument:
|
|
158
|
+
title_match = _TITLE.search(text)
|
|
159
|
+
title = title_match.group(1).strip() if title_match else ""
|
|
160
|
+
prompt = ""
|
|
161
|
+
fields: tuple[FieldSpec, ...] = ()
|
|
162
|
+
not_applicable: tuple[NotApplicableClass, ...] = ()
|
|
163
|
+
cases = []
|
|
164
|
+
for heading, body in _SECTION.findall(text):
|
|
165
|
+
heading = heading.strip()
|
|
166
|
+
normalized = _normalize(heading)
|
|
167
|
+
if normalized == "prompt":
|
|
168
|
+
prompt = body.strip()
|
|
169
|
+
elif normalized == "campos":
|
|
170
|
+
fields = _parse_fields(body)
|
|
171
|
+
elif normalized == "classes nao aplicaveis":
|
|
172
|
+
not_applicable = _parse_not_applicable(body)
|
|
173
|
+
elif normalized.startswith("caso:"):
|
|
174
|
+
name = heading.split(":", 1)[1].strip()
|
|
175
|
+
if name and not name.startswith("<"):
|
|
176
|
+
cases.append(_parse_case(name, body))
|
|
177
|
+
return CaseDocument(title=title, prompt=prompt, fields=fields, cases=tuple(cases), not_applicable=not_applicable)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def validate_document(document: CaseDocument) -> list[str]:
|
|
181
|
+
"""Erros estruturais do CASES.md. Lista vazia significa documento válido."""
|
|
182
|
+
errors: list[str] = []
|
|
183
|
+
if not document.title or document.title.startswith("<"):
|
|
184
|
+
errors.append("título ausente: a primeira linha deve ser '# <Título da funcionalidade>'")
|
|
185
|
+
if not document.prompt or document.prompt.startswith("<"):
|
|
186
|
+
errors.append("seção '## Prompt' ausente ou não preenchida")
|
|
187
|
+
if not document.cases:
|
|
188
|
+
errors.append("nenhum caso declarado: use '## Caso: <nome>' para cada caso de teste")
|
|
189
|
+
|
|
190
|
+
seen: set[str] = set()
|
|
191
|
+
declared_fields = {field.name for field in document.fields}
|
|
192
|
+
for case in document.cases:
|
|
193
|
+
key = _normalize(case.name)
|
|
194
|
+
if key in seen:
|
|
195
|
+
errors.append(f"caso duplicado: '{case.name}'")
|
|
196
|
+
seen.add(key)
|
|
197
|
+
|
|
198
|
+
bullets = {
|
|
199
|
+
"requisito": case.requirement, "camada": case.layer, "tipo": case.test_type,
|
|
200
|
+
"prioridade": case.priority, "dado": case.given, "quando": case.when, "entao": case.then,
|
|
201
|
+
}
|
|
202
|
+
for bullet in _REQUIRED_BULLETS:
|
|
203
|
+
if not bullets[bullet]:
|
|
204
|
+
errors.append(f"caso '{case.name}': campo obrigatório ausente '{bullet}'")
|
|
205
|
+
|
|
206
|
+
if case.layer and not _match_enum(case.layer, LAYERS):
|
|
207
|
+
errors.append(f"caso '{case.name}': camada inválida '{case.layer}' (use: {', '.join(LAYERS)})")
|
|
208
|
+
if case.test_type and not _match_enum(case.test_type, TEST_TYPES):
|
|
209
|
+
errors.append(f"caso '{case.name}': tipo inválido '{case.test_type}' (use: {', '.join(TEST_TYPES)})")
|
|
210
|
+
if case.priority and not _match_enum(case.priority, PRIORITIES):
|
|
211
|
+
errors.append(f"caso '{case.name}': prioridade inválida '{case.priority}' (use: {', '.join(PRIORITIES)})")
|
|
212
|
+
|
|
213
|
+
if case.equivalence_class and "/" in case.equivalence_class:
|
|
214
|
+
field_name = case.equivalence_class.split("/", 1)[0].strip()
|
|
215
|
+
if declared_fields and field_name not in declared_fields:
|
|
216
|
+
errors.append(
|
|
217
|
+
f"caso '{case.name}': classe referencia o campo '{field_name}', que não está declarado em '## Campos'"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
for item in document.not_applicable:
|
|
221
|
+
if not item.reason:
|
|
222
|
+
errors.append(f"classe não aplicável '{item.field}/{item.class_name}': falta o motivo")
|
|
223
|
+
if declared_fields and item.field not in declared_fields:
|
|
224
|
+
errors.append(
|
|
225
|
+
f"classe não aplicável referencia o campo '{item.field}', que não está declarado em '## Campos'"
|
|
226
|
+
)
|
|
227
|
+
return errors
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class CaseSpecAdapter:
|
|
231
|
+
"""Implementa a porta SpecReader lendo a matriz declarada em CASES.md."""
|
|
232
|
+
|
|
233
|
+
def __init__(self, path: Path):
|
|
234
|
+
self.path = path
|
|
235
|
+
|
|
236
|
+
def document(self) -> CaseDocument:
|
|
237
|
+
return parse_cases(self.path.read_text(encoding="utf-8"))
|
|
238
|
+
|
|
239
|
+
def scenarios(self):
|
|
240
|
+
from ..ports.inputs import SpecScenario
|
|
241
|
+
return tuple(
|
|
242
|
+
SpecScenario(case.name, case.given, case.when, case.then)
|
|
243
|
+
for case in self.document().cases
|
|
244
|
+
)
|