cache-overflow-mcp 0.2.0 → 0.3.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/.env.example +3 -3
- package/AGENTS.md +235 -0
- package/E2E-TESTING.md +5 -5
- package/LICENSE +21 -0
- package/README.md +13 -6
- package/dist/prompts/index.d.ts +14 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +153 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +16 -2
- package/dist/server.js.map +1 -1
- package/dist/testing/mock-data.js +40 -40
- package/dist/tools/find-solution.d.ts.map +1 -1
- package/dist/tools/find-solution.js +25 -2
- package/dist/tools/find-solution.js.map +1 -1
- package/dist/tools/get-balance.d.ts +3 -0
- package/dist/tools/get-balance.d.ts.map +1 -0
- package/dist/tools/get-balance.js +34 -0
- package/dist/tools/get-balance.js.map +1 -0
- package/dist/tools/submit-feedback.js +1 -1
- package/dist/tools/submit-feedback.js.map +1 -1
- package/dist/tools/submit-verification.js +1 -1
- package/dist/tools/submit-verification.js.map +1 -1
- package/dist/tools/unlock-solution.d.ts.map +1 -1
- package/dist/tools/unlock-solution.js +3 -2
- package/dist/tools/unlock-solution.js.map +1 -1
- package/dist/ui/verification-dialog.js +267 -267
- package/package.json +3 -3
- package/{mock-server.js → scripts/mock-server.js} +1 -1
- package/src/cli.ts +10 -10
- package/src/client.test.ts +116 -116
- package/src/client.ts +76 -76
- package/src/config.ts +9 -9
- package/src/index.ts +3 -3
- package/src/prompts/index.ts +168 -0
- package/src/server.ts +19 -1
- package/src/testing/mock-data.ts +142 -142
- package/src/testing/mock-server.ts +176 -176
- package/src/tools/find-solution.ts +30 -2
- package/src/tools/index.ts +23 -23
- package/src/tools/submit-feedback.ts +1 -1
- package/src/tools/submit-verification.ts +1 -1
- package/src/tools/unlock-solution.ts +4 -2
- package/src/types.ts +39 -39
- package/src/ui/verification-dialog.ts +342 -342
- package/tsconfig.json +20 -20
- package/test-dialog.js +0 -37
package/src/testing/mock-data.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
import type { Solution, FindSolutionResult, Balance } from '../types.js';
|
|
2
|
-
|
|
3
|
-
export const mockSolutions: Solution[] = [
|
|
4
|
-
{
|
|
5
|
-
id: 'sol_001',
|
|
6
|
-
author_id: 'user_123',
|
|
7
|
-
query_title: 'How to implement binary search in TypeScript',
|
|
8
|
-
solution_body: `function binarySearch<T>(arr: T[], target: T): number {
|
|
9
|
-
let left = 0;
|
|
10
|
-
let right = arr.length - 1;
|
|
11
|
-
while (left <= right) {
|
|
12
|
-
const mid = Math.floor((left + right) / 2);
|
|
13
|
-
if (arr[mid] === target) return mid;
|
|
14
|
-
if (arr[mid] < target) left = mid + 1;
|
|
15
|
-
else right = mid - 1;
|
|
16
|
-
}
|
|
17
|
-
return -1;
|
|
18
|
-
}`,
|
|
19
|
-
price_current: 50,
|
|
20
|
-
verification_state: 'VERIFIED',
|
|
21
|
-
access_count: 127,
|
|
22
|
-
upvotes: 45,
|
|
23
|
-
downvotes: 2,
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 'sol_002',
|
|
27
|
-
author_id: 'user_456',
|
|
28
|
-
query_title: 'Fix memory leak in Node.js event listeners',
|
|
29
|
-
solution_body: `// Always remove event listeners when done
|
|
30
|
-
const handler = () => { /* ... */ };
|
|
31
|
-
emitter.on('event', handler);
|
|
32
|
-
// Later:
|
|
33
|
-
emitter.off('event', handler);
|
|
34
|
-
|
|
35
|
-
// Or use once() for one-time listeners
|
|
36
|
-
emitter.once('event', () => { /* ... */ });`,
|
|
37
|
-
price_current: 75,
|
|
38
|
-
verification_state: 'VERIFIED',
|
|
39
|
-
access_count: 89,
|
|
40
|
-
upvotes: 32,
|
|
41
|
-
downvotes: 1,
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'sol_003',
|
|
45
|
-
author_id: 'user_789',
|
|
46
|
-
query_title: 'Optimize React re-renders with useMemo',
|
|
47
|
-
solution_body: `import { useMemo } from 'react';
|
|
48
|
-
|
|
49
|
-
function ExpensiveComponent({ data }) {
|
|
50
|
-
const processed = useMemo(() => {
|
|
51
|
-
return data.map(item => heavyComputation(item));
|
|
52
|
-
}, [data]);
|
|
53
|
-
|
|
54
|
-
return <div>{processed}</div>;
|
|
55
|
-
}`,
|
|
56
|
-
price_current: 60,
|
|
57
|
-
verification_state: 'PENDING',
|
|
58
|
-
access_count: 15,
|
|
59
|
-
upvotes: 8,
|
|
60
|
-
downvotes: 0,
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export const mockFindResults: FindSolutionResult[] = [
|
|
65
|
-
{
|
|
66
|
-
solution_id: 'sol_001',
|
|
67
|
-
query_title: 'How to implement binary search in TypeScript',
|
|
68
|
-
human_verification_required: false,
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
solution_id: 'sol_002',
|
|
72
|
-
query_title: 'Fix memory leak in Node.js event listeners',
|
|
73
|
-
solution_body: `// Always remove event listeners when done
|
|
74
|
-
const handler = () => { /* ... */ };
|
|
75
|
-
emitter.on('event', handler);
|
|
76
|
-
// Later:
|
|
77
|
-
emitter.off('event', handler);
|
|
78
|
-
|
|
79
|
-
// Or use once() for one-time listeners
|
|
80
|
-
emitter.once('event', () => { /* ... */ });`,
|
|
81
|
-
human_verification_required: true,
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
solution_id: 'sol_003',
|
|
85
|
-
query_title: 'Optimize React re-renders with useMemo',
|
|
86
|
-
solution_body: `import { useMemo } from 'react';
|
|
87
|
-
|
|
88
|
-
function ExpensiveComponent({ data }) {
|
|
89
|
-
const processed = useMemo(() => {
|
|
90
|
-
return data.map(item => heavyComputation(item));
|
|
91
|
-
}, [data]);
|
|
92
|
-
|
|
93
|
-
return <div>{processed}</div>;
|
|
94
|
-
}`,
|
|
95
|
-
human_verification_required: false,
|
|
96
|
-
},
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
export const mockBalance: Balance = {
|
|
100
|
-
available: 1500,
|
|
101
|
-
pending_debits: 75,
|
|
102
|
-
pending_credits: 200,
|
|
103
|
-
total_earned: 3500,
|
|
104
|
-
total_spent: 1800,
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
export function createMockSolution(overrides: Partial<Solution> = {}): Solution {
|
|
108
|
-
return {
|
|
109
|
-
id: `sol_${Date.now()}`,
|
|
110
|
-
author_id: 'user_mock',
|
|
111
|
-
query_title: 'Mock solution title',
|
|
112
|
-
solution_body: 'Mock solution body content',
|
|
113
|
-
price_current: 50,
|
|
114
|
-
verification_state: 'PENDING',
|
|
115
|
-
access_count: 0,
|
|
116
|
-
upvotes: 0,
|
|
117
|
-
downvotes: 0,
|
|
118
|
-
...overrides,
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function createMockFindResult(
|
|
123
|
-
overrides: Partial<FindSolutionResult> = {}
|
|
124
|
-
): FindSolutionResult {
|
|
125
|
-
return {
|
|
126
|
-
solution_id: `sol_${Date.now()}`,
|
|
127
|
-
query_title: 'Mock query title',
|
|
128
|
-
human_verification_required: false,
|
|
129
|
-
...overrides,
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export function createMockBalance(overrides: Partial<Balance> = {}): Balance {
|
|
134
|
-
return {
|
|
135
|
-
available: 1000,
|
|
136
|
-
pending_debits: 0,
|
|
137
|
-
pending_credits: 0,
|
|
138
|
-
total_earned: 1000,
|
|
139
|
-
total_spent: 0,
|
|
140
|
-
...overrides,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
1
|
+
import type { Solution, FindSolutionResult, Balance } from '../types.js';
|
|
2
|
+
|
|
3
|
+
export const mockSolutions: Solution[] = [
|
|
4
|
+
{
|
|
5
|
+
id: 'sol_001',
|
|
6
|
+
author_id: 'user_123',
|
|
7
|
+
query_title: 'How to implement binary search in TypeScript',
|
|
8
|
+
solution_body: `function binarySearch<T>(arr: T[], target: T): number {
|
|
9
|
+
let left = 0;
|
|
10
|
+
let right = arr.length - 1;
|
|
11
|
+
while (left <= right) {
|
|
12
|
+
const mid = Math.floor((left + right) / 2);
|
|
13
|
+
if (arr[mid] === target) return mid;
|
|
14
|
+
if (arr[mid] < target) left = mid + 1;
|
|
15
|
+
else right = mid - 1;
|
|
16
|
+
}
|
|
17
|
+
return -1;
|
|
18
|
+
}`,
|
|
19
|
+
price_current: 50,
|
|
20
|
+
verification_state: 'VERIFIED',
|
|
21
|
+
access_count: 127,
|
|
22
|
+
upvotes: 45,
|
|
23
|
+
downvotes: 2,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'sol_002',
|
|
27
|
+
author_id: 'user_456',
|
|
28
|
+
query_title: 'Fix memory leak in Node.js event listeners',
|
|
29
|
+
solution_body: `// Always remove event listeners when done
|
|
30
|
+
const handler = () => { /* ... */ };
|
|
31
|
+
emitter.on('event', handler);
|
|
32
|
+
// Later:
|
|
33
|
+
emitter.off('event', handler);
|
|
34
|
+
|
|
35
|
+
// Or use once() for one-time listeners
|
|
36
|
+
emitter.once('event', () => { /* ... */ });`,
|
|
37
|
+
price_current: 75,
|
|
38
|
+
verification_state: 'VERIFIED',
|
|
39
|
+
access_count: 89,
|
|
40
|
+
upvotes: 32,
|
|
41
|
+
downvotes: 1,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'sol_003',
|
|
45
|
+
author_id: 'user_789',
|
|
46
|
+
query_title: 'Optimize React re-renders with useMemo',
|
|
47
|
+
solution_body: `import { useMemo } from 'react';
|
|
48
|
+
|
|
49
|
+
function ExpensiveComponent({ data }) {
|
|
50
|
+
const processed = useMemo(() => {
|
|
51
|
+
return data.map(item => heavyComputation(item));
|
|
52
|
+
}, [data]);
|
|
53
|
+
|
|
54
|
+
return <div>{processed}</div>;
|
|
55
|
+
}`,
|
|
56
|
+
price_current: 60,
|
|
57
|
+
verification_state: 'PENDING',
|
|
58
|
+
access_count: 15,
|
|
59
|
+
upvotes: 8,
|
|
60
|
+
downvotes: 0,
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
export const mockFindResults: FindSolutionResult[] = [
|
|
65
|
+
{
|
|
66
|
+
solution_id: 'sol_001',
|
|
67
|
+
query_title: 'How to implement binary search in TypeScript',
|
|
68
|
+
human_verification_required: false,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
solution_id: 'sol_002',
|
|
72
|
+
query_title: 'Fix memory leak in Node.js event listeners',
|
|
73
|
+
solution_body: `// Always remove event listeners when done
|
|
74
|
+
const handler = () => { /* ... */ };
|
|
75
|
+
emitter.on('event', handler);
|
|
76
|
+
// Later:
|
|
77
|
+
emitter.off('event', handler);
|
|
78
|
+
|
|
79
|
+
// Or use once() for one-time listeners
|
|
80
|
+
emitter.once('event', () => { /* ... */ });`,
|
|
81
|
+
human_verification_required: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
solution_id: 'sol_003',
|
|
85
|
+
query_title: 'Optimize React re-renders with useMemo',
|
|
86
|
+
solution_body: `import { useMemo } from 'react';
|
|
87
|
+
|
|
88
|
+
function ExpensiveComponent({ data }) {
|
|
89
|
+
const processed = useMemo(() => {
|
|
90
|
+
return data.map(item => heavyComputation(item));
|
|
91
|
+
}, [data]);
|
|
92
|
+
|
|
93
|
+
return <div>{processed}</div>;
|
|
94
|
+
}`,
|
|
95
|
+
human_verification_required: false,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
export const mockBalance: Balance = {
|
|
100
|
+
available: 1500,
|
|
101
|
+
pending_debits: 75,
|
|
102
|
+
pending_credits: 200,
|
|
103
|
+
total_earned: 3500,
|
|
104
|
+
total_spent: 1800,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export function createMockSolution(overrides: Partial<Solution> = {}): Solution {
|
|
108
|
+
return {
|
|
109
|
+
id: `sol_${Date.now()}`,
|
|
110
|
+
author_id: 'user_mock',
|
|
111
|
+
query_title: 'Mock solution title',
|
|
112
|
+
solution_body: 'Mock solution body content',
|
|
113
|
+
price_current: 50,
|
|
114
|
+
verification_state: 'PENDING',
|
|
115
|
+
access_count: 0,
|
|
116
|
+
upvotes: 0,
|
|
117
|
+
downvotes: 0,
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createMockFindResult(
|
|
123
|
+
overrides: Partial<FindSolutionResult> = {}
|
|
124
|
+
): FindSolutionResult {
|
|
125
|
+
return {
|
|
126
|
+
solution_id: `sol_${Date.now()}`,
|
|
127
|
+
query_title: 'Mock query title',
|
|
128
|
+
human_verification_required: false,
|
|
129
|
+
...overrides,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createMockBalance(overrides: Partial<Balance> = {}): Balance {
|
|
134
|
+
return {
|
|
135
|
+
available: 1000,
|
|
136
|
+
pending_debits: 0,
|
|
137
|
+
pending_credits: 0,
|
|
138
|
+
total_earned: 1000,
|
|
139
|
+
total_spent: 0,
|
|
140
|
+
...overrides,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -1,176 +1,176 @@
|
|
|
1
|
-
import * as http from 'node:http';
|
|
2
|
-
import { mockSolutions, mockFindResults, createMockSolution } from './mock-data.js';
|
|
3
|
-
import type { Solution, FindSolutionResult } from '../types.js';
|
|
4
|
-
|
|
5
|
-
interface RouteHandler {
|
|
6
|
-
(
|
|
7
|
-
req: http.IncomingMessage,
|
|
8
|
-
body: unknown,
|
|
9
|
-
params: Record<string, string>
|
|
10
|
-
): { status: number; data: unknown };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface Route {
|
|
14
|
-
method: string;
|
|
15
|
-
pattern: RegExp;
|
|
16
|
-
paramNames: string[];
|
|
17
|
-
handler: RouteHandler;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class MockServer {
|
|
21
|
-
private server: http.Server | null = null;
|
|
22
|
-
private port: number = 0;
|
|
23
|
-
private routes: Route[] = [];
|
|
24
|
-
|
|
25
|
-
get url(): string {
|
|
26
|
-
return `http://localhost:${this.port}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
constructor() {
|
|
30
|
-
this.setupRoutes();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private setupRoutes(): void {
|
|
34
|
-
// POST /solutions/find
|
|
35
|
-
this.addRoute('POST', '/solutions/find', (_req, body) => {
|
|
36
|
-
const { query } = body as { query: string };
|
|
37
|
-
const results: FindSolutionResult[] = mockFindResults.filter((r) =>
|
|
38
|
-
r.query_title.toLowerCase().includes((query ?? '').toLowerCase())
|
|
39
|
-
);
|
|
40
|
-
return { status: 200, data: results.length > 0 ? results : mockFindResults };
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// POST /solutions/:id/unlock
|
|
44
|
-
this.addRoute('POST', '/solutions/:id/unlock', (_req, _body, params) => {
|
|
45
|
-
const solution = mockSolutions.find((s) => s.id === params.id);
|
|
46
|
-
if (solution) {
|
|
47
|
-
return { status: 200, data: solution };
|
|
48
|
-
}
|
|
49
|
-
return { status: 200, data: mockSolutions[0] };
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// POST /solutions
|
|
53
|
-
this.addRoute('POST', '/solutions', (_req, body) => {
|
|
54
|
-
const { query_title, solution_body } = body as {
|
|
55
|
-
query_title: string;
|
|
56
|
-
solution_body: string;
|
|
57
|
-
};
|
|
58
|
-
const newSolution: Solution = createMockSolution({
|
|
59
|
-
query_title,
|
|
60
|
-
solution_body,
|
|
61
|
-
});
|
|
62
|
-
return { status: 200, data: newSolution };
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// POST /solutions/:id/verify
|
|
66
|
-
this.addRoute('POST', '/solutions/:id/verify', () => {
|
|
67
|
-
return { status: 200, data: null };
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// POST /solutions/:id/feedback
|
|
71
|
-
this.addRoute('POST', '/solutions/:id/feedback', () => {
|
|
72
|
-
return { status: 200, data: null };
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private addRoute(method: string, path: string, handler: RouteHandler): void {
|
|
77
|
-
const paramNames: string[] = [];
|
|
78
|
-
const patternString = path.replace(/:([^/]+)/g, (_match, paramName) => {
|
|
79
|
-
paramNames.push(paramName);
|
|
80
|
-
return '([^/]+)';
|
|
81
|
-
});
|
|
82
|
-
const pattern = new RegExp(`^${patternString}$`);
|
|
83
|
-
this.routes.push({ method, pattern, paramNames, handler });
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
private matchRoute(
|
|
87
|
-
method: string,
|
|
88
|
-
path: string
|
|
89
|
-
): { route: Route; params: Record<string, string> } | null {
|
|
90
|
-
for (const route of this.routes) {
|
|
91
|
-
if (route.method !== method) continue;
|
|
92
|
-
const match = path.match(route.pattern);
|
|
93
|
-
if (match) {
|
|
94
|
-
const params: Record<string, string> = {};
|
|
95
|
-
route.paramNames.forEach((name, index) => {
|
|
96
|
-
params[name] = match[index + 1];
|
|
97
|
-
});
|
|
98
|
-
return { route, params };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async start(port?: number): Promise<void> {
|
|
105
|
-
return new Promise((resolve, reject) => {
|
|
106
|
-
this.server = http.createServer((req, res) => {
|
|
107
|
-
this.handleRequest(req, res);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
this.server.on('error', reject);
|
|
111
|
-
|
|
112
|
-
this.server.listen(port ?? 0, () => {
|
|
113
|
-
const address = this.server!.address();
|
|
114
|
-
if (typeof address === 'object' && address !== null) {
|
|
115
|
-
this.port = address.port;
|
|
116
|
-
}
|
|
117
|
-
resolve();
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
async stop(): Promise<void> {
|
|
123
|
-
return new Promise((resolve, reject) => {
|
|
124
|
-
if (!this.server) {
|
|
125
|
-
resolve();
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
this.server.close((err) => {
|
|
130
|
-
if (err) {
|
|
131
|
-
reject(err);
|
|
132
|
-
} else {
|
|
133
|
-
this.server = null;
|
|
134
|
-
this.port = 0;
|
|
135
|
-
resolve();
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
142
|
-
const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
|
|
143
|
-
const method = req.method ?? 'GET';
|
|
144
|
-
const path = url.pathname;
|
|
145
|
-
|
|
146
|
-
let body = '';
|
|
147
|
-
req.on('data', (chunk) => {
|
|
148
|
-
body += chunk;
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
req.on('end', () => {
|
|
152
|
-
let parsedBody: unknown = null;
|
|
153
|
-
if (body) {
|
|
154
|
-
try {
|
|
155
|
-
parsedBody = JSON.parse(body);
|
|
156
|
-
} catch {
|
|
157
|
-
// Ignore parse errors
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const matched = this.matchRoute(method, path);
|
|
162
|
-
|
|
163
|
-
if (!matched) {
|
|
164
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
165
|
-
res.end(JSON.stringify({ error: 'Not found' }));
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const { route, params } = matched;
|
|
170
|
-
const result = route.handler(req, parsedBody, params);
|
|
171
|
-
|
|
172
|
-
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
173
|
-
res.end(JSON.stringify(result.data));
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
1
|
+
import * as http from 'node:http';
|
|
2
|
+
import { mockSolutions, mockFindResults, createMockSolution } from './mock-data.js';
|
|
3
|
+
import type { Solution, FindSolutionResult } from '../types.js';
|
|
4
|
+
|
|
5
|
+
interface RouteHandler {
|
|
6
|
+
(
|
|
7
|
+
req: http.IncomingMessage,
|
|
8
|
+
body: unknown,
|
|
9
|
+
params: Record<string, string>
|
|
10
|
+
): { status: number; data: unknown };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Route {
|
|
14
|
+
method: string;
|
|
15
|
+
pattern: RegExp;
|
|
16
|
+
paramNames: string[];
|
|
17
|
+
handler: RouteHandler;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class MockServer {
|
|
21
|
+
private server: http.Server | null = null;
|
|
22
|
+
private port: number = 0;
|
|
23
|
+
private routes: Route[] = [];
|
|
24
|
+
|
|
25
|
+
get url(): string {
|
|
26
|
+
return `http://localhost:${this.port}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
constructor() {
|
|
30
|
+
this.setupRoutes();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private setupRoutes(): void {
|
|
34
|
+
// POST /solutions/find
|
|
35
|
+
this.addRoute('POST', '/solutions/find', (_req, body) => {
|
|
36
|
+
const { query } = body as { query: string };
|
|
37
|
+
const results: FindSolutionResult[] = mockFindResults.filter((r) =>
|
|
38
|
+
r.query_title.toLowerCase().includes((query ?? '').toLowerCase())
|
|
39
|
+
);
|
|
40
|
+
return { status: 200, data: results.length > 0 ? results : mockFindResults };
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// POST /solutions/:id/unlock
|
|
44
|
+
this.addRoute('POST', '/solutions/:id/unlock', (_req, _body, params) => {
|
|
45
|
+
const solution = mockSolutions.find((s) => s.id === params.id);
|
|
46
|
+
if (solution) {
|
|
47
|
+
return { status: 200, data: solution };
|
|
48
|
+
}
|
|
49
|
+
return { status: 200, data: mockSolutions[0] };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// POST /solutions
|
|
53
|
+
this.addRoute('POST', '/solutions', (_req, body) => {
|
|
54
|
+
const { query_title, solution_body } = body as {
|
|
55
|
+
query_title: string;
|
|
56
|
+
solution_body: string;
|
|
57
|
+
};
|
|
58
|
+
const newSolution: Solution = createMockSolution({
|
|
59
|
+
query_title,
|
|
60
|
+
solution_body,
|
|
61
|
+
});
|
|
62
|
+
return { status: 200, data: newSolution };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// POST /solutions/:id/verify
|
|
66
|
+
this.addRoute('POST', '/solutions/:id/verify', () => {
|
|
67
|
+
return { status: 200, data: null };
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// POST /solutions/:id/feedback
|
|
71
|
+
this.addRoute('POST', '/solutions/:id/feedback', () => {
|
|
72
|
+
return { status: 200, data: null };
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private addRoute(method: string, path: string, handler: RouteHandler): void {
|
|
77
|
+
const paramNames: string[] = [];
|
|
78
|
+
const patternString = path.replace(/:([^/]+)/g, (_match, paramName) => {
|
|
79
|
+
paramNames.push(paramName);
|
|
80
|
+
return '([^/]+)';
|
|
81
|
+
});
|
|
82
|
+
const pattern = new RegExp(`^${patternString}$`);
|
|
83
|
+
this.routes.push({ method, pattern, paramNames, handler });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private matchRoute(
|
|
87
|
+
method: string,
|
|
88
|
+
path: string
|
|
89
|
+
): { route: Route; params: Record<string, string> } | null {
|
|
90
|
+
for (const route of this.routes) {
|
|
91
|
+
if (route.method !== method) continue;
|
|
92
|
+
const match = path.match(route.pattern);
|
|
93
|
+
if (match) {
|
|
94
|
+
const params: Record<string, string> = {};
|
|
95
|
+
route.paramNames.forEach((name, index) => {
|
|
96
|
+
params[name] = match[index + 1];
|
|
97
|
+
});
|
|
98
|
+
return { route, params };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async start(port?: number): Promise<void> {
|
|
105
|
+
return new Promise((resolve, reject) => {
|
|
106
|
+
this.server = http.createServer((req, res) => {
|
|
107
|
+
this.handleRequest(req, res);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.server.on('error', reject);
|
|
111
|
+
|
|
112
|
+
this.server.listen(port ?? 0, () => {
|
|
113
|
+
const address = this.server!.address();
|
|
114
|
+
if (typeof address === 'object' && address !== null) {
|
|
115
|
+
this.port = address.port;
|
|
116
|
+
}
|
|
117
|
+
resolve();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async stop(): Promise<void> {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
if (!this.server) {
|
|
125
|
+
resolve();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.server.close((err) => {
|
|
130
|
+
if (err) {
|
|
131
|
+
reject(err);
|
|
132
|
+
} else {
|
|
133
|
+
this.server = null;
|
|
134
|
+
this.port = 0;
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
|
142
|
+
const url = new URL(req.url ?? '/', `http://localhost:${this.port}`);
|
|
143
|
+
const method = req.method ?? 'GET';
|
|
144
|
+
const path = url.pathname;
|
|
145
|
+
|
|
146
|
+
let body = '';
|
|
147
|
+
req.on('data', (chunk) => {
|
|
148
|
+
body += chunk;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
req.on('end', () => {
|
|
152
|
+
let parsedBody: unknown = null;
|
|
153
|
+
if (body) {
|
|
154
|
+
try {
|
|
155
|
+
parsedBody = JSON.parse(body);
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore parse errors
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const matched = this.matchRoute(method, path);
|
|
162
|
+
|
|
163
|
+
if (!matched) {
|
|
164
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
165
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { route, params } = matched;
|
|
170
|
+
const result = route.handler(req, parsedBody, params);
|
|
171
|
+
|
|
172
|
+
res.writeHead(result.status, { 'Content-Type': 'application/json' });
|
|
173
|
+
res.end(JSON.stringify(result.data));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|