@payconductor/houndsman 0.1.3 → 0.2.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.
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Smoke test do CLI (`src/cli/index.ts`).
3
+ *
4
+ * O `main()` carrega cada comando por **import dinâmico** — este teste invoca o
5
+ * CLI como processo (num projeto mínimo em disco) para garantir que os caminhos
6
+ * lazy resolvem em runtime. É a única cobertura end-to-end do binário.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
11
+ import { tmpdir } from "node:os";
12
+ import { join } from "node:path";
13
+
14
+ /** Caminho do executável do CLI (TS, rodado pelo Bun). */
15
+ const CLI = join(import.meta.dir, "..", "cli", "index.ts");
16
+
17
+ /** Config mínima, como objeto literal (o `loadConfig` deriva `watchdogIds`). */
18
+ const CONFIG = `export default {
19
+ projectName: "Smoke",
20
+ watchdogs: {
21
+ alpha: {
22
+ id: "alpha",
23
+ name: "Alpha",
24
+ description: "Watchdog de smoke.",
25
+ scope: "src/**",
26
+ classify: [{ value: "^src/" }],
27
+ rules: [{ severity: "attention", text: "Evitar any." }],
28
+ contextFiles: [],
29
+ },
30
+ },
31
+ exclusions: [],
32
+ docMappings: [],
33
+ };
34
+ `;
35
+
36
+ type Project = { cwd: string; artifacts: string; cleanup: () => void };
37
+
38
+ /** Projeto temporário: config + uma run com um finding. */
39
+ function makeProject(): Project {
40
+ const cwd = mkdtempSync(join(tmpdir(), "houndsman-cli-"));
41
+ const artifacts = join(cwd, ".houndsman", ".runs");
42
+ const runDir = join(artifacts, "main");
43
+ mkdirSync(join(runDir, "findings"), { recursive: true });
44
+ writeFileSync(join(cwd, "houndsman.config.ts"), CONFIG);
45
+ writeFileSync(
46
+ join(runDir, "classification.json"),
47
+ JSON.stringify({ watchdogs: { alpha: ["src/a.ts"] } }),
48
+ );
49
+ writeFileSync(
50
+ join(runDir, "findings", "alpha.json"),
51
+ JSON.stringify({
52
+ watchdogId: "alpha",
53
+ reviewedFiles: ["src/a.ts"],
54
+ skipped: [],
55
+ findings: [
56
+ {
57
+ rule: "Evitar any",
58
+ severity: "attention",
59
+ file: "src/a.ts",
60
+ line: 1,
61
+ message: "any detectado",
62
+ action: "tipar",
63
+ },
64
+ ],
65
+ }),
66
+ );
67
+ return {
68
+ cwd,
69
+ artifacts,
70
+ cleanup: () => rmSync(cwd, { recursive: true, force: true }),
71
+ };
72
+ }
73
+
74
+ type CliResult = { code: number; stdout: string; stderr: string };
75
+
76
+ /** Invoca o CLI como processo, no cwd informado. */
77
+ function runCli(cwd: string, args: string[]): CliResult {
78
+ const proc = Bun.spawnSync(["bun", CLI, ...args], {
79
+ cwd,
80
+ stdout: "pipe",
81
+ stderr: "pipe",
82
+ });
83
+ return {
84
+ code: proc.exitCode ?? -1,
85
+ stdout: proc.stdout.toString(),
86
+ stderr: proc.stderr.toString(),
87
+ };
88
+ }
89
+
90
+ describe("cli smoke — imports dinâmicos", () => {
91
+ test("help imprime o uso (sem carregar config)", () => {
92
+ const result = runCli(process.cwd(), ["help"]);
93
+ expect(result.code).toBe(0);
94
+ expect(result.stdout).toContain("Uso: houndsman");
95
+ });
96
+
97
+ test("watchdog <id> devolve o contexto (import lazy de commands/watchdog)", () => {
98
+ const project = makeProject();
99
+ try {
100
+ const result = runCli(project.cwd, ["watchdog", "alpha", "--json"]);
101
+ expect(result.code).toBe(0);
102
+ expect(JSON.parse(result.stdout).id).toBe("alpha");
103
+ } finally {
104
+ project.cleanup();
105
+ }
106
+ });
107
+
108
+ test("findings summary devolve a triagem (import lazy de commands/findings)", () => {
109
+ const project = makeProject();
110
+ try {
111
+ const result = runCli(project.cwd, [
112
+ "findings",
113
+ "summary",
114
+ "--json",
115
+ "--dir",
116
+ project.artifacts,
117
+ "--branch",
118
+ "main",
119
+ ]);
120
+ expect(result.code).toBe(0);
121
+ expect(JSON.parse(result.stdout).total).toBe(1);
122
+ } finally {
123
+ project.cleanup();
124
+ }
125
+ });
126
+
127
+ test("findings submit --dry-run devolve a prévia (fail-open sem gh)", () => {
128
+ const project = makeProject();
129
+ try {
130
+ const result = runCli(project.cwd, [
131
+ "findings",
132
+ "submit",
133
+ "--dry-run",
134
+ "--json",
135
+ "--dir",
136
+ project.artifacts,
137
+ "--branch",
138
+ "main",
139
+ ]);
140
+ expect(result.code).toBe(0);
141
+ expect(JSON.parse(result.stdout).totals.candidates).toBe(1);
142
+ } finally {
143
+ project.cleanup();
144
+ }
145
+ });
146
+
147
+ test("doctor valida a config (import lazy de registry-check)", () => {
148
+ const project = makeProject();
149
+ try {
150
+ const result = runCli(project.cwd, ["doctor"]);
151
+ expect(result.code).toBe(0);
152
+ expect(result.stdout).toContain("Validando");
153
+ } finally {
154
+ project.cleanup();
155
+ }
156
+ });
157
+
158
+ test("context é fail-open sem PR (import lazy de commands/context)", () => {
159
+ const project = makeProject();
160
+ try {
161
+ const result = runCli(project.cwd, ["context", "--branch", "main"]);
162
+ expect(result.code).toBe(0);
163
+ } finally {
164
+ project.cleanup();
165
+ }
166
+ });
167
+ });
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Testes do envio da revisão ao GitHub (`core/github-review.ts`).
3
+ *
4
+ * Usam um runner fake do `gh` — nenhum teste toca a rede/repositório real.
5
+ */
6
+
7
+ import { describe, expect, test } from "bun:test";
8
+ import {
9
+ emptyState,
10
+ type FindingDecision,
11
+ type FindingExecState,
12
+ type FindingState,
13
+ type FindingsStateFile,
14
+ } from "../core/findings-state";
15
+ import {
16
+ buildCommentBody,
17
+ buildSubmitPreview,
18
+ type GhResult,
19
+ type GhRunner,
20
+ type PrTarget,
21
+ renderGeneralBody,
22
+ resolvePrTarget,
23
+ selectSubmitCandidates,
24
+ submitFindings,
25
+ } from "../core/github-review";
26
+ import type { Severity } from "../core/types";
27
+
28
+ type FindingSpec = {
29
+ id: string;
30
+ severity?: Severity;
31
+ decision?: FindingDecision;
32
+ execState?: FindingExecState;
33
+ line?: number | null;
34
+ present?: boolean;
35
+ rule?: string;
36
+ file?: string;
37
+ commentId?: number;
38
+ };
39
+
40
+ function finding(spec: FindingSpec): FindingState {
41
+ return {
42
+ id: spec.id,
43
+ fingerprint: spec.id,
44
+ watchdogIds: ["alpha"],
45
+ rule: spec.rule ?? "Regra de teste",
46
+ severity: spec.severity ?? "blocking",
47
+ file: spec.file ?? "src/a.ts",
48
+ line: spec.line === undefined ? 3 : spec.line,
49
+ message: "mensagem",
50
+ action: "corrigir",
51
+ evidence: "const x = 1;",
52
+ anchor: null,
53
+ count: 1,
54
+ present: spec.present ?? true,
55
+ decision: spec.decision ?? "pending",
56
+ execState: spec.execState ?? "pending",
57
+ createdAt: "2026-01-01T00:00:00.000Z",
58
+ updatedAt: "2026-01-01T00:00:00.000Z",
59
+ history: [],
60
+ github: spec.commentId
61
+ ? {
62
+ commentId: spec.commentId,
63
+ kind: "line",
64
+ pr: 1,
65
+ at: "2026-01-01T00:00:00.000Z",
66
+ }
67
+ : undefined,
68
+ };
69
+ }
70
+
71
+ function stateWith(findings: FindingState[]): FindingsStateFile {
72
+ const state = emptyState("feat/x");
73
+ for (const item of findings) state.findings[item.id] = item;
74
+ state.order = findings.map((item) => item.id);
75
+ return state;
76
+ }
77
+
78
+ const TARGET: PrTarget = { number: 7, repo: "acme/app", headSha: "abc" };
79
+
80
+ function okComment(id: number): GhResult {
81
+ return {
82
+ ok: true,
83
+ code: 0,
84
+ stderr: "",
85
+ stdout: JSON.stringify({ id, html_url: `https://x/${id}` }),
86
+ };
87
+ }
88
+
89
+ /** Runner fake: o handler decide a resposta a partir dos args (os chamados ficam registrados). */
90
+ function fakeRunner(handler: (args: string[]) => GhResult): {
91
+ runner: GhRunner;
92
+ calls: string[][];
93
+ } {
94
+ const calls: string[][] = [];
95
+ const runner: GhRunner = (args) => {
96
+ calls.push(args);
97
+ return handler(args);
98
+ };
99
+ return { runner, calls };
100
+ }
101
+
102
+ /** Respostas por tipo de destino: geral (issues), arquivo (subject_type=file) e linha. */
103
+ function endpointRunner(ids: { line?: number; file?: number; general?: number } = {}): {
104
+ runner: GhRunner;
105
+ calls: string[][];
106
+ } {
107
+ return fakeRunner((args) => {
108
+ if (args[1].includes("/issues/")) return okComment(ids.general ?? 900);
109
+ if (args.includes("subject_type=file")) return okComment(ids.file ?? 800);
110
+ return okComment(ids.line ?? 700);
111
+ });
112
+ }
113
+
114
+ describe("selectSubmitCandidates", () => {
115
+ const state = stateWith([
116
+ finding({ id: "a", severity: "blocking", decision: "execute" }),
117
+ finding({ id: "b", severity: "attention", decision: "ignore" }),
118
+ finding({ id: "c", severity: "nit", line: null }),
119
+ finding({ id: "d", decision: "execute", present: false }),
120
+ ]);
121
+
122
+ test("exclui ignore e ausentes", () => {
123
+ expect(selectSubmitCandidates(state).map((c) => c.id)).toEqual(["a", "c"]);
124
+ });
125
+
126
+ test("filtra por severidade e define a âncora", () => {
127
+ const blocking = selectSubmitCandidates(state, { severities: ["blocking"] });
128
+ expect(blocking.map((c) => c.id)).toEqual(["a"]);
129
+ expect(blocking[0].kind).toBe("line");
130
+ expect(selectSubmitCandidates(state, { severities: ["nit"] })[0].kind).toBe(
131
+ "file",
132
+ );
133
+ });
134
+
135
+ test("filtra por estado de execução", () => {
136
+ const exec = selectSubmitCandidates(state, { execStates: ["pending"] });
137
+ expect(exec.map((c) => c.id)).toEqual(["a", "c"]);
138
+ expect(selectSubmitCandidates(state, { execStates: ["done"] })).toEqual([]);
139
+ });
140
+
141
+ test("marca sent/skip e respeita includeSent", () => {
142
+ const sent = stateWith([finding({ id: "a", commentId: 99 })]);
143
+ const [only] = selectSubmitCandidates(sent);
144
+ expect(only.sent).toBe(true);
145
+ expect(only.skip).toBe(true);
146
+ expect(only.sentUrl).toBeUndefined();
147
+ expect(selectSubmitCandidates(sent, { includeSent: true })[0].skip).toBe(false);
148
+ });
149
+ });
150
+
151
+ describe("buildSubmitPreview", () => {
152
+ test("disponível: conta linha/arquivo", () => {
153
+ const state = stateWith([finding({ id: "a" }), finding({ id: "c", line: null })]);
154
+ const preview = buildSubmitPreview(state, { ok: true, target: TARGET });
155
+ expect(preview.available).toBe(true);
156
+ expect(preview.target).toEqual(TARGET);
157
+ expect(preview.totals).toMatchObject({
158
+ candidates: 2,
159
+ toSend: 2,
160
+ line: 1,
161
+ file: 1,
162
+ sent: 0,
163
+ });
164
+ });
165
+
166
+ test("indisponível propaga o motivo", () => {
167
+ const preview = buildSubmitPreview(stateWith([]), {
168
+ ok: false,
169
+ reason: "sem gh",
170
+ });
171
+ expect(preview.available).toBe(false);
172
+ expect(preview.reason).toBe("sem gh");
173
+ });
174
+ });
175
+
176
+ describe("corpo dos comentários", () => {
177
+ test("buildCommentBody inclui regra, ação e evidência", () => {
178
+ const body = buildCommentBody(finding({ id: "a", rule: "Nunca usar any" }));
179
+ expect(body).toContain("Nunca usar any");
180
+ expect(body).toContain("**Ação:** corrigir");
181
+ expect(body).toContain("const x = 1;");
182
+ expect(body).toContain("houndsman");
183
+ });
184
+
185
+ test("renderGeneralBody lista os findings sem linha", () => {
186
+ const body = renderGeneralBody([
187
+ finding({ id: "a", rule: "Regra X", line: null }),
188
+ ]);
189
+ expect(body).toContain("Regra X");
190
+ expect(body).toContain("src/a.ts");
191
+ });
192
+ });
193
+
194
+ describe("resolvePrTarget", () => {
195
+ test("resolve número, repo e head", () => {
196
+ const runner: GhRunner = () => ({
197
+ ok: true,
198
+ code: 0,
199
+ stderr: "",
200
+ stdout: JSON.stringify({
201
+ number: 7,
202
+ url: "https://github.com/acme/app/pull/7",
203
+ headRefOid: "abc123",
204
+ }),
205
+ });
206
+ const result = resolvePrTarget(runner, "feat/x");
207
+ expect(result.ok).toBe(true);
208
+ if (result.ok) {
209
+ expect(result.target).toEqual({
210
+ number: 7,
211
+ repo: "acme/app",
212
+ headSha: "abc123",
213
+ url: "https://github.com/acme/app/pull/7",
214
+ });
215
+ }
216
+ });
217
+
218
+ test("fail-open sem gh", () => {
219
+ const runner: GhRunner = () => ({
220
+ ok: false,
221
+ code: 127,
222
+ stdout: "",
223
+ stderr: "command not found",
224
+ });
225
+ const result = resolvePrTarget(runner, "feat/x");
226
+ expect(result.ok).toBe(false);
227
+ if (!result.ok) expect(result.reason).toContain("gh não instalado");
228
+ });
229
+ });
230
+
231
+ describe("submitFindings", () => {
232
+ test("posta na linha e no arquivo; grava o vínculo", () => {
233
+ const state = stateWith([finding({ id: "a" }), finding({ id: "c", line: null })]);
234
+ const { runner } = endpointRunner({ line: 100, file: 150, general: 200 });
235
+ const outcome = submitFindings({ state, target: TARGET, runner });
236
+
237
+ expect(outcome.posted).toHaveLength(2);
238
+ expect(outcome.failed).toEqual([]);
239
+ expect(outcome.general).toBeUndefined();
240
+ expect(state.findings.a.github).toMatchObject({
241
+ commentId: 100,
242
+ kind: "line",
243
+ pr: 7,
244
+ });
245
+ expect(state.findings.c.github).toMatchObject({
246
+ commentId: 150,
247
+ kind: "file",
248
+ pr: 7,
249
+ });
250
+ });
251
+
252
+ test("linha fora do diff cai no comentário de arquivo", () => {
253
+ const state = stateWith([finding({ id: "a" })]);
254
+ const { runner } = fakeRunner((args) =>
255
+ args.includes("subject_type=file")
256
+ ? okComment(150)
257
+ : {
258
+ ok: false,
259
+ code: 422,
260
+ stderr: "line must be part of the diff",
261
+ stdout: "",
262
+ },
263
+ );
264
+ const outcome = submitFindings({ state, target: TARGET, runner });
265
+
266
+ expect(outcome.failed).toEqual([]);
267
+ expect(outcome.general).toBeUndefined();
268
+ expect(state.findings.a.github).toMatchObject({
269
+ kind: "file",
270
+ commentId: 150,
271
+ });
272
+ });
273
+
274
+ test("o que não cola em linha nem arquivo vai para o comentário geral", () => {
275
+ const state = stateWith([finding({ id: "a" })]);
276
+ const { runner } = fakeRunner((args) =>
277
+ args[1].includes("/issues/")
278
+ ? okComment(200)
279
+ : { ok: false, code: 422, stderr: "fora do diff", stdout: "" },
280
+ );
281
+ const outcome = submitFindings({ state, target: TARGET, runner });
282
+
283
+ expect(outcome.failed).toHaveLength(1);
284
+ expect(outcome.general?.ids).toEqual(["a"]);
285
+ expect(state.findings.a.github?.kind).toBe("comment");
286
+ expect(state.findings.a.github?.commentId).toBe(200);
287
+ });
288
+
289
+ test("pula já enviados (sem chamar o gh)", () => {
290
+ const state = stateWith([finding({ id: "a", commentId: 55 })]);
291
+ const { runner, calls } = endpointRunner();
292
+ const outcome = submitFindings({ state, target: TARGET, runner });
293
+
294
+ expect(calls).toHaveLength(0);
295
+ expect(outcome.skipped).toHaveLength(1);
296
+ expect(state.findings.a.github?.commentId).toBe(55);
297
+ });
298
+
299
+ test("manda commit_id e side=RIGHT na linha", () => {
300
+ const state = stateWith([finding({ id: "a", line: 9 })]);
301
+ const { runner, calls } = endpointRunner({ line: 100 });
302
+ submitFindings({ state, target: TARGET, runner });
303
+
304
+ const line = calls[0];
305
+ expect(line).toContain("line=9");
306
+ expect(line).toContain("side=RIGHT");
307
+ expect(line).toContain("commit_id=abc");
308
+ expect(line).toContain("path=src/a.ts");
309
+ });
310
+
311
+ test("finding sem linha usa subject_type=file", () => {
312
+ const state = stateWith([finding({ id: "a", line: null })]);
313
+ const { runner, calls } = endpointRunner({ file: 150 });
314
+ submitFindings({ state, target: TARGET, runner });
315
+
316
+ expect(calls[0]).toContain("subject_type=file");
317
+ expect(calls[0]).not.toContain("side=RIGHT");
318
+ expect(state.findings.a.github?.kind).toBe("file");
319
+ });
320
+ });
@@ -139,6 +139,42 @@ describe("registry-check", () => {
139
139
  const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
140
140
  expect(warnings).toEqual([]);
141
141
  });
142
+
143
+ test("regra duplicada no mesmo watchdog gera aviso", () => {
144
+ const config = {
145
+ ...fakeHoundsmanConfig,
146
+ watchdogs: {
147
+ ...fakeHoundsmanConfig.watchdogs,
148
+ alpha: {
149
+ ...alphaWatchdog,
150
+ rules: [
151
+ { severity: "attention" as const, text: "Evitar console.log." },
152
+ { severity: "blocking" as const, text: "Evitar console.log." },
153
+ ],
154
+ },
155
+ },
156
+ };
157
+ const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
158
+ expect(warnings.some((w) => w.includes("duplicada"))).toBe(true);
159
+ });
160
+
161
+ test("texto de regra em reviewFocus/contextFiles gera aviso", () => {
162
+ const config = {
163
+ ...fakeHoundsmanConfig,
164
+ watchdogs: {
165
+ ...fakeHoundsmanConfig.watchdogs,
166
+ alpha: {
167
+ ...alphaWatchdog,
168
+ reviewFocus: ["Evitar: console.log."],
169
+ contextFiles: ["Promover: validar a entrada."],
170
+ },
171
+ },
172
+ };
173
+ const { warnings } = checkRegistry(config as typeof fakeHoundsmanConfig);
174
+ expect(
175
+ warnings.filter((w) => w.includes("a regra vive só em 'rules'")),
176
+ ).toHaveLength(2);
177
+ });
142
178
  });
143
179
 
144
180
  // ---------------------------------------------------------------------------
@@ -47,6 +47,41 @@ function makeEnv(): {
47
47
  };
48
48
  }
49
49
 
50
+ /** Semeia um `state.json` com um finding inline (tem linha) na run. */
51
+ function seedState(artifactDir: string, branch: string): void {
52
+ writeFileSync(
53
+ join(artifactDir, branch, "state.json"),
54
+ JSON.stringify({
55
+ version: 2,
56
+ branch,
57
+ slug: branch,
58
+ updatedAt: "2026-01-01T00:00:00.000Z",
59
+ order: ["f1"],
60
+ findings: {
61
+ f1: {
62
+ id: "f1",
63
+ fingerprint: "fp1",
64
+ watchdogIds: ["alpha"],
65
+ rule: "Nunca usar any",
66
+ severity: "blocking",
67
+ file: "src/alpha/f.ts",
68
+ line: 2,
69
+ message: "any detectado",
70
+ action: "tipar",
71
+ anchor: null,
72
+ count: 1,
73
+ present: true,
74
+ decision: "pending",
75
+ execState: "pending",
76
+ createdAt: "2026-01-01T00:00:00.000Z",
77
+ updatedAt: "2026-01-01T00:00:00.000Z",
78
+ history: [],
79
+ },
80
+ },
81
+ }),
82
+ );
83
+ }
84
+
50
85
  const get = (handler: (r: Request) => Promise<Response>, path: string) =>
51
86
  handler(new Request(`http://localhost${path}`));
52
87
 
@@ -156,6 +191,88 @@ describe("server API", () => {
156
191
  }
157
192
  });
158
193
 
194
+ test("GET submit-preview resolve a PR e lista os candidatos", async () => {
195
+ const env = makeEnv();
196
+ try {
197
+ const handler = createRequestHandler(env.config, {
198
+ ghRunner: () => ({
199
+ ok: true,
200
+ code: 0,
201
+ stderr: "",
202
+ stdout: JSON.stringify({
203
+ number: 12,
204
+ url: "https://github.com/acme/app/pull/12",
205
+ headRefOid: "deadbeef",
206
+ }),
207
+ }),
208
+ });
209
+ const res = await get(handler, "/api/runs/main/submit-preview");
210
+ expect(res.status).toBe(200);
211
+ const body = (await res.json()) as {
212
+ available: boolean;
213
+ target: { number: number };
214
+ candidates: unknown[];
215
+ };
216
+ expect(body.available).toBe(true);
217
+ expect(body.target.number).toBe(12);
218
+ expect(body.candidates).toHaveLength(1);
219
+ } finally {
220
+ env.cleanup();
221
+ }
222
+ });
223
+
224
+ test("POST submit posta comentários e grava o vínculo no estado", async () => {
225
+ const env = makeEnv();
226
+ try {
227
+ seedState(env.artifactDir, "main");
228
+ const handler = createRequestHandler(env.config, {
229
+ ghRunner: (args) =>
230
+ args[0] === "pr"
231
+ ? {
232
+ ok: true,
233
+ code: 0,
234
+ stderr: "",
235
+ stdout: JSON.stringify({
236
+ number: 12,
237
+ url: "https://github.com/acme/app/pull/12",
238
+ headRefOid: "deadbeef",
239
+ }),
240
+ }
241
+ : {
242
+ ok: true,
243
+ code: 0,
244
+ stderr: "",
245
+ stdout: JSON.stringify({
246
+ id: 501,
247
+ html_url: "https://x/501",
248
+ }),
249
+ },
250
+ });
251
+ const res = await handler(
252
+ new Request("http://localhost/api/runs/main/submit", {
253
+ method: "POST",
254
+ headers: { "content-type": "application/json" },
255
+ body: JSON.stringify({}),
256
+ }),
257
+ );
258
+ expect(res.status).toBe(200);
259
+ const body = (await res.json()) as {
260
+ available: boolean;
261
+ outcome: { posted: { kind: string }[] };
262
+ };
263
+ expect(body.available).toBe(true);
264
+ expect(body.outcome.posted).toHaveLength(1);
265
+ expect(body.outcome.posted[0].kind).toBe("line");
266
+
267
+ const after = (await (
268
+ await get(handler, "/api/runs/main/findings?all=1")
269
+ ).json()) as { findings: { github?: { commentId: number } }[] };
270
+ expect(after.findings[0].github?.commentId).toBe(501);
271
+ } finally {
272
+ env.cleanup();
273
+ }
274
+ });
275
+
159
276
  test("artifacts serve arquivos da run e barra o resto", async () => {
160
277
  const env = makeEnv();
161
278
  try {