claude-crap 0.4.2 → 0.4.3

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.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-crap-plugin",
9
- "version": "0.4.2",
9
+ "version": "0.4.3",
10
10
  "dependencies": {
11
11
  "@fastify/static": "^8.0.3",
12
12
  "@modelcontextprotocol/sdk": "^1.0.4",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-crap-plugin",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for the claude-crap plugin bundle",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -30,7 +30,7 @@ export type MaintainabilityRating = "A" | "B" | "C" | "D" | "E";
30
30
  * configuration at runtime — any change must go through a server restart.
31
31
  */
32
32
  export interface CrapConfig {
33
- /** Absolute path to the plugin root on disk. Defaults to `process.cwd()`. */
33
+ /** Absolute path to the user's workspace. Resolved from `CLAUDE_PROJECT_DIR` `CLAUDE_CRAP_PLUGIN_ROOT` → `process.cwd()`. */
34
34
  readonly pluginRoot: string;
35
35
  /** Directory (relative to the workspace) where consolidated SARIF reports are written. */
36
36
  readonly sarifOutputDir: string;
@@ -98,7 +98,12 @@ function parseRating(raw: string | undefined, fallback: MaintainabilityRating):
98
98
  */
99
99
  export function loadConfig(): CrapConfig {
100
100
  return {
101
- pluginRoot: process.env.CLAUDE_CRAP_PLUGIN_ROOT ?? process.cwd(),
101
+ // CLAUDE_PROJECT_DIR is set by Claude Code to the user's workspace.
102
+ // process.cwd() is NOT reliable — Claude Code sets it to the plugin
103
+ // cache directory when starting MCP servers, not the user's project.
104
+ pluginRoot: process.env.CLAUDE_PROJECT_DIR
105
+ ?? process.env.CLAUDE_CRAP_PLUGIN_ROOT
106
+ ?? process.cwd(),
102
107
  sarifOutputDir: process.env.CLAUDE_CRAP_SARIF_OUTPUT_DIR ?? ".claude-crap/reports",
103
108
  crapThreshold: parseNumber("CLAUDE_CRAP_CRAP_THRESHOLD", process.env.CLAUDE_CRAP_CRAP_THRESHOLD, 30),
104
109
  cyclomaticMax: parseNumber("CLAUDE_CRAP_CYCLOMATIC_MAX", process.env.CLAUDE_CRAP_CYCLOMATIC_MAX, 15),
@@ -0,0 +1,185 @@
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 } 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 — defaults", () => {
95
+ let saved: Snapshot;
96
+
97
+ beforeEach(() => {
98
+ saved = snapshotEnv();
99
+ clearConfigEnv();
100
+ });
101
+
102
+ afterEach(() => {
103
+ restoreEnv(saved);
104
+ });
105
+
106
+ it("sarifOutputDir defaults to .claude-crap/reports", () => {
107
+ assert.equal(loadConfig().sarifOutputDir, ".claude-crap/reports");
108
+ });
109
+
110
+ it("crapThreshold defaults to 30", () => {
111
+ assert.equal(loadConfig().crapThreshold, 30);
112
+ });
113
+
114
+ it("cyclomaticMax defaults to 15", () => {
115
+ assert.equal(loadConfig().cyclomaticMax, 15);
116
+ });
117
+
118
+ it("tdrMaxRating defaults to C", () => {
119
+ assert.equal(loadConfig().tdrMaxRating, "C");
120
+ });
121
+
122
+ it("minutesPerLoc defaults to 30", () => {
123
+ assert.equal(loadConfig().minutesPerLoc, 30);
124
+ });
125
+
126
+ it("dashboardPort defaults to 5117", () => {
127
+ assert.equal(loadConfig().dashboardPort, 5117);
128
+ });
129
+ });
130
+
131
+ describe("loadConfig — numeric parsing", () => {
132
+ let saved: Snapshot;
133
+
134
+ beforeEach(() => {
135
+ saved = snapshotEnv();
136
+ clearConfigEnv();
137
+ });
138
+
139
+ afterEach(() => {
140
+ restoreEnv(saved);
141
+ });
142
+
143
+ it("reads CLAUDE_CRAP_CRAP_THRESHOLD from env", () => {
144
+ process.env.CLAUDE_CRAP_CRAP_THRESHOLD = "50";
145
+ assert.equal(loadConfig().crapThreshold, 50);
146
+ });
147
+
148
+ it("throws on non-numeric CLAUDE_CRAP_CRAP_THRESHOLD", () => {
149
+ process.env.CLAUDE_CRAP_CRAP_THRESHOLD = "not-a-number";
150
+ assert.throws(() => loadConfig(), /not a finite number/);
151
+ });
152
+
153
+ it("ignores empty string and falls back to default", () => {
154
+ process.env.CLAUDE_CRAP_CRAP_THRESHOLD = "";
155
+ assert.equal(loadConfig().crapThreshold, 30);
156
+ });
157
+ });
158
+
159
+ describe("loadConfig — rating parsing", () => {
160
+ let saved: Snapshot;
161
+
162
+ beforeEach(() => {
163
+ saved = snapshotEnv();
164
+ clearConfigEnv();
165
+ });
166
+
167
+ afterEach(() => {
168
+ restoreEnv(saved);
169
+ });
170
+
171
+ it("accepts lowercase rating", () => {
172
+ process.env.CLAUDE_CRAP_TDR_MAX_RATING = "a";
173
+ assert.equal(loadConfig().tdrMaxRating, "A");
174
+ });
175
+
176
+ it("accepts padded rating", () => {
177
+ process.env.CLAUDE_CRAP_TDR_MAX_RATING = " B ";
178
+ assert.equal(loadConfig().tdrMaxRating, "B");
179
+ });
180
+
181
+ it("throws on invalid rating letter", () => {
182
+ process.env.CLAUDE_CRAP_TDR_MAX_RATING = "F";
183
+ assert.throws(() => loadConfig(), /must be one of A, B, C, D, E/);
184
+ });
185
+ });