@camunda/e2e-test-suite 0.0.1456 → 0.0.1458
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.11/AppsPage.d.ts +1 -0
- package/dist/pages/8.11/AppsPage.js +24 -1
- package/dist/pages/8.11/OperateProcessInstancePage.d.ts +8 -0
- package/dist/pages/8.11/OperateProcessInstancePage.js +56 -0
- package/dist/tests/8.11/agentic-ai-user-flows.spec.js +87 -0
- package/dist/utils/agentInstanceHistoryHelpers.d.ts +11 -0
- package/dist/utils/agentInstanceHistoryHelpers.js +229 -0
- package/dist/utils/agentReasoningBpmnFixtures.d.ts +2 -0
- package/dist/utils/agentReasoningBpmnFixtures.js +130 -0
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ declare class AppsPage {
|
|
|
37
37
|
private assertHubClusterAppPresent;
|
|
38
38
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
39
39
|
goBackToHub(): Promise<void>;
|
|
40
|
+
private dismissWelcomeTourIfVisible;
|
|
40
41
|
clickModeler(): Promise<void>;
|
|
41
42
|
private isModelerAuthTransit;
|
|
42
43
|
openModelerWithRetry(banner: Locator, timeout?: number, maxRetries?: number, ready?: Locator, beforeReady?: () => Promise<void>): Promise<void>;
|
|
@@ -343,6 +343,19 @@ class AppsPage {
|
|
|
343
343
|
await this.page.goto(this.hubReturnUrl);
|
|
344
344
|
}
|
|
345
345
|
}
|
|
346
|
+
// The "Welcome to Camunda Hub" onboarding tour renders as a modal dialog
|
|
347
|
+
// (heading + "Next"/"Close" buttons) that marks the rest of the shell inert.
|
|
348
|
+
// Close it when present so a header or sidebar lookup underneath it can
|
|
349
|
+
// resolve; a no-op when the tour never opened.
|
|
350
|
+
async dismissWelcomeTourIfVisible() {
|
|
351
|
+
const tour = this.page.getByRole('dialog', {
|
|
352
|
+
name: 'Welcome to Camunda Hub',
|
|
353
|
+
});
|
|
354
|
+
if (await tour.isVisible().catch(() => false)) {
|
|
355
|
+
await tour.getByRole('button', { name: 'Close' }).click({ timeout: 10000 });
|
|
356
|
+
await (0, test_1.expect)(tour).toBeHidden({ timeout: 10000 });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
346
359
|
async clickModeler() {
|
|
347
360
|
const maxRetries = 5;
|
|
348
361
|
const modelerErrorPage = this.page
|
|
@@ -384,7 +397,17 @@ class AppsPage {
|
|
|
384
397
|
.getByRole('link', { name: 'Camunda Hub home' })
|
|
385
398
|
.and(this.page.locator(':not([href$="/login"])')))
|
|
386
399
|
.first();
|
|
387
|
-
|
|
400
|
+
// The "Welcome to Camunda Hub" onboarding tour can still mount over the
|
|
401
|
+
// shell despite the localStorage flag seeded at login (see
|
|
402
|
+
// suppressOnboardingTour in UtilitiesPage, whose key prefix the current
|
|
403
|
+
// hub build no longer reads) and marks the header inert, so the home link
|
|
404
|
+
// leaves the accessibility tree until the dialog is dismissed. Close it
|
|
405
|
+
// each interval so a tour that mounts a beat after routing is still
|
|
406
|
+
// cleared, the same way openModelerWithRetry's beforeReady loop does.
|
|
407
|
+
await (0, test_1.expect)(async () => {
|
|
408
|
+
await this.dismissWelcomeTourIfVisible();
|
|
409
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 15000 });
|
|
410
|
+
}).toPass({ timeout: 30000, intervals: [1000, 2000, 5000] });
|
|
388
411
|
await homeLink.click({ timeout: 30000 });
|
|
389
412
|
return;
|
|
390
413
|
}
|
|
@@ -17,6 +17,10 @@ declare class OperateProcessInstancePage {
|
|
|
17
17
|
readonly messageVariable: Locator;
|
|
18
18
|
readonly whatsNewPopUp: Locator;
|
|
19
19
|
readonly gotItButton: Locator;
|
|
20
|
+
readonly detailsTab: Locator;
|
|
21
|
+
readonly agentInstancePanel: Locator;
|
|
22
|
+
readonly conversationHistorySection: Locator;
|
|
23
|
+
readonly conversationHistoryTrigger: Locator;
|
|
20
24
|
constructor(page: Page);
|
|
21
25
|
connectorResultVariableValue(variableName: string): Promise<Locator>;
|
|
22
26
|
rememberInstanceUrl(timeout?: number, watchedUrl?: InstanceUrlWatcher): Promise<void>;
|
|
@@ -36,6 +40,10 @@ declare class OperateProcessInstancePage {
|
|
|
36
40
|
assertProcessVariableContainsText(variableName: string, text: string): Promise<void>;
|
|
37
41
|
assertProcessVariableHasDocumentCount(variableName: string, count: number, softFail?: boolean): Promise<void>;
|
|
38
42
|
clickDiagramTask(taskName: string): Promise<void>;
|
|
43
|
+
openAgentConversation(elementId: string): Promise<void>;
|
|
44
|
+
assertReadableReasoning(reasoningHistoryItemKey: string, answerHistoryItemKey: string): Promise<void>;
|
|
45
|
+
assertEncryptedReasoningIsHidden(reasoningHistoryItemKey: string, answerHistoryItemKey: string): Promise<void>;
|
|
46
|
+
private assertFinalAnswerVisible;
|
|
39
47
|
assertActiveTokenIsPresent(): Promise<void>;
|
|
40
48
|
assertDetailsPopoverIsVisible(): Promise<void>;
|
|
41
49
|
assertBusinessIdVisible(businessId: string): Promise<void>;
|
|
@@ -158,6 +158,10 @@ class OperateProcessInstancePage {
|
|
|
158
158
|
messageVariable;
|
|
159
159
|
whatsNewPopUp;
|
|
160
160
|
gotItButton;
|
|
161
|
+
detailsTab;
|
|
162
|
+
agentInstancePanel;
|
|
163
|
+
conversationHistorySection;
|
|
164
|
+
conversationHistoryTrigger;
|
|
161
165
|
constructor(page) {
|
|
162
166
|
this.page = page;
|
|
163
167
|
this.diagram = page.getByTestId('diagram');
|
|
@@ -185,6 +189,16 @@ class OperateProcessInstancePage {
|
|
|
185
189
|
name: "Here's what moved in Operate",
|
|
186
190
|
});
|
|
187
191
|
this.gotItButton = page.getByRole('button', { name: 'Got it' });
|
|
192
|
+
this.detailsTab = page
|
|
193
|
+
.getByLabel('Process Instance Bottom Panel Tabs')
|
|
194
|
+
.getByRole('link', { name: 'Details' });
|
|
195
|
+
this.agentInstancePanel = page.getByRole('article', {
|
|
196
|
+
name: 'Agent Instance',
|
|
197
|
+
});
|
|
198
|
+
this.conversationHistorySection = this.agentInstancePanel.getByTestId('agent-conversation-history-section');
|
|
199
|
+
this.conversationHistoryTrigger = this.conversationHistorySection.getByRole('button', {
|
|
200
|
+
name: 'Conversation history',
|
|
201
|
+
});
|
|
188
202
|
}
|
|
189
203
|
async connectorResultVariableValue(variableName) {
|
|
190
204
|
return await this.page.getByTestId(variableName).locator('td').last();
|
|
@@ -874,6 +888,48 @@ class OperateProcessInstancePage {
|
|
|
874
888
|
await (0, test_1.expect)(diagramTask).toBeVisible();
|
|
875
889
|
await diagramTask.click();
|
|
876
890
|
}
|
|
891
|
+
async openAgentConversation(elementId) {
|
|
892
|
+
const agentTask = this.diagram.locator(`.djs-element[data-element-id="${elementId}"]`);
|
|
893
|
+
// BpmnJS ignores element clicks until element statistics load and the
|
|
894
|
+
// element receives the op-selectable marker.
|
|
895
|
+
await (0, test_1.expect)(agentTask).toHaveClass(/op-selectable/, { timeout: 30000 });
|
|
896
|
+
await agentTask.click();
|
|
897
|
+
await (0, test_1.expect)(this.detailsTab).toBeVisible();
|
|
898
|
+
await this.detailsTab.click();
|
|
899
|
+
await (0, test_1.expect)(this.agentInstancePanel).toBeVisible({ timeout: 30000 });
|
|
900
|
+
await (0, test_1.expect)(this.conversationHistoryTrigger).toHaveAttribute('aria-expanded', 'false');
|
|
901
|
+
await this.conversationHistoryTrigger.click();
|
|
902
|
+
await (0, test_1.expect)(this.conversationHistoryTrigger).toHaveAttribute('aria-expanded', 'true');
|
|
903
|
+
}
|
|
904
|
+
async assertReadableReasoning(reasoningHistoryItemKey, answerHistoryItemKey) {
|
|
905
|
+
const reasoningMessage = this.conversationHistorySection.getByTestId(`conversation-message-${reasoningHistoryItemKey}`);
|
|
906
|
+
const thinkingRegion = reasoningMessage.getByRole('region', {
|
|
907
|
+
name: 'Thinking',
|
|
908
|
+
});
|
|
909
|
+
const answerMessage = this.conversationHistorySection.getByTestId(`conversation-message-${answerHistoryItemKey}`);
|
|
910
|
+
await (0, test_1.expect)(reasoningMessage).toHaveAttribute('aria-label', 'Assistant message', { timeout: 60000 });
|
|
911
|
+
await (0, test_1.expect)(thinkingRegion).toBeVisible();
|
|
912
|
+
await (0, test_1.expect)(thinkingRegion.locator('p')).toHaveText(/\S/);
|
|
913
|
+
await (0, test_1.expect)(thinkingRegion.getByRole('button')).toHaveCount(0);
|
|
914
|
+
await this.assertFinalAnswerVisible(answerMessage);
|
|
915
|
+
await (0, test_1.expect)(this.conversationHistorySection).not.toContainText(/camunda\.agenticai\.content\.type|encrypted_content|signature|"payload"/);
|
|
916
|
+
}
|
|
917
|
+
async assertEncryptedReasoningIsHidden(reasoningHistoryItemKey, answerHistoryItemKey) {
|
|
918
|
+
const reasoningMessage = this.conversationHistorySection.getByTestId(`conversation-message-${reasoningHistoryItemKey}`);
|
|
919
|
+
const answerMessage = this.conversationHistorySection.getByTestId(`conversation-message-${answerHistoryItemKey}`);
|
|
920
|
+
await (0, test_1.expect)(reasoningMessage).toHaveAttribute('aria-label', 'Assistant message', { timeout: 60000 });
|
|
921
|
+
await (0, test_1.expect)(reasoningMessage).toBeVisible({ timeout: 60000 });
|
|
922
|
+
await (0, test_1.expect)(reasoningMessage.getByRole('region', { name: 'Thinking' })).toHaveCount(0);
|
|
923
|
+
await (0, test_1.expect)(this.conversationHistorySection).not.toContainText(/encrypted_content|camunda\.agenticai\.content\.type|"payload"|signature/);
|
|
924
|
+
await this.assertFinalAnswerVisible(answerMessage);
|
|
925
|
+
}
|
|
926
|
+
async assertFinalAnswerVisible(answerMessage) {
|
|
927
|
+
await (0, test_1.expect)(answerMessage).toHaveAttribute('aria-label', 'Assistant message', { timeout: 60000 });
|
|
928
|
+
// The Thinking note is a <section> named via aria-labelledby, so match
|
|
929
|
+
// only answer text that is not nested inside it.
|
|
930
|
+
const answerOutsideThinking = answerMessage.locator('xpath=.//*[text()[contains(., "Answer:")]][not(ancestor::section[@aria-labelledby = //*[normalize-space(text())="Thinking"]/@id])]');
|
|
931
|
+
await (0, test_1.expect)(answerOutsideThinking.first()).toBeVisible({ timeout: 60000 });
|
|
932
|
+
}
|
|
877
933
|
async assertActiveTokenIsPresent() {
|
|
878
934
|
await (0, test_1.expect)(this.activeIcon).toBeVisible({ timeout: 60000 });
|
|
879
935
|
}
|
|
@@ -4,6 +4,7 @@ const _8_11_1 = require("../../fixtures/8.11");
|
|
|
4
4
|
const UtilitiesPage_1 = require("../../pages/8.11/UtilitiesPage");
|
|
5
5
|
const _setup_1 = require("../../test-setup.js");
|
|
6
6
|
const apiHelpers_1 = require("../../utils/apiHelpers");
|
|
7
|
+
const agentInstanceHistoryHelpers_1 = require("../../utils/agentInstanceHistoryHelpers");
|
|
7
8
|
const sleep_1 = require("../../utils/sleep");
|
|
8
9
|
const users_1 = require("../../utils/users");
|
|
9
10
|
const testUser = (0, users_1.getTestUser)('fourteenthUser');
|
|
@@ -62,6 +63,21 @@ const aiAgentSubProcessTestCases = [
|
|
|
62
63
|
bpmnPath: './resources/agentic_ai/ai-agent-subprocess-with-tools_aws_key.bpmn',
|
|
63
64
|
},
|
|
64
65
|
];
|
|
66
|
+
const agentReasoningTestCases = [
|
|
67
|
+
{
|
|
68
|
+
name: 'Readable reasoning appears in Operate',
|
|
69
|
+
bpmnPath: './resources/agentic_ai/ai-agent-chat-with-tools_anthropic_bedrock_apikey.bpmn',
|
|
70
|
+
provider: 'anthropic-adaptive',
|
|
71
|
+
reasoningMode: 'readable',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'Encrypted reasoning stays hidden in Operate',
|
|
75
|
+
bpmnPath: './resources/agentic_ai/ai-agent-chat-with-tools_open_ai.bpmn',
|
|
76
|
+
provider: 'openai-encrypted',
|
|
77
|
+
reasoningMode: 'encrypted-only',
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
const agentReasoningResources = new Map();
|
|
65
81
|
const regexTestCases = [
|
|
66
82
|
{
|
|
67
83
|
regex_ai_agent_time: /The current date and time in ([A-Za-zÀ-ÿ .,'-]+(?:, [A-Za-zÀ-ÿ .,'-]+)*), is approximately ((?:0?[1-9]|1[0-2]):[0-5][0-9] (?:AM|PM)) on ([A-Z][a-z]+ (?:[1-9]|[12][0-9]|3[01]), \d{4})\./,
|
|
@@ -206,4 +222,75 @@ _8_11_1.test.describe('Agentic Orchestation User Flows', () => {
|
|
|
206
222
|
});
|
|
207
223
|
}
|
|
208
224
|
});
|
|
225
|
+
_8_11_1.test.describe('AI Agent reasoning in Operate', () => {
|
|
226
|
+
// Playwright requires destructured fixture arguments.
|
|
227
|
+
// eslint-disable-next-line no-empty-pattern
|
|
228
|
+
_8_11_1.test.afterEach(async ({}, testInfo) => {
|
|
229
|
+
const resources = agentReasoningResources.get(testInfo.testId);
|
|
230
|
+
if (!resources) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const cleanupErrors = await (0, agentInstanceHistoryHelpers_1.cleanupAgentReasoningResources)(resources.processInstanceKey, resources.processDefinitionKey, authToken);
|
|
234
|
+
agentReasoningResources.delete(testInfo.testId);
|
|
235
|
+
if (cleanupErrors.length === 1) {
|
|
236
|
+
throw cleanupErrors[0];
|
|
237
|
+
}
|
|
238
|
+
if (cleanupErrors.length > 1) {
|
|
239
|
+
throw new AggregateError(cleanupErrors, 'Agent reasoning resource cleanup failed');
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
for (const params of agentReasoningTestCases) {
|
|
243
|
+
(0, _8_11_1.test)(params.name, async ({ appsPage, operateHomePage, operateProcessInstancePage, operateProcessesPage, taskPanelPage, taskDetailsPage, }, testInfo) => {
|
|
244
|
+
_8_11_1.test.slow();
|
|
245
|
+
const suffix = await (0, _setup_1.generateRandomStringAsync)(6);
|
|
246
|
+
const processId = `ai-agent-reasoning-${params.provider}-${suffix.toLowerCase()}`;
|
|
247
|
+
const processName = `AI Agent ${params.name} ${suffix}`;
|
|
248
|
+
const resources = {};
|
|
249
|
+
agentReasoningResources.set(testInfo.testId, resources);
|
|
250
|
+
const processDefinitionKey = await (0, agentInstanceHistoryHelpers_1.deployAgentReasoningProcess)(params.bpmnPath, authToken, processId, processName, params.provider);
|
|
251
|
+
resources.processDefinitionKey = processDefinitionKey;
|
|
252
|
+
const processInstanceKey = await (0, apiHelpers_1.createProcessInstance)(processDefinitionKey, authToken);
|
|
253
|
+
resources.processInstanceKey = processInstanceKey;
|
|
254
|
+
await _8_11_1.test.step('Wait for the agent feedback task and committed history', async () => {
|
|
255
|
+
await appsPage.clickCamundaApps();
|
|
256
|
+
await appsPage.clickTasklist(clusterName);
|
|
257
|
+
await taskPanelPage.openTask(processName);
|
|
258
|
+
await taskDetailsPage.clickAssignToMeButton();
|
|
259
|
+
await taskDetailsPage.assertAIResponse(/Answer:/);
|
|
260
|
+
});
|
|
261
|
+
const historyKeys = await (0, agentInstanceHistoryHelpers_1.waitForAgentReasoningHistory)(processInstanceKey, authToken, params.reasoningMode);
|
|
262
|
+
await _8_11_1.test.step('Verify the conversation in Operate', async () => {
|
|
263
|
+
await appsPage.clickCamundaApps();
|
|
264
|
+
await appsPage.clickOperate(clusterName);
|
|
265
|
+
await operateHomePage.clickProcessesTab();
|
|
266
|
+
await operateProcessesPage.applyMoreFilters('Process Instance Key(s)', processInstanceKey);
|
|
267
|
+
await operateProcessesPage.clickProcessInstanceLink(processName);
|
|
268
|
+
await operateProcessInstancePage.closePopOverIfVisible();
|
|
269
|
+
await operateProcessInstancePage.assertActiveTokenIsPresent();
|
|
270
|
+
await operateProcessInstancePage.openAgentConversation('AI_Agent');
|
|
271
|
+
if (params.reasoningMode === 'readable') {
|
|
272
|
+
await operateProcessInstancePage.assertReadableReasoning(historyKeys.reasoningHistoryItemKey, historyKeys.answerHistoryItemKey);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
await operateProcessInstancePage.assertEncryptedReasoningIsHidden(historyKeys.reasoningHistoryItemKey, historyKeys.answerHistoryItemKey);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
await _8_11_1.test.step('Complete the feedback task and finish the process', async () => {
|
|
279
|
+
await appsPage.clickCamundaApps();
|
|
280
|
+
await appsPage.clickTasklist(clusterName);
|
|
281
|
+
await taskPanelPage.openTask(processName);
|
|
282
|
+
await taskDetailsPage.checkChecklistBox('Are you satisfied with the result?');
|
|
283
|
+
await taskDetailsPage.clickCompleteTaskButton();
|
|
284
|
+
await appsPage.clickCamundaApps();
|
|
285
|
+
await appsPage.clickOperate(clusterName);
|
|
286
|
+
await operateHomePage.clickProcessesTab();
|
|
287
|
+
await operateProcessesPage.clickProcessCompletedCheckbox();
|
|
288
|
+
await operateProcessesPage.applyMoreFilters('Process Instance Key(s)', processInstanceKey);
|
|
289
|
+
await operateProcessesPage.clickProcessInstanceLink(processName);
|
|
290
|
+
await operateProcessInstancePage.assertProcessCompleteStatusWithRetry();
|
|
291
|
+
resources.processInstanceKey = undefined;
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
});
|
|
209
296
|
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type AgentReasoningProvider } from './agentReasoningBpmnFixtures';
|
|
2
|
+
export type AgentReasoningHistoryKeys = {
|
|
3
|
+
agentInstanceKey: string;
|
|
4
|
+
answerHistoryItemKey: string;
|
|
5
|
+
reasoningHistoryItemKey: string;
|
|
6
|
+
};
|
|
7
|
+
export type { AgentReasoningProvider } from './agentReasoningBpmnFixtures';
|
|
8
|
+
export declare function deployAgentReasoningProcess(bpmnPath: string, authToken: string, processId: string, processName: string, provider: AgentReasoningProvider): Promise<string>;
|
|
9
|
+
export declare function cancelAgentReasoningProcessInstance(processInstanceKey: string, authToken: string): Promise<void>;
|
|
10
|
+
export declare function cleanupAgentReasoningResources(processInstanceKey: string | undefined, processDefinitionKey: string | undefined, authToken: string): Promise<unknown[]>;
|
|
11
|
+
export declare function waitForAgentReasoningHistory(processInstanceKey: string, authToken: string, reasoningMode: 'readable' | 'encrypted-only'): Promise<AgentReasoningHistoryKeys>;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.waitForAgentReasoningHistory = exports.cleanupAgentReasoningResources = exports.cancelAgentReasoningProcessInstance = exports.deployAgentReasoningProcess = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const agentReasoningBpmnFixtures_1 = require("./agentReasoningBpmnFixtures");
|
|
6
|
+
const apiHelpers_1 = require("./apiHelpers");
|
|
7
|
+
const REASONING_TYPE = 'camunda.agenticai.content.type';
|
|
8
|
+
const FINAL_ANSWER_MARKER = 'Answer:';
|
|
9
|
+
async function deployAgentReasoningProcess(bpmnPath, authToken, processId, processName, provider) {
|
|
10
|
+
const bpmn = (0, agentReasoningBpmnFixtures_1.buildAgentReasoningBpmn)(bpmnPath, processId, processName, provider);
|
|
11
|
+
const request = await (0, apiHelpers_1.getApiRequestContext)();
|
|
12
|
+
const multipart = {
|
|
13
|
+
resources: {
|
|
14
|
+
name: `${processId}.bpmn`,
|
|
15
|
+
mimeType: 'application/vnd.bpmn+xml',
|
|
16
|
+
buffer: Buffer.from(bpmn),
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
if (process.env.IS_MT === 'true') {
|
|
20
|
+
multipart.tenantId = '<default>';
|
|
21
|
+
}
|
|
22
|
+
const response = await request.post((0, apiHelpers_1.buildZeebeApiUrl)('/v2/deployments'), {
|
|
23
|
+
headers: { Authorization: authToken },
|
|
24
|
+
multipart,
|
|
25
|
+
});
|
|
26
|
+
(0, test_1.expect)(response.status(), `Reasoning process deployment failed for ${processName}`).toBe(200);
|
|
27
|
+
const body = (await response.json());
|
|
28
|
+
const processDefinitionKey = body.deployments?.[0]?.processDefinition?.processDefinitionKey;
|
|
29
|
+
if (processDefinitionKey === undefined || processDefinitionKey === null) {
|
|
30
|
+
throw new Error(`Reasoning process deployment returned no process definition key for ${processName}`);
|
|
31
|
+
}
|
|
32
|
+
return String(processDefinitionKey);
|
|
33
|
+
}
|
|
34
|
+
exports.deployAgentReasoningProcess = deployAgentReasoningProcess;
|
|
35
|
+
async function cancelAgentReasoningProcessInstance(processInstanceKey, authToken) {
|
|
36
|
+
const request = await (0, apiHelpers_1.getApiRequestContext)();
|
|
37
|
+
const response = await request.post((0, apiHelpers_1.buildZeebeApiUrl)(`/v2/process-instances/${encodeURIComponent(processInstanceKey)}/cancellation`), {
|
|
38
|
+
headers: { Authorization: authToken },
|
|
39
|
+
data: {},
|
|
40
|
+
});
|
|
41
|
+
(0, test_1.expect)([204, 409], `Cleanup cancellation failed for process instance ${processInstanceKey}`).toContain(response.status());
|
|
42
|
+
}
|
|
43
|
+
exports.cancelAgentReasoningProcessInstance = cancelAgentReasoningProcessInstance;
|
|
44
|
+
async function deleteAgentReasoningProcessDefinition(processDefinitionKey, authToken) {
|
|
45
|
+
const request = await (0, apiHelpers_1.getApiRequestContext)();
|
|
46
|
+
const response = await request.post((0, apiHelpers_1.buildZeebeApiUrl)(`/v2/resources/${encodeURIComponent(processDefinitionKey)}/deletion`), {
|
|
47
|
+
headers: { Authorization: authToken },
|
|
48
|
+
data: {},
|
|
49
|
+
});
|
|
50
|
+
(0, test_1.expect)(response.status(), `Cleanup deletion failed for process definition ${processDefinitionKey}`).toBe(200);
|
|
51
|
+
}
|
|
52
|
+
async function cleanupAgentReasoningResources(processInstanceKey, processDefinitionKey, authToken) {
|
|
53
|
+
const cleanupErrors = [];
|
|
54
|
+
if (processInstanceKey !== undefined) {
|
|
55
|
+
try {
|
|
56
|
+
await cancelAgentReasoningProcessInstance(processInstanceKey, authToken);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
cleanupErrors.push(error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (processDefinitionKey !== undefined) {
|
|
63
|
+
try {
|
|
64
|
+
await deleteAgentReasoningProcessDefinition(processDefinitionKey, authToken);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
cleanupErrors.push(error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return cleanupErrors;
|
|
71
|
+
}
|
|
72
|
+
exports.cleanupAgentReasoningResources = cleanupAgentReasoningResources;
|
|
73
|
+
function isPlainObject(value) {
|
|
74
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const prototype = Object.getPrototypeOf(value);
|
|
78
|
+
return prototype === Object.prototype || prototype === null;
|
|
79
|
+
}
|
|
80
|
+
function summarizeAgentInstances(instances) {
|
|
81
|
+
if (instances.length === 0) {
|
|
82
|
+
return '[]';
|
|
83
|
+
}
|
|
84
|
+
return JSON.stringify(instances.map((instance) => ({
|
|
85
|
+
agentInstanceKey: instance.agentInstanceKey,
|
|
86
|
+
elementId: typeof instance.elementId === 'string' ? instance.elementId : undefined,
|
|
87
|
+
processInstanceKey: instance.processInstanceKey,
|
|
88
|
+
})));
|
|
89
|
+
}
|
|
90
|
+
function getReasoningObjects(item) {
|
|
91
|
+
return (item.content ?? [])
|
|
92
|
+
.filter((content) => content.contentType === 'OBJECT')
|
|
93
|
+
.map((content) => content.object)
|
|
94
|
+
.filter((object) => isPlainObject(object) && object[REASONING_TYPE] === 'reasoning');
|
|
95
|
+
}
|
|
96
|
+
function hasReadableReasoning(item) {
|
|
97
|
+
return getReasoningObjects(item).some((object) => typeof object.text === 'string' && object.text.trim().length > 0);
|
|
98
|
+
}
|
|
99
|
+
function hasEncryptedReasoningWithoutReadableText(item) {
|
|
100
|
+
const reasoningObjects = getReasoningObjects(item);
|
|
101
|
+
return (reasoningObjects.some((object) => {
|
|
102
|
+
const payload = object.payload;
|
|
103
|
+
const hasEncryptedPayload = isPlainObject(payload) &&
|
|
104
|
+
typeof payload.encrypted_content === 'string' &&
|
|
105
|
+
payload.encrypted_content.length > 0;
|
|
106
|
+
const hasReadableText = typeof object.text === 'string' && object.text.trim().length > 0;
|
|
107
|
+
return hasEncryptedPayload && !hasReadableText;
|
|
108
|
+
}) &&
|
|
109
|
+
!reasoningObjects.some((object) => typeof object.text === 'string' && object.text.trim().length > 0));
|
|
110
|
+
}
|
|
111
|
+
function hasFinalAnswer(item) {
|
|
112
|
+
return (item.content ?? []).some((content) => content.contentType === 'TEXT' &&
|
|
113
|
+
typeof content.text === 'string' &&
|
|
114
|
+
content.text.includes(FINAL_ANSWER_MARKER));
|
|
115
|
+
}
|
|
116
|
+
function isCommittedAssistantItem(item) {
|
|
117
|
+
return item.role === 'ASSISTANT' && item.commitStatus === 'COMMITTED';
|
|
118
|
+
}
|
|
119
|
+
async function searchAgentInstances(processInstanceKey, authToken) {
|
|
120
|
+
const request = await (0, apiHelpers_1.getApiRequestContext)();
|
|
121
|
+
const response = await (0, apiHelpers_1.retryOnTransientApiError)(async () => {
|
|
122
|
+
const result = await request.post((0, apiHelpers_1.buildZeebeApiUrl)('/v2/agent-instances/search'), {
|
|
123
|
+
headers: { Authorization: authToken },
|
|
124
|
+
data: { filter: { processInstanceKey } },
|
|
125
|
+
});
|
|
126
|
+
(0, test_1.expect)(result.status(), `Agent instance search failed for process instance ${processInstanceKey}`).toBe(200);
|
|
127
|
+
return result;
|
|
128
|
+
}, `Agent instance search for process instance ${processInstanceKey}`);
|
|
129
|
+
const body = (await response.json());
|
|
130
|
+
if (!Array.isArray(body.items)) {
|
|
131
|
+
throw new Error(`Agent instance search returned no item list for process instance ${processInstanceKey}`);
|
|
132
|
+
}
|
|
133
|
+
return body.items;
|
|
134
|
+
}
|
|
135
|
+
async function searchCommittedHistory(agentInstanceKey, authToken) {
|
|
136
|
+
const request = await (0, apiHelpers_1.getApiRequestContext)();
|
|
137
|
+
const response = await (0, apiHelpers_1.retryOnTransientApiError)(async () => {
|
|
138
|
+
const result = await request.post((0, apiHelpers_1.buildZeebeApiUrl)(`/v2/agent-instances/${encodeURIComponent(agentInstanceKey)}/history/search`), {
|
|
139
|
+
headers: { Authorization: authToken },
|
|
140
|
+
data: {
|
|
141
|
+
filter: { commitStatus: 'COMMITTED' },
|
|
142
|
+
sort: [{ field: 'producedAt', order: 'ASC' }],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
if (result.status() !== 404) {
|
|
146
|
+
(0, test_1.expect)(result.status(), `Committed agent history search failed for agent instance ${agentInstanceKey}`).toBe(200);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}, `Committed agent history search for agent instance ${agentInstanceKey}`);
|
|
150
|
+
if (response.status() === 404) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
const body = (await response.json());
|
|
154
|
+
if (!Array.isArray(body.items)) {
|
|
155
|
+
throw new Error(`Committed agent history search returned no item list for agent instance ${agentInstanceKey}`);
|
|
156
|
+
}
|
|
157
|
+
return body.items;
|
|
158
|
+
}
|
|
159
|
+
async function waitForAgentReasoningHistory(processInstanceKey, authToken, reasoningMode) {
|
|
160
|
+
let agentInstance;
|
|
161
|
+
const findMatchingAgentInstance = (instances) => {
|
|
162
|
+
agentInstance = instances.find((instance) => instance.processInstanceKey === processInstanceKey &&
|
|
163
|
+
instance.elementId === 'AI_Agent' &&
|
|
164
|
+
typeof instance.agentInstanceKey === 'string' &&
|
|
165
|
+
instance.agentInstanceKey.length > 0);
|
|
166
|
+
return agentInstance !== undefined;
|
|
167
|
+
};
|
|
168
|
+
let instances = await searchAgentInstances(processInstanceKey, authToken);
|
|
169
|
+
if (!findMatchingAgentInstance(instances)) {
|
|
170
|
+
try {
|
|
171
|
+
await test_1.expect
|
|
172
|
+
.poll(async () => {
|
|
173
|
+
instances = await searchAgentInstances(processInstanceKey, authToken);
|
|
174
|
+
return findMatchingAgentInstance(instances);
|
|
175
|
+
}, {
|
|
176
|
+
timeout: 120000,
|
|
177
|
+
intervals: [1000, 2000, 5000],
|
|
178
|
+
message: `No AI Agent instance appeared for process instance ${processInstanceKey}`,
|
|
179
|
+
})
|
|
180
|
+
.toBe(true);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
const detailsError = new Error(`No AI Agent instance appeared for process instance ${processInstanceKey}. Last search response: ${summarizeAgentInstances(instances)}`);
|
|
184
|
+
Object.assign(detailsError, { cause: error });
|
|
185
|
+
throw detailsError;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (agentInstance === undefined ||
|
|
189
|
+
typeof agentInstance.agentInstanceKey !== 'string') {
|
|
190
|
+
throw new Error(`AI Agent instance was not resolved for process instance ${processInstanceKey}`);
|
|
191
|
+
}
|
|
192
|
+
const agentInstanceKey = agentInstance.agentInstanceKey;
|
|
193
|
+
let reasoningItem;
|
|
194
|
+
let answerItem;
|
|
195
|
+
const findCommittedEvidence = (history) => {
|
|
196
|
+
const assistantItems = history.filter(isCommittedAssistantItem);
|
|
197
|
+
reasoningItem = assistantItems.find((item) => reasoningMode === 'readable'
|
|
198
|
+
? hasReadableReasoning(item)
|
|
199
|
+
: hasEncryptedReasoningWithoutReadableText(item));
|
|
200
|
+
answerItem = assistantItems.find(hasFinalAnswer);
|
|
201
|
+
return (typeof reasoningItem?.historyItemKey === 'string' &&
|
|
202
|
+
typeof answerItem?.historyItemKey === 'string');
|
|
203
|
+
};
|
|
204
|
+
let history = await searchCommittedHistory(agentInstanceKey, authToken);
|
|
205
|
+
if (!findCommittedEvidence(history)) {
|
|
206
|
+
await test_1.expect
|
|
207
|
+
.poll(async () => {
|
|
208
|
+
history = await searchCommittedHistory(agentInstanceKey, authToken);
|
|
209
|
+
return findCommittedEvidence(history);
|
|
210
|
+
}, {
|
|
211
|
+
timeout: 180000,
|
|
212
|
+
intervals: [1000, 2000, 5000],
|
|
213
|
+
message: reasoningMode === 'readable'
|
|
214
|
+
? `Committed readable reasoning and final-answer history did not appear for agent instance ${agentInstanceKey}`
|
|
215
|
+
: `Committed encrypted-only reasoning and final-answer history did not appear for agent instance ${agentInstanceKey}`,
|
|
216
|
+
})
|
|
217
|
+
.toBe(true);
|
|
218
|
+
}
|
|
219
|
+
if (typeof reasoningItem?.historyItemKey !== 'string' ||
|
|
220
|
+
typeof answerItem?.historyItemKey !== 'string') {
|
|
221
|
+
throw new Error(`Committed reasoning evidence was not resolved for agent instance ${agentInstanceKey}`);
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
agentInstanceKey,
|
|
225
|
+
reasoningHistoryItemKey: reasoningItem.historyItemKey,
|
|
226
|
+
answerHistoryItemKey: answerItem.historyItemKey,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
exports.waitForAgentReasoningHistory = waitForAgentReasoningHistory;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.buildAgentReasoningBpmn = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const REASONING_SYSTEM_PROMPT = 'Solve the quantitative problem carefully before answering. Use the supplied ' +
|
|
9
|
+
'facts and verify the result with an equation. Return a concise explanation ' +
|
|
10
|
+
'and start the final response with "Answer:".';
|
|
11
|
+
const REASONING_USER_PROMPT = '="A box contains red and blue marbles. There are 20 more red marbles than ' +
|
|
12
|
+
'blue marbles. After removing 4 red marbles and adding 2 blue marbles, there ' +
|
|
13
|
+
'are twice as many red marbles as blue marbles. How many of each color were ' +
|
|
14
|
+
'in the box originally? Include a short equation that verifies your answer."';
|
|
15
|
+
function escapeXml(value) {
|
|
16
|
+
return value
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, ''');
|
|
22
|
+
}
|
|
23
|
+
function replaceXmlAttribute(element, name, value) {
|
|
24
|
+
const attribute = new RegExp(`\\b${name}="[^"]*"`);
|
|
25
|
+
if (!attribute.test(element)) {
|
|
26
|
+
throw new Error(`The BPMN process tag has no ${name} attribute`);
|
|
27
|
+
}
|
|
28
|
+
return element.replace(attribute, `${name}="${escapeXml(value)}"`);
|
|
29
|
+
}
|
|
30
|
+
function replaceProcessIdentity(bpmn, processId, processName) {
|
|
31
|
+
const processTag = bpmn.match(/<bpmn:process\b[^>]*>/)?.[0];
|
|
32
|
+
if (!processTag) {
|
|
33
|
+
throw new Error('The BPMN fixture has no bpmn:process element');
|
|
34
|
+
}
|
|
35
|
+
const originalProcessId = processTag.match(/\bid="([^"]*)"/)?.[1];
|
|
36
|
+
if (!originalProcessId) {
|
|
37
|
+
throw new Error('The BPMN process element has no id');
|
|
38
|
+
}
|
|
39
|
+
const updatedTag = replaceXmlAttribute(replaceXmlAttribute(processTag, 'id', processId), 'name', processName);
|
|
40
|
+
return bpmn
|
|
41
|
+
.replace(processTag, updatedTag)
|
|
42
|
+
.split(`bpmnElement="${originalProcessId}"`)
|
|
43
|
+
.join(`bpmnElement="${processId}"`);
|
|
44
|
+
}
|
|
45
|
+
function getAgentTask(bpmn) {
|
|
46
|
+
const agentTask = bpmn.match(/<bpmn:serviceTask\b[^>]*\bid="AI_Agent"[\s\S]*?<\/bpmn:serviceTask>/)?.[0];
|
|
47
|
+
if (!agentTask) {
|
|
48
|
+
throw new Error('The BPMN fixture has no AI_Agent service task');
|
|
49
|
+
}
|
|
50
|
+
return agentTask;
|
|
51
|
+
}
|
|
52
|
+
function replaceAgentTask(bpmn, agentTask, updatedTask) {
|
|
53
|
+
return bpmn.replace(agentTask, updatedTask);
|
|
54
|
+
}
|
|
55
|
+
function replaceInputSource(bpmn, target, source) {
|
|
56
|
+
const agentTask = getAgentTask(bpmn);
|
|
57
|
+
const input = (agentTask.match(/<zeebe:input\b[\s\S]*?\/>/g) ?? []).find((entry) => entry.includes(`target="${target}"`));
|
|
58
|
+
if (!input) {
|
|
59
|
+
throw new Error(`The BPMN fixture has no input mapping for ${target}`);
|
|
60
|
+
}
|
|
61
|
+
const updatedInput = input.replace(/\bsource="[^"]*"/, `source="${escapeXml(source)}"`);
|
|
62
|
+
return replaceAgentTask(bpmn, agentTask, agentTask.replace(input, updatedInput));
|
|
63
|
+
}
|
|
64
|
+
function appendInputMappings(bpmn, mappings) {
|
|
65
|
+
const closingTag = '</zeebe:ioMapping>';
|
|
66
|
+
const agentTask = getAgentTask(bpmn);
|
|
67
|
+
const index = agentTask.indexOf(closingTag);
|
|
68
|
+
if (index < 0) {
|
|
69
|
+
throw new Error('The AI_Agent task has no zeebe:ioMapping element');
|
|
70
|
+
}
|
|
71
|
+
const newMappings = mappings
|
|
72
|
+
.map(({ source, target }) => ` <zeebe:input source="${escapeXml(source)}" target="${escapeXml(target)}" />`)
|
|
73
|
+
.join('\n');
|
|
74
|
+
const updatedTask = `${agentTask.slice(0, index)}${newMappings}\n${agentTask.slice(index)}`;
|
|
75
|
+
return replaceAgentTask(bpmn, agentTask, updatedTask);
|
|
76
|
+
}
|
|
77
|
+
function markAgentTask(bpmn) {
|
|
78
|
+
const agentTask = getAgentTask(bpmn);
|
|
79
|
+
if (!agentTask.includes('</bpmn:extensionElements>')) {
|
|
80
|
+
throw new Error('The BPMN fixture has no AI_Agent task extensions');
|
|
81
|
+
}
|
|
82
|
+
if (/<zeebe:agentDefinition\b/.test(agentTask)) {
|
|
83
|
+
throw new Error('The BPMN fixture already has an AI_Agent definition');
|
|
84
|
+
}
|
|
85
|
+
return replaceAgentTask(bpmn, agentTask, agentTask.replace('</bpmn:extensionElements>', ' <zeebe:agentDefinition agentType="aiAgentTask" />\n' +
|
|
86
|
+
' </bpmn:extensionElements>'));
|
|
87
|
+
}
|
|
88
|
+
function buildAgentReasoningBpmn(bpmnPath, processId, processName, provider) {
|
|
89
|
+
let bpmn = replaceProcessIdentity(fs_1.default.readFileSync(bpmnPath, 'utf8'), processId, processName);
|
|
90
|
+
bpmn = markAgentTask(bpmn);
|
|
91
|
+
bpmn = replaceInputSource(bpmn, 'data.systemPrompt.prompt', REASONING_SYSTEM_PROMPT);
|
|
92
|
+
bpmn = replaceInputSource(bpmn, 'data.userPrompt.prompt', REASONING_USER_PROMPT);
|
|
93
|
+
if (provider === 'anthropic-adaptive') {
|
|
94
|
+
bpmn = replaceInputSource(bpmn, 'data.limits.maxModelCalls', '=3');
|
|
95
|
+
bpmn = appendInputMappings(bpmn, [
|
|
96
|
+
{
|
|
97
|
+
source: '=8192',
|
|
98
|
+
target: 'provider.anthropic.model.parameters.maxTokens',
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
source: 'adaptive',
|
|
102
|
+
target: 'provider.anthropic.model.parameters.thinking.mode',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
source: 'summarized',
|
|
106
|
+
target: 'provider.anthropic.model.parameters.thinking.display',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
source: 'high',
|
|
110
|
+
target: 'provider.anthropic.model.parameters.effort',
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
bpmn = replaceInputSource(bpmn, 'provider.openai.model.model', 'gpt-5-mini-2025-08-07');
|
|
116
|
+
bpmn = replaceInputSource(bpmn, 'data.limits.maxModelCalls', '=3');
|
|
117
|
+
bpmn = appendInputMappings(bpmn, [
|
|
118
|
+
{
|
|
119
|
+
source: 'medium',
|
|
120
|
+
target: 'provider.openai.api.responses.effort',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
source: '=4096',
|
|
124
|
+
target: 'provider.openai.api.responses.maxOutputTokens',
|
|
125
|
+
},
|
|
126
|
+
]);
|
|
127
|
+
}
|
|
128
|
+
return bpmn;
|
|
129
|
+
}
|
|
130
|
+
exports.buildAgentReasoningBpmn = buildAgentReasoningBpmn;
|