mock-mcp 0.3.1 → 0.5.0
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/README.md +212 -124
- package/dist/adapter/index.cjs +712 -0
- package/dist/adapter/index.d.cts +55 -0
- package/dist/adapter/index.d.ts +55 -0
- package/dist/adapter/index.js +672 -0
- package/dist/client/connect.cjs +913 -0
- package/dist/client/connect.d.cts +211 -0
- package/dist/client/connect.d.ts +204 -7
- package/dist/client/connect.js +863 -20
- package/dist/client/index.cjs +914 -0
- package/dist/client/index.d.cts +4 -0
- package/dist/client/index.d.ts +4 -2
- package/dist/client/index.js +873 -2
- package/dist/daemon/index.cjs +667 -0
- package/dist/daemon/index.d.cts +62 -0
- package/dist/daemon/index.d.ts +62 -0
- package/dist/daemon/index.js +628 -0
- package/dist/discovery-Dc2LdF8q.d.cts +105 -0
- package/dist/discovery-Dc2LdF8q.d.ts +105 -0
- package/dist/index.cjs +2238 -0
- package/dist/index.d.cts +472 -0
- package/dist/index.d.ts +472 -11
- package/dist/index.js +2185 -53
- package/dist/protocol-CiwaQFOt.d.ts +239 -0
- package/dist/protocol-xZu-wb0n.d.cts +239 -0
- package/dist/shared/index.cjs +386 -0
- package/dist/shared/index.d.cts +4 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.js +310 -0
- package/dist/types-BKREdsyr.d.cts +32 -0
- package/dist/types-BKREdsyr.d.ts +32 -0
- package/package.json +44 -4
- package/dist/client/batch-mock-collector.d.ts +0 -111
- package/dist/client/batch-mock-collector.js +0 -308
- package/dist/client/util.d.ts +0 -1
- package/dist/client/util.js +0 -3
- package/dist/connect.cjs +0 -400
- package/dist/connect.d.cts +0 -82
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +0 -1
- package/dist/server/test-mock-mcp-server.d.ts +0 -73
- package/dist/server/test-mock-mcp-server.js +0 -419
- package/dist/types.d.ts +0 -45
- package/dist/types.js +0 -2
package/dist/client/index.js
CHANGED
|
@@ -1,2 +1,873 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import http from 'http';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
5
|
+
import WebSocket from 'ws';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import fssync from 'fs';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import { createRequire } from 'module';
|
|
11
|
+
|
|
12
|
+
// src/client/batch-mock-collector.ts
|
|
13
|
+
function debugLog(_msg) {
|
|
14
|
+
}
|
|
15
|
+
var __curDirname = (() => {
|
|
16
|
+
try {
|
|
17
|
+
const metaUrl = import.meta.url;
|
|
18
|
+
if (metaUrl && typeof metaUrl === "string" && metaUrl.startsWith("file://")) {
|
|
19
|
+
return path.dirname(fileURLToPath(metaUrl));
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
}
|
|
23
|
+
return process.cwd();
|
|
24
|
+
})();
|
|
25
|
+
function resolveProjectRoot(startDir = process.cwd()) {
|
|
26
|
+
let current = path.resolve(startDir);
|
|
27
|
+
const root = path.parse(current).root;
|
|
28
|
+
while (current !== root) {
|
|
29
|
+
const gitPath = path.join(current, ".git");
|
|
30
|
+
try {
|
|
31
|
+
const stat = fssync.statSync(gitPath);
|
|
32
|
+
if (stat.isDirectory() || stat.isFile()) {
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
} catch {
|
|
36
|
+
}
|
|
37
|
+
const pkgPath = path.join(current, "package.json");
|
|
38
|
+
try {
|
|
39
|
+
fssync.accessSync(pkgPath, fssync.constants.F_OK);
|
|
40
|
+
return current;
|
|
41
|
+
} catch {
|
|
42
|
+
}
|
|
43
|
+
current = path.dirname(current);
|
|
44
|
+
}
|
|
45
|
+
return path.resolve(startDir);
|
|
46
|
+
}
|
|
47
|
+
function computeProjectId(projectRoot) {
|
|
48
|
+
const real = fssync.realpathSync(projectRoot);
|
|
49
|
+
return crypto.createHash("sha256").update(real).digest("hex").slice(0, 16);
|
|
50
|
+
}
|
|
51
|
+
function getCacheDir(override) {
|
|
52
|
+
if (override) {
|
|
53
|
+
return override;
|
|
54
|
+
}
|
|
55
|
+
const envCacheDir = process.env.MOCK_MCP_CACHE_DIR;
|
|
56
|
+
if (envCacheDir) {
|
|
57
|
+
return envCacheDir;
|
|
58
|
+
}
|
|
59
|
+
const xdg = process.env.XDG_CACHE_HOME;
|
|
60
|
+
if (xdg) {
|
|
61
|
+
return xdg;
|
|
62
|
+
}
|
|
63
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
64
|
+
return process.env.LOCALAPPDATA;
|
|
65
|
+
}
|
|
66
|
+
const home = os.homedir();
|
|
67
|
+
if (home) {
|
|
68
|
+
return path.join(home, ".cache");
|
|
69
|
+
}
|
|
70
|
+
return os.tmpdir();
|
|
71
|
+
}
|
|
72
|
+
function getPaths(projectId, cacheDir) {
|
|
73
|
+
const base = path.join(getCacheDir(cacheDir), "mock-mcp");
|
|
74
|
+
const registryPath = path.join(base, `${projectId}.json`);
|
|
75
|
+
const lockPath = path.join(base, `${projectId}.lock`);
|
|
76
|
+
const ipcPath = process.platform === "win32" ? `\\\\.\\pipe\\mock-mcp-${projectId}` : path.join(base, `${projectId}.sock`);
|
|
77
|
+
return { base, registryPath, lockPath, ipcPath };
|
|
78
|
+
}
|
|
79
|
+
async function readRegistry(registryPath) {
|
|
80
|
+
try {
|
|
81
|
+
const txt = await fs.readFile(registryPath, "utf-8");
|
|
82
|
+
return JSON.parse(txt);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
debugLog(`readRegistry error for ${registryPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function healthCheck(ipcPath, timeoutMs = 2e3) {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const req = http.request(
|
|
91
|
+
{
|
|
92
|
+
method: "GET",
|
|
93
|
+
socketPath: ipcPath,
|
|
94
|
+
path: "/health",
|
|
95
|
+
timeout: timeoutMs
|
|
96
|
+
},
|
|
97
|
+
(res) => {
|
|
98
|
+
resolve(res.statusCode === 200);
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
req.on("error", () => resolve(false));
|
|
102
|
+
req.on("timeout", () => {
|
|
103
|
+
req.destroy();
|
|
104
|
+
resolve(false);
|
|
105
|
+
});
|
|
106
|
+
req.end();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function tryAcquireLock(lockPath) {
|
|
110
|
+
try {
|
|
111
|
+
const fh = await fs.open(lockPath, "wx");
|
|
112
|
+
await fh.write(`${process.pid}
|
|
113
|
+
`);
|
|
114
|
+
return fh;
|
|
115
|
+
} catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function releaseLock(lockPath, fh) {
|
|
120
|
+
await fh.close();
|
|
121
|
+
await fs.rm(lockPath).catch(() => {
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
function randomToken() {
|
|
125
|
+
return crypto.randomBytes(24).toString("base64url");
|
|
126
|
+
}
|
|
127
|
+
function getDaemonEntryPath() {
|
|
128
|
+
try {
|
|
129
|
+
const cwdRequire = createRequire(pathToFileURL(path.join(process.cwd(), "index.js")).href);
|
|
130
|
+
const resolved = cwdRequire.resolve("mock-mcp");
|
|
131
|
+
const distDir = path.dirname(resolved);
|
|
132
|
+
const daemonEntry = path.join(distDir, "index.js");
|
|
133
|
+
if (fssync.existsSync(daemonEntry)) {
|
|
134
|
+
return daemonEntry;
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const packageRoot = resolveProjectRoot(__curDirname);
|
|
140
|
+
const distPath = path.join(packageRoot, "dist", "index.js");
|
|
141
|
+
if (fssync.existsSync(distPath)) {
|
|
142
|
+
return distPath;
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
if (process.argv[1]) {
|
|
147
|
+
return process.argv[1];
|
|
148
|
+
}
|
|
149
|
+
return path.join(process.cwd(), "dist", "index.js");
|
|
150
|
+
}
|
|
151
|
+
async function ensureDaemonRunning(opts = {}) {
|
|
152
|
+
const projectRoot = opts.projectRoot ?? resolveProjectRoot();
|
|
153
|
+
const projectId = computeProjectId(projectRoot);
|
|
154
|
+
const { base, registryPath, lockPath, ipcPath } = getPaths(
|
|
155
|
+
projectId,
|
|
156
|
+
opts.cacheDir
|
|
157
|
+
);
|
|
158
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
159
|
+
await fs.mkdir(base, { recursive: true });
|
|
160
|
+
const existing = await readRegistry(registryPath);
|
|
161
|
+
debugLog(`Registry read result: ${existing ? "Found (PID " + existing.pid + ")" : "Null"}`);
|
|
162
|
+
if (existing) {
|
|
163
|
+
let healthy = false;
|
|
164
|
+
for (let i = 0; i < 3; i++) {
|
|
165
|
+
debugLog(`Checking health attempt ${i + 1}/3 on ${existing.ipcPath}`);
|
|
166
|
+
healthy = await healthCheck(existing.ipcPath);
|
|
167
|
+
if (healthy) break;
|
|
168
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
169
|
+
}
|
|
170
|
+
if (healthy) {
|
|
171
|
+
return existing;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (process.platform !== "win32") {
|
|
175
|
+
try {
|
|
176
|
+
await fs.rm(ipcPath);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const lock = await tryAcquireLock(lockPath);
|
|
181
|
+
if (lock) {
|
|
182
|
+
try {
|
|
183
|
+
const recheckReg = await readRegistry(registryPath);
|
|
184
|
+
if (recheckReg && await healthCheck(recheckReg.ipcPath)) {
|
|
185
|
+
return recheckReg;
|
|
186
|
+
}
|
|
187
|
+
const token = randomToken();
|
|
188
|
+
const daemonEntry = getDaemonEntryPath();
|
|
189
|
+
const child = spawn(
|
|
190
|
+
process.execPath,
|
|
191
|
+
[daemonEntry, "daemon", "--project-root", projectRoot, "--token", token],
|
|
192
|
+
{
|
|
193
|
+
detached: true,
|
|
194
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
195
|
+
env: {
|
|
196
|
+
...process.env,
|
|
197
|
+
MOCK_MCP_CACHE_DIR: opts.cacheDir ?? ""
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
);
|
|
201
|
+
let daemonStderr = "";
|
|
202
|
+
let daemonStdout = "";
|
|
203
|
+
child.stdout?.on("data", (data) => {
|
|
204
|
+
const str = data.toString();
|
|
205
|
+
debugLog(`Daemon stdout: ${str}`);
|
|
206
|
+
});
|
|
207
|
+
child.stderr?.on("data", (data) => {
|
|
208
|
+
daemonStderr += data.toString();
|
|
209
|
+
debugLog(`Daemon stderr: ${data.toString()}`);
|
|
210
|
+
});
|
|
211
|
+
child.on("error", (err) => {
|
|
212
|
+
console.error(`[mock-mcp] Daemon spawn error: ${err.message}`);
|
|
213
|
+
});
|
|
214
|
+
child.on("exit", (code, signal) => {
|
|
215
|
+
if (code !== null && code !== 0) {
|
|
216
|
+
console.error(`[mock-mcp] Daemon exited with code: ${code}`);
|
|
217
|
+
if (daemonStderr) {
|
|
218
|
+
console.error(`[mock-mcp] Daemon stderr: ${daemonStderr.slice(0, 500)}`);
|
|
219
|
+
}
|
|
220
|
+
} else if (signal) {
|
|
221
|
+
console.error(`[mock-mcp] Daemon killed by signal: ${signal}`);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
child.unref();
|
|
225
|
+
const deadline2 = Date.now() + timeoutMs;
|
|
226
|
+
while (Date.now() < deadline2) {
|
|
227
|
+
const reg = await readRegistry(registryPath);
|
|
228
|
+
if (reg && await healthCheck(reg.ipcPath)) {
|
|
229
|
+
return reg;
|
|
230
|
+
}
|
|
231
|
+
await sleep(50);
|
|
232
|
+
}
|
|
233
|
+
console.error("[mock-mcp] Daemon failed to start within timeout");
|
|
234
|
+
if (daemonStderr) {
|
|
235
|
+
console.error(`[mock-mcp] Daemon stderr:
|
|
236
|
+
${daemonStderr}`);
|
|
237
|
+
}
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Daemon start timeout after ${timeoutMs}ms. Check logs for details.`
|
|
240
|
+
);
|
|
241
|
+
} finally {
|
|
242
|
+
await releaseLock(lockPath, lock);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const deadline = Date.now() + timeoutMs;
|
|
246
|
+
while (Date.now() < deadline) {
|
|
247
|
+
const reg = await readRegistry(registryPath);
|
|
248
|
+
if (reg && await healthCheck(reg.ipcPath)) {
|
|
249
|
+
return reg;
|
|
250
|
+
}
|
|
251
|
+
await sleep(50);
|
|
252
|
+
}
|
|
253
|
+
throw new Error(
|
|
254
|
+
`Waiting for daemon timed out after ${timeoutMs}ms. Another process may have failed to start it.`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
function sleep(ms) {
|
|
258
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/shared/protocol.ts
|
|
262
|
+
var HELLO_TEST = "HELLO_TEST";
|
|
263
|
+
var HELLO_ACK = "HELLO_ACK";
|
|
264
|
+
var BATCH_MOCK_REQUEST = "BATCH_MOCK_REQUEST";
|
|
265
|
+
var BATCH_MOCK_RESULT = "BATCH_MOCK_RESULT";
|
|
266
|
+
|
|
267
|
+
// src/client/util.ts
|
|
268
|
+
var isEnabled = () => {
|
|
269
|
+
return process.env.MOCK_MCP !== void 0 && process.env.MOCK_MCP !== "0";
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// src/client/batch-mock-collector.ts
|
|
273
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
274
|
+
var DEFAULT_BATCH_DEBOUNCE_MS = 0;
|
|
275
|
+
var DEFAULT_MAX_BATCH_SIZE = 50;
|
|
276
|
+
var DEFAULT_HEARTBEAT_INTERVAL_MS = 15e3;
|
|
277
|
+
var BatchMockCollector = class {
|
|
278
|
+
ws;
|
|
279
|
+
registry;
|
|
280
|
+
runId = crypto.randomUUID();
|
|
281
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
282
|
+
queuedRequestIds = /* @__PURE__ */ new Set();
|
|
283
|
+
timeout;
|
|
284
|
+
batchDebounceMs;
|
|
285
|
+
maxBatchSize;
|
|
286
|
+
logger;
|
|
287
|
+
heartbeatIntervalMs;
|
|
288
|
+
enableReconnect;
|
|
289
|
+
projectRoot;
|
|
290
|
+
testMeta;
|
|
291
|
+
batchTimer = null;
|
|
292
|
+
heartbeatTimer = null;
|
|
293
|
+
reconnectTimer = null;
|
|
294
|
+
requestIdCounter = 0;
|
|
295
|
+
closed = false;
|
|
296
|
+
authed = false;
|
|
297
|
+
readyResolve;
|
|
298
|
+
readyReject;
|
|
299
|
+
readyPromise = Promise.resolve();
|
|
300
|
+
constructor(options = {}) {
|
|
301
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
302
|
+
this.batchDebounceMs = options.batchDebounceMs ?? DEFAULT_BATCH_DEBOUNCE_MS;
|
|
303
|
+
this.maxBatchSize = options.maxBatchSize ?? DEFAULT_MAX_BATCH_SIZE;
|
|
304
|
+
this.logger = options.logger ?? console;
|
|
305
|
+
this.heartbeatIntervalMs = options.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS;
|
|
306
|
+
this.enableReconnect = options.enableReconnect ?? true;
|
|
307
|
+
this.testMeta = options.testMeta;
|
|
308
|
+
this.projectRoot = this.resolveProjectRootFromOptions(options);
|
|
309
|
+
this.logger.log(`[mock-mcp] BatchMockCollector created`);
|
|
310
|
+
this.logger.log(`[mock-mcp] runId: ${this.runId}`);
|
|
311
|
+
this.logger.log(`[mock-mcp] timeout: ${this.timeout}ms`);
|
|
312
|
+
this.logger.log(`[mock-mcp] batchDebounceMs: ${this.batchDebounceMs}ms`);
|
|
313
|
+
this.logger.log(`[mock-mcp] maxBatchSize: ${this.maxBatchSize}`);
|
|
314
|
+
this.logger.log(`[mock-mcp] heartbeatIntervalMs: ${this.heartbeatIntervalMs}ms`);
|
|
315
|
+
this.logger.log(`[mock-mcp] enableReconnect: ${this.enableReconnect}`);
|
|
316
|
+
this.logger.log(`[mock-mcp] projectRoot: ${this.projectRoot ?? "(auto-detect)"}`);
|
|
317
|
+
if (options.filePath) {
|
|
318
|
+
this.logger.log(`[mock-mcp] filePath: ${options.filePath}`);
|
|
319
|
+
}
|
|
320
|
+
this.resetReadyPromise();
|
|
321
|
+
this.initConnection({
|
|
322
|
+
timeout: this.timeout
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Resolve projectRoot from options.
|
|
327
|
+
* Priority: projectRoot > filePath > undefined (auto-detect)
|
|
328
|
+
*/
|
|
329
|
+
resolveProjectRootFromOptions(options) {
|
|
330
|
+
if (options.projectRoot) {
|
|
331
|
+
return options.projectRoot;
|
|
332
|
+
}
|
|
333
|
+
if (options.filePath) {
|
|
334
|
+
let filePath = options.filePath;
|
|
335
|
+
if (filePath.startsWith("file://")) {
|
|
336
|
+
try {
|
|
337
|
+
filePath = fileURLToPath(filePath);
|
|
338
|
+
} catch {
|
|
339
|
+
filePath = filePath.replace(/^file:\/\//, "");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
const dir = path.dirname(filePath);
|
|
343
|
+
const resolved = resolveProjectRoot(dir);
|
|
344
|
+
this.logger.log(`[mock-mcp] Resolved projectRoot from filePath:`);
|
|
345
|
+
this.logger.log(`[mock-mcp] filePath: ${options.filePath}`);
|
|
346
|
+
this.logger.log(`[mock-mcp] dir: ${dir}`);
|
|
347
|
+
this.logger.log(`[mock-mcp] projectRoot: ${resolved}`);
|
|
348
|
+
return resolved;
|
|
349
|
+
}
|
|
350
|
+
return void 0;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* Ensures the underlying connection is ready for use.
|
|
354
|
+
*/
|
|
355
|
+
async waitUntilReady() {
|
|
356
|
+
return this.readyPromise;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Request mock data for a specific endpoint/method pair.
|
|
360
|
+
*/
|
|
361
|
+
async requestMock(endpoint, method, options = {}) {
|
|
362
|
+
if (this.closed) {
|
|
363
|
+
throw new Error("BatchMockCollector has been closed");
|
|
364
|
+
}
|
|
365
|
+
await this.waitUntilReady();
|
|
366
|
+
const requestId = `req-${++this.requestIdCounter}`;
|
|
367
|
+
const request = {
|
|
368
|
+
requestId,
|
|
369
|
+
endpoint,
|
|
370
|
+
method,
|
|
371
|
+
body: options.body,
|
|
372
|
+
headers: options.headers,
|
|
373
|
+
metadata: options.metadata
|
|
374
|
+
};
|
|
375
|
+
let settleCompletion;
|
|
376
|
+
const completion = new Promise((resolve) => {
|
|
377
|
+
settleCompletion = resolve;
|
|
378
|
+
});
|
|
379
|
+
return new Promise((resolve, reject) => {
|
|
380
|
+
const timeoutId = setTimeout(() => {
|
|
381
|
+
this.rejectRequest(
|
|
382
|
+
requestId,
|
|
383
|
+
new Error(`Mock request timed out after ${this.timeout}ms: ${method} ${endpoint}`)
|
|
384
|
+
);
|
|
385
|
+
}, this.timeout);
|
|
386
|
+
this.pendingRequests.set(requestId, {
|
|
387
|
+
request,
|
|
388
|
+
resolve: (mock) => {
|
|
389
|
+
settleCompletion({ status: "fulfilled", value: void 0 });
|
|
390
|
+
resolve(this.buildResolvedMock(mock));
|
|
391
|
+
},
|
|
392
|
+
reject: (error) => {
|
|
393
|
+
settleCompletion({ status: "rejected", reason: error });
|
|
394
|
+
reject(error);
|
|
395
|
+
},
|
|
396
|
+
timeoutId,
|
|
397
|
+
completion
|
|
398
|
+
});
|
|
399
|
+
this.enqueueRequest(requestId);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Wait for all currently pending requests to settle.
|
|
404
|
+
*/
|
|
405
|
+
async waitForPendingRequests() {
|
|
406
|
+
if (!isEnabled()) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const pendingCompletions = Array.from(this.pendingRequests.values()).map(
|
|
410
|
+
(p) => p.completion
|
|
411
|
+
);
|
|
412
|
+
const results = await Promise.all(pendingCompletions);
|
|
413
|
+
const rejected = results.find(
|
|
414
|
+
(r) => r.status === "rejected"
|
|
415
|
+
);
|
|
416
|
+
if (rejected) {
|
|
417
|
+
throw rejected.reason;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Close the connection and fail all pending requests.
|
|
422
|
+
*/
|
|
423
|
+
async close(code) {
|
|
424
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] close() called with code: ${code ?? "(default)"}`);
|
|
425
|
+
if (this.closed) {
|
|
426
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Already closed, returning`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.closed = true;
|
|
430
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Cleaning up timers...`);
|
|
431
|
+
if (this.batchTimer) {
|
|
432
|
+
clearTimeout(this.batchTimer);
|
|
433
|
+
this.batchTimer = null;
|
|
434
|
+
}
|
|
435
|
+
if (this.heartbeatTimer) {
|
|
436
|
+
clearInterval(this.heartbeatTimer);
|
|
437
|
+
this.heartbeatTimer = null;
|
|
438
|
+
}
|
|
439
|
+
if (this.reconnectTimer) {
|
|
440
|
+
clearTimeout(this.reconnectTimer);
|
|
441
|
+
this.reconnectTimer = null;
|
|
442
|
+
}
|
|
443
|
+
const pendingCount = this.pendingRequests.size;
|
|
444
|
+
const queuedCount = this.queuedRequestIds.size;
|
|
445
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Pending requests: ${pendingCount}, Queued: ${queuedCount}`);
|
|
446
|
+
this.queuedRequestIds.clear();
|
|
447
|
+
const closePromise = new Promise((resolve) => {
|
|
448
|
+
if (!this.ws) {
|
|
449
|
+
resolve();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.ws.once("close", () => resolve());
|
|
453
|
+
});
|
|
454
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Closing WebSocket...`);
|
|
455
|
+
this.ws?.close(code);
|
|
456
|
+
this.failAllPending(new Error("BatchMockCollector has been closed"));
|
|
457
|
+
await closePromise;
|
|
458
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Connection closed`);
|
|
459
|
+
}
|
|
460
|
+
// ===========================================================================
|
|
461
|
+
// Connection Management
|
|
462
|
+
// ===========================================================================
|
|
463
|
+
async initConnection({
|
|
464
|
+
timeout = 1e4
|
|
465
|
+
}) {
|
|
466
|
+
const initStartTime = Date.now();
|
|
467
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Initializing connection...`);
|
|
468
|
+
try {
|
|
469
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Ensuring daemon is running...`);
|
|
470
|
+
const daemonStartTime = Date.now();
|
|
471
|
+
this.registry = await ensureDaemonRunning({
|
|
472
|
+
projectRoot: this.projectRoot,
|
|
473
|
+
timeoutMs: timeout
|
|
474
|
+
});
|
|
475
|
+
const daemonElapsed = Date.now() - daemonStartTime;
|
|
476
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon ready (${daemonElapsed}ms)`);
|
|
477
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Project ID: ${this.registry.projectId}`);
|
|
478
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon PID: ${this.registry.pid}`);
|
|
479
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] IPC Path: ${this.registry.ipcPath}`);
|
|
480
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Creating WebSocket connection...`);
|
|
481
|
+
const wsStartTime = Date.now();
|
|
482
|
+
this.ws = await this.createWebSocket(this.registry.ipcPath);
|
|
483
|
+
const wsElapsed = Date.now() - wsStartTime;
|
|
484
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket created (${wsElapsed}ms)`);
|
|
485
|
+
this.setupWebSocket();
|
|
486
|
+
const totalElapsed = Date.now() - initStartTime;
|
|
487
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection initialized (total: ${totalElapsed}ms)`);
|
|
488
|
+
} catch (error) {
|
|
489
|
+
const elapsed = Date.now() - initStartTime;
|
|
490
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection init failed after ${elapsed}ms:`, error);
|
|
491
|
+
this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
createWebSocket(ipcPath) {
|
|
495
|
+
return new Promise((resolve, reject) => {
|
|
496
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Creating WebSocket to IPC: ${ipcPath}`);
|
|
497
|
+
const agent = new http.Agent({
|
|
498
|
+
// @ts-expect-error: Node.js supports socketPath for Unix sockets
|
|
499
|
+
socketPath: ipcPath
|
|
500
|
+
});
|
|
501
|
+
const ws = new WebSocket("ws://localhost/test", {
|
|
502
|
+
agent
|
|
503
|
+
});
|
|
504
|
+
ws.once("open", () => {
|
|
505
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket opened`);
|
|
506
|
+
resolve(ws);
|
|
507
|
+
});
|
|
508
|
+
ws.once("error", (err) => {
|
|
509
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket connection error:`, err);
|
|
510
|
+
reject(err);
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
setupWebSocket() {
|
|
515
|
+
if (!this.ws || !this.registry) {
|
|
516
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] setupWebSocket called but ws or registry is missing`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Setting up WebSocket event handlers...`);
|
|
520
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Current readyState: ${this.ws.readyState}`);
|
|
521
|
+
this.ws.on("message", (data) => this.handleMessage(data));
|
|
522
|
+
this.ws.on("error", (error) => {
|
|
523
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] \u274C WebSocket ERROR:`, error);
|
|
524
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] authed: ${this.authed}`);
|
|
525
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] readyState: ${this.ws?.readyState}`);
|
|
526
|
+
if (!this.authed) {
|
|
527
|
+
this.readyReject?.(error instanceof Error ? error : new Error(String(error)));
|
|
528
|
+
}
|
|
529
|
+
this.failAllPending(error instanceof Error ? error : new Error(String(error)));
|
|
530
|
+
});
|
|
531
|
+
this.ws.on("close", (code, reason) => {
|
|
532
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F50C} WebSocket CLOSE`);
|
|
533
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] code: ${code}`);
|
|
534
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] reason: ${reason?.toString() || "(none)"}`);
|
|
535
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] authed: ${this.authed}`);
|
|
536
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] closed: ${this.closed}`);
|
|
537
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] enableReconnect: ${this.enableReconnect}`);
|
|
538
|
+
this.authed = false;
|
|
539
|
+
this.stopHeartbeat();
|
|
540
|
+
this.failAllPending(new Error(`Daemon connection closed (code: ${code})`));
|
|
541
|
+
if (!this.closed && this.enableReconnect) {
|
|
542
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Will attempt reconnect...`);
|
|
543
|
+
this.scheduleReconnect();
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] WebSocket event handlers configured`);
|
|
547
|
+
if (this.ws.readyState === WebSocket.OPEN) {
|
|
548
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F50C} WebSocket already OPEN - sending HELLO`);
|
|
549
|
+
this.sendHello();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
sendHello() {
|
|
553
|
+
if (!this.ws || !this.registry) {
|
|
554
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] sendHello called but ws or registry is missing`);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Sending HELLO handshake...`);
|
|
558
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] runId: ${this.runId}`);
|
|
559
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] pid: ${process.pid}`);
|
|
560
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] cwd: ${process.cwd()}`);
|
|
561
|
+
if (this.testMeta) {
|
|
562
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] testFile: ${this.testMeta.testFile ?? "(none)"}`);
|
|
563
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] testName: ${this.testMeta.testName ?? "(none)"}`);
|
|
564
|
+
}
|
|
565
|
+
const hello = {
|
|
566
|
+
type: HELLO_TEST,
|
|
567
|
+
token: this.registry.token,
|
|
568
|
+
runId: this.runId,
|
|
569
|
+
pid: process.pid,
|
|
570
|
+
cwd: process.cwd(),
|
|
571
|
+
testMeta: this.testMeta
|
|
572
|
+
};
|
|
573
|
+
this.ws.send(JSON.stringify(hello));
|
|
574
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] HELLO sent, waiting for HELLO_ACK...`);
|
|
575
|
+
}
|
|
576
|
+
handleMessage(data) {
|
|
577
|
+
let msg;
|
|
578
|
+
try {
|
|
579
|
+
msg = JSON.parse(data.toString());
|
|
580
|
+
} catch {
|
|
581
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Failed to parse server message`);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const msgType = msg?.type;
|
|
585
|
+
if (this.isHelloAck(msg)) {
|
|
586
|
+
this.authed = true;
|
|
587
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Received HELLO_ACK - Authenticated!`);
|
|
588
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Connection is now READY`);
|
|
589
|
+
this.readyResolve?.();
|
|
590
|
+
this.startHeartbeat();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
if (this.isBatchMockResult(msg)) {
|
|
594
|
+
this.logger.log(
|
|
595
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F4E6} Received BATCH_MOCK_RESULT`
|
|
596
|
+
);
|
|
597
|
+
this.logger.log(
|
|
598
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] batchId: ${msg.batchId}`
|
|
599
|
+
);
|
|
600
|
+
this.logger.log(
|
|
601
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] mocks count: ${msg.mocks.length}`
|
|
602
|
+
);
|
|
603
|
+
for (const mock of msg.mocks) {
|
|
604
|
+
this.logger.log(
|
|
605
|
+
`[mock-mcp] [${this.runId.slice(0, 8)}] - ${mock.requestId}: status=${mock.status ?? 200}`
|
|
606
|
+
);
|
|
607
|
+
this.resolveRequest(mock);
|
|
608
|
+
}
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (msgType === "HEARTBEAT_ACK") {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] Received unknown message type: ${msgType}`);
|
|
615
|
+
}
|
|
616
|
+
isHelloAck(msg) {
|
|
617
|
+
return msg !== null && typeof msg === "object" && msg.type === HELLO_ACK;
|
|
618
|
+
}
|
|
619
|
+
isBatchMockResult(msg) {
|
|
620
|
+
return msg !== null && typeof msg === "object" && msg.type === BATCH_MOCK_RESULT && Array.isArray(msg.mocks);
|
|
621
|
+
}
|
|
622
|
+
// ===========================================================================
|
|
623
|
+
// Request Management
|
|
624
|
+
// ===========================================================================
|
|
625
|
+
resolveRequest(mock) {
|
|
626
|
+
const pending = this.pendingRequests.get(mock.requestId);
|
|
627
|
+
if (!pending) {
|
|
628
|
+
this.logger.warn(`Received mock for unknown request: ${mock.requestId}`);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
clearTimeout(pending.timeoutId);
|
|
632
|
+
this.pendingRequests.delete(mock.requestId);
|
|
633
|
+
const resolve = () => pending.resolve(mock);
|
|
634
|
+
if (mock.delayMs && mock.delayMs > 0) {
|
|
635
|
+
setTimeout(resolve, mock.delayMs);
|
|
636
|
+
} else {
|
|
637
|
+
resolve();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
rejectRequest(requestId, error) {
|
|
641
|
+
const pending = this.pendingRequests.get(requestId);
|
|
642
|
+
if (!pending) return;
|
|
643
|
+
clearTimeout(pending.timeoutId);
|
|
644
|
+
this.pendingRequests.delete(requestId);
|
|
645
|
+
pending.reject(error);
|
|
646
|
+
}
|
|
647
|
+
failAllPending(error) {
|
|
648
|
+
for (const requestId of Array.from(this.pendingRequests.keys())) {
|
|
649
|
+
this.rejectRequest(requestId, error);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// ===========================================================================
|
|
653
|
+
// Batching
|
|
654
|
+
// ===========================================================================
|
|
655
|
+
enqueueRequest(requestId) {
|
|
656
|
+
this.queuedRequestIds.add(requestId);
|
|
657
|
+
if (this.batchTimer) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
this.batchTimer = setTimeout(() => {
|
|
661
|
+
this.batchTimer = null;
|
|
662
|
+
this.flushQueue();
|
|
663
|
+
}, this.batchDebounceMs);
|
|
664
|
+
}
|
|
665
|
+
flushQueue() {
|
|
666
|
+
const queuedIds = Array.from(this.queuedRequestIds);
|
|
667
|
+
this.queuedRequestIds.clear();
|
|
668
|
+
if (queuedIds.length === 0) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
for (let i = 0; i < queuedIds.length; i += this.maxBatchSize) {
|
|
672
|
+
const chunkIds = queuedIds.slice(i, i + this.maxBatchSize);
|
|
673
|
+
const requests = [];
|
|
674
|
+
for (const id of chunkIds) {
|
|
675
|
+
const pending = this.pendingRequests.get(id);
|
|
676
|
+
if (pending) {
|
|
677
|
+
requests.push(pending.request);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (requests.length > 0) {
|
|
681
|
+
this.sendBatch(requests);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
sendBatch(requests) {
|
|
686
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
687
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] Cannot send batch - WebSocket not open`);
|
|
688
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] ws exists: ${!!this.ws}`);
|
|
689
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] readyState: ${this.ws?.readyState}`);
|
|
690
|
+
const error = new Error("WebSocket is not open");
|
|
691
|
+
for (const req of requests) {
|
|
692
|
+
this.rejectRequest(req.requestId, error);
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const payload = {
|
|
697
|
+
type: BATCH_MOCK_REQUEST,
|
|
698
|
+
runId: this.runId,
|
|
699
|
+
requests
|
|
700
|
+
};
|
|
701
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F4E4} Sending BATCH_MOCK_REQUEST`);
|
|
702
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] requests: ${requests.length}`);
|
|
703
|
+
for (const req of requests) {
|
|
704
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] - ${req.requestId}: ${req.method} ${req.endpoint}`);
|
|
705
|
+
}
|
|
706
|
+
this.ws.send(JSON.stringify(payload));
|
|
707
|
+
}
|
|
708
|
+
// ===========================================================================
|
|
709
|
+
// Heartbeat
|
|
710
|
+
// ===========================================================================
|
|
711
|
+
startHeartbeat() {
|
|
712
|
+
if (this.heartbeatIntervalMs <= 0 || this.heartbeatTimer) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
let lastPong = Date.now();
|
|
716
|
+
this.ws?.on("pong", () => {
|
|
717
|
+
lastPong = Date.now();
|
|
718
|
+
});
|
|
719
|
+
this.heartbeatTimer = setInterval(() => {
|
|
720
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const now = Date.now();
|
|
724
|
+
if (now - lastPong > this.heartbeatIntervalMs * 2) {
|
|
725
|
+
this.logger.warn("Heartbeat missed; closing socket to trigger reconnect...");
|
|
726
|
+
this.ws.close();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
this.ws.ping();
|
|
730
|
+
}, this.heartbeatIntervalMs);
|
|
731
|
+
this.heartbeatTimer.unref?.();
|
|
732
|
+
}
|
|
733
|
+
stopHeartbeat() {
|
|
734
|
+
if (this.heartbeatTimer) {
|
|
735
|
+
clearInterval(this.heartbeatTimer);
|
|
736
|
+
this.heartbeatTimer = null;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// ===========================================================================
|
|
740
|
+
// Reconnection
|
|
741
|
+
// ===========================================================================
|
|
742
|
+
scheduleReconnect() {
|
|
743
|
+
if (this.reconnectTimer) {
|
|
744
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Reconnect already scheduled, skipping`);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
if (this.closed) {
|
|
748
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Client is closed, not reconnecting`);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Scheduling reconnect in 1000ms...`);
|
|
752
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
753
|
+
this.reconnectTimer = null;
|
|
754
|
+
this.logger.warn(`[mock-mcp] [${this.runId.slice(0, 8)}] \u{1F504} Attempting reconnect to daemon...`);
|
|
755
|
+
this.stopHeartbeat();
|
|
756
|
+
this.resetReadyPromise();
|
|
757
|
+
this.authed = false;
|
|
758
|
+
const reconnectStartTime = Date.now();
|
|
759
|
+
try {
|
|
760
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Re-discovering daemon...`);
|
|
761
|
+
this.registry = await ensureDaemonRunning({
|
|
762
|
+
projectRoot: this.projectRoot
|
|
763
|
+
});
|
|
764
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Daemon found, creating new WebSocket...`);
|
|
765
|
+
this.ws = await this.createWebSocket(this.registry.ipcPath);
|
|
766
|
+
this.setupWebSocket();
|
|
767
|
+
const elapsed = Date.now() - reconnectStartTime;
|
|
768
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] \u2705 Reconnect successful (${elapsed}ms)`);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
const elapsed = Date.now() - reconnectStartTime;
|
|
771
|
+
this.logger.error(`[mock-mcp] [${this.runId.slice(0, 8)}] \u274C Reconnection failed after ${elapsed}ms:`, error);
|
|
772
|
+
this.logger.log(`[mock-mcp] [${this.runId.slice(0, 8)}] Will retry reconnect...`);
|
|
773
|
+
this.scheduleReconnect();
|
|
774
|
+
}
|
|
775
|
+
}, 1e3);
|
|
776
|
+
this.reconnectTimer.unref?.();
|
|
777
|
+
}
|
|
778
|
+
// ===========================================================================
|
|
779
|
+
// Utilities
|
|
780
|
+
// ===========================================================================
|
|
781
|
+
resetReadyPromise() {
|
|
782
|
+
this.readyPromise = new Promise((resolve, reject) => {
|
|
783
|
+
this.readyResolve = resolve;
|
|
784
|
+
this.readyReject = reject;
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
buildResolvedMock(mock) {
|
|
788
|
+
return {
|
|
789
|
+
requestId: mock.requestId,
|
|
790
|
+
data: mock.data,
|
|
791
|
+
status: mock.status,
|
|
792
|
+
headers: mock.headers,
|
|
793
|
+
delayMs: mock.delayMs
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
// ===========================================================================
|
|
797
|
+
// Public Getters
|
|
798
|
+
// ===========================================================================
|
|
799
|
+
/**
|
|
800
|
+
* Get the run ID for this collector instance.
|
|
801
|
+
*/
|
|
802
|
+
getRunId() {
|
|
803
|
+
return this.runId;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get the daemon registry information (after connection).
|
|
807
|
+
*/
|
|
808
|
+
getRegistry() {
|
|
809
|
+
return this.registry;
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// src/client/connect.ts
|
|
814
|
+
var DisabledMockClient = class {
|
|
815
|
+
runId = "disabled";
|
|
816
|
+
async waitUntilReady() {
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
async requestMock() {
|
|
820
|
+
throw new Error(
|
|
821
|
+
"[mock-mcp] MOCK_MCP is not enabled. Set MOCK_MCP=1 to enable mock generation."
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
async waitForPendingRequests() {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
async close() {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
getRunId() {
|
|
831
|
+
return this.runId;
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
var connect = async (options) => {
|
|
835
|
+
const logger = options?.logger ?? console;
|
|
836
|
+
const startTime = Date.now();
|
|
837
|
+
logger.log("[mock-mcp] connect() called");
|
|
838
|
+
logger.log(`[mock-mcp] PID: ${process.pid}`);
|
|
839
|
+
logger.log(`[mock-mcp] CWD: ${process.cwd()}`);
|
|
840
|
+
logger.log(`[mock-mcp] MOCK_MCP env: ${process.env.MOCK_MCP ?? "(not set)"}`);
|
|
841
|
+
if (!isEnabled()) {
|
|
842
|
+
logger.log("[mock-mcp] Skipping (set MOCK_MCP=1 to enable)");
|
|
843
|
+
return new DisabledMockClient();
|
|
844
|
+
}
|
|
845
|
+
logger.log("[mock-mcp] Creating BatchMockCollector...");
|
|
846
|
+
const collector = new BatchMockCollector(options ?? {});
|
|
847
|
+
const runId = collector.getRunId();
|
|
848
|
+
logger.log(`[mock-mcp] Run ID: ${runId}`);
|
|
849
|
+
logger.log("[mock-mcp] Waiting for connection to be ready...");
|
|
850
|
+
try {
|
|
851
|
+
await collector.waitUntilReady();
|
|
852
|
+
const elapsed = Date.now() - startTime;
|
|
853
|
+
const registry = collector.getRegistry();
|
|
854
|
+
logger.log("[mock-mcp] ========== Connection Established ==========");
|
|
855
|
+
logger.log(`[mock-mcp] Run ID: ${runId}`);
|
|
856
|
+
logger.log(`[mock-mcp] Daemon PID: ${registry?.pid ?? "unknown"}`);
|
|
857
|
+
logger.log(`[mock-mcp] Project ID: ${registry?.projectId ?? "unknown"}`);
|
|
858
|
+
logger.log(`[mock-mcp] IPC Path: ${registry?.ipcPath ?? "unknown"}`);
|
|
859
|
+
logger.log(`[mock-mcp] Connection time: ${elapsed}ms`);
|
|
860
|
+
logger.log("[mock-mcp] ==============================================");
|
|
861
|
+
return collector;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
const elapsed = Date.now() - startTime;
|
|
864
|
+
logger.error("[mock-mcp] ========== Connection Failed ==========");
|
|
865
|
+
logger.error(`[mock-mcp] Run ID: ${runId}`);
|
|
866
|
+
logger.error(`[mock-mcp] Error: ${error instanceof Error ? error.message : String(error)}`);
|
|
867
|
+
logger.error(`[mock-mcp] Elapsed time: ${elapsed}ms`);
|
|
868
|
+
logger.error("[mock-mcp] =========================================");
|
|
869
|
+
throw error;
|
|
870
|
+
}
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
export { BatchMockCollector, connect };
|