@camunda/e2e-test-suite 0.0.1190 → 0.0.1192
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.10/LoginPage.js +12 -9
- package/dist/pages/8.10/UtilitiesPage.js +3 -7
- package/dist/pages/8.8/LoginPage.js +3 -2
- package/dist/pages/8.8/UtilitiesPage.js +3 -7
- package/dist/pages/8.9/LoginPage.js +8 -3
- package/dist/pages/8.9/UtilitiesPage.js +3 -7
- package/dist/resources/mcp_server/mcp_remote_client_meta.bpmn +71 -0
- package/dist/tests/c8Run-8.10/mcp-meta.spec.d.ts +1 -0
- package/dist/tests/c8Run-8.10/mcp-meta.spec.js +73 -0
- package/dist/utils/authErrorClassification.d.ts +8 -0
- package/dist/utils/authErrorClassification.js +95 -0
- package/dist/utils/utils.d.ts +2 -1
- package/dist/utils/utils.js +5 -13
- package/package.json +1 -1
- package/resources/mcp_server/mcp_remote_client_meta.bpmn +71 -0
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
6
7
|
const userNameInputValidInputSchema = {
|
|
7
8
|
_schema: {
|
|
8
9
|
maxlength: 254,
|
|
@@ -47,9 +48,14 @@ class LoginPage {
|
|
|
47
48
|
// missing this detection entirely, so a rate-limited login here fell
|
|
48
49
|
// through to the generic 10-20s retry backoff instead of the 60-90s
|
|
49
50
|
// needed to outlast a typical rate-limit window.
|
|
51
|
+
//
|
|
52
|
+
// Kept as one locator because every caller wants to stop waiting on
|
|
53
|
+
// either page; which of the two rendered is decided at throw time by
|
|
54
|
+
// classifyAuthErrorPage, since only that distinction says whether the
|
|
55
|
+
// remedy is a higher limit or an allow-listed runner IP.
|
|
50
56
|
this.rateLimitError = page
|
|
51
|
-
.getByText(
|
|
52
|
-
.or(page.getByText(
|
|
57
|
+
.getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
|
|
58
|
+
.or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
|
|
53
59
|
this.loginButton = page.getByRole('button', {
|
|
54
60
|
name: 'Continue',
|
|
55
61
|
exact: true,
|
|
@@ -106,13 +112,10 @@ class LoginPage {
|
|
|
106
112
|
}
|
|
107
113
|
// Surface a rate-limit page with the marker loginWithRetry keys its
|
|
108
114
|
// 60-90s backoff on, rather than burning the remaining attempts on a
|
|
109
|
-
// form the provider is not going to serve.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.catch(() => false)) {
|
|
114
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
115
|
-
}
|
|
115
|
+
// form the provider is not going to serve. The marker also records
|
|
116
|
+
// which of the two Auth0 failure modes this was, and the error copy
|
|
117
|
+
// behind it, so a recurrence can be diagnosed from the run log alone.
|
|
118
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(this.page);
|
|
116
119
|
// Neither the password step nor the email step is on screen: some
|
|
117
120
|
// interstitial we do not model. Hand back to fillPassword's own wait
|
|
118
121
|
// instead of re-typing blindly into a page we cannot identify.
|
|
@@ -6,6 +6,7 @@ const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
|
6
6
|
const HomePage_1 = require("./HomePage");
|
|
7
7
|
const sleep_1 = require("../../utils/sleep");
|
|
8
8
|
const randomSleep_1 = require("../../utils/randomSleep");
|
|
9
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
9
10
|
const LoginPage_1 = require("./LoginPage");
|
|
10
11
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
11
12
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
@@ -53,12 +54,7 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
|
|
|
53
54
|
// whole budget waiting for a login form that will never appear. Bailing
|
|
54
55
|
// out immediately with a typed marker lets loginWithRetry's caller back
|
|
55
56
|
// off long enough to actually outlast the rate-limit window instead.
|
|
56
|
-
|
|
57
|
-
.first()
|
|
58
|
-
.isVisible()
|
|
59
|
-
.catch(() => false)) {
|
|
60
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
61
|
-
}
|
|
57
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
62
58
|
try {
|
|
63
59
|
// Confirm we reached the login page before filling credentials.
|
|
64
60
|
// Without this check the locator for 'Email address' can match a hidden
|
|
@@ -172,7 +168,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
172
168
|
catch (error) {
|
|
173
169
|
lastError = error;
|
|
174
170
|
if (attempt < maxRetries - 1) {
|
|
175
|
-
const rateLimited = String(error).includes(
|
|
171
|
+
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
176
172
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
177
173
|
if (rateLimited) {
|
|
178
174
|
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
7
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
7
8
|
class LoginPage {
|
|
@@ -37,8 +38,8 @@ class LoginPage {
|
|
|
37
38
|
});
|
|
38
39
|
this.errorMessage = page.getByText('Wrong email or password');
|
|
39
40
|
this.rateLimitError = page
|
|
40
|
-
.getByText(
|
|
41
|
-
.or(page.getByText(
|
|
41
|
+
.getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
|
|
42
|
+
.or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
|
|
42
43
|
}
|
|
43
44
|
async fillUsername(username) {
|
|
44
45
|
await this.usernameInput.waitFor({ state: 'visible', timeout: 60000 });
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.clickInvitationLinkInEmail = exports.assertLatestAlertEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.modelDiagramFromFile = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.loginWithRetry = exports.forceLogoutIfNeeded = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
6
7
|
const HomePage_1 = require("./HomePage");
|
|
7
8
|
const sleep_1 = require("../../utils/sleep");
|
|
@@ -80,12 +81,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
80
81
|
// the usual auth controls. Bail out early on it so the outer retry backs
|
|
81
82
|
// off long enough to outlast the window instead of burning attempts
|
|
82
83
|
// against a 60s wait for a login form that will never render.
|
|
83
|
-
|
|
84
|
-
.first()
|
|
85
|
-
.isVisible()
|
|
86
|
-
.catch(() => false)) {
|
|
87
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
88
|
-
}
|
|
84
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
89
85
|
await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
|
|
90
86
|
await loginPage.loginWithTestUser(testUser);
|
|
91
87
|
return;
|
|
@@ -93,7 +89,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
93
89
|
catch (error) {
|
|
94
90
|
lastError = error;
|
|
95
91
|
if (attempt < maxRetries - 1) {
|
|
96
|
-
const rateLimited = String(error).includes(
|
|
92
|
+
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
97
93
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
98
94
|
if (rateLimited) {
|
|
99
95
|
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
7
|
const userNameInputValidInputSchema = {
|
|
7
8
|
_schema: {
|
|
@@ -46,8 +47,8 @@ class LoginPage {
|
|
|
46
47
|
});
|
|
47
48
|
this.errorMessage = page.getByText('Wrong email or password');
|
|
48
49
|
this.rateLimitError = page
|
|
49
|
-
.getByText(
|
|
50
|
-
.or(page.getByText(
|
|
50
|
+
.getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
|
|
51
|
+
.or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
|
|
51
52
|
}
|
|
52
53
|
async fillUsername(username) {
|
|
53
54
|
await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 120000 });
|
|
@@ -127,7 +128,11 @@ class LoginPage {
|
|
|
127
128
|
.catch(() => false),
|
|
128
129
|
]);
|
|
129
130
|
if (rateLimitVisible) {
|
|
130
|
-
|
|
131
|
+
// Classify which of the two Auth0 failure modes this was, and capture
|
|
132
|
+
// the error copy, so a recurrence is diagnosable from the run log.
|
|
133
|
+
// Falls back to the bare marker if the page moved on in between.
|
|
134
|
+
throw new Error((await (0, authErrorClassification_1.classifyAuthErrorPage)(this.page)) ??
|
|
135
|
+
`${authErrorClassification_1.AUTH0_RATE_LIMIT}: identity provider returned an error page`);
|
|
131
136
|
}
|
|
132
137
|
if (passwordHeadingVisible || passwordInputVisible) {
|
|
133
138
|
authStep = 'password';
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.loginWithRetry = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const authErrorClassification_1 = require("../../utils/authErrorClassification");
|
|
5
6
|
const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
6
7
|
const HomePage_1 = require("./HomePage");
|
|
7
8
|
const sleep_1 = require("../../utils/sleep");
|
|
@@ -51,12 +52,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
51
52
|
// The identity provider occasionally returns a generic error / rate
|
|
52
53
|
// limit page that has none of the usual auth controls. Bail out early
|
|
53
54
|
// when that page is visible so the outer retry can back off.
|
|
54
|
-
|
|
55
|
-
.first()
|
|
56
|
-
.isVisible()
|
|
57
|
-
.catch(() => false)) {
|
|
58
|
-
throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
|
|
59
|
-
}
|
|
55
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
60
56
|
// Auth0 may restore directly on password entry depending on session
|
|
61
57
|
// history, so allow either identifier screen or password screen.
|
|
62
58
|
await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
|
|
@@ -66,7 +62,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
66
62
|
catch (error) {
|
|
67
63
|
lastError = error;
|
|
68
64
|
if (attempt < maxRetries - 1) {
|
|
69
|
-
const rateLimited = String(error).includes(
|
|
65
|
+
const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
|
|
70
66
|
console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
|
|
71
67
|
if (rateLimited) {
|
|
72
68
|
// Auth0 rate-limit windows commonly last 30-90s. Back off long
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
3
|
+
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
|
4
|
+
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
|
5
|
+
xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
|
|
6
|
+
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
|
|
7
|
+
xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_mcp_meta"
|
|
8
|
+
targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.42.0"
|
|
9
|
+
modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.9.0">
|
|
10
|
+
<bpmn:process id="mcp_remote_client_meta" name="mcp_remote_client_meta" isExecutable="true">
|
|
11
|
+
<bpmn:startEvent id="StartEvent_1">
|
|
12
|
+
<bpmn:outgoing>Flow_start_to_echo</bpmn:outgoing>
|
|
13
|
+
</bpmn:startEvent>
|
|
14
|
+
<bpmn:serviceTask id="Activity_echo_meta"
|
|
15
|
+
zeebe:modelerTemplate="io.camunda.connectors.agenticai.mcp.remoteclient.v0"
|
|
16
|
+
zeebe:modelerTemplateVersion="3">
|
|
17
|
+
<bpmn:extensionElements>
|
|
18
|
+
<zeebe:taskDefinition type="io.camunda.agenticai:mcpremoteclient:1" retries="3" />
|
|
19
|
+
<zeebe:ioMapping>
|
|
20
|
+
<zeebe:input source="http" target="data.transport.type" />
|
|
21
|
+
<zeebe:input source="http://localhost:12001/mcp"
|
|
22
|
+
target="data.transport.http.url" />
|
|
23
|
+
<zeebe:input source="noAuth" target="data.transport.http.authentication.type" />
|
|
24
|
+
<zeebe:input source="=false" target="data.options.clientCache" />
|
|
25
|
+
<zeebe:input source="standalone" target="data.connectorMode.type" />
|
|
26
|
+
<zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
|
|
27
|
+
<zeebe:input source="echoMeta" target="data.connectorMode.operation.toolName" />
|
|
28
|
+
<zeebe:input source="={exampleMetaKey: "exampleMetaValue"}"
|
|
29
|
+
target="data.connectorMode.meta" />
|
|
30
|
+
</zeebe:ioMapping>
|
|
31
|
+
<zeebe:taskHeaders>
|
|
32
|
+
<zeebe:header key="elementTemplateVersion" value="3" />
|
|
33
|
+
<zeebe:header key="elementTemplateId"
|
|
34
|
+
value="io.camunda.connectors.agenticai.mcp.remoteclient.v0" />
|
|
35
|
+
<zeebe:header key="resultVariable" value="echoMetaResult" />
|
|
36
|
+
<zeebe:header key="retryBackoff" value="PT0S" />
|
|
37
|
+
</zeebe:taskHeaders>
|
|
38
|
+
</bpmn:extensionElements>
|
|
39
|
+
<bpmn:incoming>Flow_start_to_echo</bpmn:incoming>
|
|
40
|
+
<bpmn:outgoing>Flow_echo_to_end</bpmn:outgoing>
|
|
41
|
+
</bpmn:serviceTask>
|
|
42
|
+
<bpmn:endEvent id="Event_end">
|
|
43
|
+
<bpmn:incoming>Flow_echo_to_end</bpmn:incoming>
|
|
44
|
+
</bpmn:endEvent>
|
|
45
|
+
<bpmn:sequenceFlow id="Flow_start_to_echo" sourceRef="StartEvent_1"
|
|
46
|
+
targetRef="Activity_echo_meta" />
|
|
47
|
+
<bpmn:sequenceFlow id="Flow_echo_to_end" sourceRef="Activity_echo_meta"
|
|
48
|
+
targetRef="Event_end" />
|
|
49
|
+
</bpmn:process>
|
|
50
|
+
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
|
51
|
+
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="mcp_remote_client_meta">
|
|
52
|
+
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
|
53
|
+
<dc:Bounds x="152" y="99" width="36" height="36" />
|
|
54
|
+
</bpmndi:BPMNShape>
|
|
55
|
+
<bpmndi:BPMNShape id="Activity_echo_meta_di" bpmnElement="Activity_echo_meta">
|
|
56
|
+
<dc:Bounds x="250" y="77" width="100" height="80" />
|
|
57
|
+
</bpmndi:BPMNShape>
|
|
58
|
+
<bpmndi:BPMNShape id="Event_end_di" bpmnElement="Event_end">
|
|
59
|
+
<dc:Bounds x="412" y="99" width="36" height="36" />
|
|
60
|
+
</bpmndi:BPMNShape>
|
|
61
|
+
<bpmndi:BPMNEdge id="Flow_start_to_echo_di" bpmnElement="Flow_start_to_echo">
|
|
62
|
+
<di:waypoint x="188" y="117" />
|
|
63
|
+
<di:waypoint x="250" y="117" />
|
|
64
|
+
</bpmndi:BPMNEdge>
|
|
65
|
+
<bpmndi:BPMNEdge id="Flow_echo_to_end_di" bpmnElement="Flow_echo_to_end">
|
|
66
|
+
<di:waypoint x="350" y="117" />
|
|
67
|
+
<di:waypoint x="412" y="117" />
|
|
68
|
+
</bpmndi:BPMNEdge>
|
|
69
|
+
</bpmndi:BPMNPlane>
|
|
70
|
+
</bpmndi:BPMNDiagram>
|
|
71
|
+
</bpmn:definitions>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const promises_1 = require("node:fs/promises");
|
|
4
|
+
const node_os_1 = require("node:os");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const c8Run_8_10_1 = require("../../fixtures/c8Run-8.10");
|
|
7
|
+
const test_1 = require("@playwright/test");
|
|
8
|
+
const UtilitiesPage_1 = require("../../pages/c8Run-8.10/UtilitiesPage");
|
|
9
|
+
const _setup_1 = require("../../test-setup.js");
|
|
10
|
+
const apiHelpers_1 = require("../../utils/apiHelpers");
|
|
11
|
+
const zeebeClient_1 = require("../../utils/zeebeClient");
|
|
12
|
+
let processId;
|
|
13
|
+
let bpmnPath;
|
|
14
|
+
c8Run_8_10_1.test.beforeAll(async () => {
|
|
15
|
+
if (process.env.MCP_SERVER_AVAILABLE !== 'true') {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await (0, apiHelpers_1.validateMcpServerHealth)();
|
|
19
|
+
await (0, apiHelpers_1.waitForConnectorsReady)(undefined, 300000, [
|
|
20
|
+
'io.camunda.agenticai:mcpremoteclient:1',
|
|
21
|
+
]);
|
|
22
|
+
processId = `mcp_remote_client_meta_${await (0, _setup_1.generateRandomStringAsync)(6)}`;
|
|
23
|
+
const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'mcp-meta-'));
|
|
24
|
+
bpmnPath = (0, node_path_1.join)(tempDir, 'mcp_remote_client_meta.bpmn');
|
|
25
|
+
const bpmn = await (0, promises_1.readFile)('./resources/mcp_server/mcp_remote_client_meta.bpmn', 'utf8');
|
|
26
|
+
await (0, promises_1.writeFile)(bpmnPath, bpmn.replaceAll('mcp_remote_client_meta', processId));
|
|
27
|
+
await (0, zeebeClient_1.deploy)([bpmnPath]);
|
|
28
|
+
await (0, zeebeClient_1.createInstances)(processId, 1, 1);
|
|
29
|
+
});
|
|
30
|
+
c8Run_8_10_1.test.afterAll(async () => {
|
|
31
|
+
if (bpmnPath) {
|
|
32
|
+
await (0, promises_1.rm)((0, node_path_1.dirname)(bpmnPath), { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
c8Run_8_10_1.test.describe('@tasklistV2 MCP Client connector _meta tests', () => {
|
|
36
|
+
c8Run_8_10_1.test.skip(process.env.MCP_SERVER_AVAILABLE !== 'true', 'MCP server is only available on Linux and macOS CI runners');
|
|
37
|
+
c8Run_8_10_1.test.beforeEach(async ({ page, operateLoginPage, operateHomePage }) => {
|
|
38
|
+
await (0, UtilitiesPage_1.navigateToApp)(page, 'operate');
|
|
39
|
+
await operateLoginPage.login('demo', 'demo');
|
|
40
|
+
await operateHomePage.operateBannerIsVisible();
|
|
41
|
+
});
|
|
42
|
+
c8Run_8_10_1.test.afterEach(async ({ page }, testInfo) => {
|
|
43
|
+
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
44
|
+
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
45
|
+
});
|
|
46
|
+
(0, c8Run_8_10_1.test)('As a user I can forward MCP _meta and have it echoed back by the server', async ({ operateHomePage, operateProcessesPage, operateProcessInstancePage, }) => {
|
|
47
|
+
c8Run_8_10_1.test.slow();
|
|
48
|
+
await c8Run_8_10_1.test.step('Navigate to completed process instance', async () => {
|
|
49
|
+
await operateHomePage.clickProcessesTab();
|
|
50
|
+
await operateProcessesPage.clickProcessCompletedCheckbox();
|
|
51
|
+
await operateProcessesPage.clickProcessInstanceLink(processId);
|
|
52
|
+
await operateProcessInstancePage.completedIconAssertion();
|
|
53
|
+
});
|
|
54
|
+
await c8Run_8_10_1.test.step('Verify echoMetaResult reflects the configured _meta', async () => {
|
|
55
|
+
await operateProcessInstancePage.verifyVariableJsonContent('echoMetaResult', (value) => {
|
|
56
|
+
const data = value;
|
|
57
|
+
(0, test_1.expect)(data.name).toBe('echoMeta');
|
|
58
|
+
(0, test_1.expect)(data.isError).toBe(false);
|
|
59
|
+
(0, test_1.expect)(data.content).toBeDefined();
|
|
60
|
+
(0, test_1.expect)(data.content.length).toBeGreaterThan(0);
|
|
61
|
+
(0, test_1.expect)(data.content[0].type).toBe('text');
|
|
62
|
+
// The mcp-test-server echoMeta tool returns the request `_meta`
|
|
63
|
+
// serialized verbatim as a JSON string. The MCP client forwards
|
|
64
|
+
// `data.connectorMode.meta` unmodified as the request `_meta`, so a
|
|
65
|
+
// successful round-trip proves end-to-end `_meta` propagation.
|
|
66
|
+
const echoed = JSON.parse(data.content[0].text);
|
|
67
|
+
(0, test_1.expect)(echoed).toMatchObject({
|
|
68
|
+
exampleMetaKey: 'exampleMetaValue',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
export declare const AUTH0_RATE_LIMIT = "AUTH0_RATE_LIMIT";
|
|
3
|
+
export declare const AUTH0_RATE_LIMIT_QUOTA = "AUTH0_RATE_LIMIT_QUOTA";
|
|
4
|
+
export declare const AUTH0_RATE_LIMIT_GENERIC = "AUTH0_RATE_LIMIT_GENERIC";
|
|
5
|
+
export declare const AUTH0_QUOTA_TEXT: RegExp;
|
|
6
|
+
export declare const AUTH0_GENERIC_ERROR_TEXT: RegExp;
|
|
7
|
+
export declare function classifyAuthErrorPage(page: Page): Promise<string | null>;
|
|
8
|
+
export declare function throwIfAuthErrorPage(page: Page): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.throwIfAuthErrorPage = exports.classifyAuthErrorPage = exports.AUTH0_GENERIC_ERROR_TEXT = exports.AUTH0_QUOTA_TEXT = exports.AUTH0_RATE_LIMIT_GENERIC = exports.AUTH0_RATE_LIMIT_QUOTA = exports.AUTH0_RATE_LIMIT = void 0;
|
|
4
|
+
// Auth0 renders an error page for two structurally different failures, and
|
|
5
|
+
// until now both were counted as one signal. They need different responses:
|
|
6
|
+
//
|
|
7
|
+
// QUOTA - the tenant's request rate limit ("...rate limit has been
|
|
8
|
+
// reached"). This is what moving the tenant to a Production
|
|
9
|
+
// environment raises.
|
|
10
|
+
// GENERIC - Auth0's catch-all error page. Attack Protection blocks
|
|
11
|
+
// (suspicious IP throttling, brute-force protection) surface here,
|
|
12
|
+
// and those are governed by their own settings rather than the
|
|
13
|
+
// tenant's rate limits, so a Production environment does nothing
|
|
14
|
+
// for them. A CI runner signing in as ~19 freshly created accounts
|
|
15
|
+
// from one egress IP inside a few minutes is precisely the pattern
|
|
16
|
+
// they exist to detect.
|
|
17
|
+
//
|
|
18
|
+
// Telling them apart matters because the remedies do not overlap: one is a
|
|
19
|
+
// limit to raise, the other is a source IP to allow-list. Counting them as one
|
|
20
|
+
// number is why we could not say which we were hitting.
|
|
21
|
+
//
|
|
22
|
+
// Both markers keep the AUTH0_RATE_LIMIT prefix, so the existing
|
|
23
|
+
// String(error).includes(AUTH0_RATE_LIMIT) backoff checks match unchanged.
|
|
24
|
+
exports.AUTH0_RATE_LIMIT = 'AUTH0_RATE_LIMIT';
|
|
25
|
+
exports.AUTH0_RATE_LIMIT_QUOTA = `${exports.AUTH0_RATE_LIMIT}_QUOTA`;
|
|
26
|
+
exports.AUTH0_RATE_LIMIT_GENERIC = `${exports.AUTH0_RATE_LIMIT}_GENERIC`;
|
|
27
|
+
exports.AUTH0_QUOTA_TEXT = /rate limit has been reached/i;
|
|
28
|
+
exports.AUTH0_GENERIC_ERROR_TEXT = /We are sorry, an error occurred/i;
|
|
29
|
+
const MAX_DETAIL_CHARS = 300;
|
|
30
|
+
// The error page's own copy and error code are what separate the two
|
|
31
|
+
// mechanisms, so capture them rather than only the fact that some error
|
|
32
|
+
// rendered. Deliberately narrow: host plus path, and Auth0's two error query
|
|
33
|
+
// params, never the whole URL - on an OAuth redirect that can carry `code` and
|
|
34
|
+
// `state`, which do not belong in a CI log.
|
|
35
|
+
async function readErrorContext(page) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
try {
|
|
38
|
+
const url = new URL(page.url());
|
|
39
|
+
parts.push(`${url.host}${url.pathname}`);
|
|
40
|
+
for (const key of ['error', 'error_description']) {
|
|
41
|
+
const value = url.searchParams.get(key);
|
|
42
|
+
if (value) {
|
|
43
|
+
parts.push(`${key}=${value}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// page.url() is not always parsable (about:blank on a torn-down context).
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const text = await page.locator('body').innerText({ timeout: 5000 });
|
|
52
|
+
const collapsed = text.replace(/\s+/g, ' ').trim();
|
|
53
|
+
if (collapsed) {
|
|
54
|
+
parts.push(`text="${collapsed.slice(0, MAX_DETAIL_CHARS)}"`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Best-effort only: this runs on a failure path and must never replace
|
|
59
|
+
// the caller's diagnosis with its own.
|
|
60
|
+
}
|
|
61
|
+
return parts.join(' | ');
|
|
62
|
+
}
|
|
63
|
+
// Returns the message to throw when an Auth0 error page is on screen, or null
|
|
64
|
+
// when it is not. Never throws, for the reason above.
|
|
65
|
+
async function classifyAuthErrorPage(page) {
|
|
66
|
+
const [quota, generic] = await Promise.all([
|
|
67
|
+
page
|
|
68
|
+
.getByText(exports.AUTH0_QUOTA_TEXT)
|
|
69
|
+
.first()
|
|
70
|
+
.isVisible()
|
|
71
|
+
.catch(() => false),
|
|
72
|
+
page
|
|
73
|
+
.getByText(exports.AUTH0_GENERIC_ERROR_TEXT)
|
|
74
|
+
.first()
|
|
75
|
+
.isVisible()
|
|
76
|
+
.catch(() => false),
|
|
77
|
+
]);
|
|
78
|
+
if (!quota && !generic) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
// Quota wins when both match: its copy is the specific diagnosis, and the
|
|
82
|
+
// generic banner can wrap it.
|
|
83
|
+
const marker = quota ? exports.AUTH0_RATE_LIMIT_QUOTA : exports.AUTH0_RATE_LIMIT_GENERIC;
|
|
84
|
+
const context = await readErrorContext(page);
|
|
85
|
+
const summary = `${marker}: identity provider returned an error page`;
|
|
86
|
+
return context ? `${summary} | ${context}` : summary;
|
|
87
|
+
}
|
|
88
|
+
exports.classifyAuthErrorPage = classifyAuthErrorPage;
|
|
89
|
+
async function throwIfAuthErrorPage(page) {
|
|
90
|
+
const message = await classifyAuthErrorPage(page);
|
|
91
|
+
if (message) {
|
|
92
|
+
throw new Error(message);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.throwIfAuthErrorPage = throwIfAuthErrorPage;
|
package/dist/utils/utils.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { Page } from '@playwright/test';
|
|
2
|
-
|
|
2
|
+
import { AUTH0_RATE_LIMIT } from './authErrorClassification';
|
|
3
|
+
export { AUTH0_RATE_LIMIT };
|
|
3
4
|
export declare function loginToApp(appName: string, page: Page): Promise<void>;
|
package/dist/utils/utils.js
CHANGED
|
@@ -3,16 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.loginToApp = exports.AUTH0_RATE_LIMIT = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const LoginPage_1 = require("../pages/8.8/LoginPage");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const rateLimited = await loginPage.rateLimitError
|
|
9
|
-
.first()
|
|
10
|
-
.isVisible()
|
|
11
|
-
.catch(() => false);
|
|
12
|
-
if (rateLimited) {
|
|
13
|
-
throw new Error(`${exports.AUTH0_RATE_LIMIT}: identity provider returned an error page`);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
6
|
+
const authErrorClassification_1 = require("./authErrorClassification");
|
|
7
|
+
Object.defineProperty(exports, "AUTH0_RATE_LIMIT", { enumerable: true, get: function () { return authErrorClassification_1.AUTH0_RATE_LIMIT; } });
|
|
16
8
|
async function loginToApp(appName, page) {
|
|
17
9
|
await page.goto(appName, { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
18
10
|
// 8.8 SaaS occasionally experiences 429s on app.js during navigation rendering,
|
|
@@ -25,13 +17,13 @@ async function loginToApp(appName, page) {
|
|
|
25
17
|
// caller can tell a rate limit apart from a slow login form, and the caller
|
|
26
18
|
// needs that distinction to pick a backoff long enough to outlast the window.
|
|
27
19
|
await (0, test_1.expect)(loginPage.usernameInput.or(loginPage.rateLimitError.first())).toBeVisible({ timeout: 60000 });
|
|
28
|
-
await
|
|
20
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
29
21
|
await loginPage.fillUsername(process.env.C8_USERNAME);
|
|
30
22
|
await (0, test_1.expect)(loginPage.usernameInput).toHaveValue(process.env.C8_USERNAME);
|
|
31
23
|
await (0, test_1.expect)(loginPage.loginMessage).toBeVisible();
|
|
32
24
|
await loginPage.clickContinueButton();
|
|
33
25
|
await (0, test_1.expect)(loginPage.passwordInput.or(loginPage.rateLimitError.first())).toBeVisible({ timeout: 90000 });
|
|
34
|
-
await
|
|
26
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
35
27
|
await loginPage.fillPassword(process.env.C8_PASSWORD);
|
|
36
28
|
await (0, test_1.expect)(loginPage.passwordHeading).toBeVisible({ timeout: 90000 });
|
|
37
29
|
await loginPage.clickLoginButton();
|
|
@@ -44,7 +36,7 @@ async function loginToApp(appName, page) {
|
|
|
44
36
|
catch (error) {
|
|
45
37
|
// Auth0 can also serve the rate-limit page as the response to the password
|
|
46
38
|
// submit, in which case the redirect back to appName never happens.
|
|
47
|
-
await
|
|
39
|
+
await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
|
|
48
40
|
throw error;
|
|
49
41
|
}
|
|
50
42
|
await page
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
|
3
|
+
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
|
4
|
+
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
|
|
5
|
+
xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
|
|
6
|
+
xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
|
|
7
|
+
xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_mcp_meta"
|
|
8
|
+
targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.42.0"
|
|
9
|
+
modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.9.0">
|
|
10
|
+
<bpmn:process id="mcp_remote_client_meta" name="mcp_remote_client_meta" isExecutable="true">
|
|
11
|
+
<bpmn:startEvent id="StartEvent_1">
|
|
12
|
+
<bpmn:outgoing>Flow_start_to_echo</bpmn:outgoing>
|
|
13
|
+
</bpmn:startEvent>
|
|
14
|
+
<bpmn:serviceTask id="Activity_echo_meta"
|
|
15
|
+
zeebe:modelerTemplate="io.camunda.connectors.agenticai.mcp.remoteclient.v0"
|
|
16
|
+
zeebe:modelerTemplateVersion="3">
|
|
17
|
+
<bpmn:extensionElements>
|
|
18
|
+
<zeebe:taskDefinition type="io.camunda.agenticai:mcpremoteclient:1" retries="3" />
|
|
19
|
+
<zeebe:ioMapping>
|
|
20
|
+
<zeebe:input source="http" target="data.transport.type" />
|
|
21
|
+
<zeebe:input source="http://localhost:12001/mcp"
|
|
22
|
+
target="data.transport.http.url" />
|
|
23
|
+
<zeebe:input source="noAuth" target="data.transport.http.authentication.type" />
|
|
24
|
+
<zeebe:input source="=false" target="data.options.clientCache" />
|
|
25
|
+
<zeebe:input source="standalone" target="data.connectorMode.type" />
|
|
26
|
+
<zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
|
|
27
|
+
<zeebe:input source="echoMeta" target="data.connectorMode.operation.toolName" />
|
|
28
|
+
<zeebe:input source="={exampleMetaKey: "exampleMetaValue"}"
|
|
29
|
+
target="data.connectorMode.meta" />
|
|
30
|
+
</zeebe:ioMapping>
|
|
31
|
+
<zeebe:taskHeaders>
|
|
32
|
+
<zeebe:header key="elementTemplateVersion" value="3" />
|
|
33
|
+
<zeebe:header key="elementTemplateId"
|
|
34
|
+
value="io.camunda.connectors.agenticai.mcp.remoteclient.v0" />
|
|
35
|
+
<zeebe:header key="resultVariable" value="echoMetaResult" />
|
|
36
|
+
<zeebe:header key="retryBackoff" value="PT0S" />
|
|
37
|
+
</zeebe:taskHeaders>
|
|
38
|
+
</bpmn:extensionElements>
|
|
39
|
+
<bpmn:incoming>Flow_start_to_echo</bpmn:incoming>
|
|
40
|
+
<bpmn:outgoing>Flow_echo_to_end</bpmn:outgoing>
|
|
41
|
+
</bpmn:serviceTask>
|
|
42
|
+
<bpmn:endEvent id="Event_end">
|
|
43
|
+
<bpmn:incoming>Flow_echo_to_end</bpmn:incoming>
|
|
44
|
+
</bpmn:endEvent>
|
|
45
|
+
<bpmn:sequenceFlow id="Flow_start_to_echo" sourceRef="StartEvent_1"
|
|
46
|
+
targetRef="Activity_echo_meta" />
|
|
47
|
+
<bpmn:sequenceFlow id="Flow_echo_to_end" sourceRef="Activity_echo_meta"
|
|
48
|
+
targetRef="Event_end" />
|
|
49
|
+
</bpmn:process>
|
|
50
|
+
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
|
51
|
+
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="mcp_remote_client_meta">
|
|
52
|
+
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
|
53
|
+
<dc:Bounds x="152" y="99" width="36" height="36" />
|
|
54
|
+
</bpmndi:BPMNShape>
|
|
55
|
+
<bpmndi:BPMNShape id="Activity_echo_meta_di" bpmnElement="Activity_echo_meta">
|
|
56
|
+
<dc:Bounds x="250" y="77" width="100" height="80" />
|
|
57
|
+
</bpmndi:BPMNShape>
|
|
58
|
+
<bpmndi:BPMNShape id="Event_end_di" bpmnElement="Event_end">
|
|
59
|
+
<dc:Bounds x="412" y="99" width="36" height="36" />
|
|
60
|
+
</bpmndi:BPMNShape>
|
|
61
|
+
<bpmndi:BPMNEdge id="Flow_start_to_echo_di" bpmnElement="Flow_start_to_echo">
|
|
62
|
+
<di:waypoint x="188" y="117" />
|
|
63
|
+
<di:waypoint x="250" y="117" />
|
|
64
|
+
</bpmndi:BPMNEdge>
|
|
65
|
+
<bpmndi:BPMNEdge id="Flow_echo_to_end_di" bpmnElement="Flow_echo_to_end">
|
|
66
|
+
<di:waypoint x="350" y="117" />
|
|
67
|
+
<di:waypoint x="412" y="117" />
|
|
68
|
+
</bpmndi:BPMNEdge>
|
|
69
|
+
</bpmndi:BPMNPlane>
|
|
70
|
+
</bpmndi:BPMNDiagram>
|
|
71
|
+
</bpmn:definitions>
|