@outfitter/testing 0.2.5 → 0.3.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/dist/cli-helpers.js +4 -91
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/mcp-harness.d.ts +1 -1
- package/dist/mcp-harness.js +3 -0
- package/dist/mock-factories.js +4 -107
- package/dist/shared/@outfitter/testing-136ebq11.d.ts +123 -0
- package/dist/shared/@outfitter/{testing-5gdrv3f5.d.ts → testing-jc0ppq2z.d.ts} +10 -3
- package/dist/shared/@outfitter/testing-s2fyaxm2.d.ts +134 -0
- package/dist/shared/@outfitter/testing-xcd5p1gm.js +111 -0
- package/dist/shared/@outfitter/testing-xsfjh1n3.js +94 -0
- package/dist/test-command.d.ts +3 -0
- package/dist/test-command.js +114 -0
- package/dist/test-tool.d.ts +2 -0
- package/dist/test-tool.js +58 -0
- package/package.json +33 -20
package/dist/cli-helpers.js
CHANGED
|
@@ -1,95 +1,8 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
super(`Process exited with code ${code}`);
|
|
7
|
-
this.code = code;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
async function captureCLI(fn) {
|
|
11
|
-
const stdoutChunks = [];
|
|
12
|
-
const stderrChunks = [];
|
|
13
|
-
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
14
|
-
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
15
|
-
const originalExit = process.exit.bind(process);
|
|
16
|
-
const originalConsoleLog = console.log;
|
|
17
|
-
const originalConsoleError = console.error;
|
|
18
|
-
process.stdout.write = (chunk, _encoding, cb) => {
|
|
19
|
-
stdoutChunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
20
|
-
if (typeof cb === "function")
|
|
21
|
-
cb();
|
|
22
|
-
return true;
|
|
23
|
-
};
|
|
24
|
-
process.stderr.write = (chunk, _encoding, cb) => {
|
|
25
|
-
stderrChunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
26
|
-
if (typeof cb === "function")
|
|
27
|
-
cb();
|
|
28
|
-
return true;
|
|
29
|
-
};
|
|
30
|
-
console.log = (...args) => {
|
|
31
|
-
const line = `${args.map(String).join(" ")}
|
|
32
|
-
`;
|
|
33
|
-
stdoutChunks.push(new TextEncoder().encode(line));
|
|
34
|
-
};
|
|
35
|
-
console.error = (...args) => {
|
|
36
|
-
const line = `${args.map(String).join(" ")}
|
|
37
|
-
`;
|
|
38
|
-
stderrChunks.push(new TextEncoder().encode(line));
|
|
39
|
-
};
|
|
40
|
-
process.exit = (code) => {
|
|
41
|
-
throw new ExitError(code ?? 0);
|
|
42
|
-
};
|
|
43
|
-
let exitCode = 0;
|
|
44
|
-
try {
|
|
45
|
-
await fn();
|
|
46
|
-
} catch (error) {
|
|
47
|
-
if (error instanceof ExitError) {
|
|
48
|
-
exitCode = error.code;
|
|
49
|
-
} else {
|
|
50
|
-
exitCode = 1;
|
|
51
|
-
}
|
|
52
|
-
} finally {
|
|
53
|
-
process.stdout.write = originalStdoutWrite;
|
|
54
|
-
process.stderr.write = originalStderrWrite;
|
|
55
|
-
process.exit = originalExit;
|
|
56
|
-
console.log = originalConsoleLog;
|
|
57
|
-
console.error = originalConsoleError;
|
|
58
|
-
}
|
|
59
|
-
const decoder = new TextDecoder("utf-8");
|
|
60
|
-
return {
|
|
61
|
-
stdout: decoder.decode(concatChunks(stdoutChunks)),
|
|
62
|
-
stderr: decoder.decode(concatChunks(stderrChunks)),
|
|
63
|
-
exitCode
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
function mockStdin(input) {
|
|
67
|
-
const originalStdin = process.stdin;
|
|
68
|
-
const encoded = new TextEncoder().encode(input);
|
|
69
|
-
const mockStream = {
|
|
70
|
-
async* [Symbol.asyncIterator]() {
|
|
71
|
-
yield encoded;
|
|
72
|
-
},
|
|
73
|
-
fd: 0,
|
|
74
|
-
isTTY: false
|
|
75
|
-
};
|
|
76
|
-
process.stdin = mockStream;
|
|
77
|
-
return {
|
|
78
|
-
restore: () => {
|
|
79
|
-
process.stdin = originalStdin;
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function concatChunks(chunks) {
|
|
84
|
-
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
85
|
-
const result = new Uint8Array(totalLength);
|
|
86
|
-
let offset = 0;
|
|
87
|
-
for (const chunk of chunks) {
|
|
88
|
-
result.set(chunk, offset);
|
|
89
|
-
offset += chunk.length;
|
|
90
|
-
}
|
|
91
|
-
return result;
|
|
92
|
-
}
|
|
2
|
+
import {
|
|
3
|
+
captureCLI,
|
|
4
|
+
mockStdin
|
|
5
|
+
} from "./shared/@outfitter/testing-xsfjh1n3.js";
|
|
93
6
|
export {
|
|
94
7
|
mockStdin,
|
|
95
8
|
captureCLI
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { TestCommandOptions, TestCommandResult, getTestContext, testCommand } from "./shared/@outfitter/testing-s2fyaxm2.js";
|
|
1
2
|
import { CliTestResult, captureCLI, mockStdin } from "./shared/@outfitter/testing-fyjbwn80.js";
|
|
3
|
+
import { TestToolOptions, TestToolResult, testTool } from "./shared/@outfitter/testing-136ebq11.js";
|
|
2
4
|
import { createFixture, loadFixture, withEnv, withTempDir } from "./shared/@outfitter/testing-98fsks4n.js";
|
|
3
|
-
import { McpHarness, McpTestHarnessOptions, McpToolResponse, createMCPTestHarness, createMcpHarness } from "./shared/@outfitter/testing-
|
|
5
|
+
import { McpHarness, McpTestHarnessOptions, McpToolResponse, createMCPTestHarness, createMcpHarness } from "./shared/@outfitter/testing-jc0ppq2z.js";
|
|
4
6
|
import { LogEntry, TestLogger, createTestConfig, createTestContext, createTestLogger } from "./shared/@outfitter/testing-jdfrrv33.js";
|
|
5
7
|
import { CliHarness, CliResult, createCliHarness } from "./shared/@outfitter/testing-xaxkt6c9.js";
|
|
6
|
-
export { withTempDir, withEnv, mockStdin, loadFixture, createTestLogger, createTestContext, createTestConfig, createMCPTestHarness as createMcpTestHarness, createMcpHarness, createMCPTestHarness, createFixture, createCliHarness, captureCLI, TestLogger, McpToolResponse, McpTestHarnessOptions, McpHarness, LogEntry, CliTestResult, CliResult, CliHarness };
|
|
8
|
+
export { withTempDir, withEnv, testTool, testCommand, mockStdin, loadFixture, getTestContext, createTestLogger, createTestContext, createTestConfig, createMCPTestHarness as createMcpTestHarness, createMcpHarness, createMCPTestHarness, createFixture, createCliHarness, captureCLI, TestToolResult, TestToolOptions, TestLogger, TestCommandResult, TestCommandOptions, McpToolResponse, McpTestHarnessOptions, McpHarness, LogEntry, CliTestResult, CliResult, CliHarness };
|
package/dist/index.js
CHANGED
|
@@ -2,4 +2,6 @@ export { createFixture, loadFixture, withEnv, withTempDir } from "./fixtures.js"
|
|
|
2
2
|
export { createCliHarness } from "./cli-harness.js";
|
|
3
3
|
export { captureCLI, mockStdin } from "./cli-helpers.js";
|
|
4
4
|
export { createMCPTestHarness, createMcpHarness, createMcpTestHarness } from "./mcp-harness.js";
|
|
5
|
+
export { getTestContext, testCommand } from "./test-command.js";
|
|
6
|
+
export { testTool } from "./test-tool.js";
|
|
5
7
|
export { createTestConfig, createTestContext, createTestLogger } from "./mock-factories.js";
|
package/dist/mcp-harness.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { McpHarness, McpHarnessOptions, McpTestHarnessOptions, McpToolResponse, createMCPTestHarness, createMcpHarness } from "./shared/@outfitter/testing-
|
|
1
|
+
import { McpHarness, McpHarnessOptions, McpTestHarnessOptions, McpToolResponse, createMCPTestHarness, createMcpHarness } from "./shared/@outfitter/testing-jc0ppq2z.js";
|
|
2
2
|
export { createMCPTestHarness as createMcpTestHarness, createMcpHarness, createMCPTestHarness, McpToolResponse, McpTestHarnessOptions, McpHarnessOptions, McpHarness };
|
package/dist/mcp-harness.js
CHANGED
|
@@ -15,6 +15,9 @@ function createMcpHarness(server, options = {}) {
|
|
|
15
15
|
listTools() {
|
|
16
16
|
return server.getTools();
|
|
17
17
|
},
|
|
18
|
+
listResources() {
|
|
19
|
+
return server.getResources();
|
|
20
|
+
},
|
|
18
21
|
searchTools(query) {
|
|
19
22
|
const normalized = query.trim().toLowerCase();
|
|
20
23
|
const tools = server.getTools();
|
package/dist/mock-factories.js
CHANGED
|
@@ -1,112 +1,9 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// packages/testing/src/mock-factories.ts
|
|
3
2
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
}
|
|
3
|
+
createTestConfig,
|
|
4
|
+
createTestContext,
|
|
5
|
+
createTestLogger
|
|
6
|
+
} from "./shared/@outfitter/testing-xcd5p1gm.js";
|
|
110
7
|
export {
|
|
111
8
|
createTestLogger,
|
|
112
9
|
createTestContext,
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { HandlerContext, MCPHint, OutfitterError, Result, ValidationError } from "@outfitter/contracts";
|
|
2
|
+
import { ToolDefinition } from "@outfitter/mcp";
|
|
3
|
+
/**
|
|
4
|
+
* Options for testTool().
|
|
5
|
+
*/
|
|
6
|
+
interface TestToolOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Custom working directory for the handler context.
|
|
9
|
+
* @deprecated Use `context.cwd` instead.
|
|
10
|
+
*/
|
|
11
|
+
readonly cwd?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Custom environment variables for the handler context.
|
|
14
|
+
* @deprecated Use `context.env` instead.
|
|
15
|
+
*/
|
|
16
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
17
|
+
/**
|
|
18
|
+
* Custom request ID for the handler context.
|
|
19
|
+
* @deprecated Use `context.requestId` instead.
|
|
20
|
+
*/
|
|
21
|
+
readonly requestId?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Full HandlerContext overrides.
|
|
24
|
+
*
|
|
25
|
+
* When provided, these values are merged with the default test context.
|
|
26
|
+
* Takes priority over the individual `cwd`, `env`, and `requestId` options.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* await testTool(tool, input, {
|
|
31
|
+
* context: {
|
|
32
|
+
* requestId: "test-req-001",
|
|
33
|
+
* cwd: "/test/dir",
|
|
34
|
+
* logger: createTestLogger(),
|
|
35
|
+
* },
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
readonly context?: Partial<HandlerContext>;
|
|
40
|
+
/**
|
|
41
|
+
* Hint generation function for asserting on hints.
|
|
42
|
+
*
|
|
43
|
+
* When provided, called with the handler's success result and the result
|
|
44
|
+
* is attached to the returned `TestToolResult.hints` for assertion.
|
|
45
|
+
* Returns `MCPHint[]` for MCP tool testing.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const result = await testTool(tool, input, {
|
|
50
|
+
* hints: (result) => [{
|
|
51
|
+
* description: "View details",
|
|
52
|
+
* tool: "get-details",
|
|
53
|
+
* input: { id: result.id },
|
|
54
|
+
* }],
|
|
55
|
+
* });
|
|
56
|
+
* expect(result.hints).toHaveLength(1);
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
readonly hints?: (result: unknown) => MCPHint[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Enhanced result from testTool() with optional hints.
|
|
63
|
+
*
|
|
64
|
+
* Extends the base `Result` with a `hints` field that contains
|
|
65
|
+
* generated hints when a `hints` function was provided in options.
|
|
66
|
+
*/
|
|
67
|
+
type TestToolResult<
|
|
68
|
+
TOutput,
|
|
69
|
+
TError extends OutfitterError
|
|
70
|
+
> = Result<TOutput, TError | InstanceType<typeof ValidationError>> & {
|
|
71
|
+
/**
|
|
72
|
+
* Generated hints from the `hints` option function.
|
|
73
|
+
*
|
|
74
|
+
* Present when `hints` option is provided and the handler succeeds.
|
|
75
|
+
* Undefined when no hints option was given or when hints array is empty.
|
|
76
|
+
*/
|
|
77
|
+
readonly hints?: MCPHint[] | undefined;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Execute an MCP tool definition with given input.
|
|
81
|
+
*
|
|
82
|
+
* Validates input against the tool's Zod schema. If validation fails,
|
|
83
|
+
* returns an `Err<ValidationError>` without invoking the handler.
|
|
84
|
+
* If validation succeeds, invokes the handler exactly once with a
|
|
85
|
+
* test `HandlerContext` and returns the handler's `Result`.
|
|
86
|
+
*
|
|
87
|
+
* ### v0.5 Enhancements
|
|
88
|
+
*
|
|
89
|
+
* - **`context`**: Full `HandlerContext` overrides (not just cwd/env/requestId).
|
|
90
|
+
* Takes priority over individual options.
|
|
91
|
+
* - **`hints`**: Hint generation function for asserting on hints.
|
|
92
|
+
* When provided, the function is called with the handler's success result
|
|
93
|
+
* and the generated hints are attached to `result.hints`.
|
|
94
|
+
*
|
|
95
|
+
* @param tool - An MCP tool definition with inputSchema and handler
|
|
96
|
+
* @param input - Raw input to validate and pass to the handler
|
|
97
|
+
* @param options - Optional context overrides and hint function
|
|
98
|
+
* @returns The handler's Result with optional hints, or Err<ValidationError> on schema failure
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* // Basic usage (backward compatible)
|
|
103
|
+
* const result = await testTool(myTool, { a: 2, b: 3 });
|
|
104
|
+
* expect(result.unwrap().sum).toBe(5);
|
|
105
|
+
*
|
|
106
|
+
* // With full context injection
|
|
107
|
+
* const result = await testTool(myTool, input, {
|
|
108
|
+
* context: { requestId: "test-001", logger: testLogger },
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* // With hints assertion
|
|
112
|
+
* const result = await testTool(myTool, input, {
|
|
113
|
+
* hints: (r) => [{ description: "Next step", tool: "other" }],
|
|
114
|
+
* });
|
|
115
|
+
* expect(result.hints).toHaveLength(1);
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
declare function testTool<
|
|
119
|
+
TInput,
|
|
120
|
+
TOutput,
|
|
121
|
+
TError extends OutfitterError
|
|
122
|
+
>(tool: ToolDefinition<TInput, TOutput, TError>, input: unknown, options?: TestToolOptions): Promise<TestToolResult<TOutput, TError>>;
|
|
123
|
+
export { TestToolOptions, TestToolResult, testTool };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OutfitterError, Result } from "@outfitter/contracts";
|
|
2
|
-
import { McpError, McpServer, SerializedTool, ToolDefinition } from "@outfitter/mcp";
|
|
2
|
+
import { McpError, McpServer, ResourceDefinition, SerializedTool, ToolDefinition } from "@outfitter/mcp";
|
|
3
3
|
/**
|
|
4
4
|
* MCP tool response content.
|
|
5
5
|
* Matches the MCP protocol shape used in the spec.
|
|
@@ -18,9 +18,12 @@ interface McpToolResponse {
|
|
|
18
18
|
interface McpHarness {
|
|
19
19
|
/**
|
|
20
20
|
* Call a tool by name with input parameters.
|
|
21
|
-
* Returns the MCP-
|
|
21
|
+
* Returns the raw handler output (not MCP-wrapped content).
|
|
22
|
+
*
|
|
23
|
+
* Callers who know the handler's return shape can pass a type parameter
|
|
24
|
+
* to avoid manual narrowing: `harness.callTool<MyOutput>("tool", input)`.
|
|
22
25
|
*/
|
|
23
|
-
callTool(name: string, input: Record<string, unknown>): Promise<Result<
|
|
26
|
+
callTool<T = unknown>(name: string, input: Record<string, unknown>): Promise<Result<T, InstanceType<typeof McpError>>>;
|
|
24
27
|
/**
|
|
25
28
|
* List all registered tools with schemas.
|
|
26
29
|
*/
|
|
@@ -34,6 +37,10 @@ interface McpHarness {
|
|
|
34
37
|
*/
|
|
35
38
|
reset(): void;
|
|
36
39
|
/**
|
|
40
|
+
* List all registered resources.
|
|
41
|
+
*/
|
|
42
|
+
listResources(): ResourceDefinition[];
|
|
43
|
+
/**
|
|
37
44
|
* Search tools by name or description (case-insensitive).
|
|
38
45
|
*/
|
|
39
46
|
searchTools(query: string): SerializedTool[];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CliTestResult } from "./testing-fyjbwn80.js";
|
|
2
|
+
import { CLI } from "@outfitter/cli/command";
|
|
3
|
+
import { CommandEnvelope } from "@outfitter/cli/envelope";
|
|
4
|
+
/**
|
|
5
|
+
* Options for testCommand().
|
|
6
|
+
*/
|
|
7
|
+
interface TestCommandOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Environment variables to set during CLI execution.
|
|
10
|
+
* Variables are restored to their original values after execution.
|
|
11
|
+
*/
|
|
12
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
13
|
+
/**
|
|
14
|
+
* Pre-parsed input object to convert to CLI arguments.
|
|
15
|
+
*
|
|
16
|
+
* Each key-value pair is converted to a `--key value` argument:
|
|
17
|
+
* - `string` → `--key value`
|
|
18
|
+
* - `number` → `--key value` (stringified)
|
|
19
|
+
* - `boolean` → `--key` (true) or omitted (false)
|
|
20
|
+
* - `string[]` → repeated `--key value1 --key value2`
|
|
21
|
+
*
|
|
22
|
+
* Input args are appended after the explicit `args` parameter,
|
|
23
|
+
* allowing both positional args and input overrides.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* // Converts to: ["greet", "--name", "World"]
|
|
28
|
+
* await testCommand(cli, ["greet"], { input: { name: "World" } });
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
readonly input?: Readonly<Record<string, unknown>>;
|
|
32
|
+
/**
|
|
33
|
+
* Mock context object to inject into the command execution.
|
|
34
|
+
*
|
|
35
|
+
* When provided, the context is stored in a test injection point
|
|
36
|
+
* and can be retrieved by context factories via `getTestContext()`.
|
|
37
|
+
* The context is cleaned up after execution.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* await testCommand(cli, ["status"], {
|
|
42
|
+
* context: { db: mockDb, config: testConfig },
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
readonly context?: Readonly<Record<string, unknown>>;
|
|
47
|
+
/**
|
|
48
|
+
* Force JSON output mode by setting OUTFITTER_JSON=1.
|
|
49
|
+
*
|
|
50
|
+
* When true, the CLI produces JSON output which is automatically
|
|
51
|
+
* parsed into the `envelope` field of the result.
|
|
52
|
+
*/
|
|
53
|
+
readonly json?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Enhanced CLI test result with optional envelope parsing.
|
|
57
|
+
*
|
|
58
|
+
* Extends the base `CliTestResult` with an `envelope` field that
|
|
59
|
+
* contains the parsed `CommandEnvelope` when JSON output is detected.
|
|
60
|
+
*/
|
|
61
|
+
interface TestCommandResult extends CliTestResult {
|
|
62
|
+
/**
|
|
63
|
+
* Parsed command envelope from JSON output.
|
|
64
|
+
*
|
|
65
|
+
* Present when the command outputs a JSON envelope (either via
|
|
66
|
+
* `runHandler()` with JSON format or when `json: true` is set).
|
|
67
|
+
* Undefined for non-JSON output.
|
|
68
|
+
*/
|
|
69
|
+
readonly envelope?: CommandEnvelope | undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Retrieve the injected test context, if any.
|
|
73
|
+
*
|
|
74
|
+
* Context factories in builder-pattern commands can call this
|
|
75
|
+
* to use the test-provided context instead of constructing a real one.
|
|
76
|
+
*
|
|
77
|
+
* @returns The injected context, or `undefined` if not in a test
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // In your command definition:
|
|
82
|
+
* command("status")
|
|
83
|
+
* .context(async (input) => {
|
|
84
|
+
* // Use test context if available, otherwise create real one
|
|
85
|
+
* const testCtx = getTestContext();
|
|
86
|
+
* if (testCtx) return testCtx;
|
|
87
|
+
* return { db: await connectDb() };
|
|
88
|
+
* })
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function getTestContext<T extends Record<string, unknown> = Record<string, unknown>>(): T | undefined;
|
|
92
|
+
/**
|
|
93
|
+
* Execute a CLI instance with given arguments and capture output.
|
|
94
|
+
*
|
|
95
|
+
* Creates a fresh CLI instance from the same program, configured with
|
|
96
|
+
* `onExit` to capture exit codes cleanly. Wraps `captureCLI()` to
|
|
97
|
+
* intercept stdout, stderr, and process.exit during execution.
|
|
98
|
+
* No side effects leak to real stdout, stderr, or process.exit.
|
|
99
|
+
* Global process state is fully restored after each call.
|
|
100
|
+
*
|
|
101
|
+
* ### v0.5 Enhancements
|
|
102
|
+
*
|
|
103
|
+
* - **`input`**: Pre-parsed input object auto-converted to CLI args.
|
|
104
|
+
* Keys become `--key value` flags, booleans become presence/absence flags.
|
|
105
|
+
* - **`context`**: Mock context object injectable via `getTestContext()`.
|
|
106
|
+
* Context factories can use this to skip real resource construction in tests.
|
|
107
|
+
* - **`json`**: Forces JSON output mode (OUTFITTER_JSON=1) so the result
|
|
108
|
+
* includes a parsed `envelope` field.
|
|
109
|
+
* - **`envelope`**: Parsed `CommandEnvelope` from JSON output, available on
|
|
110
|
+
* the result for structured assertion.
|
|
111
|
+
*
|
|
112
|
+
* @param cli - A CLI instance created by `createCLI()`
|
|
113
|
+
* @param args - Command-line arguments (without the program name prefix)
|
|
114
|
+
* @param options - Optional configuration for the test execution
|
|
115
|
+
* @returns Captured stdout, stderr, exitCode, and optional envelope
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```typescript
|
|
119
|
+
* // Basic usage (backward compatible)
|
|
120
|
+
* const result = await testCommand(cli, ["hello"]);
|
|
121
|
+
* expect(result.stdout).toContain("Hello!");
|
|
122
|
+
*
|
|
123
|
+
* // With pre-parsed input
|
|
124
|
+
* const result = await testCommand(cli, ["greet"], {
|
|
125
|
+
* input: { name: "World" },
|
|
126
|
+
* });
|
|
127
|
+
*
|
|
128
|
+
* // With JSON envelope
|
|
129
|
+
* const result = await testCommand(cli, ["info"], { json: true });
|
|
130
|
+
* expect(result.envelope?.ok).toBe(true);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
declare function testCommand(cli: CLI, args: string[], options?: TestCommandOptions): Promise<TestCommandResult>;
|
|
134
|
+
export { TestCommandOptions, TestCommandResult, getTestContext, testCommand };
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
|
|
111
|
+
export { createTestLogger, createTestConfig, createTestContext };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/testing/src/cli-helpers.ts
|
|
3
|
+
class ExitError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code) {
|
|
6
|
+
super(`Process exited with code ${code}`);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
async function captureCLI(fn) {
|
|
11
|
+
const stdoutChunks = [];
|
|
12
|
+
const stderrChunks = [];
|
|
13
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
14
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
15
|
+
const originalExit = process.exit.bind(process);
|
|
16
|
+
const originalConsoleLog = console.log;
|
|
17
|
+
const originalConsoleError = console.error;
|
|
18
|
+
process.stdout.write = (chunk, _encoding, cb) => {
|
|
19
|
+
stdoutChunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
20
|
+
if (typeof cb === "function")
|
|
21
|
+
cb();
|
|
22
|
+
return true;
|
|
23
|
+
};
|
|
24
|
+
process.stderr.write = (chunk, _encoding, cb) => {
|
|
25
|
+
stderrChunks.push(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk);
|
|
26
|
+
if (typeof cb === "function")
|
|
27
|
+
cb();
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
console.log = (...args) => {
|
|
31
|
+
const line = `${args.map(String).join(" ")}
|
|
32
|
+
`;
|
|
33
|
+
stdoutChunks.push(new TextEncoder().encode(line));
|
|
34
|
+
};
|
|
35
|
+
console.error = (...args) => {
|
|
36
|
+
const line = `${args.map(String).join(" ")}
|
|
37
|
+
`;
|
|
38
|
+
stderrChunks.push(new TextEncoder().encode(line));
|
|
39
|
+
};
|
|
40
|
+
process.exit = (code) => {
|
|
41
|
+
throw new ExitError(code ?? 0);
|
|
42
|
+
};
|
|
43
|
+
let exitCode = 0;
|
|
44
|
+
try {
|
|
45
|
+
await fn();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof ExitError) {
|
|
48
|
+
exitCode = error.code;
|
|
49
|
+
} else {
|
|
50
|
+
exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
} finally {
|
|
53
|
+
process.stdout.write = originalStdoutWrite;
|
|
54
|
+
process.stderr.write = originalStderrWrite;
|
|
55
|
+
process.exit = originalExit;
|
|
56
|
+
console.log = originalConsoleLog;
|
|
57
|
+
console.error = originalConsoleError;
|
|
58
|
+
}
|
|
59
|
+
const decoder = new TextDecoder("utf-8");
|
|
60
|
+
return {
|
|
61
|
+
stdout: decoder.decode(concatChunks(stdoutChunks)),
|
|
62
|
+
stderr: decoder.decode(concatChunks(stderrChunks)),
|
|
63
|
+
exitCode
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function mockStdin(input) {
|
|
67
|
+
const originalStdin = process.stdin;
|
|
68
|
+
const encoded = new TextEncoder().encode(input);
|
|
69
|
+
const mockStream = {
|
|
70
|
+
async* [Symbol.asyncIterator]() {
|
|
71
|
+
yield encoded;
|
|
72
|
+
},
|
|
73
|
+
fd: 0,
|
|
74
|
+
isTTY: false
|
|
75
|
+
};
|
|
76
|
+
process.stdin = mockStream;
|
|
77
|
+
return {
|
|
78
|
+
restore: () => {
|
|
79
|
+
process.stdin = originalStdin;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function concatChunks(chunks) {
|
|
84
|
+
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
|
85
|
+
const result = new Uint8Array(totalLength);
|
|
86
|
+
let offset = 0;
|
|
87
|
+
for (const chunk of chunks) {
|
|
88
|
+
result.set(chunk, offset);
|
|
89
|
+
offset += chunk.length;
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { captureCLI, mockStdin };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
captureCLI
|
|
4
|
+
} from "./shared/@outfitter/testing-xsfjh1n3.js";
|
|
5
|
+
|
|
6
|
+
// packages/testing/src/test-command.ts
|
|
7
|
+
var injectedTestContext;
|
|
8
|
+
var executionChain = Promise.resolve();
|
|
9
|
+
function getTestContext() {
|
|
10
|
+
return injectedTestContext;
|
|
11
|
+
}
|
|
12
|
+
async function withProcessLock(run) {
|
|
13
|
+
const prior = executionChain;
|
|
14
|
+
let release;
|
|
15
|
+
executionChain = new Promise((resolve) => {
|
|
16
|
+
release = resolve;
|
|
17
|
+
});
|
|
18
|
+
await prior;
|
|
19
|
+
try {
|
|
20
|
+
return await run();
|
|
21
|
+
} finally {
|
|
22
|
+
release?.();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function inputToArgs(input) {
|
|
26
|
+
const args = [];
|
|
27
|
+
for (const [key, value] of Object.entries(input)) {
|
|
28
|
+
const kebabKey = key.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
|
|
29
|
+
const flag = `--${kebabKey}`;
|
|
30
|
+
if (typeof value === "boolean") {
|
|
31
|
+
if (value) {
|
|
32
|
+
args.push(flag);
|
|
33
|
+
}
|
|
34
|
+
} else if (Array.isArray(value)) {
|
|
35
|
+
for (const item of value) {
|
|
36
|
+
args.push(flag, String(item));
|
|
37
|
+
}
|
|
38
|
+
} else if (value !== undefined && value !== null) {
|
|
39
|
+
args.push(flag, String(value));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return args;
|
|
43
|
+
}
|
|
44
|
+
function parseEnvelope(stdout, stderr) {
|
|
45
|
+
const stdoutEnvelope = tryParseEnvelope(stdout);
|
|
46
|
+
if (stdoutEnvelope)
|
|
47
|
+
return stdoutEnvelope;
|
|
48
|
+
const stderrEnvelope = tryParseEnvelope(stderr);
|
|
49
|
+
if (stderrEnvelope)
|
|
50
|
+
return stderrEnvelope;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
function tryParseEnvelope(text) {
|
|
54
|
+
const trimmed = text.trim();
|
|
55
|
+
if (!trimmed.startsWith("{"))
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(trimmed);
|
|
59
|
+
if (typeof parsed["ok"] === "boolean" && typeof parsed["command"] === "string") {
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
} catch {}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
async function testCommand(cli, args, options) {
|
|
66
|
+
return withProcessLock(async () => {
|
|
67
|
+
const originalEnv = { ...process.env };
|
|
68
|
+
if (options?.env) {
|
|
69
|
+
for (const [key, value] of Object.entries(options.env)) {
|
|
70
|
+
process.env[key] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (options?.json) {
|
|
74
|
+
process.env["OUTFITTER_JSON"] = "1";
|
|
75
|
+
}
|
|
76
|
+
if (options?.context) {
|
|
77
|
+
injectedTestContext = { ...options.context };
|
|
78
|
+
}
|
|
79
|
+
const inputArgs = options?.input ? inputToArgs(options.input) : [];
|
|
80
|
+
const fullArgs = [...args, ...inputArgs];
|
|
81
|
+
const savedArgv = process.argv;
|
|
82
|
+
const testArgv = ["node", "test", ...fullArgs];
|
|
83
|
+
process.argv = testArgv;
|
|
84
|
+
try {
|
|
85
|
+
const cliResult = await captureCLI(async () => {
|
|
86
|
+
await cli.parse(testArgv);
|
|
87
|
+
});
|
|
88
|
+
const envelope = parseEnvelope(cliResult.stdout, cliResult.stderr);
|
|
89
|
+
return {
|
|
90
|
+
...cliResult,
|
|
91
|
+
envelope
|
|
92
|
+
};
|
|
93
|
+
} finally {
|
|
94
|
+
process.argv = savedArgv;
|
|
95
|
+
for (const key of Object.keys(process.env)) {
|
|
96
|
+
if (!(key in originalEnv)) {
|
|
97
|
+
delete process.env[key];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const [key, value] of Object.entries(originalEnv)) {
|
|
101
|
+
if (value === undefined) {
|
|
102
|
+
delete process.env[key];
|
|
103
|
+
} else {
|
|
104
|
+
process.env[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
injectedTestContext = undefined;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
testCommand,
|
|
113
|
+
getTestContext
|
|
114
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
createTestContext
|
|
4
|
+
} from "./shared/@outfitter/testing-xcd5p1gm.js";
|
|
5
|
+
|
|
6
|
+
// packages/testing/src/test-tool.ts
|
|
7
|
+
import {
|
|
8
|
+
formatZodIssues,
|
|
9
|
+
Result,
|
|
10
|
+
ValidationError
|
|
11
|
+
} from "@outfitter/contracts";
|
|
12
|
+
async function testTool(tool, input, options) {
|
|
13
|
+
const parseResult = tool.inputSchema.safeParse(input);
|
|
14
|
+
if (!parseResult.success) {
|
|
15
|
+
const errorMessages = formatZodIssues(parseResult.error.issues);
|
|
16
|
+
return Result.err(new ValidationError({
|
|
17
|
+
message: `Invalid input: ${errorMessages}`
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
const ctxOverrides = {};
|
|
21
|
+
if (options?.requestId !== undefined) {
|
|
22
|
+
ctxOverrides.requestId = options.requestId;
|
|
23
|
+
}
|
|
24
|
+
if (options?.cwd !== undefined) {
|
|
25
|
+
ctxOverrides.cwd = options.cwd;
|
|
26
|
+
}
|
|
27
|
+
if (options?.env !== undefined) {
|
|
28
|
+
ctxOverrides.env = options.env;
|
|
29
|
+
}
|
|
30
|
+
if (options?.context) {
|
|
31
|
+
Object.assign(ctxOverrides, options.context);
|
|
32
|
+
}
|
|
33
|
+
const ctx = createTestContext(ctxOverrides);
|
|
34
|
+
const result = await tool.handler(parseResult.data, ctx);
|
|
35
|
+
if (options?.hints && result.isOk()) {
|
|
36
|
+
const hints = options.hints(result.value);
|
|
37
|
+
if (hints.length > 0) {
|
|
38
|
+
const enhanced = Object.create(Object.getPrototypeOf(result));
|
|
39
|
+
for (const key of Reflect.ownKeys(result)) {
|
|
40
|
+
const descriptor = Object.getOwnPropertyDescriptor(result, key);
|
|
41
|
+
if (descriptor) {
|
|
42
|
+
Object.defineProperty(enhanced, key, descriptor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
Object.defineProperty(enhanced, "hints", {
|
|
46
|
+
value: hints,
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: false
|
|
50
|
+
});
|
|
51
|
+
return enhanced;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
testTool
|
|
58
|
+
};
|
package/package.json
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outfitter/testing",
|
|
3
|
-
"version": "0.2.5",
|
|
4
3
|
"description": "Test harnesses, fixtures, and utilities for Outfitter packages",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"harness",
|
|
8
|
-
"outfitter",
|
|
9
|
-
"testing",
|
|
10
|
-
"typescript"
|
|
11
|
-
],
|
|
12
|
-
"license": "MIT",
|
|
13
|
-
"repository": {
|
|
14
|
-
"type": "git",
|
|
15
|
-
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
16
|
-
"directory": "packages/testing"
|
|
17
|
-
},
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"type": "module",
|
|
18
6
|
"files": [
|
|
19
7
|
"dist"
|
|
20
8
|
],
|
|
21
|
-
"type": "module",
|
|
22
|
-
"sideEffects": false,
|
|
23
9
|
"module": "./dist/index.js",
|
|
24
10
|
"types": "./dist/index.d.ts",
|
|
25
11
|
"exports": {
|
|
@@ -59,13 +45,39 @@
|
|
|
59
45
|
"default": "./dist/mock-factories.js"
|
|
60
46
|
}
|
|
61
47
|
},
|
|
62
|
-
"./package.json": "./package.json"
|
|
48
|
+
"./package.json": "./package.json",
|
|
49
|
+
"./test-command": {
|
|
50
|
+
"import": {
|
|
51
|
+
"types": "./dist/test-command.d.ts",
|
|
52
|
+
"default": "./dist/test-command.js"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"./test-tool": {
|
|
56
|
+
"import": {
|
|
57
|
+
"types": "./dist/test-tool.d.ts",
|
|
58
|
+
"default": "./dist/test-tool.js"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
63
61
|
},
|
|
62
|
+
"keywords": [
|
|
63
|
+
"fixtures",
|
|
64
|
+
"harness",
|
|
65
|
+
"outfitter",
|
|
66
|
+
"testing",
|
|
67
|
+
"typescript"
|
|
68
|
+
],
|
|
69
|
+
"license": "MIT",
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "https://github.com/outfitter-dev/outfitter.git",
|
|
73
|
+
"directory": "packages/testing"
|
|
74
|
+
},
|
|
75
|
+
"sideEffects": false,
|
|
64
76
|
"publishConfig": {
|
|
65
77
|
"access": "public"
|
|
66
78
|
},
|
|
67
79
|
"scripts": {
|
|
68
|
-
"build": "cd ../.. && bunup --filter @outfitter/testing",
|
|
80
|
+
"build": "cd ../.. && bash ./scripts/run-bunup-with-lock.sh bunup --filter @outfitter/testing",
|
|
69
81
|
"lint": "oxlint ./src",
|
|
70
82
|
"lint:fix": "oxlint --fix ./src",
|
|
71
83
|
"test": "bun test",
|
|
@@ -74,8 +86,9 @@
|
|
|
74
86
|
"prepublishOnly": "bun ../../scripts/check-publish-manifest.ts"
|
|
75
87
|
},
|
|
76
88
|
"dependencies": {
|
|
77
|
-
"@outfitter/
|
|
78
|
-
"@outfitter/
|
|
89
|
+
"@outfitter/cli": "1.0.0",
|
|
90
|
+
"@outfitter/contracts": "0.5.0",
|
|
91
|
+
"@outfitter/mcp": "0.5.0",
|
|
79
92
|
"zod": "^4.3.5"
|
|
80
93
|
},
|
|
81
94
|
"devDependencies": {
|