@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/LICENSE +201 -0
- package/README.md +122 -0
- package/package.json +44 -0
- package/src/fixture/assertions.js +42 -0
- package/src/fixture/cache.js +50 -0
- package/src/fixture/eval.js +146 -0
- package/src/fixture/index.js +9 -0
- package/src/fixture/pathway.js +451 -0
- package/src/fixture/services.js +56 -0
- package/src/index.js +3 -0
- package/src/mock/clients.js +264 -0
- package/src/mock/clock.js +62 -0
- package/src/mock/config.js +45 -0
- package/src/mock/data.js +46 -0
- package/src/mock/environments.js +80 -0
- package/src/mock/finder.js +83 -0
- package/src/mock/fs.js +301 -0
- package/src/mock/gh-client.js +28 -0
- package/src/mock/git-client.js +56 -0
- package/src/mock/grpc.js +94 -0
- package/src/mock/http.js +60 -0
- package/src/mock/index.js +49 -0
- package/src/mock/infra.js +297 -0
- package/src/mock/logger.js +42 -0
- package/src/mock/observer.js +78 -0
- package/src/mock/resource-index.js +95 -0
- package/src/mock/service-callbacks.js +39 -0
- package/src/mock/services.js +79 -0
- package/src/mock/spy.js +44 -0
- package/src/mock/storage.js +118 -0
- package/src/mock/subprocess.js +92 -0
- package/src/runtime.js +29 -0
|
@@ -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
|
+
}
|
package/src/mock/data.js
ADDED
|
@@ -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
|
+
}
|