@payconductor/houndsman 0.8.0 → 0.10.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/package.json +1 -1
- package/src/core/ai/ai-client.ts +21 -0
- package/src/core/ai/ai-sdk-model-provider.ts +43 -18
- package/src/core/ai/features/technical-summarizer.ts +43 -0
- package/src/core/ai/index.ts +4 -1
- package/src/core/ai/prompts/context.ts +3 -3
- package/src/core/ai/prompts/technical.ts +30 -0
- package/src/core/ai/provider-capabilities.ts +45 -0
- package/src/core/ai/schemas.ts +19 -3
- package/src/core/ai/types.ts +3 -1
- package/src/core/brief.ts +25 -4
- package/src/core/commands/scaffold.ts +5 -0
- package/src/core/config.ts +17 -1
- package/src/core/git-runner.ts +8 -1
- package/src/core/orchestrator.ts +114 -2
- package/src/core/paths.ts +3 -0
- package/src/core/pr-context.ts +11 -6
- package/src/core/project-config.ts +9 -1
- package/src/core/renderers/resume-renderer.ts +17 -34
- package/src/core/report-builder.ts +6 -6
- package/src/core/services/worktree-manager.ts +29 -5
- package/src/core/storage/types.ts +1 -0
- package/src/core/technical-summary.ts +398 -0
- package/src/harness/agent/agent-factory.ts +1 -0
- package/src/harness/agent/review-agent.ts +11 -1
- package/src/harness/config/harness-config.ts +6 -1
- package/src/index.ts +2 -0
- package/src/server/client/App.tsx +2 -1
- package/src/server/client/PrsSection.tsx +296 -0
- package/src/server/client/ResumePanel.tsx +10 -13
- package/src/server/client/RunsSection.tsx +73 -0
- package/src/server/client/Sidebar.tsx +171 -413
- package/src/server/client/sidebar-sections.tsx +73 -0
- package/src/server/client/utils/constants.ts +7 -2
- package/src/server/client/utils/sidebar-prefs.ts +64 -0
- package/src/server/client/utils/types.ts +1 -0
- package/src/server/index.ts +23 -409
- package/src/server/styles.css +454 -0
- package/src/tests/__snapshots__/baseline.test.ts.snap +5 -0
- package/src/tests/ai-core.test.ts +71 -0
- package/src/tests/ai-features.test.ts +36 -0
- package/src/tests/baseline.test.ts +4 -1
- package/src/tests/harness-agent.test.ts +19 -0
- package/src/tests/review-resume.test.ts +36 -8
- package/src/tests/review-scope.test.ts +67 -0
- package/src/tests/review-scripts.test.ts +2 -2
- package/src/tests/server.test.ts +16 -1
- package/src/tests/sidebar-sections.test.ts +94 -0
- package/src/tests/technical-summary.test.ts +176 -0
- package/src/tests/worktree.test.ts +39 -4
- package/assets/logo-legacy.png +0 -0
|
@@ -119,4 +119,23 @@ describe("ReviewAgent", () => {
|
|
|
119
119
|
expect(result.status).toBe("failed");
|
|
120
120
|
expect(result.error).toContain("429");
|
|
121
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
|
+
});
|
|
122
141
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Resumo final (`review-resume.md`) — conteúdo de negócio + pontos técnicos
|
|
3
|
-
* determinístico e limitado a `RESUME_MAX_CHARS`.
|
|
2
|
+
* Resumo final (`review-resume.md`) — conteúdo de negócio + pontos técnicos
|
|
3
|
+
* estruturais, determinístico e limitado a `RESUME_MAX_CHARS`.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
renderReviewResume,
|
|
11
11
|
} from "../core/renderers/resume-renderer";
|
|
12
12
|
import type { MergedFinding } from "../core/report-builder";
|
|
13
|
+
import type { TechnicalSummary } from "../core/technical-summary";
|
|
13
14
|
|
|
14
15
|
function finding(overrides: Partial<MergedFinding> = {}): MergedFinding {
|
|
15
16
|
return {
|
|
@@ -32,8 +33,24 @@ const classification: ResumeClassification = {
|
|
|
32
33
|
watchdogs: { alpha: ["src/a.ts"], beta: ["src/b.ts"] },
|
|
33
34
|
};
|
|
34
35
|
|
|
36
|
+
const technical: TechnicalSummary = {
|
|
37
|
+
source: "ai",
|
|
38
|
+
headSha: "abc123",
|
|
39
|
+
overview: "Novo service `bin-lookup` com provider e repository.",
|
|
40
|
+
highlights: [
|
|
41
|
+
{
|
|
42
|
+
area: "Contratos",
|
|
43
|
+
detail: "`BinLookupProvider` (src/bin-lookup/provider.ts)",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
area: "Implementações",
|
|
47
|
+
detail: "`BinLookupService` (src/bin-lookup/service.ts)",
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
|
|
35
52
|
describe("renderReviewResume", () => {
|
|
36
|
-
test("une contexto de negócio e pontos técnicos", () => {
|
|
53
|
+
test("une contexto de negócio e pontos técnicos estruturais", () => {
|
|
37
54
|
const md = renderReviewResume({
|
|
38
55
|
branch: "feat/x",
|
|
39
56
|
date: "2026-10-17",
|
|
@@ -48,7 +65,7 @@ describe("renderReviewResume", () => {
|
|
|
48
65
|
finding({ severity: "blocking", message: "quebra o cadastro" }),
|
|
49
66
|
finding({ severity: "attention", file: "src/b.ts", line: undefined }),
|
|
50
67
|
],
|
|
51
|
-
|
|
68
|
+
technical,
|
|
52
69
|
});
|
|
53
70
|
|
|
54
71
|
expect(md).toContain("# Resumo da revisão — feat/x");
|
|
@@ -58,8 +75,11 @@ describe("renderReviewResume", () => {
|
|
|
58
75
|
expect(md).toContain("**Por quê:** Reduzir o esforço manual.");
|
|
59
76
|
expect(md).toContain("- Novo fluxo de cadastro.");
|
|
60
77
|
expect(md).not.toContain("## Números");
|
|
61
|
-
expect(md).toContain("
|
|
62
|
-
expect(md).toContain("`
|
|
78
|
+
expect(md).toContain("## Pontos técnicos");
|
|
79
|
+
expect(md).toContain("Novo service `bin-lookup`");
|
|
80
|
+
expect(md).toContain("**Contratos** — `BinLookupProvider`");
|
|
81
|
+
expect(md).toContain("**Implementações** — `BinLookupService`");
|
|
82
|
+
expect(md).not.toContain("quebra o cadastro");
|
|
63
83
|
expect(md.length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
64
84
|
});
|
|
65
85
|
|
|
@@ -72,7 +92,7 @@ describe("renderReviewResume", () => {
|
|
|
72
92
|
|
|
73
93
|
expect(md).toContain("Sem resumo de contexto da PR disponível.");
|
|
74
94
|
expect(md).toContain("✅ Aprovado");
|
|
75
|
-
expect(md).toContain("
|
|
95
|
+
expect(md).toContain("Sem mudanças estruturais detectadas nesta revisão.");
|
|
76
96
|
});
|
|
77
97
|
|
|
78
98
|
test("respeita o teto de caracteres", () => {
|
|
@@ -89,10 +109,18 @@ describe("renderReviewResume", () => {
|
|
|
89
109
|
available: true,
|
|
90
110
|
objective: "o".repeat(1000),
|
|
91
111
|
motivation: "m".repeat(1000),
|
|
92
|
-
changeSummary: ["a".repeat(
|
|
112
|
+
changeSummary: ["a".repeat(300), "b".repeat(300)],
|
|
93
113
|
},
|
|
94
114
|
classification,
|
|
95
115
|
findings,
|
|
116
|
+
technical: {
|
|
117
|
+
source: "deterministic",
|
|
118
|
+
overview: "v".repeat(600),
|
|
119
|
+
highlights: Array.from({ length: 10 }, (_, index) => ({
|
|
120
|
+
area: `Área ${index}`,
|
|
121
|
+
detail: "x".repeat(300),
|
|
122
|
+
})),
|
|
123
|
+
},
|
|
96
124
|
});
|
|
97
125
|
|
|
98
126
|
expect(md.length).toBeLessThanOrEqual(RESUME_MAX_CHARS);
|
|
@@ -300,3 +300,70 @@ describe("prepare/finalize — mesmo commit", () => {
|
|
|
300
300
|
}
|
|
301
301
|
}, 30_000);
|
|
302
302
|
});
|
|
303
|
+
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// Run vazia
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
describe("prepare sem alterações", () => {
|
|
309
|
+
test("run recém-criada é descartada (sem run vazia na lista)", async () => {
|
|
310
|
+
const repo = makeRepo();
|
|
311
|
+
const originalCwd = process.cwd();
|
|
312
|
+
const originalHome = process.env.HOUNDSMAN_HOME;
|
|
313
|
+
let storage: SQLiteStorageProvider | null = null;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
process.env.HOUNDSMAN_HOME = join(repo.cwd, ".home");
|
|
317
|
+
process.chdir(repo.cwd);
|
|
318
|
+
|
|
319
|
+
storage = new SQLiteStorageProvider(join(repo.cwd, "houndsman.db"));
|
|
320
|
+
const record = storage.getOrCreateReview({
|
|
321
|
+
repo: resolveRepoIdentity(),
|
|
322
|
+
branch: "main",
|
|
323
|
+
slug: "main",
|
|
324
|
+
});
|
|
325
|
+
const opts = {
|
|
326
|
+
...defaultOptions(),
|
|
327
|
+
base: repo.head,
|
|
328
|
+
head: "HEAD",
|
|
329
|
+
noJev: true,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const result = await prepare(
|
|
333
|
+
opts,
|
|
334
|
+
"main",
|
|
335
|
+
fakeHoundsmanConfig,
|
|
336
|
+
reviewContext(storage, record),
|
|
337
|
+
);
|
|
338
|
+
expect(result.messages.join("\n")).toContain("nenhuma run criada");
|
|
339
|
+
expect(storage.getReview(record.id)).toBeNull();
|
|
340
|
+
expect(storage.listArtifacts(record.id)).toHaveLength(0);
|
|
341
|
+
|
|
342
|
+
// Run com artefatos anteriores (contexto) continua existindo.
|
|
343
|
+
const kept = storage.getOrCreateReview({
|
|
344
|
+
repo: resolveRepoIdentity(),
|
|
345
|
+
branch: "main",
|
|
346
|
+
slug: "main",
|
|
347
|
+
});
|
|
348
|
+
storage.writeArtifact(kept.id, {
|
|
349
|
+
name: "context.json",
|
|
350
|
+
kind: "context",
|
|
351
|
+
content: "{}\n",
|
|
352
|
+
});
|
|
353
|
+
await prepare(
|
|
354
|
+
opts,
|
|
355
|
+
"main",
|
|
356
|
+
fakeHoundsmanConfig,
|
|
357
|
+
reviewContext(storage, kept),
|
|
358
|
+
);
|
|
359
|
+
expect(storage.getReview(kept.id)?.status).toBe("review_done");
|
|
360
|
+
expect(storage.readArtifact(kept.id, "context.json")).not.toBeNull();
|
|
361
|
+
} finally {
|
|
362
|
+
storage?.close();
|
|
363
|
+
process.chdir(originalCwd);
|
|
364
|
+
if (originalHome === undefined) delete process.env.HOUNDSMAN_HOME;
|
|
365
|
+
else process.env.HOUNDSMAN_HOME = originalHome;
|
|
366
|
+
repo.cleanup();
|
|
367
|
+
}
|
|
368
|
+
}, 30_000);
|
|
369
|
+
});
|
|
@@ -799,9 +799,9 @@ describe("context-summary", () => {
|
|
|
799
799
|
(_, i) => `item ${i} ${"z".repeat(300)}`,
|
|
800
800
|
),
|
|
801
801
|
});
|
|
802
|
-
expect(summary?.objective?.length).toBeLessThanOrEqual(
|
|
802
|
+
expect(summary?.objective?.length).toBeLessThanOrEqual(400);
|
|
803
803
|
expect(summary?.objective?.endsWith("…")).toBe(true);
|
|
804
|
-
expect(summary?.changeSummary).toHaveLength(
|
|
804
|
+
expect(summary?.changeSummary).toHaveLength(6);
|
|
805
805
|
});
|
|
806
806
|
|
|
807
807
|
test("renderReport coloca a seção de contexto quando presente", () => {
|
package/src/tests/server.test.ts
CHANGED
|
@@ -436,7 +436,19 @@ describe("server API", () => {
|
|
|
436
436
|
try {
|
|
437
437
|
const handler = createRequestHandler(env.config, env);
|
|
438
438
|
const shell = await get(handler, "/");
|
|
439
|
-
|
|
439
|
+
const html = await shell.text();
|
|
440
|
+
expect(html).toContain('id="app"');
|
|
441
|
+
expect(html).toContain("/assets/styles.css?v=");
|
|
442
|
+
|
|
443
|
+
// O CSS vive em `styles.css`, servido como arquivo real.
|
|
444
|
+
const css = await get(handler, "/assets/styles.css");
|
|
445
|
+
expect(css.status).toBe(200);
|
|
446
|
+
expect(css.headers.get("content-type")).toContain("text/css");
|
|
447
|
+
const styles = await css.text();
|
|
448
|
+
// Sidebar: rail vertical de seções + painel redimensionável.
|
|
449
|
+
expect(styles).toContain(".side-rail");
|
|
450
|
+
expect(styles).toContain("--side-width");
|
|
451
|
+
expect(styles).not.toContain(".side-tabs");
|
|
440
452
|
|
|
441
453
|
const bundle = await get(handler, "/assets/app.js");
|
|
442
454
|
expect(bundle.status).toBe(200);
|
|
@@ -458,6 +470,9 @@ describe("server API", () => {
|
|
|
458
470
|
|
|
459
471
|
const bundle = await get(handler, "/assets/app.js");
|
|
460
472
|
expect(bundle.headers.get("cache-control")).toContain("no-store");
|
|
473
|
+
|
|
474
|
+
const css = await get(handler, "/assets/styles.css");
|
|
475
|
+
expect(css.headers.get("cache-control")).toContain("no-store");
|
|
461
476
|
} finally {
|
|
462
477
|
env.cleanup();
|
|
463
478
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Navegação da sidebar: registry de seções e preferências de layout.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_SIDEBAR_SECTION,
|
|
8
|
+
getSidebarSection,
|
|
9
|
+
resolveSidebarSectionId,
|
|
10
|
+
SIDEBAR_SECTIONS,
|
|
11
|
+
type SidebarSectionContext,
|
|
12
|
+
} from "../server/client/sidebar-sections";
|
|
13
|
+
import {
|
|
14
|
+
clampSidebarWidth,
|
|
15
|
+
readSidebarPrefs,
|
|
16
|
+
SIDEBAR_DEFAULT_WIDTH,
|
|
17
|
+
SIDEBAR_MAX_WIDTH,
|
|
18
|
+
SIDEBAR_MIN_WIDTH,
|
|
19
|
+
} from "../server/client/utils/sidebar-prefs";
|
|
20
|
+
import type { RunSummary } from "../server/client/utils/types";
|
|
21
|
+
|
|
22
|
+
function makeRun(slug: string): RunSummary {
|
|
23
|
+
return {
|
|
24
|
+
slug,
|
|
25
|
+
branch: slug,
|
|
26
|
+
summary: {
|
|
27
|
+
total: 0,
|
|
28
|
+
present: 0,
|
|
29
|
+
byDecision: { pending: 0, execute: 0, ignore: 0 },
|
|
30
|
+
byExecState: { pending: 0, in_progress: 0, done: 0, stale: 0 },
|
|
31
|
+
bySeverity: { blocking: 0, attention: 0, suggestion: 0, nit: 0 },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeContext(runs: RunSummary[]): SidebarSectionContext {
|
|
37
|
+
return {
|
|
38
|
+
runs,
|
|
39
|
+
slug: null,
|
|
40
|
+
onSelect: () => undefined,
|
|
41
|
+
onRefreshRuns: () => undefined,
|
|
42
|
+
active: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("SIDEBAR_SECTIONS", () => {
|
|
47
|
+
test("expõe Execuções e PRs na ordem canônica", () => {
|
|
48
|
+
expect(SIDEBAR_SECTIONS.map((section) => section.id)).toEqual(["runs", "prs"]);
|
|
49
|
+
expect(DEFAULT_SIDEBAR_SECTION).toBe("runs");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("cada seção tem identificação e conteúdo", () => {
|
|
53
|
+
const ids = new Set<string>();
|
|
54
|
+
for (const section of SIDEBAR_SECTIONS) {
|
|
55
|
+
expect(section.id).not.toBe("");
|
|
56
|
+
expect(ids.has(section.id)).toBe(false);
|
|
57
|
+
ids.add(section.id);
|
|
58
|
+
expect(section.label.length).toBeGreaterThan(0);
|
|
59
|
+
expect(section.title.length).toBeGreaterThan(0);
|
|
60
|
+
expect(typeof section.icon).toBe("function");
|
|
61
|
+
expect(typeof section.Content).toBe("function");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("o badge de Execuções reflete a contagem de runs", () => {
|
|
66
|
+
const runs = SIDEBAR_SECTIONS.find((section) => section.id === "runs");
|
|
67
|
+
expect(runs?.badge?.(makeContext([]))).toBeNull();
|
|
68
|
+
expect(runs?.badge?.(makeContext([makeRun("a"), makeRun("b")]))).toBe(2);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("resolve ids salvos e cai no default quando inválidos", () => {
|
|
72
|
+
expect(resolveSidebarSectionId("prs")).toBe("prs");
|
|
73
|
+
expect(resolveSidebarSectionId("desconhecida")).toBe(DEFAULT_SIDEBAR_SECTION);
|
|
74
|
+
expect(resolveSidebarSectionId(null)).toBe(DEFAULT_SIDEBAR_SECTION);
|
|
75
|
+
expect(getSidebarSection("desconhecida").id).toBe(DEFAULT_SIDEBAR_SECTION);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("preferências da sidebar", () => {
|
|
80
|
+
test("clampa a largura entre mínimo e máximo", () => {
|
|
81
|
+
expect(clampSidebarWidth(200)).toBe(SIDEBAR_MIN_WIDTH);
|
|
82
|
+
expect(clampSidebarWidth(900)).toBe(SIDEBAR_MAX_WIDTH);
|
|
83
|
+
expect(clampSidebarWidth(400)).toBe(400);
|
|
84
|
+
expect(clampSidebarWidth(Number.NaN)).toBe(SIDEBAR_DEFAULT_WIDTH);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("lê defaults sem preferências salvas", () => {
|
|
88
|
+
const prefs = readSidebarPrefs();
|
|
89
|
+
expect(prefs.width).toBeGreaterThanOrEqual(SIDEBAR_MIN_WIDTH);
|
|
90
|
+
expect(prefs.width).toBeLessThanOrEqual(SIDEBAR_MAX_WIDTH);
|
|
91
|
+
expect(typeof prefs.collapsed).toBe("boolean");
|
|
92
|
+
if (prefs.section !== null) expect(typeof prefs.section).toBe("string");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pontos técnicos estruturais do resumo final — builder determinístico e
|
|
3
|
+
* validação do artefato `technical-summary.json`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import type { AstAnalysis, AstFacts, AstImpact } from "../core/ast/types";
|
|
8
|
+
import { buildTechnicalSummary, parseTechnicalSummary } from "../core/technical-summary";
|
|
9
|
+
|
|
10
|
+
const facts: AstFacts = {
|
|
11
|
+
language: "typescript",
|
|
12
|
+
symbols: [],
|
|
13
|
+
imports: [],
|
|
14
|
+
exports: [],
|
|
15
|
+
decorators: [],
|
|
16
|
+
hasSyntaxErrors: false,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function analysis(partial: Partial<AstAnalysis> = {}): AstAnalysis {
|
|
20
|
+
return {
|
|
21
|
+
facts,
|
|
22
|
+
delta: {
|
|
23
|
+
addedSymbols: [],
|
|
24
|
+
removedSymbols: [],
|
|
25
|
+
modifiedSymbols: [],
|
|
26
|
+
changedSignatures: [],
|
|
27
|
+
addedImports: [],
|
|
28
|
+
removedImports: [],
|
|
29
|
+
changedExports: [],
|
|
30
|
+
},
|
|
31
|
+
...partial,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const classification = {
|
|
36
|
+
files: {
|
|
37
|
+
"src/bin-lookup/service.ts": { status: "added" },
|
|
38
|
+
"src/bin-lookup/provider.ts": { status: "added" },
|
|
39
|
+
},
|
|
40
|
+
churn: {
|
|
41
|
+
"src/bin-lookup/service.ts": { additions: 10, deletions: 0 },
|
|
42
|
+
"src/bin-lookup/provider.ts": { additions: 5, deletions: 0 },
|
|
43
|
+
},
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
46
|
+
describe("buildTechnicalSummary", () => {
|
|
47
|
+
test("extrai contratos, implementações, assinaturas e dependências do AST", () => {
|
|
48
|
+
const ast = new Map<string, AstAnalysis>([
|
|
49
|
+
[
|
|
50
|
+
"src/bin-lookup/service.ts",
|
|
51
|
+
analysis({
|
|
52
|
+
delta: {
|
|
53
|
+
addedSymbols: [
|
|
54
|
+
{
|
|
55
|
+
name: "BinLookupProvider",
|
|
56
|
+
kind: "interface",
|
|
57
|
+
exported: true,
|
|
58
|
+
startLine: 1,
|
|
59
|
+
endLine: 3,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: "BinLookupService",
|
|
63
|
+
kind: "class",
|
|
64
|
+
exported: true,
|
|
65
|
+
startLine: 5,
|
|
66
|
+
endLine: 20,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
removedSymbols: [],
|
|
70
|
+
modifiedSymbols: [],
|
|
71
|
+
changedSignatures: [
|
|
72
|
+
{
|
|
73
|
+
name: "lookup",
|
|
74
|
+
before: "(bin: string)",
|
|
75
|
+
after: "(bin: string, brand: string)",
|
|
76
|
+
exported: true,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
addedImports: ["./provider", "axios"],
|
|
80
|
+
removedImports: [],
|
|
81
|
+
changedExports: [{ name: "legacy", change: "removed" }],
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
],
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const summary = buildTechnicalSummary({
|
|
88
|
+
classification,
|
|
89
|
+
ast,
|
|
90
|
+
headSha: "abc",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(summary.source).toBe("deterministic");
|
|
94
|
+
expect(summary.headSha).toBe("abc");
|
|
95
|
+
const text = summary.highlights.map((item) => `${item.area}: ${item.detail}`);
|
|
96
|
+
expect(text.join("\n")).toContain("Contratos: `BinLookupProvider`");
|
|
97
|
+
expect(text.join("\n")).toContain("Implementações: `BinLookupService`");
|
|
98
|
+
expect(text.join("\n")).toContain("Assinaturas alteradas: `lookup`");
|
|
99
|
+
expect(text.join("\n")).toContain("Novas dependências: `axios`");
|
|
100
|
+
expect(text.join("\n")).not.toContain("./provider");
|
|
101
|
+
expect(text.join("\n")).toContain("Exports alterados: `legacy`");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("sem AST, agrupa arquivos novos por diretório", () => {
|
|
105
|
+
const summary = buildTechnicalSummary({ classification });
|
|
106
|
+
expect(summary.highlights[0]).toEqual({
|
|
107
|
+
area: "Estrutura",
|
|
108
|
+
detail: "Novo diretório `src/bin-lookup` com 2 arquivo(s).",
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("registra remoções e impacto", () => {
|
|
113
|
+
const impact: AstImpact = {
|
|
114
|
+
importers: { "src/bin-lookup/service.ts": ["src/a.ts"] },
|
|
115
|
+
affectedFiles: ["src/a.ts"],
|
|
116
|
+
exportedChanges: [
|
|
117
|
+
{
|
|
118
|
+
file: "src/bin-lookup/service.ts",
|
|
119
|
+
symbol: "lookup",
|
|
120
|
+
change: "removed",
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
const summary = buildTechnicalSummary({
|
|
125
|
+
classification: {
|
|
126
|
+
files: {
|
|
127
|
+
"src/old.ts": { status: "deleted" },
|
|
128
|
+
"src/bin-lookup/service.ts": { status: "modified" },
|
|
129
|
+
},
|
|
130
|
+
churn: {},
|
|
131
|
+
},
|
|
132
|
+
impact,
|
|
133
|
+
});
|
|
134
|
+
const text = summary.highlights.map((item) => `${item.area}: ${item.detail}`);
|
|
135
|
+
expect(text.join("\n")).toContain("Remoções: `src/old.ts`");
|
|
136
|
+
expect(text.join("\n")).toContain("Impacto: 1 arquivo(s) importam os alterados");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("não gera destaques quando não há mudança estrutural", () => {
|
|
140
|
+
const summary = buildTechnicalSummary({
|
|
141
|
+
classification: { files: {}, churn: {} },
|
|
142
|
+
});
|
|
143
|
+
expect(summary.highlights).toEqual([]);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe("parseTechnicalSummary", () => {
|
|
148
|
+
test("aceita artefato válido e normaliza o source", () => {
|
|
149
|
+
const summary = parseTechnicalSummary({
|
|
150
|
+
source: "deterministic",
|
|
151
|
+
headSha: "abc",
|
|
152
|
+
overview: "Estrutura",
|
|
153
|
+
highlights: [{ area: "Contratos", detail: "`X`" }],
|
|
154
|
+
});
|
|
155
|
+
expect(summary?.source).toBe("deterministic");
|
|
156
|
+
expect(summary?.highlights[0].area).toBe("Contratos");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rejeita conteúdo vazio ou inválido", () => {
|
|
160
|
+
expect(parseTechnicalSummary(null)).toBeNull();
|
|
161
|
+
expect(parseTechnicalSummary({})).toBeNull();
|
|
162
|
+
expect(
|
|
163
|
+
parseTechnicalSummary({ highlights: [{ area: "", detail: "" }] }),
|
|
164
|
+
).toBeNull();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("limita a seis destaques", () => {
|
|
168
|
+
const summary = parseTechnicalSummary({
|
|
169
|
+
highlights: Array.from({ length: 10 }, (_, index) => ({
|
|
170
|
+
area: `Área ${index}`,
|
|
171
|
+
detail: `Detalhe ${index}`,
|
|
172
|
+
})),
|
|
173
|
+
});
|
|
174
|
+
expect(summary?.highlights).toHaveLength(6);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
writeFileSync,
|
|
17
17
|
} from "node:fs";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
|
-
import { join } from "node:path";
|
|
19
|
+
import { basename, join } from "node:path";
|
|
20
20
|
import { parseReviewArgs } from "../core/commands/review";
|
|
21
21
|
import { HoundsmanEngine } from "../core/engine/houndsman-engine";
|
|
22
22
|
import { resolveReviewWorkspace } from "../core/engine/review-workspace";
|
|
@@ -151,6 +151,41 @@ describe("parseWorktreeList", () => {
|
|
|
151
151
|
});
|
|
152
152
|
});
|
|
153
153
|
|
|
154
|
+
describe("nomes de diretório do worktree", () => {
|
|
155
|
+
const manager = new WorktreeManager(
|
|
156
|
+
new GitRepository(createGitRunner(process.cwd())),
|
|
157
|
+
{
|
|
158
|
+
repoKey: "remote:github.com/tradinglabs-io/pay.conductor",
|
|
159
|
+
repoRoot: process.cwd(),
|
|
160
|
+
baseDir: join(tmpdir(), "houndsman-worktrees"),
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
test("repo e branch são slugs curtos com hash", () => {
|
|
165
|
+
// MAX_PATH do Windows: cada segmento fica limitado a legível + hash8.
|
|
166
|
+
expect(basename(manager.baseDir).length).toBeLessThanOrEqual(24 + 9);
|
|
167
|
+
expect(basename(manager.pathFor("main"))).toMatch(/^main-[0-9a-f]{8}$/);
|
|
168
|
+
|
|
169
|
+
const long = manager.pathFor("feat/fetch-branch-balance-split-accounts");
|
|
170
|
+
const name = basename(long);
|
|
171
|
+
expect(name).toMatch(/^[\w.-]{1,20}-[0-9a-f]{8}$/);
|
|
172
|
+
expect(name.length).toBeLessThanOrEqual(20 + 9);
|
|
173
|
+
expect(long.startsWith(manager.baseDir)).toBe(true);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("é determinístico e não colide em prefixos iguais", () => {
|
|
177
|
+
const branch = "feat/fetch-branch-balance-split-accounts";
|
|
178
|
+
expect(manager.pathFor(branch)).toBe(manager.pathFor(branch));
|
|
179
|
+
expect(manager.pathFor(branch)).not.toBe(
|
|
180
|
+
manager.pathFor("feat/fetch-branch-balance-split-other"),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("branches só de caracteres inválidos caem no fallback", () => {
|
|
185
|
+
expect(basename(manager.pathFor("///"))).toMatch(/^branch-[0-9a-f]{8}$/);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
154
189
|
describe("WorktreeManager", () => {
|
|
155
190
|
test("cria detached, reusa o mesmo commit e remove", () => {
|
|
156
191
|
const sandbox = makeSandbox();
|
|
@@ -180,7 +215,7 @@ describe("WorktreeManager", () => {
|
|
|
180
215
|
} finally {
|
|
181
216
|
sandbox.cleanup();
|
|
182
217
|
}
|
|
183
|
-
});
|
|
218
|
+
}, 20_000);
|
|
184
219
|
});
|
|
185
220
|
|
|
186
221
|
describe("revisões paralelas em worktrees", () => {
|
|
@@ -263,7 +298,7 @@ describe("revisões paralelas em worktrees", () => {
|
|
|
263
298
|
else process.env.HOUNDSMAN_HOME = originalHome;
|
|
264
299
|
sandbox.cleanup();
|
|
265
300
|
}
|
|
266
|
-
});
|
|
301
|
+
}, 30_000);
|
|
267
302
|
|
|
268
303
|
test("duas PRs preparam em paralelo sem se contaminar", async () => {
|
|
269
304
|
const sandbox = makeSandbox();
|
|
@@ -365,5 +400,5 @@ describe("revisões paralelas em worktrees", () => {
|
|
|
365
400
|
else process.env.HOUNDSMAN_HOME = originalHome;
|
|
366
401
|
sandbox.cleanup();
|
|
367
402
|
}
|
|
368
|
-
});
|
|
403
|
+
}, 30_000);
|
|
369
404
|
});
|
package/assets/logo-legacy.png
DELETED
|
Binary file
|