@payconductor/houndsman 0.1.0 → 0.1.3
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 +68 -89
- package/assets/logo-legacy.png +0 -0
- package/assets/logo.png +0 -0
- package/package.json +3 -2
- package/src/cli/index.ts +101 -10
- package/src/core/assets.ts +11 -0
- package/src/core/brief.ts +27 -5
- package/src/core/commands/findings.ts +618 -0
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +33 -44
- 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 +556 -0
- package/src/core/fix-batch.ts +236 -0
- package/src/core/git-revision.ts +105 -0
- package/src/core/jev/index.ts +1 -0
- package/src/core/jev/jev-client.ts +72 -11
- package/src/core/jev/jev-detector.ts +21 -19
- package/src/core/jev/jev-verify.ts +178 -0
- package/src/core/orchestrator.ts +362 -23
- package/src/core/paths.ts +9 -0
- package/src/core/registry-check.ts +15 -0
- package/src/core/report-builder.ts +118 -9
- package/src/core/run-status.ts +123 -0
- package/src/core/types.ts +10 -0
- package/src/core/verify.ts +175 -0
- package/src/core/watchdog-registry.ts +1 -2
- package/src/index.ts +78 -2
- package/src/server/artifacts.ts +67 -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 +52 -0
- package/src/server/client/SummaryBar.tsx +21 -0
- package/src/server/client/api.ts +37 -0
- package/src/server/client/constants.ts +94 -0
- package/src/server/client/index.tsx +12 -0
- package/src/server/client/types.ts +52 -0
- package/src/server/client-build.ts +47 -0
- package/src/server/index.ts +376 -0
- package/src/skill/AUDIT-PROTOCOL.md +2 -2
- package/src/skill/README.md +43 -11
- package/src/skill/SETUP.md +16 -4
- package/src/skill/SKILL.md +48 -0
- package/src/skill/fix.md +206 -0
- package/src/skill/{houndsman.md → hunt.md} +83 -33
- package/src/skill/{houndsman-update.md → train.md} +12 -12
- package/src/skill/verify.md +70 -0
- package/src/tests/findings-anchor.test.ts +120 -0
- package/src/tests/findings-commands.test.ts +476 -0
- package/src/tests/findings-state.test.ts +325 -0
- package/src/tests/fix-batch.test.ts +257 -0
- package/src/tests/review-scripts.test.ts +299 -0
- package/src/tests/run-status.test.ts +147 -0
- package/src/tests/scaffold.test.ts +59 -0
- package/src/tests/server.test.ts +242 -0
- package/src/tests/verify.test.ts +170 -0
|
@@ -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,476 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
|
|
13
|
+
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
14
|
+
|
|
15
|
+
/** Run fake em disco: classification.json + findings/alpha.json. */
|
|
16
|
+
function makeRun(findings: unknown[]): { dir: string; cleanup: () => void } {
|
|
17
|
+
const dir = mkdtempSync(join(tmpdir(), "houndsman-findings-"));
|
|
18
|
+
const outDir = join(dir, "main");
|
|
19
|
+
mkdirSync(join(outDir, "findings"), { recursive: true });
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(outDir, "classification.json"),
|
|
22
|
+
JSON.stringify({ watchdogs: { alpha: ["src/alpha/f.ts"] } }),
|
|
23
|
+
);
|
|
24
|
+
writeFileSync(
|
|
25
|
+
join(outDir, "findings", "alpha.json"),
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
watchdogId: "alpha",
|
|
28
|
+
reviewedFiles: ["src/alpha/f.ts"],
|
|
29
|
+
skipped: [],
|
|
30
|
+
findings,
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const ONE_FINDING = [
|
|
37
|
+
{
|
|
38
|
+
rule: "Nunca usar any",
|
|
39
|
+
severity: "blocking",
|
|
40
|
+
file: "src/alpha/f.ts",
|
|
41
|
+
line: 2,
|
|
42
|
+
message: "any detectado",
|
|
43
|
+
action: "tipar",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function run(args: string[]) {
|
|
48
|
+
return runFindingsCommand(fakeHoundsmanConfig, args);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe("findings command", () => {
|
|
52
|
+
test("list reconstrói o estado a partir dos artefatos", () => {
|
|
53
|
+
const run0 = makeRun(ONE_FINDING);
|
|
54
|
+
try {
|
|
55
|
+
const result = run(["list", "--dir", run0.dir, "--branch", "main", "--json"]);
|
|
56
|
+
expect(result.exitCode).toBe(0);
|
|
57
|
+
const parsed = JSON.parse(result.output);
|
|
58
|
+
expect(parsed).toHaveLength(1);
|
|
59
|
+
expect(parsed[0].decision).toBe("pending");
|
|
60
|
+
expect(parsed[0].id).toBeTruthy();
|
|
61
|
+
expect(parsed[0].severity).toBe("blocking");
|
|
62
|
+
} finally {
|
|
63
|
+
run0.cleanup();
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("set atualiza decisão + comentário e persiste", () => {
|
|
68
|
+
const run0 = makeRun(ONE_FINDING);
|
|
69
|
+
try {
|
|
70
|
+
const list = JSON.parse(
|
|
71
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
72
|
+
);
|
|
73
|
+
const id = list[0].id;
|
|
74
|
+
|
|
75
|
+
const set = run([
|
|
76
|
+
"set",
|
|
77
|
+
id,
|
|
78
|
+
"--decision",
|
|
79
|
+
"execute",
|
|
80
|
+
"--comment",
|
|
81
|
+
"trocar por unknown",
|
|
82
|
+
"--dir",
|
|
83
|
+
run0.dir,
|
|
84
|
+
"--branch",
|
|
85
|
+
"main",
|
|
86
|
+
]);
|
|
87
|
+
expect(set.exitCode).toBe(0);
|
|
88
|
+
|
|
89
|
+
const after = JSON.parse(
|
|
90
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
91
|
+
);
|
|
92
|
+
expect(after[0].decision).toBe("execute");
|
|
93
|
+
expect(after[0].comment).toBe("trocar por unknown");
|
|
94
|
+
expect(after[0].history.length).toBeGreaterThan(0);
|
|
95
|
+
} finally {
|
|
96
|
+
run0.cleanup();
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("done marca execState como done", () => {
|
|
101
|
+
const run0 = makeRun(ONE_FINDING);
|
|
102
|
+
try {
|
|
103
|
+
const id = JSON.parse(
|
|
104
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
105
|
+
)[0].id;
|
|
106
|
+
expect(
|
|
107
|
+
run(["done", id, "--dir", run0.dir, "--branch", "main"]).exitCode,
|
|
108
|
+
).toBe(0);
|
|
109
|
+
const after = JSON.parse(
|
|
110
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
111
|
+
);
|
|
112
|
+
expect(after[0].execState).toBe("done");
|
|
113
|
+
} finally {
|
|
114
|
+
run0.cleanup();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("filtra por decisão e resume contagens", () => {
|
|
119
|
+
const run0 = makeRun(ONE_FINDING);
|
|
120
|
+
try {
|
|
121
|
+
const id = JSON.parse(
|
|
122
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
123
|
+
)[0].id;
|
|
124
|
+
run([
|
|
125
|
+
"set",
|
|
126
|
+
id,
|
|
127
|
+
"--decision",
|
|
128
|
+
"ignore",
|
|
129
|
+
"--dir",
|
|
130
|
+
run0.dir,
|
|
131
|
+
"--branch",
|
|
132
|
+
"main",
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
const pending = JSON.parse(
|
|
136
|
+
run([
|
|
137
|
+
"list",
|
|
138
|
+
"--decision",
|
|
139
|
+
"pending",
|
|
140
|
+
"--dir",
|
|
141
|
+
run0.dir,
|
|
142
|
+
"--branch",
|
|
143
|
+
"main",
|
|
144
|
+
"--json",
|
|
145
|
+
]).output,
|
|
146
|
+
);
|
|
147
|
+
expect(pending).toHaveLength(0);
|
|
148
|
+
|
|
149
|
+
const summary = JSON.parse(
|
|
150
|
+
run(["summary", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
151
|
+
);
|
|
152
|
+
expect(summary.total).toBe(1);
|
|
153
|
+
expect(summary.byDecision.ignore).toBe(1);
|
|
154
|
+
} finally {
|
|
155
|
+
run0.cleanup();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("sync marca como stale quando o arquivo não está disponível", () => {
|
|
160
|
+
const run0 = makeRun(ONE_FINDING);
|
|
161
|
+
try {
|
|
162
|
+
const result = JSON.parse(
|
|
163
|
+
run(["sync", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
164
|
+
);
|
|
165
|
+
expect(result.stale).toHaveLength(1);
|
|
166
|
+
} finally {
|
|
167
|
+
run0.cleanup();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("erros claros: decisão inválida, id inexistente, run ausente", () => {
|
|
172
|
+
const run0 = makeRun(ONE_FINDING);
|
|
173
|
+
try {
|
|
174
|
+
const bad = run([
|
|
175
|
+
"set",
|
|
176
|
+
"inexistente",
|
|
177
|
+
"--decision",
|
|
178
|
+
"xpto",
|
|
179
|
+
"--dir",
|
|
180
|
+
run0.dir,
|
|
181
|
+
"--branch",
|
|
182
|
+
"main",
|
|
183
|
+
]);
|
|
184
|
+
expect(bad.exitCode).toBe(1);
|
|
185
|
+
expect(bad.output).toContain("--decision inválido");
|
|
186
|
+
|
|
187
|
+
const missing = run([
|
|
188
|
+
"show",
|
|
189
|
+
"nao-existe",
|
|
190
|
+
"--dir",
|
|
191
|
+
run0.dir,
|
|
192
|
+
"--branch",
|
|
193
|
+
"main",
|
|
194
|
+
]);
|
|
195
|
+
expect(missing.exitCode).toBe(1);
|
|
196
|
+
expect(missing.output).toContain("não encontrado");
|
|
197
|
+
|
|
198
|
+
const empty = mkdtempSync(join(tmpdir(), "houndsman-empty-"));
|
|
199
|
+
try {
|
|
200
|
+
const absent = run(["list", "--dir", empty, "--branch", "main"]);
|
|
201
|
+
expect(absent.exitCode).toBe(1);
|
|
202
|
+
expect(absent.output).toContain("--finalize");
|
|
203
|
+
} finally {
|
|
204
|
+
rmSync(empty, { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
} finally {
|
|
207
|
+
run0.cleanup();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("path devolve o caminho do state.json", () => {
|
|
212
|
+
const run0 = makeRun(ONE_FINDING);
|
|
213
|
+
try {
|
|
214
|
+
run(["list", "--dir", run0.dir, "--branch", "main"]);
|
|
215
|
+
const result = run(["path", "--dir", run0.dir, "--branch", "main"]);
|
|
216
|
+
expect(result.exitCode).toBe(0);
|
|
217
|
+
expect(result.output.replace(/\\/g, "/")).toContain("main/state.json");
|
|
218
|
+
} finally {
|
|
219
|
+
run0.cleanup();
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("sub desconhecido devolve erro", () => {
|
|
224
|
+
const run0 = makeRun(ONE_FINDING);
|
|
225
|
+
try {
|
|
226
|
+
const result = run(["xpto", "--dir", run0.dir, "--branch", "main"]);
|
|
227
|
+
expect(result.exitCode).toBe(1);
|
|
228
|
+
expect(result.output).toContain("desconhecido");
|
|
229
|
+
} finally {
|
|
230
|
+
run0.cleanup();
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("batch agrupa a fila e devolve o grupo atual + outros grupos", () => {
|
|
235
|
+
const dir = mkdtempSync(join(tmpdir(), "houndsman-batch-"));
|
|
236
|
+
const outDir = join(dir, "main");
|
|
237
|
+
mkdirSync(join(outDir, "findings"), { recursive: true });
|
|
238
|
+
writeFileSync(
|
|
239
|
+
join(outDir, "classification.json"),
|
|
240
|
+
JSON.stringify({ watchdogs: { alpha: ["src/a.ts"], beta: ["src/b.ts"] } }),
|
|
241
|
+
);
|
|
242
|
+
writeFileSync(
|
|
243
|
+
join(outDir, "findings", "alpha.json"),
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
watchdogId: "alpha",
|
|
246
|
+
reviewedFiles: ["src/a.ts"],
|
|
247
|
+
skipped: [],
|
|
248
|
+
findings: [
|
|
249
|
+
{
|
|
250
|
+
rule: "R1",
|
|
251
|
+
severity: "blocking",
|
|
252
|
+
file: "src/a.ts",
|
|
253
|
+
line: 1,
|
|
254
|
+
message: "m1",
|
|
255
|
+
action: "a",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
rule: "R2",
|
|
259
|
+
severity: "attention",
|
|
260
|
+
file: "src/a.ts",
|
|
261
|
+
line: 2,
|
|
262
|
+
message: "m2",
|
|
263
|
+
action: "a",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
rule: "R3",
|
|
267
|
+
severity: "attention",
|
|
268
|
+
file: "src/a.ts",
|
|
269
|
+
line: 3,
|
|
270
|
+
message: "m3",
|
|
271
|
+
action: "a",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
rule: "R4",
|
|
275
|
+
severity: "nit",
|
|
276
|
+
file: "src/a.ts",
|
|
277
|
+
line: 4,
|
|
278
|
+
message: "m4",
|
|
279
|
+
action: "a",
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
writeFileSync(
|
|
285
|
+
join(outDir, "findings", "beta.json"),
|
|
286
|
+
JSON.stringify({
|
|
287
|
+
watchdogId: "beta",
|
|
288
|
+
reviewedFiles: ["src/b.ts"],
|
|
289
|
+
skipped: [],
|
|
290
|
+
findings: [
|
|
291
|
+
{
|
|
292
|
+
rule: "R5",
|
|
293
|
+
severity: "attention",
|
|
294
|
+
file: "src/b.ts",
|
|
295
|
+
line: 1,
|
|
296
|
+
message: "m5",
|
|
297
|
+
action: "a",
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
rule: "R6",
|
|
301
|
+
severity: "suggestion",
|
|
302
|
+
file: "src/b.ts",
|
|
303
|
+
line: 2,
|
|
304
|
+
message: "m6",
|
|
305
|
+
action: "a",
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
}),
|
|
309
|
+
);
|
|
310
|
+
try {
|
|
311
|
+
const list = JSON.parse(
|
|
312
|
+
run(["list", "--dir", dir, "--branch", "main", "--json"]).output,
|
|
313
|
+
);
|
|
314
|
+
for (const item of list) {
|
|
315
|
+
run([
|
|
316
|
+
"set",
|
|
317
|
+
item.id,
|
|
318
|
+
"--decision",
|
|
319
|
+
"execute",
|
|
320
|
+
"--dir",
|
|
321
|
+
dir,
|
|
322
|
+
"--branch",
|
|
323
|
+
"main",
|
|
324
|
+
]);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const result = run(["batch", "--dir", dir, "--branch", "main", "--json"]);
|
|
328
|
+
expect(result.exitCode).toBe(0);
|
|
329
|
+
const parsed = JSON.parse(result.output);
|
|
330
|
+
expect(parsed.dimension).toBe("file");
|
|
331
|
+
expect(parsed.totals.queue).toBe(6);
|
|
332
|
+
expect(parsed.totals.groups).toBe(2);
|
|
333
|
+
expect(parsed.current.key).toBe("file:src/a.ts");
|
|
334
|
+
expect(parsed.current.findings).toHaveLength(4);
|
|
335
|
+
expect(parsed.others).toEqual([
|
|
336
|
+
expect.objectContaining({ key: "file:src/b.ts", count: 2 }),
|
|
337
|
+
]);
|
|
338
|
+
|
|
339
|
+
const markdown = run(["batch", "--dir", dir, "--branch", "main"]);
|
|
340
|
+
expect(markdown.output).toContain("Grupo atual");
|
|
341
|
+
expect(markdown.output).toContain("Outros grupos");
|
|
342
|
+
|
|
343
|
+
const scoped = run([
|
|
344
|
+
"batch",
|
|
345
|
+
"--group",
|
|
346
|
+
"file:src/b.ts",
|
|
347
|
+
"--dir",
|
|
348
|
+
dir,
|
|
349
|
+
"--branch",
|
|
350
|
+
"main",
|
|
351
|
+
"--json",
|
|
352
|
+
]);
|
|
353
|
+
expect(JSON.parse(scoped.output).current.key).toBe("file:src/b.ts");
|
|
354
|
+
} finally {
|
|
355
|
+
rmSync(dir, { recursive: true, force: true });
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("mutações fazem ping no run.json (status)", () => {
|
|
360
|
+
const run0 = makeRun(ONE_FINDING);
|
|
361
|
+
try {
|
|
362
|
+
const id = JSON.parse(
|
|
363
|
+
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
364
|
+
)[0].id;
|
|
365
|
+
const runJson = join(run0.dir, "main", "run.json");
|
|
366
|
+
|
|
367
|
+
run([
|
|
368
|
+
"set",
|
|
369
|
+
id,
|
|
370
|
+
"--decision",
|
|
371
|
+
"execute",
|
|
372
|
+
"--dir",
|
|
373
|
+
run0.dir,
|
|
374
|
+
"--branch",
|
|
375
|
+
"main",
|
|
376
|
+
]);
|
|
377
|
+
expect(JSON.parse(readFileSync(runJson, "utf8")).status).toBe("review_done");
|
|
378
|
+
|
|
379
|
+
run([
|
|
380
|
+
"set",
|
|
381
|
+
id,
|
|
382
|
+
"--exec",
|
|
383
|
+
"in_progress",
|
|
384
|
+
"--dir",
|
|
385
|
+
run0.dir,
|
|
386
|
+
"--branch",
|
|
387
|
+
"main",
|
|
388
|
+
]);
|
|
389
|
+
expect(JSON.parse(readFileSync(runJson, "utf8")).status).toBe("fixing");
|
|
390
|
+
|
|
391
|
+
run(["done", id, "--dir", run0.dir, "--branch", "main"]);
|
|
392
|
+
expect(JSON.parse(readFileSync(runJson, "utf8")).status).toBe("fix_done");
|
|
393
|
+
} finally {
|
|
394
|
+
run0.cleanup();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
describe("findings verify", () => {
|
|
400
|
+
const FILE = "src/tests/fixtures/fake-project.ts";
|
|
401
|
+
|
|
402
|
+
/** Run fake apontando para um arquivo real (relativo ao cwd → âncora resolvível). */
|
|
403
|
+
function makeVerifyRun(): { dir: string; outDir: string; cleanup: () => void } {
|
|
404
|
+
const dir = mkdtempSync(join(tmpdir(), "houndsman-verify-"));
|
|
405
|
+
const outDir = join(dir, "main");
|
|
406
|
+
mkdirSync(join(outDir, "findings"), { recursive: true });
|
|
407
|
+
writeFileSync(
|
|
408
|
+
join(outDir, "classification.json"),
|
|
409
|
+
JSON.stringify({ watchdogs: { alpha: [FILE] } }),
|
|
410
|
+
);
|
|
411
|
+
writeFileSync(
|
|
412
|
+
join(outDir, "findings", "alpha.json"),
|
|
413
|
+
JSON.stringify({
|
|
414
|
+
watchdogId: "alpha",
|
|
415
|
+
reviewedFiles: [FILE],
|
|
416
|
+
skipped: [],
|
|
417
|
+
findings: [
|
|
418
|
+
{
|
|
419
|
+
rule: "regra",
|
|
420
|
+
severity: "attention",
|
|
421
|
+
file: FILE,
|
|
422
|
+
line: 1,
|
|
423
|
+
message: "msg",
|
|
424
|
+
action: "ação",
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
}),
|
|
428
|
+
);
|
|
429
|
+
return {
|
|
430
|
+
dir,
|
|
431
|
+
outDir,
|
|
432
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
test("reporta persisting e grava o verify.md (tier local)", async () => {
|
|
437
|
+
const run0 = makeVerifyRun();
|
|
438
|
+
try {
|
|
439
|
+
const result = await runVerifyCommand(fakeHoundsmanConfig, [
|
|
440
|
+
"verify",
|
|
441
|
+
"--dir",
|
|
442
|
+
run0.dir,
|
|
443
|
+
"--branch",
|
|
444
|
+
"main",
|
|
445
|
+
"--json",
|
|
446
|
+
]);
|
|
447
|
+
expect(result.exitCode).toBe(0);
|
|
448
|
+
const parsed = JSON.parse(result.output);
|
|
449
|
+
expect(parsed.summary.persisting).toBe(1);
|
|
450
|
+
expect(parsed.entries[0].basis).toBe("anchor");
|
|
451
|
+
expect(existsSync(join(run0.outDir, "verify.md"))).toBe(true);
|
|
452
|
+
} finally {
|
|
453
|
+
run0.cleanup();
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("sem --apply não altera o estado", async () => {
|
|
458
|
+
const run0 = makeVerifyRun();
|
|
459
|
+
try {
|
|
460
|
+
await runVerifyCommand(fakeHoundsmanConfig, [
|
|
461
|
+
"verify",
|
|
462
|
+
"--dir",
|
|
463
|
+
run0.dir,
|
|
464
|
+
"--branch",
|
|
465
|
+
"main",
|
|
466
|
+
]);
|
|
467
|
+
const state = JSON.parse(
|
|
468
|
+
readFileSync(join(run0.outDir, "state.json"), "utf8"),
|
|
469
|
+
) as { findings: Record<string, { execState: string }> };
|
|
470
|
+
const only = Object.values(state.findings)[0];
|
|
471
|
+
expect(only.execState).toBe("pending");
|
|
472
|
+
} finally {
|
|
473
|
+
run0.cleanup();
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
});
|