@payconductor/houndsman 0.3.0 → 0.4.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 +18 -6
- package/package.json +1 -1
- package/src/cli/commands/context-command.ts +11 -9
- package/src/cli/commands/doctor-command.ts +5 -0
- package/src/cli/commands/findings-command.ts +13 -3
- package/src/cli/commands/review-command.ts +1 -2
- package/src/cli/help.ts +7 -3
- package/src/cli/index.ts +2 -2
- package/src/core/commands/context.ts +29 -13
- package/src/core/commands/findings.ts +155 -120
- package/src/core/commands/review.ts +7 -8
- package/src/core/commands/scaffold.ts +15 -16
- package/src/core/config.ts +16 -7
- package/src/core/engine/houndsman-engine.ts +15 -1
- package/src/core/engine/review-session.ts +37 -9
- package/src/core/findings-state.ts +32 -71
- package/src/core/jev/jev-candidates.ts +49 -35
- package/src/core/orchestrator.ts +175 -128
- package/src/core/paths.ts +23 -17
- package/src/core/pr-context.ts +19 -5
- package/src/core/project-config.ts +4 -5
- package/src/core/renderers/manifest-writer.ts +29 -18
- package/src/core/run-layout.ts +4 -104
- package/src/core/run-status.ts +28 -53
- package/src/core/services/git-repository.ts +16 -0
- package/src/core/services/project-files.ts +13 -1
- package/src/core/storage/factory.ts +24 -0
- package/src/core/storage/file/file-storage.ts +269 -0
- package/src/core/storage/paths.ts +64 -0
- package/src/core/storage/repo-identity.ts +70 -0
- package/src/core/storage/sqlite/mappers.ts +160 -0
- package/src/core/storage/sqlite/schema.ts +108 -0
- package/src/core/storage/sqlite/sqlite-storage.ts +427 -0
- package/src/core/storage/types.ts +143 -0
- package/src/core/storage/work-area.ts +191 -0
- package/src/index.ts +27 -6
- package/src/server/artifacts.ts +8 -61
- package/src/server/client/ArtifactsPanel.tsx +137 -23
- package/src/server/client/FindingCard.tsx +37 -6
- package/src/server/client/MarkdownText.tsx +101 -0
- package/src/server/client/SubmitModal.tsx +9 -4
- package/src/server/client/code-highlight.ts +422 -0
- package/src/server/client/constants.ts +130 -24
- package/src/server/client/json-highlight.ts +55 -0
- package/src/server/client/markdown.ts +153 -0
- package/src/server/client/types.ts +23 -0
- package/src/server/index.ts +95 -15
- package/src/server/run-repository.ts +144 -136
- package/src/skill/OUTPUT-FORMAT.md +6 -4
- package/src/skill/README.md +22 -14
- package/src/skill/SETUP.md +4 -4
- package/src/skill/fix.md +5 -5
- package/src/skill/hunt.md +27 -18
- package/src/skill/train.md +1 -1
- package/src/skill/verify.md +5 -5
- package/src/tests/__snapshots__/baseline.test.ts.snap +44 -33
- package/src/tests/baseline.test.ts +76 -34
- package/src/tests/cli.test.ts +40 -19
- package/src/tests/code-highlight.test.ts +123 -0
- package/src/tests/domain.test.ts +1 -1
- package/src/tests/findings-commands.test.ts +153 -131
- package/src/tests/findings-state.test.ts +6 -3
- package/src/tests/fixtures/fake-project.ts +0 -1
- package/src/tests/infrastructure.test.ts +33 -38
- package/src/tests/markdown.test.ts +89 -0
- package/src/tests/review-scripts.test.ts +8 -5
- package/src/tests/run-status.test.ts +33 -23
- package/src/tests/server.test.ts +108 -75
- package/src/tests/storage.test.ts +386 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destaque de código do evidence — tokenizer próprio, sem dependência.
|
|
3
|
+
*
|
|
4
|
+
* Garante que a inferência por extensão funciona, que o texto original é
|
|
5
|
+
* preservado e que os tokens principais são classificados.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
highlightCode,
|
|
11
|
+
languageFromFile,
|
|
12
|
+
languageFromName,
|
|
13
|
+
} from "../server/client/code-highlight";
|
|
14
|
+
|
|
15
|
+
/** O texto concatenado dos tokens deve ser idêntico ao input. */
|
|
16
|
+
function joined(tokens: { text: string }[]): string {
|
|
17
|
+
return tokens.map((token) => token.text).join("");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("languageFromFile", () => {
|
|
21
|
+
test("infere pela extensão e ignora extensões desconhecidas", () => {
|
|
22
|
+
expect(languageFromFile("src/a.ts")).toBe("typescript");
|
|
23
|
+
expect(languageFromFile("src/a.tsx")).toBe("typescript");
|
|
24
|
+
expect(languageFromFile("scripts/a.py")).toBe("python");
|
|
25
|
+
expect(languageFromFile("db/a.sql")).toBe("sql");
|
|
26
|
+
expect(languageFromFile("charts/a.yaml")).toBe("yaml");
|
|
27
|
+
expect(languageFromFile("README.md")).toBeNull();
|
|
28
|
+
expect(languageFromFile("Makefile")).toBeNull();
|
|
29
|
+
expect(languageFromFile("a.unknown")).toBeNull();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("aliases de fence markdown", () => {
|
|
33
|
+
expect(languageFromName("ts")).toBe("typescript");
|
|
34
|
+
expect(languageFromName("js")).toBe("javascript");
|
|
35
|
+
expect(languageFromName("py")).toBe("python");
|
|
36
|
+
expect(languageFromName("json")).toBe("json");
|
|
37
|
+
expect(languageFromName("yml")).toBe("yaml");
|
|
38
|
+
expect(languageFromName("cobol")).toBeNull();
|
|
39
|
+
expect(languageFromName(null)).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("highlightCode", () => {
|
|
44
|
+
test("TypeScript: keywords, tipos, números, strings e comentários", () => {
|
|
45
|
+
const input = 'const answer: number = 42; // resposta\nconst url = "https://x";';
|
|
46
|
+
const result = highlightCode(input, "typescript");
|
|
47
|
+
expect(result?.label).toBe("TypeScript");
|
|
48
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
49
|
+
expect(result?.tokens.find((token) => token.text === "const")?.type).toBe(
|
|
50
|
+
"keyword",
|
|
51
|
+
);
|
|
52
|
+
expect(result?.tokens.find((token) => token.text === "number")?.type).toBe(
|
|
53
|
+
"type",
|
|
54
|
+
);
|
|
55
|
+
expect(result?.tokens.find((token) => token.text === "42")?.type).toBe("number");
|
|
56
|
+
expect(result?.tokens.find((token) => token.text === "// resposta")?.type).toBe(
|
|
57
|
+
"comment",
|
|
58
|
+
);
|
|
59
|
+
expect(result?.tokens.find((token) => token.text === '"https://x"')?.type).toBe(
|
|
60
|
+
"string",
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("Python: comentários com # e strings de três aspas", () => {
|
|
65
|
+
const input = 'def greet(name):\n """doc"""\n return "hi" # ok';
|
|
66
|
+
const result = highlightCode(input, "python");
|
|
67
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
68
|
+
expect(result?.tokens.find((token) => token.text === "def")?.type).toBe(
|
|
69
|
+
"keyword",
|
|
70
|
+
);
|
|
71
|
+
expect(result?.tokens.find((token) => token.text === "return")?.type).toBe(
|
|
72
|
+
"keyword",
|
|
73
|
+
);
|
|
74
|
+
expect(result?.tokens.find((token) => token.text === '"""doc"""')?.type).toBe(
|
|
75
|
+
"string",
|
|
76
|
+
);
|
|
77
|
+
expect(result?.tokens.find((token) => token.text === "# ok")?.type).toBe(
|
|
78
|
+
"comment",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("JSON: chave vira propriedade e literal vira keyword", () => {
|
|
83
|
+
const input = '{"a": 1, "b": true}';
|
|
84
|
+
const result = highlightCode(input, "json");
|
|
85
|
+
expect(result?.label).toBe("JSON");
|
|
86
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
87
|
+
expect(result?.tokens.find((token) => token.text === '"a"')?.type).toBe(
|
|
88
|
+
"property",
|
|
89
|
+
);
|
|
90
|
+
expect(result?.tokens.find((token) => token.text === "true")?.type).toBe(
|
|
91
|
+
"keyword",
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("HTML: tag e atributo", () => {
|
|
96
|
+
const input = '<div class="x">hi</div>';
|
|
97
|
+
const result = highlightCode(input, "html");
|
|
98
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
99
|
+
expect(result?.tokens.find((token) => token.text === "div")?.type).toBe(
|
|
100
|
+
"keyword",
|
|
101
|
+
);
|
|
102
|
+
expect(result?.tokens.find((token) => token.text === "class")?.type).toBe(
|
|
103
|
+
"property",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("YAML: chave no início da linha e comentário", () => {
|
|
108
|
+
const input = "name: value # note";
|
|
109
|
+
const result = highlightCode(input, "yaml");
|
|
110
|
+
expect(joined(result?.tokens ?? [])).toBe(input);
|
|
111
|
+
expect(result?.tokens.find((token) => token.text === "name")?.type).toBe(
|
|
112
|
+
"property",
|
|
113
|
+
);
|
|
114
|
+
expect(result?.tokens.find((token) => token.text === "# note")?.type).toBe(
|
|
115
|
+
"comment",
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("sem linguagem/desconhecida devolve null", () => {
|
|
120
|
+
expect(highlightCode("const x = 1", null)).toBeNull();
|
|
121
|
+
expect(highlightCode("const x = 1", "cobol")).toBeNull();
|
|
122
|
+
});
|
|
123
|
+
});
|
package/src/tests/domain.test.ts
CHANGED
|
@@ -102,7 +102,7 @@ describe("ProjectConfig", () => {
|
|
|
102
102
|
test("resolve defaults e valores do projeto", () => {
|
|
103
103
|
const project = new ProjectConfig(fakeHoundsmanConfig);
|
|
104
104
|
expect(project.projectName).toBe("Fake Project");
|
|
105
|
-
expect(project.
|
|
105
|
+
expect(project.storage).toEqual({});
|
|
106
106
|
expect(project.maxFilesPerBrief).toBe(40);
|
|
107
107
|
expect(project.jevConcurrency).toBe(5);
|
|
108
108
|
expect(project.alwaysIncludeDocs).toEqual(["README.md"]);
|
|
@@ -1,51 +1,106 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
existsSync,
|
|
4
|
-
mkdirSync,
|
|
5
|
-
mkdtempSync,
|
|
6
|
-
readFileSync,
|
|
7
|
-
rmSync,
|
|
8
|
-
writeFileSync,
|
|
9
|
-
} from "node:fs";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
10
3
|
import { tmpdir } from "node:os";
|
|
11
4
|
import { join } from "node:path";
|
|
12
5
|
import { runFindingsCommand, runVerifyCommand } from "../core/commands/findings";
|
|
6
|
+
import { buildStateFromFindings, fileReader } from "../core/findings-state";
|
|
7
|
+
import {
|
|
8
|
+
dedupeFindings,
|
|
9
|
+
parseWatchdogReport,
|
|
10
|
+
type WatchdogReport,
|
|
11
|
+
} from "../core/report-builder";
|
|
12
|
+
import { buildRepoIdentity } from "../core/storage/repo-identity";
|
|
13
|
+
import { SQLiteStorageProvider } from "../core/storage/sqlite/sqlite-storage";
|
|
14
|
+
import type { RepoIdentity } from "../core/storage/types";
|
|
13
15
|
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
14
16
|
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
+
/** Ambiente temporário: banco + review semeada com os findings informados. */
|
|
18
|
+
type Env = {
|
|
19
|
+
storage: SQLiteStorageProvider;
|
|
20
|
+
repo: RepoIdentity;
|
|
21
|
+
reviewId: string;
|
|
22
|
+
dir: string;
|
|
23
|
+
cleanup: () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let activeEnv: Env | null = null;
|
|
27
|
+
|
|
28
|
+
function makeRun(reports: WatchdogReport[], branch = "main"): Env {
|
|
17
29
|
const dir = mkdtempSync(join(tmpdir(), "houndsman-findings-"));
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
const storage = new SQLiteStorageProvider(join(dir, "houndsman.db"));
|
|
31
|
+
const repo = buildRepoIdentity(null, dir);
|
|
32
|
+
const review = storage.getOrCreateReview({ repo, branch, slug: branch });
|
|
33
|
+
const state = buildStateFromFindings(dedupeFindings(reports), {
|
|
34
|
+
branch,
|
|
35
|
+
readFile: fileReader(process.cwd()),
|
|
36
|
+
});
|
|
37
|
+
storage.writeFindings(review.id, state);
|
|
38
|
+
const env: Env = {
|
|
39
|
+
storage,
|
|
40
|
+
repo,
|
|
41
|
+
reviewId: review.id,
|
|
42
|
+
dir,
|
|
43
|
+
cleanup: () => {
|
|
44
|
+
storage.close();
|
|
45
|
+
rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
activeEnv = env;
|
|
49
|
+
return env;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function reportOf(
|
|
53
|
+
watchdogId: string,
|
|
54
|
+
findings: unknown[],
|
|
55
|
+
files: string[],
|
|
56
|
+
): WatchdogReport {
|
|
57
|
+
const report = parseWatchdogReport(
|
|
58
|
+
{ watchdogId, reviewedFiles: files, skipped: [], findings },
|
|
59
|
+
`${watchdogId}.json`,
|
|
32
60
|
);
|
|
33
|
-
|
|
61
|
+
if (!report) throw new Error("relatório de teste inválido");
|
|
62
|
+
return report;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
const ONE_FINDING = [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
66
|
+
reportOf(
|
|
67
|
+
"alpha",
|
|
68
|
+
[
|
|
69
|
+
{
|
|
70
|
+
rule: "Nunca usar any",
|
|
71
|
+
severity: "blocking",
|
|
72
|
+
file: "src/alpha/f.ts",
|
|
73
|
+
line: 2,
|
|
74
|
+
message: "any detectado",
|
|
75
|
+
action: "tipar",
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
["src/alpha/f.ts"],
|
|
79
|
+
),
|
|
45
80
|
];
|
|
46
81
|
|
|
82
|
+
/** Remove `--dir` (flag extinta) dos args legados usados nos testes. */
|
|
83
|
+
function stripDir(args: string[]): string[] {
|
|
84
|
+
const out: string[] = [];
|
|
85
|
+
for (let i = 0; i < args.length; i++) {
|
|
86
|
+
if (args[i] === "--dir") {
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
out.push(args[i]);
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Executa o comando contra o ambiente ativo. */
|
|
47
96
|
function run(args: string[]) {
|
|
48
|
-
|
|
97
|
+
if (!activeEnv) throw new Error("makeRun não foi chamado");
|
|
98
|
+
return runFindingsCommand(
|
|
99
|
+
fakeHoundsmanConfig,
|
|
100
|
+
activeEnv.storage,
|
|
101
|
+
activeEnv.repo,
|
|
102
|
+
stripDir(args),
|
|
103
|
+
);
|
|
49
104
|
}
|
|
50
105
|
|
|
51
106
|
describe("findings command", () => {
|
|
@@ -196,11 +251,18 @@ describe("findings command", () => {
|
|
|
196
251
|
expect(missing.output).toContain("não encontrado");
|
|
197
252
|
|
|
198
253
|
const empty = mkdtempSync(join(tmpdir(), "houndsman-empty-"));
|
|
254
|
+
const emptyStorage = new SQLiteStorageProvider(join(empty, "db.sqlite"));
|
|
199
255
|
try {
|
|
200
|
-
const absent =
|
|
256
|
+
const absent = runFindingsCommand(
|
|
257
|
+
fakeHoundsmanConfig,
|
|
258
|
+
emptyStorage,
|
|
259
|
+
buildRepoIdentity(null, empty),
|
|
260
|
+
["list", "--branch", "main"],
|
|
261
|
+
);
|
|
201
262
|
expect(absent.exitCode).toBe(1);
|
|
202
263
|
expect(absent.output).toContain("--finalize");
|
|
203
264
|
} finally {
|
|
265
|
+
emptyStorage.close();
|
|
204
266
|
rmSync(empty, { recursive: true, force: true });
|
|
205
267
|
}
|
|
206
268
|
} finally {
|
|
@@ -208,13 +270,14 @@ describe("findings command", () => {
|
|
|
208
270
|
}
|
|
209
271
|
});
|
|
210
272
|
|
|
211
|
-
test("path devolve
|
|
273
|
+
test("path devolve a referência do storage", () => {
|
|
212
274
|
const run0 = makeRun(ONE_FINDING);
|
|
213
275
|
try {
|
|
214
276
|
run(["list", "--dir", run0.dir, "--branch", "main"]);
|
|
215
277
|
const result = run(["path", "--dir", run0.dir, "--branch", "main"]);
|
|
216
278
|
expect(result.exitCode).toBe(0);
|
|
217
|
-
expect(result.output
|
|
279
|
+
expect(result.output).toContain("sqlite:");
|
|
280
|
+
expect(result.output).toContain(run0.reviewId);
|
|
218
281
|
} finally {
|
|
219
282
|
run0.cleanup();
|
|
220
283
|
}
|
|
@@ -232,20 +295,10 @@ describe("findings command", () => {
|
|
|
232
295
|
});
|
|
233
296
|
|
|
234
297
|
test("batch agrupa a fila e devolve o grupo atual + outros grupos", () => {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
join(outDir, "classification.json"),
|
|
240
|
-
JSON.stringify({ watchdogs: { alpha: ["src/a.ts"], beta: ["src/b.ts"] } }),
|
|
241
|
-
);
|
|
242
|
-
writeFileSync(
|
|
243
|
-
join(outDir, "findings", "alpha.json"),
|
|
244
|
-
JSON.stringify({
|
|
245
|
-
watchdogId: "alpha",
|
|
246
|
-
reviewedFiles: ["src/a.ts"],
|
|
247
|
-
skipped: [],
|
|
248
|
-
findings: [
|
|
298
|
+
const env = makeRun([
|
|
299
|
+
reportOf(
|
|
300
|
+
"alpha",
|
|
301
|
+
[
|
|
249
302
|
{
|
|
250
303
|
rule: "R1",
|
|
251
304
|
severity: "blocking",
|
|
@@ -279,15 +332,11 @@ describe("findings command", () => {
|
|
|
279
332
|
action: "a",
|
|
280
333
|
},
|
|
281
334
|
],
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
watchdogId: "beta",
|
|
288
|
-
reviewedFiles: ["src/b.ts"],
|
|
289
|
-
skipped: [],
|
|
290
|
-
findings: [
|
|
335
|
+
["src/a.ts"],
|
|
336
|
+
),
|
|
337
|
+
reportOf(
|
|
338
|
+
"beta",
|
|
339
|
+
[
|
|
291
340
|
{
|
|
292
341
|
rule: "R5",
|
|
293
342
|
severity: "attention",
|
|
@@ -305,26 +354,16 @@ describe("findings command", () => {
|
|
|
305
354
|
action: "a",
|
|
306
355
|
},
|
|
307
356
|
],
|
|
308
|
-
|
|
309
|
-
|
|
357
|
+
["src/b.ts"],
|
|
358
|
+
),
|
|
359
|
+
]);
|
|
310
360
|
try {
|
|
311
|
-
const list = JSON.parse(
|
|
312
|
-
run(["list", "--dir", dir, "--branch", "main", "--json"]).output,
|
|
313
|
-
);
|
|
361
|
+
const list = JSON.parse(run(["list", "--branch", "main", "--json"]).output);
|
|
314
362
|
for (const item of list) {
|
|
315
|
-
run([
|
|
316
|
-
"set",
|
|
317
|
-
item.id,
|
|
318
|
-
"--decision",
|
|
319
|
-
"execute",
|
|
320
|
-
"--dir",
|
|
321
|
-
dir,
|
|
322
|
-
"--branch",
|
|
323
|
-
"main",
|
|
324
|
-
]);
|
|
363
|
+
run(["set", item.id, "--decision", "execute", "--branch", "main"]);
|
|
325
364
|
}
|
|
326
365
|
|
|
327
|
-
const result = run(["batch", "--
|
|
366
|
+
const result = run(["batch", "--branch", "main", "--json"]);
|
|
328
367
|
expect(result.exitCode).toBe(0);
|
|
329
368
|
const parsed = JSON.parse(result.output);
|
|
330
369
|
expect(parsed.dimension).toBe("file");
|
|
@@ -336,7 +375,7 @@ describe("findings command", () => {
|
|
|
336
375
|
expect.objectContaining({ key: "file:src/b.ts", count: 2 }),
|
|
337
376
|
]);
|
|
338
377
|
|
|
339
|
-
const markdown = run(["batch", "--
|
|
378
|
+
const markdown = run(["batch", "--branch", "main"]);
|
|
340
379
|
expect(markdown.output).toContain("Grupo atual");
|
|
341
380
|
expect(markdown.output).toContain("Outros grupos");
|
|
342
381
|
|
|
@@ -344,25 +383,23 @@ describe("findings command", () => {
|
|
|
344
383
|
"batch",
|
|
345
384
|
"--group",
|
|
346
385
|
"file:src/b.ts",
|
|
347
|
-
"--dir",
|
|
348
|
-
dir,
|
|
349
386
|
"--branch",
|
|
350
387
|
"main",
|
|
351
388
|
"--json",
|
|
352
389
|
]);
|
|
353
390
|
expect(JSON.parse(scoped.output).current.key).toBe("file:src/b.ts");
|
|
354
391
|
} finally {
|
|
355
|
-
|
|
392
|
+
env.cleanup();
|
|
356
393
|
}
|
|
357
394
|
});
|
|
358
395
|
|
|
359
|
-
test("mutações fazem ping no
|
|
396
|
+
test("mutações fazem ping no status da review", () => {
|
|
360
397
|
const run0 = makeRun(ONE_FINDING);
|
|
361
398
|
try {
|
|
362
399
|
const id = JSON.parse(
|
|
363
400
|
run(["list", "--dir", run0.dir, "--branch", "main", "--json"]).output,
|
|
364
401
|
)[0].id;
|
|
365
|
-
const
|
|
402
|
+
const status = () => run0.storage.getReview(run0.reviewId)?.status;
|
|
366
403
|
|
|
367
404
|
run([
|
|
368
405
|
"set",
|
|
@@ -374,7 +411,7 @@ describe("findings command", () => {
|
|
|
374
411
|
"--branch",
|
|
375
412
|
"main",
|
|
376
413
|
]);
|
|
377
|
-
expect(
|
|
414
|
+
expect(status()).toBe("review_done");
|
|
378
415
|
|
|
379
416
|
run([
|
|
380
417
|
"set",
|
|
@@ -386,10 +423,10 @@ describe("findings command", () => {
|
|
|
386
423
|
"--branch",
|
|
387
424
|
"main",
|
|
388
425
|
]);
|
|
389
|
-
expect(
|
|
426
|
+
expect(status()).toBe("fixing");
|
|
390
427
|
|
|
391
428
|
run(["done", id, "--dir", run0.dir, "--branch", "main"]);
|
|
392
|
-
expect(
|
|
429
|
+
expect(status()).toBe("fix_done");
|
|
393
430
|
} finally {
|
|
394
431
|
run0.cleanup();
|
|
395
432
|
}
|
|
@@ -400,55 +437,44 @@ describe("findings verify", () => {
|
|
|
400
437
|
const FILE = "src/tests/fixtures/fake-project.ts";
|
|
401
438
|
|
|
402
439
|
/** Run fake apontando para um arquivo real (relativo ao cwd → âncora resolvível). */
|
|
403
|
-
function makeVerifyRun():
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
440
|
+
function makeVerifyRun(): Env {
|
|
441
|
+
return makeRun(
|
|
442
|
+
[
|
|
443
|
+
reportOf(
|
|
444
|
+
"alpha",
|
|
445
|
+
[
|
|
446
|
+
{
|
|
447
|
+
rule: "regra",
|
|
448
|
+
severity: "attention",
|
|
449
|
+
file: FILE,
|
|
450
|
+
line: 1,
|
|
451
|
+
message: "msg",
|
|
452
|
+
action: "ação",
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
[FILE],
|
|
456
|
+
),
|
|
457
|
+
],
|
|
458
|
+
"main",
|
|
410
459
|
);
|
|
411
|
-
writeFileSync(
|
|
412
|
-
join(outDir, "findings", "alpha.json"),
|
|
413
|
-
JSON.stringify({
|
|
414
|
-
watchdogId: "alpha",
|
|
415
|
-
reviewedFiles: [FILE],
|
|
416
|
-
skipped: [],
|
|
417
|
-
findings: [
|
|
418
|
-
{
|
|
419
|
-
rule: "regra",
|
|
420
|
-
severity: "attention",
|
|
421
|
-
file: FILE,
|
|
422
|
-
line: 1,
|
|
423
|
-
message: "msg",
|
|
424
|
-
action: "ação",
|
|
425
|
-
},
|
|
426
|
-
],
|
|
427
|
-
}),
|
|
428
|
-
);
|
|
429
|
-
return {
|
|
430
|
-
dir,
|
|
431
|
-
outDir,
|
|
432
|
-
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
433
|
-
};
|
|
434
460
|
}
|
|
435
461
|
|
|
436
462
|
test("reporta persisting e grava o verify.md (tier local)", async () => {
|
|
437
463
|
const run0 = makeVerifyRun();
|
|
438
464
|
try {
|
|
439
|
-
const result = await runVerifyCommand(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
run0.
|
|
443
|
-
"--branch",
|
|
444
|
-
|
|
445
|
-
"--json",
|
|
446
|
-
]);
|
|
465
|
+
const result = await runVerifyCommand(
|
|
466
|
+
fakeHoundsmanConfig,
|
|
467
|
+
run0.storage,
|
|
468
|
+
run0.repo,
|
|
469
|
+
["verify", "--branch", "main", "--json"],
|
|
470
|
+
);
|
|
447
471
|
expect(result.exitCode).toBe(0);
|
|
448
472
|
const parsed = JSON.parse(result.output);
|
|
449
473
|
expect(parsed.summary.persisting).toBe(1);
|
|
450
474
|
expect(parsed.entries[0].basis).toBe("anchor");
|
|
451
|
-
expect(
|
|
475
|
+
expect(
|
|
476
|
+
run0.storage.readArtifact(run0.reviewId, "verify.md")?.content,
|
|
477
|
+
).toContain("# Verificação");
|
|
452
478
|
} finally {
|
|
453
479
|
run0.cleanup();
|
|
454
480
|
}
|
|
@@ -457,17 +483,13 @@ describe("findings verify", () => {
|
|
|
457
483
|
test("sem --apply não altera o estado", async () => {
|
|
458
484
|
const run0 = makeVerifyRun();
|
|
459
485
|
try {
|
|
460
|
-
await runVerifyCommand(fakeHoundsmanConfig, [
|
|
486
|
+
await runVerifyCommand(fakeHoundsmanConfig, run0.storage, run0.repo, [
|
|
461
487
|
"verify",
|
|
462
|
-
"--dir",
|
|
463
|
-
run0.dir,
|
|
464
488
|
"--branch",
|
|
465
489
|
"main",
|
|
466
490
|
]);
|
|
467
|
-
const state =
|
|
468
|
-
|
|
469
|
-
) as { findings: Record<string, { execState: string }> };
|
|
470
|
-
const only = Object.values(state.findings)[0];
|
|
491
|
+
const state = run0.storage.readFindings(run0.reviewId);
|
|
492
|
+
const only = Object.values(state?.findings ?? {})[0];
|
|
471
493
|
expect(only.execState).toBe("pending");
|
|
472
494
|
} finally {
|
|
473
495
|
run0.cleanup();
|
|
@@ -237,14 +237,14 @@ describe("FindingsStateStore", () => {
|
|
|
237
237
|
const project = makeProject({ "src/f.ts": DUPLICATED });
|
|
238
238
|
const outDir = join(project.cwd, "artifacts");
|
|
239
239
|
try {
|
|
240
|
-
const store = FindingsStateStore
|
|
240
|
+
const store = new FindingsStateStore(join(outDir, "state.json"), "feature/x");
|
|
241
241
|
const state = buildStateFromFindings(
|
|
242
242
|
[merged({ file: "src/f.ts", rule: "r", line: 2 })],
|
|
243
243
|
{ branch: "feature/x", readFile: fileReader(project.cwd) },
|
|
244
244
|
);
|
|
245
245
|
store.write(state);
|
|
246
246
|
|
|
247
|
-
expect(store.filePath).toBe(join(outDir, "
|
|
247
|
+
expect(store.filePath).toBe(join(outDir, "state.json"));
|
|
248
248
|
expect(existsSync(store.filePath)).toBe(true);
|
|
249
249
|
const read = store.read();
|
|
250
250
|
expect(read?.slug).toBe("feature-x");
|
|
@@ -257,7 +257,10 @@ describe("FindingsStateStore", () => {
|
|
|
257
257
|
test("read devolve null quando o arquivo não existe", () => {
|
|
258
258
|
const project = makeProject({});
|
|
259
259
|
try {
|
|
260
|
-
const store = FindingsStateStore
|
|
260
|
+
const store = new FindingsStateStore(
|
|
261
|
+
join(project.cwd, "artifacts", "state.json"),
|
|
262
|
+
"main",
|
|
263
|
+
);
|
|
261
264
|
expect(store.read()).toBeNull();
|
|
262
265
|
expect(store.update(() => {})).toBeTruthy();
|
|
263
266
|
} finally {
|
|
@@ -56,7 +56,6 @@ export type FakeWatchdogId = keyof typeof FAKE_WATCHDOG_DEFINITIONS;
|
|
|
56
56
|
export const fakeHoundsmanConfig: HoundsmanConfig<FakeWatchdogId> =
|
|
57
57
|
defineHoundsmanConfig<FakeWatchdogId>({
|
|
58
58
|
projectName: "Fake Project",
|
|
59
|
-
artifactDir: "tmp/fake-review",
|
|
60
59
|
watchdogs: FAKE_WATCHDOG_DEFINITIONS,
|
|
61
60
|
exclusions: [/^dist\//, /\.lock$/],
|
|
62
61
|
docMappings: [[/^src\//, ["README.md"]]],
|