callman-mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +45 -16
- package/dist/client/api.d.ts +64 -3
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +67 -2
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +33 -7
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +91 -2
- package/dist/client/types.d.ts.map +1 -1
- package/dist/context.d.ts +2 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +7 -2
- package/dist/context.js.map +1 -1
- package/dist/domain/contracts.d.ts +893 -0
- package/dist/domain/contracts.d.ts.map +1 -0
- package/dist/domain/contracts.js +90 -0
- package/dist/domain/contracts.js.map +1 -0
- package/dist/domain/diagram/build.d.ts +520 -18
- package/dist/domain/diagram/build.d.ts.map +1 -1
- package/dist/domain/diagram/build.js +65 -19
- package/dist/domain/diagram/build.js.map +1 -1
- package/dist/domain/diagram/patch.d.ts +624 -51
- package/dist/domain/diagram/patch.d.ts.map +1 -1
- package/dist/domain/diagram/patch.js +180 -40
- package/dist/domain/diagram/patch.js.map +1 -1
- package/dist/domain/diagram/style.d.ts +315 -0
- package/dist/domain/diagram/style.d.ts.map +1 -0
- package/dist/domain/diagram/style.js +124 -0
- package/dist/domain/diagram/style.js.map +1 -0
- package/dist/domain/diagram/summarize.d.ts +8 -0
- package/dist/domain/diagram/summarize.d.ts.map +1 -1
- package/dist/domain/diagram/summarize.js +28 -13
- package/dist/domain/diagram/summarize.js.map +1 -1
- package/dist/domain/requestIntegrations.d.ts +719 -0
- package/dist/domain/requestIntegrations.d.ts.map +1 -0
- package/dist/domain/requestIntegrations.js +222 -0
- package/dist/domain/requestIntegrations.js.map +1 -0
- package/dist/domain/requestShape.d.ts +1350 -152
- package/dist/domain/requestShape.d.ts.map +1 -1
- package/dist/domain/requestShape.js +336 -121
- package/dist/domain/requestShape.js.map +1 -1
- package/dist/domain/resolve.d.ts +2 -1
- package/dist/domain/resolve.d.ts.map +1 -1
- package/dist/domain/resolve.js +5 -1
- package/dist/domain/resolve.js.map +1 -1
- package/dist/domain/scenario/build.d.ts +82 -26
- package/dist/domain/scenario/build.d.ts.map +1 -1
- package/dist/domain/scenario/build.js +196 -65
- package/dist/domain/scenario/build.js.map +1 -1
- package/dist/domain/scenario/nodeConfig.d.ts +11 -0
- package/dist/domain/scenario/nodeConfig.d.ts.map +1 -0
- package/dist/domain/scenario/nodeConfig.js +251 -0
- package/dist/domain/scenario/nodeConfig.js.map +1 -0
- package/dist/domain/scenario/summarize.d.ts +145 -25
- package/dist/domain/scenario/summarize.d.ts.map +1 -1
- package/dist/domain/scenario/summarize.js +84 -29
- package/dist/domain/scenario/summarize.js.map +1 -1
- package/dist/lib/mask.d.ts +0 -18
- package/dist/lib/mask.d.ts.map +1 -1
- package/dist/lib/mask.js +2 -44
- package/dist/lib/mask.js.map +1 -1
- package/dist/lib/pagination.d.ts +3 -2
- package/dist/lib/pagination.d.ts.map +1 -1
- package/dist/lib/pagination.js +13 -8
- package/dist/lib/pagination.js.map +1 -1
- package/dist/lib/redact.d.ts +12 -0
- package/dist/lib/redact.d.ts.map +1 -0
- package/dist/lib/redact.js +50 -0
- package/dist/lib/redact.js.map +1 -0
- package/dist/lib/strict.d.ts +4 -0
- package/dist/lib/strict.d.ts.map +1 -0
- package/dist/lib/strict.js +14 -0
- package/dist/lib/strict.js.map +1 -0
- package/dist/prompts/index.js +7 -7
- package/dist/prompts/index.js.map +1 -1
- package/dist/resources/docs/contracts.d.ts +2 -0
- package/dist/resources/docs/contracts.d.ts.map +1 -0
- package/dist/resources/docs/contracts.js +74 -0
- package/dist/resources/docs/contracts.js.map +1 -0
- package/dist/resources/docs/diagramSchema.d.ts +1 -1
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
- package/dist/resources/docs/diagramSchema.js +95 -28
- package/dist/resources/docs/diagramSchema.js.map +1 -1
- package/dist/resources/docs/overview.d.ts +1 -1
- package/dist/resources/docs/overview.d.ts.map +1 -1
- package/dist/resources/docs/overview.js +29 -7
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/docs/requestSchema.d.ts +1 -1
- package/dist/resources/docs/requestSchema.d.ts.map +1 -1
- package/dist/resources/docs/requestSchema.js +85 -16
- package/dist/resources/docs/requestSchema.js.map +1 -1
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.d.ts.map +1 -1
- package/dist/resources/generated/aiPacks.js +3 -2
- package/dist/resources/generated/aiPacks.js.map +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +19 -19
- package/dist/resources/index.js.map +1 -1
- package/dist/run/executeCollection.d.ts +3 -1
- package/dist/run/executeCollection.d.ts.map +1 -1
- package/dist/run/executeCollection.js +16 -4
- package/dist/run/executeCollection.js.map +1 -1
- package/dist/run/summarize.d.ts +17 -6
- package/dist/run/summarize.d.ts.map +1 -1
- package/dist/run/summarize.js +48 -41
- package/dist/run/summarize.js.map +1 -1
- package/dist/run/types.d.ts +1 -0
- package/dist/run/types.d.ts.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +18 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/collections.d.ts.map +1 -1
- package/dist/tools/collections.js +53 -41
- package/dist/tools/collections.js.map +1 -1
- package/dist/tools/contracts.d.ts +4 -0
- package/dist/tools/contracts.d.ts.map +1 -0
- package/dist/tools/contracts.js +201 -0
- package/dist/tools/contracts.js.map +1 -0
- package/dist/tools/diagrams.d.ts.map +1 -1
- package/dist/tools/diagrams.js +19 -10
- package/dist/tools/diagrams.js.map +1 -1
- package/dist/tools/environments.d.ts.map +1 -1
- package/dist/tools/environments.js +184 -64
- package/dist/tools/environments.js.map +1 -1
- package/dist/tools/import.d.ts.map +1 -1
- package/dist/tools/import.js +111 -20
- package/dist/tools/import.js.map +1 -1
- package/dist/tools/index.d.ts +2 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +19 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lookups.d.ts +38 -7
- package/dist/tools/lookups.d.ts.map +1 -1
- package/dist/tools/lookups.js +113 -10
- package/dist/tools/lookups.js.map +1 -1
- package/dist/tools/meta.d.ts.map +1 -1
- package/dist/tools/meta.js +9 -4
- package/dist/tools/meta.js.map +1 -1
- package/dist/tools/requests.d.ts.map +1 -1
- package/dist/tools/requests.js +138 -50
- package/dist/tools/requests.js.map +1 -1
- package/dist/tools/run.d.ts.map +1 -1
- package/dist/tools/run.js +84 -28
- package/dist/tools/run.js.map +1 -1
- package/dist/tools/scenarios.d.ts.map +1 -1
- package/dist/tools/scenarios.js +238 -75
- package/dist/tools/scenarios.js.map +1 -1
- package/dist/tools/shared.d.ts +1 -0
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +9 -1
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/workspaces.d.ts.map +1 -1
- package/dist/tools/workspaces.js +44 -7
- package/dist/tools/workspaces.js.map +1 -1
- package/dist-cjs/client/api.js +67 -2
- package/dist-cjs/client/errors.js +33 -7
- package/dist-cjs/context.js +7 -2
- package/dist-cjs/domain/contracts.js +93 -0
- package/dist-cjs/domain/diagram/build.js +71 -20
- package/dist-cjs/domain/diagram/patch.js +179 -39
- package/dist-cjs/domain/diagram/style.js +130 -0
- package/dist-cjs/domain/diagram/summarize.js +28 -13
- package/dist-cjs/domain/requestIntegrations.js +228 -0
- package/dist-cjs/domain/requestShape.js +342 -121
- package/dist-cjs/domain/resolve.js +5 -1
- package/dist-cjs/domain/scenario/build.js +197 -65
- package/dist-cjs/domain/scenario/nodeConfig.js +260 -0
- package/dist-cjs/domain/scenario/summarize.js +86 -29
- package/dist-cjs/lib/mask.js +3 -48
- package/dist-cjs/lib/pagination.js +13 -8
- package/dist-cjs/lib/redact.js +53 -0
- package/dist-cjs/lib/strict.js +18 -0
- package/dist-cjs/prompts/index.js +7 -7
- package/dist-cjs/resources/docs/contracts.js +76 -0
- package/dist-cjs/resources/docs/diagramSchema.js +95 -28
- package/dist-cjs/resources/docs/overview.js +29 -7
- package/dist-cjs/resources/docs/requestSchema.js +85 -16
- package/dist-cjs/resources/generated/aiPacks.js +3 -2
- package/dist-cjs/resources/index.js +17 -17
- package/dist-cjs/run/executeCollection.js +16 -4
- package/dist-cjs/run/summarize.js +48 -41
- package/dist-cjs/server.js +1 -1
- package/dist-cjs/tools/catalog.js +18 -0
- package/dist-cjs/tools/collections.js +52 -40
- package/dist-cjs/tools/contracts.js +204 -0
- package/dist-cjs/tools/diagrams.js +19 -10
- package/dist-cjs/tools/environments.js +182 -62
- package/dist-cjs/tools/import.js +107 -16
- package/dist-cjs/tools/index.js +21 -2
- package/dist-cjs/tools/lookups.js +123 -11
- package/dist-cjs/tools/meta.js +9 -4
- package/dist-cjs/tools/requests.js +134 -46
- package/dist-cjs/tools/run.js +82 -26
- package/dist-cjs/tools/scenarios.js +235 -72
- package/dist-cjs/tools/shared.js +11 -2
- package/dist-cjs/tools/workspaces.js +43 -6
- package/package.json +2 -2
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.compactReportView = exports.scenarioFullView = exports.scenarioGraphView = exports.scenarioSummaryView = void 0;
|
|
3
|
+
exports.compactReportView = exports.nodeDetailView = exports.scenarioFullView = exports.scenarioNodeView = exports.scenarioGraphView = exports.scenarioSummaryView = void 0;
|
|
4
4
|
const mask_js_1 = require("../../lib/mask.js");
|
|
5
5
|
const truncate_js_1 = require("../../lib/truncate.js");
|
|
6
|
+
const nodeConfig_js_1 = require("./nodeConfig.js");
|
|
6
7
|
const scenarioSummaryView = (scenario, folderName) => ({
|
|
7
8
|
id: scenario.id,
|
|
8
9
|
name: scenario.name,
|
|
@@ -12,9 +13,11 @@ const scenarioSummaryView = (scenario, folderName) => ({
|
|
|
12
13
|
...(folderName !== undefined ? { folder: folderName } : {}),
|
|
13
14
|
reviewStatus: scenario.reviewStatus,
|
|
14
15
|
version: scenario.version,
|
|
16
|
+
...(scenario.approvedVersion !== undefined && scenario.approvedVersion !== null ? { approvedVersion: scenario.approvedVersion } : {}),
|
|
15
17
|
nodeCount: scenario.nodes.length,
|
|
16
18
|
edgeCount: scenario.edges.length,
|
|
17
19
|
environments: (scenario.environments ?? []).map((environment) => ({ id: environment.id, name: environment.name })),
|
|
20
|
+
...(scenario.dataset ? { dataset: { columns: scenario.dataset.columns, rows: scenario.dataset.rows.length } } : {}),
|
|
18
21
|
...(scenario.groupName ? { group: scenario.groupName } : {}),
|
|
19
22
|
...(scenario.permissions ? { permissions: scenario.permissions } : {}),
|
|
20
23
|
updatedAt: scenario.updatedAt ?? null,
|
|
@@ -42,33 +45,41 @@ const nodeHint = (type, config) => {
|
|
|
42
45
|
case "wait":
|
|
43
46
|
return { durationMs: config.durationMs ?? null };
|
|
44
47
|
case "subscenario":
|
|
45
|
-
|
|
48
|
+
case "foreach":
|
|
49
|
+
return { innerNodes: Array.isArray(config.nodes) ? config.nodes.length : 0 };
|
|
50
|
+
case "scenario-ref":
|
|
51
|
+
return { referencedScenario: config.referencedScenarioName ?? config.referencedScenarioId ?? null };
|
|
46
52
|
default:
|
|
47
53
|
return {};
|
|
48
54
|
}
|
|
49
55
|
};
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
const scenarioNodeView = (scenario, nodeId, options = {}) => {
|
|
57
|
+
const node = scenario.nodes.find((entry) => entry.id === nodeId || entry.data?.label === nodeId);
|
|
58
|
+
if (!node)
|
|
59
|
+
return null;
|
|
60
|
+
const config = node.data?.config ?? {};
|
|
61
|
+
return {
|
|
62
|
+
id: node.id,
|
|
63
|
+
type: node.type,
|
|
64
|
+
label: node.data?.label ?? null,
|
|
65
|
+
position: node.position,
|
|
66
|
+
config: options.reveal ? config : (0, nodeConfig_js_1.maskConfigInPlace)(config),
|
|
67
|
+
incoming: scenario.edges.filter((edge) => edge.target === node.id).map((edge) => ({ from: edge.source, ...(edge.type && edge.type !== "default" ? { type: edge.type } : {}) })),
|
|
68
|
+
outgoing: scenario.edges.filter((edge) => edge.source === node.id).map((edge) => ({ to: edge.target, ...(edge.type && edge.type !== "default" ? { type: edge.type } : {}) })),
|
|
69
|
+
};
|
|
61
70
|
};
|
|
71
|
+
exports.scenarioNodeView = scenarioNodeView;
|
|
62
72
|
const scenarioFullView = (scenario, options = {}) => {
|
|
63
73
|
const nodes = scenario.nodes.map((node) => {
|
|
64
|
-
const config =
|
|
74
|
+
const config = options.reveal ? (node.data?.config ?? {}) : (0, nodeConfig_js_1.maskConfigInPlace)(node.data?.config ?? {});
|
|
65
75
|
const text = (0, truncate_js_1.safeStringify)(config);
|
|
66
76
|
return {
|
|
67
77
|
id: node.id,
|
|
68
78
|
type: node.type,
|
|
69
79
|
label: node.data?.label ?? null,
|
|
70
80
|
position: node.position,
|
|
71
|
-
|
|
81
|
+
// A clipped config is marked so it can never be written back.
|
|
82
|
+
config: text.length > truncate_js_1.LIMITS.BODY_PREVIEW ? { _truncated: true, preview: (0, truncate_js_1.clip)(text, truncate_js_1.LIMITS.BODY_PREVIEW), note: `Read it whole with get_scenario node: "${node.id}".` } : config,
|
|
72
83
|
};
|
|
73
84
|
});
|
|
74
85
|
const view = {
|
|
@@ -85,19 +96,23 @@ const scenarioFullView = (scenario, options = {}) => {
|
|
|
85
96
|
})),
|
|
86
97
|
})),
|
|
87
98
|
dataset: scenario.dataset ?? null,
|
|
99
|
+
dbSettings: scenario.dbSettings ?? null,
|
|
88
100
|
};
|
|
89
101
|
const bytes = (0, truncate_js_1.byteLength)(view);
|
|
90
102
|
if (bytes > truncate_js_1.LIMITS.SCENARIO_FULL) {
|
|
91
|
-
return { ...(0, exports.scenarioGraphView)(scenario), truncated: true, bytes, note: `Full view is ${bytes} bytes (limit ${truncate_js_1.LIMITS.SCENARIO_FULL}); graph view returned instead.` };
|
|
103
|
+
return { ...(0, exports.scenarioGraphView)(scenario), truncated: true, bytes, note: `Full view is ${bytes} bytes (limit ${truncate_js_1.LIMITS.SCENARIO_FULL}); graph view returned instead — read nodes one by one with get_scenario node: "<id>".` };
|
|
92
104
|
}
|
|
93
105
|
return view;
|
|
94
106
|
};
|
|
95
107
|
exports.scenarioFullView = scenarioFullView;
|
|
96
108
|
// ── reports ───────────────────────────────────────────────────────────
|
|
97
109
|
const testCounts = (results) => {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
const list = results ?? [];
|
|
111
|
+
return {
|
|
112
|
+
passed: list.filter((result) => result.status === "PASS").length,
|
|
113
|
+
failed: list.filter((result) => result.status === "FAIL").length,
|
|
114
|
+
...(list.some((result) => result.status === "SKIP") ? { skipped: list.filter((result) => result.status === "SKIP").length } : {}),
|
|
115
|
+
};
|
|
101
116
|
};
|
|
102
117
|
const nodeRow = (node) => ({
|
|
103
118
|
nodeId: node.nodeId,
|
|
@@ -111,11 +126,50 @@ const nodeRow = (node) => ({
|
|
|
111
126
|
tests: testCounts(node.scriptResults),
|
|
112
127
|
contracts: testCounts(node.contractResults),
|
|
113
128
|
...(node.conditionResult !== undefined && node.conditionResult !== null ? { conditionResult: node.conditionResult } : {}),
|
|
129
|
+
...(node.activeBranch ? { branch: node.activeBranch } : {}),
|
|
130
|
+
});
|
|
131
|
+
// failed_continued = the node failed but the execution policy went on.
|
|
132
|
+
const FAILED_STATUSES = new Set(["failed", "failed_continued", "error", "timeout"]);
|
|
133
|
+
const FAILED_DETAIL_MAX = 5;
|
|
134
|
+
// Scalars as they are; objects as a bounded preview.
|
|
135
|
+
const valuePreview = (value) => (value !== null && typeof value === "object" ? (0, truncate_js_1.jsonPreview)(value) : value);
|
|
136
|
+
// Everything a model needs to fix ONE node: failed tests/checks/contracts
|
|
137
|
+
// with expected vs actual, the input as resolved at run time, the script
|
|
138
|
+
// error with its line, the response and the last log lines.
|
|
139
|
+
const nodeDetailView = (node) => ({
|
|
140
|
+
...nodeRow(node),
|
|
141
|
+
...(node.continuedAfterFailure ? { continuedAfterFailure: true } : {}),
|
|
142
|
+
failedTests: (node.scriptResults ?? []).filter((result) => result.status === "FAIL").map((result) => ({ name: result.name, message: result.message ?? null })),
|
|
143
|
+
...(node.assertionResults && node.assertionResults.length > 0
|
|
144
|
+
? {
|
|
145
|
+
checks: node.assertionResults.map((check) => ({
|
|
146
|
+
name: check.name ?? null,
|
|
147
|
+
passed: check.passed ?? null,
|
|
148
|
+
...(check.message ? { message: (0, truncate_js_1.clip)(String(check.message), 300) } : {}),
|
|
149
|
+
...(check.expected !== undefined ? { expected: valuePreview(check.expected) } : {}),
|
|
150
|
+
...(check.actual !== undefined ? { actual: valuePreview(check.actual) } : {}),
|
|
151
|
+
})),
|
|
152
|
+
}
|
|
153
|
+
: {}),
|
|
154
|
+
...(node.contractResults && node.contractResults.some((result) => result.status === "FAIL")
|
|
155
|
+
? {
|
|
156
|
+
failedContracts: node.contractResults
|
|
157
|
+
.filter((result) => result.status === "FAIL")
|
|
158
|
+
.slice(0, 20)
|
|
159
|
+
.map((result) => ({ name: result.name ?? result.path ?? null, message: result.message ? (0, truncate_js_1.clip)(result.message, 300) : null })),
|
|
160
|
+
}
|
|
161
|
+
: {}),
|
|
162
|
+
...(node.contractValidationError ? { contractValidationError: node.contractValidationError } : {}),
|
|
163
|
+
...(node.script?.runtimeError ? { scriptError: node.script.runtimeError } : {}),
|
|
164
|
+
...(node.resolvedInputSnapshot !== undefined && node.resolvedInputSnapshot !== null ? { resolvedInput: (0, truncate_js_1.jsonPreview)((0, nodeConfig_js_1.maskConfigInPlace)(node.resolvedInputSnapshot)) } : {}),
|
|
165
|
+
response: (0, truncate_js_1.jsonPreview)(node.response ?? node.outputSnapshot),
|
|
166
|
+
logs: (node.logs ?? []).slice(-10).map((entry) => (0, truncate_js_1.clip)(String(entry.message ?? ""), 300)),
|
|
114
167
|
});
|
|
115
|
-
|
|
168
|
+
exports.nodeDetailView = nodeDetailView;
|
|
116
169
|
const compactReportView = (report) => {
|
|
117
170
|
const nodes = report.nodeResults ?? [];
|
|
118
|
-
const failed = nodes.
|
|
171
|
+
const failed = nodes.filter((node) => FAILED_STATUSES.has(String(node.status)));
|
|
172
|
+
const unexpected = report.finalContext?.unexpectedError;
|
|
119
173
|
return {
|
|
120
174
|
reportId: report.id,
|
|
121
175
|
scenarioId: report.scenarioId,
|
|
@@ -123,22 +177,25 @@ const compactReportView = (report) => {
|
|
|
123
177
|
scenarioVersion: report.scenarioVersion,
|
|
124
178
|
status: report.status,
|
|
125
179
|
triggerType: report.triggerType,
|
|
180
|
+
...(report.environmentId ? { environmentId: report.environmentId } : {}),
|
|
126
181
|
startedAt: report.startedAt,
|
|
127
182
|
endedAt: report.endedAt,
|
|
128
183
|
durationMs: report.durationMs,
|
|
129
184
|
summary: report.summary,
|
|
185
|
+
// An error outside every node (bad graph at run time, engine error) —
|
|
186
|
+
// the run failed with no failing node to show.
|
|
187
|
+
...(unexpected ? { unexpectedError: typeof unexpected === "string" ? (0, truncate_js_1.clip)(unexpected, 1000) : (0, truncate_js_1.jsonPreview)(unexpected) } : {}),
|
|
130
188
|
nodes: nodes.map(nodeRow),
|
|
131
|
-
...(failed
|
|
189
|
+
...(failed.length > 0 ? { failedNodes: failed.slice(0, FAILED_DETAIL_MAX).map(exports.nodeDetailView) } : {}),
|
|
190
|
+
...(failed.length > FAILED_DETAIL_MAX ? { moreFailedNodes: failed.length - FAILED_DETAIL_MAX, note: "Read any node with get_scenario_report node: \"<nodeId>\"." } : {}),
|
|
191
|
+
...(report.iterations && report.iterations.length > 0
|
|
132
192
|
? {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
logs: (failed.logs ?? []).slice(-10).map((entry) => (0, truncate_js_1.clip)(String(entry.message ?? ""), 300)),
|
|
138
|
-
},
|
|
193
|
+
iterations: report.iterations.slice(0, 50).map((iteration, index) => ({
|
|
194
|
+
row: typeof iteration.index === "number" ? iteration.index + 1 : index + 1,
|
|
195
|
+
status: iteration.status ?? null,
|
|
196
|
+
})),
|
|
139
197
|
}
|
|
140
198
|
: {}),
|
|
141
|
-
...(report.iterations && report.iterations.length > 0 ? { iterations: report.iterations.length } : {}),
|
|
142
199
|
};
|
|
143
200
|
};
|
|
144
201
|
exports.compactReportView = compactReportView;
|
package/dist-cjs/lib/mask.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// Secret masking on the way OUT to the model. The backend already masks
|
|
3
3
|
// environment values for a personal token; this covers what it does not —
|
|
4
|
-
// request auth secrets
|
|
4
|
+
// request auth secrets and sensitive headers (requestShape.authInputView,
|
|
5
|
+
// scenario nodeConfig.maskConfigInPlace, lib/redact for local runs).
|
|
5
6
|
// Same sentinel as the backend so a masked value is recognisable and can
|
|
6
7
|
// never be written back by accident (the server rejects it).
|
|
7
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.
|
|
9
|
+
exports.maskHeaderValue = exports.isSensitiveHeader = exports.isMasked = exports.maskSecret = exports.MASK_SENTINEL = void 0;
|
|
9
10
|
exports.MASK_SENTINEL = "••••";
|
|
10
11
|
const maskSecret = (value) => value.length >= 8 ? `${exports.MASK_SENTINEL}${value.slice(-2)}` : exports.MASK_SENTINEL;
|
|
11
12
|
exports.maskSecret = maskSecret;
|
|
@@ -24,49 +25,3 @@ const isSensitiveHeader = (name) => SENSITIVE_HEADERS.has(name.trim().toLowerCas
|
|
|
24
25
|
exports.isSensitiveHeader = isSensitiveHeader;
|
|
25
26
|
const maskHeaderValue = (name, value) => (0, exports.isSensitiveHeader)(name) && value ? (0, exports.maskSecret)(value) : value;
|
|
26
27
|
exports.maskHeaderValue = maskHeaderValue;
|
|
27
|
-
const maskHeaderRecord = (headers) => {
|
|
28
|
-
const out = {};
|
|
29
|
-
for (const [key, value] of Object.entries(headers ?? {}))
|
|
30
|
-
out[key] = (0, exports.maskHeaderValue)(key, value);
|
|
31
|
-
return out;
|
|
32
|
-
};
|
|
33
|
-
exports.maskHeaderRecord = maskHeaderRecord;
|
|
34
|
-
// Compact, masked view of a request's auth: enough to know HOW it
|
|
35
|
-
// authenticates, never the credential itself.
|
|
36
|
-
const maskAuth = (auth) => {
|
|
37
|
-
const type = auth?.type ?? "none";
|
|
38
|
-
switch (type) {
|
|
39
|
-
case "bearer":
|
|
40
|
-
return { type, token: auth?.bearerToken ? (0, exports.maskSecret)(auth.bearerToken) : "" };
|
|
41
|
-
case "basic":
|
|
42
|
-
return {
|
|
43
|
-
type,
|
|
44
|
-
username: auth?.basicUsername ?? "",
|
|
45
|
-
password: auth?.basicPassword ? (0, exports.maskSecret)(auth.basicPassword) : "",
|
|
46
|
-
};
|
|
47
|
-
case "apikey":
|
|
48
|
-
return {
|
|
49
|
-
type,
|
|
50
|
-
key: auth?.apiKeyKey ?? "",
|
|
51
|
-
value: auth?.apiKeyValue ? (0, exports.maskSecret)(auth.apiKeyValue) : "",
|
|
52
|
-
location: auth?.apiKeyLocation ?? "header",
|
|
53
|
-
};
|
|
54
|
-
case "oauth2": {
|
|
55
|
-
const oauth2 = auth?.oauth2 ?? {};
|
|
56
|
-
return {
|
|
57
|
-
type,
|
|
58
|
-
grantType: oauth2.grantType ?? "client_credentials",
|
|
59
|
-
tokenUrl: oauth2.tokenUrl ?? "",
|
|
60
|
-
clientId: oauth2.clientId ?? "",
|
|
61
|
-
clientSecret: typeof oauth2.clientSecret === "string" && oauth2.clientSecret ? (0, exports.maskSecret)(oauth2.clientSecret) : "",
|
|
62
|
-
scope: oauth2.scope ?? "",
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
default:
|
|
66
|
-
return { type: "none" };
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
exports.maskAuth = maskAuth;
|
|
70
|
-
// Mask every `value` in a key/value list (environment / scenario variables).
|
|
71
|
-
const maskVariableValues = (variables) => variables.map((variable) => ({ ...variable, value: (0, exports.maskSecret)(variable.value), valueMasked: true }));
|
|
72
|
-
exports.maskVariableValues = maskVariableValues;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.sliceWithCursor = exports.nextCursorFor = exports.pageFromCursor = exports.clampLimit = exports.decodeCursor = exports.encodeCursor = void 0;
|
|
4
|
+
const errors_js_1 = require("../client/errors.js");
|
|
4
5
|
const truncate_js_1 = require("./truncate.js");
|
|
5
6
|
const encodeCursor = (cursor) => Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url");
|
|
6
7
|
exports.encodeCursor = encodeCursor;
|
|
@@ -13,7 +14,7 @@ const decodeCursor = (raw) => {
|
|
|
13
14
|
return null;
|
|
14
15
|
if (!Number.isInteger(parsed.page) || parsed.page < 1)
|
|
15
16
|
return null;
|
|
16
|
-
return { page: parsed.page, limit: (0, exports.clampLimit)(parsed.limit) };
|
|
17
|
+
return { page: parsed.page, limit: (0, exports.clampLimit)(parsed.limit), ...(typeof parsed.q === "string" ? { q: parsed.q } : {}) };
|
|
17
18
|
}
|
|
18
19
|
catch {
|
|
19
20
|
return null;
|
|
@@ -22,11 +23,15 @@ const decodeCursor = (raw) => {
|
|
|
22
23
|
exports.decodeCursor = decodeCursor;
|
|
23
24
|
const clampLimit = (limit) => Math.min(truncate_js_1.LIMITS.LIST_MAX, Math.max(1, Math.floor(limit ?? truncate_js_1.LIMITS.LIST_DEFAULT)));
|
|
24
25
|
exports.clampLimit = clampLimit;
|
|
25
|
-
const pageFromCursor = (cursor, limit) => {
|
|
26
|
+
const pageFromCursor = (cursor, limit, q = "") => {
|
|
27
|
+
if (!cursor)
|
|
28
|
+
return { page: 1, limit: (0, exports.clampLimit)(limit) };
|
|
26
29
|
const decoded = (0, exports.decodeCursor)(cursor);
|
|
27
|
-
if (decoded)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
if (!decoded)
|
|
31
|
+
throw new errors_js_1.ToolInputError("CURSOR_INVALID", "That cursor is not one this server issued — pass the nextCursor from the previous result verbatim, or omit it for the first page.");
|
|
32
|
+
if ((decoded.q ?? "") !== q)
|
|
33
|
+
throw new errors_js_1.ToolInputError("CURSOR_QUERY_MISMATCH", "That cursor belongs to a different query — omit it to start this query from the first page.");
|
|
34
|
+
return decoded;
|
|
30
35
|
};
|
|
31
36
|
exports.pageFromCursor = pageFromCursor;
|
|
32
37
|
const nextCursorFor = (pagination) => pagination.page < pagination.totalPages
|
|
@@ -34,15 +39,15 @@ const nextCursorFor = (pagination) => pagination.page < pagination.totalPages
|
|
|
34
39
|
: null;
|
|
35
40
|
exports.nextCursorFor = nextCursorFor;
|
|
36
41
|
// Client-side slice for list endpoints that are not paginated server-side.
|
|
37
|
-
const sliceWithCursor = (items, cursor, limit) => {
|
|
38
|
-
const { page, limit: size } = (0, exports.pageFromCursor)(cursor, limit);
|
|
42
|
+
const sliceWithCursor = (items, cursor, limit, q = "") => {
|
|
43
|
+
const { page, limit: size } = (0, exports.pageFromCursor)(cursor, limit, q);
|
|
39
44
|
const start = (page - 1) * size;
|
|
40
45
|
const slice = items.slice(start, start + size);
|
|
41
46
|
const totalPages = Math.max(1, Math.ceil(items.length / size));
|
|
42
47
|
return {
|
|
43
48
|
items: slice,
|
|
44
49
|
total: items.length,
|
|
45
|
-
nextCursor: page < totalPages ? (0, exports.encodeCursor)({ page: page + 1, limit: size }) : null,
|
|
50
|
+
nextCursor: page < totalPages ? (0, exports.encodeCursor)({ page: page + 1, limit: size, ...(q ? { q } : {}) }) : null,
|
|
46
51
|
};
|
|
47
52
|
};
|
|
48
53
|
exports.sliceWithCursor = sliceWithCursor;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRedactor = void 0;
|
|
4
|
+
const mask_js_1 = require("./mask.js");
|
|
5
|
+
// Run output never carries variable VALUES: every value the run held is
|
|
6
|
+
// turned back into its `{{key}}` wherever it shows up (the URL, headers, query
|
|
7
|
+
// and body as sent, the response). The model still sees WHICH variable went
|
|
8
|
+
// where — "{{baseUrl}}/users/{{userId}}" — which is what it needs to debug,
|
|
9
|
+
// and a token a login script stored is as hidden as one from the environment.
|
|
10
|
+
// Credentials that are not variables (a literal bearer token, an API key
|
|
11
|
+
// header named by the request's auth) are masked by name.
|
|
12
|
+
// Short values ("1", "yes", "GET") would shred ordinary text.
|
|
13
|
+
const MIN_VALUE_LENGTH = 4;
|
|
14
|
+
const createRedactor = (knownValues, options = {}) => {
|
|
15
|
+
const pairs = knownValues
|
|
16
|
+
.filter(([, value]) => value.length >= MIN_VALUE_LENGTH)
|
|
17
|
+
.sort((a, b) => b[1].length - a[1].length);
|
|
18
|
+
const secretHeaders = new Set([...(options.secretHeaders ?? [])].map((name) => name.toLowerCase()));
|
|
19
|
+
const secretQuery = new Set([...(options.secretQuery ?? [])].map((name) => name.toLowerCase()));
|
|
20
|
+
const text = (input) => {
|
|
21
|
+
let out = input;
|
|
22
|
+
for (const [key, value] of pairs)
|
|
23
|
+
if (out.includes(value))
|
|
24
|
+
out = out.split(value).join(`{{${key}}}`);
|
|
25
|
+
for (const name of secretQuery) {
|
|
26
|
+
out = out.replace(new RegExp(`([?&]${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=)([^&#]*)`, "gi"), (_match, prefix, secret) => `${prefix}${secret.startsWith("{{") ? secret : (0, mask_js_1.maskSecret)(secret)}`);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
};
|
|
30
|
+
const value = (input) => {
|
|
31
|
+
if (typeof input === "string")
|
|
32
|
+
return text(input);
|
|
33
|
+
if (Array.isArray(input))
|
|
34
|
+
return input.map(value);
|
|
35
|
+
if (input && typeof input === "object") {
|
|
36
|
+
return Object.fromEntries(Object.entries(input).map(([key, child]) => [key, value(child)]));
|
|
37
|
+
}
|
|
38
|
+
return input;
|
|
39
|
+
};
|
|
40
|
+
const masked = (name, raw, secretNames) => {
|
|
41
|
+
const redacted = text(raw);
|
|
42
|
+
if (redacted !== raw)
|
|
43
|
+
return redacted; // (part of) it came from a variable
|
|
44
|
+
return (secretNames.has(name.toLowerCase()) || (secretNames === secretHeaders && (0, mask_js_1.isSensitiveHeader)(name))) && raw ? (0, mask_js_1.maskSecret)(raw) : redacted;
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
text,
|
|
48
|
+
value,
|
|
49
|
+
headers: (headers) => Object.fromEntries(Object.entries(headers ?? {}).map(([name, raw]) => [name, masked(name, Array.isArray(raw) ? raw.join(", ") : String(raw), secretHeaders)])),
|
|
50
|
+
query: (query) => Object.fromEntries(Object.entries(query ?? {}).map(([name, raw]) => [name, masked(name, String(raw), secretQuery)])),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
exports.createRedactor = createRedactor;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.strictObject = exports.unknownKeysErrorMap = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
// Strict objects: an unknown or misspelled key is an ERROR naming the valid
|
|
6
|
+
// keys — never silently stripped (zod's default), because a stripped key
|
|
7
|
+
// means the tool quietly does something other than what was asked
|
|
8
|
+
// (`workspaceId` instead of `workspace` → the default workspace; `strokeColor`
|
|
9
|
+
// instead of `stroke` → an unstyled node, reported as success).
|
|
10
|
+
// `noun` names one key ("style key", "argument"); `where` says whose.
|
|
11
|
+
const unknownKeysErrorMap = (noun, validKeys, where) => (issue, ctx) => issue.code === zod_1.z.ZodIssueCode.unrecognized_keys
|
|
12
|
+
? {
|
|
13
|
+
message: `Unknown ${noun}${issue.keys.length === 1 ? "" : "s"} ${issue.keys.map((key) => `"${key}"`).join(", ")}${where ? ` for ${where}` : ""}. Valid ${noun}s: ${validKeys.join(", ")}`,
|
|
14
|
+
}
|
|
15
|
+
: { message: ctx.defaultError };
|
|
16
|
+
exports.unknownKeysErrorMap = unknownKeysErrorMap;
|
|
17
|
+
const strictObject = (shape, noun, where) => zod_1.z.object(shape, { errorMap: (0, exports.unknownKeysErrorMap)(noun, Object.keys(shape), where) }).strict();
|
|
18
|
+
exports.strictObject = strictObject;
|
|
@@ -38,7 +38,7 @@ Steps:
|
|
|
38
38
|
Steps:
|
|
39
39
|
1. Read \`callman://diagram/components\` — use ONLY those \`type\` values.
|
|
40
40
|
2. From the code and configuration (docker-compose, Kubernetes manifests, terraform, env files, package manifests) identify: services/apps, databases and caches, message brokers/queues, external APIs, auth providers, observability, cloud resources, and the boundaries they live in (VPC, cluster, namespace).
|
|
41
|
-
3. Model 8–25 nodes with 1–3 word labels. Put components inside boundaries with \`parent\`
|
|
41
|
+
3. Model 8–25 nodes with 1–3 word labels. Put components inside boundaries with \`parent\` — the boundary is a \`frame\` (or \`pool.horizontal\` / \`pool.vertical\`) node. Give each boundary its own color so zones read at a glance: \`style: { stroke: "#8B5CF6", fill: "rgba(139,92,246,0.07)", textColor: "#8B5CF6", strokeWidth: 2, bold: true }\` (see \`callman://docs/diagram-schema\` → Styling). Give each edge a meaning: \`sync\` for request/response calls, \`async\` for events/callbacks, \`bus\` for streams/queues, \`fallback\` for retries or optional paths. Short edge labels (protocol or event name) only where they add information.
|
|
42
42
|
4. Call \`create_diagram\` with \`name: "${name ?? "<system name>"}"\`, no positions (automatic layout). If it reports unknown types, fix them with the suggestions and retry.
|
|
43
43
|
5. Describe the diagram in a few sentences and tell me it is in the Callman Diagrams library (Refresh to see it). For corrections use \`patch_diagram\`, not a full rebuild.`));
|
|
44
44
|
server.registerPrompt("create_scenario_from_flow", {
|
|
@@ -52,17 +52,17 @@ Steps:
|
|
|
52
52
|
}, ({ flow, collection, workspace }) => user(`Create a Callman scenario that tests this flow${where(workspace)}: ${flow}
|
|
53
53
|
|
|
54
54
|
Steps:
|
|
55
|
-
1. Read \`callman://docs/scenario-nodes\` for the node types
|
|
56
|
-
2. Find the requests to reuse with \`search_requests\`${collection ? ` in the collection "${collection}"` : ""} and read the ones you need with \`get_request\` (
|
|
57
|
-
3. Build the graph: one \`request\` node per call; an \`assertion\` node after each important response (
|
|
58
|
-
4. Call \`create_scenario\` with \`dryRun: true\`, review the resolved
|
|
59
|
-
5. Offer to run it with \`run_scenario\` and to explain the report. On-prem:
|
|
55
|
+
1. Read \`callman://docs/scenario-nodes\` for the node types, their config fields, what is required, labels and graph rules.
|
|
56
|
+
2. Find the requests to reuse with \`search_requests\`${collection ? ` in the collection "${collection}"` : ""} and read the ones you need with \`get_request\` (reveal: true if you need the real credentials — a masked value cannot be saved). Request nodes carry their own config — copy method/url/headers/query/body/auth from the saved request (the request tools' shapes are accepted and converted) and use \`{{Label.body.path}}\` / \`{{env.KEY}}\` for values produced by earlier steps.
|
|
57
|
+
3. Build the graph: one \`request\` node per call with a unique identifier label (\`Login\`, \`CreateOrder\`); an \`assertion\` node after each important response (checks with left/operator/right); \`set-vars\` to capture ids/tokens; a \`condition\` with \`yes\`/\`no\` edges where the flow branches; \`poll\` for eventually-consistent checks; an \`end\` node last. db/redis/kafka nodes need a \`connectionId\` from \`list_connections\`. Add a scenario environment with the variables the flow needs.
|
|
58
|
+
4. Call \`create_scenario\` with \`dryRun: true\`, review the resolved nodes, then create it for real. Fix anything listed under \`warnings\` (config keys the server ignored) with \`patch_scenario\`.
|
|
59
|
+
5. Offer to run it with \`run_scenario\` and to explain the report (\`failedNodes\` has the failing checks with expected vs actual). On-prem: a personal token runs the APPROVED version — use \`submit_scenario\` and have it approved first.`));
|
|
60
60
|
server.registerPrompt("document_collection", {
|
|
61
61
|
title: "Document an API collection",
|
|
62
62
|
description: "Read a Callman collection and write API documentation (Markdown or an OpenAPI skeleton) from its requests.",
|
|
63
63
|
argsSchema: {
|
|
64
64
|
collection: zod_1.z.string().describe("Collection id or name"),
|
|
65
|
-
format: zod_1.z.
|
|
65
|
+
format: zod_1.z.enum(["markdown", "openapi"]).optional().describe("markdown (default) or openapi"),
|
|
66
66
|
workspace: zod_1.z.string().optional(),
|
|
67
67
|
},
|
|
68
68
|
}, ({ collection, format, workspace }) => user(`Document the Callman collection "${collection}"${where(workspace)} as ${format === "openapi" ? "an OpenAPI 3.1 skeleton" : "Markdown"}.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CONTRACTS_DOC = void 0;
|
|
4
|
+
const callman_core_1 = require("callman-core");
|
|
5
|
+
const contracts_js_1 = require("../../domain/contracts.js");
|
|
6
|
+
exports.CONTRACTS_DOC = `# Contract checks on a request
|
|
7
|
+
|
|
8
|
+
Two documents attached to a saved request — the desktop's Contracts tab. Every runner
|
|
9
|
+
(desktop, CLI, \`run_request\`, \`run_collection\`) evaluates them after the response and
|
|
10
|
+
reports them next to the \`pm.test\` results.
|
|
11
|
+
|
|
12
|
+
| Tool | Does |
|
|
13
|
+
|---|---|
|
|
14
|
+
| \`get_contract_checks\` | read the rules (with ids) and the data contract |
|
|
15
|
+
| \`add_contract_rules\` | add up to 200 rules at once (\`replace: true\` swaps the whole set) |
|
|
16
|
+
| \`update_contract_rule\` | change one rule's path, rules or enabled |
|
|
17
|
+
| \`delete_contract_rules\` | delete some or all rules |
|
|
18
|
+
| \`test_contract_rules\` | evaluate the enabled rules against a sample body — nothing is sent or saved |
|
|
19
|
+
| \`set_data_contract\` / \`delete_data_contract\` | replace / remove the data-driven contract |
|
|
20
|
+
|
|
21
|
+
## Field rules
|
|
22
|
+
\`{ "path": "<field path>", "rules": { … }, "enabled": true }\`
|
|
23
|
+
\`\`\`jsonc
|
|
24
|
+
{ "path": "users[].id", "rules": { "required": true, "type": "string", "format": "uuid" } }
|
|
25
|
+
{ "path": "users", "rules": { "type": "array", "minItems": 1, "unique": { "mode": "byField", "fields": ["id"] } } }
|
|
26
|
+
{ "path": "total", "rules": { "type": "number", "integer": true, "min": 0 } }
|
|
27
|
+
{ "path": "status", "rules": { "enumValues": ["NEW", "PAID", "SHIPPED"] } }
|
|
28
|
+
{ "path": "email", "rules": { "format": "email", "notEmpty": true } }
|
|
29
|
+
{ "path": "response.status", "rules": { "equals": 200 } }
|
|
30
|
+
{ "path": "response.headers.content-type", "rules": { "startsWith": "application/json" } }
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
### Paths
|
|
34
|
+
- Body fields with dots: \`user.address.city\`.
|
|
35
|
+
- \`[]\` checks EVERY element: \`users[].id\`, \`[].name\` (top-level array) — one result per element.
|
|
36
|
+
- \`[n]\` checks one element: \`items[0].price\`.
|
|
37
|
+
- \`response.status\`, \`response.headers.<lower-case name>\`, \`response.duration\` (ms) check the
|
|
38
|
+
HTTP envelope instead of the body.
|
|
39
|
+
|
|
40
|
+
### Rules (combine freely; at least one per rule)
|
|
41
|
+
| Applies to | Rules |
|
|
42
|
+
|---|---|
|
|
43
|
+
| any | \`required\`, \`type\` (string · number · boolean · object · array · null), \`equals\`, \`notEmpty\`, \`enumValues\` |
|
|
44
|
+
| numbers | \`min\`, \`max\`, \`greaterThan\`, \`lessThan\`, \`integer\`, \`positive\`, \`negative\`, \`nonZero\` |
|
|
45
|
+
| strings | \`minLength\`, \`maxLength\`, \`pattern\` (JS regex), \`contains\`, \`notContains\`, \`startsWith\`, \`endsWith\`, \`caseSensitive\` (default true), \`format\` |
|
|
46
|
+
| arrays | \`minItems\`, \`maxItems\`, \`sorted: { order: asc|desc, field? }\`, \`unique: { mode: whole|byField, fields? }\` |
|
|
47
|
+
| objects | \`requiredKeys\`, \`forbiddenKeys\`, \`minProperties\`, \`maxProperties\`, \`strictSchema\` (no keys beyond requiredKeys) |
|
|
48
|
+
|
|
49
|
+
\`format\`: ${callman_core_1.SCENARIO_CONTRACT_FORMATS.join(", ")}.
|
|
50
|
+
|
|
51
|
+
The server also refuses contradictions: min > max, minLength > maxLength, minItems > maxItems,
|
|
52
|
+
greaterThan ≥ lessThan, a key both required and forbidden, \`unique.mode: "byField"\` without
|
|
53
|
+
fields, an invalid regex.
|
|
54
|
+
|
|
55
|
+
## Data-driven contract
|
|
56
|
+
The request runs ONCE PER ROW; the row's values are \`{{data.<column>}}\` and each row checks its
|
|
57
|
+
own expected status and field assertions (same rules as above).
|
|
58
|
+
\`\`\`jsonc
|
|
59
|
+
{
|
|
60
|
+
"columns": ["userId", "expected"],
|
|
61
|
+
"rows": [
|
|
62
|
+
{ "data": { "userId": "1", "expected": "Ann" }, "expectedStatus": 200,
|
|
63
|
+
"assertions": [{ "path": "name", "rules": { "equals": "Ann" } }] },
|
|
64
|
+
{ "data": { "userId": "404", "expected": "" }, "expectedStatus": 404 }
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
\`\`\`
|
|
68
|
+
Put the \`{{data.<column>}}\` tokens into the request (url, query, headers, body) with
|
|
69
|
+
\`update_request\` — \`set_data_contract\` warns about columns the request does not use yet.
|
|
70
|
+
Limits: ${contracts_js_1.DATA_CONTRACT_LIMITS.columns} columns, ${contracts_js_1.DATA_CONTRACT_LIMITS.rows} rows, ${contracts_js_1.DATA_CONTRACT_LIMITS.assertions} assertions per row, ${contracts_js_1.DATA_CONTRACT_LIMITS.cell} characters per cell.
|
|
71
|
+
|
|
72
|
+
## Contract checks vs pm.test
|
|
73
|
+
Use contract rules for the SHAPE of a response (types, formats, required fields, ranges) —
|
|
74
|
+
declarative, shown per field in reports. Use \`pm.test\` in \`postResponseScript\` for logic that
|
|
75
|
+
needs code (cross-field checks, saving values with \`pm.environment.set\`).
|
|
76
|
+
`;
|