@payconductor/houndsman 0.1.1 → 0.1.4
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 +32 -14
- package/package.json +1 -1
- package/src/cli/index.ts +22 -11
- package/src/core/assets.ts +3 -0
- package/src/core/brief.ts +2 -3
- package/src/core/commands/findings.ts +232 -4
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +13 -44
- package/src/core/diff-source.ts +4 -4
- package/src/core/findings-state.ts +20 -2
- 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-detector.ts +2 -2
- package/src/core/jev/jev-verify.ts +178 -0
- package/src/core/orchestrator.ts +202 -15
- package/src/core/paths.ts +6 -0
- package/src/core/registry-check.ts +53 -0
- package/src/core/report-builder.ts +5 -0
- package/src/core/run-status.ts +123 -0
- package/src/core/verify.ts +175 -0
- package/src/core/watchdog-registry.ts +1 -2
- package/src/index.ts +1 -1
- package/src/server/artifacts.ts +1 -0
- package/src/server/client/RunList.tsx +22 -13
- package/src/server/client/constants.ts +32 -1
- package/src/server/client/types.ts +4 -0
- package/src/server/index.ts +12 -1
- package/src/skill/AUDIT-PROTOCOL.md +7 -2
- package/src/skill/README.md +32 -6
- package/src/skill/SETUP.md +11 -2
- package/src/skill/SKILL.md +48 -0
- package/src/skill/fix.md +206 -0
- package/src/skill/{houndsman.md → hunt.md} +42 -68
- package/src/skill/{houndsman-update.md → train.md} +38 -17
- package/src/skill/verify.md +70 -0
- package/src/tests/findings-commands.test.ts +252 -2
- package/src/tests/findings-state.test.ts +52 -1
- package/src/tests/fix-batch.test.ts +257 -0
- package/src/tests/review-scripts.test.ts +36 -0
- package/src/tests/run-status.test.ts +147 -0
- package/src/tests/scaffold.test.ts +59 -0
- package/src/tests/server.test.ts +30 -0
- package/src/tests/verify.test.ts +170 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
3
10
|
import { tmpdir } from "node:os";
|
|
4
11
|
import { join } from "node:path";
|
|
5
|
-
import { runFindingsCommand } from "../core/commands/findings";
|
|
12
|
+
import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
|
|
6
13
|
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
7
14
|
|
|
8
15
|
/** Run fake em disco: classification.json + findings/alpha.json. */
|
|
@@ -223,4 +230,247 @@ describe("findings command", () => {
|
|
|
223
230
|
run0.cleanup();
|
|
224
231
|
}
|
|
225
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
|
+
});
|
|
226
476
|
});
|
|
@@ -267,8 +267,59 @@ describe("FindingsStateStore", () => {
|
|
|
267
267
|
|
|
268
268
|
test("emptyState cria um estado vazio válido", () => {
|
|
269
269
|
const state = emptyState("feature/x");
|
|
270
|
-
expect(state.version).toBe(
|
|
270
|
+
expect(state.version).toBe(2);
|
|
271
271
|
expect(state.slug).toBe("feature-x");
|
|
272
272
|
expect(state.findings).toEqual({});
|
|
273
273
|
});
|
|
274
274
|
});
|
|
275
|
+
|
|
276
|
+
describe("rastreio de revisão (incremental)", () => {
|
|
277
|
+
test("carimba reviewed e foundAtSha nos findings do ciclo", () => {
|
|
278
|
+
const project = makeProject({ "src/f.ts": DUPLICATED });
|
|
279
|
+
try {
|
|
280
|
+
const state = buildStateFromFindings(
|
|
281
|
+
[merged({ file: "src/f.ts", rule: "r", line: 2 })],
|
|
282
|
+
{
|
|
283
|
+
branch: "main",
|
|
284
|
+
readFile: fileReader(project.cwd),
|
|
285
|
+
headSha: "abc1234",
|
|
286
|
+
reviewed: { headSha: "abc1234", at: "2026-01-01T00:00:00.000Z" },
|
|
287
|
+
},
|
|
288
|
+
);
|
|
289
|
+
expect(state.reviewed?.headSha).toBe("abc1234");
|
|
290
|
+
const only = Object.values(state.findings)[0];
|
|
291
|
+
expect(only.foundAtSha).toBe("abc1234");
|
|
292
|
+
} finally {
|
|
293
|
+
project.cleanup();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("carry-over preserva o foundAtSha original", () => {
|
|
298
|
+
const project = makeProject({ "src/f.ts": DUPLICATED });
|
|
299
|
+
try {
|
|
300
|
+
const state = buildStateFromFindings(
|
|
301
|
+
[
|
|
302
|
+
// Novo/re-reportado: recebe o head atual.
|
|
303
|
+
merged({ file: "src/f.ts", rule: "r", line: 2 }),
|
|
304
|
+
// Carry-over: carrega o `foundAtSha` de quando foi observado.
|
|
305
|
+
{
|
|
306
|
+
...merged({ file: "src/f.ts", rule: "r2", line: 5 }),
|
|
307
|
+
foundAtSha: "old0001",
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
{
|
|
311
|
+
branch: "main",
|
|
312
|
+
readFile: fileReader(project.cwd),
|
|
313
|
+
headSha: "new0002",
|
|
314
|
+
reviewed: { headSha: "new0002", at: "2026-01-02T00:00:00.000Z" },
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
const byRule = (rule: string): string | undefined =>
|
|
318
|
+
Object.values(state.findings).find((f) => f.rule === rule)?.foundAtSha;
|
|
319
|
+
expect(byRule("r")).toBe("new0002");
|
|
320
|
+
expect(byRule("r2")).toBe("old0001");
|
|
321
|
+
} finally {
|
|
322
|
+
project.cleanup();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { FindingState, FindingsStateFile } from "../core/findings-state";
|
|
3
|
+
import {
|
|
4
|
+
buildFixBatch,
|
|
5
|
+
chooseGrouping,
|
|
6
|
+
fixQueue,
|
|
7
|
+
groupFindings,
|
|
8
|
+
} from "../core/fix-batch";
|
|
9
|
+
import type { Severity } from "../core/types";
|
|
10
|
+
|
|
11
|
+
let seq = 0;
|
|
12
|
+
|
|
13
|
+
/** Finding de teste com defaults sensatos. */
|
|
14
|
+
function f(
|
|
15
|
+
partial: Partial<FindingState> & { file: string; rule: string; severity: Severity },
|
|
16
|
+
): FindingState {
|
|
17
|
+
seq++;
|
|
18
|
+
return {
|
|
19
|
+
id: partial.id ?? `id${seq}`,
|
|
20
|
+
fingerprint: partial.fingerprint ?? `fp${seq}`,
|
|
21
|
+
watchdogIds: partial.watchdogIds ?? ["alpha"],
|
|
22
|
+
rule: partial.rule,
|
|
23
|
+
severity: partial.severity,
|
|
24
|
+
file: partial.file,
|
|
25
|
+
line: partial.line ?? 1,
|
|
26
|
+
message: partial.message ?? "m",
|
|
27
|
+
action: partial.action ?? "a",
|
|
28
|
+
evidence: partial.evidence,
|
|
29
|
+
anchor: null,
|
|
30
|
+
count: partial.count ?? 1,
|
|
31
|
+
present: partial.present ?? true,
|
|
32
|
+
decision: partial.decision ?? "execute",
|
|
33
|
+
comment: partial.comment,
|
|
34
|
+
execState: partial.execState ?? "pending",
|
|
35
|
+
createdAt: "",
|
|
36
|
+
updatedAt: "",
|
|
37
|
+
history: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stateOf(findings: FindingState[]): FindingsStateFile {
|
|
42
|
+
return {
|
|
43
|
+
version: 2,
|
|
44
|
+
branch: "feature/x",
|
|
45
|
+
slug: "feature-x",
|
|
46
|
+
updatedAt: "",
|
|
47
|
+
order: findings.map((item) => item.id),
|
|
48
|
+
findings: Object.fromEntries(findings.map((item) => [item.id, item])),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// chooseGrouping (heurística determinística)
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
describe("chooseGrouping", () => {
|
|
57
|
+
test("prefere arquivo quando há poucos arquivos com coesão", () => {
|
|
58
|
+
const findings = ["f1", "f2", "f3"].flatMap((file, i) => [
|
|
59
|
+
f({
|
|
60
|
+
file,
|
|
61
|
+
rule: `r${i * 2 + 1}`,
|
|
62
|
+
severity: "attention",
|
|
63
|
+
watchdogIds: [`w${i}`],
|
|
64
|
+
}),
|
|
65
|
+
f({ file, rule: `r${i * 2 + 2}`, severity: "nit", watchdogIds: [`w${i}`] }),
|
|
66
|
+
]);
|
|
67
|
+
expect(chooseGrouping(findings)).toBe("file");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("prefere regra quando uma regra domina vários arquivos", () => {
|
|
71
|
+
const findings = ["f1", "f2", "f3", "f4", "f5", "f6"].map((file, i) =>
|
|
72
|
+
f({
|
|
73
|
+
file,
|
|
74
|
+
rule: i < 3 ? "r1" : "r2",
|
|
75
|
+
severity: "attention",
|
|
76
|
+
watchdogIds: [`w${i}`],
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
expect(chooseGrouping(findings)).toBe("rule");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("cai em watchdog quando nem arquivo nem regra têm coesão", () => {
|
|
83
|
+
const findings = ["f1", "f2", "f3", "f4", "f5", "f6"].map((file, i) =>
|
|
84
|
+
f({
|
|
85
|
+
file,
|
|
86
|
+
rule: `r${Math.min(i, 4)}`,
|
|
87
|
+
severity: "attention",
|
|
88
|
+
watchdogIds: [i < 3 ? "w1" : "w2"],
|
|
89
|
+
}),
|
|
90
|
+
);
|
|
91
|
+
expect(chooseGrouping(findings)).toBe("watchdog");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("sem elegíveis (tudo singleton), cai em file", () => {
|
|
95
|
+
const findings = ["f1", "f2"].map((file, i) =>
|
|
96
|
+
f({ file, rule: `r${i}`, severity: "nit", watchdogIds: [`w${i}`] }),
|
|
97
|
+
);
|
|
98
|
+
expect(chooseGrouping(findings)).toBe("file");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("fila vazia devolve file", () => {
|
|
102
|
+
expect(chooseGrouping([])).toBe("file");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("groupFindings", () => {
|
|
107
|
+
test("agrupa por arquivo com chaves prefixadas", () => {
|
|
108
|
+
const findings = [
|
|
109
|
+
f({ file: "src/a.ts", rule: "r1", severity: "attention" }),
|
|
110
|
+
f({ file: "src/a.ts", rule: "r2", severity: "nit" }),
|
|
111
|
+
f({ file: "src/b.ts", rule: "r3", severity: "nit" }),
|
|
112
|
+
];
|
|
113
|
+
const groups = groupFindings(findings, "file");
|
|
114
|
+
expect(groups.size).toBe(2);
|
|
115
|
+
expect(groups.get("file:src/a.ts")).toHaveLength(2);
|
|
116
|
+
expect(groups.get("file:src/b.ts")).toHaveLength(1);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// fixQueue
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
describe("fixQueue", () => {
|
|
125
|
+
test("só execute, presentes e não done; includeDone inclui", () => {
|
|
126
|
+
const done = f({
|
|
127
|
+
file: "a.ts",
|
|
128
|
+
rule: "r1",
|
|
129
|
+
severity: "attention",
|
|
130
|
+
execState: "done",
|
|
131
|
+
});
|
|
132
|
+
const pending = f({ file: "a.ts", rule: "r2", severity: "attention" });
|
|
133
|
+
const ignored = f({
|
|
134
|
+
file: "a.ts",
|
|
135
|
+
rule: "r3",
|
|
136
|
+
severity: "attention",
|
|
137
|
+
decision: "ignore",
|
|
138
|
+
});
|
|
139
|
+
const absent = f({
|
|
140
|
+
file: "a.ts",
|
|
141
|
+
rule: "r4",
|
|
142
|
+
severity: "attention",
|
|
143
|
+
present: false,
|
|
144
|
+
});
|
|
145
|
+
const state = stateOf([done, pending, ignored, absent]);
|
|
146
|
+
|
|
147
|
+
expect(fixQueue(state).map((item) => item.id)).toEqual([pending.id]);
|
|
148
|
+
expect(fixQueue(state, { includeDone: true }).map((item) => item.id)).toEqual([
|
|
149
|
+
done.id,
|
|
150
|
+
pending.id,
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("filtra por severidade", () => {
|
|
155
|
+
const blocking = f({ file: "a.ts", rule: "r1", severity: "blocking" });
|
|
156
|
+
const nit = f({ file: "a.ts", rule: "r2", severity: "nit" });
|
|
157
|
+
const state = stateOf([blocking, nit]);
|
|
158
|
+
expect(fixQueue(state, { severity: "blocking" }).map((item) => item.id)).toEqual([
|
|
159
|
+
blocking.id,
|
|
160
|
+
]);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// buildFixBatch
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
/** Fila coesa em 2 arquivos / 2 watchdogs (arquivo e watchdog empatam → file). */
|
|
169
|
+
function coesaState(): FindingsStateFile {
|
|
170
|
+
return stateOf([
|
|
171
|
+
f({ file: "src/a.ts", rule: "r1", severity: "blocking", watchdogIds: ["alpha"] }),
|
|
172
|
+
f({
|
|
173
|
+
file: "src/a.ts",
|
|
174
|
+
rule: "r2",
|
|
175
|
+
severity: "attention",
|
|
176
|
+
watchdogIds: ["alpha"],
|
|
177
|
+
}),
|
|
178
|
+
f({
|
|
179
|
+
file: "src/a.ts",
|
|
180
|
+
rule: "r3",
|
|
181
|
+
severity: "attention",
|
|
182
|
+
watchdogIds: ["alpha"],
|
|
183
|
+
}),
|
|
184
|
+
f({ file: "src/a.ts", rule: "r4", severity: "nit", watchdogIds: ["alpha"] }),
|
|
185
|
+
f({ file: "src/b.ts", rule: "r5", severity: "attention", watchdogIds: ["beta"] }),
|
|
186
|
+
f({
|
|
187
|
+
file: "src/b.ts",
|
|
188
|
+
rule: "r6",
|
|
189
|
+
severity: "suggestion",
|
|
190
|
+
watchdogIds: ["beta"],
|
|
191
|
+
}),
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
describe("buildFixBatch", () => {
|
|
196
|
+
test("escolhe o agrupamento e devolve grupo atual + outros grupos", () => {
|
|
197
|
+
const result = buildFixBatch(coesaState());
|
|
198
|
+
expect(result.dimension).toBe("file");
|
|
199
|
+
expect(result.totals).toEqual({ queue: 6, groups: 2, returned: 4 });
|
|
200
|
+
expect(result.current.key).toBe("file:src/a.ts");
|
|
201
|
+
expect(result.current.total).toBe(4);
|
|
202
|
+
expect(result.current.findings).toHaveLength(4);
|
|
203
|
+
expect(result.current.remaining).toBe(0);
|
|
204
|
+
expect(result.others).toHaveLength(1);
|
|
205
|
+
expect(result.others[0].key).toBe("file:src/b.ts");
|
|
206
|
+
expect(result.others[0].count).toBe(2);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("pagina o grupo atual com pageSize", () => {
|
|
210
|
+
const result = buildFixBatch(coesaState(), { pageSize: 2 });
|
|
211
|
+
expect(result.current.returned).toBe(2);
|
|
212
|
+
expect(result.current.remaining).toBe(2);
|
|
213
|
+
expect(result.totals.returned).toBe(2);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("--group foca um grupo específico", () => {
|
|
217
|
+
const result = buildFixBatch(coesaState(), { group: "file:src/b.ts" });
|
|
218
|
+
expect(result.current.key).toBe("file:src/b.ts");
|
|
219
|
+
expect(result.current.total).toBe(2);
|
|
220
|
+
expect(result.others).toHaveLength(1);
|
|
221
|
+
expect(result.others[0].key).toBe("file:src/a.ts");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("--group inexistente lança com as chaves disponíveis", () => {
|
|
225
|
+
expect(() => buildFixBatch(coesaState(), { group: "file:nao-existe" })).toThrow(
|
|
226
|
+
/não encontrado/,
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("--severity filtra a fila", () => {
|
|
231
|
+
const result = buildFixBatch(coesaState(), { severity: "blocking" });
|
|
232
|
+
expect(result.totals.queue).toBe(1);
|
|
233
|
+
expect(result.current.key).toBe("file:src/a.ts");
|
|
234
|
+
expect(result.current.total).toBe(1);
|
|
235
|
+
expect(result.others).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("fila vazia devolve lote vazio", () => {
|
|
239
|
+
const state = stateOf([
|
|
240
|
+
f({ file: "a.ts", rule: "r1", severity: "attention", decision: "pending" }),
|
|
241
|
+
]);
|
|
242
|
+
const result = buildFixBatch(state);
|
|
243
|
+
expect(result.totals).toEqual({ queue: 0, groups: 0, returned: 0 });
|
|
244
|
+
expect(result.current.findings).toHaveLength(0);
|
|
245
|
+
expect(result.others).toHaveLength(0);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("outros grupos trazem o mix de severidade", () => {
|
|
249
|
+
const result = buildFixBatch(coesaState());
|
|
250
|
+
expect(result.others[0].bySeverity).toEqual({
|
|
251
|
+
blocking: 0,
|
|
252
|
+
attention: 1,
|
|
253
|
+
suggestion: 1,
|
|
254
|
+
nit: 0,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -139,6 +139,42 @@ describe("registry-check", () => {
|
|
|
139
139
|
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
140
140
|
expect(warnings).toEqual([]);
|
|
141
141
|
});
|
|
142
|
+
|
|
143
|
+
test("regra duplicada no mesmo watchdog gera aviso", () => {
|
|
144
|
+
const config = {
|
|
145
|
+
...fakeHoundsmanConfig,
|
|
146
|
+
watchdogs: {
|
|
147
|
+
...fakeHoundsmanConfig.watchdogs,
|
|
148
|
+
alpha: {
|
|
149
|
+
...alphaWatchdog,
|
|
150
|
+
rules: [
|
|
151
|
+
{ severity: "attention" as const, text: "Evitar console.log." },
|
|
152
|
+
{ severity: "blocking" as const, text: "Evitar console.log." },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
158
|
+
expect(warnings.some((w) => w.includes("duplicada"))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("texto de regra em reviewFocus/contextFiles gera aviso", () => {
|
|
162
|
+
const config = {
|
|
163
|
+
...fakeHoundsmanConfig,
|
|
164
|
+
watchdogs: {
|
|
165
|
+
...fakeHoundsmanConfig.watchdogs,
|
|
166
|
+
alpha: {
|
|
167
|
+
...alphaWatchdog,
|
|
168
|
+
reviewFocus: ["Evitar: console.log."],
|
|
169
|
+
contextFiles: ["Promover: validar a entrada."],
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
|
|
174
|
+
expect(
|
|
175
|
+
warnings.filter((w) => w.includes("a regra vive só em 'rules'")),
|
|
176
|
+
).toHaveLength(2);
|
|
177
|
+
});
|
|
142
178
|
});
|
|
143
179
|
|
|
144
180
|
// ---------------------------------------------------------------------------
|