@payconductor/houndsman 0.11.0 → 0.12.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.
Files changed (118) hide show
  1. package/README.md +4 -19
  2. package/package.json +1 -1
  3. package/src/cli/help.ts +0 -7
  4. package/src/cli/index.ts +0 -2
  5. package/src/core/ai/ai-client.ts +3 -7
  6. package/src/core/ai/ai-sdk-model-provider.ts +4 -24
  7. package/src/core/ai/features/brief-enricher.ts +0 -1
  8. package/src/core/ai/features/classifier.ts +0 -1
  9. package/src/core/ai/features/context-summarizer.ts +0 -1
  10. package/src/core/ai/features/pre-analyzer.ts +0 -1
  11. package/src/core/ai/features/technical-summarizer.ts +0 -1
  12. package/src/core/ai/features/verifier.ts +0 -1
  13. package/src/core/ai/index.ts +0 -3
  14. package/src/core/ai/prompts/pre-analysis.ts +3 -2
  15. package/src/core/ai/schemas.ts +1 -23
  16. package/src/core/ai/types.ts +2 -15
  17. package/src/core/ai/usage-budget.ts +1 -1
  18. package/src/core/ast/diff.ts +37 -2
  19. package/src/core/ast/service.ts +1 -1
  20. package/src/core/brief.ts +54 -5
  21. package/src/core/changed-scope.ts +207 -0
  22. package/src/core/commands/scaffold.ts +1 -18
  23. package/src/core/config.ts +2 -15
  24. package/src/core/defaults.ts +0 -6
  25. package/src/core/engine/review-scope.ts +55 -8
  26. package/src/core/jev/jev-detector.ts +7 -2
  27. package/src/core/logging/index.ts +1 -1
  28. package/src/core/logging/logger.ts +3 -3
  29. package/src/core/orchestrator.ts +19 -14
  30. package/src/core/project-config.ts +0 -28
  31. package/src/core/registry-check.ts +2 -12
  32. package/src/core/rule-text.ts +3 -55
  33. package/src/core/services/git-repository.ts +8 -0
  34. package/src/core/storage/types.ts +0 -1
  35. package/src/core/types.ts +3 -14
  36. package/src/index.ts +25 -3
  37. package/src/server/client/utils/constants.ts +3 -6
  38. package/src/server/client/utils/types.ts +0 -1
  39. package/src/skill/OUTPUT-FORMAT.md +5 -0
  40. package/src/skill/README.md +2 -1
  41. package/src/skill/SETUP.md +1 -1
  42. package/src/skill/fix.md +1 -1
  43. package/src/skill/hunt.md +2 -6
  44. package/src/tests/__snapshots__/baseline.test.ts.snap +50 -32
  45. package/src/tests/changed-scope.test.ts +197 -0
  46. package/src/tests/fallback.test.ts +1 -96
  47. package/src/tests/fixtures/fake-model-provider.ts +0 -1
  48. package/src/tests/review-scope.test.ts +33 -0
  49. package/src/tests/worktree.test.ts +1 -3
  50. package/src/cli/commands/hunt-command.ts +0 -51
  51. package/src/harness/README.md +0 -15
  52. package/src/harness/agent/agent-context.ts +0 -33
  53. package/src/harness/agent/agent-errors.ts +0 -30
  54. package/src/harness/agent/agent-factory.ts +0 -46
  55. package/src/harness/agent/agent-reporter.ts +0 -34
  56. package/src/harness/agent/agent-result.ts +0 -16
  57. package/src/harness/agent/agent-runner.ts +0 -32
  58. package/src/harness/agent/agent-scheduler.ts +0 -37
  59. package/src/harness/agent/review-agent.ts +0 -401
  60. package/src/harness/config/harness-config.ts +0 -89
  61. package/src/harness/hunt/hunt-harness.ts +0 -83
  62. package/src/harness/hunt/hunt-options.ts +0 -92
  63. package/src/harness/hunt/hunt-result.ts +0 -17
  64. package/src/harness/hunt/hunt-session.ts +0 -359
  65. package/src/harness/hunt/review-planner.ts +0 -98
  66. package/src/harness/index.ts +0 -36
  67. package/src/harness/investigation/agent-state.ts +0 -134
  68. package/src/harness/investigation/evidence-verifier.ts +0 -129
  69. package/src/harness/investigation/evidence.ts +0 -110
  70. package/src/harness/investigation/finding-critic.ts +0 -96
  71. package/src/harness/investigation/investigation-planner.ts +0 -118
  72. package/src/harness/investigation/investigation-task.ts +0 -10
  73. package/src/harness/investigation/review-journal.ts +0 -87
  74. package/src/harness/investigation/rule-analyzer.ts +0 -178
  75. package/src/harness/investigation/rule-signals.ts +0 -110
  76. package/src/harness/prompts/agent-schemas.ts +0 -57
  77. package/src/harness/prompts/reviewer-prompt-builder.ts +0 -101
  78. package/src/harness/prompts/states/conclude-prompt.ts +0 -16
  79. package/src/harness/prompts/states/investigate-prompt.ts +0 -37
  80. package/src/harness/prompts/states/orient-prompt.ts +0 -15
  81. package/src/harness/prompts/states/verify-prompt.ts +0 -22
  82. package/src/harness/tools/agent-tool.ts +0 -102
  83. package/src/harness/tools/builtin/changed-symbols-tool.ts +0 -48
  84. package/src/harness/tools/builtin/doc-context-tool.ts +0 -74
  85. package/src/harness/tools/builtin/file-outline-tool.ts +0 -66
  86. package/src/harness/tools/builtin/find-references-tool.ts +0 -66
  87. package/src/harness/tools/builtin/find-symbol-tool.ts +0 -91
  88. package/src/harness/tools/builtin/helpers.ts +0 -115
  89. package/src/harness/tools/builtin/impact-tool.ts +0 -48
  90. package/src/harness/tools/builtin/imports-exports-tool.ts +0 -53
  91. package/src/harness/tools/builtin/index.ts +0 -59
  92. package/src/harness/tools/builtin/list-files-tool.ts +0 -85
  93. package/src/harness/tools/builtin/pr-context-tool.ts +0 -41
  94. package/src/harness/tools/builtin/read-file-range-tool.ts +0 -73
  95. package/src/harness/tools/builtin/rule-signals-tool.ts +0 -36
  96. package/src/harness/tools/builtin/search-code-tool.ts +0 -65
  97. package/src/harness/tools/builtin/symbol-context-tool.ts +0 -106
  98. package/src/harness/tools/builtin/tests-for-symbol-tool.ts +0 -61
  99. package/src/harness/tools/path-guard.ts +0 -90
  100. package/src/harness/tools/result-limiter.ts +0 -49
  101. package/src/harness/tools/secret-redactor.ts +0 -54
  102. package/src/harness/tools/tool-cache.ts +0 -50
  103. package/src/harness/tools/tool-call-budget.ts +0 -60
  104. package/src/harness/tools/tool-context.ts +0 -5
  105. package/src/harness/tools/tool-middleware.ts +0 -105
  106. package/src/harness/tools/tool-policy.ts +0 -51
  107. package/src/harness/tools/tool-registry.ts +0 -112
  108. package/src/harness/tools/tool-result.ts +0 -15
  109. package/src/harness/tools/toolbox.ts +0 -36
  110. package/src/harness/validation/finding-validator.ts +0 -119
  111. package/src/tests/fixtures/fake-agent-tools.ts +0 -69
  112. package/src/tests/fixtures/fake-investigation.ts +0 -64
  113. package/src/tests/harness-agent.test.ts +0 -141
  114. package/src/tests/harness-investigation.test.ts +0 -206
  115. package/src/tests/harness-tools.test.ts +0 -148
  116. package/src/tests/hunt-command.test.ts +0 -212
  117. package/src/tests/rule-analyzer.test.ts +0 -61
  118. package/src/tests/tool-contracts.test.ts +0 -82
@@ -1,141 +0,0 @@
1
- /**
2
- * ReviewAgent — máquina de estados com provider fake e tools reais.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { SilentReporter } from "../harness/agent/agent-reporter";
10
- import { ReviewAgent } from "../harness/agent/review-agent";
11
- import { fakeAgentContext, fakeAgentDeps } from "./fixtures/fake-investigation";
12
- import { FakeModelProvider } from "./fixtures/fake-model-provider";
13
-
14
- const RULE = "Nunca usar `any`.";
15
-
16
- const dir = mkdtempSync(join(tmpdir(), "houndsman-agent-"));
17
- mkdirSync(join(dir, "src/alpha"), { recursive: true });
18
- writeFileSync(join(dir, "src/alpha/a.ts"), "const x: any = 1;\nconsole.log(x);\n");
19
- afterAll(() => rmSync(dir, { recursive: true, force: true }));
20
-
21
- /** Provider que investiga com `read_file_range` e devolve um draft. */
22
- function investigatingProvider(options: { withEvidence?: boolean } = {}) {
23
- return new FakeModelProvider(async (request, index) => {
24
- if (index === 0) return { hypotheses: ["uso de any"] };
25
- if (index === 1) return { taskIds: ["rule-0"] };
26
- const tool = request.tools?.read_file_range;
27
- if (!tool) throw new Error("tool read_file_range ausente");
28
- const raw = await tool.execute({
29
- path: "src/alpha/a.ts",
30
- startLine: 1,
31
- endLine: 2,
32
- });
33
- const parsed = JSON.parse(raw) as {
34
- evidence?: { id: string }[];
35
- };
36
- const evidenceId = parsed.evidence?.[0]?.id;
37
- return {
38
- findingDrafts: [
39
- {
40
- rule: RULE,
41
- severity: "blocking",
42
- file: "src/alpha/a.ts",
43
- line: 1,
44
- message: "Uso de any.",
45
- action: "Tipar o valor.",
46
- evidenceIds:
47
- options.withEvidence === false || !evidenceId ? [] : [evidenceId],
48
- confidence: 0.9,
49
- },
50
- ],
51
- coverage: [
52
- { rule: RULE, status: "checked" },
53
- { rule: "Validar entrada do usuário.", status: "not_applicable" },
54
- { rule: "Evitar console.log.", status: "checked" },
55
- ],
56
- };
57
- });
58
- }
59
-
60
- function buildAgent(provider: FakeModelProvider): {
61
- agent: ReviewAgent;
62
- reporter: SilentReporter;
63
- } {
64
- const reporter = new SilentReporter();
65
- const deps = fakeAgentDeps(provider, {
66
- context: fakeAgentContext({ repoRoot: dir }),
67
- reporter,
68
- });
69
- return { agent: new ReviewAgent(deps), reporter };
70
- }
71
-
72
- describe("ReviewAgent", () => {
73
- test("ORIENT→PLAN→INVESTIGATE→VERIFY→CONCLUDE com evidência", async () => {
74
- const { agent, reporter } = buildAgent(investigatingProvider());
75
- const result = await agent.run();
76
- expect(result.status).toBe("done");
77
- expect(result.report?.findings).toHaveLength(1);
78
- expect(result.report?.findings[0]?.evidence).toContain("src/alpha/a.ts");
79
- expect(result.report?.reviewedFiles).toContain("src/alpha/a.ts");
80
- const transitions = reporter.journal.transitions.map((item) => item.to);
81
- expect(transitions).toEqual(["PLAN", "INVESTIGATE", "VERIFY", "CONCLUDE"]);
82
- expect(result.state.coverage.size).toBe(3);
83
- });
84
-
85
- test("VERIFY é obrigatório: draft sem evidência é descartado", async () => {
86
- const { agent } = buildAgent(investigatingProvider({ withEvidence: false }));
87
- const result = await agent.run();
88
- expect(result.status).toBe("done");
89
- expect(result.report?.findings).toHaveLength(0);
90
- const coverage = result.state.coverageOf(RULE);
91
- expect(coverage?.status).toBe("checked");
92
- expect(coverage?.reason).toContain("refutado");
93
- });
94
-
95
- test("toda regra termina com cobertura (mesmo sem o modelo cobrir)", async () => {
96
- const provider = new FakeModelProvider((_request, index) => {
97
- if (index === 0) return { hypotheses: [] };
98
- if (index === 1) return { taskIds: [] };
99
- return { findingDrafts: [], coverage: [] };
100
- });
101
- const { agent } = buildAgent(provider);
102
- const result = await agent.run();
103
- expect(result.state.coverage.size).toBe(3);
104
- expect(
105
- [...result.state.coverage.values()].every(
106
- (item) => item.status === "skipped",
107
- ),
108
- ).toBe(true);
109
- });
110
-
111
- test("falha do provider vira status failed", async () => {
112
- const provider = new FakeModelProvider((_request, index) => {
113
- if (index === 0) throw new Error("boom");
114
- if (index === 1) throw new Error("boom");
115
- throw new Error("429 rate limit");
116
- });
117
- const { agent } = buildAgent(provider);
118
- const result = await agent.run();
119
- expect(result.status).toBe("failed");
120
- expect(result.error).toContain("429");
121
- });
122
-
123
- test("repassa providerOptions da config em toda chamada", async () => {
124
- const provider = investigatingProvider();
125
- const reporter = new SilentReporter();
126
- const agent = new ReviewAgent(
127
- fakeAgentDeps(provider, {
128
- context: fakeAgentContext({ repoRoot: dir }),
129
- reporter,
130
- providerOptions: { deepseek: { thinking: { type: "disabled" } } },
131
- }),
132
- );
133
- await agent.run();
134
- expect(provider.requests.length).toBeGreaterThan(0);
135
- for (const request of provider.requests) {
136
- expect(request.providerOptions).toEqual({
137
- deepseek: { thinking: { type: "disabled" } },
138
- });
139
- }
140
- });
141
- });
@@ -1,206 +0,0 @@
1
- /**
2
- * Investigação: planner, evidência, crítico e cobertura.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { AgentState } from "../harness/investigation/agent-state";
10
- import { EvidenceVerifier } from "../harness/investigation/evidence-verifier";
11
- import { FindingCritic } from "../harness/investigation/finding-critic";
12
- import { InvestigationPlanner } from "../harness/investigation/investigation-planner";
13
- import { RuleAnalyzer } from "../harness/investigation/rule-analyzer";
14
-
15
- const RULE = "Nunca usar `any`.";
16
- const analyses = new RuleAnalyzer().analyze([
17
- { severity: "suggestion", text: "Evitar console.log." },
18
- { severity: "blocking", text: RULE },
19
- { severity: "attention", text: "Validar entrada do usuário." },
20
- ]);
21
-
22
- const dir = mkdtempSync(join(tmpdir(), "houndsman-evidence-"));
23
- const file = join(dir, "a.ts");
24
- writeFileSync(file, "const x: any = 1;\nconsole.log(x);\n");
25
- afterAll(() => rmSync(dir, { recursive: true, force: true }));
26
-
27
- function verifier(): EvidenceVerifier {
28
- return new EvidenceVerifier({
29
- allowedFiles: ["a.ts"],
30
- readFile: (path) =>
31
- path === "a.ts" ? "const x: any = 1;\nconsole.log(x);\n" : null,
32
- });
33
- }
34
-
35
- describe("InvestigationPlanner", () => {
36
- test("prioriza por severidade e calcula estimativa", () => {
37
- const plan = new InvestigationPlanner().plan(
38
- { watchdogId: "alpha", files: ["a.ts"], symbols: [] },
39
- analyses,
40
- );
41
- expect(plan.tasks[0]?.rule).toBe(RULE);
42
- expect(plan.tasks[0]?.severity).toBe("blocking");
43
- expect(plan.totalFiles).toBe(1);
44
- expect(plan.estimatedToolCalls).toBeGreaterThan(0);
45
- });
46
-
47
- test("filtra arquivos por hint de path", () => {
48
- const [analysis] = new RuleAnalyzer().analyze([
49
- {
50
- severity: "attention",
51
- text: "Evitar: query no banco em rota.",
52
- hints: { paths: ["^src/alpha/"] },
53
- },
54
- ]);
55
- const plan = new InvestigationPlanner().plan(
56
- {
57
- watchdogId: "alpha",
58
- files: ["src/alpha/a.ts", "src/beta/b.ts"],
59
- symbols: [],
60
- },
61
- analysis ? [analysis] : [],
62
- );
63
- expect(plan.tasks[0]?.files).toEqual(["src/alpha/a.ts"]);
64
- });
65
- });
66
-
67
- describe("EvidenceVerifier", () => {
68
- test("aceita snippet na linha e reancora quando deslocado", () => {
69
- const state = new AgentState();
70
- const exact = state.addEvidence({
71
- file: "a.ts",
72
- startLine: 1,
73
- endLine: 1,
74
- snippet: "const x: any = 1;",
75
- kind: "definition",
76
- sourceTool: "t",
77
- confidence: 0.9,
78
- });
79
- const moved = state.addEvidence({
80
- file: "a.ts",
81
- startLine: 9,
82
- endLine: 9,
83
- snippet: "console.log(x);",
84
- kind: "reference",
85
- sourceTool: "t",
86
- confidence: 0.7,
87
- });
88
- expect(verifier().verify(exact).valid).toBe(true);
89
- const verdict = verifier().verify(moved);
90
- expect(verdict.valid).toBe(true);
91
- expect(verdict.startLine).toBe(2);
92
- });
93
-
94
- test("rejeita arquivo fora do domínio e snippet inexistente", () => {
95
- const state = new AgentState();
96
- const outside = state.addEvidence({
97
- file: "b.ts",
98
- startLine: 1,
99
- endLine: 1,
100
- snippet: "x",
101
- kind: "definition",
102
- sourceTool: "t",
103
- confidence: 1,
104
- });
105
- const missing = state.addEvidence({
106
- file: "a.ts",
107
- startLine: 1,
108
- endLine: 1,
109
- snippet: "não existe",
110
- kind: "definition",
111
- sourceTool: "t",
112
- confidence: 1,
113
- });
114
- expect(verifier().verify(outside).valid).toBe(false);
115
- expect(verifier().verify(missing).valid).toBe(false);
116
- });
117
-
118
- test("draft sem evidência válida é inválido", () => {
119
- const state = new AgentState();
120
- const evidence = state.addEvidence({
121
- file: "a.ts",
122
- startLine: 1,
123
- endLine: 1,
124
- snippet: "const x: any = 1;",
125
- kind: "definition",
126
- sourceTool: "t",
127
- confidence: 0.9,
128
- });
129
- const draft = {
130
- rule: RULE,
131
- severity: "blocking" as const,
132
- file: "a.ts",
133
- line: 1,
134
- message: "m",
135
- action: "a",
136
- evidenceIds: [evidence.id],
137
- confidence: 0.9,
138
- };
139
- expect(verifier().verifyDraft(draft, state).valid).toBe(true);
140
- expect(
141
- verifier().verifyDraft({ ...draft, evidenceIds: ["nope"] }, state).valid,
142
- ).toBe(false);
143
- });
144
- });
145
-
146
- describe("FindingCritic", () => {
147
- const state = new AgentState();
148
- const evidence = state.addEvidence({
149
- file: "a.ts",
150
- startLine: 1,
151
- endLine: 1,
152
- snippet: "const x: any = 1;",
153
- kind: "definition",
154
- sourceTool: "t",
155
- confidence: 0.9,
156
- });
157
- const critic = new FindingCritic({
158
- knownRules: new Set([RULE]),
159
- verifier: verifier(),
160
- });
161
- const draft = {
162
- rule: RULE,
163
- severity: "blocking" as const,
164
- file: "a.ts",
165
- line: 1,
166
- message: "m",
167
- action: "a",
168
- evidenceIds: [evidence.id],
169
- confidence: 0.9,
170
- };
171
-
172
- test("keep com evidência e confiança", () => {
173
- expect(critic.reviewOne(draft, state).verdict).toBe("keep");
174
- });
175
-
176
- test("drop sem evidência, sem regra ou confiança baixa", () => {
177
- expect(critic.reviewOne({ ...draft, evidenceIds: [] }, state).verdict).toBe(
178
- "drop",
179
- );
180
- expect(critic.reviewOne({ ...draft, rule: "inexistente" }, state).verdict).toBe(
181
- "drop",
182
- );
183
- expect(critic.reviewOne({ ...draft, confidence: 0.2 }, state).verdict).toBe(
184
- "drop",
185
- );
186
- });
187
-
188
- test("downgrade quando a confiança não sustenta a severidade", () => {
189
- expect(critic.reviewOne({ ...draft, confidence: 0.5 }, state).verdict).toBe(
190
- "downgrade",
191
- );
192
- });
193
- });
194
-
195
- describe("AgentState", () => {
196
- test("cobertura e hipóteses sem duplicar", () => {
197
- const state = new AgentState();
198
- state.addHypothesis("a");
199
- state.addHypothesis("a");
200
- expect(state.hypotheses).toEqual(["a"]);
201
- state.setCoverage(RULE, "skipped", "motivo");
202
- expect(state.isCovered(RULE)).toBe(true);
203
- state.setCoverage(RULE, "checked");
204
- expect(state.coverageOf(RULE)?.status).toBe("checked");
205
- });
206
- });
@@ -1,148 +0,0 @@
1
- /**
2
- * Tools do harness: path guard, limites, cache, budget e redaction.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { AstService } from "../core/ast/service";
10
- import { Watchdog } from "../core/domain/watchdog";
11
- import { SilentReporter } from "../harness/agent/agent-reporter";
12
- import { AgentState } from "../harness/investigation/agent-state";
13
- import type { ToolContext } from "../harness/tools/agent-tool";
14
- import { BuiltinToolsModule } from "../harness/tools/builtin";
15
- import { PathGuard } from "../harness/tools/path-guard";
16
- import { redactSecrets } from "../harness/tools/secret-redactor";
17
- import { ToolCache } from "../harness/tools/tool-cache";
18
- import { ToolCallBudget } from "../harness/tools/tool-call-budget";
19
- import { ToolMiddleware } from "../harness/tools/tool-middleware";
20
- import { ToolRegistry } from "../harness/tools/tool-registry";
21
- import { EchoTool } from "./fixtures/fake-agent-tools";
22
- import { fakeAgentContext } from "./fixtures/fake-investigation";
23
- import { fakeHoundsmanConfig } from "./fixtures/fake-project";
24
-
25
- const dir = mkdtempSync(join(tmpdir(), "houndsman-tools-"));
26
- mkdirSync(join(dir, "src/alpha"), { recursive: true });
27
- writeFileSync(
28
- join(dir, "src/alpha/a.ts"),
29
- "const x: any = 1;\nconsole.log(x);\nconst y = 2;\n",
30
- );
31
- afterAll(() => rmSync(dir, { recursive: true, force: true }));
32
-
33
- function context(): ToolContext {
34
- const state = new AgentState();
35
- return {
36
- repoRoot: dir,
37
- watchdog: new Watchdog(fakeHoundsmanConfig.watchdogs.alpha),
38
- diff: { byFile: new Map(), churn: new Map() },
39
- ast: new AstService(fakeHoundsmanConfig),
40
- state,
41
- budget: new ToolCallBudget(20, 10),
42
- cache: new ToolCache(),
43
- logger: new SilentReporter(),
44
- agent: fakeAgentContext({ repoRoot: dir }),
45
- rules: new Map(),
46
- };
47
- }
48
-
49
- describe("PathGuard", () => {
50
- test("bloqueia traversal, .git e .env", () => {
51
- const guard = new PathGuard(dir);
52
- expect(() => guard.resolvePath("../segredo")).toThrow();
53
- expect(() => guard.resolvePath(".git/config")).toThrow();
54
- expect(() => guard.resolvePath(".env")).toThrow();
55
- });
56
-
57
- test("aceita caminho interno", () => {
58
- const guard = new PathGuard(dir);
59
- expect(guard.resolvePath("src/alpha/a.ts")).toContain("a.ts");
60
- });
61
- });
62
-
63
- describe("read_file_range", () => {
64
- test("devolve faixa numerada + evidência com id", async () => {
65
- const registry = new ToolRegistry();
66
- registry.registerModule(new BuiltinToolsModule());
67
- const middleware = new ToolMiddleware();
68
- const ctx = context();
69
- const result = await registry.execute(
70
- "read_file_range",
71
- { path: "src/alpha/a.ts", startLine: 1, endLine: 2 },
72
- ctx,
73
- middleware,
74
- );
75
- expect(result.ok).toBe(true);
76
- const data = result.data as { content: string };
77
- expect(data.content).toContain("1\tconst x: any = 1;");
78
- expect(data.content).toContain("2\tconsole.log(x);");
79
- expect(result.evidence?.[0]?.id).toBeTruthy();
80
- expect(ctx.state.evidence.size).toBe(1);
81
- });
82
-
83
- test("caminho fora do repo vira erro estruturado", async () => {
84
- const registry = new ToolRegistry();
85
- registry.registerModule(new BuiltinToolsModule());
86
- const result = await registry.execute(
87
- "read_file_range",
88
- { path: "../x", startLine: 1, endLine: 2 },
89
- context(),
90
- new ToolMiddleware(),
91
- );
92
- expect(result.ok).toBe(false);
93
- expect(result.error).toContain("fora do repositório");
94
- });
95
- });
96
-
97
- describe("ToolRegistry / middleware", () => {
98
- test("nome duplicado falha no registro", () => {
99
- const registry = new ToolRegistry();
100
- registry.registerTool(new EchoTool());
101
- expect(() => registry.registerTool(new EchoTool())).toThrow();
102
- });
103
-
104
- test("resultado em cache não consome budget", async () => {
105
- const registry = new ToolRegistry();
106
- registry.registerTool(new EchoTool());
107
- const middleware = new ToolMiddleware();
108
- const ctx = context();
109
- await registry.execute("echo", { value: "a" }, ctx, middleware);
110
- expect(ctx.budget.total).toBe(1);
111
- await registry.execute("echo", { value: "a" }, ctx, middleware);
112
- expect(ctx.budget.total).toBe(1);
113
- await registry.execute("echo", { value: "b" }, ctx, middleware);
114
- expect(ctx.budget.total).toBe(2);
115
- });
116
-
117
- test("input inválido vira erro (Zod)", async () => {
118
- const registry = new ToolRegistry();
119
- registry.registerTool(new EchoTool());
120
- const result = await registry.execute(
121
- "echo",
122
- { value: 1 },
123
- context(),
124
- new ToolMiddleware(),
125
- );
126
- expect(result.ok).toBe(false);
127
- expect(result.error).toContain("input inválido");
128
- });
129
-
130
- test("budget esgotado interrompe", async () => {
131
- const registry = new ToolRegistry();
132
- registry.registerTool(new EchoTool());
133
- const ctx = context();
134
- ctx.budget = new ToolCallBudget(1, 1);
135
- const middleware = new ToolMiddleware();
136
- await registry.execute("echo", { value: "a" }, ctx, middleware);
137
- const second = await registry.execute("echo", { value: "b" }, ctx, middleware);
138
- expect(second.ok).toBe(false);
139
- expect(second.error).toContain("Orçamento");
140
- });
141
- });
142
-
143
- describe("redaction", () => {
144
- test("remove tokens e chaves", () => {
145
- expect(redactSecrets("token sk-abcdefgh123")).toContain("[REDACTED]");
146
- expect(redactSecrets("api_key = segredo")).toContain("[REDACTED]");
147
- });
148
- });
@@ -1,212 +0,0 @@
1
- /**
2
- * `hunt` — parser de flags, dry-run e fluxo completo com provider fake.
3
- */
4
-
5
- import { afterAll, describe, expect, test } from "bun:test";
6
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
7
- import { tmpdir } from "node:os";
8
- import { join } from "node:path";
9
- import { defineHoundsmanConfig } from "../core/config";
10
- import { HoundsmanEngine } from "../core/engine/houndsman-engine";
11
- import { SilentLogger } from "../core/logging";
12
- import { defineWatchdog } from "../core/types";
13
- import { HuntHarness } from "../harness/hunt/hunt-harness";
14
- import { parseHuntArgs } from "../harness/hunt/hunt-options";
15
- import { FakeModelProvider } from "./fixtures/fake-model-provider";
16
-
17
- const RULE = "Nunca usar `any`.";
18
-
19
- const configBase = defineHoundsmanConfig({
20
- projectName: "Hunt",
21
- watchdogs: {
22
- alpha: defineWatchdog({
23
- id: "alpha",
24
- name: "Alpha",
25
- description: "Watchdog do hunt.",
26
- scope: "src/alpha/**",
27
- classify: [{ value: "^src/alpha/" }],
28
- rules: [{ severity: "blocking", text: RULE }],
29
- contextFiles: [],
30
- }),
31
- },
32
- exclusions: [],
33
- docMappings: [],
34
- });
35
-
36
- describe("parseHuntArgs", () => {
37
- test("flags e defaults", () => {
38
- const options = parseHuntArgs([
39
- "--base",
40
- "origin/main",
41
- "--head",
42
- "abc",
43
- "--watchdog",
44
- "alpha",
45
- "--full",
46
- "--no-ast",
47
- "--resume",
48
- "--allow-partial",
49
- "--dry-run",
50
- "--fail-on-blocking",
51
- "--max-calls",
52
- "7",
53
- "--concurrency",
54
- "3",
55
- "--model",
56
- "m",
57
- ]);
58
- expect(options.base).toBe("origin/main");
59
- expect(options.head).toBe("abc");
60
- expect(options.watchdog).toBe("alpha");
61
- expect(options.full).toBe(true);
62
- expect(options.noAst).toBe(true);
63
- expect(options.noJev).toBe(true);
64
- expect(options.resume).toBe(true);
65
- expect(options.allowPartial).toBe(true);
66
- expect(options.dryRun).toBe(true);
67
- expect(options.failOnBlocking).toBe(true);
68
- expect(options.maxCalls).toBe(7);
69
- expect(options.concurrency).toBe(3);
70
- expect(options.model).toBe("m");
71
- });
72
- });
73
-
74
- // ---------------------------------------------------------------------------
75
- // Repo git temporário
76
- // ---------------------------------------------------------------------------
77
-
78
- const cwd = mkdtempSync(join(tmpdir(), "houndsman-hunt-"));
79
- const originalCwd = process.cwd();
80
- const originalHome = process.env.HOUNDSMAN_HOME;
81
- const originalKey = process.env.HOUNDSMAN_DEEP_SEEK_API_KEY;
82
- let base = "";
83
-
84
- function git(args: string[]): string {
85
- const result = Bun.spawnSync(["git", ...args], {
86
- cwd,
87
- stdout: "pipe",
88
- stderr: "pipe",
89
- });
90
- if (result.exitCode !== 0) {
91
- throw new Error(`git ${args.join(" ")}: ${result.stderr.toString()}`);
92
- }
93
- return result.stdout.toString();
94
- }
95
-
96
- function setupRepo(): void {
97
- git(["init", "-b", "main"]);
98
- git(["config", "user.email", "hunt@example.test"]);
99
- git(["config", "user.name", "Hunt"]);
100
- git(["config", "commit.gpgsign", "false"]);
101
- mkdirSync(join(cwd, "src", "alpha"), { recursive: true });
102
- writeFileSync(join(cwd, "src/alpha/a.ts"), "export const a = 1;\n");
103
- git(["add", "."]);
104
- git(["commit", "-m", "initial"]);
105
- base = git(["rev-parse", "HEAD"]).trim();
106
- writeFileSync(join(cwd, "src/alpha/a.ts"), "export const a: any = 1;\n");
107
- git(["add", "-A"]);
108
- git(["commit", "-m", "change"]);
109
- process.env.HOUNDSMAN_HOME = join(cwd, ".home");
110
- process.chdir(cwd);
111
- }
112
-
113
- function engine(): HoundsmanEngine {
114
- return new HoundsmanEngine({
115
- ...configBase,
116
- storage: { provider: "sqlite", path: join(cwd, "houndsman.db") },
117
- });
118
- }
119
-
120
- setupRepo();
121
- afterAll(() => {
122
- process.chdir(originalCwd);
123
- if (originalHome === undefined) delete process.env.HOUNDSMAN_HOME;
124
- else process.env.HOUNDSMAN_HOME = originalHome;
125
- if (originalKey === undefined) delete process.env.HOUNDSMAN_DEEP_SEEK_API_KEY;
126
- else process.env.HOUNDSMAN_DEEP_SEEK_API_KEY = originalKey;
127
- rmSync(cwd, { recursive: true, force: true });
128
- });
129
-
130
- describe("hunt", () => {
131
- test("dry-run lista watchdogs sem chamar IA", async () => {
132
- const instance = engine();
133
- try {
134
- const harness = await HuntHarness.from(
135
- instance,
136
- parseHuntArgs(["--base", base, "--head", "HEAD", "--dry-run"]),
137
- new SilentLogger(),
138
- );
139
- const result = await harness.run();
140
- expect(result.status).toBe("dry-run");
141
- expect(result.messages.join("\n")).toContain("alpha");
142
- } finally {
143
- instance.close();
144
- }
145
- });
146
-
147
- test("sem chave e sem --dry-run falha com mensagem clara", async () => {
148
- delete process.env.HOUNDSMAN_DEEP_SEEK_API_KEY;
149
- const instance = engine();
150
- try {
151
- await expect(
152
- HuntHarness.from(
153
- instance,
154
- parseHuntArgs(["--base", base, "--head", "HEAD"]),
155
- new SilentLogger(),
156
- ),
157
- ).rejects.toThrow(/hunt exige IA configurada/);
158
- } finally {
159
- instance.close();
160
- }
161
- });
162
-
163
- test("provider fake investiga, grava findings e finaliza", async () => {
164
- const provider = new FakeModelProvider(async (request, index) => {
165
- if (index === 0) return { hypotheses: ["any"] };
166
- if (index === 1) return { taskIds: ["rule-0"] };
167
- const tool = request.tools?.read_file_range;
168
- if (!tool) throw new Error("read_file_range ausente");
169
- const parsed = JSON.parse(
170
- await tool.execute({ path: "src/alpha/a.ts", startLine: 1, endLine: 1 }),
171
- ) as { evidence?: { id: string }[] };
172
- return {
173
- findingDrafts: [
174
- {
175
- rule: RULE,
176
- severity: "blocking",
177
- file: "src/alpha/a.ts",
178
- line: 1,
179
- message: "any detectado",
180
- action: "tipar",
181
- evidenceIds: [parsed.evidence?.[0]?.id].filter(Boolean),
182
- confidence: 0.9,
183
- },
184
- ],
185
- coverage: [{ rule: RULE, status: "checked" }],
186
- };
187
- });
188
- const instance = engine();
189
- try {
190
- const harness = await HuntHarness.from(
191
- instance,
192
- parseHuntArgs(["--base", base, "--head", "HEAD"]),
193
- new SilentLogger(),
194
- provider,
195
- );
196
- const result = await harness.run();
197
- expect(result.status).toBe("completed");
198
- expect(result.blockers).toBe(1);
199
- const review = instance.storage.readArtifact(result.reviewId, "review.md");
200
- expect(review?.content).toContain("any detectado");
201
- const transcript = instance.storage.readArtifact(
202
- result.reviewId,
203
- "harness-transcript.json",
204
- );
205
- expect(transcript?.content).toContain("VERIFY");
206
- const usage = instance.storage.readArtifact(result.reviewId, "ai-usage.json");
207
- expect(usage?.content).toContain('"calls"');
208
- } finally {
209
- instance.close();
210
- }
211
- });
212
- });