@payconductor/houndsman 0.5.0 → 0.7.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 +24 -4
- package/assets/grammars/tree-sitter-javascript.wasm +0 -0
- package/assets/grammars/tree-sitter-tsx.wasm +0 -0
- package/assets/grammars/tree-sitter-typescript.wasm +0 -0
- package/assets/grammars/web-tree-sitter.wasm +0 -0
- package/package.json +11 -3
- package/src/cli/commands/command.ts +5 -0
- package/src/cli/commands/doctor-command.ts +116 -24
- package/src/cli/commands/hunt-command.ts +25 -0
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/help.ts +18 -2
- package/src/cli/index.ts +10 -6
- package/src/core/ai/ai-client.ts +221 -0
- package/src/core/ai/ai-sdk-model-provider.ts +101 -0
- package/src/core/ai/errors.ts +36 -0
- package/src/core/ai/features/brief-enricher.ts +62 -0
- package/src/core/ai/features/classifier.ts +50 -0
- package/src/core/ai/features/context-summarizer.ts +31 -0
- package/src/core/ai/features/pre-analyzer.ts +51 -0
- package/src/core/ai/features/verifier.ts +45 -0
- package/src/core/ai/index.ts +56 -0
- package/src/core/ai/model-factory.ts +137 -0
- package/src/core/ai/prompts/base.ts +36 -0
- package/src/core/ai/prompts/brief.ts +87 -0
- package/src/core/ai/prompts/classification.ts +36 -0
- package/src/core/ai/prompts/context.ts +29 -0
- package/src/core/ai/prompts/pre-analysis.ts +49 -0
- package/src/core/ai/prompts/verify.ts +40 -0
- package/src/core/ai/schemas.ts +110 -0
- package/src/core/ai/types.ts +76 -0
- package/src/core/ai/usage-budget.ts +75 -0
- package/src/core/ai/usage-recorder.ts +79 -0
- package/src/core/assets.ts +16 -0
- package/src/core/ast/diff.ts +220 -0
- package/src/core/ast/impact.ts +127 -0
- package/src/core/ast/parser.ts +167 -0
- package/src/core/ast/service.ts +233 -0
- package/src/core/ast/symbols.ts +294 -0
- package/src/core/ast/types.ts +125 -0
- package/src/core/brief.ts +138 -0
- package/src/core/commands/findings.ts +65 -2
- package/src/core/commands/review.ts +2 -0
- package/src/core/commands/scaffold.ts +159 -10
- package/src/core/commands/update.ts +5 -3
- package/src/core/config.ts +85 -0
- package/src/core/defaults.ts +27 -0
- package/src/core/domain/watchdog.ts +43 -5
- package/src/core/engine/houndsman-engine.ts +16 -1
- package/src/core/engine/review-scope.ts +52 -6
- package/src/core/engine/review-session.ts +27 -2
- package/src/core/env.ts +101 -0
- package/src/core/file-classifier.ts +25 -4
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-client.ts +8 -7
- package/src/core/jev/jev-verify.ts +2 -1
- package/src/core/logging/index.ts +17 -0
- package/src/core/logging/logger.ts +187 -0
- package/src/core/orchestrator.ts +357 -9
- package/src/core/project-config.ts +123 -1
- package/src/core/registry-check.ts +148 -6
- package/src/core/renderers/manifest-writer.ts +33 -0
- package/src/core/rule-text.ts +97 -0
- package/src/core/services/file-classifier.ts +10 -3
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/verifier.ts +14 -3
- package/src/core/storage/paths.ts +6 -5
- package/src/core/storage/types.ts +6 -0
- package/src/core/storage/work-area.ts +12 -0
- package/src/core/types.ts +37 -1
- package/src/core/verify.ts +17 -5
- package/src/harness/README.md +15 -0
- package/src/harness/agent/agent-context.ts +33 -0
- package/src/harness/agent/agent-errors.ts +30 -0
- package/src/harness/agent/agent-factory.ts +45 -0
- package/src/harness/agent/agent-reporter.ts +34 -0
- package/src/harness/agent/agent-result.ts +16 -0
- package/src/harness/agent/agent-runner.ts +32 -0
- package/src/harness/agent/agent-scheduler.ts +37 -0
- package/src/harness/agent/review-agent.ts +391 -0
- package/src/harness/config/harness-config.ts +84 -0
- package/src/harness/hunt/hunt-harness.ts +83 -0
- package/src/harness/hunt/hunt-options.ts +75 -0
- package/src/harness/hunt/hunt-result.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +372 -0
- package/src/harness/hunt/review-planner.ts +98 -0
- package/src/harness/index.ts +36 -0
- package/src/harness/investigation/agent-state.ts +134 -0
- package/src/harness/investigation/evidence-verifier.ts +129 -0
- package/src/harness/investigation/evidence.ts +110 -0
- package/src/harness/investigation/finding-critic.ts +96 -0
- package/src/harness/investigation/investigation-planner.ts +118 -0
- package/src/harness/investigation/investigation-task.ts +10 -0
- package/src/harness/investigation/review-journal.ts +87 -0
- package/src/harness/investigation/rule-analyzer.ts +178 -0
- package/src/harness/investigation/rule-signals.ts +110 -0
- package/src/harness/prompts/agent-schemas.ts +57 -0
- package/src/harness/prompts/reviewer-prompt-builder.ts +101 -0
- package/src/harness/prompts/states/conclude-prompt.ts +16 -0
- package/src/harness/prompts/states/investigate-prompt.ts +37 -0
- package/src/harness/prompts/states/orient-prompt.ts +15 -0
- package/src/harness/prompts/states/verify-prompt.ts +22 -0
- package/src/harness/tools/agent-tool.ts +102 -0
- package/src/harness/tools/builtin/changed-symbols-tool.ts +48 -0
- package/src/harness/tools/builtin/doc-context-tool.ts +74 -0
- package/src/harness/tools/builtin/file-outline-tool.ts +66 -0
- package/src/harness/tools/builtin/find-references-tool.ts +66 -0
- package/src/harness/tools/builtin/find-symbol-tool.ts +91 -0
- package/src/harness/tools/builtin/helpers.ts +115 -0
- package/src/harness/tools/builtin/impact-tool.ts +48 -0
- package/src/harness/tools/builtin/imports-exports-tool.ts +53 -0
- package/src/harness/tools/builtin/index.ts +59 -0
- package/src/harness/tools/builtin/list-files-tool.ts +85 -0
- package/src/harness/tools/builtin/pr-context-tool.ts +41 -0
- package/src/harness/tools/builtin/read-file-range-tool.ts +73 -0
- package/src/harness/tools/builtin/rule-signals-tool.ts +36 -0
- package/src/harness/tools/builtin/search-code-tool.ts +65 -0
- package/src/harness/tools/builtin/symbol-context-tool.ts +106 -0
- package/src/harness/tools/builtin/tests-for-symbol-tool.ts +61 -0
- package/src/harness/tools/path-guard.ts +90 -0
- package/src/harness/tools/result-limiter.ts +49 -0
- package/src/harness/tools/secret-redactor.ts +54 -0
- package/src/harness/tools/tool-cache.ts +50 -0
- package/src/harness/tools/tool-call-budget.ts +60 -0
- package/src/harness/tools/tool-context.ts +5 -0
- package/src/harness/tools/tool-middleware.ts +105 -0
- package/src/harness/tools/tool-policy.ts +51 -0
- package/src/harness/tools/tool-registry.ts +112 -0
- package/src/harness/tools/tool-result.ts +15 -0
- package/src/harness/tools/toolbox.ts +36 -0
- package/src/harness/validation/finding-validator.ts +119 -0
- package/src/index.ts +46 -0
- package/src/server/client/App.tsx +38 -12
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/Sidebar.tsx +397 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +19 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +29 -5
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +88 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +115 -5
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +202 -0
- package/src/server/run-repository.ts +33 -1
- package/src/skill/OUTPUT-FORMAT.md +12 -0
- package/src/skill/SETUP.md +6 -2
- package/src/skill/hunt.md +14 -2
- package/src/skill/verify.md +1 -1
- package/src/tests/__snapshots__/baseline.test.ts.snap +96 -14
- package/src/tests/ai-core.test.ts +120 -0
- package/src/tests/ai-features.test.ts +183 -0
- package/src/tests/ast.test.ts +335 -0
- package/src/tests/cli.test.ts +33 -0
- package/src/tests/env.test.ts +69 -0
- package/src/tests/fallback.test.ts +172 -0
- package/src/tests/fixtures/fake-agent-tools.ts +69 -0
- package/src/tests/fixtures/fake-investigation.ts +64 -0
- package/src/tests/fixtures/fake-model-provider.ts +48 -0
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/harness-agent.test.ts +122 -0
- package/src/tests/harness-investigation.test.ts +206 -0
- package/src/tests/harness-tools.test.ts +148 -0
- package/src/tests/hunt-command.test.ts +212 -0
- package/src/tests/logger.test.ts +83 -0
- package/src/tests/review-scope.test.ts +11 -11
- package/src/tests/rule-analyzer.test.ts +61 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +295 -2
- package/src/tests/tool-contracts.test.ts +82 -0
- package/src/tests/verify.test.ts +1 -1
- package/src/server/client/RunList.tsx +0 -75
package/src/tests/server.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
5
|
import type { HoundsmanConfig } from "../core/config";
|
|
6
6
|
import { buildStateFromFindings, fileReader } from "../core/findings-state";
|
|
7
7
|
import { dedupeFindings, parseWatchdogReport } from "../core/report-builder";
|
|
@@ -10,6 +10,7 @@ import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
|
|
|
10
10
|
import type { RepoIdentity } from "../core/storage/types";
|
|
11
11
|
import { createRequestHandler, startServer } from "../server";
|
|
12
12
|
import { isSlug } from "../server/artifacts";
|
|
13
|
+
import { clearPrCommentsCache } from "../server/pr-comments";
|
|
13
14
|
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
14
15
|
|
|
15
16
|
/** Ambiente fake: banco temporário + review `main` com um finding. */
|
|
@@ -220,11 +221,12 @@ describe("server API", () => {
|
|
|
220
221
|
const body = (await res.json()) as {
|
|
221
222
|
available: boolean;
|
|
222
223
|
target: { number: number };
|
|
223
|
-
candidates:
|
|
224
|
+
candidates: { decision: string }[];
|
|
224
225
|
};
|
|
225
226
|
expect(body.available).toBe(true);
|
|
226
227
|
expect(body.target.number).toBe(12);
|
|
227
228
|
expect(body.candidates).toHaveLength(1);
|
|
229
|
+
expect(body.candidates[0].decision).toBe("pending");
|
|
228
230
|
} finally {
|
|
229
231
|
env.cleanup();
|
|
230
232
|
}
|
|
@@ -445,3 +447,294 @@ describe("startServer", () => {
|
|
|
445
447
|
}
|
|
446
448
|
});
|
|
447
449
|
});
|
|
450
|
+
|
|
451
|
+
describe("server — editor, PRs e comentários", () => {
|
|
452
|
+
const gitOk = (stdout = "") => ({ ok: true, code: 0, stdout, stderr: "" });
|
|
453
|
+
|
|
454
|
+
test("GET /api/editor devolve o contexto injetado", async () => {
|
|
455
|
+
const env = makeEnv();
|
|
456
|
+
try {
|
|
457
|
+
const handler = createRequestHandler(env.config, {
|
|
458
|
+
storage: env.storage,
|
|
459
|
+
repo: env.repo,
|
|
460
|
+
editor: { kind: "jetbrains", project: "app", product: "webstorm" },
|
|
461
|
+
});
|
|
462
|
+
const body = await (await get(handler, "/api/editor")).json();
|
|
463
|
+
expect(body).toEqual({
|
|
464
|
+
kind: "jetbrains",
|
|
465
|
+
project: "app",
|
|
466
|
+
product: "webstorm",
|
|
467
|
+
});
|
|
468
|
+
} finally {
|
|
469
|
+
env.cleanup();
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
test("GET /api/prs lista PRs abertas e casa com a run da branch", async () => {
|
|
474
|
+
const env = makeEnv();
|
|
475
|
+
try {
|
|
476
|
+
env.storage.getOrCreateReview({
|
|
477
|
+
repo: env.repo,
|
|
478
|
+
branch: "feat/x",
|
|
479
|
+
slug: "feat-x",
|
|
480
|
+
});
|
|
481
|
+
const calls: string[][] = [];
|
|
482
|
+
const handler = createRequestHandler(env.config, {
|
|
483
|
+
storage: env.storage,
|
|
484
|
+
repo: env.repo,
|
|
485
|
+
gitRunner: (args) => {
|
|
486
|
+
if (args[0] === "status") return gitOk("");
|
|
487
|
+
if (args[0] === "branch") return gitOk("main\n");
|
|
488
|
+
return gitOk("");
|
|
489
|
+
},
|
|
490
|
+
ghRunner: (args) => {
|
|
491
|
+
calls.push(args);
|
|
492
|
+
return ghJson([
|
|
493
|
+
{
|
|
494
|
+
number: 7,
|
|
495
|
+
title: "Feat X",
|
|
496
|
+
headRefName: "feat/x",
|
|
497
|
+
baseRefName: "main",
|
|
498
|
+
author: { login: "ana" },
|
|
499
|
+
updatedAt: "2026-01-02T00:00:00Z",
|
|
500
|
+
isDraft: false,
|
|
501
|
+
reviewDecision: "APPROVED",
|
|
502
|
+
url: "https://github.com/acme/app/pull/7",
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
number: 8,
|
|
506
|
+
title: "Outra",
|
|
507
|
+
headRefName: "feat/y",
|
|
508
|
+
baseRefName: "main",
|
|
509
|
+
author: { login: "bia" },
|
|
510
|
+
updatedAt: "2026-01-03T00:00:00Z",
|
|
511
|
+
isDraft: true,
|
|
512
|
+
reviewDecision: "",
|
|
513
|
+
url: "https://github.com/acme/app/pull/8",
|
|
514
|
+
},
|
|
515
|
+
]);
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
const res = await get(handler, "/api/prs");
|
|
520
|
+
expect(res.status).toBe(200);
|
|
521
|
+
const body = (await res.json()) as {
|
|
522
|
+
available: boolean;
|
|
523
|
+
currentBranch: string;
|
|
524
|
+
worktree: { clean: boolean };
|
|
525
|
+
prs: { number: number; runSlug?: string }[];
|
|
526
|
+
};
|
|
527
|
+
expect(body.available).toBe(true);
|
|
528
|
+
expect(body.currentBranch).toBe("main");
|
|
529
|
+
expect(body.worktree.clean).toBe(true);
|
|
530
|
+
expect(body.prs.map((pr) => pr.number)).toEqual([8, 7]);
|
|
531
|
+
expect(body.prs.find((pr) => pr.number === 7)?.runSlug).toBe("feat-x");
|
|
532
|
+
expect(calls[0]).toEqual([
|
|
533
|
+
"pr",
|
|
534
|
+
"list",
|
|
535
|
+
"--state",
|
|
536
|
+
"open",
|
|
537
|
+
"--limit",
|
|
538
|
+
"50",
|
|
539
|
+
"--json",
|
|
540
|
+
"number,title,headRefName,baseRefName,author,updatedAt,isDraft,reviewDecision,url",
|
|
541
|
+
]);
|
|
542
|
+
} finally {
|
|
543
|
+
env.cleanup();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
test("POST /api/prs/checkout bloqueia árvore suja sem chamar gh", async () => {
|
|
548
|
+
const env = makeEnv();
|
|
549
|
+
try {
|
|
550
|
+
let ghCalls = 0;
|
|
551
|
+
const handler = createRequestHandler(env.config, {
|
|
552
|
+
storage: env.storage,
|
|
553
|
+
repo: env.repo,
|
|
554
|
+
gitRunner: (args) =>
|
|
555
|
+
args[0] === "status" ? gitOk(" M src/alpha/f.ts\n") : gitOk(""),
|
|
556
|
+
ghRunner: () => {
|
|
557
|
+
ghCalls++;
|
|
558
|
+
return gitOk("");
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const res = await handler(
|
|
563
|
+
new Request("http://localhost/api/prs/checkout", {
|
|
564
|
+
method: "POST",
|
|
565
|
+
headers: { "content-type": "application/json" },
|
|
566
|
+
body: JSON.stringify({ number: 7 }),
|
|
567
|
+
}),
|
|
568
|
+
);
|
|
569
|
+
const body = (await res.json()) as {
|
|
570
|
+
ok: boolean;
|
|
571
|
+
reason: string;
|
|
572
|
+
changes?: string[];
|
|
573
|
+
};
|
|
574
|
+
expect(body.ok).toBe(false);
|
|
575
|
+
expect(body.reason).toBe("dirty");
|
|
576
|
+
expect(body.changes).toEqual([" M src/alpha/f.ts"]);
|
|
577
|
+
expect(ghCalls).toBe(0);
|
|
578
|
+
} finally {
|
|
579
|
+
env.cleanup();
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
test("POST /api/prs/checkout usa gh pr checkout quando a árvore está limpa", async () => {
|
|
584
|
+
const env = makeEnv();
|
|
585
|
+
try {
|
|
586
|
+
const calls: string[][] = [];
|
|
587
|
+
const handler = createRequestHandler(env.config, {
|
|
588
|
+
storage: env.storage,
|
|
589
|
+
repo: env.repo,
|
|
590
|
+
gitRunner: (args) => {
|
|
591
|
+
if (args[0] === "status") return gitOk("");
|
|
592
|
+
if (args[0] === "branch") return gitOk("feat/x\n");
|
|
593
|
+
return gitOk("");
|
|
594
|
+
},
|
|
595
|
+
ghRunner: (args) => {
|
|
596
|
+
calls.push(args);
|
|
597
|
+
return gitOk("");
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
const res = await handler(
|
|
602
|
+
new Request("http://localhost/api/prs/checkout", {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "content-type": "application/json" },
|
|
605
|
+
body: JSON.stringify({ number: 7 }),
|
|
606
|
+
}),
|
|
607
|
+
);
|
|
608
|
+
expect(await res.json()).toEqual({
|
|
609
|
+
ok: true,
|
|
610
|
+
number: 7,
|
|
611
|
+
branch: "feat/x",
|
|
612
|
+
});
|
|
613
|
+
expect(calls).toContainEqual(["pr", "checkout", "7"]);
|
|
614
|
+
} finally {
|
|
615
|
+
env.cleanup();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("GET comments agrupa threads, usa cache e respeita ?refresh=1", async () => {
|
|
620
|
+
const env = makeEnv();
|
|
621
|
+
try {
|
|
622
|
+
clearPrCommentsCache();
|
|
623
|
+
let inlineCalls = 0;
|
|
624
|
+
let issueCalls = 0;
|
|
625
|
+
const handler = createRequestHandler(env.config, {
|
|
626
|
+
storage: env.storage,
|
|
627
|
+
repo: env.repo,
|
|
628
|
+
ghRunner: (args) => {
|
|
629
|
+
if (args[0] === "pr") {
|
|
630
|
+
return ghJson({
|
|
631
|
+
number: 12,
|
|
632
|
+
url: "https://github.com/acme/app/pull/12",
|
|
633
|
+
headRefOid: "deadbeef",
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
const path = args[args.length - 1];
|
|
637
|
+
if (path === "repos/acme/app/pulls/12/comments") {
|
|
638
|
+
inlineCalls++;
|
|
639
|
+
return ghJson([
|
|
640
|
+
[
|
|
641
|
+
{
|
|
642
|
+
id: 1,
|
|
643
|
+
body: "raiz",
|
|
644
|
+
path: "src/alpha/f.ts",
|
|
645
|
+
line: 2,
|
|
646
|
+
created_at: "2026-01-01T00:00:00Z",
|
|
647
|
+
html_url:
|
|
648
|
+
"https://github.com/acme/app/pull/12#discussion_r1",
|
|
649
|
+
user: { login: "ana", type: "User" },
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
id: 2,
|
|
653
|
+
body: "resposta",
|
|
654
|
+
path: "src/alpha/f.ts",
|
|
655
|
+
line: 2,
|
|
656
|
+
in_reply_to_id: 1,
|
|
657
|
+
created_at: "2026-01-01T00:01:00Z",
|
|
658
|
+
user: { login: "bia", type: "User" },
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
]);
|
|
662
|
+
}
|
|
663
|
+
if (path === "repos/acme/app/issues/12/comments") {
|
|
664
|
+
issueCalls++;
|
|
665
|
+
return ghJson([
|
|
666
|
+
[
|
|
667
|
+
{
|
|
668
|
+
id: 3,
|
|
669
|
+
body: "geral",
|
|
670
|
+
created_at: "2026-01-01T00:02:00Z",
|
|
671
|
+
user: { login: "copilot[bot]", type: "Bot" },
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
]);
|
|
675
|
+
}
|
|
676
|
+
return { ok: false, code: 404, stdout: "", stderr: "not found" };
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
const first = (await (
|
|
681
|
+
await get(handler, "/api/runs/main/comments")
|
|
682
|
+
).json()) as {
|
|
683
|
+
available: boolean;
|
|
684
|
+
counts: { threads: number; inline: number; issue: number };
|
|
685
|
+
threads: {
|
|
686
|
+
path: string;
|
|
687
|
+
absFile: string;
|
|
688
|
+
comments: { author: string }[];
|
|
689
|
+
}[];
|
|
690
|
+
issueComments: { authorType: string }[];
|
|
691
|
+
};
|
|
692
|
+
expect(first.available).toBe(true);
|
|
693
|
+
expect(first.counts).toEqual({ threads: 1, inline: 2, issue: 1 });
|
|
694
|
+
expect(first.threads[0].path).toBe("src/alpha/f.ts");
|
|
695
|
+
expect(first.threads[0].absFile).toBe(
|
|
696
|
+
resolve(env.repo.root, "src/alpha/f.ts"),
|
|
697
|
+
);
|
|
698
|
+
expect(first.threads[0].comments.map((c) => c.author)).toEqual([
|
|
699
|
+
"ana",
|
|
700
|
+
"bia",
|
|
701
|
+
]);
|
|
702
|
+
expect(first.issueComments[0].authorType).toBe("Bot");
|
|
703
|
+
|
|
704
|
+
await get(handler, "/api/runs/main/comments");
|
|
705
|
+
expect(inlineCalls).toBe(1);
|
|
706
|
+
expect(issueCalls).toBe(1);
|
|
707
|
+
|
|
708
|
+
await get(handler, "/api/runs/main/comments?refresh=1");
|
|
709
|
+
expect(inlineCalls).toBe(2);
|
|
710
|
+
expect(issueCalls).toBe(2);
|
|
711
|
+
} finally {
|
|
712
|
+
clearPrCommentsCache();
|
|
713
|
+
env.cleanup();
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("GET comments é fail-open quando o gh não está disponível", async () => {
|
|
718
|
+
const env = makeEnv();
|
|
719
|
+
try {
|
|
720
|
+
clearPrCommentsCache();
|
|
721
|
+
const handler = createRequestHandler(env.config, {
|
|
722
|
+
storage: env.storage,
|
|
723
|
+
repo: env.repo,
|
|
724
|
+
ghRunner: () => ({
|
|
725
|
+
ok: false,
|
|
726
|
+
code: 127,
|
|
727
|
+
stdout: "",
|
|
728
|
+
stderr: "gh: not found",
|
|
729
|
+
}),
|
|
730
|
+
});
|
|
731
|
+
const body = (await (
|
|
732
|
+
await get(handler, "/api/runs/main/comments")
|
|
733
|
+
).json()) as { available: boolean; reason: string };
|
|
734
|
+
expect(body.available).toBe(false);
|
|
735
|
+
expect(body.reason).toContain("gh");
|
|
736
|
+
} finally {
|
|
737
|
+
env.cleanup();
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test das tools built-in: metadata, schema e erros estruturados.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { AstService } from "../core/ast/service";
|
|
7
|
+
import { Watchdog } from "../core/domain/watchdog";
|
|
8
|
+
import { SilentReporter } from "../harness/agent/agent-reporter";
|
|
9
|
+
import { AgentState } from "../harness/investigation/agent-state";
|
|
10
|
+
import type { ToolContext } from "../harness/tools/agent-tool";
|
|
11
|
+
import { BuiltinToolsModule } from "../harness/tools/builtin";
|
|
12
|
+
import { ToolCache } from "../harness/tools/tool-cache";
|
|
13
|
+
import { ToolCallBudget } from "../harness/tools/tool-call-budget";
|
|
14
|
+
import { ToolMiddleware } from "../harness/tools/tool-middleware";
|
|
15
|
+
import { ToolRegistry } from "../harness/tools/tool-registry";
|
|
16
|
+
import { fakeAgentContext } from "./fixtures/fake-investigation";
|
|
17
|
+
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
18
|
+
|
|
19
|
+
function registry(): ToolRegistry {
|
|
20
|
+
const value = new ToolRegistry();
|
|
21
|
+
value.registerModule(new BuiltinToolsModule());
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function context(): ToolContext {
|
|
26
|
+
return {
|
|
27
|
+
repoRoot: process.cwd(),
|
|
28
|
+
watchdog: new Watchdog(fakeHoundsmanConfig.watchdogs.alpha),
|
|
29
|
+
diff: { byFile: new Map(), churn: new Map() },
|
|
30
|
+
ast: new AstService(fakeHoundsmanConfig),
|
|
31
|
+
state: new AgentState(),
|
|
32
|
+
budget: new ToolCallBudget(20, 10),
|
|
33
|
+
cache: new ToolCache(),
|
|
34
|
+
logger: new SilentReporter(),
|
|
35
|
+
agent: fakeAgentContext({ repoRoot: process.cwd() }),
|
|
36
|
+
rules: new Map(),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("contratos das tools", () => {
|
|
41
|
+
test("todas declaram metadata read-only e capabilities", () => {
|
|
42
|
+
for (const tool of registry().list()) {
|
|
43
|
+
expect(tool.metadata.name.length).toBeGreaterThan(0);
|
|
44
|
+
expect(tool.metadata.description.length).toBeGreaterThan(0);
|
|
45
|
+
expect(tool.metadata.readOnly).toBe(true);
|
|
46
|
+
expect(tool.metadata.capabilities.length).toBeGreaterThan(0);
|
|
47
|
+
expect(tool.metadata.cost).toBeGreaterThanOrEqual(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("nenhuma tool devolve arquivo inteiro por padrão", () => {
|
|
52
|
+
// `read_file_range` exige faixa explícita.
|
|
53
|
+
const range = registry().get("read_file_range");
|
|
54
|
+
expect(range?.inputSchema.safeParse({ path: "a.ts" }).success).toBe(false);
|
|
55
|
+
// `file_outline` só devolve metadados (não tem parâmetro de corpo).
|
|
56
|
+
const outline = registry().get("file_outline");
|
|
57
|
+
expect(outline?.inputSchema.safeParse({ path: "a.ts" }).success).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("tools de arquivo devolvem erro estruturado (não lançam)", async () => {
|
|
61
|
+
const middleware = new ToolMiddleware();
|
|
62
|
+
for (const [name, input] of [
|
|
63
|
+
["read_file_range", { path: "../x", startLine: 1, endLine: 2 }],
|
|
64
|
+
["file_outline", { path: "../x" }],
|
|
65
|
+
["imports_exports", { file: "../x" }],
|
|
66
|
+
] as const) {
|
|
67
|
+
const result = await registry().execute(name, input, context(), middleware);
|
|
68
|
+
expect(result.ok).toBe(false);
|
|
69
|
+
expect(typeof result.error).toBe("string");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("rule_signals devolve erro quando a regra não existe", async () => {
|
|
74
|
+
const result = await registry().execute(
|
|
75
|
+
"rule_signals",
|
|
76
|
+
{ rule: "regra inexistente" },
|
|
77
|
+
context(),
|
|
78
|
+
new ToolMiddleware(),
|
|
79
|
+
);
|
|
80
|
+
expect(result.ok).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/tests/verify.test.ts
CHANGED
|
@@ -158,7 +158,7 @@ describe("JevVerifier", () => {
|
|
|
158
158
|
]);
|
|
159
159
|
expect(outcome.checked).toBe(0);
|
|
160
160
|
expect(outcome.skipped).toBe(1);
|
|
161
|
-
expect(outcome.reason).toContain("
|
|
161
|
+
expect(outcome.reason).toContain("HOUNDSMAN_TYPE_SAFE_API_KEY");
|
|
162
162
|
});
|
|
163
163
|
|
|
164
164
|
test("sem casos ⇒ vazio", async () => {
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar: logo do houndsman + lista de execuções (mais recentes primeiro),
|
|
3
|
-
* com data/hora, relação de findings presentes e badge de correção pronta.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { BranchIcon, Clock, Sparkles } from "./icons";
|
|
7
|
-
import { describeRunStatus } from "./utils/constants";
|
|
8
|
-
import { formatRunTime } from "./utils/format";
|
|
9
|
-
import type { RunSummary } from "./utils/types";
|
|
10
|
-
|
|
11
|
-
export function RunList({
|
|
12
|
-
runs,
|
|
13
|
-
slug,
|
|
14
|
-
onSelect,
|
|
15
|
-
}: {
|
|
16
|
-
runs: RunSummary[];
|
|
17
|
-
slug: string | null;
|
|
18
|
-
onSelect: (slug: string) => void;
|
|
19
|
-
}) {
|
|
20
|
-
return (
|
|
21
|
-
<aside class="sidebar">
|
|
22
|
-
<div class="brand">
|
|
23
|
-
<img class="logo" src="/assets/logo.png" alt="" />
|
|
24
|
-
<div>
|
|
25
|
-
<h1>Houndsman</h1>
|
|
26
|
-
<p class="sub">Revisões e triagem</p>
|
|
27
|
-
</div>
|
|
28
|
-
</div>
|
|
29
|
-
<p class="section-label">Execuções</p>
|
|
30
|
-
<nav class="runs">
|
|
31
|
-
{runs.length === 0 && <p class="empty">Nenhuma run encontrada.</p>}
|
|
32
|
-
{runs.map((run) => {
|
|
33
|
-
const status = describeRunStatus(run);
|
|
34
|
-
const fixReady = status?.kind === "fix_done";
|
|
35
|
-
return (
|
|
36
|
-
<button
|
|
37
|
-
type="button"
|
|
38
|
-
title={`Abrir a run da branch ${run.branch}`}
|
|
39
|
-
class={`run${run.slug === slug ? " active" : ""}`}
|
|
40
|
-
onClick={() => onSelect(run.slug)}
|
|
41
|
-
>
|
|
42
|
-
<span class="run-top">
|
|
43
|
-
<BranchIcon size={14} class="run-icon" />
|
|
44
|
-
<span class="run-branch">{run.branch}</span>
|
|
45
|
-
</span>
|
|
46
|
-
<span class="run-meta">
|
|
47
|
-
<span class="run-when">
|
|
48
|
-
<Clock size={12} />
|
|
49
|
-
{formatRunTime(run.updatedAt) || "sem data"}
|
|
50
|
-
</span>
|
|
51
|
-
<span
|
|
52
|
-
class="run-count"
|
|
53
|
-
title={`${run.summary.present} de ${run.summary.total} findings presentes`}
|
|
54
|
-
>
|
|
55
|
-
{run.summary.present}/{run.summary.total}
|
|
56
|
-
</span>
|
|
57
|
-
</span>
|
|
58
|
-
{fixReady ? (
|
|
59
|
-
<span class="run-badge">
|
|
60
|
-
<Sparkles size={12} /> Correção pronta
|
|
61
|
-
</span>
|
|
62
|
-
) : (
|
|
63
|
-
status && (
|
|
64
|
-
<span class={`run-status status-${status.kind}`}>
|
|
65
|
-
{status.label}
|
|
66
|
-
</span>
|
|
67
|
-
)
|
|
68
|
-
)}
|
|
69
|
-
</button>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</nav>
|
|
73
|
-
</aside>
|
|
74
|
-
);
|
|
75
|
-
}
|