@forwardimpact/libmock 0.1.2

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/src/mock/fs.js ADDED
@@ -0,0 +1,301 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ import { spy } from "./spy.js";
3
+ /**
4
+ * Creates a mock filesystem backed by an in-memory Map
5
+ * @param {Object<string, string>} files - Initial file contents keyed by path
6
+ * @returns {object} Mock fs with readFile, writeFile, readdir, stat, mkdir, access, copyFile
7
+ */
8
+ export function createMockFs(files = {}) {
9
+ const data = new Map(Object.entries(files));
10
+ const dirs = new Set();
11
+ let nextFd = 3;
12
+ const openFds = new Map();
13
+
14
+ // Auto-create parent directories for initial files
15
+ for (const path of data.keys()) {
16
+ const parts = path.split("/");
17
+ for (let i = 1; i < parts.length; i++) {
18
+ dirs.add(parts.slice(0, i).join("/"));
19
+ }
20
+ }
21
+
22
+ return {
23
+ data,
24
+ dirs,
25
+ readFile: spy(async (path, encoding) => {
26
+ const content = data.get(path);
27
+ if (content === undefined) {
28
+ const err = new Error(
29
+ `ENOENT: no such file or directory, open '${path}'`,
30
+ );
31
+ err.code = "ENOENT";
32
+ throw err;
33
+ }
34
+ return encoding ? content : Buffer.from(content);
35
+ }),
36
+ writeFile: spy(async (path, content) => {
37
+ data.set(
38
+ path,
39
+ typeof content === "string" ? content : content.toString(),
40
+ );
41
+ }),
42
+ appendFile: spy(async (path, content) => {
43
+ const chunk = typeof content === "string" ? content : content.toString();
44
+ data.set(path, (data.get(path) ?? "") + chunk);
45
+ }),
46
+ rename: spy(async (src, dest) => {
47
+ if (!data.has(src)) {
48
+ const err = new Error(
49
+ `ENOENT: no such file or directory, rename '${src}' -> '${dest}'`,
50
+ );
51
+ err.code = "ENOENT";
52
+ throw err;
53
+ }
54
+ data.set(dest, data.get(src));
55
+ data.delete(src);
56
+ }),
57
+ readdir: spy(async (path, opts = {}) => {
58
+ // Collect immediate children; an entry is a directory if anything lives
59
+ // below it (a deeper key) or it was registered via mkdir/cp.
60
+ const prefix = path.endsWith("/") ? path : `${path}/`;
61
+ const isDir = new Map();
62
+ for (const key of [...data.keys(), ...dirs]) {
63
+ if (!key.startsWith(prefix)) continue;
64
+ const rest = key.slice(prefix.length);
65
+ const name = rest.split("/")[0];
66
+ if (!name) continue;
67
+ const dir = rest.includes("/") || dirs.has(`${prefix}${name}`);
68
+ isDir.set(name, isDir.get(name) || dir);
69
+ }
70
+ const names = [...isDir.keys()];
71
+ if (!opts.withFileTypes) return names;
72
+ // Mirror node:fs Dirent: name + isDirectory()/isFile()/isSymbolicLink().
73
+ return names.map((name) => ({
74
+ name,
75
+ isDirectory: () => isDir.get(name),
76
+ isFile: () => !isDir.get(name),
77
+ isSymbolicLink: () => false,
78
+ }));
79
+ }),
80
+ stat: spy(async (path) => {
81
+ if (data.has(path)) {
82
+ return { isFile: () => true, isDirectory: () => false };
83
+ }
84
+ if (dirs.has(path)) {
85
+ return { isFile: () => false, isDirectory: () => true };
86
+ }
87
+ const err = new Error(
88
+ `ENOENT: no such file or directory, stat '${path}'`,
89
+ );
90
+ err.code = "ENOENT";
91
+ throw err;
92
+ }),
93
+ mkdir: spy(async (path) => {
94
+ dirs.add(path);
95
+ }),
96
+ mkdtemp: spy(async (prefix) => {
97
+ // Mirror node:fs/promises.mkdtemp: append 6 random chars to the prefix
98
+ // and register the directory. Returns the created path.
99
+ const path = `${prefix}${Math.random().toString(36).slice(2, 8)}`;
100
+ dirs.add(path);
101
+ return path;
102
+ }),
103
+ rm: spy(async (path, opts = {}) => {
104
+ // Mirror fs.rm(path, { recursive }): with `recursive`, drop the entry
105
+ // and every descendant so subsequent readdir/access don't see ghosts.
106
+ const prefix = path.endsWith("/") ? path : `${path}/`;
107
+ const matches = (k) =>
108
+ k === path || (opts.recursive && k.startsWith(prefix));
109
+ for (const k of [...data.keys()]) if (matches(k)) data.delete(k);
110
+ for (const k of [...dirs]) if (matches(k)) dirs.delete(k);
111
+ }),
112
+ lstat: spy(async (path) => {
113
+ if (data.has(path)) {
114
+ return {
115
+ isFile: () => true,
116
+ isDirectory: () => false,
117
+ isSymbolicLink: () => false,
118
+ };
119
+ }
120
+ if (dirs.has(path)) {
121
+ return {
122
+ isFile: () => false,
123
+ isDirectory: () => true,
124
+ isSymbolicLink: () => false,
125
+ };
126
+ }
127
+ const err = new Error(
128
+ `ENOENT: no such file or directory, lstat '${path}'`,
129
+ );
130
+ err.code = "ENOENT";
131
+ throw err;
132
+ }),
133
+ unlink: spy(async (path) => {
134
+ data.delete(path);
135
+ }),
136
+ symlink: spy(async (_target, path) => {
137
+ dirs.add(path);
138
+ }),
139
+ access: spy(async (path) => {
140
+ if (!data.has(path) && !dirs.has(path)) {
141
+ const err = new Error(
142
+ `ENOENT: no such file or directory, access '${path}'`,
143
+ );
144
+ err.code = "ENOENT";
145
+ throw err;
146
+ }
147
+ }),
148
+ copyFile: spy(async (src, dest) => {
149
+ const content = data.get(src);
150
+ if (content === undefined) {
151
+ const err = new Error(
152
+ `ENOENT: no such file or directory, copyFile '${src}'`,
153
+ );
154
+ err.code = "ENOENT";
155
+ throw err;
156
+ }
157
+ data.set(dest, content);
158
+ }),
159
+ cp: spy(async (src, dest) => {
160
+ // Mirror fs.cp(src, dest, { recursive }): copy a file, or a directory
161
+ // subtree (every entry under `src/` re-rooted under `dest/`).
162
+ if (data.has(src)) {
163
+ data.set(dest, data.get(src));
164
+ return;
165
+ }
166
+ const prefix = src.endsWith("/") ? src : `${src}/`;
167
+ const destPrefix = dest.endsWith("/") ? dest : `${dest}/`;
168
+ const reroot = (k) => destPrefix + k.slice(prefix.length);
169
+ const under = (k) => k.startsWith(prefix);
170
+ dirs.add(dest);
171
+ for (const key of [...data.keys()].filter(under)) {
172
+ data.set(reroot(key), data.get(key));
173
+ }
174
+ for (const dir of [...dirs].filter(under)) dirs.add(reroot(dir));
175
+ }),
176
+ // Timestamps and permissions are not modeled by the in-memory store; these
177
+ // record the call (via spy) so consumers stay testable and assertable.
178
+ utimes: spy(async () => {}),
179
+ chmod: spy(async () => {}),
180
+ existsSync: spy((path) => data.has(path) || dirs.has(path)),
181
+ readFileSync: spy((path, encoding) => {
182
+ const content = data.get(path);
183
+ if (content === undefined) {
184
+ const err = new Error(
185
+ `ENOENT: no such file or directory, open '${path}'`,
186
+ );
187
+ err.code = "ENOENT";
188
+ throw err;
189
+ }
190
+ return encoding ? content : Buffer.from(content);
191
+ }),
192
+ writeFileSync: spy((path, content) => {
193
+ data.set(
194
+ path,
195
+ typeof content === "string" ? content : content.toString(),
196
+ );
197
+ }),
198
+ mkdirSync: spy((path) => {
199
+ dirs.add(path);
200
+ }),
201
+ statSync: spy((path) => {
202
+ const content = data.get(path);
203
+ if (content !== undefined)
204
+ return {
205
+ size: Buffer.byteLength(content),
206
+ mtimeMs: 0,
207
+ isFile: () => true,
208
+ isDirectory: () => false,
209
+ };
210
+ if (dirs.has(path))
211
+ return {
212
+ size: 0,
213
+ mtimeMs: 0,
214
+ isFile: () => false,
215
+ isDirectory: () => true,
216
+ };
217
+ const err = new Error(
218
+ `ENOENT: no such file or directory, stat '${path}'`,
219
+ );
220
+ err.code = "ENOENT";
221
+ throw err;
222
+ }),
223
+ readdirSync: spy((dir) => {
224
+ const prefix = dir.endsWith("/") ? dir : `${dir}/`;
225
+ const names = new Set();
226
+ for (const key of [...data.keys(), ...dirs]) {
227
+ if (!key.startsWith(prefix)) continue;
228
+ const name = key.slice(prefix.length).split("/")[0];
229
+ if (name) names.add(name);
230
+ }
231
+ return [...names];
232
+ }),
233
+ // Permissions are not modeled by the in-memory store; record the call.
234
+ chmodSync: spy(() => {}),
235
+ openSync: spy((path, flags = "r") => {
236
+ // For write/append flags, create/truncate; for read flags, require the
237
+ // file to exist (mirror node:fs.openSync ENOENT). Hand back a synthetic
238
+ // descriptor that records its backing path.
239
+ const reading = typeof flags === "string" && flags.startsWith("r");
240
+ if (reading && !data.has(path)) {
241
+ const err = new Error(
242
+ `ENOENT: no such file or directory, open '${path}'`,
243
+ );
244
+ err.code = "ENOENT";
245
+ throw err;
246
+ }
247
+ if (!reading) data.set(path, "");
248
+ const fd = nextFd++;
249
+ openFds.set(fd, path);
250
+ return fd;
251
+ }),
252
+ readSync: spy((fd, buffer, offset = 0, length, position = 0) => {
253
+ // Mirror fs.readSync(fd, buffer, offset, length, position): copy bytes
254
+ // from the file's stored content into `buffer`, returning the byte count.
255
+ const path = openFds.get(fd);
256
+ if (path === undefined) {
257
+ const err = new Error("EBADF: bad file descriptor, read");
258
+ err.code = "EBADF";
259
+ throw err;
260
+ }
261
+ const content = Buffer.from(data.get(path) ?? "");
262
+ const start = position ?? 0;
263
+ const want = length ?? buffer.length - offset;
264
+ const slice = content.subarray(start, start + want);
265
+ slice.copy(buffer, offset);
266
+ return slice.length;
267
+ }),
268
+ closeSync: spy((fd) => {
269
+ openFds.delete(fd);
270
+ }),
271
+ unlinkSync: spy((path) => {
272
+ data.delete(path);
273
+ }),
274
+ createReadStream: spy((path) => {
275
+ // Stream the stored content (or error asynchronously, like node:fs).
276
+ const content = data.get(path);
277
+ if (content === undefined) {
278
+ const err = new Error(
279
+ `ENOENT: no such file or directory, open '${path}'`,
280
+ );
281
+ err.code = "ENOENT";
282
+ const stream = new Readable({ read() {} });
283
+ queueMicrotask(() => stream.emit("error", err));
284
+ return stream;
285
+ }
286
+ return Readable.from([Buffer.from(content)]);
287
+ }),
288
+ createWriteStream: spy((path, opts = {}) => {
289
+ // Accumulate writes into the in-memory store. `flags: "a"` appends.
290
+ const append = opts.flags === "a";
291
+ if (!append || !data.has(path)) data.set(path, "");
292
+ const stream = new Writable({
293
+ write(chunk, _enc, cb) {
294
+ data.set(path, (data.get(path) ?? "") + chunk.toString());
295
+ cb();
296
+ },
297
+ });
298
+ return stream;
299
+ }),
300
+ };
301
+ }
@@ -0,0 +1,28 @@
1
+ import { spy } from "./spy.js";
2
+
3
+ const GH_METHODS = ["prCreate", "prMerge", "apiGet", "apiPost"];
4
+
5
+ /**
6
+ * Creates a mock `GhClient` collaborator. Each method is a spy returning the
7
+ * configured `responses[method]` value (or a no-op default). Invocations are
8
+ * recorded on `calls`.
9
+ *
10
+ * @param {object} [options]
11
+ * @param {Record<string, unknown>} [options.responses] - Per-method returns.
12
+ * @returns {object} The mock gh client.
13
+ */
14
+ export function createMockGhClient({ responses = {} } = {}) {
15
+ const calls = [];
16
+ const client = { calls };
17
+
18
+ for (const method of GH_METHODS) {
19
+ client[method] = spy(async (...args) => {
20
+ calls.push({ method, args });
21
+ if (method in responses) return responses[method];
22
+ if (method === "prCreate") return "";
23
+ return null;
24
+ });
25
+ }
26
+
27
+ return client;
28
+ }
@@ -0,0 +1,56 @@
1
+ import { spy } from "./spy.js";
2
+
3
+ const GIT_METHODS = [
4
+ "clone",
5
+ "init",
6
+ "fetch",
7
+ "status",
8
+ "rebase",
9
+ "rebaseAbort",
10
+ "mergeOursStrategy",
11
+ "commitAll",
12
+ "push",
13
+ "revListCount",
14
+ "configGet",
15
+ "configSet",
16
+ "aheadCount",
17
+ "remoteGetUrl",
18
+ ];
19
+
20
+ /**
21
+ * Creates a mock `GitClient` collaborator. Every method on the real
22
+ * `GitClient` surface is a spy returning a no-op success by default, or the
23
+ * configured `responses[method]` value. `withAuth(token)` returns a client
24
+ * sharing the same `calls` log. Invocations are recorded on `calls`.
25
+ *
26
+ * @param {object} [options]
27
+ * @param {Record<string, unknown>} [options.responses] - Per-method returns.
28
+ * @returns {object} The mock git client.
29
+ */
30
+ export function createMockGitClient({ responses = {} } = {}) {
31
+ const calls = [];
32
+ const client = { calls };
33
+
34
+ for (const method of GIT_METHODS) {
35
+ client[method] = spy(async (...args) => {
36
+ calls.push({ method, args });
37
+ if (method in responses) return responses[method];
38
+ if (method === "revListCount" || method === "aheadCount") return 0;
39
+ if (
40
+ method === "status" ||
41
+ method === "configGet" ||
42
+ method === "remoteGetUrl"
43
+ ) {
44
+ return "";
45
+ }
46
+ return { stdout: "", stderr: "", exitCode: 0 };
47
+ });
48
+ }
49
+
50
+ client.withAuth = spy((token) => {
51
+ calls.push({ method: "withAuth", args: [token] });
52
+ return client;
53
+ });
54
+
55
+ return client;
56
+ }
@@ -0,0 +1,94 @@
1
+ import { spy } from "./spy.js";
2
+ /**
3
+ * Creates a mock gRPC factory function
4
+ * @param {object} overrides - Method overrides
5
+ * @returns {Function} Mock gRPC factory
6
+ */
7
+ export function createMockGrpcFn(overrides = {}) {
8
+ const mockGrpc = {
9
+ Server: function () {
10
+ return {
11
+ addService: spy(),
12
+ bindAsync: spy((uri, creds, callback) => callback(null, 5000)),
13
+ tryShutdown: spy((callback) => callback()),
14
+ ...overrides.server,
15
+ };
16
+ },
17
+ loadPackageDefinition: spy(() => ({
18
+ test: { Test: { service: {} } },
19
+ })),
20
+ makeGenericClientConstructor: spy(() => function () {}),
21
+ ServerCredentials: {
22
+ createInsecure: spy(),
23
+ },
24
+ credentials: {
25
+ createInsecure: spy(),
26
+ },
27
+ status: {
28
+ OK: 0,
29
+ CANCELLED: 1,
30
+ UNKNOWN: 2,
31
+ INVALID_ARGUMENT: 3,
32
+ DEADLINE_EXCEEDED: 4,
33
+ NOT_FOUND: 5,
34
+ ALREADY_EXISTS: 6,
35
+ PERMISSION_DENIED: 7,
36
+ RESOURCE_EXHAUSTED: 8,
37
+ FAILED_PRECONDITION: 9,
38
+ ABORTED: 10,
39
+ OUT_OF_RANGE: 11,
40
+ UNIMPLEMENTED: 12,
41
+ INTERNAL: 13,
42
+ UNAVAILABLE: 14,
43
+ DATA_LOSS: 15,
44
+ UNAUTHENTICATED: 16,
45
+ },
46
+ ...overrides.grpc,
47
+ };
48
+
49
+ const mockProtoLoader = {
50
+ loadSync: spy(() => ({})),
51
+ ...overrides.protoLoader,
52
+ };
53
+
54
+ return () => ({ grpc: mockGrpc, protoLoader: mockProtoLoader });
55
+ }
56
+
57
+ /**
58
+ * Creates a mock gRPC Metadata class
59
+ */
60
+ export class MockMetadata {
61
+ /**
62
+ * Creates a new MockMetadata instance
63
+ */
64
+ constructor() {
65
+ this.data = new Map();
66
+ }
67
+
68
+ /**
69
+ * Sets a metadata key-value pair
70
+ * @param {string} key - The metadata key
71
+ * @param {string} value - The metadata value
72
+ */
73
+ set(key, value) {
74
+ this.data.set(key, value);
75
+ }
76
+
77
+ /**
78
+ * Gets metadata value by key
79
+ * @param {string} key - The metadata key
80
+ * @returns {string[]} Array containing the value, or empty array if not found
81
+ */
82
+ get(key) {
83
+ const value = this.data.get(key);
84
+ return value !== undefined ? [value] : [];
85
+ }
86
+
87
+ /**
88
+ * Returns all metadata as a plain object
89
+ * @returns {object} Object with all metadata key-value pairs
90
+ */
91
+ getMap() {
92
+ return Object.fromEntries(this.data);
93
+ }
94
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Creates a mock HTTP request with event emitter behavior
3
+ * @param {object} options - Request options
4
+ * @returns {object} Mock request
5
+ */
6
+ export function createMockRequest(options = {}) {
7
+ const { method = "GET", url = "/", body = {}, headers = {} } = options;
8
+
9
+ const bodyStr = JSON.stringify(body);
10
+ let dataCallback;
11
+ let endCallback;
12
+ let errorCallback;
13
+
14
+ return {
15
+ method,
16
+ url,
17
+ headers,
18
+ on(event, callback) {
19
+ if (event === "data") dataCallback = callback;
20
+ if (event === "end") endCallback = callback;
21
+ if (event === "error") errorCallback = callback;
22
+ },
23
+ simulateBody() {
24
+ if (dataCallback) dataCallback(bodyStr);
25
+ if (endCallback) endCallback();
26
+ },
27
+ simulateError(err) {
28
+ if (errorCallback) errorCallback(err);
29
+ },
30
+ };
31
+ }
32
+
33
+ /**
34
+ * Creates a mock HTTP response with tracking
35
+ * @returns {object} Mock response
36
+ */
37
+ export function createMockResponse() {
38
+ const response = {
39
+ headersSent: false,
40
+ statusCode: null,
41
+ headers: {},
42
+ body: "",
43
+ chunks: [],
44
+ writeHead(status, headers) {
45
+ response.statusCode = status;
46
+ response.headers = headers || {};
47
+ response.headersSent = true;
48
+ },
49
+ write(data) {
50
+ response.chunks.push(data);
51
+ },
52
+ end(data) {
53
+ response.body = data || "";
54
+ },
55
+ getBody() {
56
+ return response.chunks.join("") + response.body;
57
+ },
58
+ };
59
+ return response;
60
+ }
@@ -0,0 +1,49 @@
1
+ export {
2
+ createMockConfig,
3
+ createMockServiceConfig,
4
+ createMockExtensionConfig,
5
+ } from "./config.js";
6
+ export { createMockStorage, MockStorage } from "./storage.js";
7
+ export { createMockLogger, createSilentLogger } from "./logger.js";
8
+ export { createMockGrpcFn, MockMetadata } from "./grpc.js";
9
+ export { createMockRequest, createMockResponse } from "./http.js";
10
+ export {
11
+ createMockObserverFn,
12
+ createMockTracer,
13
+ createMockAuthFn,
14
+ } from "./observer.js";
15
+
16
+ export { createMockResourceIndex } from "./resource-index.js";
17
+ export {
18
+ createMockMemoryClient,
19
+ createMockLlmClient,
20
+ createMockAgentClient,
21
+ createMockTraceClient,
22
+ createMockVectorClient,
23
+ createMockGraphClient,
24
+ createMockToolClient,
25
+ createMockDiscussionClient,
26
+ createStatefulDiscussionClient,
27
+ } from "./clients.js";
28
+ export { createMockServiceCallbacks } from "./service-callbacks.js";
29
+ export { createMockFs } from "./fs.js";
30
+ export { createMockClock } from "./clock.js";
31
+ export { createMockSubprocess } from "./subprocess.js";
32
+ export { createMockFinder } from "./finder.js";
33
+ export { createMockGitClient } from "./git-client.js";
34
+ export { createMockGhClient } from "./gh-client.js";
35
+ export { spy } from "./spy.js";
36
+ export {
37
+ createMockSupabaseClient,
38
+ createTurtleHelpers,
39
+ createMockProcess,
40
+ createMockStdin,
41
+ withSilentConsole,
42
+ createMockS3Client,
43
+ createMockQueries,
44
+ } from "./infra.js";
45
+ export {
46
+ createGraphIndexFixture,
47
+ createMockGrpcHealthDefinition,
48
+ createReplEnvironment,
49
+ } from "./environments.js";