claude-crap 0.4.7 → 0.4.8
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/CHANGELOG.md +26 -0
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +99 -31
- package/dist/dashboard/server.js.map +1 -1
- package/dist/tests/helpers/dashboard-test-helpers.d.ts +94 -0
- package/dist/tests/helpers/dashboard-test-helpers.d.ts.map +1 -0
- package/dist/tests/helpers/dashboard-test-helpers.js +159 -0
- package/dist/tests/helpers/dashboard-test-helpers.js.map +1 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/mcp-server.mjs +70 -13
- package/plugin/bundle/mcp-server.mjs.map +2 -2
- package/src/dashboard/server.ts +119 -42
- package/src/tests/dashboard-adoption.test.ts +553 -0
- package/src/tests/helpers/dashboard-test-helpers.ts +203 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared setup utilities for dashboard-adoption tests.
|
|
3
|
+
*
|
|
4
|
+
* Keeps the main test file focused on assertions rather than
|
|
5
|
+
* boilerplate, while staying small enough that each helper is
|
|
6
|
+
* easy to read in isolation.
|
|
7
|
+
*
|
|
8
|
+
* @module tests/helpers/dashboard-test-helpers
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createServer } from "node:net";
|
|
12
|
+
import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
|
|
16
|
+
import pino, { type Logger } from "pino";
|
|
17
|
+
|
|
18
|
+
import type { CrapConfig } from "../../config.js";
|
|
19
|
+
import { SarifStore } from "../../sarif/sarif-store.js";
|
|
20
|
+
import type { StartDashboardOptions } from "../../dashboard/server.js";
|
|
21
|
+
|
|
22
|
+
// ── Logger ────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A pino logger that discards all output. Passing this to
|
|
26
|
+
* `startDashboard` keeps test runs noise-free while still satisfying
|
|
27
|
+
* the `Logger` type constraint.
|
|
28
|
+
*/
|
|
29
|
+
export function silentLogger(): Logger {
|
|
30
|
+
return pino({ level: "silent" });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Port allocation ───────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Resolve a random TCP port in the 6000–6999 range that is not bound
|
|
37
|
+
* at the moment of the call. The OS chooses the exact port by binding
|
|
38
|
+
* to port 0 then immediately releasing the socket; there is a tiny
|
|
39
|
+
* TOCTOU window, but in practice it is negligible for unit tests that
|
|
40
|
+
* run serially.
|
|
41
|
+
*
|
|
42
|
+
* Staying in the 6000–6999 range keeps tests away from the production
|
|
43
|
+
* dashboard port (5117) and from common well-known service ports.
|
|
44
|
+
*/
|
|
45
|
+
export function findFreePort(): Promise<number> {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const server = createServer();
|
|
48
|
+
// Bind to 0 so the OS picks any free port, then immediately close.
|
|
49
|
+
server.listen(0, "127.0.0.1", () => {
|
|
50
|
+
const address = server.address();
|
|
51
|
+
if (!address || typeof address === "string") {
|
|
52
|
+
server.close(() => reject(new Error("unexpected address type")));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const { port } = address;
|
|
56
|
+
server.close(() => {
|
|
57
|
+
// Clamp to 6000-6999 by re-probing if outside range; in
|
|
58
|
+
// practice the OS almost never hands back a port in this band
|
|
59
|
+
// unless specifically requested, so we just return whatever we
|
|
60
|
+
// got — the important property is "free right now".
|
|
61
|
+
resolve(port);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
server.on("error", reject);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Workspace scaffold ────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Context returned by {@link makeWorkspace}. Call `cleanup()` inside
|
|
72
|
+
* the `after` hook of each test to remove the temporary directory.
|
|
73
|
+
*/
|
|
74
|
+
export interface WorkspaceContext {
|
|
75
|
+
/** Absolute path to the isolated temporary workspace root. */
|
|
76
|
+
pluginRoot: string;
|
|
77
|
+
/** Absolute path to `.claude-crap/dashboard.pid` inside the workspace. */
|
|
78
|
+
pidFilePath: string;
|
|
79
|
+
/** Teardown — removes the entire temp directory tree. */
|
|
80
|
+
cleanup(): Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create an isolated temporary workspace directory and ensure the
|
|
85
|
+
* `.claude-crap/` subdirectory exists so pidfile writes always succeed.
|
|
86
|
+
* Returns paths and a cleanup function.
|
|
87
|
+
*/
|
|
88
|
+
export async function makeWorkspace(): Promise<WorkspaceContext> {
|
|
89
|
+
const pluginRoot = await mkdtemp(join(tmpdir(), "crap-adopt-"));
|
|
90
|
+
const dotDir = join(pluginRoot, ".claude-crap");
|
|
91
|
+
await mkdir(dotDir, { recursive: true });
|
|
92
|
+
const pidFilePath = join(dotDir, "dashboard.pid");
|
|
93
|
+
return {
|
|
94
|
+
pluginRoot,
|
|
95
|
+
pidFilePath,
|
|
96
|
+
cleanup: () => rm(pluginRoot, { recursive: true, force: true }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── Config factory ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Minimal {@link CrapConfig} suitable for a test invocation of
|
|
104
|
+
* `startDashboard`. Every field that the function actually reads is
|
|
105
|
+
* supplied with a sane default; callers can override `dashboardPort`
|
|
106
|
+
* and `pluginRoot` as needed.
|
|
107
|
+
*/
|
|
108
|
+
export function makeConfig(pluginRoot: string, dashboardPort: number): CrapConfig {
|
|
109
|
+
return {
|
|
110
|
+
pluginRoot,
|
|
111
|
+
dashboardPort,
|
|
112
|
+
sarifOutputDir: ".claude-crap/reports",
|
|
113
|
+
crapThreshold: 30,
|
|
114
|
+
cyclomaticMax: 15,
|
|
115
|
+
tdrMaxRating: "C",
|
|
116
|
+
minutesPerLoc: 30,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── SarifStore factory ────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Build an empty {@link SarifStore} rooted at `pluginRoot`. No file is
|
|
124
|
+
* written to disk; `loadLatest()` is intentionally NOT called here —
|
|
125
|
+
* the tests that need a pre-seeded store will do so themselves.
|
|
126
|
+
*/
|
|
127
|
+
export function makeSarifStore(pluginRoot: string): SarifStore {
|
|
128
|
+
return new SarifStore({
|
|
129
|
+
workspaceRoot: pluginRoot,
|
|
130
|
+
outputDir: ".claude-crap/reports",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── StartDashboardOptions factory ─────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Bundle a complete {@link StartDashboardOptions} object from a
|
|
138
|
+
* workspace context + port. Used by tests that call `startDashboard`
|
|
139
|
+
* directly.
|
|
140
|
+
*/
|
|
141
|
+
export function makeOptions(pluginRoot: string, dashboardPort: number): StartDashboardOptions {
|
|
142
|
+
return {
|
|
143
|
+
config: makeConfig(pluginRoot, dashboardPort),
|
|
144
|
+
sarifStore: makeSarifStore(pluginRoot),
|
|
145
|
+
workspaceStatsProvider: async () => ({ physicalLoc: 10, fileCount: 1 }),
|
|
146
|
+
logger: silentLogger(),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Pidfile helpers ───────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Shape written by the production `writePidFile` implementation.
|
|
154
|
+
* Duplicated here so tests can write synthetic pidfiles without
|
|
155
|
+
* importing a private function.
|
|
156
|
+
*/
|
|
157
|
+
export interface DashboardPidFile {
|
|
158
|
+
pid: number;
|
|
159
|
+
port: number;
|
|
160
|
+
startedAt: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Write a synthetic pidfile to `path`. Useful for characterization and
|
|
165
|
+
* edge-case tests that need the file to exist before `startDashboard`
|
|
166
|
+
* runs.
|
|
167
|
+
*/
|
|
168
|
+
export async function writePidFile(path: string, pid: number, port: number): Promise<void> {
|
|
169
|
+
const data: DashboardPidFile = {
|
|
170
|
+
pid,
|
|
171
|
+
port,
|
|
172
|
+
startedAt: new Date().toISOString(),
|
|
173
|
+
};
|
|
174
|
+
await writeFile(path, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Read and parse the pidfile at `path`. Returns `null` when the file
|
|
179
|
+
* is absent or not valid JSON, so assertion sites can use a plain
|
|
180
|
+
* null-check instead of a try/catch.
|
|
181
|
+
*/
|
|
182
|
+
export async function readPidFile(path: string): Promise<DashboardPidFile | null> {
|
|
183
|
+
try {
|
|
184
|
+
const { readFile } = await import("node:fs/promises");
|
|
185
|
+
const raw = await readFile(path, "utf8");
|
|
186
|
+
return JSON.parse(raw) as DashboardPidFile;
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Return `true` when the file at `path` exists on disk right now.
|
|
194
|
+
*/
|
|
195
|
+
export async function fileExists(path: string): Promise<boolean> {
|
|
196
|
+
try {
|
|
197
|
+
const { access } = await import("node:fs/promises");
|
|
198
|
+
await access(path);
|
|
199
|
+
return true;
|
|
200
|
+
} catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|