@payconductor/houndsman 0.1.0 → 0.1.1
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/README.md +39 -78
- package/assets/logo-legacy.png +0 -0
- package/assets/logo.png +0 -0
- package/package.json +3 -2
- package/src/cli/index.ts +89 -9
- package/src/core/assets.ts +8 -0
- package/src/core/brief.ts +25 -2
- package/src/core/commands/findings.ts +390 -0
- package/src/core/commands/scaffold.ts +20 -0
- package/src/core/config.ts +42 -0
- package/src/core/diff-source.ts +140 -0
- package/src/core/file-classifier.ts +50 -2
- package/src/core/finding-anchor.ts +147 -0
- package/src/core/findings-state.ts +538 -0
- package/src/core/jev/jev-client.ts +72 -11
- package/src/core/jev/jev-detector.ts +21 -19
- package/src/core/orchestrator.ts +169 -17
- package/src/core/paths.ts +3 -0
- package/src/core/registry-check.ts +15 -0
- package/src/core/report-builder.ts +113 -9
- package/src/core/types.ts +10 -0
- package/src/index.ts +78 -2
- package/src/server/artifacts.ts +66 -0
- package/src/server/client/App.tsx +158 -0
- package/src/server/client/ArtifactsPanel.tsx +48 -0
- package/src/server/client/Filters.tsx +83 -0
- package/src/server/client/FindingCard.tsx +122 -0
- package/src/server/client/FindingsList.tsx +54 -0
- package/src/server/client/RunList.tsx +43 -0
- package/src/server/client/SummaryBar.tsx +21 -0
- package/src/server/client/api.ts +37 -0
- package/src/server/client/constants.ts +63 -0
- package/src/server/client/index.tsx +12 -0
- package/src/server/client/types.ts +48 -0
- package/src/server/client-build.ts +47 -0
- package/src/server/index.ts +365 -0
- package/src/skill/README.md +14 -6
- package/src/skill/SETUP.md +5 -3
- package/src/skill/houndsman-update.md +1 -1
- package/src/skill/houndsman.md +87 -10
- package/src/tests/findings-anchor.test.ts +120 -0
- package/src/tests/findings-commands.test.ts +226 -0
- package/src/tests/findings-state.test.ts +274 -0
- package/src/tests/review-scripts.test.ts +299 -0
- package/src/tests/server.test.ts +212 -0
|
@@ -225,7 +225,7 @@ todos os itens — em especial: **geral/reutilizável** (não regra de negócio
|
|
|
225
225
|
## Verificação (obrigatória)
|
|
226
226
|
|
|
227
227
|
```bash
|
|
228
|
-
bun run houndsman doctor # config sem drift + ambiente
|
|
228
|
+
bun run houndsman doctor # config sem drift + ambiente + credencial Jev
|
|
229
229
|
bun run houndsman test # suíte da lib (project-agnóstica)
|
|
230
230
|
bun run ci:fix # formatação/imports/lint (no repo da lib)
|
|
231
231
|
bun run ci:check # tipagem (no repo da lib)
|
package/src/skill/houndsman.md
CHANGED
|
@@ -79,16 +79,31 @@ Gera em `.houndsman/.runs/<branch-slug>/`:
|
|
|
79
79
|
|
|
80
80
|
| Artefato | Conteúdo |
|
|
81
81
|
|---|---|
|
|
82
|
-
| `classification.json` | Watchdogs ativados por arquivo + status (A/M/D/R) |
|
|
82
|
+
| `classification.json` | Watchdogs ativados por arquivo + status (A/M/D/R) + churn |
|
|
83
83
|
| `candidates.json` | Indícios da pré-análise Jev (regra + arquivo + confiança) |
|
|
84
|
-
| `briefs/<watchdog>.md` | Instruções prontas para cada subagente |
|
|
85
|
-
| `manifest.json` | Resumo: watchdogs, arquivos, deletados, status, contexto, Jev |
|
|
84
|
+
| `briefs/<watchdog>.md` | Instruções prontas para cada subagente (arquivos por risco) |
|
|
85
|
+
| `manifest.json` | Resumo: watchdogs, arquivos, deletados, status, contexto, Jev, orçamento |
|
|
86
86
|
|
|
87
87
|
**Excluídos automaticamente:** lock files, `dist/`, `build/`, `.env*`, imagens, source
|
|
88
88
|
maps (padrões do projeto em `.houndsman/exclusions.ts`).
|
|
89
89
|
**Deletados:** não entram como conteúdo; vão para o brief de watchdogs com
|
|
90
90
|
`breakingChange` como candidatos a **breaking change**.
|
|
91
91
|
|
|
92
|
+
### Orçamento por brief (PRs grandes)
|
|
93
|
+
|
|
94
|
+
O motor coleta o diff **uma única vez** (`git diff` + `git diff --numstat`) e classifica
|
|
95
|
+
com **churn** (linhas adicionadas/removidas) por arquivo — o custo não cresce com o nº de
|
|
96
|
+
arquivos da PR. Cada brief recebe no máximo `budgets.maxFilesPerBrief` arquivos (default 40),
|
|
97
|
+
**priorizados pelo maior churn**:
|
|
98
|
+
|
|
99
|
+
- O excedente vai para a seção **Fora do orçamento (NÃO revisar)** do brief e o watchdog deve
|
|
100
|
+
registrá-lo em `skipped` — a tabela de **Cobertura** mostra o que ficou de fora (nunca some
|
|
101
|
+
silenciosamente).
|
|
102
|
+
- Isso mantém o contexto do subagente limitado mesmo num PR de 10k linhas com centenas de
|
|
103
|
+
arquivos num único domínio.
|
|
104
|
+
- Ajuste em `houndsman.config.ts`: `budgets: { maxFilesPerBrief: 60 }` e
|
|
105
|
+
`concurrency: { jev: 8 }`. Valores não-positivos caem no default (o `doctor` avisa).
|
|
106
|
+
|
|
92
107
|
### Pré-análise Jev (Passo 1.5, opcional)
|
|
93
108
|
|
|
94
109
|
Se `TYPESAFE_API_KEY` estiver definida, cada arquivo é avaliado pelo modelo **Jev** contra
|
|
@@ -96,14 +111,15 @@ as regras atômicas dos watchdogs. Respostas acima do limiar viram **candidatos*
|
|
|
96
111
|
`candidates.json` e entram no brief como *warm start* — o subagente **confirma ou refuta**.
|
|
97
112
|
|
|
98
113
|
- **Não é achado**: candidatos não bloqueiam.
|
|
99
|
-
- **
|
|
114
|
+
- **Opcional**: sem chave / erro / timeout → `available:false`; a revisão segue idêntica.
|
|
100
115
|
- Desativar: `houndsman --no-jev`.
|
|
101
116
|
|
|
102
117
|
---
|
|
103
118
|
|
|
104
119
|
## Passo 2 — Revisar (skill despacha os subagentes)
|
|
105
120
|
|
|
106
|
-
Um subagente por brief
|
|
121
|
+
Um subagente por brief. Despache em **ondas** (ex.: até 8 subagentes por bloco de tool
|
|
122
|
+
calls) — evita estourar limites de taxa quando há muitos watchdogs. O prompt é o próprio brief:
|
|
107
123
|
|
|
108
124
|
```
|
|
109
125
|
Leia e execute o brief: .houndsman/.runs/<branch-slug>/briefs/<watchdog-id>.md
|
|
@@ -111,7 +127,8 @@ Você é o watchdog <watchdog-id> do {{projectName}}.
|
|
|
111
127
|
```
|
|
112
128
|
|
|
113
129
|
Cada subagente grava `findings/<watchdog-id>.json` (schema `.houndsman/core/skill/OUTPUT-FORMAT.md`).
|
|
114
|
-
Sem achados → `"findings": []` (arquivo obrigatório).
|
|
130
|
+
Sem achados → `"findings": []` (arquivo obrigatório). Arquivos na seção **Fora do orçamento**
|
|
131
|
+
não são lidos: registre-os em `skipped`.
|
|
115
132
|
|
|
116
133
|
**NUNCA enviar:** `git diff` completo · histórico de commits · arquivos de outros domínios ·
|
|
117
134
|
`AGENTS.md` completo (já está no contexto global).
|
|
@@ -130,6 +147,50 @@ Falha com mensagem clara se faltar algum `findings/<watchdog>.json`. Quando todo
|
|
|
130
147
|
3. Agrupa por severidade: ❌ blocking > ⚠️ attention > 💡 suggestion > 🔎 nit
|
|
131
148
|
4. Grava `.houndsman/.runs/<branch-slug>/review.md` + tabela de cobertura
|
|
132
149
|
5. Anexa os candidatos Jev não confirmados em `🔎 Candidatos não verificados` (não bloqueiam)
|
|
150
|
+
6. Agrupa os achados **por arquivo** dentro de cada severidade (navegabilidade em PRs grandes)
|
|
151
|
+
7. Grava um `review.<watchdog>.md` por watchdog com achados e indexa no `review.md`
|
|
152
|
+
(**Relatórios por domínio**) — mantém o relatório consolidado enxuto
|
|
153
|
+
8. Grava `.houndsman/.runs/<branch-slug>/state.json` — a **triagem**: um `id` curto e um
|
|
154
|
+
`fingerprint` de conteúdo por finding (Passo 4), preservados entre execuções
|
|
155
|
+
|
|
156
|
+
> Compatível com `classification.json` antigo: se faltar o churn, o orçamento não é aplicado.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Passo 4 — Triagem (opcional)
|
|
161
|
+
|
|
162
|
+
Cada finding recebe um `id` curto e um `fingerprint` de **conteúdo** no `--finalize`,
|
|
163
|
+
gravados em `.houndsman/.runs/<branch-slug>/state.json`. A triagem é feita pela UI ou
|
|
164
|
+
pelo agente — sem editar o JSON à mão. Duas dimensões independentes:
|
|
165
|
+
|
|
166
|
+
- **decision:** `pending` · `execute` · `ignore` (+ `comment` opcional).
|
|
167
|
+
- **execState:** `pending` · `in_progress` · `done` · `stale`.
|
|
168
|
+
|
|
169
|
+
### UI local
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
houndsman server --open
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Abre a UI (Bun.serve + Preact): lista as runs, triar os findings por severidade, comentar,
|
|
176
|
+
marcar como feito e ler os artefatos (`manifest.json`, `classification.json`, `review.md`).
|
|
177
|
+
Bind em `127.0.0.1` (ferramenta local, sem auth).
|
|
178
|
+
|
|
179
|
+
### Auto-fix pelo agente
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
houndsman findings list --decision execute --json # pendentes marcados p/ corrigir
|
|
183
|
+
# ... o agente aplica as correções ...
|
|
184
|
+
houndsman findings sync # reancora por conteúdo
|
|
185
|
+
houndsman findings done <id> # confirma os concluídos
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
> **A linha não é a identidade.** O auto-fix desloca linhas; o motor reancora cada finding
|
|
189
|
+
> pelo **conteúdo** (trecho + contexto) e não perde itens que se movem. Se a âncora some
|
|
190
|
+
> (o trecho foi corrigido), o finding vira `done` automaticamente quando a decisão era
|
|
191
|
+
> `execute`, ou `stale` caso contrário.
|
|
192
|
+
> Ocorrências byte-idênticas (mesmo trecho E mesmo contexto) compartilham o id — são
|
|
193
|
+
> tratadas como um grupo (`×N`); limitação conhecida.
|
|
133
194
|
|
|
134
195
|
---
|
|
135
196
|
|
|
@@ -150,10 +211,17 @@ Falha com mensagem clara se faltar algum `findings/<watchdog>.json`. Quando todo
|
|
|
150
211
|
- ...
|
|
151
212
|
|
|
152
213
|
## ❌ blocking
|
|
153
|
-
|
|
214
|
+
### `src/routes/order-route.ts`
|
|
215
|
+
- **<regra>** — `src/routes/order-route.ts:5` · _<watchdogs>_
|
|
154
216
|
<mensagem>
|
|
155
217
|
**Ação:** <ação>
|
|
156
218
|
|
|
219
|
+
## 🔎 Candidatos não verificados (Jev) ← se houver
|
|
220
|
+
|
|
221
|
+
## Relatórios por domínio
|
|
222
|
+
| Watchdog | Achados | Revisados | Relatório |
|
|
223
|
+
|---|---|---|---|
|
|
224
|
+
|
|
157
225
|
## Cobertura
|
|
158
226
|
| Watchdog | Arquivos revisados | Findings |
|
|
159
227
|
|---|---|---|
|
|
@@ -175,7 +243,9 @@ git diff --name-status (base...head)
|
|
|
175
243
|
↓
|
|
176
244
|
[2] N subagentes em paralelo ──► findings/<watchdog>.json
|
|
177
245
|
↓
|
|
178
|
-
[3] houndsman --finalize ──► review.md
|
|
246
|
+
[3] houndsman --finalize ──► review.md + state.json
|
|
247
|
+
↓
|
|
248
|
+
[4] triagem (UI ou agente) ──► state.json (decision/execState)
|
|
179
249
|
```
|
|
180
250
|
|
|
181
251
|
---
|
|
@@ -197,9 +267,11 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
|
|
|
197
267
|
| `houndsman` | Passo 1 — preparar (briefs + manifesto) |
|
|
198
268
|
| `houndsman --finalize` | Passo 3 — gerar o relatório |
|
|
199
269
|
| `houndsman watchdog <id>` | Contexto de um watchdog (`--json`, `--rules`) |
|
|
270
|
+
| `houndsman findings <sub>` | Triagem dos findings (list/show/set/done/reset/sync/summary/path) |
|
|
271
|
+
| `houndsman server` | UI local de triagem + leitura de artefatos (`--port`, `--host`, `--open`) |
|
|
200
272
|
| `houndsman setup` | Cria a configuração inicial do projeto |
|
|
201
273
|
| `houndsman update` | Sincroniza os arquivos auto-gerados (`.houndsman/core`) |
|
|
202
|
-
| `houndsman doctor` | Valida a configuração
|
|
274
|
+
| `houndsman doctor` | Valida a configuração, o ambiente e a credencial Jev |
|
|
203
275
|
| `houndsman test` | Suíte de testes da lib |
|
|
204
276
|
|
|
205
277
|
---
|
|
@@ -208,6 +280,7 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
|
|
|
208
280
|
|
|
209
281
|
- ❌ Enviar git diff completo para qualquer watchdog
|
|
210
282
|
- ❌ Ativar watchdogs sem arquivos no seu domínio
|
|
283
|
+
- ❌ Revisar arquivos marcados como **Fora do orçamento** (registre-os em `skipped`)
|
|
211
284
|
- ❌ Carregar documentação que o watchdog não precisa
|
|
212
285
|
- ❌ Revisar arquivos gerados (lock files, builds, migrations)
|
|
213
286
|
- ❌ Escrever markdown livre em vez do JSON de `OUTPUT-FORMAT.md`
|
|
@@ -225,7 +298,7 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
|
|
|
225
298
|
> skill **`houndsman-update`** (`src/skill/houndsman-update.md`).
|
|
226
299
|
> Regra de ouro: só é regra o que o projeto **aplica hoje** — prove com evidência no código
|
|
227
300
|
> (regra negativa com 0 ocorrências = seguida → **manter**).
|
|
228
|
-
> Após qualquer alteração, rodar `houndsman doctor`
|
|
301
|
+
> Após qualquer alteração, rodar `houndsman doctor` + `houndsman test` + `ci:fix` + `ci:check`.
|
|
229
302
|
|
|
230
303
|
---
|
|
231
304
|
|
|
@@ -237,3 +310,7 @@ Se a branch não for informada, detecta com `git branch --show-current`. Base do
|
|
|
237
310
|
| Médio (5k linhas) | 4-7 | ~1-2 min |
|
|
238
311
|
| Grande (50k linhas) | 7-10 | ~3-5 min |
|
|
239
312
|
| Enorme (300k linhas) | 10-15 | ~5-8 min |
|
|
313
|
+
|
|
314
|
+
> O custo de coleta é **constante** (2 execuções de `git`), independente do nº de arquivos.
|
|
315
|
+
> Em PRs muito grandes, o limitante é o nº de subagentes (ondas) e o orçamento por brief —
|
|
316
|
+
> não o diff. Ajuste `budgets.maxFilesPerBrief` para revisar mais/menos arquivos por domínio.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildAnchor,
|
|
4
|
+
fingerprintOf,
|
|
5
|
+
locateAnchor,
|
|
6
|
+
normalizeLine,
|
|
7
|
+
reanchor,
|
|
8
|
+
} from "../core/finding-anchor";
|
|
9
|
+
|
|
10
|
+
const DUPLICATED = [
|
|
11
|
+
"function a() {",
|
|
12
|
+
'\tconsole.log("x");',
|
|
13
|
+
"}",
|
|
14
|
+
"function b() {",
|
|
15
|
+
'\tconsole.log("x");',
|
|
16
|
+
"}",
|
|
17
|
+
].join("\n");
|
|
18
|
+
|
|
19
|
+
// Após um "auto-fix" que remove o PRIMEIRO console.log.
|
|
20
|
+
const AFTER_FIX = [
|
|
21
|
+
"function a() {",
|
|
22
|
+
"}",
|
|
23
|
+
"function b() {",
|
|
24
|
+
'\tconsole.log("x");',
|
|
25
|
+
"}",
|
|
26
|
+
].join("\n");
|
|
27
|
+
|
|
28
|
+
describe("finding-anchor", () => {
|
|
29
|
+
test("normalizeLine colapsa espaços e apara as pontas", () => {
|
|
30
|
+
expect(normalizeLine(' \t console.log( "x" ); ')).toBe('console.log( "x" );');
|
|
31
|
+
expect(normalizeLine(" ")).toBe("");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("buildAnchor usa a linha quando válida", () => {
|
|
35
|
+
const anchor = buildAnchor(DUPLICATED, 2);
|
|
36
|
+
expect(anchor).not.toBeNull();
|
|
37
|
+
expect(anchor?.snippet).toBe('console.log("x");');
|
|
38
|
+
expect(anchor?.contextBefore).toBe("function a() {");
|
|
39
|
+
expect(anchor?.contextAfter).toBe("}");
|
|
40
|
+
expect(anchor?.line).toBe(2);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("buildAnchor cai na evidence quando a linha é inválida", () => {
|
|
44
|
+
const anchor = buildAnchor(DUPLICATED, 999, 'console.log("x");');
|
|
45
|
+
expect(anchor?.line).toBe(2);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("buildAnchor devolve null sem linha válida nem evidence localizável", () => {
|
|
49
|
+
expect(buildAnchor(DUPLICATED, 999, "inexistente")).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("locateAnchor encontra a linha e conta as ocorrências", () => {
|
|
53
|
+
const anchor = buildAnchor(DUPLICATED, 2);
|
|
54
|
+
const located = locateAnchor(DUPLICATED, anchor);
|
|
55
|
+
expect(located.line).toBe(2);
|
|
56
|
+
expect(located.occurrences).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("fingerprint é estável e distingue contexto", () => {
|
|
60
|
+
const a1 = buildAnchor(DUPLICATED, 2);
|
|
61
|
+
const a2 = buildAnchor(DUPLICATED, 5);
|
|
62
|
+
|
|
63
|
+
// Mesmo arquivo + regra + âncora → mesmo fingerprint.
|
|
64
|
+
expect(fingerprintOf("f.ts", "rule", a1)).toBe(
|
|
65
|
+
fingerprintOf("f.ts", "rule", buildAnchor(DUPLICATED, 2)),
|
|
66
|
+
);
|
|
67
|
+
// Snippet idêntico mas contexto diferente → fingerprints diferentes.
|
|
68
|
+
expect(fingerprintOf("f.ts", "rule", a1)).not.toBe(
|
|
69
|
+
fingerprintOf("f.ts", "rule", a2),
|
|
70
|
+
);
|
|
71
|
+
// Regra diferente muda o fingerprint.
|
|
72
|
+
expect(fingerprintOf("f.ts", "outra", a1)).not.toBe(
|
|
73
|
+
fingerprintOf("f.ts", "rule", a1),
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("fingerprint sem âncora usa o fallback", () => {
|
|
78
|
+
expect(fingerprintOf("f.ts", "rule", null, "mensagem")).toBe(
|
|
79
|
+
fingerprintOf("f.ts", "rule", null, "mensagem"),
|
|
80
|
+
);
|
|
81
|
+
expect(fingerprintOf("f.ts", "rule", null, "a")).not.toBe(
|
|
82
|
+
fingerprintOf("f.ts", "rule", null, "b"),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("reanchor: ok, moved e stale", () => {
|
|
87
|
+
const anchor = buildAnchor(DUPLICATED, 5);
|
|
88
|
+
expect(reanchor(DUPLICATED, anchor, 5).status).toBe("ok");
|
|
89
|
+
|
|
90
|
+
const moved = reanchor(AFTER_FIX, anchor, 5);
|
|
91
|
+
expect(moved.status).toBe("moved");
|
|
92
|
+
expect(moved.line).toBe(4);
|
|
93
|
+
|
|
94
|
+
const removed = buildAnchor(DUPLICATED, 2);
|
|
95
|
+
expect(reanchor(AFTER_FIX, removed, 2).status).toBe("stale");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("duas ocorrências iguais não se perdem quando o fix remove a primeira", () => {
|
|
99
|
+
const first = buildAnchor(DUPLICATED, 2);
|
|
100
|
+
const second = buildAnchor(DUPLICATED, 5);
|
|
101
|
+
|
|
102
|
+
// A âncora removida vira stale; a sobrevivente apenas desloca de linha.
|
|
103
|
+
expect(reanchor(AFTER_FIX, first, 2).status).toBe("stale");
|
|
104
|
+
const survivor = reanchor(AFTER_FIX, second, 5);
|
|
105
|
+
expect(survivor.status).toBe("moved");
|
|
106
|
+
expect(survivor.line).toBe(4);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("ocorrências byte-idênticas compartilham o fingerprint (grupo)", () => {
|
|
110
|
+
// Mesmo snippet E mesmo contexto (sem vizinhos) → mesmo fingerprint.
|
|
111
|
+
const content = "x\ny\nx\ny";
|
|
112
|
+
const a1 = buildAnchor(content, 1);
|
|
113
|
+
const a2 = buildAnchor(content, 3);
|
|
114
|
+
// contextBefore ausente em ambos (linha 1 vs 3 tem 'y' antes → difere), então
|
|
115
|
+
// forçamos âncoras sem contexto para simular colisão real.
|
|
116
|
+
const bare = a1 && { ...a1, contextBefore: undefined, contextAfter: undefined };
|
|
117
|
+
const bare2 = a2 && { ...a2, contextBefore: undefined, contextAfter: undefined };
|
|
118
|
+
expect(fingerprintOf("f.ts", "r", bare)).toBe(fingerprintOf("f.ts", "r", bare2));
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { runFindingsCommand } from "../core/commands/findings";
|
|
6
|
+
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
7
|
+
|
|
8
|
+
/** Run fake em disco: classification.json + findings/alpha.json. */
|
|
9
|
+
function makeRun(findings: unknown[]): { dir: string; cleanup: () => void } {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "houndsman-findings-"));
|
|
11
|
+
const outDir = join(dir, "main");
|
|
12
|
+
mkdirSync(join(outDir, "findings"), { recursive: true });
|
|
13
|
+
writeFileSync(
|
|
14
|
+
join(outDir, "classification.json"),
|
|
15
|
+
JSON.stringify({ watchdogs: { alpha: ["src/alpha/f.ts"] } }),
|
|
16
|
+
);
|
|
17
|
+
writeFileSync(
|
|
18
|
+
join(outDir, "findings", "alpha.json"),
|
|
19
|
+
JSON.stringify({
|
|
20
|
+
watchdogId: "alpha",
|
|
21
|
+
reviewedFiles: ["src/alpha/f.ts"],
|
|
22
|
+
skipped: [],
|
|
23
|
+
findings,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ONE_FINDING = [
|
|
30
|
+
{
|
|
31
|
+
rule: "Nunca usar any",
|
|
32
|
+
severity: "blocking",
|
|
33
|
+
file: "src/alpha/f.ts",
|
|
34
|
+
line: 2,
|
|
35
|
+
message: "any detectado",
|
|
36
|
+
action: "tipar",
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function run(args: string[]) {
|
|
41
|
+
return runFindingsCommand(fakeHoundsmanConfig, args);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("findings command", () => {
|
|
45
|
+
test("list reconstrói o estado a partir dos artefatos", () => {
|
|
46
|
+
const run0 = makeRun(ONE_FINDING);
|
|
47
|
+
try {
|
|
48
|
+
const result = run(["list", "--dir", run0.dir, "--branch", "main", "--json"]);
|
|
49
|
+
expect(result.exitCode).toBe(0);
|
|
50
|
+
const parsed = JSON.parse(result.output);
|
|
51
|
+
expect(parsed).toHaveLength(1);
|
|
52
|
+
expect(parsed[0].decision).toBe("pending");
|
|
53
|
+
expect(parsed[0].id).toBeTruthy();
|
|
54
|
+
expect(parsed[0].severity).toBe("blocking");
|
|
55
|
+
} finally {
|
|
56
|
+
run0.cleanup();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("set atualiza decisão + comentário e persiste", () => {
|
|
61
|
+
const run0 = makeRun(ONE_FINDING);
|
|
62
|
+
try {
|
|
63
|
+
const list = JSON.parse(
|
|
64
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
65
|
+
);
|
|
66
|
+
const id = list[0].id;
|
|
67
|
+
|
|
68
|
+
const set = run([
|
|
69
|
+
"set",
|
|
70
|
+
id,
|
|
71
|
+
"--decision",
|
|
72
|
+
"execute",
|
|
73
|
+
"--comment",
|
|
74
|
+
"trocar por unknown",
|
|
75
|
+
"--dir",
|
|
76
|
+
run0.dir,
|
|
77
|
+
"--branch",
|
|
78
|
+
"main",
|
|
79
|
+
]);
|
|
80
|
+
expect(set.exitCode).toBe(0);
|
|
81
|
+
|
|
82
|
+
const after = JSON.parse(
|
|
83
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
84
|
+
);
|
|
85
|
+
expect(after[0].decision).toBe("execute");
|
|
86
|
+
expect(after[0].comment).toBe("trocar por unknown");
|
|
87
|
+
expect(after[0].history.length).toBeGreaterThan(0);
|
|
88
|
+
} finally {
|
|
89
|
+
run0.cleanup();
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("done marca execState como done", () => {
|
|
94
|
+
const run0 = makeRun(ONE_FINDING);
|
|
95
|
+
try {
|
|
96
|
+
const id = JSON.parse(
|
|
97
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
98
|
+
)[0].id;
|
|
99
|
+
expect(
|
|
100
|
+
run(["done", id, "--dir", run0.dir, "--branch", "main"]).exitCode,
|
|
101
|
+
).toBe(0);
|
|
102
|
+
const after = JSON.parse(
|
|
103
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
104
|
+
);
|
|
105
|
+
expect(after[0].execState).toBe("done");
|
|
106
|
+
} finally {
|
|
107
|
+
run0.cleanup();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("filtra por decisão e resume contagens", () => {
|
|
112
|
+
const run0 = makeRun(ONE_FINDING);
|
|
113
|
+
try {
|
|
114
|
+
const id = JSON.parse(
|
|
115
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
116
|
+
)[0].id;
|
|
117
|
+
run([
|
|
118
|
+
"set",
|
|
119
|
+
id,
|
|
120
|
+
"--decision",
|
|
121
|
+
"ignore",
|
|
122
|
+
"--dir",
|
|
123
|
+
run0.dir,
|
|
124
|
+
"--branch",
|
|
125
|
+
"main",
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const pending = JSON.parse(
|
|
129
|
+
run([
|
|
130
|
+
"list",
|
|
131
|
+
"--decision",
|
|
132
|
+
"pending",
|
|
133
|
+
"--dir",
|
|
134
|
+
run0.dir,
|
|
135
|
+
"--branch",
|
|
136
|
+
"main",
|
|
137
|
+
"--json",
|
|
138
|
+
]).output,
|
|
139
|
+
);
|
|
140
|
+
expect(pending).toHaveLength(0);
|
|
141
|
+
|
|
142
|
+
const summary = JSON.parse(
|
|
143
|
+
run(["summary", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
144
|
+
);
|
|
145
|
+
expect(summary.total).toBe(1);
|
|
146
|
+
expect(summary.byDecision.ignore).toBe(1);
|
|
147
|
+
} finally {
|
|
148
|
+
run0.cleanup();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("sync marca como stale quando o arquivo não está disponível", () => {
|
|
153
|
+
const run0 = makeRun(ONE_FINDING);
|
|
154
|
+
try {
|
|
155
|
+
const result = JSON.parse(
|
|
156
|
+
run(["sync", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
157
|
+
);
|
|
158
|
+
expect(result.stale).toHaveLength(1);
|
|
159
|
+
} finally {
|
|
160
|
+
run0.cleanup();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("erros claros: decisão inválida, id inexistente, run ausente", () => {
|
|
165
|
+
const run0 = makeRun(ONE_FINDING);
|
|
166
|
+
try {
|
|
167
|
+
const bad = run([
|
|
168
|
+
"set",
|
|
169
|
+
"inexistente",
|
|
170
|
+
"--decision",
|
|
171
|
+
"xpto",
|
|
172
|
+
"--dir",
|
|
173
|
+
run0.dir,
|
|
174
|
+
"--branch",
|
|
175
|
+
"main",
|
|
176
|
+
]);
|
|
177
|
+
expect(bad.exitCode).toBe(1);
|
|
178
|
+
expect(bad.output).toContain("--decision inválido");
|
|
179
|
+
|
|
180
|
+
const missing = run([
|
|
181
|
+
"show",
|
|
182
|
+
"nao-existe",
|
|
183
|
+
"--dir",
|
|
184
|
+
run0.dir,
|
|
185
|
+
"--branch",
|
|
186
|
+
"main",
|
|
187
|
+
]);
|
|
188
|
+
expect(missing.exitCode).toBe(1);
|
|
189
|
+
expect(missing.output).toContain("não encontrado");
|
|
190
|
+
|
|
191
|
+
const empty = mkdtempSync(join(tmpdir(), "houndsman-empty-"));
|
|
192
|
+
try {
|
|
193
|
+
const absent = run(["list", "--dir", empty, "--branch", "main"]);
|
|
194
|
+
expect(absent.exitCode).toBe(1);
|
|
195
|
+
expect(absent.output).toContain("--finalize");
|
|
196
|
+
} finally {
|
|
197
|
+
rmSync(empty, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
run0.cleanup();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("path devolve o caminho do state.json", () => {
|
|
205
|
+
const run0 = makeRun(ONE_FINDING);
|
|
206
|
+
try {
|
|
207
|
+
run(["list", "--dir", run0.dir, "--branch", "main"]);
|
|
208
|
+
const result = run(["path", "--dir", run0.dir, "--branch", "main"]);
|
|
209
|
+
expect(result.exitCode).toBe(0);
|
|
210
|
+
expect(result.output.replace(/\\/g, "/")).toContain("main/state.json");
|
|
211
|
+
} finally {
|
|
212
|
+
run0.cleanup();
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("sub desconhecido devolve erro", () => {
|
|
217
|
+
const run0 = makeRun(ONE_FINDING);
|
|
218
|
+
try {
|
|
219
|
+
const result = run(["xpto", "--dir", run0.dir, "--branch", "main"]);
|
|
220
|
+
expect(result.exitCode).toBe(1);
|
|
221
|
+
expect(result.output).toContain("desconhecido");
|
|
222
|
+
} finally {
|
|
223
|
+
run0.cleanup();
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|