@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,297 @@
1
+ /**
2
+ * Additional infrastructure mocks for services/products tests. Centralizes
3
+ * variants that consumers previously inlined.
4
+ */
5
+ import { Writable } from "node:stream";
6
+
7
+ /**
8
+ * A capturing `Writable` used as the mock `proc.stdout`/`stderr`. It retains
9
+ * the `chunks` accessor existing assertions read AND is a real `Writable`, so
10
+ * it accepts piped input (e.g. a `pipeline()` from a read stream).
11
+ */
12
+ class CaptureWritable extends Writable {
13
+ constructor() {
14
+ super();
15
+ this.chunks = [];
16
+ }
17
+
18
+ /**
19
+ * @param {Buffer|string} chunk - The written chunk.
20
+ * @param {string} _encoding - Chunk encoding (unused).
21
+ * @param {Function} callback - Completion callback.
22
+ */
23
+ _write(chunk, _encoding, callback) {
24
+ this.chunks.push(String(chunk));
25
+ callback();
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Creates a mock Supabase-style client with configurable table and storage
31
+ * behaviour. Covers the patterns found across products/map/test/activity/*.
32
+ *
33
+ * @param {object} [options]
34
+ * @param {Record<string, object>} [options.tables] - Map of table name to
35
+ * override. Each override may expose `select`, `insert`, `upsert`, `delete`
36
+ * as async functions. Unspecified methods return `{ data: [], error: null }`.
37
+ * @param {Record<string, string>} [options.files] - Files exposed via
38
+ * `storage.from(...).list(prefix)` / `.download(path)`.
39
+ * @returns {object} Mock client and call-tracking arrays.
40
+ */
41
+ export function createMockSupabaseClient({ tables = {}, files = {} } = {}) {
42
+ const calls = {
43
+ select: [],
44
+ insert: [],
45
+ upsert: [],
46
+ delete: [],
47
+ download: [],
48
+ list: [],
49
+ };
50
+
51
+ function record(kind, entry) {
52
+ calls[kind].push(entry);
53
+ }
54
+
55
+ return {
56
+ calls,
57
+ from(table) {
58
+ const override = tables[table] ?? {};
59
+ return {
60
+ async select(...args) {
61
+ record("select", { table, args });
62
+ if (override.select) return override.select(...args);
63
+ return { data: [], error: null };
64
+ },
65
+ async insert(rows, opts) {
66
+ record("insert", { table, rows, options: opts });
67
+ if (override.insert) return override.insert(rows, opts);
68
+ return { data: rows, error: null };
69
+ },
70
+ async upsert(rows, opts) {
71
+ record("upsert", {
72
+ table,
73
+ rows,
74
+ onConflict: opts?.onConflict,
75
+ options: opts,
76
+ });
77
+ if (override.upsert) return override.upsert(rows, opts);
78
+ return { data: rows, error: null };
79
+ },
80
+ async delete(...args) {
81
+ record("delete", { table, args });
82
+ if (override.delete) return override.delete(...args);
83
+ return { error: null };
84
+ },
85
+ };
86
+ },
87
+ storage: {
88
+ from() {
89
+ return {
90
+ async list(prefix) {
91
+ record("list", { prefix });
92
+ const names = Object.keys(files)
93
+ .filter((k) => k.startsWith(prefix))
94
+ .map((k) => ({
95
+ name: k.slice(prefix.length),
96
+ created_at: "z",
97
+ }));
98
+ return { data: names, error: null };
99
+ },
100
+ async download(path) {
101
+ record("download", { path });
102
+ const content = files[path];
103
+ if (content === undefined) {
104
+ return { data: null, error: { message: "not found" } };
105
+ }
106
+ return {
107
+ data: { text: async () => content },
108
+ error: null,
109
+ };
110
+ },
111
+ };
112
+ },
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Creates Turtle parsing helpers bound to an injected n3 Parser. Keeps
119
+ * libmock free of an n3 dependency while allowing services to share the
120
+ * parseQuads / findAll / findOne idiom.
121
+ *
122
+ * @param {import("n3").Parser | Function} ParserOrInstance - n3 Parser class
123
+ * or a pre-built parser instance.
124
+ * @param {object} [options]
125
+ * @param {string} [options.format="Turtle"] - Parser format.
126
+ * @returns {object} { parseQuads, findAll, findOne }
127
+ */
128
+ export function createTurtleHelpers(
129
+ ParserOrInstance,
130
+ { format = "Turtle" } = {},
131
+ ) {
132
+ const isClass =
133
+ typeof ParserOrInstance === "function" &&
134
+ ParserOrInstance.prototype &&
135
+ typeof ParserOrInstance.prototype.parse === "function";
136
+ const parser = isClass ? new ParserOrInstance({ format }) : ParserOrInstance;
137
+
138
+ function parseQuads(turtle) {
139
+ return parser.parse(turtle);
140
+ }
141
+
142
+ function findAll(quads, { subject, predicate, object } = {}) {
143
+ return quads.filter(
144
+ (q) =>
145
+ (!subject || q.subject.value === subject) &&
146
+ (!predicate || q.predicate.value === predicate) &&
147
+ (!object || q.object.value === object),
148
+ );
149
+ }
150
+
151
+ function findOne(quads, pattern) {
152
+ return findAll(quads, pattern)[0];
153
+ }
154
+
155
+ return { parseQuads, findAll, findOne };
156
+ }
157
+
158
+ /**
159
+ * Build an `AsyncIterable<string>` over a fixed list of input chunks, used as
160
+ * the mock `proc.stdin`.
161
+ * @param {string[]} chunks - Lines/chunks the iterator yields in order.
162
+ * @returns {AsyncIterable<string>}
163
+ */
164
+ export function createMockStdin(chunks = []) {
165
+ return {
166
+ async *[Symbol.asyncIterator]() {
167
+ for (const chunk of chunks) yield chunk;
168
+ },
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Creates a mock `process`-like object matching the `Runtime.proc` surface:
174
+ * `cwd()`, `env`, `argv`, `stdin`, `stdout`/`stderr` (capturing `Writable`s),
175
+ * `exit(code)`, `kill(pid, signal)`, `pid`, `platform`, `on(event, handler)`,
176
+ * and a settable `exitCode`. Writes are captured on `stdout.chunks` /
177
+ * `stderr.chunks` (and the streams accept piped input); kill calls on `kills`;
178
+ * event handlers on `handlers` (fire them via `emit(event, ...args)` to
179
+ * simulate a signal).
180
+ *
181
+ * @param {object} [options]
182
+ * @param {Record<string, string>} [options.env] - Initial env map.
183
+ * @param {string} [options.cwd] - Working directory `cwd()` returns.
184
+ * @param {string[]} [options.argv] - The frozen `argv` array.
185
+ * @param {string[]} [options.stdin] - Chunks the `stdin` iterator yields.
186
+ * @param {(pid: number, signal: string|number) => any} [options.kill] - Optional
187
+ * `kill` implementation (e.g. to model a liveness probe); calls are always
188
+ * recorded on the returned `kills` array regardless.
189
+ * @param {number} [options.pid] - The fake's `pid` (default 1234).
190
+ * @param {string} [options.platform] - The fake's `platform` string
191
+ * (default `"linux"`; set `"darwin"`/`"win32"` to exercise per-platform code).
192
+ * @returns {object}
193
+ */
194
+ export function createMockProcess({
195
+ env = {},
196
+ cwd,
197
+ argv,
198
+ stdin,
199
+ kill,
200
+ pid = 1234,
201
+ platform = "linux",
202
+ } = {}) {
203
+ const stdout = new CaptureWritable();
204
+ const stderr = new CaptureWritable();
205
+ const kills = [];
206
+ // Registered event handlers (e.g. "SIGTERM"/"SIGINT"); a test can fire them
207
+ // via `emit(event, ...args)` to simulate a signal without a real process.
208
+ const handlers = {};
209
+ return {
210
+ env: { ...env },
211
+ cwd: () => cwd ?? "/work",
212
+ argv: Object.freeze([...(argv ?? ["/usr/bin/node", "/tmp/test-bin.js"])]),
213
+ stdin: createMockStdin(stdin ?? []),
214
+ stdout,
215
+ stderr,
216
+ pid,
217
+ platform,
218
+ exitCode: 0,
219
+ exit(code = 0) {
220
+ this.exitCode = code;
221
+ },
222
+ kills,
223
+ kill(pid, signal) {
224
+ kills.push({ pid, signal });
225
+ return kill?.(pid, signal);
226
+ },
227
+ handlers,
228
+ on(event, handler) {
229
+ (handlers[event] ??= []).push(handler);
230
+ },
231
+ emit(event, ...args) {
232
+ for (const handler of handlers[event] ?? []) handler(...args);
233
+ },
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Runs `fn` with `console.log`, `console.info`, and `console.warn` suppressed,
239
+ * returning whatever `fn` returns. Errors still propagate.
240
+ *
241
+ * @template T
242
+ * @param {() => T | Promise<T>} fn
243
+ * @returns {Promise<T>}
244
+ */
245
+ export async function withSilentConsole(fn) {
246
+ const originals = {
247
+ log: console.log,
248
+ info: console.info,
249
+ warn: console.warn,
250
+ };
251
+ console.log = () => {};
252
+ console.info = () => {};
253
+ console.warn = () => {};
254
+ try {
255
+ return await fn();
256
+ } finally {
257
+ console.log = originals.log;
258
+ console.info = originals.info;
259
+ console.warn = originals.warn;
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Creates a bag of async query stubs from a plain values object. A function
265
+ * value is passed through untouched; anything else becomes an async function
266
+ * returning that value. Collapses landmark-style `stubQueries` boilerplate.
267
+ *
268
+ * @param {Record<string, unknown>} values
269
+ * @returns {Record<string, Function>}
270
+ */
271
+ export function createMockQueries(values = {}) {
272
+ const out = {};
273
+ for (const [key, val] of Object.entries(values)) {
274
+ out[key] = typeof val === "function" ? val : async () => val;
275
+ }
276
+ return out;
277
+ }
278
+
279
+ /**
280
+ * Creates a minimal mock S3 client that records command sends and returns a
281
+ * configurable response.
282
+ *
283
+ * @param {object} [options]
284
+ * @param {(command: object) => unknown} [options.sendFn] - Custom send handler.
285
+ * @returns {object} { client, sends }
286
+ */
287
+ export function createMockS3Client({ sendFn } = {}) {
288
+ const sends = [];
289
+ return {
290
+ sends,
291
+ async send(command) {
292
+ sends.push(command);
293
+ if (sendFn) return sendFn(command);
294
+ return {};
295
+ },
296
+ };
297
+ }
@@ -0,0 +1,42 @@
1
+ import { spy } from "./spy.js";
2
+ /**
3
+ * Creates a mock logger with call tracking
4
+ * @param {object} options - Logger options
5
+ * @param {boolean} options.captureOutput - Whether to capture log output
6
+ * @returns {object} Mock logger
7
+ */
8
+ export function createMockLogger(options = {}) {
9
+ const logs = [];
10
+ const capture = options.captureOutput ?? false;
11
+
12
+ const createMethod = (level) =>
13
+ spy((appId, msg, attributes) => {
14
+ if (capture) {
15
+ logs.push({ level, appId, msg, attributes });
16
+ }
17
+ });
18
+
19
+ return {
20
+ logs,
21
+ debug: createMethod("debug"),
22
+ info: createMethod("info"),
23
+ warn: createMethod("warn"),
24
+ error: createMethod("error"),
25
+ exception: createMethod("exception"),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * Creates a silent logger that does nothing
31
+ * @returns {object} Silent logger
32
+ */
33
+ export function createSilentLogger() {
34
+ const noop = () => {};
35
+ return {
36
+ debug: noop,
37
+ info: noop,
38
+ warn: noop,
39
+ error: noop,
40
+ exception: noop,
41
+ };
42
+ }
@@ -0,0 +1,78 @@
1
+ import { spy } from "./spy.js";
2
+ /**
3
+ * Creates a mock observer factory
4
+ * @param {object} logger - Logger to use (optional)
5
+ * @returns {Function} Mock observer factory
6
+ */
7
+ export function createMockObserverFn(logger = null) {
8
+ const mockLogger = logger || {
9
+ debug: spy(),
10
+ info: spy(),
11
+ error: spy(),
12
+ };
13
+
14
+ return () => ({
15
+ observeServerUnaryCall: async (_method, handler, call, callback) => {
16
+ return await handler(call, callback);
17
+ },
18
+ observeClientUnaryCall: async (_method, _request, fn) => {
19
+ return await fn();
20
+ },
21
+ observeClientStreamingCall: (_method, _request, fn) => {
22
+ return fn();
23
+ },
24
+ logger: () => mockLogger,
25
+ });
26
+ }
27
+
28
+ /**
29
+ * Creates a mock tracer
30
+ * @param {object} overrides - Method overrides
31
+ * @returns {object} Mock tracer
32
+ */
33
+ export function createMockTracer(overrides = {}) {
34
+ return {
35
+ startSpan: spy((name, options = {}) => ({
36
+ span_id: `span-${Date.now()}`,
37
+ trace_id: `trace-${Date.now()}`,
38
+ name,
39
+ ...options,
40
+ addEvent: spy(),
41
+ setOk: spy(),
42
+ setError: spy(),
43
+ end: spy(async () => {}),
44
+ })),
45
+ startClientSpan: spy((_service, _method) => ({
46
+ span: {
47
+ span_id: `client-span-${Date.now()}`,
48
+ trace_id: `trace-${Date.now()}`,
49
+ },
50
+ metadata: { get: () => [], set: () => {} },
51
+ })),
52
+ startServerSpan: spy((_service, _method, _request, metadata) => ({
53
+ span_id: `server-span-${Date.now()}`,
54
+ trace_id: metadata?.get?.("x-trace-id")?.[0] || `trace-${Date.now()}`,
55
+ })),
56
+ getSpanContext: () => ({
57
+ run: (span, fn) => fn(),
58
+ getStore: () => null,
59
+ }),
60
+ endSpan: spy(),
61
+ recordError: spy(),
62
+ ...overrides,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Creates a mock auth factory
68
+ * @param {object} options - Auth options
69
+ * @returns {Function} Mock auth factory
70
+ */
71
+ export function createMockAuthFn(options = {}) {
72
+ const { isValid = true, serviceId = "test" } = options;
73
+
74
+ return () => ({
75
+ createClientInterceptor: () => () => {},
76
+ validateCall: () => ({ isValid, serviceId }),
77
+ });
78
+ }
@@ -0,0 +1,95 @@
1
+ import { common, tool } from "@forwardimpact/libtype";
2
+
3
+ /**
4
+ * Creates a mock resource index with common test data
5
+ * @param {object} options - Setup options
6
+ * @returns {object} Mock resource index
7
+ */
8
+ export function createMockResourceIndex(options = {}) {
9
+ const resources = new Map();
10
+
11
+ const index = {
12
+ resources,
13
+
14
+ async get(identifiers, _actor) {
15
+ if (!identifiers || identifiers.length === 0) return [];
16
+ return identifiers
17
+ .map((id) => {
18
+ const key = typeof id === "string" ? id : id.toString?.() || id.name;
19
+ return resources.get(key);
20
+ })
21
+ .filter(Boolean);
22
+ },
23
+
24
+ put(resource) {
25
+ const key = resource.id?.toString?.() || resource.id?.name;
26
+ if (key) resources.set(key, resource);
27
+ },
28
+
29
+ async has(id) {
30
+ const key = typeof id === "string" ? id : id.toString?.();
31
+ return resources.has(key);
32
+ },
33
+
34
+ /**
35
+ * Sets up default test resources
36
+ * @param {object} setupOptions - Setup options
37
+ * @param {string[]} [setupOptions.tools] - Tool names to seed
38
+ * @param {string} [setupOptions.conversationId] - Conversation ID
39
+ */
40
+ setupDefaults(setupOptions = {}) {
41
+ const { tools = [], conversationId = "test-conversation" } = setupOptions;
42
+
43
+ resources.set(
44
+ conversationId,
45
+ common.Conversation.fromObject({
46
+ id: { name: conversationId },
47
+ }),
48
+ );
49
+
50
+ for (const name of tools) {
51
+ resources.set(
52
+ `tool.ToolFunction.${name}`,
53
+ tool.ToolFunction.fromObject({
54
+ id: { name, tokens: 20 },
55
+ name,
56
+ description: `${name} tool`,
57
+ }),
58
+ );
59
+ }
60
+ },
61
+
62
+ /**
63
+ * Adds a message resource
64
+ * @param {object} msg - Message to add
65
+ */
66
+ addMessage(msg) {
67
+ const id =
68
+ msg.id?.type && msg.id?.toString
69
+ ? msg.id.toString()
70
+ : msg.id?.name || String(msg.id);
71
+ resources.set(id, msg);
72
+ },
73
+
74
+ /**
75
+ * Find resources by prefix
76
+ * @param {string} prefix - Prefix to search for
77
+ * @returns {Promise<string[]>} List of matching keys
78
+ */
79
+ async findByPrefix(prefix) {
80
+ const keys = [];
81
+ for (const key of resources.keys()) {
82
+ if (key.startsWith(prefix)) {
83
+ keys.push(key);
84
+ }
85
+ }
86
+ return keys;
87
+ },
88
+ };
89
+
90
+ if (options.tools || options.conversationId) {
91
+ index.setupDefaults(options);
92
+ }
93
+
94
+ return index;
95
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Creates a mock service callbacks object for agent testing
3
+ * @param {object} overrides - Method overrides per service
4
+ * @returns {object} Service callbacks object
5
+ */
6
+ export function createMockServiceCallbacks(overrides = {}) {
7
+ return {
8
+ memory: {
9
+ append: async () => ({}),
10
+ get: async () => ({
11
+ messages: [{ role: "system", content: "You are an assistant" }],
12
+ tools: [],
13
+ }),
14
+ ...overrides.memory,
15
+ },
16
+ llm: {
17
+ createCompletions: async () => ({
18
+ choices: [
19
+ {
20
+ message: {
21
+ role: "assistant",
22
+ content: "Test response",
23
+ tool_calls: [],
24
+ },
25
+ },
26
+ ],
27
+ }),
28
+ ...overrides.llm,
29
+ },
30
+ tool: {
31
+ call: async () => ({
32
+ role: "tool",
33
+ content: "Tool result",
34
+ }),
35
+ ...overrides.tool,
36
+ },
37
+ ...overrides,
38
+ };
39
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Mock service utilities for testing
3
+ */
4
+
5
+ /**
6
+ * Creates a mock vector service
7
+ * @param {object} overrides - Properties to override in the mock
8
+ * @returns {object} Mock vector service
9
+ */
10
+ export function mockVectorService(overrides = {}) {
11
+ return {
12
+ QueryItems: async (request, callback) => {
13
+ callback(null, { results: [], total: 0 });
14
+ },
15
+ close: () => {},
16
+ ...overrides,
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Creates a mock history service
22
+ * @param {object} overrides - Properties to override in the mock
23
+ * @returns {object} Mock history service
24
+ */
25
+ export function mockHistoryService(overrides = {}) {
26
+ return {
27
+ GetHistory: async (request, callback) => {
28
+ callback(null, { messages: [] });
29
+ },
30
+ UpdateHistory: async (request, callback) => {
31
+ callback(null, {});
32
+ },
33
+ close: () => {},
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Creates a mock LLM service
40
+ * @param {object} overrides - Properties to override in the mock
41
+ * @returns {object} Mock LLM service
42
+ */
43
+ export function mockLlmService(overrides = {}) {
44
+ return {
45
+ CreateCompletions: async (request, callback) => {
46
+ callback(null, {
47
+ choices: [
48
+ {
49
+ index: 0,
50
+ message: { role: "assistant", content: "Test response" },
51
+ finish_reason: "stop",
52
+ },
53
+ ],
54
+ });
55
+ },
56
+ CreateEmbeddings: async (request, callback) => {
57
+ callback(null, {
58
+ data: [{ index: 0, embedding: [0.1, 0.2, 0.3] }],
59
+ });
60
+ },
61
+ close: () => {},
62
+ ...overrides,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Creates a mock text service
68
+ * @param {object} overrides - Properties to override in the mock
69
+ * @returns {object} Mock text service
70
+ */
71
+ export function mockTextService(overrides = {}) {
72
+ return {
73
+ GetChunks: async (request, callback) => {
74
+ callback(null, { chunks: {} });
75
+ },
76
+ close: () => {},
77
+ ...overrides,
78
+ };
79
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Portable mock function helper. Replaces `mock.fn` from `node:test` so the
3
+ * test suite can run under either node:test or bun:test.
4
+ *
5
+ * Shape matches node:test's `mock.fn` to keep call-inspection sites
6
+ * (`fn.mock.calls[0].arguments`, `fn.mock.callCount()`, `fn.mock.resetCalls()`)
7
+ * unchanged across the codebase.
8
+ *
9
+ * @template T
10
+ * @param {(...args: any[]) => T} [impl] - Initial implementation.
11
+ * @returns {((...args: any[]) => T) & { mock: { calls: Array<{arguments: any[], result?: T, error?: unknown, this: unknown}>, callCount: () => number, resetCalls: () => void, mockImplementation: (newImpl: (...args: any[]) => T) => void } }}
12
+ */
13
+ export function spy(impl) {
14
+ let _impl = impl;
15
+ const calls = [];
16
+ const fn = function (...args) {
17
+ const rec = { arguments: args, this: this };
18
+ if (!_impl) {
19
+ calls.push(rec);
20
+ return undefined;
21
+ }
22
+ try {
23
+ const result = _impl.apply(this, args);
24
+ rec.result = result;
25
+ calls.push(rec);
26
+ return result;
27
+ } catch (err) {
28
+ rec.error = err;
29
+ calls.push(rec);
30
+ throw err;
31
+ }
32
+ };
33
+ fn.mock = {
34
+ calls,
35
+ callCount: () => calls.length,
36
+ resetCalls: () => {
37
+ calls.length = 0;
38
+ },
39
+ mockImplementation: (newImpl) => {
40
+ _impl = newImpl;
41
+ },
42
+ };
43
+ return fn;
44
+ }