Skip to content

Commit 6f1a413

Browse files
PaulJPhilpclaude
andcommitted
feat: Add Pattern Server with REST API and comprehensive test suite
Implemented a complete Pattern Server + CLI workflow with full test coverage: ## Pattern Server (server/index.ts) - GET /health - Health check endpoint - GET /api/v1/rules - List all Effect patterns - GET /api/v1/rules/:id - Get single pattern by ID - Schema validation with Effect Schema - Tagged error handling (RuleNotFoundError, RuleLoadError, etc.) - Structured logging for all requests ## CLI Tool (scripts/ep.ts) - `ep rules add --tool cursor` command - Fetches rules from Pattern Server API - Injects into .cursor/rules.md with managed block markers - Preserves user content outside managed blocks - Uses FetchHttpClient for Bun compatibility ## Test Suite (37 tests, 100% passing) - server/server.test.ts: 10 API endpoint tests - scripts/ep-rules-add.test.ts: 18 CLI command tests - scripts/integration.test.ts: 9 E2E integration tests - vitest.config.ts: Sequential execution for test isolation - Tests cover: API validation, CLI operations, E2E flows, error handling All tests use Effect-based HTTP clients and proper server lifecycle management. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 0a37dfc commit 6f1a413

7 files changed

Lines changed: 3156 additions & 1 deletion

File tree

package.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"private": true,
66
"description": "A community-driven knowledge base for Effect-TS patterns.",
77
"scripts": {
8+
"ep": "bun run scripts/ep.ts",
89
"publish": "bun run scripts/publish/publish.ts",
910
"validate": "bun run scripts/publish/validate-improved.ts",
1011
"validate:simple": "bun run scripts/publish/validate.ts",
@@ -19,6 +20,7 @@
1920
"lint:all": "bun run lint && bun run lint:effect",
2021
"rules": "bun run scripts/publish/rules-improved.ts",
2122
"rules:simple": "bun run scripts/publish/rules.ts",
23+
"rules:claude": "bun run scripts/publish/generate-claude-rules.ts",
2224
"ingest": "bun run scripts/ingest/ingest-pipeline-improved.ts",
2325
"ingest:old": "bun run scripts/ingest/process.ts",
2426
"pipeline": "bun run scripts/publish/pipeline.ts",
@@ -38,7 +40,12 @@
3840
"qa:status": "bun run scripts/qa/qa-status.ts",
3941
"qa:repair:dry": "bun run scripts/qa/qa-repair.ts --dry-run",
4042
"qa:all": "./scripts/qa/qa-process.sh && bun run qa:report",
41-
"qa:test": "bun run scripts/qa/test-enhanced-qa.ts"
43+
"qa:test": "bun run scripts/qa/test-enhanced-qa.ts",
44+
"server:dev": "bun --watch server/index.ts",
45+
"test:server": "vitest run server",
46+
"test:cli": "vitest run scripts/ep-rules-add.test.ts",
47+
"test:e2e": "vitest run scripts/integration.test.ts",
48+
"test:api": "vitest run server scripts/ep-rules-add.test.ts scripts/integration.test.ts"
4249
},
4350
"dependencies": {
4451
"@effect/ai": "^0.25.2",
@@ -59,13 +66,17 @@
5966
"@opentelemetry/sdk-trace-node": "^2.1.0",
6067
"@opentelemetry/semantic-conventions": "^1.37.0",
6168
"cli-table3": "^0.6.5",
69+
"conventional-commits-parser": "^6.2.0",
70+
"conventional-recommended-bump": "^11.2.0",
6271
"dotenv": "^17.2.2",
6372
"effect": "^3.17.14",
6473
"effect-mdx": "^0.1.0",
74+
"glob": "^11.0.3",
6575
"gray-matter": "^4.0.3",
6676
"js-yaml": "^4.1.0",
6777
"liquidjs": "^10.21.1",
6878
"process": "^0.11.10",
79+
"semver": "^7.7.2",
6980
"yaml": "^2.8.1"
7081
},
7182
"devDependencies": {

scripts/ep-rules-add.test.ts

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
/**
2+
* ep rules add Command Tests
3+
*
4+
* Comprehensive test suite for the CLI rules add command
5+
*/
6+
7+
import { FileSystem } from "@effect/platform";
8+
import { NodeContext } from "@effect/platform-node";
9+
import { Effect } from "effect";
10+
import { describe, expect, it, beforeEach, afterEach } from "vitest";
11+
import { spawn, type ChildProcess } from "child_process";
12+
import * as fs from "fs/promises";
13+
import * as path from "path";
14+
15+
// --- TEST UTILITIES ---
16+
17+
let serverProcess: ChildProcess | null = null;
18+
19+
const startServer = async () => {
20+
serverProcess = spawn("bun", ["run", "server/index.ts"], {
21+
stdio: "pipe",
22+
});
23+
await new Promise((resolve) => setTimeout(resolve, 2000));
24+
};
25+
26+
const stopServer = () => {
27+
if (serverProcess) {
28+
serverProcess.kill();
29+
serverProcess = null;
30+
}
31+
};
32+
33+
const runCommand = async (args: string[]): Promise<{ stdout: string; stderr: string; exitCode: number }> => {
34+
return new Promise((resolve) => {
35+
const proc = spawn("bun", ["run", "scripts/ep.ts", ...args], {
36+
stdio: "pipe",
37+
});
38+
39+
let stdout = "";
40+
let stderr = "";
41+
42+
proc.stdout?.on("data", (data) => {
43+
stdout += data.toString();
44+
});
45+
46+
proc.stderr?.on("data", (data) => {
47+
stderr += data.toString();
48+
});
49+
50+
proc.on("close", (code) => {
51+
resolve({ stdout, stderr, exitCode: code || 0 });
52+
});
53+
});
54+
};
55+
56+
const TEST_DIR = ".cursor-test";
57+
const TEST_FILE = path.join(TEST_DIR, "rules.md");
58+
59+
// --- TESTS ---
60+
61+
describe.sequential("ep rules add command", () => {
62+
beforeEach(async () => {
63+
// Start server before each test
64+
await startServer();
65+
66+
// Clean up test directory
67+
try {
68+
await fs.rm(TEST_DIR, { recursive: true });
69+
} catch {}
70+
});
71+
72+
afterEach(async () => {
73+
// Stop server after each test
74+
stopServer();
75+
76+
// Clean up test directory
77+
try {
78+
await fs.rm(TEST_DIR, { recursive: true });
79+
} catch {}
80+
});
81+
82+
describe("Tool Validation", () => {
83+
it("should require --tool option", async () => {
84+
const result = await runCommand(["rules", "add"]);
85+
86+
expect(result.exitCode).not.toBe(0);
87+
expect(result.stderr).toContain("tool");
88+
});
89+
90+
it("should reject unsupported tools", async () => {
91+
const result = await runCommand(["rules", "add", "--tool", "windsurf"]);
92+
93+
expect(result.exitCode).not.toBe(0);
94+
expect(result.stdout).toContain("not supported");
95+
expect(result.stdout).toContain("windsurf");
96+
});
97+
98+
it("should accept cursor tool", async () => {
99+
// Override target file for test
100+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
101+
102+
expect(result.exitCode).toBe(0);
103+
expect(result.stdout).toContain("Fetching rules");
104+
});
105+
});
106+
107+
describe("API Integration", () => {
108+
it("should fetch rules from server", async () => {
109+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
110+
111+
expect(result.exitCode).toBe(0);
112+
expect(result.stdout).toContain("Fetched");
113+
expect(result.stdout).toContain("rules");
114+
});
115+
116+
it("should handle server unavailable gracefully", async () => {
117+
// Stop server first
118+
stopServer();
119+
120+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
121+
122+
expect(result.exitCode).not.toBe(0);
123+
expect(result.stdout).toContain("Failed to fetch");
124+
});
125+
});
126+
127+
describe("File Operations", () => {
128+
it("should create .cursor directory if not exists", async () => {
129+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
130+
131+
expect(result.exitCode).toBe(0);
132+
133+
// Check directory was created
134+
const dirExists = await fs.stat(".cursor")
135+
.then(() => true)
136+
.catch(() => false);
137+
138+
expect(dirExists).toBe(true);
139+
});
140+
141+
it("should create rules.md file", async () => {
142+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
143+
144+
expect(result.exitCode).toBe(0);
145+
146+
// Check file was created
147+
const fileExists = await fs.stat(".cursor/rules.md")
148+
.then(() => true)
149+
.catch(() => false);
150+
151+
expect(fileExists).toBe(true);
152+
});
153+
154+
it("should include managed block markers", async () => {
155+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
156+
157+
expect(result.exitCode).toBe(0);
158+
159+
const content = await fs.readFile(".cursor/rules.md", "utf-8");
160+
161+
expect(content).toContain("# --- BEGIN EFFECTPATTERNS RULES ---");
162+
expect(content).toContain("# --- END EFFECTPATTERNS RULES ---");
163+
});
164+
165+
it("should format rules correctly", async () => {
166+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
167+
168+
expect(result.exitCode).toBe(0);
169+
170+
const content = await fs.readFile(".cursor/rules.md", "utf-8");
171+
172+
// Check for rule formatting
173+
expect(content).toContain("###");
174+
expect(content).toContain("**ID:**");
175+
expect(content).toContain("**Use Case:**");
176+
expect(content).toContain("**Skill Level:**");
177+
});
178+
});
179+
180+
describe("Update Behavior", () => {
181+
it("should replace existing managed block", async () => {
182+
// Create initial file
183+
await fs.mkdir(".cursor", { recursive: true });
184+
const initialContent = `# My Custom Rules
185+
186+
Some custom content here
187+
188+
# --- BEGIN EFFECTPATTERNS RULES ---
189+
Old rules content
190+
# --- END EFFECTPATTERNS RULES ---
191+
192+
More custom content`;
193+
194+
await fs.writeFile(".cursor/rules.md", initialContent);
195+
196+
// Run command
197+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
198+
expect(result.exitCode).toBe(0);
199+
200+
// Check content
201+
const content = await fs.readFile(".cursor/rules.md", "utf-8");
202+
203+
// Should preserve custom content
204+
expect(content).toContain("My Custom Rules");
205+
expect(content).toContain("Some custom content here");
206+
expect(content).toContain("More custom content");
207+
208+
// Should replace managed block
209+
expect(content).not.toContain("Old rules content");
210+
expect(content).toContain("# --- BEGIN EFFECTPATTERNS RULES ---");
211+
expect(content).toContain("# --- END EFFECTPATTERNS RULES ---");
212+
});
213+
214+
it("should append managed block if not present", async () => {
215+
// Create initial file without managed block
216+
await fs.mkdir(".cursor", { recursive: true });
217+
const initialContent = `# My Custom Rules
218+
219+
Some custom content here`;
220+
221+
await fs.writeFile(".cursor/rules.md", initialContent);
222+
223+
// Run command
224+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
225+
expect(result.exitCode).toBe(0);
226+
227+
// Check content
228+
const content = await fs.readFile(".cursor/rules.md", "utf-8");
229+
230+
// Should preserve custom content
231+
expect(content).toContain("My Custom Rules");
232+
expect(content).toContain("Some custom content here");
233+
234+
// Should add managed block
235+
expect(content).toContain("# --- BEGIN EFFECTPATTERNS RULES ---");
236+
expect(content).toContain("# --- END EFFECTPATTERNS RULES ---");
237+
});
238+
});
239+
240+
describe("Output Messages", () => {
241+
it("should show progress messages", async () => {
242+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
243+
244+
expect(result.exitCode).toBe(0);
245+
expect(result.stdout).toContain("Fetching rules");
246+
expect(result.stdout).toContain("Injecting rules");
247+
expect(result.stdout).toContain("Successfully added");
248+
expect(result.stdout).toContain("Rules integration complete");
249+
});
250+
251+
it("should show rule count", async () => {
252+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
253+
254+
expect(result.exitCode).toBe(0);
255+
expect(result.stdout).toMatch(/Fetched \d+ rules/);
256+
expect(result.stdout).toMatch(/Successfully added \d+ rules/);
257+
});
258+
});
259+
260+
describe("Error Handling", () => {
261+
it("should handle file system errors", async () => {
262+
// Clean up .cursor directory if it exists
263+
try {
264+
await fs.rm(".cursor", { recursive: true });
265+
} catch {}
266+
267+
// Create a file where directory should be
268+
await fs.writeFile(".cursor", "This is a file, not a directory");
269+
270+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
271+
272+
// Should fail due to file system error
273+
expect(result.exitCode).not.toBe(0);
274+
275+
// Clean up the file
276+
await fs.unlink(".cursor");
277+
});
278+
279+
it("should show helpful error messages", async () => {
280+
stopServer();
281+
282+
const result = await runCommand(["rules", "add", "--tool", "cursor"]);
283+
284+
expect(result.stdout).toContain("Make sure the Pattern Server is running");
285+
expect(result.stdout).toContain("bun run server:dev");
286+
});
287+
});
288+
});
289+
290+
describe.sequential("ep rules command structure", () => {
291+
it("should have rules subcommand", async () => {
292+
const result = await runCommand(["rules", "--help"]);
293+
294+
expect(result.exitCode).toBe(0);
295+
expect(result.stdout).toContain("rules");
296+
});
297+
298+
it("should have add subcommand", async () => {
299+
const result = await runCommand(["rules", "--help"]);
300+
301+
expect(result.stdout).toContain("add");
302+
});
303+
304+
it("should have generate subcommand", async () => {
305+
const result = await runCommand(["rules", "--help"]);
306+
307+
expect(result.stdout).toContain("generate");
308+
});
309+
});

0 commit comments

Comments
 (0)