@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,118 @@
1
+ import { spy } from "./spy.js";
2
+ /**
3
+ * Creates a mock storage instance with tracking
4
+ * @param {object} overrides - Method overrides
5
+ * @returns {object} Mock storage with data tracking
6
+ */
7
+ export function createMockStorage(overrides = {}) {
8
+ const data = new Map();
9
+
10
+ return {
11
+ data,
12
+ exists: spy((key) => Promise.resolve(data.has(key))),
13
+ get: spy((key) => {
14
+ const value = data.get(key);
15
+ if (!value) return Promise.reject(new Error("Not found"));
16
+
17
+ if (key.endsWith(".json")) {
18
+ return Promise.resolve(value ? JSON.parse(value) : {});
19
+ }
20
+ if (key.endsWith(".jsonl")) {
21
+ const lines = value.split("\n").filter(Boolean);
22
+ return Promise.resolve(lines.map((line) => JSON.parse(line)));
23
+ }
24
+ return Promise.resolve(value);
25
+ }),
26
+ put: spy((key, value) => {
27
+ data.set(key, value);
28
+ return Promise.resolve();
29
+ }),
30
+ append: spy((key, value) => {
31
+ const existing = data.get(key) || "";
32
+ data.set(key, existing ? `${existing}\n${value}` : value);
33
+ return Promise.resolve();
34
+ }),
35
+ delete: spy((key) => {
36
+ data.delete(key);
37
+ return Promise.resolve();
38
+ }),
39
+ findByPrefix: spy(() => Promise.resolve([])),
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * MockStorage class for OOP-style usage
46
+ */
47
+ export class MockStorage {
48
+ /**
49
+ * Creates a new MockStorage instance
50
+ */
51
+ constructor() {
52
+ this.data = new Map();
53
+ }
54
+
55
+ /**
56
+ * Gets a value by key
57
+ * @param {string} key - The storage key
58
+ * @returns {Promise<*>} The stored value
59
+ */
60
+ async get(key) {
61
+ const value = this.data.get(key);
62
+ if (!value) throw new Error("Not found");
63
+
64
+ if (key.endsWith(".jsonl")) {
65
+ return value.split("\n").map((line) => JSON.parse(line));
66
+ }
67
+ return value;
68
+ }
69
+
70
+ /**
71
+ * Stores a value by key
72
+ * @param {string} key - The storage key
73
+ * @param {*} value - The value to store
74
+ * @returns {Promise<void>}
75
+ */
76
+ async put(key, value) {
77
+ this.data.set(key, value);
78
+ }
79
+
80
+ /**
81
+ * Alias for put - stores a value by key
82
+ * @param {string} key - The storage key
83
+ * @param {*} value - The value to store
84
+ * @returns {Promise<void>}
85
+ */
86
+ async set(key, value) {
87
+ this.data.set(key, value);
88
+ }
89
+
90
+ /**
91
+ * Appends a value to an existing key
92
+ * @param {string} key - The storage key
93
+ * @param {string} value - The value to append
94
+ * @returns {Promise<void>}
95
+ */
96
+ async append(key, value) {
97
+ const existing = this.data.get(key) || "";
98
+ this.data.set(key, existing ? `${existing}\n${value}` : value);
99
+ }
100
+
101
+ /**
102
+ * Checks if a key exists
103
+ * @param {string} key - The storage key
104
+ * @returns {Promise<boolean>} True if key exists
105
+ */
106
+ async exists(key) {
107
+ return this.data.has(key);
108
+ }
109
+
110
+ /**
111
+ * Deletes a value by key
112
+ * @param {string} key - The storage key
113
+ * @returns {Promise<void>}
114
+ */
115
+ async delete(key) {
116
+ this.data.delete(key);
117
+ }
118
+ }
@@ -0,0 +1,92 @@
1
+ import { spy } from "./spy.js";
2
+
3
+ function asyncIterableOf(str) {
4
+ return {
5
+ async *[Symbol.asyncIterator]() {
6
+ if (str) yield str;
7
+ },
8
+ };
9
+ }
10
+
11
+ /**
12
+ * A captured-chunks stub for a spawned child's writable stdin. Records every
13
+ * `write(chunk)` on `chunks`; `end()`/`destroy()` are no-ops. Mirrors the
14
+ * shape `createDefaultSubprocess().spawn` exposes (the child's `node:stream`
15
+ * Writable) closely enough for a supervisor that pipes into it.
16
+ */
17
+ function createMockStdinSink() {
18
+ const sink = {
19
+ chunks: [],
20
+ write(chunk) {
21
+ sink.chunks.push(typeof chunk === "string" ? chunk : chunk.toString());
22
+ return true;
23
+ },
24
+ end() {},
25
+ destroy() {},
26
+ on() {
27
+ return sink;
28
+ },
29
+ once() {
30
+ return sink;
31
+ },
32
+ };
33
+ return sink;
34
+ }
35
+
36
+ /**
37
+ * Creates a mock subprocess collaborator matching the `Runtime.subprocess`
38
+ * surface. `run(cmd, args, opts)` resolves to `{ stdout, stderr, exitCode }`
39
+ * consulting `responses[cmd]` (default: empty success); `runSync` is its
40
+ * synchronous sibling returning the same shape. `spawn` returns a streaming
41
+ * quad backed by the same responses (its result carries `stdout`/`stderr`
42
+ * AsyncIterables, a captured-chunks `stdin` sink, `exitCode`/`signal` Promises,
43
+ * a `kill(signal)` spy recording on `kills`, and `pid`). All invocations are
44
+ * recorded on `calls`.
45
+ *
46
+ * @param {object} [options]
47
+ * @param {Record<string, {stdout?: string, stderr?: string, exitCode?: number}>} [options.responses]
48
+ * @returns {{run: Function, runSync: Function, spawn: Function, calls: Array<{cmd: string, args: string[], opts: object}>}}
49
+ */
50
+ export function createMockSubprocess({ responses = {} } = {}) {
51
+ const calls = [];
52
+ const resolve = (cmd) => ({
53
+ stdout: "",
54
+ stderr: "",
55
+ exitCode: 0,
56
+ signal: null,
57
+ ...(responses[cmd] ?? {}),
58
+ });
59
+
60
+ const run = spy(async (cmd, args = [], opts = {}) => {
61
+ calls.push({ cmd, args, opts });
62
+ return resolve(cmd);
63
+ });
64
+
65
+ const runSync = spy((cmd, args = [], opts = {}) => {
66
+ calls.push({ cmd, args, opts });
67
+ return resolve(cmd);
68
+ });
69
+
70
+ const spawn = spy((cmd, args = [], opts = {}) => {
71
+ calls.push({ cmd, args, opts });
72
+ const r = resolve(cmd);
73
+ const kills = [];
74
+ return {
75
+ stdout: asyncIterableOf(r.stdout),
76
+ stderr: asyncIterableOf(r.stderr),
77
+ // A captured-chunks writable; `null` only when a response explicitly
78
+ // sets `stdin: null` (matching a child spawned without a stdin pipe).
79
+ stdin: r.stdin === null ? null : createMockStdinSink(),
80
+ exitCode: Promise.resolve(r.exitCode),
81
+ // Terminating signal: `null` (clean exit) unless a response overrides it.
82
+ signal: Promise.resolve(r.signal ?? null),
83
+ kills,
84
+ kill: spy((signal) => {
85
+ kills.push(signal);
86
+ }),
87
+ pid: r.pid ?? 4321,
88
+ };
89
+ });
90
+
91
+ return { run, runSync, spawn, calls };
92
+ }
package/src/runtime.js ADDED
@@ -0,0 +1,29 @@
1
+ import { createMockClock } from "./mock/clock.js";
2
+ import { createMockFs } from "./mock/fs.js";
3
+ import { createMockProcess } from "./mock/infra.js";
4
+ import { createMockSubprocess } from "./mock/subprocess.js";
5
+ import { createMockFinder } from "./mock/finder.js";
6
+
7
+ /**
8
+ * Build a frozen mock runtime bag for tests, matching the production
9
+ * `Runtime` typedef. Every field defaults to its canonical
10
+ * libmock fake and is independently overridable via `overrides`.
11
+ *
12
+ * @param {object} [overrides] - Per-field replacements.
13
+ * @param {object} [overrides.fs] - Async fs surface (default `createMockFs()`).
14
+ * @param {object} [overrides.fsSync] - Sync fs surface (defaults to `fs`).
15
+ * @param {object} [overrides.proc] - Process surface.
16
+ * @param {object} [overrides.clock] - Clock surface.
17
+ * @param {object} [overrides.subprocess] - Subprocess surface.
18
+ * @param {object} [overrides.finder] - Finder collaborator.
19
+ * @returns {Readonly<import('../../libutil/src/runtime.js').Runtime>}
20
+ */
21
+ export function createTestRuntime(overrides = {}) {
22
+ const fs = overrides.fs ?? createMockFs();
23
+ const fsSync = overrides.fsSync ?? fs;
24
+ const proc = overrides.proc ?? createMockProcess();
25
+ const clock = overrides.clock ?? createMockClock();
26
+ const subprocess = overrides.subprocess ?? createMockSubprocess();
27
+ const finder = overrides.finder ?? createMockFinder();
28
+ return Object.freeze({ fs, fsSync, proc, clock, subprocess, finder });
29
+ }