@fluentcommerce/fluent-mcp-extn 0.1.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/LICENSE +21 -0
- package/README.md +818 -0
- package/dist/config.js +195 -0
- package/dist/entity-registry.js +418 -0
- package/dist/entity-tools.js +414 -0
- package/dist/environment-tools.js +573 -0
- package/dist/errors.js +150 -0
- package/dist/event-payload.js +22 -0
- package/dist/fluent-client.js +229 -0
- package/dist/index.js +47 -0
- package/dist/resilience.js +52 -0
- package/dist/response-shaper.js +361 -0
- package/dist/sdk-client.js +237 -0
- package/dist/settings-tools.js +348 -0
- package/dist/test-tools.js +388 -0
- package/dist/tools.js +3254 -0
- package/dist/workflow-tools.js +752 -0
- package/docs/CONTRIBUTING.md +100 -0
- package/docs/E2E_TESTING.md +739 -0
- package/docs/HANDOVER_COPILOT_SETUP_STEPS.example.yml +35 -0
- package/docs/HANDOVER_ENV.example +29 -0
- package/docs/HANDOVER_GITHUB_COPILOT.md +165 -0
- package/docs/HANDOVER_GITHUB_REPO_MCP_CONFIG.example.json +31 -0
- package/docs/HANDOVER_VSCODE_MCP_JSON.example.json +10 -0
- package/docs/IMPLEMENTATION_GUIDE.md +299 -0
- package/docs/RUNBOOK.md +312 -0
- package/docs/TOOL_REFERENCE.md +1810 -0
- package/package.json +68 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test tools: test.assert
|
|
3
|
+
*
|
|
4
|
+
* Entity state assertion with optional polling for agentic verify loops.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { ToolError } from "./errors.js";
|
|
8
|
+
import { getEntityMeta, SUPPORTED_ENTITY_TYPES } from "./entity-registry.js";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Input schemas
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export const TestAssertInputSchema = z.object({
|
|
13
|
+
entityType: z.string().min(1),
|
|
14
|
+
id: z.string().optional().describe("Entity ID (preferred)"),
|
|
15
|
+
ref: z.string().optional().describe("Entity ref (fallback)"),
|
|
16
|
+
assertions: z.object({
|
|
17
|
+
status: z.string().optional().describe("Expected entity status"),
|
|
18
|
+
type: z.string().optional().describe("Expected entity type"),
|
|
19
|
+
subtype: z.string().optional().describe("Expected entity subtype"),
|
|
20
|
+
attributes: z
|
|
21
|
+
.record(z.string(), z.unknown())
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Expected attribute key-value pairs"),
|
|
24
|
+
edges: z
|
|
25
|
+
.record(z.string(), z.object({
|
|
26
|
+
minCount: z
|
|
27
|
+
.number()
|
|
28
|
+
.int()
|
|
29
|
+
.min(0)
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("Minimum expected edge count"),
|
|
32
|
+
maxCount: z
|
|
33
|
+
.number()
|
|
34
|
+
.int()
|
|
35
|
+
.min(0)
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Maximum expected edge count"),
|
|
38
|
+
status: z
|
|
39
|
+
.string()
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Expected status on related entities"),
|
|
42
|
+
}))
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Assertions on related entity edges"),
|
|
45
|
+
}),
|
|
46
|
+
poll: z
|
|
47
|
+
.boolean()
|
|
48
|
+
.default(false)
|
|
49
|
+
.describe("If true, retry assertions until they pass or timeout."),
|
|
50
|
+
timeoutMs: z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.min(1000)
|
|
54
|
+
.max(300000)
|
|
55
|
+
.default(60000)
|
|
56
|
+
.describe("Polling timeout in ms (default: 60000)."),
|
|
57
|
+
intervalMs: z
|
|
58
|
+
.number()
|
|
59
|
+
.int()
|
|
60
|
+
.min(1000)
|
|
61
|
+
.max(30000)
|
|
62
|
+
.default(5000)
|
|
63
|
+
.describe("Polling interval in ms (default: 5000)."),
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Tool definitions (JSON Schema for MCP)
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
export const TEST_TOOL_DEFINITIONS = [
|
|
69
|
+
{
|
|
70
|
+
name: "test.assert",
|
|
71
|
+
description: [
|
|
72
|
+
"Assert entity state matches expectations with optional polling.",
|
|
73
|
+
"",
|
|
74
|
+
"Deep assertion on entity fields, attributes, and edge counts/statuses.",
|
|
75
|
+
"Human-readable failure messages.",
|
|
76
|
+
"",
|
|
77
|
+
"POLLING MODE: Set poll=true to retry until assertions pass or timeout.",
|
|
78
|
+
"Useful after sending events when state changes are async.",
|
|
79
|
+
"",
|
|
80
|
+
"ASSERTIONS:",
|
|
81
|
+
"- status: exact match on entity status",
|
|
82
|
+
"- type: exact match on entity type",
|
|
83
|
+
"- subtype: exact match on entity subtype",
|
|
84
|
+
"- attributes: key-value pairs that must be present on the entity",
|
|
85
|
+
"- edges: min/max count and status on related entities",
|
|
86
|
+
"",
|
|
87
|
+
"EXAMPLE: Assert order is BOOKED with at least 1 fulfilment:",
|
|
88
|
+
'{ entityType: "ORDER", ref: "HD-001", assertions: { status: "BOOKED", edges: { fulfilments: { minCount: 1 } } } }',
|
|
89
|
+
].join("\n"),
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
entityType: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Entity type (ORDER, FULFILMENT, etc.)",
|
|
96
|
+
},
|
|
97
|
+
id: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Entity ID (preferred)",
|
|
100
|
+
},
|
|
101
|
+
ref: {
|
|
102
|
+
type: "string",
|
|
103
|
+
description: "Entity ref (fallback)",
|
|
104
|
+
},
|
|
105
|
+
assertions: {
|
|
106
|
+
type: "object",
|
|
107
|
+
properties: {
|
|
108
|
+
status: { type: "string", description: "Expected status" },
|
|
109
|
+
type: { type: "string", description: "Expected type" },
|
|
110
|
+
subtype: { type: "string", description: "Expected subtype" },
|
|
111
|
+
attributes: {
|
|
112
|
+
type: "object",
|
|
113
|
+
additionalProperties: true,
|
|
114
|
+
description: "Expected attribute key-value pairs",
|
|
115
|
+
},
|
|
116
|
+
edges: {
|
|
117
|
+
type: "object",
|
|
118
|
+
additionalProperties: {
|
|
119
|
+
type: "object",
|
|
120
|
+
properties: {
|
|
121
|
+
minCount: { type: "integer", minimum: 0 },
|
|
122
|
+
maxCount: { type: "integer", minimum: 0 },
|
|
123
|
+
status: { type: "string" },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
description: "Assertions on entity edges",
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
description: "Assertion conditions",
|
|
130
|
+
},
|
|
131
|
+
poll: {
|
|
132
|
+
type: "boolean",
|
|
133
|
+
description: "Enable polling mode (retry until pass or timeout)",
|
|
134
|
+
},
|
|
135
|
+
timeoutMs: {
|
|
136
|
+
type: "integer",
|
|
137
|
+
minimum: 1000,
|
|
138
|
+
maximum: 300000,
|
|
139
|
+
description: "Polling timeout in ms (default: 60000)",
|
|
140
|
+
},
|
|
141
|
+
intervalMs: {
|
|
142
|
+
type: "integer",
|
|
143
|
+
minimum: 1000,
|
|
144
|
+
maximum: 30000,
|
|
145
|
+
description: "Polling interval in ms (default: 5000)",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
required: ["entityType", "assertions"],
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
function buildEntityQuery(entityType, lookupById, edgeNames) {
|
|
154
|
+
const meta = getEntityMeta(entityType);
|
|
155
|
+
if (!meta) {
|
|
156
|
+
throw new ToolError("VALIDATION_ERROR", `Unsupported entity type: "${entityType}". Supported: ${SUPPORTED_ENTITY_TYPES.join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
const fields = [...meta.defaultFields];
|
|
159
|
+
// Add attributes edge for attribute assertions
|
|
160
|
+
if (!fields.includes("attributes")) {
|
|
161
|
+
fields.push("attributes { name type value }");
|
|
162
|
+
}
|
|
163
|
+
// Add requested edges
|
|
164
|
+
for (const edge of edgeNames) {
|
|
165
|
+
if (meta.edges.includes(edge)) {
|
|
166
|
+
fields.push(`${edge} { edges { node { id ref status type } } }`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const fieldSelection = fields.join("\n ");
|
|
170
|
+
if (lookupById) {
|
|
171
|
+
return `query GetEntity($id: ID!) {
|
|
172
|
+
${meta.queryById}(id: $id) {
|
|
173
|
+
${fieldSelection}
|
|
174
|
+
}
|
|
175
|
+
}`;
|
|
176
|
+
}
|
|
177
|
+
return `query SearchEntity($ref: [String]) {
|
|
178
|
+
${meta.queryRoot}(ref: $ref, first: 1) {
|
|
179
|
+
edges {
|
|
180
|
+
node {
|
|
181
|
+
${fieldSelection}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}`;
|
|
186
|
+
}
|
|
187
|
+
function evaluateAssertions(entity, assertions) {
|
|
188
|
+
const failures = [];
|
|
189
|
+
// Status assertion
|
|
190
|
+
if (assertions.status !== undefined) {
|
|
191
|
+
if (entity.status !== assertions.status) {
|
|
192
|
+
failures.push({
|
|
193
|
+
field: "status",
|
|
194
|
+
expected: assertions.status,
|
|
195
|
+
actual: entity.status,
|
|
196
|
+
message: `Expected status "${assertions.status}" but got "${entity.status}"`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Type assertion
|
|
201
|
+
if (assertions.type !== undefined) {
|
|
202
|
+
if (entity.type !== assertions.type) {
|
|
203
|
+
failures.push({
|
|
204
|
+
field: "type",
|
|
205
|
+
expected: assertions.type,
|
|
206
|
+
actual: entity.type,
|
|
207
|
+
message: `Expected type "${assertions.type}" but got "${entity.type}"`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// Subtype assertion
|
|
212
|
+
if (assertions.subtype !== undefined) {
|
|
213
|
+
if (entity.subtype !== assertions.subtype) {
|
|
214
|
+
failures.push({
|
|
215
|
+
field: "subtype",
|
|
216
|
+
expected: assertions.subtype,
|
|
217
|
+
actual: entity.subtype,
|
|
218
|
+
message: `Expected subtype "${assertions.subtype}" but got "${entity.subtype}"`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// Attribute assertions
|
|
223
|
+
if (assertions.attributes) {
|
|
224
|
+
const entityAttrs = entity.attributes;
|
|
225
|
+
const attrMap = new Map();
|
|
226
|
+
if (Array.isArray(entityAttrs)) {
|
|
227
|
+
for (const attr of entityAttrs) {
|
|
228
|
+
const rec = attr;
|
|
229
|
+
if (rec?.name)
|
|
230
|
+
attrMap.set(rec.name, rec.value);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (entityAttrs && typeof entityAttrs === "object") {
|
|
234
|
+
for (const [key, val] of Object.entries(entityAttrs)) {
|
|
235
|
+
attrMap.set(key, val);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
for (const [key, expectedValue] of Object.entries(assertions.attributes)) {
|
|
239
|
+
const actualValue = attrMap.get(key);
|
|
240
|
+
if (actualValue === undefined) {
|
|
241
|
+
failures.push({
|
|
242
|
+
field: `attributes.${key}`,
|
|
243
|
+
expected: expectedValue,
|
|
244
|
+
actual: undefined,
|
|
245
|
+
message: `Attribute "${key}" not found on entity`,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
else if (JSON.stringify(actualValue) !== JSON.stringify(expectedValue)) {
|
|
249
|
+
failures.push({
|
|
250
|
+
field: `attributes.${key}`,
|
|
251
|
+
expected: expectedValue,
|
|
252
|
+
actual: actualValue,
|
|
253
|
+
message: `Attribute "${key}": expected ${JSON.stringify(expectedValue)} but got ${JSON.stringify(actualValue)}`,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Edge assertions
|
|
259
|
+
if (assertions.edges) {
|
|
260
|
+
for (const [edgeName, edgeAssert] of Object.entries(assertions.edges)) {
|
|
261
|
+
const edgeData = entity[edgeName];
|
|
262
|
+
const edges = edgeData?.edges;
|
|
263
|
+
const nodes = (edges ?? [])
|
|
264
|
+
.map((e) => e?.node)
|
|
265
|
+
.filter(Boolean);
|
|
266
|
+
if (edgeAssert.minCount !== undefined && nodes.length < edgeAssert.minCount) {
|
|
267
|
+
failures.push({
|
|
268
|
+
field: `edges.${edgeName}.count`,
|
|
269
|
+
expected: `>= ${edgeAssert.minCount}`,
|
|
270
|
+
actual: nodes.length,
|
|
271
|
+
message: `Expected at least ${edgeAssert.minCount} ${edgeName} but found ${nodes.length}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
if (edgeAssert.maxCount !== undefined && nodes.length > edgeAssert.maxCount) {
|
|
275
|
+
failures.push({
|
|
276
|
+
field: `edges.${edgeName}.count`,
|
|
277
|
+
expected: `<= ${edgeAssert.maxCount}`,
|
|
278
|
+
actual: nodes.length,
|
|
279
|
+
message: `Expected at most ${edgeAssert.maxCount} ${edgeName} but found ${nodes.length}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
if (edgeAssert.status !== undefined) {
|
|
283
|
+
const nonMatching = nodes.filter((n) => n.status !== edgeAssert.status);
|
|
284
|
+
if (nonMatching.length > 0) {
|
|
285
|
+
failures.push({
|
|
286
|
+
field: `edges.${edgeName}.status`,
|
|
287
|
+
expected: edgeAssert.status,
|
|
288
|
+
actual: nonMatching.map((n) => n.status),
|
|
289
|
+
message: `Expected all ${edgeName} to have status "${edgeAssert.status}" but ${nonMatching.length} do not`,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return failures;
|
|
296
|
+
}
|
|
297
|
+
function requireTestClient(ctx) {
|
|
298
|
+
if (!ctx.client) {
|
|
299
|
+
throw new ToolError("CONFIG_ERROR", "SDK client is not available. Run config.validate and fix auth/base URL.");
|
|
300
|
+
}
|
|
301
|
+
return ctx.client;
|
|
302
|
+
}
|
|
303
|
+
async function fetchEntity(client, entityType, id, ref, edgeNames) {
|
|
304
|
+
const meta = getEntityMeta(entityType);
|
|
305
|
+
if (!meta)
|
|
306
|
+
return null;
|
|
307
|
+
const lookupById = !!id;
|
|
308
|
+
const query = buildEntityQuery(entityType, lookupById, edgeNames);
|
|
309
|
+
const variables = lookupById
|
|
310
|
+
? { id }
|
|
311
|
+
: { ref: [ref] };
|
|
312
|
+
const response = await client.graphql({
|
|
313
|
+
query,
|
|
314
|
+
variables: variables,
|
|
315
|
+
});
|
|
316
|
+
const data = response?.data;
|
|
317
|
+
if (lookupById) {
|
|
318
|
+
return data?.[meta.queryById] ?? null;
|
|
319
|
+
}
|
|
320
|
+
const connection = data?.[meta.queryRoot];
|
|
321
|
+
const edges = connection?.edges;
|
|
322
|
+
return edges?.[0]?.node ?? null;
|
|
323
|
+
}
|
|
324
|
+
function sleep(ms) {
|
|
325
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Handle test.assert tool call.
|
|
329
|
+
*/
|
|
330
|
+
export async function handleTestAssert(args, ctx) {
|
|
331
|
+
const parsed = TestAssertInputSchema.parse(args);
|
|
332
|
+
if (!parsed.id && !parsed.ref) {
|
|
333
|
+
throw new ToolError("VALIDATION_ERROR", "Either id or ref must be provided for entity lookup.");
|
|
334
|
+
}
|
|
335
|
+
const client = requireTestClient(ctx);
|
|
336
|
+
const edgeNames = parsed.assertions.edges
|
|
337
|
+
? Object.keys(parsed.assertions.edges)
|
|
338
|
+
: [];
|
|
339
|
+
const startTime = Date.now();
|
|
340
|
+
let attempts = 0;
|
|
341
|
+
let lastFailures = [];
|
|
342
|
+
let entity = null;
|
|
343
|
+
do {
|
|
344
|
+
attempts++;
|
|
345
|
+
entity = await fetchEntity(client, parsed.entityType, parsed.id, parsed.ref, edgeNames);
|
|
346
|
+
if (!entity) {
|
|
347
|
+
if (!parsed.poll || Date.now() - startTime >= parsed.timeoutMs) {
|
|
348
|
+
return {
|
|
349
|
+
ok: true,
|
|
350
|
+
passed: false,
|
|
351
|
+
found: false,
|
|
352
|
+
entityType: parsed.entityType,
|
|
353
|
+
lookupBy: parsed.id ? "id" : "ref",
|
|
354
|
+
lookupValue: parsed.id ?? parsed.ref,
|
|
355
|
+
attempts,
|
|
356
|
+
message: "Entity not found",
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
await sleep(parsed.intervalMs);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
lastFailures = evaluateAssertions(entity, parsed.assertions);
|
|
363
|
+
if (lastFailures.length === 0) {
|
|
364
|
+
return {
|
|
365
|
+
ok: true,
|
|
366
|
+
passed: true,
|
|
367
|
+
entityType: parsed.entityType,
|
|
368
|
+
entity,
|
|
369
|
+
attempts,
|
|
370
|
+
durationMs: Date.now() - startTime,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
if (!parsed.poll || Date.now() - startTime >= parsed.timeoutMs) {
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
await sleep(parsed.intervalMs);
|
|
377
|
+
} while (Date.now() - startTime < parsed.timeoutMs);
|
|
378
|
+
return {
|
|
379
|
+
ok: true,
|
|
380
|
+
passed: false,
|
|
381
|
+
entityType: parsed.entityType,
|
|
382
|
+
entity,
|
|
383
|
+
failures: lastFailures,
|
|
384
|
+
attempts,
|
|
385
|
+
durationMs: Date.now() - startTime,
|
|
386
|
+
message: `${lastFailures.length} assertion(s) failed after ${attempts} attempt(s)`,
|
|
387
|
+
};
|
|
388
|
+
}
|