@outfitter/testing 0.2.3 → 0.2.5
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/README.md +24 -19
- package/dist/cli-harness.d.ts +2 -0
- package/dist/cli-harness.js +30 -0
- package/dist/cli-helpers.d.ts +2 -0
- package/dist/cli-helpers.js +96 -0
- package/dist/fixtures.d.ts +2 -0
- package/dist/fixtures.js +13 -0
- package/dist/index.d.ts +6 -268
- package/dist/index.js +5 -405
- package/dist/mcp-harness.d.ts +2 -0
- package/dist/mcp-harness.js +50 -0
- package/dist/mock-factories.d.ts +2 -0
- package/dist/mock-factories.js +114 -0
- package/dist/shared/@outfitter/testing-1wd76sr8.js +126 -0
- package/dist/shared/@outfitter/testing-5gdrv3f5.d.ts +66 -0
- package/dist/shared/@outfitter/testing-98fsks4n.d.ts +97 -0
- package/dist/shared/@outfitter/testing-fyjbwn80.d.ts +28 -0
- package/dist/shared/@outfitter/testing-jdfrrv33.d.ts +17 -0
- package/dist/shared/@outfitter/testing-xaxkt6c9.d.ts +64 -0
- package/package.json +28 -27
package/dist/index.js
CHANGED
|
@@ -1,405 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
throw new Error("Node.js built-ins are unavailable in this runtime.");
|
|
7
|
-
}
|
|
8
|
-
return cachedRequire;
|
|
9
|
-
}
|
|
10
|
-
const metaRequire = import.meta.require;
|
|
11
|
-
if (typeof metaRequire === "function") {
|
|
12
|
-
cachedRequire = metaRequire;
|
|
13
|
-
return metaRequire;
|
|
14
|
-
}
|
|
15
|
-
const globalRequire = globalThis.require;
|
|
16
|
-
if (typeof globalRequire === "function") {
|
|
17
|
-
cachedRequire = globalRequire;
|
|
18
|
-
return globalRequire;
|
|
19
|
-
}
|
|
20
|
-
cachedRequire = null;
|
|
21
|
-
throw new Error("Node.js built-ins are unavailable in this runtime.");
|
|
22
|
-
}
|
|
23
|
-
function getNodeFs() {
|
|
24
|
-
return getNodeRequire()("node:fs");
|
|
25
|
-
}
|
|
26
|
-
function getNodeFsPromises() {
|
|
27
|
-
return getNodeRequire()("node:fs/promises");
|
|
28
|
-
}
|
|
29
|
-
function getNodeOs() {
|
|
30
|
-
return getNodeRequire()("node:os");
|
|
31
|
-
}
|
|
32
|
-
function getNodePath() {
|
|
33
|
-
return getNodeRequire()("node:path");
|
|
34
|
-
}
|
|
35
|
-
function isPlainObject(value) {
|
|
36
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
function deepMerge(target, source) {
|
|
39
|
-
const result = { ...target };
|
|
40
|
-
for (const key of Object.keys(source)) {
|
|
41
|
-
const sourceValue = source[key];
|
|
42
|
-
const targetValue = target[key];
|
|
43
|
-
if (sourceValue === undefined) {
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
|
|
47
|
-
result[key] = deepMerge(targetValue, sourceValue);
|
|
48
|
-
} else {
|
|
49
|
-
result[key] = sourceValue;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
function deepClone(obj) {
|
|
55
|
-
if (obj === null || typeof obj !== "object") {
|
|
56
|
-
return obj;
|
|
57
|
-
}
|
|
58
|
-
if (Array.isArray(obj)) {
|
|
59
|
-
return obj.map((item) => deepClone(item));
|
|
60
|
-
}
|
|
61
|
-
const cloned = {};
|
|
62
|
-
for (const key of Object.keys(obj)) {
|
|
63
|
-
cloned[key] = deepClone(obj[key]);
|
|
64
|
-
}
|
|
65
|
-
return cloned;
|
|
66
|
-
}
|
|
67
|
-
function createFixture(defaults) {
|
|
68
|
-
return (overrides) => {
|
|
69
|
-
const cloned = deepClone(defaults);
|
|
70
|
-
if (overrides === undefined) {
|
|
71
|
-
return cloned;
|
|
72
|
-
}
|
|
73
|
-
return deepMerge(cloned, overrides);
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
function generateTempDirPath() {
|
|
77
|
-
const { tmpdir } = getNodeOs();
|
|
78
|
-
const { join } = getNodePath();
|
|
79
|
-
const timestamp = Date.now();
|
|
80
|
-
const random = Math.random().toString(36).slice(2, 10);
|
|
81
|
-
return join(tmpdir(), `outfitter-test-${timestamp}-${random}`);
|
|
82
|
-
}
|
|
83
|
-
async function withTempDir(fn) {
|
|
84
|
-
const { mkdir, rm } = getNodeFsPromises();
|
|
85
|
-
const dir = generateTempDirPath();
|
|
86
|
-
await mkdir(dir, { recursive: true });
|
|
87
|
-
try {
|
|
88
|
-
return await fn(dir);
|
|
89
|
-
} finally {
|
|
90
|
-
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
async function withEnv(vars, fn) {
|
|
94
|
-
const originalValues = new Map;
|
|
95
|
-
for (const key of Object.keys(vars)) {
|
|
96
|
-
originalValues.set(key, process.env[key]);
|
|
97
|
-
}
|
|
98
|
-
for (const [key, value] of Object.entries(vars)) {
|
|
99
|
-
process.env[key] = value;
|
|
100
|
-
}
|
|
101
|
-
try {
|
|
102
|
-
return await fn();
|
|
103
|
-
} finally {
|
|
104
|
-
for (const [key, originalValue] of originalValues) {
|
|
105
|
-
if (originalValue === undefined) {
|
|
106
|
-
delete process.env[key];
|
|
107
|
-
} else {
|
|
108
|
-
process.env[key] = originalValue;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
function loadFixture(name, options) {
|
|
114
|
-
const { readFileSync } = getNodeFs();
|
|
115
|
-
const { extname, join } = getNodePath();
|
|
116
|
-
const baseDir = options?.fixturesDir ?? join(process.cwd(), "__fixtures__");
|
|
117
|
-
const filePath = join(baseDir, name);
|
|
118
|
-
const content = readFileSync(filePath, "utf-8");
|
|
119
|
-
if (extname(filePath) === ".json") {
|
|
120
|
-
return JSON.parse(content);
|
|
121
|
-
}
|
|
122
|
-
return content;
|
|
123
|
-
}
|
|
124
|
-
// src/cli-harness.ts
|
|
125
|
-
function createCliHarness(command) {
|
|
126
|
-
return {
|
|
127
|
-
async run(args) {
|
|
128
|
-
const child = Bun.spawn([command, ...args], {
|
|
129
|
-
stdin: "pipe",
|
|
130
|
-
stdout: "pipe",
|
|
131
|
-
stderr: "pipe"
|
|
132
|
-
});
|
|
133
|
-
child.stdin?.end();
|
|
134
|
-
const stdoutPromise = child.stdout ? new Response(child.stdout).text() : Promise.resolve("");
|
|
135
|
-
const stderrPromise = child.stderr ? new Response(child.stderr).text() : Promise.resolve("");
|
|
136
|
-
const exitCodePromise = child.exited;
|
|
137
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
138
|
-
stdoutPromise,
|
|
139
|
-
stderrPromise,
|
|
140
|
-
exitCodePromise
|
|
141
|
-
]);
|
|
142
|
-
return {
|
|
143
|
-
stdout,
|
|
144
|
-
stderr,
|
|
145
|
-
exitCode: typeof exitCode === "number" ? exitCode : 1
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
// src/cli-helpers.ts
|
|
151
|
-
class ExitError extends Error {
|
|
152
|
-
code;
|
|
153
|
-
constructor(code) {
|
|
154
|
-
super(`Process exited with code ${code}`);
|
|
155
|
-
this.code = code;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async function captureCLI(fn) {
|
|
159
|
-
const stdoutChunks = [];
|
|
160
|
-
const stderrChunks = [];
|
|
161
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
162
|
-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
163
|
-
const originalExit = process.exit.bind(process);
|
|
164
|
-
const originalConsoleLog = console.log;
|
|
165
|
-
const originalConsoleError = console.error;
|
|
166
|
-
process.stdout.write = (chunk, _encoding, cb) => {
|
|
167
|
-
stdoutChunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
168
|
-
if (typeof cb === "function")
|
|
169
|
-
cb();
|
|
170
|
-
return true;
|
|
171
|
-
};
|
|
172
|
-
process.stderr.write = (chunk, _encoding, cb) => {
|
|
173
|
-
stderrChunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
174
|
-
if (typeof cb === "function")
|
|
175
|
-
cb();
|
|
176
|
-
return true;
|
|
177
|
-
};
|
|
178
|
-
console.log = (...args) => {
|
|
179
|
-
const line = `${args.map(String).join(" ")}
|
|
180
|
-
`;
|
|
181
|
-
stdoutChunks.push(new TextEncoder().encode(line));
|
|
182
|
-
};
|
|
183
|
-
console.error = (...args) => {
|
|
184
|
-
const line = `${args.map(String).join(" ")}
|
|
185
|
-
`;
|
|
186
|
-
stderrChunks.push(new TextEncoder().encode(line));
|
|
187
|
-
};
|
|
188
|
-
process.exit = (code) => {
|
|
189
|
-
throw new ExitError(code ?? 0);
|
|
190
|
-
};
|
|
191
|
-
let exitCode = 0;
|
|
192
|
-
try {
|
|
193
|
-
await fn();
|
|
194
|
-
} catch (error) {
|
|
195
|
-
if (error instanceof ExitError) {
|
|
196
|
-
exitCode = error.code;
|
|
197
|
-
} else {
|
|
198
|
-
exitCode = 1;
|
|
199
|
-
}
|
|
200
|
-
} finally {
|
|
201
|
-
process.stdout.write = originalStdoutWrite;
|
|
202
|
-
process.stderr.write = originalStderrWrite;
|
|
203
|
-
process.exit = originalExit;
|
|
204
|
-
console.log = originalConsoleLog;
|
|
205
|
-
console.error = originalConsoleError;
|
|
206
|
-
}
|
|
207
|
-
const decoder = new TextDecoder("utf-8");
|
|
208
|
-
return {
|
|
209
|
-
stdout: decoder.decode(concatChunks(stdoutChunks)),
|
|
210
|
-
stderr: decoder.decode(concatChunks(stderrChunks)),
|
|
211
|
-
exitCode
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
function mockStdin(input) {
|
|
215
|
-
const originalStdin = process.stdin;
|
|
216
|
-
const encoded = new TextEncoder().encode(input);
|
|
217
|
-
const mockStream = {
|
|
218
|
-
async* [Symbol.asyncIterator]() {
|
|
219
|
-
yield encoded;
|
|
220
|
-
},
|
|
221
|
-
fd: 0,
|
|
222
|
-
isTTY: false
|
|
223
|
-
};
|
|
224
|
-
process.stdin = mockStream;
|
|
225
|
-
return {
|
|
226
|
-
restore: () => {
|
|
227
|
-
process.stdin = originalStdin;
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
function concatChunks(chunks) {
|
|
232
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
233
|
-
const result = new Uint8Array(totalLength);
|
|
234
|
-
let offset = 0;
|
|
235
|
-
for (const chunk of chunks) {
|
|
236
|
-
result.set(chunk, offset);
|
|
237
|
-
offset += chunk.length;
|
|
238
|
-
}
|
|
239
|
-
return result;
|
|
240
|
-
}
|
|
241
|
-
// src/mcp-harness.ts
|
|
242
|
-
import {
|
|
243
|
-
createMcpServer
|
|
244
|
-
} from "@outfitter/mcp";
|
|
245
|
-
function createMcpHarness(server, options = {}) {
|
|
246
|
-
return {
|
|
247
|
-
callTool(name, input) {
|
|
248
|
-
return server.invokeTool(name, input);
|
|
249
|
-
},
|
|
250
|
-
listTools() {
|
|
251
|
-
return server.getTools();
|
|
252
|
-
},
|
|
253
|
-
searchTools(query) {
|
|
254
|
-
const normalized = query.trim().toLowerCase();
|
|
255
|
-
const tools = server.getTools();
|
|
256
|
-
if (normalized.length === 0) {
|
|
257
|
-
return tools;
|
|
258
|
-
}
|
|
259
|
-
return tools.filter((tool) => {
|
|
260
|
-
const nameMatch = tool.name.toLowerCase().includes(normalized);
|
|
261
|
-
const descriptionMatch = tool.description.toLowerCase().includes(normalized);
|
|
262
|
-
return nameMatch || descriptionMatch;
|
|
263
|
-
});
|
|
264
|
-
},
|
|
265
|
-
loadFixture(name) {
|
|
266
|
-
return loadFixture(name, options.fixturesDir ? { fixturesDir: options.fixturesDir } : undefined);
|
|
267
|
-
},
|
|
268
|
-
reset() {}
|
|
269
|
-
};
|
|
270
|
-
}
|
|
271
|
-
function createMCPTestHarness(options) {
|
|
272
|
-
const server = createMcpServer({
|
|
273
|
-
name: options.name ?? "mcp-test",
|
|
274
|
-
version: options.version ?? "0.0.0"
|
|
275
|
-
});
|
|
276
|
-
for (const tool of options.tools) {
|
|
277
|
-
server.registerTool(tool);
|
|
278
|
-
}
|
|
279
|
-
return createMcpHarness(server, {
|
|
280
|
-
...options.fixturesDir !== undefined ? { fixturesDir: options.fixturesDir } : {}
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
// src/mock-factories.ts
|
|
284
|
-
import {
|
|
285
|
-
generateRequestId
|
|
286
|
-
} from "@outfitter/contracts";
|
|
287
|
-
function createTestLogger(context = {}) {
|
|
288
|
-
return createTestLoggerWithContext(context, []);
|
|
289
|
-
}
|
|
290
|
-
function createTestLoggerWithContext(context, logs) {
|
|
291
|
-
const write = (level, message, data) => {
|
|
292
|
-
const merged = { ...context, ...data ?? {} };
|
|
293
|
-
const entry = {
|
|
294
|
-
level,
|
|
295
|
-
message
|
|
296
|
-
};
|
|
297
|
-
if (Object.keys(merged).length > 0) {
|
|
298
|
-
entry.data = merged;
|
|
299
|
-
}
|
|
300
|
-
logs.push(entry);
|
|
301
|
-
};
|
|
302
|
-
return {
|
|
303
|
-
logs,
|
|
304
|
-
clear() {
|
|
305
|
-
logs.length = 0;
|
|
306
|
-
},
|
|
307
|
-
trace: (message, metadata) => {
|
|
308
|
-
write("trace", message, metadata);
|
|
309
|
-
},
|
|
310
|
-
debug: (message, metadata) => {
|
|
311
|
-
write("debug", message, metadata);
|
|
312
|
-
},
|
|
313
|
-
info: (message, metadata) => {
|
|
314
|
-
write("info", message, metadata);
|
|
315
|
-
},
|
|
316
|
-
warn: (message, metadata) => {
|
|
317
|
-
write("warn", message, metadata);
|
|
318
|
-
},
|
|
319
|
-
error: (message, metadata) => {
|
|
320
|
-
write("error", message, metadata);
|
|
321
|
-
},
|
|
322
|
-
fatal: (message, metadata) => {
|
|
323
|
-
write("fatal", message, metadata);
|
|
324
|
-
},
|
|
325
|
-
child(childContext) {
|
|
326
|
-
return createTestLoggerWithContext({ ...context, ...childContext }, logs);
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
function createTestConfig(schema, values) {
|
|
331
|
-
const parsed = schema.safeParse(values);
|
|
332
|
-
let data;
|
|
333
|
-
if (parsed.success) {
|
|
334
|
-
data = parsed.data;
|
|
335
|
-
} else {
|
|
336
|
-
const maybePartial = schema.partial;
|
|
337
|
-
if (typeof maybePartial !== "function") {
|
|
338
|
-
throw parsed.error;
|
|
339
|
-
}
|
|
340
|
-
const partialSchema = maybePartial.call(schema);
|
|
341
|
-
const partialParsed = partialSchema.safeParse(values);
|
|
342
|
-
if (!partialParsed.success) {
|
|
343
|
-
throw partialParsed.error;
|
|
344
|
-
}
|
|
345
|
-
data = partialParsed.data;
|
|
346
|
-
}
|
|
347
|
-
return {
|
|
348
|
-
get(key) {
|
|
349
|
-
return getPath(data, key);
|
|
350
|
-
},
|
|
351
|
-
getRequired(key) {
|
|
352
|
-
const value = getPath(data, key);
|
|
353
|
-
if (value === undefined) {
|
|
354
|
-
throw new Error(`Missing required config value: ${key}`);
|
|
355
|
-
}
|
|
356
|
-
return value;
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
function createTestContext(overrides = {}) {
|
|
361
|
-
const logger = overrides.logger ?? createTestLogger();
|
|
362
|
-
const requestId = overrides.requestId ?? generateRequestId();
|
|
363
|
-
const context = {
|
|
364
|
-
requestId,
|
|
365
|
-
logger,
|
|
366
|
-
cwd: overrides.cwd ?? process.cwd(),
|
|
367
|
-
env: overrides.env ?? { ...process.env }
|
|
368
|
-
};
|
|
369
|
-
if (overrides.config !== undefined) {
|
|
370
|
-
context.config = overrides.config;
|
|
371
|
-
}
|
|
372
|
-
if (overrides.signal !== undefined) {
|
|
373
|
-
context.signal = overrides.signal;
|
|
374
|
-
}
|
|
375
|
-
if (overrides.workspaceRoot !== undefined) {
|
|
376
|
-
context.workspaceRoot = overrides.workspaceRoot;
|
|
377
|
-
}
|
|
378
|
-
return context;
|
|
379
|
-
}
|
|
380
|
-
function getPath(obj, key) {
|
|
381
|
-
const parts = key.split(".").filter((part) => part.length > 0);
|
|
382
|
-
let current = obj;
|
|
383
|
-
for (const part of parts) {
|
|
384
|
-
if (current === null || typeof current !== "object") {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
current = current[part];
|
|
388
|
-
}
|
|
389
|
-
return current;
|
|
390
|
-
}
|
|
391
|
-
export {
|
|
392
|
-
withTempDir,
|
|
393
|
-
withEnv,
|
|
394
|
-
mockStdin,
|
|
395
|
-
loadFixture,
|
|
396
|
-
createTestLogger,
|
|
397
|
-
createTestContext,
|
|
398
|
-
createTestConfig,
|
|
399
|
-
createMCPTestHarness as createMcpTestHarness,
|
|
400
|
-
createMcpHarness,
|
|
401
|
-
createMCPTestHarness,
|
|
402
|
-
createFixture,
|
|
403
|
-
createCliHarness,
|
|
404
|
-
captureCLI
|
|
405
|
-
};
|
|
1
|
+
export { createFixture, loadFixture, withEnv, withTempDir } from "./fixtures.js";
|
|
2
|
+
export { createCliHarness } from "./cli-harness.js";
|
|
3
|
+
export { captureCLI, mockStdin } from "./cli-helpers.js";
|
|
4
|
+
export { createMCPTestHarness, createMcpHarness, createMcpTestHarness } from "./mcp-harness.js";
|
|
5
|
+
export { createTestConfig, createTestContext, createTestLogger } from "./mock-factories.js";
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { McpHarness, McpHarnessOptions, McpTestHarnessOptions, McpToolResponse, createMCPTestHarness, createMcpHarness } from "./shared/@outfitter/testing-5gdrv3f5.js";
|
|
2
|
+
export { createMCPTestHarness as createMcpTestHarness, createMcpHarness, createMCPTestHarness, McpToolResponse, McpTestHarnessOptions, McpHarnessOptions, McpHarness };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
loadFixture
|
|
4
|
+
} from "./shared/@outfitter/testing-1wd76sr8.js";
|
|
5
|
+
|
|
6
|
+
// packages/testing/src/mcp-harness.ts
|
|
7
|
+
import {
|
|
8
|
+
createMcpServer
|
|
9
|
+
} from "@outfitter/mcp";
|
|
10
|
+
function createMcpHarness(server, options = {}) {
|
|
11
|
+
return {
|
|
12
|
+
callTool(name, input) {
|
|
13
|
+
return server.invokeTool(name, input);
|
|
14
|
+
},
|
|
15
|
+
listTools() {
|
|
16
|
+
return server.getTools();
|
|
17
|
+
},
|
|
18
|
+
searchTools(query) {
|
|
19
|
+
const normalized = query.trim().toLowerCase();
|
|
20
|
+
const tools = server.getTools();
|
|
21
|
+
if (normalized.length === 0) {
|
|
22
|
+
return tools;
|
|
23
|
+
}
|
|
24
|
+
return tools.filter((tool) => {
|
|
25
|
+
const nameMatch = tool.name.toLowerCase().includes(normalized);
|
|
26
|
+
const descriptionMatch = tool.description.toLowerCase().includes(normalized);
|
|
27
|
+
return nameMatch || descriptionMatch;
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
loadFixture(name) {
|
|
31
|
+
return loadFixture(name, options.fixturesDir ? { fixturesDir: options.fixturesDir } : undefined);
|
|
32
|
+
},
|
|
33
|
+
reset() {}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function createMCPTestHarness(options) {
|
|
37
|
+
const server = createMcpServer({
|
|
38
|
+
name: options.name ?? "mcp-test",
|
|
39
|
+
version: options.version ?? "0.0.0"
|
|
40
|
+
});
|
|
41
|
+
for (const tool of options.tools) {
|
|
42
|
+
server.registerTool(tool);
|
|
43
|
+
}
|
|
44
|
+
return createMcpHarness(server, options.fixturesDir !== undefined ? { fixturesDir: options.fixturesDir } : {});
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
createMCPTestHarness as createMcpTestHarness,
|
|
48
|
+
createMcpHarness,
|
|
49
|
+
createMCPTestHarness
|
|
50
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/testing/src/mock-factories.ts
|
|
3
|
+
import {
|
|
4
|
+
generateRequestId
|
|
5
|
+
} from "@outfitter/contracts";
|
|
6
|
+
function createTestLogger(context = {}) {
|
|
7
|
+
return createTestLoggerWithContext(context, []);
|
|
8
|
+
}
|
|
9
|
+
function createTestLoggerWithContext(context, logs) {
|
|
10
|
+
const write = (level, message, data) => {
|
|
11
|
+
const merged = { ...context, ...data };
|
|
12
|
+
const entry = {
|
|
13
|
+
level,
|
|
14
|
+
message
|
|
15
|
+
};
|
|
16
|
+
if (Object.keys(merged).length > 0) {
|
|
17
|
+
entry.data = merged;
|
|
18
|
+
}
|
|
19
|
+
logs.push(entry);
|
|
20
|
+
};
|
|
21
|
+
return {
|
|
22
|
+
logs,
|
|
23
|
+
clear() {
|
|
24
|
+
logs.length = 0;
|
|
25
|
+
},
|
|
26
|
+
trace: (message, metadata) => {
|
|
27
|
+
write("trace", message, metadata);
|
|
28
|
+
},
|
|
29
|
+
debug: (message, metadata) => {
|
|
30
|
+
write("debug", message, metadata);
|
|
31
|
+
},
|
|
32
|
+
info: (message, metadata) => {
|
|
33
|
+
write("info", message, metadata);
|
|
34
|
+
},
|
|
35
|
+
warn: (message, metadata) => {
|
|
36
|
+
write("warn", message, metadata);
|
|
37
|
+
},
|
|
38
|
+
error: (message, metadata) => {
|
|
39
|
+
write("error", message, metadata);
|
|
40
|
+
},
|
|
41
|
+
fatal: (message, metadata) => {
|
|
42
|
+
write("fatal", message, metadata);
|
|
43
|
+
},
|
|
44
|
+
child(childContext) {
|
|
45
|
+
return createTestLoggerWithContext({ ...context, ...childContext }, logs);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createTestConfig(schema, values) {
|
|
50
|
+
const parsed = schema.safeParse(values);
|
|
51
|
+
let data;
|
|
52
|
+
if (parsed.success) {
|
|
53
|
+
data = parsed.data;
|
|
54
|
+
} else {
|
|
55
|
+
const maybePartial = schema.partial;
|
|
56
|
+
if (typeof maybePartial !== "function") {
|
|
57
|
+
throw parsed.error;
|
|
58
|
+
}
|
|
59
|
+
const partialSchema = maybePartial.call(schema);
|
|
60
|
+
const partialParsed = partialSchema.safeParse(values);
|
|
61
|
+
if (!partialParsed.success) {
|
|
62
|
+
throw partialParsed.error;
|
|
63
|
+
}
|
|
64
|
+
data = partialParsed.data;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
get(key) {
|
|
68
|
+
return getPath(data, key);
|
|
69
|
+
},
|
|
70
|
+
getRequired(key) {
|
|
71
|
+
const value = getPath(data, key);
|
|
72
|
+
if (value === undefined) {
|
|
73
|
+
throw new Error(`Missing required config value: ${key}`);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createTestContext(overrides = {}) {
|
|
80
|
+
const logger = overrides.logger ?? createTestLogger();
|
|
81
|
+
const requestId = overrides.requestId ?? generateRequestId();
|
|
82
|
+
const context = {
|
|
83
|
+
requestId,
|
|
84
|
+
logger,
|
|
85
|
+
cwd: overrides.cwd ?? process.cwd(),
|
|
86
|
+
env: overrides.env ?? { ...process.env }
|
|
87
|
+
};
|
|
88
|
+
if (overrides.config !== undefined) {
|
|
89
|
+
context.config = overrides.config;
|
|
90
|
+
}
|
|
91
|
+
if (overrides.signal !== undefined) {
|
|
92
|
+
context.signal = overrides.signal;
|
|
93
|
+
}
|
|
94
|
+
if (overrides.workspaceRoot !== undefined) {
|
|
95
|
+
context.workspaceRoot = overrides.workspaceRoot;
|
|
96
|
+
}
|
|
97
|
+
return context;
|
|
98
|
+
}
|
|
99
|
+
function getPath(obj, key) {
|
|
100
|
+
const parts = key.split(".").filter((part) => part.length > 0);
|
|
101
|
+
let current = obj;
|
|
102
|
+
for (const part of parts) {
|
|
103
|
+
if (current === null || typeof current !== "object") {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
current = current[part];
|
|
107
|
+
}
|
|
108
|
+
return current;
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
createTestLogger,
|
|
112
|
+
createTestContext,
|
|
113
|
+
createTestConfig
|
|
114
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/testing/src/fixtures.ts
|
|
3
|
+
var cachedRequire;
|
|
4
|
+
function getNodeRequire() {
|
|
5
|
+
if (cachedRequire !== undefined) {
|
|
6
|
+
if (cachedRequire === null) {
|
|
7
|
+
throw new Error("Node.js built-ins are unavailable in this runtime.");
|
|
8
|
+
}
|
|
9
|
+
return cachedRequire;
|
|
10
|
+
}
|
|
11
|
+
const metaRequire = import.meta.require;
|
|
12
|
+
if (typeof metaRequire === "function") {
|
|
13
|
+
cachedRequire = metaRequire;
|
|
14
|
+
return metaRequire;
|
|
15
|
+
}
|
|
16
|
+
const globalRequire = globalThis.require;
|
|
17
|
+
if (typeof globalRequire === "function") {
|
|
18
|
+
cachedRequire = globalRequire;
|
|
19
|
+
return globalRequire;
|
|
20
|
+
}
|
|
21
|
+
cachedRequire = null;
|
|
22
|
+
throw new Error("Node.js built-ins are unavailable in this runtime.");
|
|
23
|
+
}
|
|
24
|
+
function getNodeFs() {
|
|
25
|
+
return getNodeRequire()("node:fs");
|
|
26
|
+
}
|
|
27
|
+
function getNodeFsPromises() {
|
|
28
|
+
return getNodeRequire()("node:fs/promises");
|
|
29
|
+
}
|
|
30
|
+
function getNodeOs() {
|
|
31
|
+
return getNodeRequire()("node:os");
|
|
32
|
+
}
|
|
33
|
+
function getNodePath() {
|
|
34
|
+
return getNodeRequire()("node:path");
|
|
35
|
+
}
|
|
36
|
+
function isPlainObject(value) {
|
|
37
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
function deepMerge(target, source) {
|
|
40
|
+
const result = { ...target };
|
|
41
|
+
for (const key of Object.keys(source)) {
|
|
42
|
+
const sourceValue = source[key];
|
|
43
|
+
const targetValue = target[key];
|
|
44
|
+
if (sourceValue === undefined) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (isPlainObject(sourceValue) && isPlainObject(targetValue)) {
|
|
48
|
+
result[key] = deepMerge(targetValue, sourceValue);
|
|
49
|
+
} else {
|
|
50
|
+
result[key] = sourceValue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function deepClone(obj) {
|
|
56
|
+
if (obj === null || typeof obj !== "object") {
|
|
57
|
+
return obj;
|
|
58
|
+
}
|
|
59
|
+
if (Array.isArray(obj)) {
|
|
60
|
+
return obj.map((item) => deepClone(item));
|
|
61
|
+
}
|
|
62
|
+
const cloned = {};
|
|
63
|
+
for (const key of Object.keys(obj)) {
|
|
64
|
+
cloned[key] = deepClone(obj[key]);
|
|
65
|
+
}
|
|
66
|
+
return cloned;
|
|
67
|
+
}
|
|
68
|
+
function createFixture(defaults) {
|
|
69
|
+
return (overrides) => {
|
|
70
|
+
const cloned = deepClone(defaults);
|
|
71
|
+
if (overrides === undefined) {
|
|
72
|
+
return cloned;
|
|
73
|
+
}
|
|
74
|
+
return deepMerge(cloned, overrides);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function generateTempDirPath() {
|
|
78
|
+
const { tmpdir } = getNodeOs();
|
|
79
|
+
const { join } = getNodePath();
|
|
80
|
+
const timestamp = Date.now();
|
|
81
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
82
|
+
return join(tmpdir(), `outfitter-test-${timestamp}-${random}`);
|
|
83
|
+
}
|
|
84
|
+
async function withTempDir(fn) {
|
|
85
|
+
const { mkdir, rm } = getNodeFsPromises();
|
|
86
|
+
const dir = generateTempDirPath();
|
|
87
|
+
await mkdir(dir, { recursive: true });
|
|
88
|
+
try {
|
|
89
|
+
return await fn(dir);
|
|
90
|
+
} finally {
|
|
91
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function withEnv(vars, fn) {
|
|
95
|
+
const originalValues = new Map;
|
|
96
|
+
for (const key of Object.keys(vars)) {
|
|
97
|
+
originalValues.set(key, process.env[key]);
|
|
98
|
+
}
|
|
99
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
100
|
+
process.env[key] = value;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
return await fn();
|
|
104
|
+
} finally {
|
|
105
|
+
for (const [key, originalValue] of originalValues) {
|
|
106
|
+
if (originalValue === undefined) {
|
|
107
|
+
delete process.env[key];
|
|
108
|
+
} else {
|
|
109
|
+
process.env[key] = originalValue;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function loadFixture(name, options) {
|
|
115
|
+
const { readFileSync } = getNodeFs();
|
|
116
|
+
const { extname, join } = getNodePath();
|
|
117
|
+
const baseDir = options?.fixturesDir ?? join(process.cwd(), "__fixtures__");
|
|
118
|
+
const filePath = join(baseDir, name);
|
|
119
|
+
const content = readFileSync(filePath, "utf-8");
|
|
120
|
+
if (extname(filePath) === ".json") {
|
|
121
|
+
return JSON.parse(content);
|
|
122
|
+
}
|
|
123
|
+
return content;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export { createFixture, withTempDir, withEnv, loadFixture };
|