@emseepea/testing 0.14.0 → 0.15.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 +32 -0
- package/package.json +4 -2
- package/published-contract/cli.mjs +188 -0
package/README.md
CHANGED
|
@@ -163,6 +163,38 @@ Keep the capture version, release policy, baseline location, and retention in
|
|
|
163
163
|
the application repository. Run capture only for a contract that has actually
|
|
164
164
|
been published or submitted for review.
|
|
165
165
|
|
|
166
|
+
For the same comparison without a custom wrapper, add project-local commands:
|
|
167
|
+
|
|
168
|
+
```json
|
|
169
|
+
{
|
|
170
|
+
"scripts": {
|
|
171
|
+
"contract:capture": "emseepea-contract capture --factory ./dist/app.js --factory-export createApp",
|
|
172
|
+
"contract:check": "emseepea-contract check --factory ./dist/app.js --factory-export createApp --baselines contract-baselines"
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Capture a version only when your application policy says it is published:
|
|
178
|
+
|
|
179
|
+
```sh
|
|
180
|
+
npm run contract:capture -- --version 1.0.0 --baseline contract-baselines/1.0.0.json
|
|
181
|
+
npm run contract:check
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Use `--url https://example.com/mcp` instead of `--factory` to inspect an MCP
|
|
185
|
+
endpoint. For protected discovery, set the token in an environment variable
|
|
186
|
+
and pass its name with `--token-env`; the token value is not accepted as a
|
|
187
|
+
command argument or written to output or baselines. Use `--protocol-version`
|
|
188
|
+
when the server requires a supported older protocol revision.
|
|
189
|
+
|
|
190
|
+
Capture exits 0 after writing the requested baseline. Check exits 0 when every
|
|
191
|
+
JSON baseline in the directory is compatible, 1 for compatibility failures,
|
|
192
|
+
and 2 for invalid options, extraction failures, or invalid baseline files.
|
|
193
|
+
|
|
194
|
+
Custom normalization, legacy migration, stricter comparison rules, baseline
|
|
195
|
+
retention, approvals, and deployment policy stay in application code using the
|
|
196
|
+
lower-level API above.
|
|
197
|
+
|
|
166
198
|
## Check Static Color Contrast
|
|
167
199
|
|
|
168
200
|
Use `wcagContrastRatio` to measure one opaque solid sRGB color pair. Use
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "MCP integration and semantic testing helpers",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,10 +30,12 @@
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
|
+
"emseepea-contract": "./published-contract/cli.mjs",
|
|
33
34
|
"emseepea-test": "./semantic/cli.mjs"
|
|
34
35
|
},
|
|
35
36
|
"files": [
|
|
36
37
|
"dist",
|
|
38
|
+
"published-contract",
|
|
37
39
|
"semantic",
|
|
38
40
|
"LICENSE",
|
|
39
41
|
"README.md"
|
|
@@ -45,7 +47,7 @@
|
|
|
45
47
|
"test:built": "node --test test/*.test.mjs"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
|
-
"@emseepea/server": "0.
|
|
50
|
+
"@emseepea/server": "0.14.0",
|
|
49
51
|
"@modelcontextprotocol/client": "2.0.0"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
assertPublishedMcpContractCompatible,
|
|
10
|
+
extractPublishedMcpContract,
|
|
11
|
+
startEmseepea,
|
|
12
|
+
writePublishedMcpContractBaseline,
|
|
13
|
+
} from "../dist/index.js";
|
|
14
|
+
|
|
15
|
+
const protocolVersions = new Set([
|
|
16
|
+
"2026-07-28",
|
|
17
|
+
"2025-11-25",
|
|
18
|
+
"2025-06-18",
|
|
19
|
+
"2025-03-26",
|
|
20
|
+
"2024-11-05",
|
|
21
|
+
"2024-10-07",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
class CommandError extends Error {
|
|
25
|
+
constructor(exitCode, text) {
|
|
26
|
+
super(text);
|
|
27
|
+
this.exitCode = exitCode;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (process.argv.includes("--help")) {
|
|
32
|
+
console.log(usage());
|
|
33
|
+
} else {
|
|
34
|
+
let secret;
|
|
35
|
+
try {
|
|
36
|
+
const options = parse(process.argv.slice(2));
|
|
37
|
+
secret = options.tokenEnv ? process.env[options.tokenEnv] : undefined;
|
|
38
|
+
if (options.tokenEnv && !secret) fail(`Environment variable ${options.tokenEnv} is empty or unset`);
|
|
39
|
+
const current = await currentContract(options, secret);
|
|
40
|
+
if (options.command === "capture") {
|
|
41
|
+
await writePublishedMcpContractBaseline(options.baseline, { version: options.version, contract: current });
|
|
42
|
+
console.log(`Captured published MCP contract ${options.version} at ${options.baseline}`);
|
|
43
|
+
} else {
|
|
44
|
+
const baselines = await loadBaselines(options.baselines);
|
|
45
|
+
try {
|
|
46
|
+
assertPublishedMcpContractCompatible(current, baselines);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new CommandError(1, message(error));
|
|
49
|
+
}
|
|
50
|
+
console.log(`Published MCP contract is compatible with ${baselines.length} baseline(s)`);
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(redact(message(error), secret));
|
|
54
|
+
process.exitCode = error instanceof CommandError ? error.exitCode : 2;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function currentContract(options, token) {
|
|
59
|
+
if (options.factory) {
|
|
60
|
+
const imported = await import(pathToFileURL(resolve(options.factory)).href);
|
|
61
|
+
const factory = imported[options.factoryExport];
|
|
62
|
+
if (typeof factory !== "function") fail(`${options.factory} does not export ${options.factoryExport} as a function`);
|
|
63
|
+
const running = await startEmseepea(await factory(), {
|
|
64
|
+
clientName: "emseepea-contract",
|
|
65
|
+
protocolVersion: options.protocolVersion,
|
|
66
|
+
token,
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
return await extractPublishedMcpContract(await running.connect());
|
|
70
|
+
} finally {
|
|
71
|
+
await running.close();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const client = new Client(
|
|
76
|
+
{ name: "emseepea-contract", version: "0.0.0" },
|
|
77
|
+
options.protocolVersion === "2026-07-28"
|
|
78
|
+
? { versionNegotiation: { mode: { pin: options.protocolVersion } } }
|
|
79
|
+
: { supportedProtocolVersions: [options.protocolVersion], versionNegotiation: { mode: "legacy" } },
|
|
80
|
+
);
|
|
81
|
+
try {
|
|
82
|
+
await client.connect(new StreamableHTTPClientTransport(
|
|
83
|
+
new URL(options.url),
|
|
84
|
+
token ? { authProvider: { token: async () => token } } : undefined,
|
|
85
|
+
));
|
|
86
|
+
return await extractPublishedMcpContract(client);
|
|
87
|
+
} finally {
|
|
88
|
+
await client.close().catch(() => undefined);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function loadBaselines(directory) {
|
|
93
|
+
const entries = (await readdir(directory, { withFileTypes: true }))
|
|
94
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
95
|
+
.map((entry) => entry.name)
|
|
96
|
+
.sort();
|
|
97
|
+
if (entries.length === 0) fail(`No JSON baselines found in ${directory}`);
|
|
98
|
+
const baselines = [];
|
|
99
|
+
for (const name of entries) {
|
|
100
|
+
const file = resolve(directory, name);
|
|
101
|
+
const baseline = JSON.parse(await readFile(file, "utf8"));
|
|
102
|
+
if (!isBaseline(baseline)) fail(`Invalid published MCP contract baseline: ${file}`);
|
|
103
|
+
baselines.push(baseline);
|
|
104
|
+
}
|
|
105
|
+
return baselines;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parse(argv) {
|
|
109
|
+
const command = argv.shift();
|
|
110
|
+
if (command !== "capture" && command !== "check") fail(usage());
|
|
111
|
+
const values = new Map();
|
|
112
|
+
while (argv.length > 0) {
|
|
113
|
+
const option = argv.shift();
|
|
114
|
+
if (!option?.startsWith("--")) fail(`Unexpected argument: ${String(option)}`);
|
|
115
|
+
if (values.has(option)) fail(`Duplicate option: ${option}`);
|
|
116
|
+
const value = argv.shift();
|
|
117
|
+
if (!value || value.startsWith("--")) fail(`Missing value for ${option}`);
|
|
118
|
+
values.set(option, value);
|
|
119
|
+
}
|
|
120
|
+
const known = new Set([
|
|
121
|
+
"--factory", "--factory-export", "--url", "--protocol-version", "--token-env",
|
|
122
|
+
"--baseline", "--baselines", "--version",
|
|
123
|
+
]);
|
|
124
|
+
for (const option of values.keys()) if (!known.has(option)) fail(`Unknown option: ${option}`);
|
|
125
|
+
const factory = values.get("--factory");
|
|
126
|
+
const url = values.get("--url");
|
|
127
|
+
if (Boolean(factory) === Boolean(url)) fail("Supply exactly one of --factory or --url");
|
|
128
|
+
if (!factory && values.has("--factory-export")) fail("--factory-export requires --factory");
|
|
129
|
+
if (url) {
|
|
130
|
+
try {
|
|
131
|
+
new URL(url);
|
|
132
|
+
} catch {
|
|
133
|
+
fail(`Invalid MCP URL: ${url}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const protocolVersion = values.get("--protocol-version") ?? "2026-07-28";
|
|
137
|
+
if (!protocolVersions.has(protocolVersion)) fail(`Unsupported protocol version: ${protocolVersion}`);
|
|
138
|
+
const tokenEnv = values.get("--token-env");
|
|
139
|
+
if (tokenEnv && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(tokenEnv)) fail(`Invalid environment variable name: ${tokenEnv}`);
|
|
140
|
+
|
|
141
|
+
const common = {
|
|
142
|
+
command,
|
|
143
|
+
factory,
|
|
144
|
+
factoryExport: values.get("--factory-export") ?? "createApp",
|
|
145
|
+
url,
|
|
146
|
+
protocolVersion,
|
|
147
|
+
tokenEnv,
|
|
148
|
+
};
|
|
149
|
+
if (command === "capture") {
|
|
150
|
+
const baseline = values.get("--baseline");
|
|
151
|
+
const version = values.get("--version");
|
|
152
|
+
if (!baseline || !version) fail("capture requires --baseline and --version");
|
|
153
|
+
if (values.has("--baselines")) fail("capture does not accept --baselines");
|
|
154
|
+
return { ...common, baseline, version };
|
|
155
|
+
}
|
|
156
|
+
const baselines = values.get("--baselines");
|
|
157
|
+
if (!baselines) fail("check requires --baselines");
|
|
158
|
+
if (values.has("--baseline") || values.has("--version")) fail("check does not accept --baseline or --version");
|
|
159
|
+
return { ...common, baselines };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isBaseline(value) {
|
|
163
|
+
return value !== null && typeof value === "object"
|
|
164
|
+
&& typeof value.version === "string" && value.version.length > 0
|
|
165
|
+
&& value.contract !== null && typeof value.contract === "object"
|
|
166
|
+
&& Array.isArray(value.contract.tools) && Array.isArray(value.contract.resources);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function fail(text) {
|
|
170
|
+
throw new CommandError(2, text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function message(error) {
|
|
174
|
+
return error instanceof Error ? error.message : String(error);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function redact(text, secret) {
|
|
178
|
+
return secret ? text.replaceAll(secret, "[redacted]") : text;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function usage() {
|
|
182
|
+
return [
|
|
183
|
+
"Usage:",
|
|
184
|
+
" emseepea-contract capture (--factory <module> | --url <mcp-url>) --version <label> --baseline <file> [options]",
|
|
185
|
+
" emseepea-contract check (--factory <module> | --url <mcp-url>) --baselines <directory> [options]",
|
|
186
|
+
"Options: --factory-export <name> --protocol-version <version> --token-env <name>",
|
|
187
|
+
].join("\n");
|
|
188
|
+
}
|