@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.
@@ -0,0 +1,264 @@
1
+ import { spy } from "./spy.js";
2
+ import { common } from "@forwardimpact/libtype";
3
+ import grpc from "@grpc/grpc-js";
4
+
5
+ /**
6
+ * Creates a mock memory client
7
+ * @param {object} overrides - Method overrides
8
+ * @returns {object} Mock memory client
9
+ */
10
+ export function createMockMemoryClient(overrides = {}) {
11
+ return {
12
+ GetWindow: spy(() =>
13
+ Promise.resolve({
14
+ messages: [{ role: "system", content: "You are an assistant" }],
15
+ tools: [],
16
+ }),
17
+ ),
18
+ AppendMemory: spy(() => Promise.resolve({ accepted: "test-id" })),
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Creates a mock LLM client
25
+ * @param {object} overrides - Method overrides
26
+ * @returns {object} Mock LLM client
27
+ */
28
+ export function createMockLlmClient(overrides = {}) {
29
+ return {
30
+ CreateCompletions: spy(() =>
31
+ Promise.resolve({
32
+ id: "test-completion",
33
+ choices: [
34
+ {
35
+ message: common.Message.fromObject({
36
+ role: "assistant",
37
+ content: "Test response",
38
+ }),
39
+ },
40
+ ],
41
+ usage: { total_tokens: 100 },
42
+ }),
43
+ ),
44
+ CreateEmbeddings: spy(() =>
45
+ Promise.resolve({
46
+ data: [{ index: 0, embedding: [0.1, 0.2, 0.3] }],
47
+ }),
48
+ ),
49
+ ...overrides,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Creates a mock agent client
55
+ * @param {object} overrides - Method overrides
56
+ * @returns {object} Mock agent client
57
+ */
58
+ export function createMockAgentClient(overrides = {}) {
59
+ return {
60
+ ProcessUnary: spy(() =>
61
+ Promise.resolve({
62
+ resource_id: "test-conversation",
63
+ choices: [
64
+ {
65
+ message: common.Message.fromObject({
66
+ role: "assistant",
67
+ content: "Test response",
68
+ }),
69
+ },
70
+ ],
71
+ }),
72
+ ),
73
+ ProcessStream: spy(),
74
+ ...overrides,
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Creates a mock trace client
80
+ * @param {object} overrides - Method overrides
81
+ * @returns {object} Mock trace client
82
+ */
83
+ export function createMockTraceClient(overrides = {}) {
84
+ return {
85
+ RecordSpan: spy(() => Promise.resolve()),
86
+ ...overrides,
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Creates a mock vector client
92
+ * @param {object} overrides - Method overrides
93
+ * @returns {object} Mock vector client
94
+ */
95
+ export function createMockVectorClient(overrides = {}) {
96
+ return {
97
+ SearchContent: spy(() =>
98
+ Promise.resolve({
99
+ identifiers: [],
100
+ }),
101
+ ),
102
+ ...overrides,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Creates a mock graph client
108
+ * @param {object} overrides - Method overrides
109
+ * @returns {object} Mock graph client
110
+ */
111
+ export function createMockGraphClient(overrides = {}) {
112
+ return {
113
+ QueryByPattern: spy(() =>
114
+ Promise.resolve({
115
+ identifiers: [],
116
+ }),
117
+ ),
118
+ ...overrides,
119
+ };
120
+ }
121
+
122
+ /**
123
+ * Creates a mock tool client
124
+ * @param {object} overrides - Method overrides
125
+ * @returns {object} Mock tool client
126
+ */
127
+ export function createMockToolClient(overrides = {}) {
128
+ return {
129
+ CallTool: spy(() =>
130
+ Promise.resolve({
131
+ content: "Tool result",
132
+ }),
133
+ ),
134
+ ...overrides,
135
+ };
136
+ }
137
+
138
+ function notFound() {
139
+ return Object.assign(new Error("not found"), {
140
+ code: grpc.status.NOT_FOUND,
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Creates a mock discussion (bridge) client
146
+ * @param {object} overrides - Method overrides
147
+ * @returns {object} Mock discussion client
148
+ */
149
+ export function createMockDiscussionClient(overrides = {}) {
150
+ return {
151
+ LoadDiscussion: spy(() => Promise.reject(notFound())),
152
+ LoadDiscussionByCorrelation: spy(() => Promise.reject(notFound())),
153
+ ListOpenRecesses: spy(() => Promise.resolve({ refs: [] })),
154
+ SaveDiscussion: spy(() => Promise.resolve({})),
155
+ HasOrigin: spy(() => Promise.resolve({ exists: false })),
156
+ RecordOrigin: spy(() => Promise.resolve({})),
157
+ Sweep: spy(() =>
158
+ Promise.resolve({
159
+ evicted_discussions: 0,
160
+ evicted_origins: 0,
161
+ evicted_pending: 0,
162
+ }),
163
+ ),
164
+ PutPendingDispatch: spy(() => Promise.resolve({})),
165
+ ResolvePendingDispatch: spy(() => Promise.reject(notFound())),
166
+ ...overrides,
167
+ };
168
+ }
169
+
170
+ function coerceInt64Fields(obj) {
171
+ obj.open_rfcs ??= {};
172
+ obj.pending_callbacks ??= {};
173
+ obj.history ??= [];
174
+ obj.participants ??= [];
175
+ obj.dispatches = (obj.dispatches ?? []).map(Number);
176
+ if (obj.last_active_at != null)
177
+ obj.last_active_at = Number(obj.last_active_at);
178
+ for (const rfc of Object.values(obj.open_rfcs)) {
179
+ if (rfc.due_at != null) rfc.due_at = Number(rfc.due_at);
180
+ if (rfc.opened_at != null) rfc.opened_at = Number(rfc.opened_at);
181
+ if (rfc.history_index_at_open != null)
182
+ rfc.history_index_at_open = Number(rfc.history_index_at_open);
183
+ if (rfc.trigger?.replies != null)
184
+ rfc.trigger.replies = Number(rfc.trigger.replies);
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Creates a stateful mock discussion client that retains records across
190
+ * save/load cycles, coercing proto int64 fields back to numbers.
191
+ * @returns {object} Stateful mock discussion client
192
+ */
193
+ export function createStatefulDiscussionClient() {
194
+ const records = new Map();
195
+ const origins = new Map();
196
+ const pending = new Map();
197
+
198
+ return {
199
+ SaveDiscussion: spy(async (req) => {
200
+ const obj = req?.toJSON?.() ?? req;
201
+ coerceInt64Fields(obj);
202
+ records.set(obj.id, obj);
203
+ return {};
204
+ }),
205
+ LoadDiscussion: spy(async (req) => {
206
+ const obj = req?.toJSON?.() ?? req;
207
+ const key = `${obj.channel}:${obj.discussion_id}`;
208
+ const rec = records.get(key);
209
+ if (!rec) throw notFound();
210
+ return rec;
211
+ }),
212
+ LoadDiscussionByCorrelation: spy(async (req) => {
213
+ const obj = req?.toJSON?.() ?? req;
214
+ for (const rec of records.values()) {
215
+ if (
216
+ Object.values(rec.pending_callbacks ?? {}).includes(
217
+ obj.correlation_id,
218
+ ) ||
219
+ rec.open_rfcs?.[obj.correlation_id]
220
+ )
221
+ return rec;
222
+ }
223
+ throw notFound();
224
+ }),
225
+ ListOpenRecesses: spy(async () => {
226
+ const refs = [];
227
+ for (const rec of records.values())
228
+ for (const [cid, rfc] of Object.entries(rec.open_rfcs ?? {}))
229
+ if (typeof rfc.due_at === "number")
230
+ refs.push({ correlation_id: cid, due_at: rfc.due_at });
231
+ return { refs };
232
+ }),
233
+ HasOrigin: spy(async (req) => {
234
+ const obj = req?.toJSON?.() ?? req;
235
+ return { exists: origins.has(obj.id) };
236
+ }),
237
+ RecordOrigin: spy(async (req) => {
238
+ const obj = req?.toJSON?.() ?? req;
239
+ origins.set(obj.id, obj);
240
+ return {};
241
+ }),
242
+ Sweep: spy(async () => ({
243
+ evicted_discussions: 0,
244
+ evicted_origins: 0,
245
+ evicted_pending: 0,
246
+ })),
247
+ PutPendingDispatch: spy(async (req) => {
248
+ const obj = req?.toJSON?.() ?? req;
249
+ const p = obj.pending ?? obj;
250
+ pending.set(p.link_token, p);
251
+ return {};
252
+ }),
253
+ ResolvePendingDispatch: spy(async (req) => {
254
+ const obj = req?.toJSON?.() ?? req;
255
+ const token = obj.link_token;
256
+ const rec = pending.get(token);
257
+ if (!rec) throw notFound();
258
+ pending.delete(token);
259
+ return rec;
260
+ }),
261
+ EnqueueInbox: spy(async () => ({})),
262
+ DrainInbox: spy(async () => ({ messages: [] })),
263
+ };
264
+ }
@@ -0,0 +1,62 @@
1
+ import { spy } from "./spy.js";
2
+
3
+ /**
4
+ * Creates a mock clock with controllable time and a no-wait sleep.
5
+ *
6
+ * `now()` returns the current virtual time in ms. `sleep(ms)` advances
7
+ * virtual time by `ms` and resolves on the next microtask — no real
8
+ * timers are scheduled. `advance(ms)` lets a test move time forward
9
+ * without going through `sleep` (e.g. to expire a token).
10
+ *
11
+ * Pass `{ sleep, now }` from a returned clock to any constructor that
12
+ * accepts those collaborators to make its tests deterministic.
13
+ *
14
+ * `setTimeout(fn, ms)` / `clearTimeout(handle)` / `setInterval(fn, ms)` /
15
+ * `clearInterval(handle)` delegate to the host's real timers (matching
16
+ * `createDefaultClock`), so a migrated module that schedules work through the
17
+ * injected clock keeps its real-timer test behaviour. Virtual `now()` and the
18
+ * real timers are intentionally independent — a test that needs a fired timer
19
+ * waits on real time as it did before migration; a periodic sweep timer is
20
+ * typically `.unref()`'d and never fires during a unit test, which exercises
21
+ * its eviction path through an explicit `now` argument instead.
22
+ *
23
+ * @param {object} [options]
24
+ * @param {number} [options.start=0] - Initial virtual time in ms.
25
+ * @returns {{
26
+ * now: () => number,
27
+ * sleep: (ms: number) => Promise<void>,
28
+ * setTimeout: (fn: Function, ms: number) => *,
29
+ * clearTimeout: (handle: *) => void,
30
+ * setInterval: (fn: Function, ms: number) => *,
31
+ * clearInterval: (handle: *) => void,
32
+ * advance: (ms: number) => void,
33
+ * set: (ms: number) => void,
34
+ * sleeps: Array<number>,
35
+ * }}
36
+ */
37
+ export function createMockClock({ start = 0 } = {}) {
38
+ let current = start;
39
+ const sleeps = [];
40
+
41
+ const now = spy(() => current);
42
+ const sleep = spy(async (ms) => {
43
+ sleeps.push(ms);
44
+ current += ms;
45
+ });
46
+
47
+ return {
48
+ now,
49
+ sleep,
50
+ setTimeout: (fn, ms) => setTimeout(fn, ms),
51
+ clearTimeout: (handle) => clearTimeout(handle),
52
+ setInterval: (fn, ms) => setInterval(fn, ms),
53
+ clearInterval: (handle) => clearInterval(handle),
54
+ advance(ms) {
55
+ current += ms;
56
+ },
57
+ set(ms) {
58
+ current = ms;
59
+ },
60
+ sleeps,
61
+ };
62
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Creates a mock configuration object
3
+ * @param {string} name - Service/extension name
4
+ * @param {object} overrides - Properties to override
5
+ * @returns {object} Mock config
6
+ */
7
+ export function createMockConfig(name = "test-service", overrides = {}) {
8
+ return {
9
+ name,
10
+ namespace: "test",
11
+ host: "0.0.0.0",
12
+ port: 3000,
13
+ max_tokens: 4096,
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Creates a mock service config with common service properties
20
+ * @param {string} name - Service name
21
+ * @param {object} overrides - Properties to override
22
+ * @returns {object} Mock service config
23
+ */
24
+ export function createMockServiceConfig(name, overrides = {}) {
25
+ return createMockConfig(name, {
26
+ budget: 1000,
27
+ threshold: 0.3,
28
+ limit: 10,
29
+ ...overrides,
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Creates a mock extension config
35
+ * @param {string} name - Extension name
36
+ * @param {object} overrides - Properties to override
37
+ * @returns {object} Mock extension config
38
+ */
39
+ export function createMockExtensionConfig(name, overrides = {}) {
40
+ return createMockConfig(name, {
41
+ secret: "test-secret",
42
+ anthropicToken: async () => "test-anthropic-key",
43
+ ...overrides,
44
+ });
45
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared test data for common testing scenarios
3
+ */
4
+
5
+ // Sample request data
6
+ export const testRequestData = {
7
+ query: "test query",
8
+ userId: "test-user-123",
9
+ sessionId: "test-session-456",
10
+ };
11
+
12
+ // Sample vector data
13
+ export const testVectorData = [
14
+ { id: "vector-1", embedding: [0.1, 0.2, 0.3], score: 0.9 },
15
+ { id: "vector-2", embedding: [0.4, 0.5, 0.6], score: 0.8 },
16
+ { id: "vector-3", embedding: [0.7, 0.8, 0.9], score: 0.7 },
17
+ ];
18
+
19
+ // Sample message data
20
+ export const testMessageData = [
21
+ { role: "system", content: "You are a helpful assistant." },
22
+ { role: "user", content: "Hello, how are you?" },
23
+ { role: "assistant", content: "I'm doing well, thank you for asking!" },
24
+ ];
25
+
26
+ // Sample chunk data
27
+ export const testChunkData = [
28
+ {
29
+ id: "chunk-1",
30
+ text: "This is a sample text chunk for testing purposes.",
31
+ tokens: 10,
32
+ },
33
+ {
34
+ id: "chunk-2",
35
+ text: "Another sample chunk with different content.",
36
+ tokens: 8,
37
+ },
38
+ ];
39
+
40
+ // Sample configuration data
41
+ export const testConfigData = {
42
+ host: "localhost",
43
+ port: 3000,
44
+ threshold: 0.5,
45
+ limit: 100,
46
+ };
@@ -0,0 +1,80 @@
1
+ import { createMockStorage } from "./storage.js";
2
+
3
+ /**
4
+ * Graph-index test triple: a mock storage, an n3 Store, and a GraphIndex wired
5
+ * to both. GraphIndex and Store are injected so libmock stays dependency-free.
6
+ * @param {object} opts
7
+ * @param {Function} opts.GraphIndex - libgraph GraphIndex constructor.
8
+ * @param {Function} opts.Store - n3 Store constructor.
9
+ * @param {object} [opts.storageOverrides] - passed to createMockStorage.
10
+ * @param {*} [opts.prefixes] - prefixes arg for GraphIndex (default {}).
11
+ * @param {string} [opts.indexKey] - jsonl key (default "test-graph.jsonl").
12
+ * @returns {{ n3Store: object, graphIndex: object, mockStorage: object }}
13
+ */
14
+ export function createGraphIndexFixture({
15
+ GraphIndex,
16
+ Store,
17
+ storageOverrides,
18
+ prefixes = {},
19
+ indexKey = "test-graph.jsonl",
20
+ }) {
21
+ const mockStorage = createMockStorage(storageOverrides);
22
+ const n3Store = new Store();
23
+ const graphIndex = new GraphIndex(mockStorage, n3Store, prefixes, indexKey);
24
+ return { n3Store, graphIndex, mockStorage };
25
+ }
26
+
27
+ /**
28
+ * The stripped gRPC health service definition consumers' tests fake — the
29
+ * `{ Check: { path, requestStream, responseStream } }` shape, not librpc's
30
+ * real `healthDefinition` (which librpc's own tests exercise directly).
31
+ * @returns {{ Check: { path: string, requestStream: boolean, responseStream: boolean } }}
32
+ */
33
+ export function createMockGrpcHealthDefinition() {
34
+ return {
35
+ Check: {
36
+ path: "/grpc.health.v1.Health/Check",
37
+ requestStream: false,
38
+ responseStream: false,
39
+ },
40
+ };
41
+ }
42
+
43
+ /**
44
+ * The readline/process/os/formatter/storage bundle librepl's tests inject.
45
+ * Mirrors libraries/librepl/test/librepl.test.js's pre-collapse beforeEach.
46
+ * @returns {{ readline: object, process: object, os: object, formatter: Function, storage: object }}
47
+ */
48
+ export function createReplEnvironment() {
49
+ const proc = {
50
+ argv: ["node", "script.js"],
51
+ stdin: {
52
+ isTTY: true,
53
+ setEncoding: () => {},
54
+ async *[Symbol.asyncIterator]() {
55
+ yield "test input";
56
+ },
57
+ },
58
+ stdout: { write: () => {} },
59
+ stderr: { write: () => {} },
60
+ exit: (code) => {
61
+ proc._exitCalled = true;
62
+ proc._exitCode = code;
63
+ },
64
+ _exitCalled: false,
65
+ _exitCode: null,
66
+ };
67
+ return {
68
+ readline: {
69
+ createInterface: () => ({
70
+ on: () => {},
71
+ prompt: () => {},
72
+ close: () => {},
73
+ }),
74
+ },
75
+ process: proc,
76
+ os: { userInfo: () => ({ uid: 1000 }) },
77
+ formatter: () => ({ format: (text) => `formatted: ${text}` }),
78
+ storage: createMockStorage(),
79
+ };
80
+ }
@@ -0,0 +1,83 @@
1
+ import path from "node:path";
2
+ import { spy } from "./spy.js";
3
+
4
+ /**
5
+ * Creates a mock `Finder` collaborator over an in-memory `files` map. Mimics
6
+ * the real `Finder` surface (`findUpward`, `findData`, `findProjectRoot`,
7
+ * `findPackagePath`, `findGeneratedPath`, `createSymlink`,
8
+ * `createPackageSymlinks`) without touching the real filesystem. Every call
9
+ * is recorded on `calls`.
10
+ *
11
+ * @param {object} [options]
12
+ * @param {Object<string, true|string>} [options.files] - Existing paths.
13
+ * @param {string} [options.cwd="/work"] - Working directory for `findData`.
14
+ * @returns {object} The mock finder.
15
+ */
16
+ export function createMockFinder({ files = {}, cwd = "/work" } = {}) {
17
+ const calls = [];
18
+ const has = (p) => Object.hasOwn(files, p);
19
+ const record = (name, args) => calls.push({ name, args });
20
+
21
+ const findUpward = spy((root, relativePath, maxDepth = 3) => {
22
+ record("findUpward", [root, relativePath, maxDepth]);
23
+ let current = root;
24
+ for (let depth = 0; depth < maxDepth; depth++) {
25
+ const candidate = path.join(current, relativePath);
26
+ if (has(candidate)) return candidate;
27
+ const parent = path.dirname(current);
28
+ if (parent === current) break;
29
+ current = parent;
30
+ }
31
+ return null;
32
+ });
33
+
34
+ const findData = spy((baseName, homeDir) => {
35
+ record("findData", [baseName, homeDir]);
36
+ const found = findUpward(cwd, baseName);
37
+ if (found) return found;
38
+ const homePath = path.join(homeDir, ".fit", baseName);
39
+ if (has(homePath)) return homePath;
40
+ throw new Error(`No ${baseName} directory found.`);
41
+ });
42
+
43
+ const findProjectRoot = spy((startPath) => {
44
+ record("findProjectRoot", [startPath]);
45
+ const pkg = findUpward(startPath, "package.json", 5);
46
+ if (pkg) return path.dirname(pkg);
47
+ throw new Error("Could not find project root");
48
+ });
49
+
50
+ const findPackagePath = spy((projectRoot, packageName) => {
51
+ record("findPackagePath", [projectRoot, packageName]);
52
+ return path.join(projectRoot, "libraries", packageName);
53
+ });
54
+
55
+ const findGeneratedPath = spy((projectRoot, packageName) => {
56
+ record("findGeneratedPath", [projectRoot, packageName]);
57
+ return path.join(
58
+ findPackagePath(projectRoot, packageName),
59
+ "src",
60
+ "generated",
61
+ );
62
+ });
63
+
64
+ const createSymlink = spy(async (sourcePath, targetPath) => {
65
+ record("createSymlink", [sourcePath, targetPath]);
66
+ files[targetPath] = sourcePath;
67
+ });
68
+
69
+ const createPackageSymlinks = spy(async (generatedPath) => {
70
+ record("createPackageSymlinks", [generatedPath]);
71
+ });
72
+
73
+ return {
74
+ findUpward,
75
+ findData,
76
+ findProjectRoot,
77
+ findPackagePath,
78
+ findGeneratedPath,
79
+ createSymlink,
80
+ createPackageSymlinks,
81
+ calls,
82
+ };
83
+ }