@payconductor/houndsman 0.11.0 → 0.13.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 +14 -13
- package/package.json +1 -1
- package/src/cli/commands/doctor-command.ts +22 -0
- package/src/cli/commands/hunt-command.ts +440 -29
- package/src/cli/commands/review-command.ts +20 -2
- package/src/cli/help.ts +16 -8
- package/src/cli/index.ts +15 -2
- package/src/cli/progress.ts +45 -0
- package/src/core/ai/ai-client.ts +3 -7
- package/src/core/ai/ai-sdk-model-provider.ts +43 -24
- package/src/core/ai/features/brief-enricher.ts +0 -1
- package/src/core/ai/features/classifier.ts +0 -1
- package/src/core/ai/features/context-summarizer.ts +0 -1
- package/src/core/ai/features/pre-analyzer.ts +0 -1
- package/src/core/ai/features/technical-summarizer.ts +0 -1
- package/src/core/ai/features/verifier.ts +0 -1
- package/src/core/ai/index.ts +6 -4
- package/src/core/ai/prompts/pre-analysis.ts +3 -2
- package/src/core/ai/schemas.ts +1 -23
- package/src/core/ai/types.ts +4 -15
- package/src/core/ai/usage-budget.ts +1 -1
- package/src/core/ast/diff.ts +37 -2
- package/src/core/ast/service.ts +1 -1
- package/src/core/brief.ts +54 -5
- package/src/core/changed-scope.ts +207 -0
- package/src/core/commands/scaffold.ts +12 -18
- package/src/core/config.ts +30 -15
- package/src/core/defaults.ts +14 -5
- package/src/core/engine/review-scope.ts +131 -17
- package/src/core/engine/review-session.ts +7 -3
- package/src/core/engine/review-workspace.ts +14 -0
- package/src/core/env.ts +14 -0
- package/src/core/hunt/hunt-runner.ts +328 -0
- package/src/core/hunt/index.ts +59 -0
- package/src/core/hunt/maki-install.ts +331 -0
- package/src/core/hunt/maki-model.ts +101 -0
- package/src/core/hunt/maki-prompt.ts +71 -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 +30 -0
- package/src/core/jev/jev-detector.ts +7 -2
- package/src/core/logging/index.ts +3 -1
- package/src/core/logging/logger.ts +22 -3
- package/src/core/logging/progress.ts +282 -0
- package/src/core/orchestrator.ts +173 -59
- package/src/core/project-config.ts +34 -24
- package/src/core/registry-check.ts +2 -12
- package/src/core/rule-text.ts +3 -55
- package/src/core/services/git-repository.ts +8 -0
- package/src/core/storage/types.ts +1 -1
- package/src/core/types.ts +3 -14
- package/src/index.ts +74 -3
- package/src/server/client/utils/constants.ts +3 -6
- package/src/server/client/utils/types.ts +0 -1
- package/src/skill/OUTPUT-FORMAT.md +5 -0
- package/src/skill/README.md +14 -1
- package/src/skill/SETUP.md +22 -2
- package/src/skill/SKILL.md +4 -0
- package/src/skill/fix.md +1 -1
- package/src/skill/hunt.md +38 -6
- package/src/tests/__snapshots__/baseline.test.ts.snap +69 -32
- package/src/tests/ai-core.test.ts +38 -1
- package/src/tests/changed-scope.test.ts +197 -0
- package/src/tests/cli.test.ts +1 -0
- package/src/tests/fallback.test.ts +1 -96
- package/src/tests/fixtures/fake-model-provider.ts +0 -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 +467 -0
- package/src/tests/progress.test.ts +117 -0
- package/src/tests/review-scope.test.ts +95 -0
- package/src/tests/worktree.test.ts +1 -3
- package/src/harness/README.md +0 -15
- package/src/harness/agent/agent-context.ts +0 -33
- package/src/harness/agent/agent-errors.ts +0 -30
- package/src/harness/agent/agent-factory.ts +0 -46
- package/src/harness/agent/agent-reporter.ts +0 -34
- package/src/harness/agent/agent-result.ts +0 -16
- package/src/harness/agent/agent-runner.ts +0 -32
- package/src/harness/agent/agent-scheduler.ts +0 -37
- package/src/harness/agent/review-agent.ts +0 -401
- package/src/harness/config/harness-config.ts +0 -89
- package/src/harness/hunt/hunt-harness.ts +0 -83
- package/src/harness/hunt/hunt-options.ts +0 -92
- package/src/harness/hunt/hunt-result.ts +0 -17
- package/src/harness/hunt/hunt-session.ts +0 -359
- package/src/harness/hunt/review-planner.ts +0 -98
- package/src/harness/index.ts +0 -36
- package/src/harness/investigation/agent-state.ts +0 -134
- package/src/harness/investigation/evidence-verifier.ts +0 -129
- package/src/harness/investigation/evidence.ts +0 -110
- package/src/harness/investigation/finding-critic.ts +0 -96
- package/src/harness/investigation/investigation-planner.ts +0 -118
- package/src/harness/investigation/investigation-task.ts +0 -10
- package/src/harness/investigation/review-journal.ts +0 -87
- package/src/harness/investigation/rule-analyzer.ts +0 -178
- package/src/harness/investigation/rule-signals.ts +0 -110
- package/src/harness/prompts/agent-schemas.ts +0 -57
- package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
- package/src/harness/prompts/states/conclude-prompt.ts +0 -16
- package/src/harness/prompts/states/investigate-prompt.ts +0 -37
- package/src/harness/prompts/states/orient-prompt.ts +0 -15
- package/src/harness/prompts/states/verify-prompt.ts +0 -22
- package/src/harness/tools/agent-tool.ts +0 -102
- package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
- package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
- package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
- package/src/harness/tools/builtin/find-references-tool.ts +0 -66
- package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
- package/src/harness/tools/builtin/helpers.ts +0 -115
- package/src/harness/tools/builtin/impact-tool.ts +0 -48
- package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
- package/src/harness/tools/builtin/index.ts +0 -59
- package/src/harness/tools/builtin/list-files-tool.ts +0 -85
- package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
- package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
- package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
- package/src/harness/tools/builtin/search-code-tool.ts +0 -65
- package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
- package/src/harness/tools/path-guard.ts +0 -90
- package/src/harness/tools/result-limiter.ts +0 -49
- package/src/harness/tools/secret-redactor.ts +0 -54
- package/src/harness/tools/tool-cache.ts +0 -50
- package/src/harness/tools/tool-call-budget.ts +0 -60
- package/src/harness/tools/tool-context.ts +0 -5
- package/src/harness/tools/tool-middleware.ts +0 -105
- package/src/harness/tools/tool-policy.ts +0 -51
- package/src/harness/tools/tool-registry.ts +0 -112
- package/src/harness/tools/tool-result.ts +0 -15
- package/src/harness/tools/toolbox.ts +0 -36
- package/src/harness/validation/finding-validator.ts +0 -119
- package/src/tests/fixtures/fake-agent-tools.ts +0 -69
- package/src/tests/fixtures/fake-investigation.ts +0 -64
- package/src/tests/harness-agent.test.ts +0 -141
- package/src/tests/harness-investigation.test.ts +0 -206
- package/src/tests/harness-tools.test.ts +0 -148
- package/src/tests/hunt-command.test.ts +0 -212
- package/src/tests/rule-analyzer.test.ts +0 -61
- package/src/tests/tool-contracts.test.ts +0 -82
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escopo do diff — âncoras, símbolos alterados e fallback nos hunks.
|
|
3
|
+
*
|
|
4
|
+
* Congela a regra que impede o watchdog de reportar código pré-existente: só as
|
|
5
|
+
* faixas tocadas pela revisão (símbolo folha do AST ou hunk) são revisáveis.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { changedAnchors } from "../core/ast/diff";
|
|
10
|
+
import type { AstAnalysis, AstFacts, AstSymbol } from "../core/ast/types";
|
|
11
|
+
import {
|
|
12
|
+
buildChangedScope,
|
|
13
|
+
formatRanges,
|
|
14
|
+
formatSymbols,
|
|
15
|
+
isLineInScope,
|
|
16
|
+
mergeRanges,
|
|
17
|
+
summarizeScope,
|
|
18
|
+
} from "../core/changed-scope";
|
|
19
|
+
import type { DiffSource } from "../core/diff-source";
|
|
20
|
+
|
|
21
|
+
/** Cabecalho padrão de um patch de arquivo. */
|
|
22
|
+
function patch(body: string): string {
|
|
23
|
+
return `diff --git a/src/a.ts b/src/a.ts\n--- a/src/a.ts\n+++ b/src/a.ts\n${body}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function diffOf(patchText: string): DiffSource {
|
|
27
|
+
return {
|
|
28
|
+
byFile: new Map([["src/a.ts", patchText]]),
|
|
29
|
+
churn: new Map(),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function symbol(
|
|
34
|
+
overrides: Partial<AstSymbol> & Pick<AstSymbol, "name" | "kind">,
|
|
35
|
+
): AstSymbol {
|
|
36
|
+
return {
|
|
37
|
+
exported: false,
|
|
38
|
+
startLine: 1,
|
|
39
|
+
endLine: 1,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function analysis(facts: AstFacts): AstAnalysis {
|
|
45
|
+
return { facts };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const FACTS: AstFacts = {
|
|
49
|
+
language: "typescript",
|
|
50
|
+
imports: [],
|
|
51
|
+
exports: [],
|
|
52
|
+
decorators: [],
|
|
53
|
+
hasSyntaxErrors: false,
|
|
54
|
+
symbols: [
|
|
55
|
+
symbol({ name: "Outer", kind: "class", startLine: 1, endLine: 20 }),
|
|
56
|
+
symbol({ name: "Outer.inner", kind: "method", startLine: 5, endLine: 10 }),
|
|
57
|
+
symbol({ name: "top", kind: "function", startLine: 25, endLine: 30 }),
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
describe("changedAnchors", () => {
|
|
62
|
+
test("adições e remoções apontam para o lado novo", () => {
|
|
63
|
+
const anchors = changedAnchors(
|
|
64
|
+
patch(
|
|
65
|
+
"@@ -10,3 +10,4 @@\n const before = 1;\n-const old = 2;\n+const current = 2;\n+const extra = 3;\n const after = 4;\n",
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
expect([...anchors.added].sort((a, b) => a - b)).toEqual([11, 12]);
|
|
70
|
+
expect([...anchors.deletedAt]).toEqual([11]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("remoção pura ancora no ponto do arquivo (sem consumir linha nova)", () => {
|
|
74
|
+
const anchors = changedAnchors(patch("@@ -10,2 +10,0 @@\n-a\n-b\n"));
|
|
75
|
+
|
|
76
|
+
expect([...anchors.added]).toEqual([]);
|
|
77
|
+
expect([...anchors.deletedAt]).toEqual([10]);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("buildChangedScope", () => {
|
|
82
|
+
const entries = [{ path: "src/a.ts", status: "modified" }];
|
|
83
|
+
|
|
84
|
+
test("âncora dentro de símbolo folha vira o intervalo do símbolo", () => {
|
|
85
|
+
const text = patch(
|
|
86
|
+
[
|
|
87
|
+
"@@ -2,0 +3,1 @@",
|
|
88
|
+
"+export const header = 1;",
|
|
89
|
+
"@@ -7,0 +8,1 @@",
|
|
90
|
+
"+ const inside = 1;",
|
|
91
|
+
"@@ -26,0 +27,1 @@",
|
|
92
|
+
"+ return 1;",
|
|
93
|
+
"",
|
|
94
|
+
].join("\n"),
|
|
95
|
+
);
|
|
96
|
+
const scope = buildChangedScope({
|
|
97
|
+
entries,
|
|
98
|
+
diff: diffOf(text),
|
|
99
|
+
ast: new Map([["src/a.ts", analysis(FACTS)]]),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const file = scope["src/a.ts"];
|
|
103
|
+
expect(file?.source).toBe("ast");
|
|
104
|
+
// `inner` (5-10) e `top` (25-30); o container `Outer` não expande.
|
|
105
|
+
expect(file?.symbols.map((item) => item.name)).toEqual(["Outer.inner", "top"]);
|
|
106
|
+
// Cabeçalho da classe (linha 3) fica como faixa avulsa.
|
|
107
|
+
expect(file?.ranges).toEqual([
|
|
108
|
+
{ start: 3, end: 3 },
|
|
109
|
+
{ start: 5, end: 10 },
|
|
110
|
+
{ start: 25, end: 30 },
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("sem AST cai para os hunks do diff", () => {
|
|
115
|
+
const text = patch("@@ -10,0 +11,2 @@\n+a\n+b\n");
|
|
116
|
+
const scope = buildChangedScope({ entries, diff: diffOf(text) });
|
|
117
|
+
|
|
118
|
+
const file = scope["src/a.ts"];
|
|
119
|
+
expect(file?.source).toBe("hunks");
|
|
120
|
+
expect(file?.symbols).toEqual([]);
|
|
121
|
+
expect(file?.ranges).toEqual([{ start: 11, end: 12 }]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("rename/binário sem hunks fica sem faixas", () => {
|
|
125
|
+
const scope = buildChangedScope({
|
|
126
|
+
entries: [{ path: "src/a.ts", status: "renamed" }],
|
|
127
|
+
diff: diffOf(patch("")),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(scope["src/a.ts"]).toEqual({ ranges: [], symbols: [], source: "hunks" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("arquivo deletado nem entra no escopo", () => {
|
|
134
|
+
const scope = buildChangedScope({
|
|
135
|
+
entries: [{ path: "src/a.ts", status: "deleted" }],
|
|
136
|
+
diff: diffOf(patch("@@ -1,1 +0,0 @@\n-x\n")),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(scope["src/a.ts"]).toBeUndefined();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("helpers de escopo", () => {
|
|
144
|
+
test("isLineInScope respeita as faixas", () => {
|
|
145
|
+
const scope = {
|
|
146
|
+
"src/a.ts": {
|
|
147
|
+
ranges: [{ start: 5, end: 10 }],
|
|
148
|
+
symbols: [],
|
|
149
|
+
source: "hunks" as const,
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
expect(isLineInScope(scope, "src/a.ts", 7)).toBe(true);
|
|
154
|
+
expect(isLineInScope(scope, "src/a.ts", 11)).toBe(false);
|
|
155
|
+
expect(isLineInScope(scope, "src/b.ts", 7)).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("mergeRanges une sobrepostos e adjacentes", () => {
|
|
159
|
+
expect(
|
|
160
|
+
mergeRanges([
|
|
161
|
+
{ start: 10, end: 12 },
|
|
162
|
+
{ start: 11, end: 15 },
|
|
163
|
+
{ start: 16, end: 18 },
|
|
164
|
+
{ start: 30, end: 30 },
|
|
165
|
+
]),
|
|
166
|
+
).toEqual([
|
|
167
|
+
{ start: 10, end: 18 },
|
|
168
|
+
{ start: 30, end: 30 },
|
|
169
|
+
]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("formatadores compactos", () => {
|
|
173
|
+
expect(formatRanges([])).toBe("sem linhas alteradas");
|
|
174
|
+
expect(
|
|
175
|
+
formatRanges([
|
|
176
|
+
{ start: 10, end: 14 },
|
|
177
|
+
{ start: 22, end: 22 },
|
|
178
|
+
]),
|
|
179
|
+
).toBe("10-14, 22");
|
|
180
|
+
expect(
|
|
181
|
+
formatSymbols([
|
|
182
|
+
{ name: "foo", kind: "function", startLine: 10, endLine: 14 },
|
|
183
|
+
]),
|
|
184
|
+
).toBe("`foo` (10-14)");
|
|
185
|
+
expect(
|
|
186
|
+
summarizeScope({
|
|
187
|
+
"src/a.ts": {
|
|
188
|
+
ranges: [{ start: 1, end: 2 }],
|
|
189
|
+
symbols: [
|
|
190
|
+
{ name: "foo", kind: "function", startLine: 1, endLine: 2 },
|
|
191
|
+
],
|
|
192
|
+
source: "ast",
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
).toEqual({ files: 1, ranges: 1, symbols: 1 });
|
|
196
|
+
});
|
|
197
|
+
});
|
package/src/tests/cli.test.ts
CHANGED
|
@@ -192,6 +192,7 @@ describe("cli smoke — imports dinâmicos", () => {
|
|
|
192
192
|
const result = runCli(project.cwd, ["doctor", "--no-ping"]);
|
|
193
193
|
expect(result.code).toBe(0);
|
|
194
194
|
expect(result.stdout).toContain("Validando");
|
|
195
|
+
expect(result.stdout).toContain("maki (hunt local)");
|
|
195
196
|
} finally {
|
|
196
197
|
project.cleanup();
|
|
197
198
|
}
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fallback de classificação e findings emergentes (lógica/domínio).
|
|
3
|
-
*/
|
|
1
|
+
/** Fallback de classificação (arquivos sem dono). */
|
|
4
2
|
|
|
5
3
|
import { describe, expect, test } from "bun:test";
|
|
6
4
|
import { defineHoundsmanConfig } from "../core/config";
|
|
7
5
|
import { classifyEntries } from "../core/file-classifier";
|
|
8
6
|
import { checkRegistry } from "../core/registry-check";
|
|
9
7
|
import { defineWatchdog } from "../core/types";
|
|
10
|
-
import { AgentState } from "../harness/investigation/agent-state";
|
|
11
|
-
import { EvidenceVerifier } from "../harness/investigation/evidence-verifier";
|
|
12
|
-
import { FindingCritic } from "../harness/investigation/finding-critic";
|
|
13
|
-
import { FindingValidator } from "../harness/validation/finding-validator";
|
|
14
8
|
|
|
15
9
|
const alpha = defineWatchdog({
|
|
16
10
|
id: "alpha",
|
|
@@ -28,7 +22,6 @@ const fallback = defineWatchdog({
|
|
|
28
22
|
description: "Fallback de lógica/domínio.",
|
|
29
23
|
scope: "arquivos sem dono",
|
|
30
24
|
fallback: true,
|
|
31
|
-
allowEmergentFindings: true,
|
|
32
25
|
rules: [
|
|
33
26
|
{
|
|
34
27
|
severity: "attention",
|
|
@@ -82,91 +75,3 @@ describe("classificação com fallback", () => {
|
|
|
82
75
|
).toBe(true);
|
|
83
76
|
});
|
|
84
77
|
});
|
|
85
|
-
|
|
86
|
-
describe("findings emergentes", () => {
|
|
87
|
-
const file = "a.ts";
|
|
88
|
-
const content = "const x = 1;\n";
|
|
89
|
-
const state = new AgentState();
|
|
90
|
-
const evidence = state.addEvidence({
|
|
91
|
-
file,
|
|
92
|
-
startLine: 1,
|
|
93
|
-
endLine: 1,
|
|
94
|
-
snippet: content.trim(),
|
|
95
|
-
kind: "definition",
|
|
96
|
-
sourceTool: "test",
|
|
97
|
-
confidence: 0.9,
|
|
98
|
-
});
|
|
99
|
-
const draft = {
|
|
100
|
-
rule: "Lógica: a função fazA na verdade fazB",
|
|
101
|
-
severity: "blocking" as const,
|
|
102
|
-
file,
|
|
103
|
-
line: 1,
|
|
104
|
-
message: "comportamento diverge do nome",
|
|
105
|
-
action: "renomear ou corrigir",
|
|
106
|
-
evidenceIds: [evidence.id],
|
|
107
|
-
confidence: 0.9,
|
|
108
|
-
};
|
|
109
|
-
const verifier = new EvidenceVerifier({
|
|
110
|
-
allowedFiles: [file],
|
|
111
|
-
readFile: (path) => (path === file ? content : null),
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
test("crítico aceita e limita a severidade quando habilitado", () => {
|
|
115
|
-
const critic = new FindingCritic({
|
|
116
|
-
knownRules: new Set([fallback.rules[0]?.text ?? ""]),
|
|
117
|
-
verifier,
|
|
118
|
-
allowEmergentFindings: true,
|
|
119
|
-
});
|
|
120
|
-
const decision = critic.reviewOne(draft, state);
|
|
121
|
-
expect(decision.verdict).toBe("keep");
|
|
122
|
-
expect(decision.draft.severity).toBe("attention");
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("crítico descarta quando o opt-in está desligado", () => {
|
|
126
|
-
const critic = new FindingCritic({
|
|
127
|
-
knownRules: new Set([fallback.rules[0]?.text ?? ""]),
|
|
128
|
-
verifier,
|
|
129
|
-
});
|
|
130
|
-
const decision = critic.reviewOne(draft, state);
|
|
131
|
-
expect(decision.verdict).toBe("drop");
|
|
132
|
-
expect(decision.reason).toContain("desabilitado");
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("validador espelha a mesma regra", () => {
|
|
136
|
-
const report = {
|
|
137
|
-
watchdogId: "default",
|
|
138
|
-
reviewedFiles: [file],
|
|
139
|
-
skipped: [],
|
|
140
|
-
findings: [draft],
|
|
141
|
-
};
|
|
142
|
-
const allowed = new FindingValidator({
|
|
143
|
-
watchdogId: "default",
|
|
144
|
-
knownRules: new Set([fallback.rules[0]?.text ?? ""]),
|
|
145
|
-
allowedFiles: new Set([file]),
|
|
146
|
-
overflow: new Set(),
|
|
147
|
-
readFile: (path) => (path === file ? content : null),
|
|
148
|
-
allowEmergentFindings: true,
|
|
149
|
-
}).validate(report);
|
|
150
|
-
expect(allowed.report.findings[0]?.severity).toBe("attention");
|
|
151
|
-
expect(allowed.dropped).toHaveLength(0);
|
|
152
|
-
|
|
153
|
-
const denied = new FindingValidator({
|
|
154
|
-
watchdogId: "default",
|
|
155
|
-
knownRules: new Set([fallback.rules[0]?.text ?? ""]),
|
|
156
|
-
allowedFiles: new Set([file]),
|
|
157
|
-
overflow: new Set(),
|
|
158
|
-
readFile: (path) => (path === file ? content : null),
|
|
159
|
-
}).validate(report);
|
|
160
|
-
expect(denied.report.findings).toHaveLength(0);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("finding emergente sem descrição é descartado", () => {
|
|
164
|
-
const critic = new FindingCritic({
|
|
165
|
-
knownRules: new Set([fallback.rules[0]?.text ?? ""]),
|
|
166
|
-
verifier,
|
|
167
|
-
allowEmergentFindings: true,
|
|
168
|
-
});
|
|
169
|
-
const decision = critic.reviewOne({ ...draft, rule: "Lógica:" }, state);
|
|
170
|
-
expect(decision.verdict).toBe("drop");
|
|
171
|
-
});
|
|
172
|
-
});
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smoke test do comando `hunt` (harness local).
|
|
3
|
+
*
|
|
4
|
+
* Não executa o maki real: cobre help, doctor e os erros acionáveis antes do
|
|
5
|
+
* prepare. A skill `/houndsman hunt` continua coberta pelos testes da camada.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
const CLI = join(import.meta.dir, "..", "cli", "index.ts");
|
|
14
|
+
|
|
15
|
+
const CONFIG = (hunt = "") => `export default {
|
|
16
|
+
projectName: "Hunt Smoke",
|
|
17
|
+
storage: { provider: "sqlite", path: "./houndsman.db" },
|
|
18
|
+
watchdogs: {
|
|
19
|
+
alpha: {
|
|
20
|
+
id: "alpha",
|
|
21
|
+
name: "Alpha",
|
|
22
|
+
description: "Watchdog de smoke.",
|
|
23
|
+
scope: "src/**",
|
|
24
|
+
classify: [{ value: "^src/" }],
|
|
25
|
+
rules: [{ severity: "attention", text: "Evitar any." }],
|
|
26
|
+
contextFiles: [],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
exclusions: [],
|
|
30
|
+
docMappings: [],
|
|
31
|
+
${hunt}
|
|
32
|
+
};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
type Project = { cwd: string; home: string; cleanup: () => void };
|
|
36
|
+
|
|
37
|
+
function makeProject(hunt = ""): Project {
|
|
38
|
+
const cwd = mkdtempSync(join(tmpdir(), "houndsman-hunt-cli-"));
|
|
39
|
+
const home = mkdtempSync(join(tmpdir(), "houndsman-hunt-home-"));
|
|
40
|
+
writeFileSync(join(cwd, "houndsman.config.ts"), CONFIG(hunt));
|
|
41
|
+
return {
|
|
42
|
+
cwd,
|
|
43
|
+
home,
|
|
44
|
+
cleanup: () => {
|
|
45
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
46
|
+
rmSync(home, { recursive: true, force: true });
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type CliResult = { code: number; stdout: string; stderr: string };
|
|
52
|
+
|
|
53
|
+
function runCli(
|
|
54
|
+
cwd: string,
|
|
55
|
+
args: string[],
|
|
56
|
+
env: Record<string, string> = {},
|
|
57
|
+
): CliResult {
|
|
58
|
+
const proc = Bun.spawnSync(["bun", CLI, ...args], {
|
|
59
|
+
cwd,
|
|
60
|
+
env: { ...process.env, NO_COLOR: "1", ...env },
|
|
61
|
+
stdout: "pipe",
|
|
62
|
+
stderr: "pipe",
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
code: proc.exitCode ?? -1,
|
|
66
|
+
stdout: proc.stdout.toString(),
|
|
67
|
+
stderr: proc.stderr.toString(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("hunt — help e diagnóstico", () => {
|
|
72
|
+
test("--help distingue a skill do comando CLI", () => {
|
|
73
|
+
const result = runCli(process.cwd(), ["hunt", "--help"]);
|
|
74
|
+
expect(result.code).toBe(0);
|
|
75
|
+
expect(result.stdout).toContain("/houndsman hunt");
|
|
76
|
+
expect(result.stdout).toContain("houndsman hunt install");
|
|
77
|
+
expect(result.stdout).toContain("--allow-partial");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("doctor não falha sem maki e trata o harness como opcional", () => {
|
|
81
|
+
const project = makeProject();
|
|
82
|
+
try {
|
|
83
|
+
const result = runCli(project.cwd, ["hunt", "doctor"], {
|
|
84
|
+
HOUNDSMAN_HOME: project.home,
|
|
85
|
+
HOUNDSMAN_MAKI_PATH: join(project.cwd, "maki-inexistente"),
|
|
86
|
+
});
|
|
87
|
+
expect(result.code).toBe(0);
|
|
88
|
+
expect(result.stdout).toContain("Harness local");
|
|
89
|
+
expect(result.stdout).toContain("modelo:");
|
|
90
|
+
expect(result.stdout).toContain("harness:");
|
|
91
|
+
} finally {
|
|
92
|
+
project.cleanup();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("doctor funciona sem projeto configurado (diagnóstico global)", () => {
|
|
97
|
+
const cwd = mkdtempSync(join(tmpdir(), "houndsman-hunt-nocfg-"));
|
|
98
|
+
const home = mkdtempSync(join(tmpdir(), "houndsman-hunt-home-"));
|
|
99
|
+
try {
|
|
100
|
+
const result = runCli(cwd, ["hunt", "doctor"], {
|
|
101
|
+
HOUNDSMAN_HOME: home,
|
|
102
|
+
HOUNDSMAN_MAKI_PATH: join(cwd, "maki-inexistente"),
|
|
103
|
+
});
|
|
104
|
+
expect(result.code).toBe(0);
|
|
105
|
+
expect(result.stdout).toContain("Harness local");
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(cwd, { recursive: true, force: true });
|
|
108
|
+
rmSync(home, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("hunt — erros antes do prepare", () => {
|
|
114
|
+
test("sem binário, orienta a instalar (e não toca na review)", () => {
|
|
115
|
+
const project = makeProject();
|
|
116
|
+
try {
|
|
117
|
+
const result = runCli(project.cwd, ["hunt", "--branch", "main"], {
|
|
118
|
+
HOUNDSMAN_HOME: project.home,
|
|
119
|
+
HOUNDSMAN_MAKI_PATH: join(project.cwd, "maki-inexistente"),
|
|
120
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY: "sk-test",
|
|
121
|
+
});
|
|
122
|
+
expect(result.code).toBe(1);
|
|
123
|
+
expect(result.stderr).toContain("hunt install");
|
|
124
|
+
} finally {
|
|
125
|
+
project.cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("sem credencial, aponta a env correta", () => {
|
|
130
|
+
const project = makeProject();
|
|
131
|
+
try {
|
|
132
|
+
const env = { ...process.env };
|
|
133
|
+
delete env.HOUNDSMAN_DEEP_SEEK_API_KEY;
|
|
134
|
+
const proc = Bun.spawnSync(["bun", CLI, "hunt", "--branch", "main"], {
|
|
135
|
+
cwd: project.cwd,
|
|
136
|
+
env: {
|
|
137
|
+
...env,
|
|
138
|
+
NO_COLOR: "1",
|
|
139
|
+
HOUNDSMAN_HOME: project.home,
|
|
140
|
+
HOUNDSMAN_MAKI_PATH: join(project.cwd, "maki-inexistente"),
|
|
141
|
+
},
|
|
142
|
+
stdout: "pipe",
|
|
143
|
+
stderr: "pipe",
|
|
144
|
+
});
|
|
145
|
+
expect(proc.exitCode).toBe(1);
|
|
146
|
+
expect(proc.stderr.toString()).toContain("HOUNDSMAN_DEEP_SEEK_API_KEY");
|
|
147
|
+
} finally {
|
|
148
|
+
project.cleanup();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("respeita hunt.enabled: false", () => {
|
|
153
|
+
const project = makeProject("hunt: { enabled: false },");
|
|
154
|
+
try {
|
|
155
|
+
const result = runCli(project.cwd, ["hunt"], {
|
|
156
|
+
HOUNDSMAN_HOME: project.home,
|
|
157
|
+
HOUNDSMAN_DEEP_SEEK_API_KEY: "sk-test",
|
|
158
|
+
});
|
|
159
|
+
expect(result.code).toBe(1);
|
|
160
|
+
expect(result.stderr).toContain("desligado");
|
|
161
|
+
} finally {
|
|
162
|
+
project.cleanup();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("rejeita --finalize (o hunt já finaliza)", () => {
|
|
167
|
+
const project = makeProject();
|
|
168
|
+
try {
|
|
169
|
+
const result = runCli(project.cwd, ["hunt", "--finalize"], {
|
|
170
|
+
HOUNDSMAN_HOME: project.home,
|
|
171
|
+
});
|
|
172
|
+
expect(result.code).toBe(1);
|
|
173
|
+
expect(result.stderr).toContain("já finaliza");
|
|
174
|
+
} finally {
|
|
175
|
+
project.cleanup();
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("subcomando desconhecido mostra o help", () => {
|
|
180
|
+
const result = runCli(process.cwd(), ["hunt", "foo"]);
|
|
181
|
+
expect(result.code).toBe(1);
|
|
182
|
+
expect(result.stderr).toContain("desconhecido");
|
|
183
|
+
expect(result.stdout).toContain("houndsman hunt install");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instalação local do maki (sem rede).
|
|
3
|
+
*
|
|
4
|
+
* Valida a resolução de asset/versão/checksum e a instalação a partir de um
|
|
5
|
+
* arquivo local (`--from`), além do ponteiro gerenciado e do uninstall.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
existsSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
mkdtempSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import {
|
|
20
|
+
installMaki,
|
|
21
|
+
makiCurrentFile,
|
|
22
|
+
makiInstallDir,
|
|
23
|
+
makiSourceLabel,
|
|
24
|
+
readMakiInstall,
|
|
25
|
+
resolveMakiBinary,
|
|
26
|
+
uninstallMaki,
|
|
27
|
+
} from "../core/hunt/maki-install";
|
|
28
|
+
import {
|
|
29
|
+
effectiveMakiVersion,
|
|
30
|
+
makiAssetFor,
|
|
31
|
+
makiBinaryName,
|
|
32
|
+
makiChecksumsUrl,
|
|
33
|
+
makiDownloadUrl,
|
|
34
|
+
parseChecksums,
|
|
35
|
+
} from "../core/hunt/maki-release";
|
|
36
|
+
|
|
37
|
+
const original = {
|
|
38
|
+
home: process.env.HOUNDSMAN_HOME,
|
|
39
|
+
makiPath: process.env.HOUNDSMAN_MAKI_PATH,
|
|
40
|
+
makiVersion: process.env.HOUNDSMAN_MAKI_VERSION,
|
|
41
|
+
};
|
|
42
|
+
const dirs: string[] = [];
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
restoreEnv("HOUNDSMAN_HOME", original.home);
|
|
46
|
+
restoreEnv("HOUNDSMAN_MAKI_PATH", original.makiPath);
|
|
47
|
+
restoreEnv("HOUNDSMAN_MAKI_VERSION", original.makiVersion);
|
|
48
|
+
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function restoreEnv(name: string, value: string | undefined): void {
|
|
52
|
+
if (value === undefined) delete process.env[name];
|
|
53
|
+
else process.env[name] = value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tempDir(prefix: string): string {
|
|
57
|
+
const dir = mkdtempSync(join(tmpdir(), prefix));
|
|
58
|
+
dirs.push(dir);
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("maki-release", () => {
|
|
63
|
+
test("resolve asset por plataforma/arquitetura", () => {
|
|
64
|
+
expect(makiAssetFor("win32", "x64", "0.6.5")?.name).toBe(
|
|
65
|
+
"maki-v0.6.5-x86_64-pc-windows-msvc.zip",
|
|
66
|
+
);
|
|
67
|
+
expect(makiAssetFor("darwin", "arm64", "v0.6.5")?.name).toBe(
|
|
68
|
+
"maki-v0.6.5-aarch64-apple-darwin.tar.gz",
|
|
69
|
+
);
|
|
70
|
+
expect(makiAssetFor("linux", "x64", "0.6.5")?.name).toBe(
|
|
71
|
+
"maki-v0.6.5-x86_64-unknown-linux-musl.tar.gz",
|
|
72
|
+
);
|
|
73
|
+
expect(makiAssetFor("solaris", "x64", "0.6.5")).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("versão normaliza v e aceita override por env", () => {
|
|
77
|
+
expect(effectiveMakiVersion("v1.2.3")).toBe("1.2.3");
|
|
78
|
+
process.env.HOUNDSMAN_MAKI_VERSION = "v2.0.0";
|
|
79
|
+
expect(effectiveMakiVersion()).toBe("2.0.0");
|
|
80
|
+
expect(() => effectiveMakiVersion("../../x")).toThrow("Versão do maki inválida");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("URLs e parse de sha256sums", () => {
|
|
84
|
+
expect(makiDownloadUrl("0.6.5", "maki.tar.gz")).toContain(
|
|
85
|
+
"/releases/download/v0.6.5/maki.tar.gz",
|
|
86
|
+
);
|
|
87
|
+
expect(makiChecksumsUrl("0.6.5").endsWith("/v0.6.5/sha256sums.txt")).toBe(true);
|
|
88
|
+
const checksums = parseChecksums(
|
|
89
|
+
[
|
|
90
|
+
`${"a".repeat(64)} maki-a.tar.gz`,
|
|
91
|
+
`${"b".repeat(64)} *maki-b.zip`,
|
|
92
|
+
"linha inválida",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
);
|
|
95
|
+
expect(checksums.get("maki-a.tar.gz")).toBe("a".repeat(64));
|
|
96
|
+
expect(checksums.get("maki-b.zip")).toBe("b".repeat(64));
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("resolução do binário", () => {
|
|
101
|
+
test("HOUNDSMAN_MAKI_PATH tem precedência", () => {
|
|
102
|
+
const root = tempDir("houndsman-maki-resolve-");
|
|
103
|
+
const binary = join(root, "maki-custom");
|
|
104
|
+
writeFileSync(binary, "fake");
|
|
105
|
+
process.env.HOUNDSMAN_MAKI_PATH = binary;
|
|
106
|
+
const resolved = resolveMakiBinary();
|
|
107
|
+
expect(resolved?.path).toBe(binary);
|
|
108
|
+
expect(resolved?.source).toBe("env");
|
|
109
|
+
expect(makiSourceLabel("env")).toBe("HOUNDSMAN_MAKI_PATH");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("installMaki (arquivo local)", () => {
|
|
114
|
+
const hasTar = Boolean(Bun.which("tar"));
|
|
115
|
+
|
|
116
|
+
test.skipIf(!hasTar)("extrai, grava o ponteiro e remove", async () => {
|
|
117
|
+
const home = tempDir("houndsman-maki-home-");
|
|
118
|
+
process.env.HOUNDSMAN_HOME = home;
|
|
119
|
+
|
|
120
|
+
const src = tempDir("houndsman-maki-src-");
|
|
121
|
+
const name = makiBinaryName();
|
|
122
|
+
mkdirSync(join(src, "nested"), { recursive: true });
|
|
123
|
+
writeFileSync(join(src, "nested", name), "#!/bin/sh\necho fake\n");
|
|
124
|
+
const archive = join(tempDir("houndsman-maki-archive-"), "maki.tar.gz");
|
|
125
|
+
const tar = Bun.spawnSync(["tar", "-czf", archive, "-C", src, "."], {
|
|
126
|
+
stdout: "pipe",
|
|
127
|
+
stderr: "pipe",
|
|
128
|
+
});
|
|
129
|
+
expect(tar.exitCode).toBe(0);
|
|
130
|
+
|
|
131
|
+
const first = await installMaki({ version: "9.9.9", from: archive });
|
|
132
|
+
expect(first.skipped).toBe(false);
|
|
133
|
+
expect(existsSync(first.path)).toBe(true);
|
|
134
|
+
expect(first.path).toBe(join(makiInstallDir("9.9.9"), name));
|
|
135
|
+
expect(readFileSync(first.path, "utf8")).toContain("echo fake");
|
|
136
|
+
|
|
137
|
+
const manifest = readMakiInstall();
|
|
138
|
+
expect(manifest?.version).toBe("9.9.9");
|
|
139
|
+
expect(manifest?.path).toBe(first.path);
|
|
140
|
+
expect(existsSync(makiCurrentFile())).toBe(true);
|
|
141
|
+
|
|
142
|
+
const resolved = resolveMakiBinary();
|
|
143
|
+
expect(resolved?.source).toBe("managed");
|
|
144
|
+
expect(resolved?.version).toBe("9.9.9");
|
|
145
|
+
|
|
146
|
+
const second = await installMaki({ version: "9.9.9", from: archive });
|
|
147
|
+
expect(second.skipped).toBe(true);
|
|
148
|
+
|
|
149
|
+
const removed = uninstallMaki();
|
|
150
|
+
expect(removed).toBe(join(home, "tools", "maki"));
|
|
151
|
+
expect(existsSync(join(home, "tools", "maki"))).toBe(false);
|
|
152
|
+
});
|
|
153
|
+
});
|