@payconductor/houndsman 0.1.0 → 0.1.1
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 +39 -78
- package/assets/logo-legacy.png +0 -0
- package/assets/logo.png +0 -0
- package/package.json +3 -2
- package/src/cli/index.ts +89 -9
- package/src/core/assets.ts +8 -0
- package/src/core/brief.ts +25 -2
- package/src/core/commands/findings.ts +390 -0
- package/src/core/commands/scaffold.ts +20 -0
- package/src/core/config.ts +42 -0
- package/src/core/diff-source.ts +140 -0
- package/src/core/file-classifier.ts +50 -2
- package/src/core/finding-anchor.ts +147 -0
- package/src/core/findings-state.ts +538 -0
- package/src/core/jev/jev-client.ts +72 -11
- package/src/core/jev/jev-detector.ts +21 -19
- package/src/core/orchestrator.ts +169 -17
- package/src/core/paths.ts +3 -0
- package/src/core/registry-check.ts +15 -0
- package/src/core/report-builder.ts +113 -9
- package/src/core/types.ts +10 -0
- package/src/index.ts +78 -2
- package/src/server/artifacts.ts +66 -0
- package/src/server/client/App.tsx +158 -0
- package/src/server/client/ArtifactsPanel.tsx +48 -0
- package/src/server/client/Filters.tsx +83 -0
- package/src/server/client/FindingCard.tsx +122 -0
- package/src/server/client/FindingsList.tsx +54 -0
- package/src/server/client/RunList.tsx +43 -0
- package/src/server/client/SummaryBar.tsx +21 -0
- package/src/server/client/api.ts +37 -0
- package/src/server/client/constants.ts +63 -0
- package/src/server/client/index.tsx +12 -0
- package/src/server/client/types.ts +48 -0
- package/src/server/client-build.ts +47 -0
- package/src/server/index.ts +365 -0
- package/src/skill/README.md +14 -6
- package/src/skill/SETUP.md +5 -3
- package/src/skill/houndsman-update.md +1 -1
- package/src/skill/houndsman.md +87 -10
- package/src/tests/findings-anchor.test.ts +120 -0
- package/src/tests/findings-commands.test.ts +226 -0
- package/src/tests/findings-state.test.ts +274 -0
- package/src/tests/review-scripts.test.ts +299 -0
- package/src/tests/server.test.ts +212 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { HoundsmanConfig } from "../core/config";
|
|
6
|
+
import { createRequestHandler, startServer } from "../server";
|
|
7
|
+
import { readRunFile, runDirForSlug } from "../server/artifacts";
|
|
8
|
+
import { fakeHoundsmanConfig } from "./fixtures/fake-project";
|
|
9
|
+
|
|
10
|
+
/** Run fake em disco + config apontando para ela. */
|
|
11
|
+
function makeEnv(): {
|
|
12
|
+
artifactDir: string;
|
|
13
|
+
config: HoundsmanConfig;
|
|
14
|
+
cleanup: () => void;
|
|
15
|
+
} {
|
|
16
|
+
const artifactDir = mkdtempSync(join(tmpdir(), "houndsman-server-"));
|
|
17
|
+
const outDir = join(artifactDir, "main");
|
|
18
|
+
mkdirSync(join(outDir, "findings"), { recursive: true });
|
|
19
|
+
writeFileSync(join(outDir, "manifest.json"), JSON.stringify({ branch: "main" }));
|
|
20
|
+
writeFileSync(
|
|
21
|
+
join(outDir, "classification.json"),
|
|
22
|
+
JSON.stringify({ watchdogs: { alpha: ["src/alpha/f.ts"] } }),
|
|
23
|
+
);
|
|
24
|
+
writeFileSync(join(outDir, "review.md"), "# Review\n");
|
|
25
|
+
writeFileSync(
|
|
26
|
+
join(outDir, "findings", "alpha.json"),
|
|
27
|
+
JSON.stringify({
|
|
28
|
+
watchdogId: "alpha",
|
|
29
|
+
reviewedFiles: ["src/alpha/f.ts"],
|
|
30
|
+
skipped: [],
|
|
31
|
+
findings: [
|
|
32
|
+
{
|
|
33
|
+
rule: "Nunca usar any",
|
|
34
|
+
severity: "blocking",
|
|
35
|
+
file: "src/alpha/f.ts",
|
|
36
|
+
line: 2,
|
|
37
|
+
message: "any detectado",
|
|
38
|
+
action: "tipar",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
return {
|
|
44
|
+
artifactDir,
|
|
45
|
+
config: { ...fakeHoundsmanConfig, artifactDir },
|
|
46
|
+
cleanup: () => rmSync(artifactDir, { recursive: true, force: true }),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const get = (handler: (r: Request) => Promise<Response>, path: string) =>
|
|
51
|
+
handler(new Request(`http://localhost${path}`));
|
|
52
|
+
|
|
53
|
+
describe("server API", () => {
|
|
54
|
+
test("GET /api/runs lista as runs com resumo", async () => {
|
|
55
|
+
const env = makeEnv();
|
|
56
|
+
try {
|
|
57
|
+
const handler = createRequestHandler(env.config);
|
|
58
|
+
const res = await get(handler, "/api/runs");
|
|
59
|
+
expect(res.status).toBe(200);
|
|
60
|
+
const body = (await res.json()) as {
|
|
61
|
+
runs: { slug: string; summary: { total: number } }[];
|
|
62
|
+
};
|
|
63
|
+
expect(body.runs).toHaveLength(1);
|
|
64
|
+
expect(body.runs[0].slug).toBe("main");
|
|
65
|
+
expect(body.runs[0].summary.total).toBe(1);
|
|
66
|
+
} finally {
|
|
67
|
+
env.cleanup();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("GET findings reconstrói do artefato e PATCH atualiza o estado", async () => {
|
|
72
|
+
const env = makeEnv();
|
|
73
|
+
try {
|
|
74
|
+
const handler = createRequestHandler(env.config);
|
|
75
|
+
const list = await get(handler, "/api/runs/main/findings?all=1");
|
|
76
|
+
const body = (await list.json()) as {
|
|
77
|
+
findings: { id: string; decision: string }[];
|
|
78
|
+
};
|
|
79
|
+
expect(body.findings).toHaveLength(1);
|
|
80
|
+
const id = body.findings[0].id;
|
|
81
|
+
expect(body.findings[0].decision).toBe("pending");
|
|
82
|
+
|
|
83
|
+
const patch = await handler(
|
|
84
|
+
new Request(`http://localhost/api/runs/main/findings/${id}`, {
|
|
85
|
+
method: "PATCH",
|
|
86
|
+
headers: { "content-type": "application/json" },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
decision: "execute",
|
|
89
|
+
comment: "trocar por unknown",
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
expect(patch.status).toBe(200);
|
|
94
|
+
const updated = (await patch.json()) as {
|
|
95
|
+
finding: { decision: string; comment?: string };
|
|
96
|
+
};
|
|
97
|
+
expect(updated.finding.decision).toBe("execute");
|
|
98
|
+
expect(updated.finding.comment).toBe("trocar por unknown");
|
|
99
|
+
|
|
100
|
+
const after = (await (
|
|
101
|
+
await get(handler, "/api/runs/main/findings?all=1")
|
|
102
|
+
).json()) as {
|
|
103
|
+
findings: { decision: string }[];
|
|
104
|
+
summary: { byDecision: Record<string, number> };
|
|
105
|
+
};
|
|
106
|
+
expect(after.findings[0].decision).toBe("execute");
|
|
107
|
+
expect(after.summary.byDecision.execute).toBe(1);
|
|
108
|
+
} finally {
|
|
109
|
+
env.cleanup();
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("POST sync devolve o resultado da reancoragem", async () => {
|
|
114
|
+
const env = makeEnv();
|
|
115
|
+
try {
|
|
116
|
+
const handler = createRequestHandler(env.config);
|
|
117
|
+
await get(handler, "/api/runs/main/findings?all=1");
|
|
118
|
+
const res = await handler(
|
|
119
|
+
new Request("http://localhost/api/runs/main/sync", { method: "POST" }),
|
|
120
|
+
);
|
|
121
|
+
expect(res.status).toBe(200);
|
|
122
|
+
const body = (await res.json()) as { result: { stale: string[] } };
|
|
123
|
+
expect(Array.isArray(body.result.stale)).toBe(true);
|
|
124
|
+
} finally {
|
|
125
|
+
env.cleanup();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("artifacts serve arquivos da run e barra o resto", async () => {
|
|
130
|
+
const env = makeEnv();
|
|
131
|
+
try {
|
|
132
|
+
const handler = createRequestHandler(env.config);
|
|
133
|
+
const ok = await get(handler, "/api/runs/main/artifacts/manifest.json");
|
|
134
|
+
expect(ok.status).toBe(200);
|
|
135
|
+
expect(await ok.json()).toEqual({ branch: "main" });
|
|
136
|
+
|
|
137
|
+
const review = await get(handler, "/api/runs/main/artifacts/review.md");
|
|
138
|
+
expect(review.status).toBe(200);
|
|
139
|
+
expect(await review.text()).toContain("# Review");
|
|
140
|
+
|
|
141
|
+
const denied = await get(handler, "/api/runs/main/artifacts/evil.txt");
|
|
142
|
+
expect(denied.status).toBe(404);
|
|
143
|
+
|
|
144
|
+
const missingRun = await get(handler, "/api/runs/nao-existe/findings");
|
|
145
|
+
expect(missingRun.status).toBe(404);
|
|
146
|
+
} finally {
|
|
147
|
+
env.cleanup();
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("readRunFile barra path traversal", () => {
|
|
152
|
+
const env = makeEnv();
|
|
153
|
+
try {
|
|
154
|
+
const dir = runDirForSlug(env.artifactDir, "main");
|
|
155
|
+
expect(dir).not.toBeNull();
|
|
156
|
+
expect(readRunFile(dir as string, "../../../etc/passwd")).toBeNull();
|
|
157
|
+
expect(readRunFile(dir as string, "manifest.json")).toContain("main");
|
|
158
|
+
expect(runDirForSlug(env.artifactDir, "../main")).toBeNull();
|
|
159
|
+
} finally {
|
|
160
|
+
env.cleanup();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("GET / serve o shell e /assets/app.js empacota o cliente", async () => {
|
|
165
|
+
const env = makeEnv();
|
|
166
|
+
try {
|
|
167
|
+
const handler = createRequestHandler(env.config);
|
|
168
|
+
const shell = await get(handler, "/");
|
|
169
|
+
expect(await shell.text()).toContain('id="app"');
|
|
170
|
+
|
|
171
|
+
const bundle = await get(handler, "/assets/app.js");
|
|
172
|
+
expect(bundle.status).toBe(200);
|
|
173
|
+
const code = await bundle.text();
|
|
174
|
+
expect(code.length).toBeGreaterThan(100);
|
|
175
|
+
expect(code).toContain("preact");
|
|
176
|
+
} finally {
|
|
177
|
+
env.cleanup();
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("serve sem cache (no-store) e busta o bundle com um build id", async () => {
|
|
182
|
+
const env = makeEnv();
|
|
183
|
+
try {
|
|
184
|
+
const handler = createRequestHandler(env.config);
|
|
185
|
+
const shell = await get(handler, "/");
|
|
186
|
+
expect(shell.headers.get("cache-control")).toContain("no-store");
|
|
187
|
+
expect(await shell.text()).toContain("/assets/app.js?v=");
|
|
188
|
+
|
|
189
|
+
const bundle = await get(handler, "/assets/app.js");
|
|
190
|
+
expect(bundle.headers.get("cache-control")).toContain("no-store");
|
|
191
|
+
} finally {
|
|
192
|
+
env.cleanup();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe("startServer", () => {
|
|
198
|
+
test("sobe numa porta efêmera e responde ao health check", async () => {
|
|
199
|
+
const env = makeEnv();
|
|
200
|
+
let server: ReturnType<typeof startServer>["server"] | undefined;
|
|
201
|
+
try {
|
|
202
|
+
const started = startServer({ config: env.config, port: 0 });
|
|
203
|
+
server = started.server;
|
|
204
|
+
const res = await fetch(`${started.url}/api/health`);
|
|
205
|
+
expect(res.status).toBe(200);
|
|
206
|
+
expect(await res.json()).toEqual({ ok: true });
|
|
207
|
+
} finally {
|
|
208
|
+
server?.stop(true);
|
|
209
|
+
env.cleanup();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|