claude-crap 0.4.2 → 0.4.4
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 +98 -0
- package/README.md +1 -1
- package/dist/config.d.ts +29 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +95 -3
- package/dist/config.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/bundle/mcp-server.mjs +47 -1
- package/plugin/bundle/mcp-server.mjs.map +2 -2
- package/plugin/package-lock.json +2 -2
- package/plugin/package.json +1 -1
- package/src/config.ts +113 -4
- package/src/tests/config.test.ts +295 -0
package/plugin/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-crap-plugin",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-crap-plugin",
|
|
9
|
-
"version": "0.4.
|
|
9
|
+
"version": "0.4.4",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@fastify/static": "^8.0.3",
|
|
12
12
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
package/plugin/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
* user settings → plugin.json "options" → .mcp.json "env" → this file
|
|
10
10
|
*
|
|
11
11
|
* If any environment variable is missing or empty, a safe default is used,
|
|
12
|
-
* but the loader NEVER invents stochastic values
|
|
13
|
-
*
|
|
12
|
+
* but the loader NEVER invents stochastic values. This module is the
|
|
13
|
+
* single source of truth for runtime configuration.
|
|
14
14
|
*
|
|
15
15
|
* @module config
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { readlinkSync } from "node:fs";
|
|
20
|
+
|
|
18
21
|
/**
|
|
19
22
|
* Maintainability rating letter grades used throughout claude-crap.
|
|
20
23
|
*
|
|
@@ -30,7 +33,7 @@ export type MaintainabilityRating = "A" | "B" | "C" | "D" | "E";
|
|
|
30
33
|
* configuration at runtime — any change must go through a server restart.
|
|
31
34
|
*/
|
|
32
35
|
export interface CrapConfig {
|
|
33
|
-
/** Absolute path to the
|
|
36
|
+
/** Absolute path to the user's workspace. Resolved via {@link discoverWorkspaceRoot}. */
|
|
34
37
|
readonly pluginRoot: string;
|
|
35
38
|
/** Directory (relative to the workspace) where consolidated SARIF reports are written. */
|
|
36
39
|
readonly sarifOutputDir: string;
|
|
@@ -46,6 +49,112 @@ export interface CrapConfig {
|
|
|
46
49
|
readonly dashboardPort: number;
|
|
47
50
|
}
|
|
48
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Detects an unexpanded `.mcp.json` variable template such as
|
|
54
|
+
* `${CLAUDE_PROJECT_DIR}`. Claude Code only expands `${CLAUDE_PLUGIN_ROOT}`
|
|
55
|
+
* inside `.mcp.json`; every other `${VAR}` is passed through verbatim and
|
|
56
|
+
* must NOT be treated as a real filesystem path.
|
|
57
|
+
*/
|
|
58
|
+
function isLiteralVarTemplate(value: string | undefined): boolean {
|
|
59
|
+
if (value === undefined) return false;
|
|
60
|
+
return /^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value.trim());
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Normalize an environment variable that is expected to contain a path.
|
|
65
|
+
* Returns `undefined` if the value is missing, empty, or an unexpanded
|
|
66
|
+
* `${...}` template. Any non-empty concrete string is returned as-is.
|
|
67
|
+
*/
|
|
68
|
+
function sanitizeEnvPath(value: string | undefined): string | undefined {
|
|
69
|
+
if (value === undefined || value === "") return undefined;
|
|
70
|
+
if (isLiteralVarTemplate(value)) return undefined;
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read the current working directory of the parent process. Claude Code
|
|
76
|
+
* spawns MCP servers with its own cwd set to the user's workspace, so the
|
|
77
|
+
* parent's cwd is the most reliable fallback when `CLAUDE_PROJECT_DIR` is
|
|
78
|
+
* not inherited (e.g. because Claude Code only exports it for hooks).
|
|
79
|
+
*
|
|
80
|
+
* Returns `undefined` on any platform or failure mode the probe cannot
|
|
81
|
+
* handle — callers must be prepared for a missing result.
|
|
82
|
+
*/
|
|
83
|
+
function readParentCwdDefault(): string | undefined {
|
|
84
|
+
try {
|
|
85
|
+
const ppid = process.ppid;
|
|
86
|
+
if (!ppid || ppid === 0) return undefined;
|
|
87
|
+
|
|
88
|
+
if (process.platform === "linux") {
|
|
89
|
+
// /proc/<pid>/cwd is a symlink to the process's cwd.
|
|
90
|
+
return readlinkSync(`/proc/${ppid}/cwd`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (process.platform === "darwin") {
|
|
94
|
+
// `lsof -a -p <pid> -d cwd -F n` emits a single line starting with
|
|
95
|
+
// `n<path>` for the cwd file descriptor. `-F` keeps the output
|
|
96
|
+
// machine-readable.
|
|
97
|
+
const output = execFileSync(
|
|
98
|
+
"lsof",
|
|
99
|
+
["-a", "-p", String(ppid), "-d", "cwd", "-F", "n"],
|
|
100
|
+
{
|
|
101
|
+
encoding: "utf8",
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
timeout: 2000,
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
const match = output.match(/^n(.+)$/m);
|
|
107
|
+
return match?.[1];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Windows and other platforms: no reliable no-dep probe.
|
|
111
|
+
return undefined;
|
|
112
|
+
} catch {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parameters accepted by {@link discoverWorkspaceRoot}. The only field is
|
|
119
|
+
* an injectable `readParentCwd` implementation so that tests can pin the
|
|
120
|
+
* fallback behavior without spawning `lsof` or reading `/proc`.
|
|
121
|
+
*/
|
|
122
|
+
export interface DiscoverWorkspaceOptions {
|
|
123
|
+
readParentCwd?: () => string | undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the user's workspace directory. Strategy, in strict priority
|
|
128
|
+
* order:
|
|
129
|
+
*
|
|
130
|
+
* 1. `CLAUDE_PROJECT_DIR` (sanitized — ignored if it's `${...}`)
|
|
131
|
+
* 2. `CLAUDE_CRAP_PLUGIN_ROOT` (sanitized — legacy explicit override)
|
|
132
|
+
* 3. Parent process cwd (Claude Code's cwd = the workspace)
|
|
133
|
+
* 4. `process.cwd()` (last-resort fallback; usually wrong for
|
|
134
|
+
* MCP servers because Claude Code sets
|
|
135
|
+
* cwd to the plugin cache directory)
|
|
136
|
+
*
|
|
137
|
+
* This function NEVER returns an unexpanded `${...}` template; any source
|
|
138
|
+
* that contains one is skipped as if it were unset.
|
|
139
|
+
*
|
|
140
|
+
* @param options Injection points for tests.
|
|
141
|
+
* @returns A concrete filesystem path.
|
|
142
|
+
*/
|
|
143
|
+
export function discoverWorkspaceRoot(options: DiscoverWorkspaceOptions = {}): string {
|
|
144
|
+
const readParentCwd = options.readParentCwd ?? readParentCwdDefault;
|
|
145
|
+
|
|
146
|
+
const fromProjectDir = sanitizeEnvPath(process.env.CLAUDE_PROJECT_DIR);
|
|
147
|
+
if (fromProjectDir) return fromProjectDir;
|
|
148
|
+
|
|
149
|
+
const fromPluginRoot = sanitizeEnvPath(process.env.CLAUDE_CRAP_PLUGIN_ROOT);
|
|
150
|
+
if (fromPluginRoot) return fromPluginRoot;
|
|
151
|
+
|
|
152
|
+
const fromParent = sanitizeEnvPath(readParentCwd());
|
|
153
|
+
if (fromParent) return fromParent;
|
|
154
|
+
|
|
155
|
+
return process.cwd();
|
|
156
|
+
}
|
|
157
|
+
|
|
49
158
|
/**
|
|
50
159
|
* Parse a numeric environment variable, falling back to `fallback` when the
|
|
51
160
|
* variable is undefined or empty. Throws if the value is present but not a
|
|
@@ -98,7 +207,7 @@ function parseRating(raw: string | undefined, fallback: MaintainabilityRating):
|
|
|
98
207
|
*/
|
|
99
208
|
export function loadConfig(): CrapConfig {
|
|
100
209
|
return {
|
|
101
|
-
pluginRoot:
|
|
210
|
+
pluginRoot: discoverWorkspaceRoot(),
|
|
102
211
|
sarifOutputDir: process.env.CLAUDE_CRAP_SARIF_OUTPUT_DIR ?? ".claude-crap/reports",
|
|
103
212
|
crapThreshold: parseNumber("CLAUDE_CRAP_CRAP_THRESHOLD", process.env.CLAUDE_CRAP_CRAP_THRESHOLD, 30),
|
|
104
213
|
cyclomaticMax: parseNumber("CLAUDE_CRAP_CYCLOMATIC_MAX", process.env.CLAUDE_CRAP_CYCLOMATIC_MAX, 15),
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Characterization tests for the MCP server config loader.
|
|
3
|
+
*
|
|
4
|
+
* `loadConfig()` resolves `pluginRoot` from three env vars with
|
|
5
|
+
* strict priority:
|
|
6
|
+
*
|
|
7
|
+
* 1. `CLAUDE_PROJECT_DIR` — set by Claude Code to the workspace
|
|
8
|
+
* 2. `CLAUDE_CRAP_PLUGIN_ROOT` — legacy explicit override
|
|
9
|
+
* 3. `process.cwd()` — last-resort fallback
|
|
10
|
+
*
|
|
11
|
+
* These tests pin the priority chain and the numeric/rating parsers
|
|
12
|
+
* so that regressions in workspace resolution are caught before they
|
|
13
|
+
* ship a bundle that writes SARIF reports into the plugin cache.
|
|
14
|
+
*
|
|
15
|
+
* @module tests/config.test
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
|
|
21
|
+
import { loadConfig, discoverWorkspaceRoot } from "../config.js";
|
|
22
|
+
|
|
23
|
+
/* ── env snapshot / restore ─────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
const ENV_KEYS = [
|
|
26
|
+
"CLAUDE_PROJECT_DIR",
|
|
27
|
+
"CLAUDE_CRAP_PLUGIN_ROOT",
|
|
28
|
+
"CLAUDE_CRAP_SARIF_OUTPUT_DIR",
|
|
29
|
+
"CLAUDE_CRAP_CRAP_THRESHOLD",
|
|
30
|
+
"CLAUDE_CRAP_CYCLOMATIC_MAX",
|
|
31
|
+
"CLAUDE_CRAP_TDR_MAX_RATING",
|
|
32
|
+
"CLAUDE_CRAP_MINUTES_PER_LOC",
|
|
33
|
+
"CLAUDE_CRAP_DASHBOARD_PORT",
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
type Snapshot = Map<string, string | undefined>;
|
|
37
|
+
|
|
38
|
+
function snapshotEnv(): Snapshot {
|
|
39
|
+
const snap = new Map<string, string | undefined>();
|
|
40
|
+
for (const key of ENV_KEYS) snap.set(key, process.env[key]);
|
|
41
|
+
return snap;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function restoreEnv(snap: Snapshot): void {
|
|
45
|
+
for (const [key, val] of snap) {
|
|
46
|
+
if (val === undefined) delete process.env[key];
|
|
47
|
+
else process.env[key] = val;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clearConfigEnv(): void {
|
|
52
|
+
for (const key of ENV_KEYS) delete process.env[key];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* ── tests ──────────────────────────────────────────────────────── */
|
|
56
|
+
|
|
57
|
+
describe("loadConfig — pluginRoot priority chain", () => {
|
|
58
|
+
let saved: Snapshot;
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
saved = snapshotEnv();
|
|
62
|
+
clearConfigEnv();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
restoreEnv(saved);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("falls back to process.cwd() when no env vars are set", () => {
|
|
70
|
+
const cfg = loadConfig();
|
|
71
|
+
assert.equal(cfg.pluginRoot, process.cwd());
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("CLAUDE_CRAP_PLUGIN_ROOT wins over process.cwd()", () => {
|
|
75
|
+
process.env.CLAUDE_CRAP_PLUGIN_ROOT = "/explicit/plugin/root";
|
|
76
|
+
const cfg = loadConfig();
|
|
77
|
+
assert.equal(cfg.pluginRoot, "/explicit/plugin/root");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("CLAUDE_PROJECT_DIR wins over CLAUDE_CRAP_PLUGIN_ROOT", () => {
|
|
81
|
+
process.env.CLAUDE_PROJECT_DIR = "/workspace/project";
|
|
82
|
+
process.env.CLAUDE_CRAP_PLUGIN_ROOT = "/explicit/plugin/root";
|
|
83
|
+
const cfg = loadConfig();
|
|
84
|
+
assert.equal(cfg.pluginRoot, "/workspace/project");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("CLAUDE_PROJECT_DIR alone resolves correctly", () => {
|
|
88
|
+
process.env.CLAUDE_PROJECT_DIR = "/workspace/project";
|
|
89
|
+
const cfg = loadConfig();
|
|
90
|
+
assert.equal(cfg.pluginRoot, "/workspace/project");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("loadConfig — defensive ${...} literal detection", () => {
|
|
95
|
+
// Claude Code only expands ${CLAUDE_PLUGIN_ROOT} in .mcp.json — any
|
|
96
|
+
// other ${VAR} is passed through verbatim. We must never treat such
|
|
97
|
+
// a literal template as a real path.
|
|
98
|
+
let saved: Snapshot;
|
|
99
|
+
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
saved = snapshotEnv();
|
|
102
|
+
clearConfigEnv();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
afterEach(() => {
|
|
106
|
+
restoreEnv(saved);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("ignores literal ${CLAUDE_PROJECT_DIR} and falls through", () => {
|
|
110
|
+
process.env.CLAUDE_PROJECT_DIR = "${CLAUDE_PROJECT_DIR}";
|
|
111
|
+
const cfg = loadConfig();
|
|
112
|
+
assert.notEqual(cfg.pluginRoot, "${CLAUDE_PROJECT_DIR}");
|
|
113
|
+
// Falls through to CLAUDE_CRAP_PLUGIN_ROOT or the last-resort fallback.
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("ignores literal ${CLAUDE_CRAP_PLUGIN_ROOT} and falls through", () => {
|
|
117
|
+
process.env.CLAUDE_CRAP_PLUGIN_ROOT = "${CLAUDE_CRAP_PLUGIN_ROOT}";
|
|
118
|
+
const cfg = loadConfig();
|
|
119
|
+
assert.notEqual(cfg.pluginRoot, "${CLAUDE_CRAP_PLUGIN_ROOT}");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("ignores literal ${SOMETHING_ELSE} placeholder", () => {
|
|
123
|
+
process.env.CLAUDE_PROJECT_DIR = "${NOT_A_REAL_VAR}";
|
|
124
|
+
const cfg = loadConfig();
|
|
125
|
+
assert.notEqual(cfg.pluginRoot, "${NOT_A_REAL_VAR}");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("a literal CLAUDE_PROJECT_DIR falls through to a valid CLAUDE_CRAP_PLUGIN_ROOT", () => {
|
|
129
|
+
process.env.CLAUDE_PROJECT_DIR = "${CLAUDE_PROJECT_DIR}";
|
|
130
|
+
process.env.CLAUDE_CRAP_PLUGIN_ROOT = "/explicit/plugin/root";
|
|
131
|
+
const cfg = loadConfig();
|
|
132
|
+
assert.equal(cfg.pluginRoot, "/explicit/plugin/root");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("treats an empty CLAUDE_PROJECT_DIR as unset", () => {
|
|
136
|
+
process.env.CLAUDE_PROJECT_DIR = "";
|
|
137
|
+
process.env.CLAUDE_CRAP_PLUGIN_ROOT = "/explicit/plugin/root";
|
|
138
|
+
const cfg = loadConfig();
|
|
139
|
+
assert.equal(cfg.pluginRoot, "/explicit/plugin/root");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("discoverWorkspaceRoot — injectable parent-cwd fallback", () => {
|
|
144
|
+
// The real readParentCwd implementation reads the parent process's
|
|
145
|
+
// cwd via lsof/proc. For tests we inject a stub so we can pin the
|
|
146
|
+
// behavior without spawning lsof or depending on the host OS.
|
|
147
|
+
let saved: Snapshot;
|
|
148
|
+
|
|
149
|
+
beforeEach(() => {
|
|
150
|
+
saved = snapshotEnv();
|
|
151
|
+
clearConfigEnv();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
afterEach(() => {
|
|
155
|
+
restoreEnv(saved);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns env CLAUDE_PROJECT_DIR before consulting parent cwd", () => {
|
|
159
|
+
process.env.CLAUDE_PROJECT_DIR = "/workspace/project";
|
|
160
|
+
const root = discoverWorkspaceRoot({
|
|
161
|
+
readParentCwd: () => "/should/not/be/used",
|
|
162
|
+
});
|
|
163
|
+
assert.equal(root, "/workspace/project");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("falls through literal ${...} and uses injected parent cwd", () => {
|
|
167
|
+
process.env.CLAUDE_PROJECT_DIR = "${CLAUDE_PROJECT_DIR}";
|
|
168
|
+
const root = discoverWorkspaceRoot({
|
|
169
|
+
readParentCwd: () => "/real/workspace",
|
|
170
|
+
});
|
|
171
|
+
assert.equal(root, "/real/workspace");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("uses parent cwd when no env vars are set", () => {
|
|
175
|
+
const root = discoverWorkspaceRoot({
|
|
176
|
+
readParentCwd: () => "/discovered/via/parent",
|
|
177
|
+
});
|
|
178
|
+
assert.equal(root, "/discovered/via/parent");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("falls back to process.cwd() when parent cwd is unavailable", () => {
|
|
182
|
+
const root = discoverWorkspaceRoot({
|
|
183
|
+
readParentCwd: () => undefined,
|
|
184
|
+
});
|
|
185
|
+
assert.equal(root, process.cwd());
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("ignores literal ${...} from parent cwd as well", () => {
|
|
189
|
+
const root = discoverWorkspaceRoot({
|
|
190
|
+
readParentCwd: () => "${something}",
|
|
191
|
+
});
|
|
192
|
+
assert.equal(root, process.cwd());
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("CLAUDE_CRAP_PLUGIN_ROOT wins over parent cwd", () => {
|
|
196
|
+
process.env.CLAUDE_CRAP_PLUGIN_ROOT = "/explicit";
|
|
197
|
+
const root = discoverWorkspaceRoot({
|
|
198
|
+
readParentCwd: () => "/parent/cwd",
|
|
199
|
+
});
|
|
200
|
+
assert.equal(root, "/explicit");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("loadConfig — defaults", () => {
|
|
205
|
+
let saved: Snapshot;
|
|
206
|
+
|
|
207
|
+
beforeEach(() => {
|
|
208
|
+
saved = snapshotEnv();
|
|
209
|
+
clearConfigEnv();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
afterEach(() => {
|
|
213
|
+
restoreEnv(saved);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("sarifOutputDir defaults to .claude-crap/reports", () => {
|
|
217
|
+
assert.equal(loadConfig().sarifOutputDir, ".claude-crap/reports");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("crapThreshold defaults to 30", () => {
|
|
221
|
+
assert.equal(loadConfig().crapThreshold, 30);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("cyclomaticMax defaults to 15", () => {
|
|
225
|
+
assert.equal(loadConfig().cyclomaticMax, 15);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("tdrMaxRating defaults to C", () => {
|
|
229
|
+
assert.equal(loadConfig().tdrMaxRating, "C");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("minutesPerLoc defaults to 30", () => {
|
|
233
|
+
assert.equal(loadConfig().minutesPerLoc, 30);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("dashboardPort defaults to 5117", () => {
|
|
237
|
+
assert.equal(loadConfig().dashboardPort, 5117);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("loadConfig — numeric parsing", () => {
|
|
242
|
+
let saved: Snapshot;
|
|
243
|
+
|
|
244
|
+
beforeEach(() => {
|
|
245
|
+
saved = snapshotEnv();
|
|
246
|
+
clearConfigEnv();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
afterEach(() => {
|
|
250
|
+
restoreEnv(saved);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("reads CLAUDE_CRAP_CRAP_THRESHOLD from env", () => {
|
|
254
|
+
process.env.CLAUDE_CRAP_CRAP_THRESHOLD = "50";
|
|
255
|
+
assert.equal(loadConfig().crapThreshold, 50);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("throws on non-numeric CLAUDE_CRAP_CRAP_THRESHOLD", () => {
|
|
259
|
+
process.env.CLAUDE_CRAP_CRAP_THRESHOLD = "not-a-number";
|
|
260
|
+
assert.throws(() => loadConfig(), /not a finite number/);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("ignores empty string and falls back to default", () => {
|
|
264
|
+
process.env.CLAUDE_CRAP_CRAP_THRESHOLD = "";
|
|
265
|
+
assert.equal(loadConfig().crapThreshold, 30);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe("loadConfig — rating parsing", () => {
|
|
270
|
+
let saved: Snapshot;
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
saved = snapshotEnv();
|
|
274
|
+
clearConfigEnv();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
afterEach(() => {
|
|
278
|
+
restoreEnv(saved);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("accepts lowercase rating", () => {
|
|
282
|
+
process.env.CLAUDE_CRAP_TDR_MAX_RATING = "a";
|
|
283
|
+
assert.equal(loadConfig().tdrMaxRating, "A");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("accepts padded rating", () => {
|
|
287
|
+
process.env.CLAUDE_CRAP_TDR_MAX_RATING = " B ";
|
|
288
|
+
assert.equal(loadConfig().tdrMaxRating, "B");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("throws on invalid rating letter", () => {
|
|
292
|
+
process.env.CLAUDE_CRAP_TDR_MAX_RATING = "F";
|
|
293
|
+
assert.throws(() => loadConfig(), /must be one of A, B, C, D, E/);
|
|
294
|
+
});
|
|
295
|
+
});
|