@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 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
- import { readFileSync } from "node:fs";
3
- import { mkdir, rm } from "node:fs/promises";
4
- import { tmpdir } from "node:os";
5
- import { extname, join } from "node:path";
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
- return new Promise((resolve, reject) => {
97
- const child = spawn(command, args, {
98
- shell: false,
99
- stdio: ["pipe", "pipe", "pipe"]
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.1.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.1.0",
37
- "@outfitter/mcp": "0.1.0",
36
+ "@outfitter/contracts": "0.2.0",
37
+ "@outfitter/mcp": "0.2.0",
38
38
  "zod": "^4.3.5"
39
39
  },
40
40
  "devDependencies": {