@payconductor/houndsman 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/package.json +1 -1
- package/src/cli/commands/compare-command.ts +112 -0
- package/src/cli/commands/context-command.ts +1 -0
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/findings-command.ts +11 -1
- package/src/cli/commands/hunt-command.ts +479 -0
- package/src/cli/commands/review-command.ts +58 -2
- package/src/cli/commands/status-command.ts +3 -2
- package/src/cli/help.ts +20 -3
- package/src/cli/index.ts +20 -3
- package/src/cli/progress.ts +50 -0
- package/src/core/ai/ai-client.ts +38 -2
- package/src/core/ai/ai-sdk-model-provider.ts +39 -0
- package/src/core/ai/features/brief-enricher.ts +1 -1
- package/src/core/ai/features/business-analyzer.ts +61 -0
- package/src/core/ai/features/business-summarizer.ts +44 -0
- package/src/core/ai/index.ts +14 -1
- package/src/core/ai/prompts/brief.ts +33 -7
- package/src/core/ai/prompts/business.ts +76 -0
- package/src/core/ai/provider-capabilities.ts +12 -0
- package/src/core/ai/schemas.ts +70 -2
- package/src/core/ai/types.ts +2 -0
- package/src/core/ai/usage-recorder.ts +53 -1
- package/src/core/brief.ts +246 -12
- package/src/core/builtins.ts +108 -0
- package/src/core/business-summary.ts +583 -0
- package/src/core/commands/compare.ts +233 -0
- package/src/core/commands/context.ts +6 -2
- package/src/core/commands/findings.ts +38 -30
- package/src/core/commands/review.ts +6 -0
- package/src/core/commands/scaffold.ts +38 -0
- package/src/core/commands/status.ts +9 -4
- package/src/core/compare.ts +243 -0
- package/src/core/concurrency.ts +30 -0
- package/src/core/config.ts +57 -1
- package/src/core/defaults.ts +63 -2
- package/src/core/domain/budget.ts +62 -4
- package/src/core/domain/classification.ts +12 -2
- package/src/core/engine/carryover.ts +4 -0
- package/src/core/engine/finalize-pipeline.ts +347 -0
- package/src/core/engine/prepare-pipeline.ts +1294 -0
- package/src/core/engine/review-context.ts +51 -0
- package/src/core/engine/review-options.ts +47 -0
- package/src/core/engine/review-scope.ts +106 -26
- package/src/core/engine/review-session.ts +18 -16
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/engine/run-artifacts.ts +196 -0
- package/src/core/engine/run-format.ts +4 -0
- package/src/core/env.ts +14 -0
- package/src/core/findings-state.ts +13 -1
- package/src/core/github-review.ts +5 -2
- package/src/core/hunt/hunt-runner.ts +418 -0
- package/src/core/hunt/index.ts +61 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-intent-prompt.ts +50 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +83 -0
- package/src/core/hunt/maki-release.ts +104 -0
- package/src/core/hunt/maki-runner.ts +276 -0
- package/src/core/hunt/pool.ts +8 -0
- package/src/core/logging/index.ts +7 -0
- package/src/core/logging/logger.ts +19 -0
- package/src/core/logging/progress.ts +366 -0
- package/src/core/orchestrator.ts +33 -1180
- package/src/core/paths.ts +6 -3
- package/src/core/pr-context.ts +2 -4
- package/src/core/prepare-report.ts +61 -0
- package/src/core/project-config.ts +46 -1
- package/src/core/propagation.ts +127 -0
- package/src/core/registry-check.ts +60 -21
- package/src/core/renderers/manifest-writer.ts +13 -0
- package/src/core/renderers/resume-renderer.ts +151 -11
- package/src/core/report-builder.ts +108 -278
- package/src/core/storage/file/file-storage.ts +7 -3
- package/src/core/storage/sqlite/mappers.ts +10 -0
- package/src/core/storage/sqlite/schema.ts +104 -23
- package/src/core/storage/sqlite/sqlite-storage.ts +22 -9
- package/src/core/storage/types.ts +17 -4
- package/src/core/storage/work-area.ts +59 -1
- package/src/core/types.ts +31 -0
- package/src/core/watchdog-registry.ts +21 -3
- package/src/index.ts +78 -4
- package/src/server/client/ArtifactsPanel.tsx +4 -2
- package/src/server/client/FindingCard.tsx +21 -1
- package/src/server/client/RunsSection.tsx +5 -0
- package/src/server/client/utils/constants.ts +21 -11
- package/src/server/client/utils/markdown.ts +1 -1
- package/src/server/client/utils/types.ts +14 -2
- package/src/server/run-repository.ts +35 -14
- package/src/server/styles.css +2 -0
- package/src/skill/OUTPUT-FORMAT.md +36 -10
- package/src/skill/README.md +30 -14
- package/src/skill/SETUP.md +22 -1
- package/src/skill/SKILL.md +17 -4
- package/src/skill/example-resume.md +43 -0
- package/src/skill/hunt.md +181 -58
- package/src/tests/__snapshots__/baseline.test.ts.snap +206 -178
- package/src/tests/ai-core.test.ts +84 -1
- package/src/tests/ai-features.test.ts +95 -4
- package/src/tests/baseline.test.ts +29 -121
- package/src/tests/brief-scope.test.ts +250 -0
- package/src/tests/business-summary.test.ts +258 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/compare.test.ts +233 -0
- package/src/tests/fixtures/fake-model-provider.ts +2 -1
- package/src/tests/hunt-cli.test.ts +185 -0
- package/src/tests/hunt-install.test.ts +153 -0
- package/src/tests/hunt-maki.test.ts +525 -0
- package/src/tests/markdown.test.ts +3 -3
- package/src/tests/progress.test.ts +145 -0
- package/src/tests/propagation.test.ts +119 -0
- package/src/tests/review-resume.test.ts +60 -1
- package/src/tests/review-scope.test.ts +90 -19
- package/src/tests/review-scripts.test.ts +106 -218
- package/src/tests/server.test.ts +7 -4
- package/src/tests/storage.test.ts +194 -9
- package/src/core/renderers/report-renderer.ts +0 -66
- package/src/skill/example-review.md +0 -66
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progresso do CLI — formatação, fases sequenciais e board concorrente.
|
|
3
|
+
*
|
|
4
|
+
* O board em TTY pinta um bloco com loader; fora de TTY mantém as linhas de
|
|
5
|
+
* início/fim e um heartbeat periódico.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
formatDuration,
|
|
11
|
+
Logger,
|
|
12
|
+
ParallelPhaseReporter,
|
|
13
|
+
PhaseReporter,
|
|
14
|
+
ProgressBoard,
|
|
15
|
+
} from "../core/logging";
|
|
16
|
+
|
|
17
|
+
function memoryStream(): { stream: NodeJS.WritableStream; text: () => string } {
|
|
18
|
+
let content = "";
|
|
19
|
+
const stream = {
|
|
20
|
+
write(chunk: string) {
|
|
21
|
+
content += chunk;
|
|
22
|
+
return true;
|
|
23
|
+
},
|
|
24
|
+
isTTY: false,
|
|
25
|
+
} as unknown as NodeJS.WritableStream;
|
|
26
|
+
return { stream, text: () => content };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function loggerFor(tty: boolean): {
|
|
30
|
+
logger: Logger;
|
|
31
|
+
out: ReturnType<typeof memoryStream>;
|
|
32
|
+
err: ReturnType<typeof memoryStream>;
|
|
33
|
+
} {
|
|
34
|
+
const out = memoryStream();
|
|
35
|
+
const err = memoryStream();
|
|
36
|
+
const logger = new Logger({
|
|
37
|
+
stdout: out.stream,
|
|
38
|
+
stderr: err.stream,
|
|
39
|
+
color: false,
|
|
40
|
+
isTTY: tty,
|
|
41
|
+
});
|
|
42
|
+
return { logger, out, err };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
46
|
+
|
|
47
|
+
describe("formatDuration", () => {
|
|
48
|
+
test("ms, segundos e minutos", () => {
|
|
49
|
+
expect(formatDuration(820)).toBe("820ms");
|
|
50
|
+
expect(formatDuration(3_400)).toBe("3.4s");
|
|
51
|
+
expect(formatDuration(42_000)).toBe("42s");
|
|
52
|
+
expect(formatDuration(125_000)).toBe("2m05s");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("PhaseReporter", () => {
|
|
57
|
+
test("em pipe imprime início e fim com a duração", () => {
|
|
58
|
+
const { logger, out } = loggerFor(false);
|
|
59
|
+
const reporter = new PhaseReporter(logger);
|
|
60
|
+
reporter.begin("diff", "Lendo o diff");
|
|
61
|
+
expect(out.text()).toContain("→ Lendo o diff");
|
|
62
|
+
reporter.end("diff", "Diff lido", 1_200);
|
|
63
|
+
expect(out.text()).toContain("✔ Diff lido (1.2s)");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("fase sem início ainda reporta o fim", () => {
|
|
67
|
+
const { logger, out } = loggerFor(false);
|
|
68
|
+
new PhaseReporter(logger).end("ast", "AST analisada", 40);
|
|
69
|
+
expect(out.text()).toContain("AST analisada (40ms)");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("ParallelPhaseReporter", () => {
|
|
74
|
+
test("em pipe aceita fases concorrentes e loga cada uma", () => {
|
|
75
|
+
const { logger, out } = loggerFor(false);
|
|
76
|
+
const reporter = new ParallelPhaseReporter(logger);
|
|
77
|
+
reporter.begin("context", "Resumindo o contexto");
|
|
78
|
+
reporter.begin("business", "Reconstruindo a intenção");
|
|
79
|
+
reporter.end("context", "Contexto resumido", 50);
|
|
80
|
+
reporter.end("business", "Intenção reconstruída", 80);
|
|
81
|
+
reporter.stop();
|
|
82
|
+
const text = out.text();
|
|
83
|
+
expect(text).toContain("→ Resumindo o contexto");
|
|
84
|
+
expect(text).toContain("→ Reconstruindo a intenção");
|
|
85
|
+
expect(text).toContain("✔ Contexto resumido (50ms)");
|
|
86
|
+
expect(text).toContain("✔ Intenção reconstruída (80ms)");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("stop sem fases ativas não lança", () => {
|
|
90
|
+
const { logger } = loggerFor(false);
|
|
91
|
+
expect(() => new ParallelPhaseReporter(logger).stop()).not.toThrow();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("ProgressBoard", () => {
|
|
96
|
+
test("em pipe loga início/fim e não usa ANSI", () => {
|
|
97
|
+
const { logger, out, err } = loggerFor(false);
|
|
98
|
+
const board = new ProgressBoard({ logger, heartbeatMs: 10_000 });
|
|
99
|
+
board.start(2);
|
|
100
|
+
board.add("alpha", "revisando");
|
|
101
|
+
board.add("beta", "revisando");
|
|
102
|
+
board.succeed("alpha", "alpha — 2 finding(s) (12s)");
|
|
103
|
+
board.fail("beta", "beta falhou");
|
|
104
|
+
board.stop();
|
|
105
|
+
const text = out.text();
|
|
106
|
+
expect(text).toContain("alpha: revisando…");
|
|
107
|
+
expect(text).toContain("alpha — 2 finding(s) (12s)");
|
|
108
|
+
expect(err.text()).toContain("beta falhou");
|
|
109
|
+
expect(`${text}${err.text()}`).not.toContain("\u001b[");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("em pipe emite heartbeat enquanto há tarefa em execução", async () => {
|
|
113
|
+
const { logger, out } = loggerFor(false);
|
|
114
|
+
const board = new ProgressBoard({ logger, heartbeatMs: 5 });
|
|
115
|
+
board.start(1);
|
|
116
|
+
board.add("alpha", "revisando");
|
|
117
|
+
await sleep(40);
|
|
118
|
+
board.stop();
|
|
119
|
+
expect(out.text()).toContain("1 tarefa(s) em execução: alpha");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("em TTY pinta o bloco com loader e apaga ao concluir", () => {
|
|
123
|
+
const { logger, out } = loggerFor(true);
|
|
124
|
+
const board = new ProgressBoard({ logger, frameMs: 1_000 });
|
|
125
|
+
board.start(1);
|
|
126
|
+
board.add("alpha", "revisando");
|
|
127
|
+
const painted = out.text();
|
|
128
|
+
expect(painted).toContain("\u001b[2K");
|
|
129
|
+
expect(painted).toContain("alpha");
|
|
130
|
+
expect(painted).toContain("0/1 concluído(s)");
|
|
131
|
+
board.succeed("alpha", "alpha — 1 finding(s) (5s)");
|
|
132
|
+
board.stop();
|
|
133
|
+
expect(out.text()).toContain("✔ alpha — 1 finding(s) (5s)");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("retry atualiza o detalhe sem derrubar o board", () => {
|
|
137
|
+
const { logger, out } = loggerFor(false);
|
|
138
|
+
const board = new ProgressBoard({ logger });
|
|
139
|
+
board.start(1);
|
|
140
|
+
board.add("alpha", "revisando");
|
|
141
|
+
board.retry("alpha", "tentativa 2/2", "alpha: repetindo (2/2)");
|
|
142
|
+
board.stop();
|
|
143
|
+
expect(out.text()).toContain("alpha: repetindo (2/2)");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Propagação mecânica — cluster de micro-alterações e lacunas de importadores.
|
|
3
|
+
*
|
|
4
|
+
* Congela a heurística usada no brief para não ler N diffs de uma linha e para
|
|
5
|
+
* apontar contratos cujos importadores ficaram fora do diff.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import type { AstAnalysis, AstImpact } from "../core/ast/types";
|
|
10
|
+
import { detectPropagation, detectPropagationGaps } from "../core/propagation";
|
|
11
|
+
import type { ChurnMap } from "../core/types";
|
|
12
|
+
|
|
13
|
+
const files = Array.from({ length: 7 }, (_, index) => `src/f${index}.ts`);
|
|
14
|
+
|
|
15
|
+
function patchFor(line: string): Map<string, string> {
|
|
16
|
+
return new Map(
|
|
17
|
+
files.map((file) => [file, `@@ -1 +1 @@\n- await pay(charge);\n+${line}`]),
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function churnFor(extra: ChurnMap = {}): ChurnMap {
|
|
22
|
+
const base: ChurnMap = Object.fromEntries(
|
|
23
|
+
files.map((file) => [file, { additions: 1, deletions: 1 }]),
|
|
24
|
+
);
|
|
25
|
+
return { ...base, ...extra };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("detectPropagation", () => {
|
|
29
|
+
test("agrupa o padrão dominante e isola os outliers", () => {
|
|
30
|
+
const patch = patchFor(" await pay(charge, true);");
|
|
31
|
+
patch.set(
|
|
32
|
+
"src/f6.ts",
|
|
33
|
+
"@@ -1 +1 @@\n- await pay(charge);\n+ await pay(charge, false);",
|
|
34
|
+
);
|
|
35
|
+
const cluster = detectPropagation(files, patch, churnFor());
|
|
36
|
+
expect(cluster?.files).toEqual(files.slice(0, 6));
|
|
37
|
+
expect(cluster?.lines).toEqual(["await pay(charge, true);"]);
|
|
38
|
+
expect(cluster?.outliers).toEqual([
|
|
39
|
+
{ file: "src/f6.ts", lines: ["await pay(charge, false);"] },
|
|
40
|
+
]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("ignora arquivos com churn acima do limite de micro-alteração", () => {
|
|
44
|
+
const patch = patchFor(" await pay(charge, true);");
|
|
45
|
+
patch.set("src/big.ts", "+ await pay(charge, true);");
|
|
46
|
+
const cluster = detectPropagation([...files, "src/big.ts"], patch, {
|
|
47
|
+
...churnFor(),
|
|
48
|
+
"src/big.ts": { additions: 40, deletions: 0 },
|
|
49
|
+
});
|
|
50
|
+
expect(cluster?.files).not.toContain("src/big.ts");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("sem grupo dominante devolve null", () => {
|
|
54
|
+
const patch = new Map(
|
|
55
|
+
files.slice(0, 3).map((file) => [file, "+ await pay(charge, true);"]),
|
|
56
|
+
);
|
|
57
|
+
expect(detectPropagation(files.slice(0, 3), patch, churnFor())).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("detectPropagationGaps", () => {
|
|
62
|
+
const analysis = {
|
|
63
|
+
facts: {},
|
|
64
|
+
delta: {
|
|
65
|
+
addedSymbols: [],
|
|
66
|
+
removedSymbols: [],
|
|
67
|
+
modifiedSymbols: [],
|
|
68
|
+
changedSignatures: [
|
|
69
|
+
{
|
|
70
|
+
name: "pay",
|
|
71
|
+
before: "pay(charge)",
|
|
72
|
+
after: "pay(charge, x)",
|
|
73
|
+
exported: true,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
changedExports: [],
|
|
77
|
+
addedImports: [],
|
|
78
|
+
removedImports: [],
|
|
79
|
+
},
|
|
80
|
+
} as unknown as AstAnalysis;
|
|
81
|
+
const impact = {
|
|
82
|
+
affectedFiles: [],
|
|
83
|
+
importers: {
|
|
84
|
+
"src/types.ts": ["src/a.ts", "src/b.ts"],
|
|
85
|
+
"src/other.ts": ["src/c.ts"],
|
|
86
|
+
},
|
|
87
|
+
exportedChanges: [],
|
|
88
|
+
} as unknown as AstImpact;
|
|
89
|
+
|
|
90
|
+
test("lista só importadores que não estão no diff", () => {
|
|
91
|
+
const gaps = detectPropagationGaps(
|
|
92
|
+
["src/types.ts", "src/other.ts"],
|
|
93
|
+
new Map([["src/types.ts", analysis]]),
|
|
94
|
+
impact,
|
|
95
|
+
new Set(["src/a.ts"]),
|
|
96
|
+
);
|
|
97
|
+
expect(gaps).toEqual([{ file: "src/types.ts", importers: ["src/b.ts"] }]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("ignora mudança que não toca contrato exportado", () => {
|
|
101
|
+
const internal = {
|
|
102
|
+
...analysis,
|
|
103
|
+
delta: {
|
|
104
|
+
...analysis.delta,
|
|
105
|
+
changedSignatures: [
|
|
106
|
+
{ name: "pay", before: "pay(a)", after: "pay(b)", exported: false },
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
} as unknown as AstAnalysis;
|
|
110
|
+
expect(
|
|
111
|
+
detectPropagationGaps(
|
|
112
|
+
["src/types.ts"],
|
|
113
|
+
new Map([["src/types.ts", internal]]),
|
|
114
|
+
impact,
|
|
115
|
+
new Set(),
|
|
116
|
+
),
|
|
117
|
+
).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -95,6 +95,63 @@ describe("renderReviewResume", () => {
|
|
|
95
95
|
expect(md).toContain("Sem mudanças estruturais detectadas nesta revisão.");
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
+
test("inclui cobertura por domínio, candidatos e conclusão", () => {
|
|
99
|
+
const md = renderReviewResume({
|
|
100
|
+
branch: "feat/x",
|
|
101
|
+
date: "2026-10-17",
|
|
102
|
+
classification,
|
|
103
|
+
findings: [
|
|
104
|
+
finding({ severity: "blocking" }),
|
|
105
|
+
finding({ severity: "attention", file: "src/b.ts" }),
|
|
106
|
+
],
|
|
107
|
+
domains: [
|
|
108
|
+
{
|
|
109
|
+
id: "alpha",
|
|
110
|
+
name: "Alpha",
|
|
111
|
+
findings: 1,
|
|
112
|
+
reviewedFiles: 1,
|
|
113
|
+
expectedFiles: 2,
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "beta",
|
|
117
|
+
name: "Beta",
|
|
118
|
+
findings: 1,
|
|
119
|
+
reviewedFiles: 1,
|
|
120
|
+
expectedFiles: 1,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
candidates: [
|
|
124
|
+
{
|
|
125
|
+
watchdogId: "alpha",
|
|
126
|
+
rule: "Suspeita X",
|
|
127
|
+
severity: "attention",
|
|
128
|
+
file: "src/c.ts",
|
|
129
|
+
confidence: 0.7,
|
|
130
|
+
questionKey: "q1",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
watchdogId: "alpha",
|
|
134
|
+
rule: "regra",
|
|
135
|
+
severity: "attention",
|
|
136
|
+
file: "src/a.ts",
|
|
137
|
+
confidence: 0.7,
|
|
138
|
+
questionKey: "q2",
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(md).toContain("## Cobertura por domínio");
|
|
144
|
+
expect(md).toContain("| Alpha | 1 | 1/2 | — | 1 |");
|
|
145
|
+
expect(md).toContain("| Beta | 1 | 1/1 | — | — |");
|
|
146
|
+
expect(md).toContain("## Candidatos não verificados (1)");
|
|
147
|
+
expect(md).toContain("Suspeita X");
|
|
148
|
+
expect(md).not.toContain("`src/a.ts` ·");
|
|
149
|
+
expect(md).toContain("## Conclusão");
|
|
150
|
+
expect(md).toContain(
|
|
151
|
+
"**Resumo:** 1 bloqueantes · 1 atenção · 0 sugestões · 0 nits.",
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
98
155
|
test("respeita o teto de caracteres", () => {
|
|
99
156
|
const findings = Array.from({ length: 40 }, (_, index) =>
|
|
100
157
|
finding({
|
|
@@ -124,6 +181,8 @@ describe("renderReviewResume", () => {
|
|
|
124
181
|
});
|
|
125
182
|
|
|
126
183
|
expect(md.length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
127
|
-
|
|
184
|
+
// O corte acontece no texto narrativo; tabelas e conclusão seguem inteiras.
|
|
185
|
+
expect(md).toContain("\n…");
|
|
186
|
+
expect(md).toContain("## Conclusão");
|
|
128
187
|
});
|
|
129
188
|
});
|
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { AstService } from "../core/ast/service";
|
|
13
13
|
import { ReviewScopeResolver, type ScopeOptions } from "../core/engine/review-scope";
|
|
14
14
|
import type { FindingState, FindingsStateFile } from "../core/findings-state";
|
|
15
|
+
import type { PrepareProgressEvent } from "../core/orchestrator";
|
|
15
16
|
import { defaultOptions, finalize, prepare, reviewContext } from "../core/orchestrator";
|
|
16
17
|
import { resolveRepoIdentity } from "../core/storage/repo-identity";
|
|
17
18
|
import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
|
|
@@ -255,7 +256,7 @@ describe("prepare/finalize — mesmo commit", () => {
|
|
|
255
256
|
};
|
|
256
257
|
|
|
257
258
|
let ctx = reviewContext(storage, record);
|
|
258
|
-
await prepare(
|
|
259
|
+
await prepare(ctx, fakeHoundsmanConfig, opts);
|
|
259
260
|
expect(storage.getReview(record.id)?.status).toBe("reviewing");
|
|
260
261
|
|
|
261
262
|
writeFileSync(
|
|
@@ -280,30 +281,46 @@ describe("prepare/finalize — mesmo commit", () => {
|
|
|
280
281
|
2,
|
|
281
282
|
)}\n`,
|
|
282
283
|
);
|
|
283
|
-
|
|
284
|
+
writeFileSync(
|
|
285
|
+
ctx.work.findingFile("general"),
|
|
286
|
+
`${JSON.stringify(
|
|
287
|
+
{
|
|
288
|
+
watchdogId: "general",
|
|
289
|
+
reviewedFiles: ["src/alpha/a.ts"],
|
|
290
|
+
skipped: [],
|
|
291
|
+
findings: [],
|
|
292
|
+
},
|
|
293
|
+
null,
|
|
294
|
+
2,
|
|
295
|
+
)}\n`,
|
|
296
|
+
);
|
|
297
|
+
finalize(ctx, fakeHoundsmanConfig, opts);
|
|
284
298
|
|
|
285
299
|
const classificationBefore = storage.readArtifact(
|
|
286
300
|
record.id,
|
|
287
301
|
"classification.json",
|
|
288
302
|
)?.content;
|
|
289
|
-
const reportBefore = storage.readArtifact(
|
|
303
|
+
const reportBefore = storage.readArtifact(
|
|
304
|
+
record.id,
|
|
305
|
+
"review-resume.md",
|
|
306
|
+
)?.content;
|
|
290
307
|
const findingsBefore = JSON.stringify(
|
|
291
308
|
storage.readFindings(record.id)?.findings,
|
|
292
309
|
);
|
|
293
310
|
const reviewedBefore = storage.getReview(record.id)?.reviewedSha;
|
|
294
311
|
expect(classificationBefore).toBeTruthy();
|
|
295
|
-
expect(reportBefore).toContain("
|
|
312
|
+
expect(reportBefore).toContain("## Cobertura por domínio");
|
|
296
313
|
|
|
297
314
|
// Mesmo commit: prepare vira no-op e nada é reescrito.
|
|
298
315
|
ctx = reviewContext(storage, storage.getReview(record.id) ?? record);
|
|
299
|
-
const again = await prepare(
|
|
316
|
+
const again = await prepare(ctx, fakeHoundsmanConfig, opts);
|
|
300
317
|
expect(again.messages.join("\n")).toContain(
|
|
301
318
|
"Nenhuma alteração desde a última revisão",
|
|
302
319
|
);
|
|
303
320
|
expect(storage.readArtifact(record.id, "classification.json")?.content).toBe(
|
|
304
321
|
classificationBefore,
|
|
305
322
|
);
|
|
306
|
-
expect(storage.readArtifact(record.id, "review.md")?.content).toBe(
|
|
323
|
+
expect(storage.readArtifact(record.id, "review-resume.md")?.content).toBe(
|
|
307
324
|
reportBefore,
|
|
308
325
|
);
|
|
309
326
|
expect(storage.getReview(record.id)?.status).toBe("review_done");
|
|
@@ -311,13 +328,12 @@ describe("prepare/finalize — mesmo commit", () => {
|
|
|
311
328
|
|
|
312
329
|
// Finalize sem ciclo preparado: no-op (não degrada o relatório).
|
|
313
330
|
const finalizedAgain = finalize(
|
|
314
|
-
opts,
|
|
315
|
-
"main",
|
|
316
|
-
fakeHoundsmanConfig,
|
|
317
331
|
reviewContext(storage, storage.getReview(record.id) ?? record),
|
|
332
|
+
fakeHoundsmanConfig,
|
|
333
|
+
opts,
|
|
318
334
|
);
|
|
319
335
|
expect(finalizedAgain.messages.join("\n")).toContain("Nada a finalizar");
|
|
320
|
-
expect(storage.readArtifact(record.id, "review.md")?.content).toBe(
|
|
336
|
+
expect(storage.readArtifact(record.id, "review-resume.md")?.content).toBe(
|
|
321
337
|
reportBefore,
|
|
322
338
|
);
|
|
323
339
|
expect(JSON.stringify(storage.readFindings(record.id)?.findings)).toBe(
|
|
@@ -363,10 +379,9 @@ describe("prepare sem alterações", () => {
|
|
|
363
379
|
};
|
|
364
380
|
|
|
365
381
|
const result = await prepare(
|
|
366
|
-
opts,
|
|
367
|
-
"main",
|
|
368
|
-
fakeHoundsmanConfig,
|
|
369
382
|
reviewContext(storage, record),
|
|
383
|
+
fakeHoundsmanConfig,
|
|
384
|
+
opts,
|
|
370
385
|
);
|
|
371
386
|
expect(result.messages.join("\n")).toContain("nenhuma run criada");
|
|
372
387
|
expect(storage.getReview(record.id)).toBeNull();
|
|
@@ -383,12 +398,7 @@ describe("prepare sem alterações", () => {
|
|
|
383
398
|
kind: "context",
|
|
384
399
|
content: "{}\n",
|
|
385
400
|
});
|
|
386
|
-
await prepare(
|
|
387
|
-
opts,
|
|
388
|
-
"main",
|
|
389
|
-
fakeHoundsmanConfig,
|
|
390
|
-
reviewContext(storage, kept),
|
|
391
|
-
);
|
|
401
|
+
await prepare(reviewContext(storage, kept), fakeHoundsmanConfig, opts);
|
|
392
402
|
expect(storage.getReview(kept.id)?.status).toBe("review_done");
|
|
393
403
|
expect(storage.readArtifact(kept.id, "context.json")).not.toBeNull();
|
|
394
404
|
} finally {
|
|
@@ -400,3 +410,64 @@ describe("prepare sem alterações", () => {
|
|
|
400
410
|
}
|
|
401
411
|
}, 30_000);
|
|
402
412
|
});
|
|
413
|
+
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
// Progresso do prepare
|
|
416
|
+
// ---------------------------------------------------------------------------
|
|
417
|
+
|
|
418
|
+
describe("prepare — progresso", () => {
|
|
419
|
+
test("emite início/fim das etapas que realmente rodam", async () => {
|
|
420
|
+
const repo = makeRepo();
|
|
421
|
+
const originalCwd = process.cwd();
|
|
422
|
+
const originalHome = process.env.HOUNDSMAN_HOME;
|
|
423
|
+
let storage: SQLiteStorageProvider | null = null;
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
process.env.HOUNDSMAN_HOME = join(repo.cwd, ".home");
|
|
427
|
+
process.chdir(repo.cwd);
|
|
428
|
+
|
|
429
|
+
storage = new SQLiteStorageProvider(join(repo.cwd, "houndsman.db"));
|
|
430
|
+
const record = storage.getOrCreateReview({
|
|
431
|
+
repo: resolveRepoIdentity(),
|
|
432
|
+
branch: "main",
|
|
433
|
+
slug: "main",
|
|
434
|
+
});
|
|
435
|
+
const opts = {
|
|
436
|
+
...defaultOptions(),
|
|
437
|
+
base: repo.base,
|
|
438
|
+
head: "HEAD",
|
|
439
|
+
noJev: true,
|
|
440
|
+
noAst: true,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const events: PrepareProgressEvent[] = [];
|
|
444
|
+
await prepare(reviewContext(storage, record), fakeHoundsmanConfig, opts, {
|
|
445
|
+
onProgress: (event) => events.push(event),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const starts = events
|
|
449
|
+
.filter((event) => event.type === "start")
|
|
450
|
+
.map((event) => event.stage);
|
|
451
|
+
expect(starts).toEqual([
|
|
452
|
+
"diff",
|
|
453
|
+
"classify",
|
|
454
|
+
"business",
|
|
455
|
+
"briefs",
|
|
456
|
+
"manifest",
|
|
457
|
+
]);
|
|
458
|
+
const done = events.filter((event) => event.type === "done");
|
|
459
|
+
expect(done.map((event) => event.stage)).toEqual(starts);
|
|
460
|
+
for (const event of done) {
|
|
461
|
+
if (event.type === "done") {
|
|
462
|
+
expect(event.durationMs).toBeGreaterThanOrEqual(0);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} finally {
|
|
466
|
+
storage?.close();
|
|
467
|
+
process.chdir(originalCwd);
|
|
468
|
+
if (originalHome === undefined) delete process.env.HOUNDSMAN_HOME;
|
|
469
|
+
else process.env.HOUNDSMAN_HOME = originalHome;
|
|
470
|
+
repo.cleanup();
|
|
471
|
+
}
|
|
472
|
+
}, 30_000);
|
|
473
|
+
});
|