@outfitter/testing 0.1.0 → 0.2.0
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 +27 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +63 -57
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -69,6 +69,33 @@ const user2 = createUser({
|
|
|
69
69
|
|
|
70
70
|
Each call returns a fresh copy, preventing test pollution from shared mutable state.
|
|
71
71
|
|
|
72
|
+
#### Deep Merge Behavior
|
|
73
|
+
|
|
74
|
+
Overrides are deep-merged for nested objects:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const createConfig = createFixture({
|
|
78
|
+
env: {
|
|
79
|
+
region: "us-east-1",
|
|
80
|
+
flags: { beta: false, audit: true },
|
|
81
|
+
},
|
|
82
|
+
retries: 3,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const config = createConfig({
|
|
86
|
+
env: { flags: { beta: true } },
|
|
87
|
+
});
|
|
88
|
+
// Result:
|
|
89
|
+
// {
|
|
90
|
+
// env: { region: "us-east-1", flags: { beta: true, audit: true } },
|
|
91
|
+
// retries: 3
|
|
92
|
+
// }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Notes:
|
|
96
|
+
- Arrays are replaced, not merged.
|
|
97
|
+
- `undefined` override values are ignored (defaults remain).
|
|
98
|
+
|
|
72
99
|
### withTempDir
|
|
73
100
|
|
|
74
101
|
Runs a function with an isolated temporary directory that is automatically cleaned up.
|
package/dist/index.d.ts
CHANGED
|
@@ -95,6 +95,15 @@ interface LoadFixtureOptions {
|
|
|
95
95
|
*/
|
|
96
96
|
declare function loadFixture2<T = string>(name: string, options?: LoadFixtureOptions): T;
|
|
97
97
|
/**
|
|
98
|
+
* @outfitter/testing - CLI Harness
|
|
99
|
+
*
|
|
100
|
+
* Test harness for executing and capturing CLI command output.
|
|
101
|
+
* Provides a simple interface for running CLI commands in tests
|
|
102
|
+
* and capturing their stdout, stderr, and exit code.
|
|
103
|
+
*
|
|
104
|
+
* @packageDocumentation
|
|
105
|
+
*/
|
|
106
|
+
/**
|
|
98
107
|
* Result of a CLI command execution.
|
|
99
108
|
*
|
|
100
109
|
* Contains the captured output streams and exit code from the command.
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
// src/fixtures.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
var cachedRequire;
|
|
3
|
+
function getNodeRequire() {
|
|
4
|
+
if (cachedRequire !== undefined) {
|
|
5
|
+
if (cachedRequire === null) {
|
|
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
|
+
}
|
|
6
35
|
function isPlainObject(value) {
|
|
7
36
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
37
|
}
|
|
@@ -45,11 +74,14 @@ function createFixture(defaults) {
|
|
|
45
74
|
};
|
|
46
75
|
}
|
|
47
76
|
function generateTempDirPath() {
|
|
77
|
+
const { tmpdir } = getNodeOs();
|
|
78
|
+
const { join } = getNodePath();
|
|
48
79
|
const timestamp = Date.now();
|
|
49
80
|
const random = Math.random().toString(36).slice(2, 10);
|
|
50
81
|
return join(tmpdir(), `outfitter-test-${timestamp}-${random}`);
|
|
51
82
|
}
|
|
52
83
|
async function withTempDir(fn) {
|
|
84
|
+
const { mkdir, rm } = getNodeFsPromises();
|
|
53
85
|
const dir = generateTempDirPath();
|
|
54
86
|
await mkdir(dir, { recursive: true });
|
|
55
87
|
try {
|
|
@@ -79,6 +111,8 @@ async function withEnv(vars, fn) {
|
|
|
79
111
|
}
|
|
80
112
|
}
|
|
81
113
|
function loadFixture(name, options) {
|
|
114
|
+
const { readFileSync } = getNodeFs();
|
|
115
|
+
const { extname, join } = getNodePath();
|
|
82
116
|
const baseDir = options?.fixturesDir ?? join(process.cwd(), "__fixtures__");
|
|
83
117
|
const filePath = join(baseDir, name);
|
|
84
118
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -88,59 +122,31 @@ function loadFixture(name, options) {
|
|
|
88
122
|
return content;
|
|
89
123
|
}
|
|
90
124
|
// src/cli-harness.ts
|
|
91
|
-
import { spawn } from "node:child_process";
|
|
92
|
-
import { constants } from "node:os";
|
|
93
125
|
function createCliHarness(command) {
|
|
94
126
|
return {
|
|
95
|
-
run(args) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
});
|
|
101
|
-
child.stdin.end();
|
|
102
|
-
const stdoutChunks = [];
|
|
103
|
-
const stderrChunks = [];
|
|
104
|
-
child.stdout.on("data", (chunk) => {
|
|
105
|
-
stdoutChunks.push(chunk);
|
|
106
|
-
});
|
|
107
|
-
child.stderr.on("data", (chunk) => {
|
|
108
|
-
stderrChunks.push(chunk);
|
|
109
|
-
});
|
|
110
|
-
child.on("error", (err) => {
|
|
111
|
-
reject(err);
|
|
112
|
-
});
|
|
113
|
-
child.on("close", (exitCode, signal) => {
|
|
114
|
-
const decoder = new TextDecoder("utf-8");
|
|
115
|
-
let finalExitCode;
|
|
116
|
-
if (exitCode !== null) {
|
|
117
|
-
finalExitCode = exitCode;
|
|
118
|
-
} else if (signal !== null) {
|
|
119
|
-
const signalNumber = constants.signals[signal];
|
|
120
|
-
finalExitCode = signalNumber !== undefined ? 128 + signalNumber : 1;
|
|
121
|
-
} else {
|
|
122
|
-
finalExitCode = 1;
|
|
123
|
-
}
|
|
124
|
-
resolve({
|
|
125
|
-
stdout: decoder.decode(concatUint8Arrays(stdoutChunks)),
|
|
126
|
-
stderr: decoder.decode(concatUint8Arrays(stderrChunks)),
|
|
127
|
-
exitCode: finalExitCode
|
|
128
|
-
});
|
|
129
|
-
});
|
|
127
|
+
async run(args) {
|
|
128
|
+
const child = Bun.spawn([command, ...args], {
|
|
129
|
+
stdin: "pipe",
|
|
130
|
+
stdout: "pipe",
|
|
131
|
+
stderr: "pipe"
|
|
130
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
|
+
};
|
|
131
147
|
}
|
|
132
148
|
};
|
|
133
149
|
}
|
|
134
|
-
function concatUint8Arrays(chunks) {
|
|
135
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
136
|
-
const result = new Uint8Array(totalLength);
|
|
137
|
-
let offset = 0;
|
|
138
|
-
for (const chunk of chunks) {
|
|
139
|
-
result.set(chunk, offset);
|
|
140
|
-
offset += chunk.length;
|
|
141
|
-
}
|
|
142
|
-
return result;
|
|
143
|
-
}
|
|
144
150
|
// src/cli-helpers.ts
|
|
145
151
|
class ExitError extends Error {
|
|
146
152
|
code;
|
|
@@ -298,22 +304,22 @@ function createTestLoggerWithContext(context, logs) {
|
|
|
298
304
|
clear() {
|
|
299
305
|
logs.length = 0;
|
|
300
306
|
},
|
|
301
|
-
trace(message, metadata) {
|
|
307
|
+
trace: (message, metadata) => {
|
|
302
308
|
write("trace", message, metadata);
|
|
303
309
|
},
|
|
304
|
-
debug(message, metadata) {
|
|
310
|
+
debug: (message, metadata) => {
|
|
305
311
|
write("debug", message, metadata);
|
|
306
312
|
},
|
|
307
|
-
info(message, metadata) {
|
|
313
|
+
info: (message, metadata) => {
|
|
308
314
|
write("info", message, metadata);
|
|
309
315
|
},
|
|
310
|
-
warn(message, metadata) {
|
|
316
|
+
warn: (message, metadata) => {
|
|
311
317
|
write("warn", message, metadata);
|
|
312
318
|
},
|
|
313
|
-
error(message, metadata) {
|
|
319
|
+
error: (message, metadata) => {
|
|
314
320
|
write("error", message, metadata);
|
|
315
321
|
},
|
|
316
|
-
fatal(message, metadata) {
|
|
322
|
+
fatal: (message, metadata) => {
|
|
317
323
|
write("fatal", message, metadata);
|
|
318
324
|
},
|
|
319
325
|
child(childContext) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/testing",
|
|
3
3
|
"description": "Test harnesses, fixtures, and utilities for Outfitter packages",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist"
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"clean": "rm -rf dist"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@outfitter/contracts": "0.
|
|
37
|
-
"@outfitter/mcp": "0.
|
|
36
|
+
"@outfitter/contracts": "0.2.0",
|
|
37
|
+
"@outfitter/mcp": "0.2.0",
|
|
38
38
|
"zod": "^4.3.5"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|