@testsmith/api-spector 0.3.4 → 0.3.6
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/bin/cli.js +11 -7
- package/out/main/agents.js +1 -1
- package/out/main/chunks/environments-iM3SUM-4.js +28 -0
- package/out/main/chunks/{handle-C0IQL-Vl.js → handle-BtRMtQJg.js} +3 -1
- package/out/main/chunks/{import-C5Ngq8hk.js → import-CZxL6J96.js} +4 -4
- package/out/main/chunks/{recorder-DFxJgn9c.js → recorder-0Ij921El.js} +1 -1
- package/out/main/chunks/request-collection-CBuPjBwa.js +118 -0
- package/out/main/chunks/{request-collection-8TOVNXE0.js → request-exec-DbNHbA8x.js} +602 -146
- package/out/main/chunks/snapshots-rV5tPwAd.js +1658 -0
- package/out/main/chunks/{soap-handler-B9x_YCtj.js → soap-handler-CqXVreNa.js} +3 -3
- package/out/main/contract.js +508 -91
- package/out/main/index.js +158 -87
- package/out/main/record.js +3 -3
- package/out/main/runner.js +23 -20
- package/out/main/wsdl.js +4 -4
- package/out/preload/index.js +5 -1
- package/out/renderer/assets/index-DWiG0dQo.css +2 -0
- package/out/renderer/assets/{index-BjjOh-E5.js → index-htBErIFG.js} +915 -176
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/readme.md +2 -2
- package/out/main/chunks/auth-builder-CUs9yzOF.js +0 -623
- package/out/main/chunks/snapshots-UFd3XgSS.js +0 -928
- package/out/renderer/assets/index-CjvjKHtF.css +0 -2
package/out/main/contract.js
CHANGED
|
@@ -1,19 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
3
25
|
const promises = require("fs/promises");
|
|
4
26
|
const path = require("path");
|
|
5
|
-
const snapshots = require("./chunks/snapshots-
|
|
27
|
+
const snapshots = require("./chunks/snapshots-rV5tPwAd.js");
|
|
6
28
|
const crypto = require("crypto");
|
|
29
|
+
const undici = require("undici");
|
|
7
30
|
const os = require("os");
|
|
8
31
|
const cliCommon = require("./chunks/cli-common-CDQY1erJ.js");
|
|
9
|
-
require("
|
|
10
|
-
require("./chunks/
|
|
11
|
-
require("
|
|
12
|
-
require("./chunks/handle-C0IQL-Vl.js");
|
|
32
|
+
const environments = require("./chunks/environments-iM3SUM-4.js");
|
|
33
|
+
require("./chunks/request-exec-DbNHbA8x.js");
|
|
34
|
+
require("./chunks/handle-BtRMtQJg.js");
|
|
13
35
|
require("dayjs");
|
|
14
36
|
require("vm");
|
|
15
|
-
require("
|
|
37
|
+
require("tv4");
|
|
38
|
+
require("jsonpath-plus");
|
|
39
|
+
require("@xmldom/xmldom");
|
|
40
|
+
require("http");
|
|
16
41
|
require("ajv");
|
|
42
|
+
require("js-yaml");
|
|
17
43
|
function escapeXml(s) {
|
|
18
44
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
19
45
|
}
|
|
@@ -45,76 +71,6 @@ function toJUnitXml(report, suiteName = "contract") {
|
|
|
45
71
|
];
|
|
46
72
|
return lines.join("\n") + "\n";
|
|
47
73
|
}
|
|
48
|
-
const RESULTS_DIR = "contracts/results";
|
|
49
|
-
function safe(part) {
|
|
50
|
-
return part.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
|
|
51
|
-
}
|
|
52
|
-
function resultPath(dir, pacticipant, version) {
|
|
53
|
-
return path.join(dir, RESULTS_DIR, safe(pacticipant), `${safe(version)}.json`);
|
|
54
|
-
}
|
|
55
|
-
async function recordResult(dir, pacticipant, version, report, now) {
|
|
56
|
-
const file = resultPath(dir, pacticipant, version);
|
|
57
|
-
await promises.mkdir(path.join(dir, RESULTS_DIR, safe(pacticipant)), { recursive: true });
|
|
58
|
-
const record = {
|
|
59
|
-
pacticipant,
|
|
60
|
-
version,
|
|
61
|
-
recordedAt: now,
|
|
62
|
-
passed: report.failed === 0,
|
|
63
|
-
report
|
|
64
|
-
};
|
|
65
|
-
await promises.writeFile(file, JSON.stringify(record, null, 2), "utf8");
|
|
66
|
-
return file;
|
|
67
|
-
}
|
|
68
|
-
async function listResults(dir) {
|
|
69
|
-
const root = path.join(dir, RESULTS_DIR);
|
|
70
|
-
const out = [];
|
|
71
|
-
let pacticipants;
|
|
72
|
-
try {
|
|
73
|
-
pacticipants = await promises.readdir(root);
|
|
74
|
-
} catch {
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
for (const p of pacticipants) {
|
|
78
|
-
let files;
|
|
79
|
-
try {
|
|
80
|
-
files = await promises.readdir(path.join(root, p));
|
|
81
|
-
} catch {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
for (const f of files) {
|
|
85
|
-
if (!f.endsWith(".json")) continue;
|
|
86
|
-
try {
|
|
87
|
-
out.push(JSON.parse(await promises.readFile(path.join(root, p, f), "utf8")));
|
|
88
|
-
} catch {
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
async function canIDeploy(dir, pacticipant, version) {
|
|
95
|
-
const file = resultPath(dir, pacticipant, version);
|
|
96
|
-
let record;
|
|
97
|
-
try {
|
|
98
|
-
record = JSON.parse(await promises.readFile(file, "utf8"));
|
|
99
|
-
} catch {
|
|
100
|
-
return {
|
|
101
|
-
deployable: false,
|
|
102
|
-
reason: `No verification result recorded for ${pacticipant}@${version}. Run \`contract run … --record --pacticipant ${pacticipant} --app-version ${version}\` first.`
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
if (record.passed) {
|
|
106
|
-
return {
|
|
107
|
-
deployable: true,
|
|
108
|
-
reason: `${pacticipant}@${version} passed all ${record.report.total} contract checks (verified ${record.recordedAt}).`,
|
|
109
|
-
record
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
return {
|
|
113
|
-
deployable: false,
|
|
114
|
-
reason: `${pacticipant}@${version} has ${record.report.failed}/${record.report.total} failing contract checks (verified ${record.recordedAt}).`,
|
|
115
|
-
record
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
74
|
function parsePath(path2) {
|
|
119
75
|
const tokens = [];
|
|
120
76
|
const re = /\.([^.[\]]+)|\['([^']*)'\]|\[(\d+|\*)\]/g;
|
|
@@ -142,6 +98,8 @@ function ruleToMatcher(value, rule) {
|
|
|
142
98
|
return { [snapshots.MATCH_KEY]: "decimal", value };
|
|
143
99
|
case "boolean":
|
|
144
100
|
return { [snapshots.MATCH_KEY]: "boolean", value };
|
|
101
|
+
case "null":
|
|
102
|
+
return { [snapshots.MATCH_KEY]: "null", value: null };
|
|
145
103
|
case "datetime":
|
|
146
104
|
case "timestamp":
|
|
147
105
|
return { [snapshots.MATCH_KEY]: "datetime", value, format: rule.format };
|
|
@@ -313,8 +271,14 @@ function exampleToPactBody(node) {
|
|
|
313
271
|
rules[path2] = { matchers: [{ match: "decimal" }] };
|
|
314
272
|
return n.value ?? 0;
|
|
315
273
|
case "boolean":
|
|
316
|
-
rules[path2] = { matchers: [{ match: "
|
|
274
|
+
rules[path2] = { matchers: [{ match: "boolean" }] };
|
|
317
275
|
return n.value ?? false;
|
|
276
|
+
case "string":
|
|
277
|
+
rules[path2] = { matchers: [{ match: "type" }] };
|
|
278
|
+
return n.value ?? "";
|
|
279
|
+
case "null":
|
|
280
|
+
rules[path2] = { matchers: [{ match: "null" }] };
|
|
281
|
+
return null;
|
|
318
282
|
case "datetime":
|
|
319
283
|
case "timestamp":
|
|
320
284
|
rules[path2] = { matchers: [{ match: "datetime", format: n.format }] };
|
|
@@ -391,6 +355,149 @@ function exportPact(consumer, provider, requests) {
|
|
|
391
355
|
}
|
|
392
356
|
};
|
|
393
357
|
}
|
|
358
|
+
const PENDING_FILE = "contracts/pending.json";
|
|
359
|
+
function interactionKey(req) {
|
|
360
|
+
const contractHash = crypto.createHash("sha256").update(JSON.stringify(req.contract ?? {})).digest("hex").slice(0, 16);
|
|
361
|
+
return `${req.id}:${contractHash}`;
|
|
362
|
+
}
|
|
363
|
+
async function loadPendingStore(dir) {
|
|
364
|
+
try {
|
|
365
|
+
return JSON.parse(await promises.readFile(path.join(dir, PENDING_FILE), "utf8"));
|
|
366
|
+
} catch {
|
|
367
|
+
return { firstPassed: {} };
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function savePendingStore(dir, store) {
|
|
371
|
+
const file = path.join(dir, PENDING_FILE);
|
|
372
|
+
await promises.mkdir(path.dirname(file), { recursive: true });
|
|
373
|
+
await promises.writeFile(file, JSON.stringify(store, null, 2), "utf8");
|
|
374
|
+
}
|
|
375
|
+
function applyPendingSemantics(report, requests, store, now) {
|
|
376
|
+
const byId = new Map(requests.map((r) => [r.id, r]));
|
|
377
|
+
const newlyPassed = [];
|
|
378
|
+
let pendingCount = 0;
|
|
379
|
+
for (const result of report.results) {
|
|
380
|
+
const req = byId.get(result.requestId);
|
|
381
|
+
if (!req) continue;
|
|
382
|
+
const key = interactionKey(req);
|
|
383
|
+
if (result.passed) {
|
|
384
|
+
if (!store.firstPassed[key]) {
|
|
385
|
+
store.firstPassed[key] = now;
|
|
386
|
+
newlyPassed.push(key);
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (!store.firstPassed[key]) {
|
|
391
|
+
result.pending = true;
|
|
392
|
+
pendingCount++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (pendingCount > 0) {
|
|
396
|
+
report.failed -= pendingCount;
|
|
397
|
+
report.pending = pendingCount;
|
|
398
|
+
}
|
|
399
|
+
return { newlyPassed };
|
|
400
|
+
}
|
|
401
|
+
const WEBHOOKS_FILE = "contracts/webhooks.json";
|
|
402
|
+
function substituteEnv(value, env) {
|
|
403
|
+
return value.replace(/\$([A-Z_][A-Z0-9_]*)/g, (_, name) => env[name] ?? "");
|
|
404
|
+
}
|
|
405
|
+
async function loadWebhookConfig(dir) {
|
|
406
|
+
let raw;
|
|
407
|
+
try {
|
|
408
|
+
raw = await promises.readFile(path.join(dir, WEBHOOKS_FILE), "utf8");
|
|
409
|
+
} catch {
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
const parsed = JSON.parse(raw);
|
|
413
|
+
const hooks = parsed.webhooks ?? [];
|
|
414
|
+
return hooks.filter((h) => typeof h.url === "string" && h.url.trim().length > 0);
|
|
415
|
+
}
|
|
416
|
+
async function fireWebhooks(hooks, payload, env = process.env, log = console.log) {
|
|
417
|
+
const matching = hooks.filter((h) => !h.events?.length || h.events.includes(payload.event));
|
|
418
|
+
await Promise.all(matching.map(async (hook) => {
|
|
419
|
+
const url = substituteEnv(hook.url, env);
|
|
420
|
+
const headers = { "Content-Type": "application/json" };
|
|
421
|
+
for (const [k, v] of Object.entries(hook.headers ?? {})) headers[k] = substituteEnv(v, env);
|
|
422
|
+
const label = hook.name ?? url;
|
|
423
|
+
try {
|
|
424
|
+
const res = await undici.fetch(url, {
|
|
425
|
+
method: "POST",
|
|
426
|
+
headers,
|
|
427
|
+
body: JSON.stringify(payload),
|
|
428
|
+
signal: AbortSignal.timeout(1e4)
|
|
429
|
+
});
|
|
430
|
+
log(` [webhook] ${payload.event} → ${label}: ${res.status}`);
|
|
431
|
+
} catch (e) {
|
|
432
|
+
log(` [webhook] ${payload.event} → ${label}: failed (${e instanceof Error ? e.message : String(e)})`);
|
|
433
|
+
}
|
|
434
|
+
}));
|
|
435
|
+
}
|
|
436
|
+
function snapshotState(results, envs) {
|
|
437
|
+
const state = { results: {}, deployments: {} };
|
|
438
|
+
for (const r of results) state.results[`${r.pacticipant}@@${r.version}`] = r.recordedAt;
|
|
439
|
+
for (const e of envs) {
|
|
440
|
+
for (const [p, d] of Object.entries(e.deployed)) {
|
|
441
|
+
state.deployments[`${e.name}@@${p}`] = `${d.version}@@${d.recordedAt}`;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return state;
|
|
445
|
+
}
|
|
446
|
+
function diffState(prev, next, results) {
|
|
447
|
+
const events = [];
|
|
448
|
+
for (const [key, recordedAt] of Object.entries(next.results)) {
|
|
449
|
+
if (prev.results[key] === recordedAt) continue;
|
|
450
|
+
const [pacticipant, version] = key.split("@@");
|
|
451
|
+
const rec = results.find((r) => r.pacticipant === pacticipant && r.version === version);
|
|
452
|
+
events.push({
|
|
453
|
+
event: "result-recorded",
|
|
454
|
+
pacticipant,
|
|
455
|
+
version,
|
|
456
|
+
passed: rec?.passed,
|
|
457
|
+
recordedAt
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
for (const [key, value] of Object.entries(next.deployments)) {
|
|
461
|
+
if (prev.deployments[key] === value) continue;
|
|
462
|
+
const [environment, pacticipant] = key.split("@@");
|
|
463
|
+
const [version, recordedAt] = value.split("@@");
|
|
464
|
+
events.push({
|
|
465
|
+
event: "deployment-recorded",
|
|
466
|
+
pacticipant,
|
|
467
|
+
version,
|
|
468
|
+
environment,
|
|
469
|
+
recordedAt
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
return events;
|
|
473
|
+
}
|
|
474
|
+
function watchContractEvents(dir, hooks, intervalMs = 1e4, log = console.log) {
|
|
475
|
+
let prev = null;
|
|
476
|
+
let running = false;
|
|
477
|
+
const tick = async () => {
|
|
478
|
+
if (running) return;
|
|
479
|
+
running = true;
|
|
480
|
+
try {
|
|
481
|
+
const [results, envs] = await Promise.all([snapshots.listResults(dir), snapshots.listEnvironments(dir)]);
|
|
482
|
+
const next = snapshotState(results, envs);
|
|
483
|
+
if (prev !== null) {
|
|
484
|
+
for (const payload of diffState(prev, next, results)) {
|
|
485
|
+
await fireWebhooks(hooks, payload, process.env, log);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
prev = next;
|
|
489
|
+
} catch (e) {
|
|
490
|
+
log(` [webhook] scan failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
491
|
+
} finally {
|
|
492
|
+
running = false;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
void tick();
|
|
496
|
+
const timer = setInterval(() => {
|
|
497
|
+
void tick();
|
|
498
|
+
}, intervalMs);
|
|
499
|
+
return () => clearInterval(timer);
|
|
500
|
+
}
|
|
394
501
|
async function cmdList(args) {
|
|
395
502
|
const wsArg = args["workspace"];
|
|
396
503
|
if (typeof wsArg !== "string") {
|
|
@@ -401,7 +508,7 @@ async function cmdList(args) {
|
|
|
401
508
|
const snapshots$1 = await snapshots.listSnapshots(dir, workspace.contracts ?? []);
|
|
402
509
|
if (snapshots$1.length === 0) {
|
|
403
510
|
console.log(" No contract snapshots. Capture one from the app or via:");
|
|
404
|
-
console.log(" api-spector contract
|
|
511
|
+
console.log(" api-spector contract pin --workspace <path> --spec-url <url>");
|
|
405
512
|
return;
|
|
406
513
|
}
|
|
407
514
|
console.log("");
|
|
@@ -410,7 +517,7 @@ async function cmdList(args) {
|
|
|
410
517
|
for (const { snapshot } of snapshots$1) {
|
|
411
518
|
const id = snapshot.id.slice(0, 8);
|
|
412
519
|
const name = snapshot.name.slice(0, 35).padEnd(35);
|
|
413
|
-
const version = (snapshot.specVersion ?? "
|
|
520
|
+
const version = (snapshot.specVersion ?? "-").slice(0, 11).padEnd(11);
|
|
414
521
|
const when = snapshot.capturedAt.slice(0, 19).replace("T", " ");
|
|
415
522
|
console.log(` ${id} ${name} ${version} ${when}`);
|
|
416
523
|
}
|
|
@@ -418,6 +525,33 @@ async function cmdList(args) {
|
|
|
418
525
|
console.log(" Run against a snapshot:");
|
|
419
526
|
console.log(" api-spector contract run --workspace <path> --mode provider --snapshot <id>");
|
|
420
527
|
}
|
|
528
|
+
async function cmdPin(args) {
|
|
529
|
+
const wsArg = args["workspace"];
|
|
530
|
+
if (typeof wsArg !== "string") {
|
|
531
|
+
console.error(" [error] --workspace <path> is required");
|
|
532
|
+
process.exit(2);
|
|
533
|
+
}
|
|
534
|
+
const specUrl = typeof args["spec-url"] === "string" ? args["spec-url"] : void 0;
|
|
535
|
+
const specPath = typeof args["spec-path"] === "string" ? path.resolve(args["spec-path"]) : void 0;
|
|
536
|
+
if (!specUrl && !specPath) {
|
|
537
|
+
console.error(" [error] --spec-url <url> or --spec-path <file> is required");
|
|
538
|
+
process.exit(2);
|
|
539
|
+
}
|
|
540
|
+
const name = typeof args["name"] === "string" ? args["name"] : void 0;
|
|
541
|
+
const { workspace, dir, file } = await cliCommon.loadWorkspace(wsArg);
|
|
542
|
+
const snapshot = await snapshots.captureSnapshot(dir, { specUrl, specPath, name });
|
|
543
|
+
const relPath = snapshots.relPathOf(snapshot);
|
|
544
|
+
if (relPath && !(workspace.contracts ?? []).includes(relPath)) {
|
|
545
|
+
workspace.contracts = [...workspace.contracts ?? [], relPath];
|
|
546
|
+
await promises.writeFile(file, JSON.stringify(workspace, null, 2), "utf8");
|
|
547
|
+
}
|
|
548
|
+
console.log(` Pinned "${snapshot.name}" (spec version ${snapshot.specVersion ?? "unknown"}, sha256 ${snapshot.sha256.slice(0, 12)}...)`);
|
|
549
|
+
console.log(` ID: ${snapshot.id.slice(0, 8)}`);
|
|
550
|
+
if (relPath) console.log(` File: ${relPath}`);
|
|
551
|
+
console.log("");
|
|
552
|
+
console.log(" Run against it:");
|
|
553
|
+
console.log(` api-spector contract run --workspace ${wsArg} --mode provider --snapshot ${snapshot.id.slice(0, 8)}`);
|
|
554
|
+
}
|
|
421
555
|
async function resolveSnapshot(ws, dir, needle) {
|
|
422
556
|
const all = await snapshots.listSnapshots(dir, ws.contracts ?? []);
|
|
423
557
|
const matches = all.filter(
|
|
@@ -446,7 +580,7 @@ async function cmdRun(args) {
|
|
|
446
580
|
const collections = await cliCommon.loadCollections(workspace, dir, { filterName: collectionName });
|
|
447
581
|
const envs = await cliCommon.loadEnvironments(workspace, dir);
|
|
448
582
|
const envName = typeof args["environment"] === "string" ? args["environment"] : void 0;
|
|
449
|
-
const activeEnv = envName ? envs
|
|
583
|
+
const activeEnv = environments.selectEnvironment(workspace, envs, envName) ?? (envName ? void 0 : envs[0] ? environments.resolveEnvironmentChain(envs[0], envs) : void 0);
|
|
450
584
|
const envVars = {};
|
|
451
585
|
for (const v of activeEnv?.variables ?? []) if (v.enabled) envVars[v.key] = v.value;
|
|
452
586
|
const collectionVars = {};
|
|
@@ -487,7 +621,7 @@ async function cmdRun(args) {
|
|
|
487
621
|
report = await snapshots.runConsumerContracts(contractRequests, envVars, collectionVars);
|
|
488
622
|
break;
|
|
489
623
|
case "provider":
|
|
490
|
-
report = await snapshots.runProviderVerification(allRequests, envVars, specUrl, specPath, requestBaseUrl);
|
|
624
|
+
report = await snapshots.runProviderVerification(allRequests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
491
625
|
break;
|
|
492
626
|
case "provider-live":
|
|
493
627
|
report = await snapshots.runLiveProviderVerification(contractRequests, envVars, collectionVars, providerBaseUrl, stateHandlerUrl);
|
|
@@ -496,13 +630,29 @@ async function cmdRun(args) {
|
|
|
496
630
|
report = await snapshots.runBidirectional(contractRequests, envVars, collectionVars, specUrl, specPath, requestBaseUrl);
|
|
497
631
|
break;
|
|
498
632
|
}
|
|
633
|
+
if (args["allow-pending"]) {
|
|
634
|
+
const store = await loadPendingStore(dir);
|
|
635
|
+
const verified = mode === "provider" ? allRequests : contractRequests;
|
|
636
|
+
applyPendingSemantics(report, verified, store, (/* @__PURE__ */ new Date()).toISOString());
|
|
637
|
+
await savePendingStore(dir, store);
|
|
638
|
+
}
|
|
499
639
|
console.log("");
|
|
500
640
|
if (report.failed === 0) {
|
|
501
|
-
console.log(` ✓ All ${report.passed}/${report.total} passed in ${report.durationMs}ms`);
|
|
641
|
+
console.log(` ✓ All ${report.passed}/${report.total - (report.pending ?? 0)} required passed in ${report.durationMs}ms`);
|
|
502
642
|
} else {
|
|
503
643
|
console.log(` ✗ ${report.failed}/${report.total} failed (${report.passed} passed) in ${report.durationMs}ms`);
|
|
504
644
|
console.log("");
|
|
505
|
-
for (const r of report.results.filter((r2) => !r2.passed)) {
|
|
645
|
+
for (const r of report.results.filter((r2) => !r2.passed && !r2.pending)) {
|
|
646
|
+
console.log(` ${r.method} ${r.requestName}`);
|
|
647
|
+
for (const v of r.violations) {
|
|
648
|
+
console.log(` · ${v.type}: ${v.message}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (report.pending) {
|
|
653
|
+
console.log("");
|
|
654
|
+
console.log(` ⚠ ${report.pending} pending contract${report.pending === 1 ? "" : "s"} failed (never verified before; not blocking):`);
|
|
655
|
+
for (const r of report.results.filter((r2) => r2.pending)) {
|
|
506
656
|
console.log(` ${r.method} ${r.requestName}`);
|
|
507
657
|
for (const v of r.violations) {
|
|
508
658
|
console.log(` · ${v.type}: ${v.message}`);
|
|
@@ -535,7 +685,7 @@ async function cmdRun(args) {
|
|
|
535
685
|
process.exit(2);
|
|
536
686
|
}
|
|
537
687
|
const pacticipant = typeof args["pacticipant"] === "string" ? args["pacticipant"] : collections[0]?.name ?? "app";
|
|
538
|
-
const file = await recordResult(dir, pacticipant, appVersion, report, (/* @__PURE__ */ new Date()).toISOString());
|
|
688
|
+
const file = await snapshots.recordResult(dir, pacticipant, appVersion, report, (/* @__PURE__ */ new Date()).toISOString());
|
|
539
689
|
console.log(` Recorded result for ${pacticipant}@${appVersion} → ${file}`);
|
|
540
690
|
}
|
|
541
691
|
process.exit(report.failed === 0 ? 0 : 1);
|
|
@@ -556,23 +706,278 @@ async function cmdCanIDeploy(args) {
|
|
|
556
706
|
console.error(" [error] --app-version <version> is required");
|
|
557
707
|
process.exit(2);
|
|
558
708
|
}
|
|
709
|
+
const toEnv = typeof args["to"] === "string" ? args["to"] : void 0;
|
|
559
710
|
const { dir } = await cliCommon.loadWorkspace(wsArg);
|
|
560
|
-
const verdict = await canIDeploy(dir, pacticipant, appVersion);
|
|
711
|
+
const verdict = await snapshots.canIDeploy(dir, pacticipant, appVersion, toEnv);
|
|
561
712
|
console.log("");
|
|
562
|
-
console.log(verdict.deployable ? ` ✓ Computer says yes
|
|
713
|
+
console.log(verdict.deployable ? ` ✓ Computer says yes - safe to deploy.` : ` ✗ Computer says no.`);
|
|
563
714
|
console.log(` ${verdict.reason}`);
|
|
715
|
+
if (toEnv) {
|
|
716
|
+
if (verdict.currentlyDeployed) {
|
|
717
|
+
console.log(` ${toEnv} currently runs ${pacticipant}@${verdict.currentlyDeployed.version} (since ${verdict.currentlyDeployed.recordedAt}).`);
|
|
718
|
+
} else {
|
|
719
|
+
console.log(` ${toEnv} has no recorded deployment of ${pacticipant} yet.`);
|
|
720
|
+
}
|
|
721
|
+
if (verdict.deployable) {
|
|
722
|
+
console.log(` After deploying, record it:`);
|
|
723
|
+
console.log(` api-spector contract record-deployment --workspace ${wsArg} --pacticipant ${pacticipant} --app-version ${appVersion} --env ${toEnv}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
564
726
|
process.exit(verdict.deployable ? 0 : 1);
|
|
565
727
|
}
|
|
728
|
+
async function cmdRecordDeployment(args) {
|
|
729
|
+
const wsArg = args["workspace"];
|
|
730
|
+
const pacticipant = args["pacticipant"];
|
|
731
|
+
const appVersion = args["app-version"];
|
|
732
|
+
const env = args["env"];
|
|
733
|
+
if (typeof wsArg !== "string") {
|
|
734
|
+
console.error(" [error] --workspace <path> is required");
|
|
735
|
+
process.exit(2);
|
|
736
|
+
}
|
|
737
|
+
if (typeof pacticipant !== "string") {
|
|
738
|
+
console.error(" [error] --pacticipant <name> is required");
|
|
739
|
+
process.exit(2);
|
|
740
|
+
}
|
|
741
|
+
if (typeof appVersion !== "string") {
|
|
742
|
+
console.error(" [error] --app-version <version> is required");
|
|
743
|
+
process.exit(2);
|
|
744
|
+
}
|
|
745
|
+
if (typeof env !== "string") {
|
|
746
|
+
console.error(" [error] --env <name> is required (e.g. staging, prod)");
|
|
747
|
+
process.exit(2);
|
|
748
|
+
}
|
|
749
|
+
const { dir } = await cliCommon.loadWorkspace(wsArg);
|
|
750
|
+
const verdict = await snapshots.canIDeploy(dir, pacticipant, appVersion);
|
|
751
|
+
const { file, previous } = await snapshots.recordDeployment(dir, env, pacticipant, appVersion, (/* @__PURE__ */ new Date()).toISOString());
|
|
752
|
+
console.log(` Recorded: ${pacticipant}@${appVersion} deployed to ${env}`);
|
|
753
|
+
if (previous) console.log(` Replaces: ${pacticipant}@${previous.version} (deployed ${previous.recordedAt})`);
|
|
754
|
+
console.log(` File: ${file}`);
|
|
755
|
+
if (!verdict.deployable) {
|
|
756
|
+
console.log("");
|
|
757
|
+
console.log(` [warn] ${verdict.reason}`);
|
|
758
|
+
console.log(` [warn] This version was deployed without a passing contract verification.`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async function cmdEnvironments(args) {
|
|
762
|
+
const wsArg = args["workspace"];
|
|
763
|
+
if (typeof wsArg !== "string") {
|
|
764
|
+
console.error(" [error] --workspace <path> is required");
|
|
765
|
+
process.exit(2);
|
|
766
|
+
}
|
|
767
|
+
const { dir } = await cliCommon.loadWorkspace(wsArg);
|
|
768
|
+
const envs = await snapshots.listEnvironments(dir);
|
|
769
|
+
if (envs.length === 0) {
|
|
770
|
+
console.log(" No deployments recorded. After a deploy, run:");
|
|
771
|
+
console.log(" api-spector contract record-deployment --workspace <path> --pacticipant <name> --app-version <ver> --env <name>");
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
for (const e of envs) {
|
|
775
|
+
console.log("");
|
|
776
|
+
console.log(` ${e.name}`);
|
|
777
|
+
for (const [p, d] of Object.entries(e.deployed).sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
778
|
+
console.log(` ${p}@${d.version} since ${d.recordedAt.slice(0, 19).replace("T", " ")}`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
console.log("");
|
|
782
|
+
}
|
|
783
|
+
async function cmdFuzz(args) {
|
|
784
|
+
const wsArg = args["workspace"];
|
|
785
|
+
if (typeof wsArg !== "string") {
|
|
786
|
+
console.error(" [error] --workspace <path> is required");
|
|
787
|
+
process.exit(2);
|
|
788
|
+
}
|
|
789
|
+
const { workspace, dir } = await cliCommon.loadWorkspace(wsArg);
|
|
790
|
+
const collectionName = typeof args["collection"] === "string" ? args["collection"] : void 0;
|
|
791
|
+
const collections = await cliCommon.loadCollections(workspace, dir, { filterName: collectionName });
|
|
792
|
+
const envs = await cliCommon.loadEnvironments(workspace, dir);
|
|
793
|
+
const envName = typeof args["environment"] === "string" ? args["environment"] : void 0;
|
|
794
|
+
const activeEnv = environments.selectEnvironment(workspace, envs, envName) ?? (envName ? void 0 : envs[0] ? environments.resolveEnvironmentChain(envs[0], envs) : void 0);
|
|
795
|
+
const envVars = {};
|
|
796
|
+
for (const v of activeEnv?.variables ?? []) if (v.enabled) envVars[v.key] = v.value;
|
|
797
|
+
const collectionVars = {};
|
|
798
|
+
for (const c of collections) Object.assign(collectionVars, c.collectionVariables ?? {});
|
|
799
|
+
const allRequests = collections.flatMap((c) => Object.values(c.requests));
|
|
800
|
+
let specUrl = typeof args["spec-url"] === "string" ? args["spec-url"] : void 0;
|
|
801
|
+
let specPath = typeof args["spec-path"] === "string" ? args["spec-path"] : void 0;
|
|
802
|
+
if (typeof args["snapshot"] === "string") {
|
|
803
|
+
const { snapshot } = await resolveSnapshot(workspace, dir, args["snapshot"]);
|
|
804
|
+
const tmp = path.join(os.tmpdir(), `api-spector-${crypto.randomUUID()}.${snapshot.format === "yaml" ? "yaml" : "json"}`);
|
|
805
|
+
await promises.writeFile(tmp, snapshot.spec, "utf8");
|
|
806
|
+
specPath = tmp;
|
|
807
|
+
specUrl = void 0;
|
|
808
|
+
}
|
|
809
|
+
const providerBaseUrl = typeof args["provider-base-url"] === "string" ? args["provider-base-url"] : void 0;
|
|
810
|
+
if (!providerBaseUrl && !specUrl && !specPath) {
|
|
811
|
+
console.error(" [error] fuzz needs a target: --provider-base-url <url> (and optionally a spec via --snapshot / --spec-url / --spec-path)");
|
|
812
|
+
process.exit(2);
|
|
813
|
+
}
|
|
814
|
+
const includeWrites = Boolean(args["include-writes"]);
|
|
815
|
+
console.log(` Fuzzing ${allRequests.length} request(s)${specUrl || specPath ? " against the spec" : " from request bodies"}...`);
|
|
816
|
+
if (!includeWrites) console.log(" Write methods (POST/PUT/PATCH/DELETE) are skipped. Add --include-writes to fuzz them (sends malformed writes; target staging or a mock).");
|
|
817
|
+
const report = await snapshots.runFuzz({
|
|
818
|
+
requests: allRequests,
|
|
819
|
+
envVars,
|
|
820
|
+
collectionVars,
|
|
821
|
+
specUrl,
|
|
822
|
+
specPath,
|
|
823
|
+
providerBaseUrl,
|
|
824
|
+
requestBaseUrl: typeof args["request-base-url"] === "string" ? args["request-base-url"] : void 0,
|
|
825
|
+
casesPerOperation: typeof args["cases"] === "string" ? Math.max(1, Number(args["cases"])) : void 0,
|
|
826
|
+
seed: typeof args["seed"] === "string" ? Number(args["seed"]) : void 0,
|
|
827
|
+
includeWrites,
|
|
828
|
+
strictStatus: Boolean(args["strict-status"]),
|
|
829
|
+
checkResponses: Boolean(args["check-responses"]),
|
|
830
|
+
trace: Boolean(args["trace"])
|
|
831
|
+
});
|
|
832
|
+
console.log("");
|
|
833
|
+
if (report.totalFindings === 0) {
|
|
834
|
+
console.log(` ✓ No findings across ${report.totalCases} malformed cases (seed ${report.seed}, ${report.durationMs}ms)`);
|
|
835
|
+
} else {
|
|
836
|
+
console.log(` ✗ ${report.totalFindings} finding(s) across ${report.totalCases} cases (seed ${report.seed}, ${report.durationMs}ms)`);
|
|
837
|
+
for (const op of report.results.filter((r) => r.findings.length > 0)) {
|
|
838
|
+
console.log("");
|
|
839
|
+
console.log(` ${op.method} ${op.requestName} (${op.findings.length}/${op.cases})`);
|
|
840
|
+
for (const f of op.findings) {
|
|
841
|
+
console.log(` · [${f.oracle}] HTTP ${f.status} on ${f.mutation.target} (${f.mutation.kind}): ${f.message}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (report.skippedWrites) console.log(`
|
|
846
|
+
${report.skippedWrites} write-method request(s) skipped (use --include-writes).`);
|
|
847
|
+
if (report.skippedNoBody) console.log(` ${report.skippedNoBody} request(s) had no body to fuzz.`);
|
|
848
|
+
if (args["trace"]) {
|
|
849
|
+
for (const op of report.results) {
|
|
850
|
+
if (!op.trace?.length) continue;
|
|
851
|
+
console.log("");
|
|
852
|
+
console.log(` ${op.method} ${op.requestName} - ${op.trace.length} cases sent:`);
|
|
853
|
+
for (const t of op.trace) {
|
|
854
|
+
const mark = t.finding ? "✗" : "·";
|
|
855
|
+
console.log(` ${mark} HTTP ${t.status} ${t.mutation.target} (${t.mutation.kind})`);
|
|
856
|
+
console.log(` req: ${(t.request.body ?? "").slice(0, 200)}`);
|
|
857
|
+
console.log(` resp: ${(t.responseSample ?? "").replace(/\s+/g, " ").slice(0, 200)}`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (typeof args["output"] === "string") {
|
|
862
|
+
await promises.writeFile(args["output"], JSON.stringify(report, null, 2), "utf8");
|
|
863
|
+
console.log(`
|
|
864
|
+
Report written to ${args["output"]}`);
|
|
865
|
+
}
|
|
866
|
+
if (typeof args["html"] === "string") {
|
|
867
|
+
await promises.writeFile(args["html"], snapshots.fuzzReportToHtml(report, (/* @__PURE__ */ new Date()).toISOString()), "utf8");
|
|
868
|
+
console.log(` HTML report written to ${args["html"]}`);
|
|
869
|
+
}
|
|
870
|
+
process.exit(report.totalFindings === 0 ? 0 : 1);
|
|
871
|
+
}
|
|
872
|
+
async function cmdWebhooks(args) {
|
|
873
|
+
const wsArg = args["workspace"];
|
|
874
|
+
if (typeof wsArg !== "string") {
|
|
875
|
+
console.error(" [error] --workspace <path> is required");
|
|
876
|
+
process.exit(2);
|
|
877
|
+
}
|
|
878
|
+
const { dir } = await cliCommon.loadWorkspace(wsArg);
|
|
879
|
+
const hooks = await loadWebhookConfig(dir);
|
|
880
|
+
if (hooks.length === 0) {
|
|
881
|
+
console.log(" No webhooks configured. Create contracts/webhooks.json in the workspace:");
|
|
882
|
+
console.log("");
|
|
883
|
+
console.log(" {");
|
|
884
|
+
console.log(' "webhooks": [');
|
|
885
|
+
console.log(" {");
|
|
886
|
+
console.log(' "name": "trigger provider CI",');
|
|
887
|
+
console.log(' "url": "https://ci.example.com/api/trigger",');
|
|
888
|
+
console.log(' "events": ["result-recorded", "deployment-recorded"],');
|
|
889
|
+
console.log(' "headers": { "Authorization": "Bearer $CI_TOKEN" }');
|
|
890
|
+
console.log(" }");
|
|
891
|
+
console.log(" ]");
|
|
892
|
+
console.log(" }");
|
|
893
|
+
console.log("");
|
|
894
|
+
console.log(" $NAME tokens are replaced from the serving process environment.");
|
|
895
|
+
console.log(" The dashboard (`contract report --serve`) fires them when new results appear.");
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
console.log("");
|
|
899
|
+
for (const h of hooks) {
|
|
900
|
+
console.log(` ${h.name ?? "(unnamed)"}`);
|
|
901
|
+
console.log(` url: ${h.url}`);
|
|
902
|
+
console.log(` events: ${h.events?.length ? h.events.join(", ") : "all"}`);
|
|
903
|
+
}
|
|
904
|
+
console.log("");
|
|
905
|
+
if (args["test"]) {
|
|
906
|
+
console.log(" Sending test event...");
|
|
907
|
+
await fireWebhooks(hooks, {
|
|
908
|
+
event: "result-recorded",
|
|
909
|
+
pacticipant: "webhook-test",
|
|
910
|
+
version: "0.0.0",
|
|
911
|
+
passed: true,
|
|
912
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
566
916
|
async function cmdReport(args) {
|
|
567
917
|
const wsArg = args["workspace"];
|
|
568
918
|
if (typeof wsArg !== "string") {
|
|
569
919
|
console.error(" [error] --workspace <path> is required");
|
|
570
920
|
process.exit(2);
|
|
571
921
|
}
|
|
572
|
-
const out = typeof args["html"] === "string" ? args["html"] : "contract-dashboard.html";
|
|
573
922
|
const { dir } = await cliCommon.loadWorkspace(wsArg);
|
|
574
|
-
|
|
575
|
-
|
|
923
|
+
if (args["serve"]) {
|
|
924
|
+
const port = typeof args["port"] === "string" ? Number(args["port"]) : 8080;
|
|
925
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
926
|
+
console.error(" [error] --port must be a number between 1 and 65535");
|
|
927
|
+
process.exit(2);
|
|
928
|
+
}
|
|
929
|
+
const { createServer } = await import("http");
|
|
930
|
+
const server = createServer(async (req, res) => {
|
|
931
|
+
try {
|
|
932
|
+
const url = req.url ?? "/";
|
|
933
|
+
if (url === "/healthz") {
|
|
934
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
935
|
+
res.end("ok");
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const records2 = await snapshots.listResults(dir);
|
|
939
|
+
const runMatch = /^\/run\/([^/]+)\/([^/]+)$/.exec(url);
|
|
940
|
+
if (runMatch) {
|
|
941
|
+
const pacticipant = decodeURIComponent(runMatch[1]);
|
|
942
|
+
const version = decodeURIComponent(runMatch[2]);
|
|
943
|
+
const rec = records2.find((r) => r.pacticipant === pacticipant && r.version === version);
|
|
944
|
+
if (!rec) {
|
|
945
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
946
|
+
res.end("No recorded result for that pacticipant/version");
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
950
|
+
res.end(snapshots.reportToHtml(rec.report, {
|
|
951
|
+
title: `${pacticipant} @ ${version}`,
|
|
952
|
+
generatedAt: rec.recordedAt
|
|
953
|
+
}));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const environments22 = await snapshots.listEnvironments(dir);
|
|
957
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
958
|
+
res.end(snapshots.dashboardToHtml(records2, (/* @__PURE__ */ new Date()).toISOString(), { runLinkBase: "/run", environments: environments22 }));
|
|
959
|
+
} catch (e) {
|
|
960
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
961
|
+
res.end(e instanceof Error ? e.message : String(e));
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
server.listen(port, () => {
|
|
965
|
+
console.log(` Contract dashboard serving at http://localhost:${port}`);
|
|
966
|
+
console.log(` Workspace: ${wsArg} (results re-read on every request)`);
|
|
967
|
+
console.log(" Read-only: record new results via `contract run --record`, then refresh.");
|
|
968
|
+
});
|
|
969
|
+
const hooks = await loadWebhookConfig(dir);
|
|
970
|
+
if (hooks.length > 0) {
|
|
971
|
+
const intervalMs = typeof args["webhook-interval"] === "string" ? Math.max(2, Number(args["webhook-interval"])) * 1e3 : 1e4;
|
|
972
|
+
watchContractEvents(dir, hooks, intervalMs);
|
|
973
|
+
console.log(` Webhooks: ${hooks.length} configured (polling every ${intervalMs / 1e3}s)`);
|
|
974
|
+
}
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const out = typeof args["html"] === "string" ? args["html"] : "contract-dashboard.html";
|
|
978
|
+
const records = await snapshots.listResults(dir);
|
|
979
|
+
const environments2 = await snapshots.listEnvironments(dir);
|
|
980
|
+
await promises.writeFile(out, snapshots.dashboardToHtml(records, (/* @__PURE__ */ new Date()).toISOString(), { environments: environments2 }), "utf8");
|
|
576
981
|
console.log(` Dashboard with ${records.length} recorded result(s) written to ${out}`);
|
|
577
982
|
}
|
|
578
983
|
async function cmdPactImport(args) {
|
|
@@ -620,17 +1025,27 @@ async function main() {
|
|
|
620
1025
|
const [, , sub, ...rest] = process.argv;
|
|
621
1026
|
const args = cliCommon.parseArgs(rest);
|
|
622
1027
|
if (sub === "list") return cmdList(args);
|
|
1028
|
+
if (sub === "pin") return cmdPin(args);
|
|
623
1029
|
if (sub === "run") return cmdRun(args);
|
|
624
1030
|
if (sub === "can-i-deploy") return cmdCanIDeploy(args);
|
|
1031
|
+
if (sub === "record-deployment") return cmdRecordDeployment(args);
|
|
1032
|
+
if (sub === "environments") return cmdEnvironments(args);
|
|
1033
|
+
if (sub === "webhooks") return cmdWebhooks(args);
|
|
1034
|
+
if (sub === "fuzz") return cmdFuzz(args);
|
|
625
1035
|
if (sub === "report") return cmdReport(args);
|
|
626
1036
|
if (sub === "pact-import") return cmdPactImport(args);
|
|
627
1037
|
if (sub === "pact-export") return cmdPactExport(args);
|
|
628
1038
|
if (args["help"] || !sub) {
|
|
629
1039
|
console.log(`
|
|
630
1040
|
api-spector contract list --workspace <path>
|
|
1041
|
+
api-spector contract pin --workspace <path> --spec-url <url> | --spec-path <file> [--name <label>]
|
|
631
1042
|
api-spector contract run --workspace <path> --mode <consumer|provider|provider-live|bidirectional> [options]
|
|
632
|
-
api-spector contract report --workspace <path> [--html <path>]
|
|
633
|
-
api-spector contract can-i-deploy --workspace <path> --pacticipant <name> --app-version <ver>
|
|
1043
|
+
api-spector contract report --workspace <path> [--html <path>] [--serve [--port <n>]]
|
|
1044
|
+
api-spector contract can-i-deploy --workspace <path> --pacticipant <name> --app-version <ver> [--to <env>]
|
|
1045
|
+
api-spector contract record-deployment --workspace <path> --pacticipant <name> --app-version <ver> --env <name>
|
|
1046
|
+
api-spector contract environments --workspace <path>
|
|
1047
|
+
api-spector contract webhooks --workspace <path> [--test]
|
|
1048
|
+
api-spector contract fuzz --workspace <path> --provider-base-url <url> [--snapshot <id> | --spec-url <url>] [--cases <n>] [--seed <n>] [--include-writes] [--trace] [--html <path>]
|
|
634
1049
|
api-spector contract pact-import --file <pact.json> [--out <collection.json>]
|
|
635
1050
|
api-spector contract pact-export --workspace <path> --out <pact.json> [--consumer <name> --provider <name> --collection <name>]
|
|
636
1051
|
|
|
@@ -656,6 +1071,8 @@ async function main() {
|
|
|
656
1071
|
--record Record the result for can-i-deploy gating
|
|
657
1072
|
--pacticipant <name> Name to record under (default: collection name)
|
|
658
1073
|
--app-version <ver> Version to record under (required with --record)
|
|
1074
|
+
--allow-pending Failures of never-verified interactions report as
|
|
1075
|
+
pending instead of blocking (exit 0)
|
|
659
1076
|
`);
|
|
660
1077
|
return;
|
|
661
1078
|
}
|