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